// Package cmd defines the rickub CLI command tree (built on Cobra). package cmd import ( "context" "encoding/json" "fmt" "io" "os" "text/tabwriter" "time" "rickub.com/rickub/cli/internal/api" "rickub.com/rickub/cli/internal/config" "github.com/spf13/cobra" ) // Global flags, bound on the root command. var ( flagHost string flagToken string flagJSON bool ) // Version is stamped by main (overridable at build time). var Version = "dev" // rootCmd is the base `rickub` command. var rootCmd = &cobra.Command{ Use: "rickub", Short: "rickub — the smartest git in the universe, on the command line", Long: `rickub is the command-line interface to a rickub git host. It talks to the rickub JSON API (/api/v1) with a personal access token. Authenticate once with "rickub auth login", then drive repos, merge requests, CI runs, orgs, and code browsing from your terminal.`, SilenceUsage: true, SilenceErrors: true, } // Execute runs the root command. main() maps the returned error to an exit code. func Execute() error { return rootCmd.Execute() } func init() { pf := rootCmd.PersistentFlags() pf.StringVar(&flagHost, "host", "", "API host (default https://rickub.com; or RICKUB_HOST)") pf.StringVar(&flagToken, "token", "", "personal access token (or RICKUB_TOKEN)") pf.BoolVar(&flagJSON, "json", false, "output raw JSON instead of a table") } // loadConfig loads the persisted config (empty if none). func loadConfig() (*config.Config, error) { return config.Load() } // newClient builds an authenticated API client from flags/env/config. It errors // with a friendly message when no token is resolvable. // // The stored token is bound to the host it was minted for, so an unexpected // --host / RICKUB_HOST does not get the production credential; when that is why // no token was found, the error says so instead of claiming none is configured. func newClient() (*api.Client, error) { cfg, err := loadConfig() if err != nil { return nil, err } host := config.ResolveHost(flagHost, cfg) token := config.ResolveToken(flagToken, cfg, host) if token == "" { if cfg.Host != "" && cfg.TokenFor(cfg.Host) != "" { return nil, fmt.Errorf("no token stored for %s (the stored token belongs to %s); run `rickub auth login --host %s` or pass --token / set RICKUB_TOKEN", host, cfg.Host, host) } return nil, fmt.Errorf("no token configured; run `rickub auth login` or pass --token / set RICKUB_TOKEN") } config.WarnIfInsecure(os.Stderr, host) return api.New(host, token), nil } // ctx returns a background context (a place to add cancellation later). func ctx() context.Context { return context.Background() } // ---- output helpers ---- // printJSON marshals v as indented JSON to the given writer. func printJSON(w io.Writer, v any) error { enc := json.NewEncoder(w) enc.SetIndent("", " ") enc.SetEscapeHTML(false) return enc.Encode(v) } // newTabw returns a tabwriter over the given writer configured for the CLI's // two-space-padded column style. func newTabw(w io.Writer) *tabwriter.Writer { return tabwriter.NewWriter(w, 0, 4, 2, ' ', 0) } // printPageFooter prints a pagination hint when more pages are available. func printPageFooter(cmd *cobra.Command, p api.Page) { if p.HasNext { next := p.Page + 1 if next < 2 { next = 2 } fmt.Fprintf(cmd.ErrOrStderr(), "(more results — pass --page %d)\n", next) } } // dash renders empty strings as a dash for table cells. func dash(s string) string { if s == "" { return "-" } return s } // humanTime renders an API RFC3339 timestamp (often with sub-second precision, // e.g. "2026-07-21T00:07:53.58474Z") in a friendlier form for the default, // human-readable output: "2006-01-02 15:04 UTC". An empty value renders as a // dash; an unparseable value is returned unchanged so we never hide data. This is // used ONLY for human output — the --json path prints the raw struct untouched. func humanTime(s string) string { if s == "" { return "-" } t, err := time.Parse(time.RFC3339, s) if err != nil { return s } return t.UTC().Format("2006-01-02 15:04 UTC") }