carlos: group the top-level command list by role
Bare `carlos` printed every verb in one flat list, each with its caveats inline — the route entry alone ran 24 lines. It read as a wall of text rather than an offer of what to do next. Three sections now, by the role you are in when you type the command: your apps (auth, ship, promote, env, secrets, ...), your deployment (bootstrap, add, route, release-keygen, ...), and what systemd runs on the host (edge, agent, adopt, ops). One line each; the caveats all still print from each command's own usage, bare or with -h. Also: `rollback` was wired up in main.go but missing from the list, so it is listed now; `canary` is dropped from it, since it log.Fatals "not implemented" and advertising it was noise (the case is untouched). `help`/`-h`/`--help` now print to stdout and exit 0, so `carlos help | less` works — a bare `carlos` still goes to stderr with exit 2.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+48
−75
main.go+48 −75
diff --git a/main.go b/main.go| index bcdc89bd5..a2abeffe4 100644 |
| --- a/main.go |
| +++ b/main.go |
| @@ -4,6 +4,7 @@ package main |
| import ( |
| "fmt" |
| + "io" |
| "log" |
| "os" |
| @@ -16,11 +17,15 @@ import ( |
| func main() { |
| if len(os.Args) < 2 { |
| - usage() |
| + usage(os.Stderr) |
| os.Exit(2) |
| } |
| cmd, args := os.Args[1], os.Args[2:] |
| switch cmd { |
| + // Asking for help is not an error: it prints on stdout and exits 0, so |
| + // `carlos help | less` works. A bare `carlos` still fails (stderr, 2). |
| + case "help", "-h", "--help": |
| + usage(os.Stdout) |
| case "version": |
| fmt.Printf("carlos %s\n", version.Build) |
| case "edge": |
| @@ -70,83 +75,51 @@ func main() { |
| case "canary": |
| log.Fatalf("not implemented: %s", cmd) |
| default: |
| - usage() |
| + usage(os.Stderr) |
| os.Exit(2) |
| } |
| } |
| -func usage() { |
| - fmt.Fprintln(os.Stderr, `usage: carlos <command> |
| +// usage prints the top-level command list, grouped by the role you are in |
| +// when you type the command: shipping your own app, standing up and |
| +// administering a deployment, or the verbs a host runs on itself. The |
| +// one-liners here are deliberately thin — every command prints its own full |
| +// usage (bare, or with -h), and that is where the detail belongs. A flat |
| +// alphabetical list of every verb with every caveat inline was the previous |
| +// shape, and it read as a wall rather than as an offer of what to do next. |
| +func usage(w io.Writer) { |
| + fmt.Fprintln(w, `usage: carlos <command> [flags] |
| -commands: |
| - version print the build id |
| - edge run the mesh front door (router) |
| - agent run edge + activator + release adoption on this host |
| - add | remove | instances manage routes (bare "instances" lists THIS |
| - box's registry routes; "instances enable" |
| - opts an app into instance provisioning — |
| - signed-in mode pins it to the account's |
| - platform subdomain; "instances |
| - create|list|delete" manage instance records |
| - through the console API, signed-in only) |
| - route -host <h> | -app <a> [-account <sqid|name|all>] |
| - [-channel <c>] [-environment <name>] [-dry-run]: |
| - change which release channel and/or named |
| - config environment a route follows — at least |
| - one of -channel/-environment is required, both |
| - may be combined (-account defaults to ops; |
| - -app is all-or-nothing and skips |
| - platform-minted routes; -channel refuses a |
| - version change unless -allow-version-change is |
| - passed; -environment refuses a bundle that |
| - does not exist yet, and "-" clears to default) |
| - -host <h> (-grant | -revoke) <port:n|host-role>: |
| - grant or revoke an operator-consented capability |
| - for ONE route on THIS box (never -app, never |
| - combined with a repoint; 22/80/443 are reserved |
| - and ungrantable) |
| - -host <h> -hibernate[=false]: hibernate when idle |
| - (refused for a route whose release declares the |
| - daemon capability) |
| - -host <h> -backing <unit|"">: who owns this |
| - route's process — a carlos-app@<host> systemd |
| - unit, or an exec child of the agent (the |
| - migration's one write; grants need unit backing) |
| - auth login | whoami | logout — who this terminal is |
| - (headless? don't log in: pass -owner instead) |
| - accounts create | list | migrate — the tenancy primitive |
| - apps create | delete | restore — claim a new app |
| - name (console API only, rate limited per |
| - address per day; the console form is not), |
| - trash an app for 30 days (routes drop |
| - everywhere; refused while flagged production), |
| - or undo that within the window (no routes come |
| - back; carlos add <host> after); delete/restore |
| - run signed-in via the console or headless |
| - against the bucket (-account-id required |
| - headless); no purge here — "Delete now" is |
| - console-only |
| - env set | unset | list — plain per-app config vars |
| - secrets set | unset | list | genkey — sealed per-app secrets |
| - ship | promote | channels publish and promote releases (-account <sqid|name>) |
| - promote climbs the ladder canary -> edge -> beta |
| - -> stable; -hotfix is the recorded bypass |
| - ledger append | head | list | publish | blob | verify — |
| - the open-ledger primitive (carlos ledger -h for |
| - the full usage); every sub but "verify -url" |
| - goes through the console API, never bucket |
| - credentials — verify -url needs no credentials |
| - at all |
| - release-keygen mint the deployment's root signing key, or with |
| - -scope <rungs> -name <label> [-expires <dur>] a |
| - SCOPED key that can only sign those rungs |
| - (stable is never delegable; needs the root key |
| - in CARLOS_RELEASE_KEY to sign the grant) |
| - adopt run one release-adoption pass (the hourly timer's verb) |
| - canary ship this worktree to an ephemeral canary channel |
| - bootstrap prepare a host (units, dirs, user) |
| - ops litestream | restore-verify |
| - status -dry-run <dir>: fleet status page leak self-check |
| - (live publish has no CLI mode; the agent owns it) |
| - vet check an app release against the platform contract`) |
| +One binary, three roles. "carlos <command> -h" prints a command's own flags. |
| + |
| +Your apps — ship and run software on a CARLOS deployment |
| + auth log in, see who this terminal is, log out |
| + apps create | delete | restore an app name |
| + ship publish a release of an app |
| + promote move a version up the ladder: canary → edge → beta → stable |
| + rollback point a channel back at an earlier version |
| + channels show what each of an app's channels is serving |
| + env plain per-app config vars: set | unset | list |
| + secrets sealed per-app secrets: set | unset | list | genkey |
| + instances an app's instances: enable | create | list | delete |
| + ledger open ledgers: append | head | list | publish | blob | verify |
| + vet check a release against the platform contract |
| + |
| +Your deployment — stand up and administer boxes, routes and accounts |
| + bootstrap prepare a host: units, dirs, user |
| + accounts the tenancy primitive: create | list | migrate |
| + add route a hostname on this box to an app |
| + remove drop one of this box's routes |
| + route repoint a route's channel or config environment, grant or |
| + revoke a capability, hibernate it, change its backing |
| + instances with no sub-verb: list THIS box's registry routes |
| + release-keygen mint the root signing key, or a scoped signing key |
| + status -live: what every box last reported; -dry-run: leak check |
| + |
| +On the host — what systemd runs; rarely typed by hand |
| + edge the mesh front door (router) |
| + agent edge + activator + release adoption for this host |
| + adopt one release-adoption pass (the hourly timer's verb) |
| + ops box operations: litestream | restore-verify |
| + version print the build id`) |
| } |