Files
pages/internal/clicmd/root.go
T
2026-08-15 07:13:00 +00:00

273 lines
7.4 KiB
Go

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: "<server|token|project|output> <value>",
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
},
}
}