// carlos is the CARLOS platform binary: edge, host agent, and operator CLI // in one artifact, selected by subcommand. package main import ( "fmt" "io" "log" "os" "strings" "github.com/carlosframework/platform/internal/cgroup" "github.com/carlosframework/platform/internal/edge" "github.com/carlosframework/platform/internal/host" "github.com/carlosframework/platform/internal/instanceexec" "github.com/carlosframework/platform/internal/rootaws" "github.com/carlosframework/platform/internal/version" "github.com/carlosframework/platform/internal/vet" ) func main() { // The placement shim (internal/cgroup, #272) must run before anything // else touches the environment or the logger: a shim process carries // none of the tenant's settings and exits from inside this call. cgroup.RunShimIfRequested() rootaws.ScrubEnvironment() // Failure is reported through log.Fatal everywhere in this binary, so // the logger is where the "this went wrong" colour goes — once, rather // than at a few hundred call sites. Off unless stderr is a terminal, so // journald and CI logs see exactly what they saw before. log.SetOutput(newErrorWriter(os.Stderr)) if len(os.Args) < 2 { usage(os.Stderr) os.Exit(2) } cmd, args := os.Args[1], os.Args[2:] if cmd == "__registry-child" { if err := host.RunRegistryChild(args); err != nil { log.Fatal(err) } return } 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": // `carlos version` on its own is the build id and nothing else — // unchanged, because every install script and support request in // existence types it. `carlos version target ...` is the only string // that dispatches anywhere new (release versioning spec, 2026-08-08): // a sub-verb rather than a top-level `carlos target`, because what it // sets IS the app's version, and it reads that way at the terminal. if len(args) > 0 && args[0] == "target" { runVersionTarget(args[1:]) } else { fmt.Printf("carlos %s\n", version.Build) } case "edge": edge.Run(args) case "add": runAdd(args) case "remove": runRemove(args) case "route": runRoute(args) case "instances": runInstances(args) case "sidecars": runSidecars(args) case "routes": runRoutes(args) case "domains": runDomains(args) case "features": runFeatures(args) case "gate": runGate(args) case "ship": runShip(args) case "promote": runPromote(args) case "deploy": runDeploy(args) case "restart": runRestart(args) case "db": runDB(args) case "run": runRun(args) case "steering": runSteering(args) case "schedule": runSchedule(args) case "economics": runEconomics(args) case "store": runStore(args) case "relay": runRelay(args) case "email": runEmail(args) case "logs": runLogs(args) case "errors": runErrors(args) case "availability": runAvailability(args) case "geo": runGeo(args) case "analytics": runAnalytics(args) case "channels": // Two sub-verbs, resign and digests (#253); bare `carlos channels` // still lists, the `carlos releases retention` precedent. if len(args) > 0 && args[0] == "resign" { runChannelsResign(args[1:]) return } if len(args) > 0 && args[0] == "digests" { runChannelsDigests(args[1:]) return } runChannels(args) case "pipeline": runPipeline(args) case "skills": runSkills(args) case "releases": // Two sub-verbs, `retention` and `authorize` — the `carlos version // target` judgment call: flags-only verbs stay flat until a word // genuinely names a different action, and a policy write or a // signature is not a listing. if len(args) > 0 && args[0] == "retention" { runReleasesRetention(args[1:]) return } if len(args) > 0 && args[0] == "authorize" { runReleasesAuthorize(args[1:]) return } runReleases(args) case "rollback": runRollback(args) case "ledger": RunLedger(args) case "auth": RunAuth(args) case "config": RunConfig(args) case "accounts": RunAccounts(args) case "apps": RunApps(args) case "fleets": RunFleets(args) case "services": RunServices(args) case "env": RunEnv(args) case "secrets": RunSecrets(args) case "release-keygen": runKeygen(args) case "proof": runProof(args) case "ops": runOps(args) case "system": runSystem(args) case "move": runMove(args) case "status": RunStatus(args) case "agent": host.RunAgent(args) case "bootstrap": host.RunBootstrap(args) case "adopt": host.RunAdopt(args) case "instance-control": host.RunInstanceControl(args) case "instance-exec": instanceexec.RunExec(args) case "instance-exit": instanceexec.RunExit(args) case "instance-probe": instanceexec.RunProbe(args) case "vet": vet.Run(args) case "update": runUpdate(args) case "canary": runCanary(args) default: usage(os.Stderr) os.Exit(2) } // After a verb that ran to success: the once-a-day newer-release hint // (#161). Verbs that fail never reach here — log.Fatal exits — so a // broken command is never followed by an upsell. maybeUpdateHint(cmd) } // 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. // moveUsageLine is the move verb's line of the usage (MV-N363), one literal // so the copy gate reads it whole. It prints wrapped like its neighbours. const moveUsageLine = "move an instance to another region: status | cut | abort | resolve --retry (operators; off unless the console enables it)" func moveUsageWrapped() string { return strings.Replace(moveUsageLine, "abort | ", "abort |\n ", 1) } func usage(w io.Writer) { fmt.Fprintln(w, `usage: carlos [flags] One binary, three roles. "carlos -h" prints a command's own flags; --app, the flag on nearly every app verb, shortens to -a. Your apps — ship and run software on a CARLOS deployment auth log in, see who this terminal is, log out config edit .carlos/config: "config destinations" lists, adds, changes and removes the named places -d deploys to apps create | place | delete | restore an app name — --place fleet/ claims and places in one step, and "apps place" repoints an app's routes and instances onto a customer fleet or back to "" ship publish a release of an app — --label and --notes name it, and --version is optional once a version target is set promote move a shipped version onto one of the app's own channels — the channel is optional when the app has only one ("carlos channels" lists them; "carlos pipeline" orders them) deploy ship + promote + wait until the URL serves the new version canary the same, onto a host and channel of this branch's own, so nothing the app already serves changes: deploy | ls | rm restart cycle an app's instances — no new version, no config change; --host or --environment narrows it, --dry-run names the set db read a copy of an instance's database, restored from its replica: pull (save it) | query (one read-only query) run run one of your app's commands against one instance, as that instance schedule recurring or one-shot work per app: ls | set | rm | run store object storage: create | status | apply | approvals | approve | decline | grant | rotate relay the platform STUN/TURN relay: serve (what the unit runs) and keygen (mint a region master) email tenant email sending: enable | domains add | status | test | credentials create/list/revoke | rotate | pause | resume rollback point a channel back at an earlier version channels show what each of an app's channels is serving pipeline show or shape an app's release channels releases every version shipped, newest first, with labels and tags version target the semver ship counts iterations under: print it, or set it — "carlos version target --app 0.5.0" 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 | set-upstreams | set-channel — set-channel is the only door onto which channel a route follows sidecars a process nothing dials, one per enrolled member: enable | create | ls | wake | rm | logs steering opt an instance into DNS latency steering, or back off — --app --host latency|off (shared pool only, v1) routes an app's routes and what each is bound to: upstream, channel, config environment, status domains a customer's own hostname attached to an app's route: attach | detach | list features an app's own feature flags, stored and served by the platform, never interpreted: set | list gate a shared password in front of an app's hostnames logs an app's merged log timeline: its own output plus platform events (wakes, restarts, failures); -f follows live errors error groups for an app: log lines at the set level plus 500s the edge saw availability requests an app's instances could not answer (502/503/504), grouped by cause, plus failed wakes and exits analytics an app's page views and visitors from a terminal, counted at the edge as it serves your app geo what the platform's GeoIP database says about an address ledger open ledgers: append | head | list | publish | blob | verify vet check a release against the platform contract update update this carlos binary to the latest release (or name the brew/apt command when a package manager owns it); -y skips the prompt, terminal still required skills the platform's published agent skills: list them, or print one — "carlos skills getting-started" pipes the recipe to an agent Your deployment — stand up and administer boxes, routes and accounts bootstrap prepare a host: units, dirs, user accounts the tenancy primitive: create | list | migrate fleets account-owned groups of remote boxes that dial this console: create | add-box | rotate-token | tag-box | detach-box | ls — box tokens print exactly once, at mint or rotate, and a compromised box needs detach-box, not rotate-token services credentials a SERVER holds, not a person: create | ls | rotate | revoke, owner-only and closed to service credentials themselves — secrets print exactly once, and a compromised one needs revoke, not rotate 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 proof dev proofs a box requires before adopting: keygen | get | put economics bill: record the month's AWS bill; backhaul: what region edges cost us this period status --live: what every box last reported; --dry-run: leak check move `+moveUsageWrapped()+` 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 system platform self-update: update | status | rollback (apply is the root oneshot); antivirus: deployment-wide object scanning version print the build id`) }