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
Initial import of the rickub CLI as a standalone public project 1a1d430Unverified · on main · Olivier Girardot · 7h ago
root.go · 134 lines · 4.0 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// 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")
}