| 1 | // carlos is the CARLOS platform binary: edge, host agent, and operator CLI |
| 2 | // in one artifact, selected by subcommand. |
| 3 | package main |
| 4 | |
| 5 | import ( |
| 6 | "fmt" |
| 7 | "io" |
| 8 | "log" |
| 9 | "os" |
| 10 | "strings" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/cgroup" |
| 13 | "github.com/carlosframework/platform/internal/edge" |
| 14 | "github.com/carlosframework/platform/internal/host" |
| 15 | "github.com/carlosframework/platform/internal/instanceexec" |
| 16 | "github.com/carlosframework/platform/internal/rootaws" |
| 17 | "github.com/carlosframework/platform/internal/version" |
| 18 | "github.com/carlosframework/platform/internal/vet" |
| 19 | ) |
| 20 | |
| 21 | func main() { |
| 22 | // The placement shim (internal/cgroup, #272) must run before anything |
| 23 | // else touches the environment or the logger: a shim process carries |
| 24 | // none of the tenant's settings and exits from inside this call. |
| 25 | cgroup.RunShimIfRequested() |
| 26 | rootaws.ScrubEnvironment() |
| 27 | // Failure is reported through log.Fatal everywhere in this binary, so |
| 28 | // the logger is where the "this went wrong" colour goes — once, rather |
| 29 | // than at a few hundred call sites. Off unless stderr is a terminal, so |
| 30 | // journald and CI logs see exactly what they saw before. |
| 31 | log.SetOutput(newErrorWriter(os.Stderr)) |
| 32 | if len(os.Args) < 2 { |
| 33 | usage(os.Stderr) |
| 34 | os.Exit(2) |
| 35 | } |
| 36 | cmd, args := os.Args[1], os.Args[2:] |
| 37 | if cmd == "__registry-child" { |
| 38 | if err := host.RunRegistryChild(args); err != nil { |
| 39 | log.Fatal(err) |
| 40 | } |
| 41 | return |
| 42 | } |
| 43 | switch cmd { |
| 44 | // Asking for help is not an error: it prints on stdout and exits 0, so |
| 45 | // `carlos help | less` works. A bare `carlos` still fails (stderr, 2). |
| 46 | case "help", "-h", "--help": |
| 47 | usage(os.Stdout) |
| 48 | case "version": |
| 49 | // `carlos version` on its own is the build id and nothing else — |
| 50 | // unchanged, because every install script and support request in |
| 51 | // existence types it. `carlos version target ...` is the only string |
| 52 | // that dispatches anywhere new (release versioning spec, 2026-08-08): |
| 53 | // a sub-verb rather than a top-level `carlos target`, because what it |
| 54 | // sets IS the app's version, and it reads that way at the terminal. |
| 55 | if len(args) > 0 && args[0] == "target" { |
| 56 | runVersionTarget(args[1:]) |
| 57 | } else { |
| 58 | fmt.Printf("carlos %s\n", version.Build) |
| 59 | } |
| 60 | case "edge": |
| 61 | edge.Run(args) |
| 62 | case "add": |
| 63 | runAdd(args) |
| 64 | case "remove": |
| 65 | runRemove(args) |
| 66 | case "route": |
| 67 | runRoute(args) |
| 68 | case "instances": |
| 69 | runInstances(args) |
| 70 | case "sidecars": |
| 71 | runSidecars(args) |
| 72 | case "routes": |
| 73 | runRoutes(args) |
| 74 | case "domains": |
| 75 | runDomains(args) |
| 76 | case "features": |
| 77 | runFeatures(args) |
| 78 | case "gate": |
| 79 | runGate(args) |
| 80 | case "ship": |
| 81 | runShip(args) |
| 82 | case "promote": |
| 83 | runPromote(args) |
| 84 | case "deploy": |
| 85 | runDeploy(args) |
| 86 | case "restart": |
| 87 | runRestart(args) |
| 88 | case "db": |
| 89 | runDB(args) |
| 90 | case "run": |
| 91 | runRun(args) |
| 92 | case "steering": |
| 93 | runSteering(args) |
| 94 | case "schedule": |
| 95 | runSchedule(args) |
| 96 | case "economics": |
| 97 | runEconomics(args) |
| 98 | case "store": |
| 99 | runStore(args) |
| 100 | case "relay": |
| 101 | runRelay(args) |
| 102 | case "email": |
| 103 | runEmail(args) |
| 104 | case "logs": |
| 105 | runLogs(args) |
| 106 | case "errors": |
| 107 | runErrors(args) |
| 108 | case "availability": |
| 109 | runAvailability(args) |
| 110 | case "geo": |
| 111 | runGeo(args) |
| 112 | case "analytics": |
| 113 | runAnalytics(args) |
| 114 | case "channels": |
| 115 | // Two sub-verbs, resign and digests (#253); bare `carlos channels` |
| 116 | // still lists, the `carlos releases retention` precedent. |
| 117 | if len(args) > 0 && args[0] == "resign" { |
| 118 | runChannelsResign(args[1:]) |
| 119 | return |
| 120 | } |
| 121 | if len(args) > 0 && args[0] == "digests" { |
| 122 | runChannelsDigests(args[1:]) |
| 123 | return |
| 124 | } |
| 125 | runChannels(args) |
| 126 | case "pipeline": |
| 127 | runPipeline(args) |
| 128 | case "skills": |
| 129 | runSkills(args) |
| 130 | case "releases": |
| 131 | // Two sub-verbs, `retention` and `authorize` — the `carlos version |
| 132 | // target` judgment call: flags-only verbs stay flat until a word |
| 133 | // genuinely names a different action, and a policy write or a |
| 134 | // signature is not a listing. |
| 135 | if len(args) > 0 && args[0] == "retention" { |
| 136 | runReleasesRetention(args[1:]) |
| 137 | return |
| 138 | } |
| 139 | if len(args) > 0 && args[0] == "authorize" { |
| 140 | runReleasesAuthorize(args[1:]) |
| 141 | return |
| 142 | } |
| 143 | runReleases(args) |
| 144 | case "rollback": |
| 145 | runRollback(args) |
| 146 | case "ledger": |
| 147 | RunLedger(args) |
| 148 | case "auth": |
| 149 | RunAuth(args) |
| 150 | case "config": |
| 151 | RunConfig(args) |
| 152 | case "accounts": |
| 153 | RunAccounts(args) |
| 154 | case "apps": |
| 155 | RunApps(args) |
| 156 | case "fleets": |
| 157 | RunFleets(args) |
| 158 | case "services": |
| 159 | RunServices(args) |
| 160 | case "env": |
| 161 | RunEnv(args) |
| 162 | case "secrets": |
| 163 | RunSecrets(args) |
| 164 | case "release-keygen": |
| 165 | runKeygen(args) |
| 166 | case "proof": |
| 167 | runProof(args) |
| 168 | case "ops": |
| 169 | runOps(args) |
| 170 | case "system": |
| 171 | runSystem(args) |
| 172 | case "move": |
| 173 | runMove(args) |
| 174 | case "status": |
| 175 | RunStatus(args) |
| 176 | case "agent": |
| 177 | host.RunAgent(args) |
| 178 | case "bootstrap": |
| 179 | host.RunBootstrap(args) |
| 180 | case "adopt": |
| 181 | host.RunAdopt(args) |
| 182 | case "instance-control": |
| 183 | host.RunInstanceControl(args) |
| 184 | case "instance-exec": |
| 185 | instanceexec.RunExec(args) |
| 186 | case "instance-exit": |
| 187 | instanceexec.RunExit(args) |
| 188 | case "instance-probe": |
| 189 | instanceexec.RunProbe(args) |
| 190 | case "vet": |
| 191 | vet.Run(args) |
| 192 | case "update": |
| 193 | runUpdate(args) |
| 194 | case "canary": |
| 195 | runCanary(args) |
| 196 | default: |
| 197 | usage(os.Stderr) |
| 198 | os.Exit(2) |
| 199 | } |
| 200 | // After a verb that ran to success: the once-a-day newer-release hint |
| 201 | // (#161). Verbs that fail never reach here — log.Fatal exits — so a |
| 202 | // broken command is never followed by an upsell. |
| 203 | maybeUpdateHint(cmd) |
| 204 | } |
| 205 | |
| 206 | // usage prints the top-level command list, grouped by the role you are in |
| 207 | // when you type the command: shipping your own app, standing up and |
| 208 | // administering a deployment, or the verbs a host runs on itself. The |
| 209 | // one-liners here are deliberately thin — every command prints its own full |
| 210 | // usage (bare, or with -h), and that is where the detail belongs. A flat |
| 211 | // alphabetical list of every verb with every caveat inline was the previous |
| 212 | // shape, and it read as a wall rather than as an offer of what to do next. |
| 213 | // moveUsageLine is the move verb's line of the usage (MV-N363), one literal |
| 214 | // so the copy gate reads it whole. It prints wrapped like its neighbours. |
| 215 | const moveUsageLine = "move an instance to another region: status | cut | abort | resolve --retry (operators; off unless the console enables it)" |
| 216 | |
| 217 | func moveUsageWrapped() string { |
| 218 | return strings.Replace(moveUsageLine, "abort | ", "abort |\n ", 1) |
| 219 | } |
| 220 | |
| 221 | func usage(w io.Writer) { |
| 222 | fmt.Fprintln(w, `usage: carlos <command> [flags] |
| 223 | |
| 224 | One binary, three roles. "carlos <command> -h" prints a command's own |
| 225 | flags; --app, the flag on nearly every app verb, shortens to -a. |
| 226 | |
| 227 | Your apps — ship and run software on a CARLOS deployment |
| 228 | auth log in, see who this terminal is, log out |
| 229 | config edit .carlos/config: "config destinations" lists, adds, |
| 230 | changes and removes the named places -d deploys to |
| 231 | apps create | place | delete | restore an app name — --place |
| 232 | fleet/<f> claims and places in one step, and "apps place" |
| 233 | repoints an app's routes and instances onto a customer |
| 234 | fleet or back to "" |
| 235 | ship publish a release of an app — --label and --notes name it, |
| 236 | and --version is optional once a version target is set |
| 237 | promote move a shipped version onto one of the app's own channels — |
| 238 | the channel is optional when the app has only one |
| 239 | ("carlos channels" lists them; "carlos pipeline" orders them) |
| 240 | deploy ship + promote + wait until the URL serves the new version |
| 241 | canary the same, onto a host and channel of this branch's own, so |
| 242 | nothing the app already serves changes: deploy | ls | rm |
| 243 | restart cycle an app's instances — no new version, no config change; |
| 244 | --host or --environment narrows it, --dry-run names the set |
| 245 | db read a copy of an instance's database, restored from its |
| 246 | replica: pull (save it) | query (one read-only query) |
| 247 | run run one of your app's commands against one instance, as that instance |
| 248 | schedule recurring or one-shot work per app: ls | set | rm | run |
| 249 | store object storage: create | status | apply | approvals | approve | decline | grant | rotate |
| 250 | relay the platform STUN/TURN relay: serve (what the unit runs) |
| 251 | and keygen (mint a region master) |
| 252 | email tenant email sending: enable | domains add | status | test | |
| 253 | credentials create/list/revoke | rotate | pause | resume |
| 254 | rollback point a channel back at an earlier version |
| 255 | channels show what each of an app's channels is serving |
| 256 | pipeline show or shape an app's release channels |
| 257 | releases every version shipped, newest first, with labels and tags |
| 258 | version target the semver ship counts iterations under: print it, or set |
| 259 | it — "carlos version target --app <a> 0.5.0" |
| 260 | env plain per-app config vars: set | unset | list |
| 261 | secrets sealed per-app secrets: set | unset | list | genkey |
| 262 | instances an app's instances: enable | create | list | delete | |
| 263 | set-upstreams | set-channel — set-channel is the only |
| 264 | door onto which channel a route follows |
| 265 | sidecars a process nothing dials, one per enrolled member: enable | |
| 266 | create | ls | wake | rm | logs |
| 267 | steering opt an instance into DNS latency steering, or back off — |
| 268 | --app <a> --host <h> latency|off (shared pool only, v1) |
| 269 | routes an app's routes and what each is bound to: upstream, |
| 270 | channel, config environment, status |
| 271 | domains a customer's own hostname attached to an app's route: |
| 272 | attach | detach | list |
| 273 | features an app's own feature flags, stored and served by the |
| 274 | platform, never interpreted: set | list |
| 275 | gate a shared password in front of an app's hostnames |
| 276 | logs an app's merged log timeline: its own output plus platform |
| 277 | events (wakes, restarts, failures); -f follows live |
| 278 | errors error groups for an app: log lines at the set level plus |
| 279 | 500s the edge saw |
| 280 | availability requests an app's instances could not answer (502/503/504), |
| 281 | grouped by cause, plus failed wakes and exits |
| 282 | analytics an app's page views and visitors from a terminal, counted at |
| 283 | the edge as it serves your app |
| 284 | geo what the platform's GeoIP database says about an address |
| 285 | ledger open ledgers: append | head | list | publish | blob | verify |
| 286 | vet check a release against the platform contract |
| 287 | update update this carlos binary to the latest release (or name |
| 288 | the brew/apt command when a package manager owns it); |
| 289 | -y skips the prompt, terminal still required |
| 290 | skills the platform's published agent skills: list them, or print one — |
| 291 | "carlos skills getting-started" pipes the recipe to an agent |
| 292 | |
| 293 | Your deployment — stand up and administer boxes, routes and accounts |
| 294 | bootstrap prepare a host: units, dirs, user |
| 295 | accounts the tenancy primitive: create | list | migrate |
| 296 | fleets account-owned groups of remote boxes that dial this |
| 297 | console: create | add-box | rotate-token | tag-box | |
| 298 | detach-box | ls — box tokens print exactly once, at mint |
| 299 | or rotate, and a compromised box needs detach-box, not |
| 300 | rotate-token |
| 301 | services credentials a SERVER holds, not a person: create | ls | |
| 302 | rotate | revoke, owner-only and closed to service |
| 303 | credentials themselves — secrets print exactly once, and |
| 304 | a compromised one needs revoke, not rotate |
| 305 | add route a hostname on this box to an app |
| 306 | remove drop one of this box's routes |
| 307 | route repoint a route's channel or config environment, grant or |
| 308 | revoke a capability, hibernate it, change its backing |
| 309 | instances with no sub-verb: list THIS box's registry routes |
| 310 | release-keygen mint the root signing key, or a scoped signing key |
| 311 | proof dev proofs a box requires before adopting: keygen | get | put |
| 312 | economics bill: record the month's AWS bill; backhaul: what region |
| 313 | edges cost us this period |
| 314 | status --live: what every box last reported; --dry-run: leak check |
| 315 | move `+moveUsageWrapped()+` |
| 316 | |
| 317 | On the host — what systemd runs; rarely typed by hand |
| 318 | edge the mesh front door (router) |
| 319 | agent edge + activator + release adoption for this host |
| 320 | adopt one release-adoption pass (the hourly timer's verb) |
| 321 | ops box operations: litestream | restore-verify |
| 322 | system platform self-update: update | status | rollback (apply is the |
| 323 | root oneshot); antivirus: deployment-wide object scanning |
| 324 | version print the build id`) |
| 325 | } |
| 326 | |