package clicmd import ( "context" "flag" "fmt" "strconv" "time" "github.com/iceBear67/simplepages/internal/cliutil" "github.com/iceBear67/simplepages/internal/version" ) // Root builds the command tree. func Root(g *Globals) *cliutil.Command { var showVersion bool return &cliutil.Command{ Name: "pages", Short: "Deploy static sites atomically", Long: "Settings are taken from flags first, then PAGES_* environment\n" + "variables, then the config file, then built-in defaults.\n\n" + " PAGES_SERVER management API base URL\n" + " PAGES_TOKEN API token\n" + " PAGES_TOKEN_FILE file holding the API token\n" + " PAGES_PROJECT default project\n" + " PAGES_OUTPUT table or json\n" + " PAGES_TIMEOUT per-request timeout\n" + " PAGES_CONFIG config file path", Flags: func(fs *flag.FlagSet) { fs.BoolVar(&showVersion, "version", false, "print the version and exit") }, Exec: func(ctx context.Context, args []string) error { if showVersion { fmt.Fprintln(g.Out, version.String()) return nil } return cliutil.ErrUsage }, Sub: []*cliutil.Command{ deployCmd(g), projectCmd(g), deploymentCmd(g), keyCmd(g), whoamiCmd(g), systemCmd(g), configCmd(g), versionCmd(g), }, } } func versionCmd(g *Globals) *cliutil.Command { return &cliutil.Command{ Name: "version", Short: "Print the version", Long: "Reports the client build only; it does not contact the server.", Exec: func(ctx context.Context, args []string) error { fmt.Fprintln(g.Out, version.String()) return nil }, } } func whoamiCmd(g *Globals) *cliutil.Command { return &cliutil.Command{ Name: "whoami", Short: "Show which key is being used", Long: "Useful for confirming a CI runner picked up the credential you meant.", Exec: func(ctx context.Context, args []string) error { if err := exactArgs(args, 0, "no arguments"); err != nil { return err } c, err := g.Client() if err != nil { return err } who, err := c.WhoAmI(ctx) if err != nil { return err } p, err := g.Printer() if err != nil { return err } return p.Print(who, func() *cliutil.Table { t := cliutil.NewTable() t.Row("key_id", who.KeyID) t.Row("scope", who.Scope) t.Row("project", cliutil.Str(who.Project)) t.Row("name", cliutil.Str(who.Name)) t.Row("expires_at", cliutil.TimePtr(who.ExpiresAt)) t.Row("server", c.BaseURL()) return t }) }, } } func systemCmd(g *Globals) *cliutil.Command { return &cliutil.Command{ Name: "system", Short: "Inspect and maintain the server", Sub: []*cliutil.Command{systemInfoCmd(g), systemGCCmd(g), systemFsckCmd(g)}, } } func systemInfoCmd(g *Globals) *cliutil.Command { return &cliutil.Command{ Name: "info", Short: "Show server version and storage counters", Long: "Requires an admin key.", Exec: func(ctx context.Context, args []string) error { if err := exactArgs(args, 0, "no arguments"); err != nil { return err } c, err := g.Client() if err != nil { return err } info, err := c.SystemInfo(ctx) if err != nil { return err } p, err := g.Printer() if err != nil { return err } return p.Print(info, func() *cliutil.Table { t := cliutil.NewTable() t.Row("version", info.Version) t.Row("uptime", (time.Duration(info.UptimeS) * time.Second).String()) t.Row("schema_version", strconv.Itoa(info.SchemaVer)) t.Row("projects", strconv.FormatInt(info.Projects, 10)) t.Row("deployments", strconv.FormatInt(info.Deployments, 10)) t.Row("blobs", strconv.FormatInt(info.Blobs, 10)) t.Row("cas_bytes", cliutil.Bytes(info.CASBytes)) // How deployment trees are built on this host: hardlink shares // inodes with the store, copy does not, and the difference shows // up as disk usage. t.Row("link_mode", info.LinkMode) return t }) }, } } // -------------------------------------------------------------------- config func configCmd(g *Globals) *cliutil.Command { return &cliutil.Command{ Name: "config", Short: "Read and write the CLI config file", Long: "The config file is JSON, mode 0600, at $PAGES_CONFIG or\n" + "$XDG_CONFIG_HOME/pages/config.json. It is a convenience for a\n" + "workstation; CI should use PAGES_* environment variables instead.", Sub: []*cliutil.Command{ configShowCmd(g), configSetCmd(g), configPathCmd(g), }, } } // configView is what "config show" prints: the file's contents with the token // reduced to its public half. type configView struct { Path string `json:"path"` Server string `json:"server,omitempty"` Token string `json:"token,omitempty"` Project string `json:"project,omitempty"` Output string `json:"output,omitempty"` } func configShowCmd(g *Globals) *cliutil.Command { return &cliutil.Command{ Name: "show", Short: "Show the config file, with the token redacted", Exec: func(ctx context.Context, args []string) error { if err := exactArgs(args, 0, "no arguments"); err != nil { return err } if err := g.Resolve(); err != nil { return err } // From the file, not from the resolved settings: this command answers // "what is stored here", and printing an environment-supplied token // back at the user would be actively misleading. view := configView{ Path: g.Config, Server: g.file.Server, Token: redactToken(g.file.Token), Project: g.file.Project, Output: g.file.Output, } p, err := g.Printer() if err != nil { return err } return p.Print(view, func() *cliutil.Table { t := cliutil.NewTable() t.Row("path", cliutil.Str(view.Path)) t.Row("server", cliutil.Str(view.Server)) t.Row("token", cliutil.Str(view.Token)) t.Row("project", cliutil.Str(view.Project)) t.Row("output", cliutil.Str(view.Output)) return t }) }, } } func configSetCmd(g *Globals) *cliutil.Command { return &cliutil.Command{ Name: "set", Args: " ", Short: "Set one config value", Long: "An empty value removes the setting. The file is created mode 0600.\n" + "Reading a token from a file avoids putting it in your shell history:\n" + " pages config set token \"$(cat token.txt)\"", Exec: func(ctx context.Context, args []string) error { if err := exactArgs(args, 2, "a key and a value"); err != nil { return err } if err := g.Resolve(); err != nil { return err } key, value := args[0], args[1] f := g.file switch key { case "server": f.Server = value case "token": f.Token = value case "project": f.Project = value case "output": if value != "" && !cliutil.ValidFormat(value) { return fmt.Errorf("unknown output format %q: use table or json", value) } f.Output = value default: return cliutil.UsageErrorf("unknown config key %q", key) } if err := SaveConfig(g.Config, f); err != nil { return err } p, err := g.Printer() if err != nil { return err } p.Printf("set %s in %s\n", key, g.Config) return nil }, } } func configPathCmd(g *Globals) *cliutil.Command { return &cliutil.Command{ Name: "path", Short: "Print the config file path", Exec: func(ctx context.Context, args []string) error { if err := exactArgs(args, 0, "no arguments"); err != nil { return err } if err := g.Resolve(); err != nil { return err } fmt.Fprintln(g.Out, g.Config) return nil }, } }