| 1 | // π€ The `lchat` command surface: the subcommand list `lchat help` prints, and |
| 2 | // the guard that stops an unrecognised word from silently starting a server. |
| 3 | // |
| 4 | // Why the guard exists: main()'s dispatch falls through to "run a single |
| 5 | // instance" for anything it doesn't recognise, because a bare `lchat` plus |
| 6 | // flags is a valid, supported invocation. That is right for flags and wrong |
| 7 | // for words β a typo (`lchat routes`, `lchat help`) used to boot a full |
| 8 | // messenger server rather than say "no such command". Usually it died on a |
| 9 | // port clash and looked like a confusing error; when the port was free it kept |
| 10 | // running, which is how a stray `lchat help` process sat on a production box |
| 11 | // for five days. |
| 12 | // |
| 13 | // So: a first argument starting with "-" is a flag (fall through, unchanged), |
| 14 | // and anything else must name a subcommand or we print usage and exit 2. |
| 15 | package main |
| 16 | |
| 17 | import ( |
| 18 | "fmt" |
| 19 | "io" |
| 20 | "os" |
| 21 | "strings" |
| 22 | ) |
| 23 | |
| 24 | // subcommand is one row of `lchat help`. The blurb is one line, imperative, |
| 25 | // and describes the verb β flags belong to `lchat <name> -h`. |
| 26 | type subcommand struct{ name, blurb string } |
| 27 | |
| 28 | // subcommands is what `lchat help` prints, in the order it prints them β |
| 29 | // grouped by what an operator is trying to do rather than alphabetically, |
| 30 | // since the list is long enough that alphabetical hides the shape. |
| 31 | // |
| 32 | // main()'s switch is the REAL dispatcher; this is only the human-facing list. |
| 33 | // TestUsageCoversEverySubcommand reads main.go and pins the two together, so a |
| 34 | // new `case` that nobody documents (or a documented verb that doesn't |
| 35 | // dispatch) fails the tests rather than drifting quietly. |
| 36 | var subcommands = []subcommand{ |
| 37 | // Long-running processes: the front door and the sibling services. |
| 38 | {"router", "run the front door (TLS termination + host routing)"}, |
| 39 | {"controld", "run the local control daemon (root-only provisioning API)"}, |
| 40 | {"cloud", "run the public self-serve space provisioner"}, |
| 41 | {"tunnel", "run the trial-tunnel gateway that fronts `lchat try`"}, |
| 42 | {"links", "run the short-link redirector (`links run`)"}, |
| 43 | {"fetch", "run the public-web fetch service: unfurl + signed image proxy (`fetch run`)"}, |
| 44 | {"hooks", "run the webhook-ingest sidecar (`hooks run`)"}, |
| 45 | {"bot", "run the bot sidecar (`bot run`)"}, |
| 46 | |
| 47 | // Standing a box up, and putting things on it. |
| 48 | {"bootstrap", "install/refresh this box's systemd units, /data, and the router"}, |
| 49 | {"add-instance", "create a chat instance and route it"}, |
| 50 | {"add-service", "front an already-running backend on a unix socket"}, |
| 51 | {"add-bot", "provision an instance's bot sidecar"}, |
| 52 | {"add-hooks", "provision an instance's webhook-ingest sidecar"}, |
| 53 | |
| 54 | // Shipping code: the versioned, side-by-side deploy path. |
| 55 | {"stage", "register a built binary in the version store"}, |
| 56 | {"install", "put a staged build onto one instance without restarting it"}, |
| 57 | {"deploy", "repoint an instance at a version and restart (health-checked)"}, |
| 58 | {"rollback", "step an instance back to its previous version"}, |
| 59 | {"init-versions", "one-time adoption of the version store on an old box"}, |
| 60 | {"update", "the release-channel auto-updater (`update status`)"}, |
| 61 | {"release", "the signing side of the release feed (keygen/sign/verify)"}, |
| 62 | |
| 63 | // Looking after what's already running. |
| 64 | {"instances", "list every instance and service on this box"}, |
| 65 | {"env", "amend an instance's env in place (`env set <host>|--all KEY=VALUE`)"}, |
| 66 | {"remove", "remove an instance (deliberately leaves /data behind)"}, |
| 67 | {"reset", "archive a canary's data (canary-flag gated)"}, |
| 68 | {"canary", "set/clear an instance's throwaway-canary flag"}, |
| 69 | {"hibernate", "flip an instance between always-on and hibernating"}, |
| 70 | {"replication", "read or set one instance's replication class"}, |
| 71 | {"relocate", "move an instance to another region"}, |
| 72 | {"mesh-seed", "bulk-replicate every space from a peer box"}, |
| 73 | {"admin-link", "print an instance's admin link"}, |
| 74 | {"setup-invite", "mint an instance's one-time setup link"}, |
| 75 | |
| 76 | // Local development. |
| 77 | {"try", "expose a local server at a real public origin"}, |
| 78 | {"relay", "run a space through the tunnel gateway"}, |
| 79 | {"version", "print this binary's build id"}, |
| 80 | } |
| 81 | |
| 82 | // isSubcommand reports whether arg names a subcommand. Kept separate from the |
| 83 | // dispatch switch so it is testable without running main(). |
| 84 | func isSubcommand(arg string) bool { |
| 85 | for _, c := range subcommands { |
| 86 | if c.name == arg { |
| 87 | return true |
| 88 | } |
| 89 | } |
| 90 | return false |
| 91 | } |
| 92 | |
| 93 | // looksLikeFlag reports whether arg is a flag rather than a mistyped |
| 94 | // subcommand. A bare "-" is not a flag β nothing here reads stdin, so it is a |
| 95 | // typo, and naming it as one beats letting the flag package complain. |
| 96 | func looksLikeFlag(arg string) bool { return strings.HasPrefix(arg, "-") && arg != "-" } |
| 97 | |
| 98 | // writeUsage prints the command surface. Instance flags are deliberately NOT |
| 99 | // listed: they belong to the no-subcommand form, and `lchat -h` still prints |
| 100 | // them via the flag package exactly as it always has. |
| 101 | func writeUsage(w io.Writer) { |
| 102 | fmt.Fprint(w, "lchat β the Eleven messenger server, router, and operator CLI.\n\n"+ |
| 103 | "Usage:\n"+ |
| 104 | " lchat <command> [flags] run a command (see below)\n"+ |
| 105 | " lchat [flags] run a single instance directly (`lchat -h` for its flags)\n\n"+ |
| 106 | "Commands:\n") |
| 107 | width := 0 |
| 108 | for _, c := range subcommands { |
| 109 | if len(c.name) > width { |
| 110 | width = len(c.name) |
| 111 | } |
| 112 | } |
| 113 | for _, c := range subcommands { |
| 114 | fmt.Fprintf(w, " %-*s %s\n", width, c.name, c.blurb) |
| 115 | } |
| 116 | fmt.Fprint(w, "\nRun `lchat <command> -h` for a command's own flags.\n") |
| 117 | } |
| 118 | |
| 119 | // checkSubcommand guards main()'s dispatch. It is called only when the switch |
| 120 | // found no match, so reaching here with a non-flag word means the word names |
| 121 | // nothing β print usage and exit rather than falling through and starting a |
| 122 | // server. `help` is handled here too (it is a word, not a flag, so it used to |
| 123 | // boot a server as well). |
| 124 | func checkSubcommand(arg string) { |
| 125 | if arg == "help" { |
| 126 | writeUsage(os.Stdout) |
| 127 | os.Exit(0) |
| 128 | } |
| 129 | if looksLikeFlag(arg) { |
| 130 | return // a flag: fall through to the single-instance form, unchanged |
| 131 | } |
| 132 | fmt.Fprintf(os.Stderr, "lchat: unknown command %q\n\n", arg) |
| 133 | writeUsage(os.Stderr) |
| 134 | os.Exit(2) |
| 135 | } |
| 136 | |