carlos / platform Public

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
Paul Campbell pushed by paul@keymail.dev deab7b64911358448939a7ee905363af9059e6bc parent c6447b0
4 files changed, +104 −1
  • main.go +3 −0
  • system.go +73 −0
  • system_test.go +27 −0
  • update.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 {