cli: carlos system, with apply wired to the root oneshot
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QCUCpfxbCyEAVQ2m51dH5
4 files changed,
+104
−1
main.go+3 −0system.go+73 −0system_test.go+27 −0update.go+1 −1
diff --git a/main.go b/main.go| index 662b28bf5..defc1514f 100644 |
| --- a/main.go |
| +++ b/main.go |
| @@ -116,6 +116,8 @@ func main() { |
| runKeygen(args) |
| case "ops": |
| ops.Run(args) |
| + case "system": |
| + runSystem(args) |
| case "status": |
| RunStatus(args) |
| case "agent": |
| @@ -229,5 +231,6 @@ On the host โ what systemd runs; rarely typed by hand |
| agent edge + activator + release adoption for this host |
| adopt one release-adoption pass (the hourly timer's verb) |
| ops box operations: litestream | restore-verify |
| + system platform self-update: update | status | rollback (apply is the root oneshot) |
| version print the build id`) |
| } |
diff --git a/system.go b/system.go| new file mode 100644 |
| index 000000000..5ff3c017c |
| --- /dev/null |
| +++ b/system.go |
| @@ -0,0 +1,73 @@ |
| +package main |
| + |
| +import ( |
| + "fmt" |
| + "log" |
| + "os" |
| + |
| + "github.com/carlosframework/platform/internal/host" |
| +) |
| + |
| +// carlos system: what is not an app (spec ยง0, the console-as-an-app |
| +// dividing line). One component today, the router โ the substrate binary. |
| +// `carlos system router update` is the same verb scoped to it, so the |
| +// command Paul named works and a future split needs no new verb. |
| + |
| +const systemComponentRouter = "router" |
| + |
| +func systemVerb(args []string) (verb string, rest []string, component string) { |
| + component = systemComponentRouter |
| + if len(args) > 0 && args[0] == systemComponentRouter { |
| + args = args[1:] |
| + } |
| + if len(args) == 0 { |
| + return "", nil, component |
| + } |
| + switch args[0] { |
| + case "update", "status", "rollback", "apply", "help", "-h", "--help": |
| + return args[0], args[1:], component |
| + } |
| + return "", args, component |
| +} |
| + |
| +func runSystem(args []string) { |
| + verb, rest, _ := systemVerb(args) |
| + switch verb { |
| + case "apply": |
| + host.RunSystemApply(rest) |
| + case "update": |
| + runSystemUpdate(rest) |
| + case "status": |
| + runSystemStatus(rest) |
| + case "rollback": |
| + runSystemRollback(rest) |
| + case "help", "-h", "--help": |
| + systemUsage() |
| + default: |
| + if len(args) > 0 && verb == "" { |
| + fmt.Fprintf(os.Stderr, "system: unknown verb %q\n\n", args[0]) |
| + } |
| + systemUsage() |
| + os.Exit(2) |
| + } |
| +} |
| + |
| +func systemUsage() { |
| + fmt.Fprint(os.Stderr, `usage: carlos system <verb> [flags] |
| + carlos system router <verb> [flags] |
| + |
| + update promote a platform release to a channel; every box on it rolls itself |
| + status every box: platform, channel, running build, channel target, last apply outcome |
| + rollback point a channel back at the release before this one |
| + apply (root oneshot behind carlos-system-update.service; not for operators) |
| + |
| +The platform is not an app: the console is updated with carlos deploy --app console. |
| +`) |
| +} |
| + |
| +// Placeholders until Task 12. |
| +func runSystemUpdate(args []string) { log.Fatal("carlos system update: not yet wired to the console") } |
| +func runSystemStatus(args []string) { log.Fatal("carlos system status: not yet wired to the console") } |
| +func runSystemRollback(args []string) { |
| + log.Fatal("carlos system rollback: not yet wired to the console") |
| +} |
diff --git a/system_test.go b/system_test.go| new file mode 100644 |
| index 000000000..1ece63463 |
| --- /dev/null |
| +++ b/system_test.go |
| @@ -0,0 +1,27 @@ |
| +package main |
| + |
| +import "testing" |
| + |
| +func TestSystemVerbAcceptsTheComponentForm(t *testing.T) { |
| + cases := []struct { |
| + in []string |
| + verb, comp string |
| + rest int |
| + }{ |
| + {[]string{"update", "--channel", "canary"}, "update", "router", 2}, |
| + {[]string{"router", "update"}, "update", "router", 0}, |
| + {[]string{"router", "status"}, "status", "router", 0}, |
| + {[]string{"apply", "--recover"}, "apply", "router", 1}, |
| + {[]string{}, "", "router", 0}, |
| + {[]string{"router"}, "", "router", 0}, |
| + } |
| + for _, c := range cases { |
| + verb, rest, comp := systemVerb(c.in) |
| + if verb != c.verb || comp != c.comp || len(rest) != c.rest { |
| + t.Errorf("systemVerb(%v) = %q %v %q", c.in, verb, rest, comp) |
| + } |
| + } |
| + if verb, _, _ := systemVerb([]string{"console", "update"}); verb != "" { |
| + t.Errorf("an unknown component must not be a verb: got %q", verb) |
| + } |
| +} |
diff --git a/update.go b/update.go| index 8730335fa..e13ccf31c 100644 |
| --- a/update.go |
| +++ b/update.go |
| @@ -75,7 +75,7 @@ var noHintVerbs = map[string]bool{ |
| "add": true, "remove": true, "route": true, "ops": true, |
| "status": true, "vet": true, "release-keygen": true, |
| "version": true, "help": true, "-h": true, "--help": true, |
| - "update": true, |
| + "update": true, "system": true, |
| } |
| type updateCache struct { |