eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

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.
15package main
16
17import (
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`.
26type 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.
36var 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().
84func 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.
96func 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.
101func 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).
124func 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