rickub/clipublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/rickub/cli.git
git clone ssh://git@rickub.com/rickub/cli.git
root.go · 134 lines · 4.0 KBGo Blame HistoryRaw
Initial import of the rickub CLI as a standalone public project 1a1d430 Olivier Girardot 9h ago1// Package cmd defines the rickub CLI command tree (built on Cobra).
2package cmd
3
4import (
5 "context"
6 "encoding/json"
7 "fmt"
8 "io"
9 "os"
10 "text/tabwriter"
11 "time"
12
13 "rickub.com/rickub/cli/internal/api"
14 "rickub.com/rickub/cli/internal/config"
15
16 "github.com/spf13/cobra"
17)
18
19// Global flags, bound on the root command.
20var (
21 flagHost string
22 flagToken string
23 flagJSON bool
24)
25
26// Version is stamped by main (overridable at build time).
27var Version = "dev"
28
29// rootCmd is the base `rickub` command.
30var rootCmd = &cobra.Command{
31 Use: "rickub",
32 Short: "rickub — the smartest git in the universe, on the command line",
33 Long: `rickub is the command-line interface to a rickub git host.
34
35It talks to the rickub JSON API (/api/v1) with a personal access token.
36Authenticate once with "rickub auth login", then drive repos, merge requests,
37CI runs, orgs, and code browsing from your terminal.`,
38 SilenceUsage: true,
39 SilenceErrors: true,
40}
41
42// Execute runs the root command. main() maps the returned error to an exit code.
43func Execute() error {
44 return rootCmd.Execute()
45}
46
47func init() {
48 pf := rootCmd.PersistentFlags()
49 pf.StringVar(&flagHost, "host", "", "API host (default https://rickub.com; or RICKUB_HOST)")
50 pf.StringVar(&flagToken, "token", "", "personal access token (or RICKUB_TOKEN)")
51 pf.BoolVar(&flagJSON, "json", false, "output raw JSON instead of a table")
52}
53
54// loadConfig loads the persisted config (empty if none).
55func loadConfig() (*config.Config, error) {
56 return config.Load()
57}
58
59// newClient builds an authenticated API client from flags/env/config. It errors
60// with a friendly message when no token is resolvable.
61//
62// The stored token is bound to the host it was minted for, so an unexpected
63// --host / RICKUB_HOST does not get the production credential; when that is why
64// no token was found, the error says so instead of claiming none is configured.
65func newClient() (*api.Client, error) {
66 cfg, err := loadConfig()
67 if err != nil {
68 return nil, err
69 }
70 host := config.ResolveHost(flagHost, cfg)
71 token := config.ResolveToken(flagToken, cfg, host)
72 if token == "" {
73 if cfg.Host != "" && cfg.TokenFor(cfg.Host) != "" {
74 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)
75 }
76 return nil, fmt.Errorf("no token configured; run `rickub auth login` or pass --token / set RICKUB_TOKEN")
77 }
78 config.WarnIfInsecure(os.Stderr, host)
79 return api.New(host, token), nil
80}
81
82// ctx returns a background context (a place to add cancellation later).
83func ctx() context.Context { return context.Background() }
84
85// ---- output helpers ----
86
87// printJSON marshals v as indented JSON to the given writer.
88func printJSON(w io.Writer, v any) error {
89 enc := json.NewEncoder(w)
90 enc.SetIndent("", " ")
91 enc.SetEscapeHTML(false)
92 return enc.Encode(v)
93}
94
95// newTabw returns a tabwriter over the given writer configured for the CLI's
96// two-space-padded column style.
97func newTabw(w io.Writer) *tabwriter.Writer {
98 return tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
99}
100
101// printPageFooter prints a pagination hint when more pages are available.
102func printPageFooter(cmd *cobra.Command, p api.Page) {
103 if p.HasNext {
104 next := p.Page + 1
105 if next < 2 {
106 next = 2
107 }
108 fmt.Fprintf(cmd.ErrOrStderr(), "(more results — pass --page %d)\n", next)
109 }
110}
111
112// dash renders empty strings as a dash for table cells.
113func dash(s string) string {
114 if s == "" {
115 return "-"
116 }
117 return s
118}
119
120// humanTime renders an API RFC3339 timestamp (often with sub-second precision,
121// e.g. "2026-07-21T00:07:53.58474Z") in a friendlier form for the default,
122// human-readable output: "2006-01-02 15:04 UTC". An empty value renders as a
123// dash; an unparseable value is returned unchanged so we never hide data. This is
124// used ONLY for human output — the --json path prints the raw struct untouched.
125func humanTime(s string) string {
126 if s == "" {
127 return "-"
128 }
129 t, err := time.Parse(time.RFC3339, s)
130 if err != nil {
131 return s
132 }
133 return t.UTC().Format("2006-01-02 15:04 UTC")
134}