cli: every aliased flag registers through the helpers; every verb prints through flagUsage
Mechanical sweep, no behaviour change at the parser: 60 -app, 9 -channel (one keeping its stable default via aliasedString), 8 -environment and 2 -version registrations convert to the helpers; all 84 command flag sets wire fs.Usage = flagUsage(fs, <their synopsis const, or "" for the derived one>); the three hand fs.Usage overrides (logs, economics backhaul, pipeline) convert, logsUsage dropping its hand-kept flag list — the drift surface the shared printer exists to close. The C46 deliver-line matchers in services_cli_test.go read the new row shape, still name-exact and still rejecting fall-through pages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
21 files changed,
+185
−113
accounts.go+4 −1appconfig_cli.go+25 −16apps.go+7 −3auth.go+4 −0cli.go+36 −18deploy.go+4 −3domains_cli.go+6 −3economics.go+2 −1fleets.go+7 −1instances_cli.go+17 −10logs.go+2 −20mail_cli.go+18 −9pipeline.go+5 −6restart.go+2 −1retention.go+2 −1routes_cli.go+2 −1schedule.go+8 −4services.go+5 −1services_cli_test.go+20 −10store.go+8 −4update.go+1 −0
diff --git a/accounts.go b/accounts.go| index d20bcac4f..8aef6de91 100644 |
| --- a/accounts.go |
| +++ b/accounts.go |
| @@ -89,6 +89,7 @@ func runAccountsCreate(args []string) { |
| owner := fs.String("owner", "", "keymail address that owns the account — the box-side/headless path, "+ |
| "and what to use when this terminal is not logged in") |
| console := fs.String("console", "", "which logged-in console to create it through (only needed with several)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| *name = strings.TrimSpace(*name) |
| if *name == "" { |
| @@ -212,6 +213,7 @@ func runAccountsList(args []string) { |
| fs := flag.NewFlagSet("accounts list", flag.ExitOnError) |
| regPath := fs.String("registry", defaultRegistry(), "registry database (box mode)") |
| consoleFlag := fs.String("console", "", "list through a console instead (default: the box registry when one exists, else your logged-in console)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| // Mode split by what exists, not by resolveMode: this verb predates the |
| // console API and must keep working on a box that has a registry and no |
| @@ -310,10 +312,11 @@ func runAccountsListRegistry(regPath string) { |
| func runAccountsMigrate(args []string) { |
| fs := flag.NewFlagSet("accounts migrate", flag.ExitOnError) |
| regPath := fs.String("registry", defaultRegistry(), "registry database") |
| - app := fs.String("app", "", "app to move") |
| + app := appFlag(fs, "app to move") |
| to := fs.String("to", "", "destination account sqid") |
| from := fs.String("from", "", "source account sqid, or \"legacy\" for the pre-accounts keys (default: ops, then legacy)") |
| dryRun := fs.Bool("dry-run", false, "print the plan and touch nothing") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| if *app == "" || *to == "" { |
| log.Fatal("usage: carlos accounts migrate -app <a> -to <sqid> [-from <sqid>] [-dry-run]") |
diff --git a/appconfig_cli.go b/appconfig_cli.go| index 9ad738c13..659c9d335 100644 |
| --- a/appconfig_cli.go |
| +++ b/appconfig_cli.go |
| @@ -201,10 +201,11 @@ func parseKV(args []string) (map[string]string, error) { |
| func runEnvSet(args []string) { |
| fs := flag.NewFlagSet("env set", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| - environment := fs.String("environment", "", "named environment bundle to set in (omit for the default bundle)") |
| + app := appFlag(fs, "") |
| + environment := environmentFlag(fs, "named environment bundle to set in (omit for the default bundle)") |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos env set -app <a> [-environment <name>] [-console <origin>] KEY=value [KEY2=value2 ...]" |
| checkEnvironmentFlag(*environment) |
| @@ -244,10 +245,11 @@ func runEnvSet(args []string) { |
| func runEnvUnset(args []string) { |
| fs := flag.NewFlagSet("env unset", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| - environment := fs.String("environment", "", "named environment bundle to remove from (omit for the default bundle)") |
| + app := appFlag(fs, "") |
| + environment := environmentFlag(fs, "named environment bundle to remove from (omit for the default bundle)") |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos env unset -app <a> [-environment <name>] [-console <origin>] KEY [KEY2 ...]" |
| if fs.NArg() == 0 { |
| @@ -284,10 +286,11 @@ func runEnvUnset(args []string) { |
| func runEnvList(args []string) { |
| fs := flag.NewFlagSet("env list", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| - environment := fs.String("environment", "", "named environment bundle to list (omit for the default bundle)") |
| + app := appFlag(fs, "") |
| + environment := environmentFlag(fs, "named environment bundle to list (omit for the default bundle)") |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos env list -app <a> [-environment <name>] [-console <origin>]" |
| checkEnvironmentFlag(*environment) |
| @@ -386,11 +389,12 @@ func printEnvList(app, environment, execDelivery string, vars map[string]string) |
| // is refused there, by name, rather than accepted as a synonym. |
| func runEnvExecDelivery(args []string) { |
| fs := flag.NewFlagSet("env exec-delivery", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| - environment := fs.String("environment", "", |
| + app := appFlag(fs, "") |
| + environment := environmentFlag(fs, |
| "named environment bundle to set the flag in (omit for the app's default bundle)") |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos env exec-delivery -app <a> [-console <origin>] file|off\n" + |
| " or: carlos env exec-delivery -app <a> -environment <name> [-console <origin>] file|inherit" |
| @@ -474,9 +478,10 @@ func bundleAppName(app string) string { |
| func runEnvEnvironments(args []string) { |
| fs := flag.NewFlagSet("env environments", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| + app := appFlag(fs, "") |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos env environments -app <a> [-console <origin>]" |
| @@ -527,9 +532,10 @@ func runEnvEnvironments(args []string) { |
| // per-bundle" design (generation.go). |
| func runEnvSync(args []string) { |
| fs := flag.NewFlagSet("env sync", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| + app := appFlag(fs, "") |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos env sync -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| @@ -648,10 +654,11 @@ func secretsPubKeyAPI(sess *apiSession, account apiAcct, app string) *ecdh.Publi |
| func runSecretsSet(args []string) { |
| fs := flag.NewFlagSet("secrets set", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| - environment := fs.String("environment", "", "named environment bundle to set in (omit for the default bundle)") |
| + app := appFlag(fs, "") |
| + environment := environmentFlag(fs, "named environment bundle to set in (omit for the default bundle)") |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos secrets set -app <a> [-environment <name>] [-console <origin>] KEY=value [KEY2=value2 ...]" |
| checkEnvironmentFlag(*environment) |
| @@ -720,10 +727,11 @@ func runSecretsSet(args []string) { |
| func runSecretsUnset(args []string) { |
| fs := flag.NewFlagSet("secrets unset", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| - environment := fs.String("environment", "", "named environment bundle to remove from (omit for the default bundle)") |
| + app := appFlag(fs, "") |
| + environment := environmentFlag(fs, "named environment bundle to remove from (omit for the default bundle)") |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos secrets unset -app <a> [-environment <name>] [-console <origin>] KEY [KEY2 ...]" |
| if fs.NArg() == 0 { |
| @@ -772,10 +780,11 @@ func printSecretsList(app, environment string, keys []string) { |
| func runSecretsList(args []string) { |
| fs := flag.NewFlagSet("secrets list", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| - environment := fs.String("environment", "", "named environment bundle to list (omit for the default bundle)") |
| + app := appFlag(fs, "") |
| + environment := environmentFlag(fs, "named environment bundle to list (omit for the default bundle)") |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to work through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos secrets list -app <a> [-environment <name>] [-console <origin>]" |
| checkEnvironmentFlag(*environment) |
diff --git a/apps.go b/apps.go| index baee56d07..9f6c19bfb 100644 |
| --- a/apps.go |
| +++ b/apps.go |
| @@ -180,6 +180,7 @@ func runAppsDelete(args []string) { |
| fs := flag.NewFlagSet("apps delete", flag.ExitOnError) |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to delete through (signed-in mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| if fs.NArg() != 1 { |
| log.Fatal("usage: carlos apps delete [-account <sqid|name>] [-account-id <sqid>] [-console <origin>] <app>") |
| @@ -300,6 +301,7 @@ func runAppsRestore(args []string) { |
| fs := flag.NewFlagSet("apps restore", flag.ExitOnError) |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to restore through (signed-in mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| if fs.NArg() != 1 { |
| log.Fatal("usage: carlos apps restore [-account <sqid|name>] [-account-id <sqid>] [-console <origin>] <app>") |
| @@ -378,7 +380,7 @@ func validPlaceTarget(target string) error { |
| func runAppsCreate(args []string) { |
| fs := flag.NewFlagSet("apps create", flag.ExitOnError) |
| - app := fs.String("app", "", "app name to claim (lowercase letters, digits, dashes)") |
| + app := appFlag(fs, "app name to claim (lowercase letters, digits, dashes)") |
| place := fs.String("place", "", `place the app on a customer fleet at create time — "" (default) or "fleet/<name>"`) |
| // Not accountFlags: that helper also registers -registry for direct-mode |
| // resolution, and this verb never resolves locally — the console resolves |
| @@ -386,6 +388,7 @@ func runAppsCreate(args []string) { |
| acctRaw := fs.String("account", "", "account (sqid or name) to claim the app in (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account; this is the flag an ambiguity error tells you to use") |
| consoleFlag := fs.String("console", "", "console to create it through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| if strings.TrimSpace(*app) == "" { |
| log.Fatal("usage: carlos apps create -app <name> [-place fleet/<f>] [-account <sqid|name>] " + |
| @@ -471,7 +474,7 @@ func placementMismatchErr(app, requested, echoed string) error { |
| // local refusal, not a spent write-rate-limit slot. |
| func runAppsPlace(args []string) { |
| fs := flag.NewFlagSet("apps place", flag.ExitOnError) |
| - app := fs.String("app", "", "app to place") |
| + app := appFlag(fs, "app to place") |
| target := fs.String("target", "", `fleet target — "" clears placement, or "fleet/<name>"`) |
| // Not accountFlags: that helper also registers -registry for direct-mode |
| // resolution, and this verb never resolves locally — placement is |
| @@ -479,9 +482,10 @@ func runAppsPlace(args []string) { |
| acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account; this is the flag an ambiguity error tells you to use") |
| consoleFlag := fs.String("console", "", "console to place through (default: the only logged-in one)") |
| - fs.Parse(args) |
| const placeUsage = `usage: carlos apps place -app <name> -target fleet/<f>|"" [-account <sqid|name>] ` + |
| "[-account-id <sqid>] [-console <origin>]" |
| + fs.Usage = flagUsage(fs, placeUsage) |
| + fs.Parse(args) |
| if strings.TrimSpace(*app) == "" { |
| log.Fatal(placeUsage) |
| } |
diff --git a/auth.go b/auth.go| index 81c66d55f..d34241836 100644 |
| --- a/auth.go |
| +++ b/auth.go |
| @@ -629,6 +629,7 @@ func authLogin(out io.Writer, args []string) error { |
| fs.SetOutput(out) |
| console := fs.String("console", "", "the console to log in to (default: the configured default, else "+defaultConsole+")") |
| label := fs.String("label", "", "what to call this terminal on the console (default: user@host)") |
| + fs.Usage = flagUsage(fs, "") |
| if err := fs.Parse(args); err != nil { |
| return err |
| } |
| @@ -736,6 +737,7 @@ func authWhoami(out io.Writer, args []string) error { |
| fs := flag.NewFlagSet("auth whoami", flag.ContinueOnError) |
| fs.SetOutput(out) |
| console := fs.String("console", "", "which console (only needed with several)") |
| + fs.Usage = flagUsage(fs, "") |
| if err := fs.Parse(args); err != nil { |
| return err |
| } |
| @@ -790,6 +792,7 @@ func authLogout(out io.Writer, args []string) error { |
| fs := flag.NewFlagSet("auth logout", flag.ContinueOnError) |
| fs.SetOutput(out) |
| console := fs.String("console", "", "which console (only needed with several)") |
| + fs.Usage = flagUsage(fs, "") |
| if err := fs.Parse(args); err != nil { |
| return err |
| } |
| @@ -835,6 +838,7 @@ func authDefault(out io.Writer, args []string) error { |
| fs := flag.NewFlagSet("auth default", flag.ContinueOnError) |
| fs.SetOutput(out) |
| project := fs.Bool("project", false, "write ./.carlos/config (this directory) instead of ~/.carlos/config") |
| + fs.Usage = flagUsage(fs, "") |
| if err := fs.Parse(args); err != nil { |
| return err |
| } |
diff --git a/cli.go b/cli.go| index bc352d0b0..ae7e31af7 100644 |
| --- a/cli.go |
| +++ b/cli.go |
| @@ -68,19 +68,20 @@ func reservedSlugs() map[string]bool { |
| func runAdd(args []string) { |
| fs := flag.NewFlagSet("add", flag.ExitOnError) |
| reg := fs.String("registry", defaultRegistry(), "registry database") |
| - app := fs.String("app", "", "app whose releases this route runs") |
| + app := appFlag(fs, "app whose releases this route runs") |
| acct := fs.String("account", "ops", "account (sqid or name) the app belongs to") |
| acctID := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| slug := fs.String("slug", "", "root-domain short name (optional)") |
| kind := fs.String("kind", "instance", "instance | service | static") |
| socket := fs.String("socket", "", "unix socket upstream") |
| addr := fs.String("addr", "", "TCP upstream host:port (whole-app tenants)") |
| - channel := fs.String("channel", "stable", "release channel to follow") |
| + channel := aliasedString(fs, "channel", "stable", "release channel to follow") |
| hibernate := fs.Bool("hibernate", false, "hibernate when idle") |
| statusNamed := fs.Bool("status-named", false, "opt this app into being named on the public status page") |
| backing := fs.String("backing", "", `process backing: "" (agent exec child) | "unit" (carlos-app@<host> systemd unit)`) |
| unit := fs.String("unit", "", "systemd unit that owns this route's process when it is neither an exec child nor a carlos-app@ tenant (e.g. console.service)") |
| dbPath := fs.String("db", "", "instance database path") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| if fs.NArg() != 1 { |
| log.Fatal("usage: carlos add [flags] <host>") |
| @@ -249,6 +250,7 @@ func runAdd(args []string) { |
| func runRemove(args []string) { |
| fs := flag.NewFlagSet("remove", flag.ExitOnError) |
| reg := fs.String("registry", defaultRegistry(), "registry database") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| if fs.NArg() != 1 { |
| log.Fatal("usage: carlos remove <host>") |
| @@ -294,6 +296,7 @@ func runInstances(args []string) { |
| } |
| fs := flag.NewFlagSet("instances", flag.ExitOnError) |
| reg := fs.String("registry", defaultRegistry(), "registry database") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| r, err := registry.Open(*reg) |
| if err != nil { |
| @@ -455,11 +458,11 @@ func runRoute(args []string) { |
| fs := flag.NewFlagSet("route", flag.ExitOnError) |
| regPath := fs.String("registry", defaultRegistry(), "registry database") |
| host := fs.String("host", "", "the one route to repoint") |
| - app := fs.String("app", "", "repoint every non-platform route of this app") |
| + app := appFlag(fs, "repoint every non-platform route of this app") |
| acctFilter := fs.String("account", "ops", `account (sqid or name) to filter -app routes by; "all" for every account`) |
| acctIDFilter := fs.String("account-id", "", "account sqid to filter -app routes by, matched as a sqid ONLY — supersedes -account") |
| - channel := fs.String("channel", "", "release channel to subscribe the route(s) to") |
| - environment := fs.String("environment", "", |
| + channel := channelFlag(fs, "release channel to subscribe the route(s) to") |
| + environment := environmentFlag(fs, |
| `named config environment to bind the route(s) to (must already exist in the bucket); "-" clears to default`) |
| grant := fs.String("grant", "", "capability token to grant this route ON THIS BOX: port:<n> (privileged, never 22/80/443) | host-role") |
| revoke := fs.String("revoke", "", "capability token to revoke from this route on this box") |
| @@ -473,6 +476,7 @@ func runRoute(args []string) { |
| "allow repointing to a channel that RESOLVES but holds a different version than the route currently serves "+ |
| "(never overrides an unresolvable target, a bad signature, or a missing pubkey)") |
| dryRun := fs.Bool("dry-run", false, "run the full safety-rail check and print the plan, but write nothing") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| // -hibernate has to be TRI-state: a plain flag.Bool cannot tell "the |
| // operator did not mention hibernation" from "the operator asked for |
| @@ -1717,16 +1721,17 @@ func publicKey() ed25519.PublicKey { |
| func runShip(args []string) { |
| fs := flag.NewFlagSet("ship", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| + app := appFlag(fs, "") |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to ship through (API mode only; default: the only logged-in one)") |
| kind := fs.String("kind", "binary", "binary | static") |
| - ver := fs.String("version", "", "version (git short sha); omit to mint the next iteration under the app's version target") |
| + ver := versionFlag(fs, "version (git short sha); omit to mint the next iteration under the app's version target") |
| target := fs.String("target", "linux-arm64", "GOOS-GOARCH the binary was built for (binary kind, single file)") |
| capsFile := fs.String("capabilities", "", "path to a JSON capability block (internal/caps) to embed in the manifest") |
| label := fs.String("label", "", "one-line human label for this release (\"fixing modals\")") |
| notes := fs.String("notes", "", "changelog fragment for this release") |
| notesFile := fs.String("notes-file", "", "file holding the changelog fragment (mutually exclusive with -notes)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| // -version is no longer required: with a version target set, the next |
| // iteration under it is inferable, and inferring it is the whole point of |
| @@ -2050,10 +2055,11 @@ func runShipAPI(sess *apiSession, acct apiAcct, fs *flag.FlagSet, app, ver, kind |
| // bypass — explicit, so it can be seen. |
| func runPromote(args []string) { |
| fs := flag.NewFlagSet("promote", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| + app := appFlag(fs, "") |
| hotfix := fs.Bool("hotfix", false, "bypass the channel ladder (recorded as a hotfix)") |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to promote through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos promote -app <a> [-hotfix] [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>] <version> <channel>" |
| if fs.NArg() != 2 { |
| @@ -2143,9 +2149,10 @@ func promoteActor() string { |
| func runChannels(args []string) { |
| fs := flag.NewFlagSet("channels", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| + app := appFlag(fs, "") |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to read through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos channels -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| @@ -2235,9 +2242,10 @@ func runChannels(args []string) { |
| // program, typed by every install script there is. |
| func runVersionTarget(args []string) { |
| fs := flag.NewFlagSet("version target", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| + app := appFlag(fs, "") |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to read or set through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos version target -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>] [<semver>]" |
| if fs.NArg() > 1 { |
| @@ -2396,9 +2404,10 @@ func localReleases(ctx context.Context, s release.Store, acct, app string) ([]re |
| // there to run". |
| func runReleases(args []string) { |
| fs := flag.NewFlagSet("releases", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| + app := appFlag(fs, "") |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to read through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos releases -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| @@ -2447,9 +2456,10 @@ func runReleases(args []string) { |
| func runRollback(args []string) { |
| fs := flag.NewFlagSet("rollback", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| + app := appFlag(fs, "") |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to roll back through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, ledgerUsage) |
| fs.Parse(args) |
| usage := "usage: carlos rollback -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>] <channel>" |
| if fs.NArg() != 1 { |
| @@ -2597,9 +2607,10 @@ func ledgerAccountFlags(fs *flag.FlagSet) (raw, rawID *string) { |
| func runLedgerAppend(args []string) { |
| fs := flag.NewFlagSet("ledger append", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| + app := appFlag(fs, "") |
| acctRaw, acctIDRaw := ledgerAccountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to append through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos ledger append -app <a> [-account <acct>] [-account-id <sqid>] <ledger> <entry.json|->" |
| if fs.NArg() != 2 { |
| @@ -2644,9 +2655,10 @@ func runLedgerAppend(args []string) { |
| // a JSON blob. |
| func runLedgerHead(args []string) { |
| fs := flag.NewFlagSet("ledger head", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| + app := appFlag(fs, "") |
| acctRaw, acctIDRaw := ledgerAccountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to read through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos ledger head -app <a> [-account <acct>] [-account-id <sqid>] <ledger>" |
| if fs.NArg() != 1 { |
| @@ -2673,9 +2685,10 @@ func runLedgerHead(args []string) { |
| func runLedgerList(args []string) { |
| fs := flag.NewFlagSet("ledger list", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| + app := appFlag(fs, "") |
| acctRaw, acctIDRaw := ledgerAccountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to read through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos ledger list -app <a> [-account <acct>] [-account-id <sqid>]" |
| @@ -2721,11 +2734,12 @@ func runLedgerList(args []string) { |
| func runLedgerPublish(args []string) { |
| fs := flag.NewFlagSet("ledger publish", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| + app := appFlag(fs, "") |
| acctRaw, acctIDRaw := ledgerAccountFlags(fs) |
| public := fs.Bool("public", true, "serve these ledgers publicly (-public=false unpublishes)") |
| title := fs.String("title", "", "display title for the published set") |
| consoleFlag := fs.String("console", "", "console origin to publish through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos ledger publish -app <a> [-account <acct>] [-account-id <sqid>] [-public=true|false] [-title <t>] <ledger>..." |
| if fs.NArg() == 0 { |
| @@ -2766,9 +2780,10 @@ func runLedgerPublish(args []string) { |
| func runLedgerBlob(args []string) { |
| fs := flag.NewFlagSet("ledger blob", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| + app := appFlag(fs, "") |
| acctRaw, acctIDRaw := ledgerAccountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to upload through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos ledger blob -app <a> [-account <acct>] [-account-id <sqid>] <ledger> <file>..." |
| if fs.NArg() < 2 { |
| @@ -2810,9 +2825,10 @@ func runLedgerBlob(args []string) { |
| // here). |
| func runLedgerVerify(args []string) { |
| fs := flag.NewFlagSet("ledger verify", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| + app := appFlag(fs, "") |
| account := fs.String("account", "", "account SQID (not name) — the public status-host path is keyed by sqid") |
| urlBase := fs.String("url", "", "public status-host base URL, e.g. https://status.example.com — no credentials needed") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| if *app == "" || *account == "" || *urlBase == "" || fs.NArg() != 1 { |
| log.Fatal("usage: carlos ledger verify -app <a> -account <sqid> -url <https://status-host> <ledger>") |
| @@ -2855,6 +2871,7 @@ func runKeygen(args []string) { |
| scope := fs.String("scope", "", "mint a SCOPED key for these rungs, comma separated (canary,edge,beta) — stable is never delegable") |
| name := fs.String("name", "", "label for the scoped key; it appears in every refusal it causes") |
| expires := fs.Duration("expires", defaultGrantTTL, "how long the scoped key may sign for (max 8760h)") |
| + fs.Usage = flagUsage(fs, statusUsage) |
| fs.Parse(args) |
| if *scope == "" { |
| @@ -2938,6 +2955,7 @@ func RunStatus(args []string) { |
| fs := flag.NewFlagSet("status", flag.ExitOnError) |
| dryRunDir := fs.String("dry-run", "", "build a fixture fleet, publish it into this directory, and verify no leaks") |
| liveFlag := fs.Bool("live", false, "read status/boxes/*.json from the status bucket and print per-host metrics") |
| + fs.Usage = flagUsage(fs, statusUsage) |
| fs.Parse(args) |
| if *liveFlag { |
diff --git a/deploy.go b/deploy.go| index 341fc2d40..b9d3dbea4 100644 |
| --- a/deploy.go |
| +++ b/deploy.go |
| @@ -34,16 +34,17 @@ import ( |
| // never a box command, per the operate-via-API rule. |
| func runDeploy(args []string) { |
| fs := flag.NewFlagSet("deploy", flag.ExitOnError) |
| - app := fs.String("app", "", "app name") |
| + app := appFlag(fs, "") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to deploy through (API mode only; default: the only logged-in one)") |
| kind := fs.String("kind", "binary", "binary | static") |
| - ver := fs.String("version", "", "version (default: git rev-parse --short HEAD)") |
| + ver := versionFlag(fs, "version (default: git rev-parse --short HEAD)") |
| target := fs.String("target", "linux-arm64", "GOOS-GOARCH the binary was built for (binary kind, single file)") |
| capsFile := fs.String("capabilities", "", "path to a JSON capability block (internal/caps) to embed in the manifest") |
| - channelFlag := fs.String("channel", "", "channel to promote to (default: resolved from the app's instances, or its one channel if it has no instances; error if ambiguous)") |
| + channelFlag := channelFlag(fs, "channel to promote to (default: resolved from the app's instances, or its one channel if it has no instances; error if ambiguous)") |
| hostFlag := fs.String("host", "", "serve on this hostname instead of the app's own platform host (also scopes channel resolution and the watch to it)") |
| timeout := fs.Duration("timeout", defaultDeployTimeout, "how long to watch the URL before failing") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos deploy -app <a> [-version <sha>] [-kind binary|static] [-target linux-arm64] " + |
| "[-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>] [-capabilities <file.json>] " + |
diff --git a/domains_cli.go b/domains_cli.go| index 38514d743..8439f201b 100644 |
| --- a/domains_cli.go |
| +++ b/domains_cli.go |
| @@ -71,13 +71,14 @@ func runDomains(args []string) { |
| func runDomainsAttach(args []string) { |
| fs := flag.NewFlagSet("domains attach", flag.ExitOnError) |
| - app := fs.String("app", "", "app the hostname belongs to") |
| + app := appFlag(fs, "app the hostname belongs to") |
| route := fs.String("route", "", "route hostname to attach to") |
| wildcard := fs.Bool("wildcard", false, "enable a delegated wildcard cert for this hostname (needs a CNAME — see the 422 message if refused)") |
| catchall := fs.Bool("catchall", false, "derive a *.<host> catch-all alias to the same route (needs -wildcard)") |
| consoleFlag := fs.String("console", "", "console to attach through (default: the only logged-in one)") |
| acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| + fs.Usage = flagUsage(fs, domainsAttachUsage) |
| fs.Parse(args) |
| host := strings.TrimSpace(fs.Arg(0)) |
| if host == "" || fs.NArg() > 1 { |
| @@ -155,10 +156,11 @@ func printDomainDNSLine(ctx context.Context, sess *apiSession, acct apiAcct, app |
| func runDomainsDetach(args []string) { |
| fs := flag.NewFlagSet("domains detach", flag.ExitOnError) |
| - app := fs.String("app", "", "app the hostname belongs to") |
| + app := appFlag(fs, "app the hostname belongs to") |
| consoleFlag := fs.String("console", "", "console to detach through (default: the only logged-in one)") |
| acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| + fs.Usage = flagUsage(fs, domainsDetachUsage) |
| fs.Parse(args) |
| host := strings.TrimSpace(fs.Arg(0)) |
| if host == "" || fs.NArg() > 1 { |
| @@ -181,10 +183,11 @@ func runDomainsDetach(args []string) { |
| func runDomainsList(args []string) { |
| fs := flag.NewFlagSet("domains list", flag.ExitOnError) |
| - app := fs.String("app", "", "app whose hostname claims to list") |
| + app := appFlag(fs, "app whose hostname claims to list") |
| consoleFlag := fs.String("console", "", "console to list through (default: the only logged-in one)") |
| acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| + fs.Usage = flagUsage(fs, domainsListUsage) |
| fs.Parse(args) |
| sess := requireAPISession("list", *consoleFlag) |
| printUsing(stdout, sess) |
diff --git a/economics.go b/economics.go| index 9c99ecb61..10cdf0edd 100644 |
| --- a/economics.go |
| +++ b/economics.go |
| @@ -201,7 +201,7 @@ func runEconomicsBackhaul(args []string) { |
| fs := flag.NewFlagSet("economics backhaul", flag.ExitOnError) |
| period := fs.String("period", "", "mtd (default), last, or 7d") |
| consoleFlag := fs.String("console", "", "console to ask (default: the only logged-in one)") |
| - fs.Usage = func() { fmt.Fprint(os.Stderr, economicsBackhaulUsage) } |
| + fs.Usage = flagUsage(fs, economicsBackhaulUsage) |
| fs.Parse(args) |
| sess, err := resolveMode(*consoleFlag) |
| if err != nil { |
| @@ -289,6 +289,7 @@ func runEconomicsBill(args []string) { |
| lines := serviceLines{} |
| fs.Var(lines, "line", "one line item, repeatable: Service=usd") |
| consoleFlag := fs.String("console", "", "console to record through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, economicsUsage) |
| fs.Parse(args) |
| set := map[string]bool{} |
diff --git a/fleets.go b/fleets.go| index efaa88cda..d70d05b74 100644 |
| --- a/fleets.go |
| +++ b/fleets.go |
| @@ -159,6 +159,7 @@ func runFleetsCreate(args []string) { |
| acctRaw := fs.String("account", "", "account (sqid or name) to own the fleet (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| consoleFlag := fs.String("console", "", "console to create it through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| if fs.NArg() != 1 || strings.TrimSpace(*bucket) == "" { |
| log.Fatal("usage: carlos fleets create -bucket <bucket> [-account <sqid|name>] " + |
| @@ -186,6 +187,7 @@ func runFleetsAddBox(args []string) { |
| acctRaw := fs.String("account", "", "account (sqid or name) the fleet belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| consoleFlag := fs.String("console", "", "console to add the box through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| if fs.NArg() != 2 { |
| log.Fatal("usage: carlos fleets add-box [-account <sqid|name>] [-account-id <sqid>] " + |
| @@ -214,6 +216,7 @@ func runFleetsRotateToken(args []string) { |
| acctRaw := fs.String("account", "", "account (sqid or name) the fleet belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| consoleFlag := fs.String("console", "", "console to rotate through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| if fs.NArg() != 2 { |
| log.Fatal("usage: carlos fleets rotate-token [-account <sqid|name>] [-account-id <sqid>] " + |
| @@ -278,9 +281,10 @@ func runFleetsTagBox(args []string) { |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| consoleFlag := fs.String("console", "", "console to tag through (default: the only logged-in one)") |
| tags := fs.String("tags", "", `comma-separated tags, full replace ("" clears every tag) — required`) |
| - fs.Parse(args) |
| const tagBoxUsage = "usage: carlos fleets tag-box -tags <tags> [-account <sqid|name>] [-account-id <sqid>] " + |
| "[-console <origin>] <fleet> <label>" |
| + fs.Usage = flagUsage(fs, tagBoxUsage) |
| + fs.Parse(args) |
| if fs.NArg() != 2 { |
| log.Fatal(tagBoxUsage) |
| } |
| @@ -323,6 +327,7 @@ func runFleetsDetachBox(args []string) { |
| acctRaw := fs.String("account", "", "account (sqid or name) the fleet belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| consoleFlag := fs.String("console", "", "console to detach through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| if fs.NArg() != 2 { |
| log.Fatal("usage: carlos fleets detach-box [-account <sqid|name>] [-account-id <sqid>] " + |
| @@ -350,6 +355,7 @@ func runFleetsList(args []string) { |
| acctRaw := fs.String("account", "", "account (sqid or name) to list fleets for (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| consoleFlag := fs.String("console", "", "console to list through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| sess := fleetsSignedInSession(*consoleFlag) |
diff --git a/instances_cli.go b/instances_cli.go| index f4859da26..8d62d67dd 100644 |
| --- a/instances_cli.go |
| +++ b/instances_cli.go |
| @@ -74,18 +74,19 @@ until this is set (spec §3.1). |
| func runInstancesEnable(args []string) { |
| fs := flag.NewFlagSet("instances enable", flag.ExitOnError) |
| - app := fs.String("app", "", "app whose instances this enables") |
| + app := appFlag(fs, "app whose instances this enables") |
| domain := fs.String("domain", "", "instance host domain suffix (store mode only)") |
| pool := fs.String("pool", "", "pool stamped into every record at create (store mode only)") |
| // Default "" so signed-in mode can tell "unset" from "asked for": an |
| // omitted -channel must preserve whatever the manifest already carries, |
| // not silently reset it to stable. Store mode applies the documented |
| // "stable" default below, so its behaviour is unchanged. |
| - channel := fs.String("channel", "", "release channel stamped into every record at create") |
| + channel := channelFlag(fs, "release channel stamped into every record at create") |
| regions := fs.String("regions", "", "comma-separated region allow-list (store mode only); empty = any shape-valid region") |
| seed := fs.String("seed", "", "argv of the app's first-boot init command, whitespace-split (strings.Fields)") |
| consoleFlag := fs.String("console", "", "console to enable through (signed-in mode only; default: the only logged-in one)") |
| acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| + fs.Usage = flagUsage(fs, instancesEnableUsage) |
| fs.Parse(args) |
| if *app != "" && !release.ValidApp(*app) { |
| log.Fatalf("carlos instances enable: %q is not a valid app name", *app) |
| @@ -292,10 +293,10 @@ func requireAPISession(verb, consoleFlag string) *apiSession { |
| func runInstancesCreate(args []string) { |
| fs := flag.NewFlagSet("instances create", flag.ExitOnError) |
| - app := fs.String("app", "", "app the instance belongs to") |
| + app := appFlag(fs, "app the instance belongs to") |
| host := fs.String("host", "", "instance host — one DNS label + \".\" + the app's instance domain") |
| region := fs.String("region", "", "region code for the record (subject to the manifest's allow-list)") |
| - channel := fs.String("channel", "", "release channel the instance follows (default: the manifest's, then stable)") |
| + channel := channelFlag(fs, "release channel the instance follows (default: the manifest's, then stable)") |
| kind := fs.String("kind", "", "route class: instance (default) | service | static — a static record serves a file tree off the channel pointer and has no process") |
| addr := fs.String("addr", "", "comma-separated TCP upstream set, host:port each (e.g. 10.0.0.1:8090,10.0.0.2:8090); omitted means the platform's own default upstream") |
| healthPath := fs.String("health-path", "", "states the whole probe alongside -health-status: request path (default \"/\"); honoured for a single -addr too") |
| @@ -303,6 +304,7 @@ func runInstancesCreate(args []string) { |
| consoleFlag := fs.String("console", "", "console to create through (default: the only logged-in one)") |
| acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| + fs.Usage = flagUsage(fs, instancesRecordsUsage) |
| fs.Parse(args) |
| if *host == "" { |
| log.Fatal(instancesRecordsUsage) |
| @@ -377,12 +379,13 @@ func upstreamsCell(ups []apiUpstreamState) string { |
| func runInstancesList(args []string) { |
| fs := flag.NewFlagSet("instances list", flag.ExitOnError) |
| - app := fs.String("app", "", "app whose instance records to list") |
| + app := appFlag(fs, "app whose instance records to list") |
| match := fs.String("match", "", "only hosts containing this substring") |
| health := fs.String("health", "", "only this state: running, serving, setting-up, asleep, no-readings, not-responding, failed") |
| consoleFlag := fs.String("console", "", "console to list through (default: the only logged-in one)") |
| acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| + fs.Usage = flagUsage(fs, instancesRecordsUsage) |
| fs.Parse(args) |
| sess := requireAPISession("list", *consoleFlag) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| @@ -417,11 +420,12 @@ func runInstancesList(args []string) { |
| func runInstancesDelete(args []string) { |
| fs := flag.NewFlagSet("instances delete", flag.ExitOnError) |
| - app := fs.String("app", "", "app the instance belongs to") |
| + app := appFlag(fs, "app the instance belongs to") |
| host := fs.String("host", "", "instance host whose record to retire") |
| consoleFlag := fs.String("console", "", "console to delete through (default: the only logged-in one)") |
| acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| + fs.Usage = flagUsage(fs, instancesRecordsUsage) |
| fs.Parse(args) |
| if *host == "" { |
| log.Fatal(instancesRecordsUsage) |
| @@ -454,7 +458,7 @@ func runInstancesDelete(args []string) { |
| // operator guidance, not something to summarize). |
| func runInstancesSetUpstreams(args []string) { |
| fs := flag.NewFlagSet("instances set-upstreams", flag.ExitOnError) |
| - app := fs.String("app", "", "app the instance belongs to") |
| + app := appFlag(fs, "app the instance belongs to") |
| host := fs.String("host", "", "instance host whose upstream set to repoint") |
| addrsFlag := fs.String("addrs", "", "comma-separated TCP upstream set, host:port each, replacing the record's whole upstream set; omitted leaves the existing set alone; an explicit -addrs \"\" clears it (400 for a kind:instance record — the DerivesPaths rail refuses that shape transition in place)") |
| healthPath := fs.String("health-path", "", "states the WHOLE probe block, replacing whatever was there — not a per-field update; a field neither flag names reverts to the platform's own default") |
| @@ -462,6 +466,7 @@ func runInstancesSetUpstreams(args []string) { |
| consoleFlag := fs.String("console", "", "console to update through (default: the only logged-in one)") |
| acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| + fs.Usage = flagUsage(fs, instancesRecordsUsage) |
| fs.Parse(args) |
| if *host == "" { |
| log.Fatal(instancesRecordsUsage) |
| @@ -565,11 +570,12 @@ opt-in, it does not turn steering ON for the fleet. |
| // it here would throw that away. |
| func runSteering(args []string) { |
| fs := flag.NewFlagSet("steering", flag.ExitOnError) |
| - app := fs.String("app", "", "app the instance belongs to") |
| + app := appFlag(fs, "app the instance belongs to") |
| host := fs.String("host", "", "instance host to steer") |
| consoleFlag := fs.String("console", "", "console to act through (default: the only logged-in one)") |
| acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| + fs.Usage = flagUsage(fs, steeringUsage) |
| fs.Parse(args) |
| if *host == "" || fs.NArg() != 1 { |
| log.Fatal(steeringUsage) |
| @@ -611,12 +617,13 @@ func runSteering(args []string) { |
| // next reconcile overwrites, and says so. |
| func runInstancesSetChannel(args []string) { |
| fs := flag.NewFlagSet("instances set-channel", flag.ExitOnError) |
| - app := fs.String("app", "", "app the instance belongs to") |
| + app := appFlag(fs, "app the instance belongs to") |
| host := fs.String("host", "", "instance host whose channel to change") |
| - channel := fs.String("channel", "", "release channel the route should follow") |
| + channel := channelFlag(fs, "release channel the route should follow") |
| consoleFlag := fs.String("console", "", "console to act through (default: the only logged-in one)") |
| acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| + fs.Usage = flagUsage(fs, instancesRecordsUsage) |
| fs.Parse(args) |
| if *host == "" || *channel == "" { |
| log.Fatal(instancesRecordsUsage) |
diff --git a/logs.go b/logs.go| index 5f338fc89..937449252 100644 |
| --- a/logs.go |
| +++ b/logs.go |
| @@ -13,7 +13,6 @@ import ( |
| "io" |
| "log" |
| "net/url" |
| - "os" |
| "sort" |
| "strconv" |
| "time" |
| @@ -27,23 +26,6 @@ Shows the newest lines first-to-last: the app's own stdout/stderr ("app") |
| merged with the platform's events about it ("platform" — wakes, restarts, |
| failures with the reason). A site served from files alone has no app |
| stream; platform events still appear. |
| - |
| -flags: |
| - -app <a> app whose logs to read (required) |
| - -since <t> window start: a duration back from now (15m, 1h, 24h) |
| - or an RFC3339 time (default 1h) |
| - -until <t> window end, same forms (default: now) |
| - -stream <s> only one stream: app | platform | edge | release |
| - -host <h> only one instance (its route host) |
| - -level <l> minimum level: debug | info | warn | error — only lines |
| - that CARRY a level; plain text never claims one |
| - -grep <re> only lines whose message matches this RE2 pattern |
| - -field k=v only structured lines with this field (repeatable) |
| - -n <n> at most this many lines (default 1000, max 5000) |
| - -f follow: poll for new lines every 2s until interrupted |
| - -account <a> account (sqid or name) the app belongs to (default "ops") |
| - -account-id <id> account sqid, matched as a sqid ONLY — supersedes -account |
| - -console <o> console to read through (default: the only logged-in one) |
| ` |
| // logsFollowInterval is the -f poll cadence — the serial poll's own 2s, the |
| @@ -55,7 +37,7 @@ var logsMaxPolls = 0 |
| func runLogs(args []string) { |
| fs := flag.NewFlagSet("logs", flag.ExitOnError) |
| - app := fs.String("app", "", "app whose logs to read") |
| + app := appFlag(fs, "app whose logs to read") |
| since := fs.String("since", "1h", "window start: duration back from now, or RFC3339") |
| until := fs.String("until", "", "window end (default: now)") |
| stream := fs.String("stream", "", "only one stream: app | platform | edge | release") |
| @@ -69,7 +51,7 @@ func runLogs(args []string) { |
| consoleFlag := fs.String("console", "", "console to read through (default: the only logged-in one)") |
| acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| - fs.Usage = func() { fmt.Fprint(os.Stderr, logsUsage) } |
| + fs.Usage = flagUsage(fs, logsUsage) |
| fs.Parse(args) |
| q := url.Values{} |
diff --git a/mail_cli.go b/mail_cli.go| index fcfbde984..89cef1ea3 100644 |
| --- a/mail_cli.go |
| +++ b/mail_cli.go |
| @@ -596,13 +596,14 @@ func waitForDomainVerified(ctx context.Context, verify func(context.Context) (ap |
| func runEmailEnable(args []string) { |
| fs := flag.NewFlagSet("email enable", flag.ExitOnError) |
| - app := fs.String("app", "", "app to enable email for") |
| + app := appFlag(fs, "app to enable email for") |
| envPrefix := fs.String("env-prefix", "", "env var prefix to deliver under (default CARLOS_SMTP)") |
| from := fs.String("from", "", "the From address this app sends as (default: noreply@ the domain)") |
| domain := fs.String("domain", "", "sending domain to ensure and verify (default: the domain half of -from)") |
| timeout := fs.Duration("timeout", 10*time.Minute, "how long to wait for domain verification before giving up") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to enable through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| usage := "usage: carlos email enable -app <a> [-env-prefix <PREFIX>] [-from <addr>] [-domain <d>] [-timeout <dur>] " + |
| @@ -711,10 +712,11 @@ func runEmailDomains(args []string) { |
| func runEmailDomainsAdd(args []string) { |
| fs := flag.NewFlagSet("email domains add", flag.ExitOnError) |
| - app := fs.String("app", "", "app to add the domain for") |
| + app := appFlag(fs, "app to add the domain for") |
| timeout := fs.Duration("timeout", 10*time.Minute, "how long to wait for verification before giving up") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos email domains add -app <a> [-timeout <dur>] [-account <sqid-or-name>] " + |
| "[-account-id <sqid>] [-console <origin>] <domain>" |
| @@ -773,9 +775,10 @@ func printPendingRecords(w io.Writer, recs []apiDNSRecord) { |
| func runEmailStatus(args []string) { |
| fs := flag.NewFlagSet("email status", flag.ExitOnError) |
| - app := fs.String("app", "", "app whose email to show") |
| + app := appFlag(fs, "app whose email to show") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| usage := "usage: carlos email status -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| @@ -936,10 +939,11 @@ func printEmailEnabled(w io.Writer, res apiEmailReply) { |
| func runEmailTest(args []string) { |
| fs := flag.NewFlagSet("email test", flag.ExitOnError) |
| - app := fs.String("app", "", "app to send a test message through") |
| + app := appFlag(fs, "app to send a test message through") |
| to := fs.String("to", "", "address to send the test message to") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| toAddr := strings.TrimSpace(*to) |
| @@ -1141,10 +1145,11 @@ func runEmailCredentials(args []string) { |
| func runEmailCredentialsCreate(args []string) { |
| fs := flag.NewFlagSet("email credentials create", flag.ExitOnError) |
| - app := fs.String("app", "", "app to mint a standalone credential for") |
| + app := appFlag(fs, "app to mint a standalone credential for") |
| credName := fs.String("name", "", "credential name (lowercase, digits, hyphens)") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos email credentials create -app <a> -name <n> [-account <sqid-or-name>] " + |
| "[-account-id <sqid>] [-console <origin>]" |
| @@ -1186,9 +1191,10 @@ func printEmailCredentialsCreate(w io.Writer, res apiEmailCredsReply) { |
| func runEmailCredentialsList(args []string) { |
| fs := flag.NewFlagSet("email credentials list", flag.ExitOnError) |
| - app := fs.String("app", "", "app whose standalone credentials to list") |
| + app := appFlag(fs, "app whose standalone credentials to list") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| usage := "usage: carlos email credentials list -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| @@ -1227,10 +1233,11 @@ func printEmailCredentialsList(w io.Writer, sa []apiEmailStandalone) { |
| func runEmailCredentialsRevoke(args []string) { |
| fs := flag.NewFlagSet("email credentials revoke", flag.ExitOnError) |
| - app := fs.String("app", "", "app whose standalone credential to revoke") |
| + app := appFlag(fs, "app whose standalone credential to revoke") |
| credName := fs.String("name", "", "credential name to revoke") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| usage := "usage: carlos email credentials revoke -app <a> -name <n> [-account <sqid-or-name>] " + |
| "[-account-id <sqid>] [-console <origin>]" |
| @@ -1260,10 +1267,11 @@ func runEmailCredentialsRevoke(args []string) { |
| func runEmailRotate(args []string) { |
| fs := flag.NewFlagSet("email rotate", flag.ExitOnError) |
| - app := fs.String("app", "", "app whose delivered email credential to rotate") |
| + app := appFlag(fs, "app whose delivered email credential to rotate") |
| finish := fs.Bool("finish", false, "delete the previous key instead of minting a new one") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| usage := "usage: carlos email rotate -app <a> [-finish] [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| @@ -1306,9 +1314,10 @@ func runEmailPause(args []string, paused bool) { |
| verb = "resume" |
| } |
| fs := flag.NewFlagSet("email "+verb, flag.ExitOnError) |
| - app := fs.String("app", "", "app to "+verb) |
| + app := appFlag(fs, "app to "+verb) |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| usage := fmt.Sprintf("usage: carlos email %s -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]", verb) |
diff --git a/pipeline.go b/pipeline.go| index d6cd07d7e..26916a482 100644 |
| --- a/pipeline.go |
| +++ b/pipeline.go |
| @@ -20,7 +20,6 @@ import ( |
| "fmt" |
| "io" |
| "log" |
| - "os" |
| "strings" |
| ) |
| @@ -117,12 +116,12 @@ func pipelineSignedInSession(consoleFlag string) *apiSession { |
| // subcommand, not just the one typed. |
| func pipelineFlagSet(name string) (fs *flag.FlagSet, app, acctRaw, acctIDRaw, consoleFlag *string) { |
| fs = flag.NewFlagSet(name, flag.ExitOnError) |
| - app = fs.String("app", "", "app name") |
| + app = appFlag(fs, "") |
| // API mode never opens the local registry — the third return |
| // (accountFlags' direct-mode resolver) has no use here and is dropped. |
| acctRaw, acctIDRaw, _ = accountFlags(fs) |
| consoleFlag = fs.String("console", "", "console origin to work through (default: the only logged-in one)") |
| - fs.Usage = func() { fmt.Fprint(os.Stderr, pipelineUsage) } |
| + fs.Usage = flagUsage(fs, pipelineUsage) |
| return fs, app, acctRaw, acctIDRaw, consoleFlag |
| } |
| @@ -319,7 +318,7 @@ func pipelinePolicyFlags(fs *flag.FlagSet) (bake *string, passkey *bool, promote |
| // a rule someone else set, and PUTs the whole list back. |
| func runPipelineSet(args []string) { |
| fs, app, acctRaw, acctIDRaw, consoleFlag := pipelineFlagSet("pipeline set") |
| - channel := fs.String("channel", "", "channel to edit") |
| + channel := channelFlag(fs, "channel to edit") |
| bake, passkey, promoteApprovals, changeApprovals := pipelinePolicyFlags(fs) |
| fs.Parse(args) |
| usage := "usage: carlos pipeline set -channel <name> [-app <a>] [-bake <dur>] [-passkey[=false]] " + |
| @@ -382,7 +381,7 @@ func runPipelineSet(args []string) { |
| // silently made the ENTRY channel, which -after edge would spell instead). |
| func runPipelineAdd(args []string) { |
| fs, app, acctRaw, acctIDRaw, consoleFlag := pipelineFlagSet("pipeline add") |
| - channel := fs.String("channel", "", "channel to add") |
| + channel := channelFlag(fs, "channel to add") |
| after := fs.String("after", "", "insert immediately after this existing channel (default: append at the end)") |
| bake, passkey, promoteApprovals, changeApprovals := pipelinePolicyFlags(fs) |
| fs.Parse(args) |
| @@ -437,7 +436,7 @@ func runPipelineAdd(args []string) { |
| // as it is the one place every other pipeline shape rule is enforced. |
| func runPipelineRemove(args []string) { |
| fs, app, acctRaw, acctIDRaw, consoleFlag := pipelineFlagSet("pipeline remove") |
| - channel := fs.String("channel", "", "channel to remove") |
| + channel := channelFlag(fs, "channel to remove") |
| fs.Parse(args) |
| usage := "usage: carlos pipeline remove -channel <name> [-app <a>] [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| if *channel == "" { |
diff --git a/restart.go b/restart.go| index 213705f78..77e1ccb82 100644 |
| --- a/restart.go |
| +++ b/restart.go |
| @@ -22,9 +22,10 @@ import ( |
| // write, and the object it touches means nothing without a fleet reading it. |
| func runRestart(args []string) { |
| fs := flag.NewFlagSet("restart", flag.ExitOnError) |
| - app := fs.String("app", "", "app whose instances to cycle") |
| + app := appFlag(fs, "app whose instances to cycle") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to restart through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| usage := "usage: carlos restart -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
diff --git a/retention.go b/retention.go| index d31cdfc66..274e95be2 100644 |
| --- a/retention.go |
| +++ b/retention.go |
| @@ -21,12 +21,13 @@ import ( |
| func runReleasesRetention(args []string) { |
| fs := flag.NewFlagSet("releases retention", flag.ExitOnError) |
| - app := fs.String("app", "", "app whose retention policy to read or write") |
| + app := appFlag(fs, "app whose retention policy to read or write") |
| keep := fs.Int("keep", 0, fmt.Sprintf("retain the newest N releases (minimum %d)", release.KeepFloor)) |
| off := fs.Bool("off", false, "clear the retention policy — keep releases forever again") |
| dryRun := fs.Bool("dry-run", false, "with -keep: list what would be pruned, write nothing") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| usage := "usage: carlos releases retention -app <a> [-keep <n> [-dry-run] | -off] [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
diff --git a/routes_cli.go b/routes_cli.go| index a982da920..1671e5a5d 100644 |
| --- a/routes_cli.go |
| +++ b/routes_cli.go |
| @@ -37,11 +37,12 @@ Signed-in only (carlos auth login). On a box, the local listing is |
| func runRoutes(args []string) { |
| fs := flag.NewFlagSet("routes", flag.ExitOnError) |
| - app := fs.String("app", "", "app whose routes to list") |
| + app := appFlag(fs, "app whose routes to list") |
| match := fs.String("match", "", "only hosts containing this substring") |
| consoleFlag := fs.String("console", "", "console to list through (default: the only logged-in one)") |
| acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| + fs.Usage = flagUsage(fs, routesUsage) |
| fs.Parse(args) |
| sess := requireAPISession("routes", *consoleFlag) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
diff --git a/schedule.go b/schedule.go| index 69e809b97..ca350fee4 100644 |
| --- a/schedule.go |
| +++ b/schedule.go |
| @@ -105,9 +105,10 @@ func requireScheduleAPISession(consoleFlag string) *apiSession { |
| func runScheduleLs(args []string) { |
| fs := flag.NewFlagSet("schedule ls", flag.ExitOnError) |
| - app := fs.String("app", "", "app whose schedules to list") |
| + app := appFlag(fs, "app whose schedules to list") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console to list through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, scheduleLsUsage) |
| fs.Parse(args) |
| sess := requireScheduleAPISession(*consoleFlag) |
| @@ -123,13 +124,14 @@ func runScheduleLs(args []string) { |
| func runScheduleSet(args []string) { |
| fs := flag.NewFlagSet("schedule set", flag.ExitOnError) |
| - app := fs.String("app", "", "app the schedule belongs to") |
| + app := appFlag(fs, "app the schedule belongs to") |
| name := fs.String("name", "", "schedule name — [a-z0-9][a-z0-9-]{0,31}") |
| every := fs.String("every", "", "fire every duration, whole minutes only (1m to 720h) — give this or -cron, not both") |
| cron := fs.String("cron", "", `fire on a 5-field cron expression ("min hour dom mon dow") — give this or -every, not both`) |
| path := fs.String("path", "", "absolute path on the app's own instance to POST at each fire") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console to record through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, scheduleSetUsage) |
| fs.Parse(args) |
| sc := schedule.Schedule{ |
| @@ -171,10 +173,11 @@ func runScheduleSet(args []string) { |
| func runScheduleRm(args []string) { |
| fs := flag.NewFlagSet("schedule rm", flag.ExitOnError) |
| - app := fs.String("app", "", "app the schedule belongs to") |
| + app := appFlag(fs, "app the schedule belongs to") |
| name := fs.String("name", "", "schedule name to remove") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console to remove through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, scheduleRmUsage) |
| fs.Parse(args) |
| scheduleName := strings.TrimSpace(*name) |
| if scheduleName == "" { |
| @@ -194,10 +197,11 @@ func runScheduleRm(args []string) { |
| func runScheduleRun(args []string) { |
| fs := flag.NewFlagSet("schedule run", flag.ExitOnError) |
| - app := fs.String("app", "", "app the schedule belongs to") |
| + app := appFlag(fs, "app the schedule belongs to") |
| name := fs.String("name", "", "schedule name to run now") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console to run through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, scheduleRunUsage) |
| fs.Parse(args) |
| scheduleName := strings.TrimSpace(*name) |
| if scheduleName == "" { |
diff --git a/services.go b/services.go| index d7714e829..e3d164256 100644 |
| --- a/services.go |
| +++ b/services.go |
| @@ -155,10 +155,11 @@ func validServiceHandle(handle string) error { |
| func runServicesCreate(args []string) { |
| fs := flag.NewFlagSet("services create", flag.ExitOnError) |
| role := fs.String("role", "", "role: publish, instance, operate or admin (required)") |
| - app := fs.String("app", "", "bind the credential to one app (default: account-wide)") |
| + app := appFlag(fs, "bind the credential to one app (default: account-wide)") |
| acctRaw := fs.String("account", "", "account (sqid or name) to mint the credential in (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| consoleFlag := fs.String("console", "", "console to create it through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| const usageMsg = "usage: carlos services create -role <publish|instance|operate|admin> [-app <app>] " + |
| "[-account <sqid|name>] [-account-id <sqid>] [-console <origin>] <name>" |
| @@ -273,6 +274,7 @@ func runServicesList(args []string) { |
| acctRaw := fs.String("account", "", "account (sqid or name) to list credentials for (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| consoleFlag := fs.String("console", "", "console to list through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| sess := servicesSignedInSession(*consoleFlag) |
| @@ -320,6 +322,7 @@ func runServicesRotate(args []string) { |
| acctRaw := fs.String("account", "", "account (sqid or name) the credential belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| consoleFlag := fs.String("console", "", "console to rotate through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| if fs.NArg() != 1 { |
| log.Fatal("usage: carlos services rotate [-account <sqid|name>] [-account-id <sqid>] " + |
| @@ -368,6 +371,7 @@ func runServicesRevoke(args []string) { |
| acctRaw := fs.String("account", "", "account (sqid or name) the credential belongs to (default: your only account, or the configured/chosen one)") |
| acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| consoleFlag := fs.String("console", "", "console to revoke through (default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| if fs.NArg() != 1 { |
| log.Fatal("usage: carlos services revoke [-account <sqid|name>] [-account-id <sqid>] " + |
diff --git a/services_cli_test.go b/services_cli_test.go| index c03d213ce..36ef35354 100644 |
| --- a/services_cli_test.go |
| +++ b/services_cli_test.go |
| @@ -553,27 +553,37 @@ func parseDeliverCommand(t *testing.T, line string) (verb, flags []string) { |
| // family's own bare usage constant (secretsUsage etc.), neither of which |
| // ever prints "Usage of <name>:" for the verb that was actually typed. |
| func helpOutputIsForVerb(helpOut string, verb []string) bool { |
| - want := "Usage of " + strings.Join(verb, " ") + ":" |
| + // The shared printer's synopsis line: "usage: carlos <verb> ..." — |
| + // matched to the verb WORDS, so the top-level page ("usage: carlos |
| + // <command> [flags]") can never pass for "secrets set". |
| + want := "usage: carlos " + strings.Join(verb, " ") |
| for _, line := range strings.Split(helpOut, "\n") { |
| - if strings.TrimSpace(line) == want { |
| + trimmed := strings.TrimSpace(line) |
| + if trimmed == want || strings.HasPrefix(trimmed, want+" ") { |
| return true |
| } |
| } |
| return false |
| } |
| -// helpOutputHasFlag reports whether helpOut's flag.PrintDefaults output |
| -// lists EXACTLY flagName as a registered flag — a NAME match, never a |
| -// substring match (C46, review). Each flag prints as " -name" or " -name |
| -// type" at the START of its own line, so this checks that shape rather than |
| +// helpOutputHasFlag reports whether helpOut's flagUsage output lists |
| +// EXACTLY flagName as a registered flag — a NAME match, never a substring |
| +// match (C46, review). Each flag prints as " --name <type> usage" (or |
| +// " --name, -n <type> usage"; single-letter primaries as " -n"), so |
| +// this compares the row head's dash-stripped first token rather than |
| // searching for flagName anywhere in the text: a strings.Contains check |
| // would let a search for -app pass against a renamed -application flag, |
| // since "-application" begins with the plain substring "-app" — the exact |
| // false positive the review demonstrated. |
| func helpOutputHasFlag(helpOut, flagName string) bool { |
| + name := strings.TrimLeft(flagName, "-") |
| for _, line := range strings.Split(helpOut, "\n") { |
| trimmed := strings.TrimSpace(line) |
| - if trimmed == flagName || strings.HasPrefix(trimmed, flagName+" ") { |
| + // A row's head is "--name" or "-n", optionally ", -alias", then |
| + // " <type>". Take the first token and compare the NAME exactly. |
| + head, _, _ := strings.Cut(trimmed, " ") |
| + head = strings.TrimSuffix(head, ",") |
| + if strings.TrimLeft(head, "-") == name && strings.HasPrefix(head, "-") { |
| return true |
| } |
| } |
| @@ -596,7 +606,7 @@ func TestHelpOutputIsForVerbRejectsFallThrough(t *testing.T) { |
| t.Fatal("helpOutputIsForVerb matched a fall-through top-level usage page, " + |
| "which happens to contain \"-app\" as a substring in an unrelated verb's description") |
| } |
| - real := "Usage of secrets set:\n -app string\n \tapp name\n" |
| + real := "usage: carlos secrets set [flags]\nflags:\n --app, -a <string> app name\n" |
| if !helpOutputIsForVerb(real, []string{"secrets", "set"}) { |
| t.Fatal("helpOutputIsForVerb did not match the real usage page for the verb it names") |
| } |
| @@ -608,14 +618,14 @@ func TestHelpOutputIsForVerbRejectsFallThrough(t *testing.T) { |
| // search for -app pass against a hypothetically renamed -application flag, |
| // since "-application" begins with the plain substring "-app". |
| func TestHelpOutputHasFlagRejectsPrefixPreservingRename(t *testing.T) { |
| - renamed := "Usage of secrets set:\n -application string\n \tapp name\n" |
| + renamed := "usage: carlos secrets set [flags]\nflags:\n --application <string> app name\n" |
| if helpOutputHasFlag(renamed, "-app") { |
| t.Fatal("helpOutputHasFlag(-app) matched -application -- a substring match, not an exact one") |
| } |
| if !helpOutputHasFlag(renamed, "-application") { |
| t.Fatal("helpOutputHasFlag(-application) should match its own exact name") |
| } |
| - if !helpOutputHasFlag("Usage of secrets set:\n -app string\n \tapp name\n", "-app") { |
| + if !helpOutputHasFlag("usage: carlos secrets set [flags]\nflags:\n --app, -a <string> app name\n", "-app") { |
| t.Fatal("helpOutputHasFlag(-app) should match its own exact name") |
| } |
| } |
diff --git a/store.go b/store.go| index c1d99e10b..8adacc55f 100644 |
| --- a/store.go |
| +++ b/store.go |
| @@ -126,10 +126,11 @@ func requireStoreAPISession(consoleFlag string) *apiSession { |
| func runStoreCreate(args []string) { |
| fs := flag.NewFlagSet("store create", flag.ExitOnError) |
| - app := fs.String("app", "", "app to declare a store for") |
| + app := appFlag(fs, "app to declare a store for") |
| envPrefix := fs.String("env-prefix", "", "env var prefix to deliver under (default CARLOS_STORE)") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to declare through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| usage := "usage: carlos store create -app <a> [-env-prefix <PREFIX>] [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| @@ -155,9 +156,10 @@ func runStoreCreate(args []string) { |
| func runStoreStatus(args []string) { |
| fs := flag.NewFlagSet("store status", flag.ExitOnError) |
| - app := fs.String("app", "", "app whose store to show") |
| + app := appFlag(fs, "app whose store to show") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to read through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| usage := "usage: carlos store status -app <a> [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| @@ -180,10 +182,11 @@ func runStoreStatus(args []string) { |
| func runStoreGrant(args []string) { |
| fs := flag.NewFlagSet("store grant", flag.ExitOnError) |
| - app := fs.String("app", "", "app to grant a store for") |
| + app := appFlag(fs, "app to grant a store for") |
| bucket := fs.String("bucket", "", "adopt this existing bucket instead of minting one") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to grant through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| usage := "usage: carlos store grant -app <a> [-bucket <name>] [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
| @@ -206,10 +209,11 @@ func runStoreGrant(args []string) { |
| func runStoreRotate(args []string) { |
| fs := flag.NewFlagSet("store rotate", flag.ExitOnError) |
| - app := fs.String("app", "", "app whose store credential to rotate") |
| + app := appFlag(fs, "app whose store credential to rotate") |
| finish := fs.Bool("finish", false, "delete the previous key instead of minting a new one") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := fs.String("console", "", "console origin to rotate through (API mode only; default: the only logged-in one)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| usage := "usage: carlos store rotate -app <a> [-finish] [-account <sqid-or-name>] [-account-id <sqid>] [-console <origin>]" |
diff --git a/update.go b/update.go| index 94f521f47..ed8c89e4f 100644 |
| --- a/update.go |
| +++ b/update.go |
| @@ -214,6 +214,7 @@ func installMethod(exe string) string { |
| func runUpdate(args []string) { |
| fs := flag.NewFlagSet("update", flag.ExitOnError) |
| yes := fs.Bool("y", false, "replace without asking (plain-binary installs only)") |
| + fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| if os.Getenv("CARLOS_DEPLOYMENT_BUCKET") != "" || os.Getenv("CARLOS_DEPLOYMENT_DIR") != "" { |
| log.Fatal("carlos update is for workstation installs — a box's binary is rolled by update-platform-binary.sh and adoption") |