spawnfile 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/README.md +21 -3
  2. package/dist/auth/runtimeCredentials.js +18 -2
  3. package/dist/cli/devCommands.d.ts +3 -0
  4. package/dist/cli/devCommands.js +115 -0
  5. package/dist/cli/lifecycleCommands.js +103 -1
  6. package/dist/cli/modelCommands.js +3 -0
  7. package/dist/cli/resolveCommandInput.d.ts +19 -0
  8. package/dist/cli/resolveCommandInput.js +25 -0
  9. package/dist/cli/runCli.d.ts +10 -1
  10. package/dist/cli/runCli.js +41 -13
  11. package/dist/cli/runtimeCommands.js +1 -0
  12. package/dist/cli/statusCommand.d.ts +43 -0
  13. package/dist/cli/statusCommand.js +428 -0
  14. package/dist/cli/surfaceCommands.js +4 -0
  15. package/dist/compiler/agentSurfaces.js +2 -4
  16. package/dist/compiler/buildProject.d.ts +9 -1
  17. package/dist/compiler/buildProject.js +44 -5
  18. package/dist/compiler/compileProject.d.ts +2 -0
  19. package/dist/compiler/compileProject.js +22 -11
  20. package/dist/compiler/containerArtifacts.d.ts +1 -0
  21. package/dist/compiler/containerArtifacts.js +208 -32
  22. package/dist/compiler/containerArtifactsPlans.js +7 -4
  23. package/dist/compiler/containerArtifactsRender.d.ts +7 -1
  24. package/dist/compiler/containerArtifactsRender.js +31 -69
  25. package/dist/compiler/containerArtifactsTypes.d.ts +9 -0
  26. package/dist/compiler/containerEntrypointRender.js +22 -32
  27. package/dist/compiler/containerPackageRender.d.ts +15 -0
  28. package/dist/compiler/containerPackageRender.js +63 -0
  29. package/dist/compiler/containerTargetResources.js +3 -1
  30. package/dist/compiler/index.d.ts +1 -0
  31. package/dist/compiler/index.js +1 -0
  32. package/dist/compiler/moltnetArtifacts.js +21 -1
  33. package/dist/compiler/moltnetBinaries.d.ts +4 -1
  34. package/dist/compiler/moltnetBinaries.js +24 -3
  35. package/dist/compiler/moltnetClientConfig.d.ts +1 -1
  36. package/dist/compiler/moltnetClientConfig.js +14 -16
  37. package/dist/compiler/moltnetNodeConfig.js +2 -4
  38. package/dist/compiler/moltnetResolution.js +1 -1
  39. package/dist/compiler/moltnetRoomMemberships.js +1 -2
  40. package/dist/compiler/moltnetRuntimeConfig.d.ts +1 -1
  41. package/dist/compiler/moltnetRuntimeConfig.js +9 -10
  42. package/dist/compiler/networkBinding.d.ts +14 -0
  43. package/dist/compiler/networkBinding.js +34 -0
  44. package/dist/compiler/publishProject.d.ts +13 -0
  45. package/dist/compiler/publishProject.js +72 -0
  46. package/dist/compiler/runProject.d.ts +26 -12
  47. package/dist/compiler/runProject.js +109 -126
  48. package/dist/compiler/runProjectAuth.d.ts +5 -0
  49. package/dist/compiler/runProjectAuth.js +98 -0
  50. package/dist/compiler/runProjectDocker.d.ts +19 -0
  51. package/dist/compiler/runProjectDocker.js +202 -0
  52. package/dist/compiler/types.d.ts +1 -2
  53. package/dist/compiler/upProject.d.ts +6 -0
  54. package/dist/compiler/upProject.js +43 -8
  55. package/dist/compiler/view/buildOrganizationView.js +4 -1
  56. package/dist/compiler/view/declarations.d.ts +4 -0
  57. package/dist/compiler/view/declarations.js +104 -0
  58. package/dist/compiler/view/renderNetworks.js +1 -2
  59. package/dist/compiler/view/renderTree.js +6 -2
  60. package/dist/compiler/view/types.d.ts +64 -1
  61. package/dist/deployment/dockerInspect.d.ts +29 -0
  62. package/dist/deployment/dockerInspect.js +130 -0
  63. package/dist/deployment/dockerLabels.d.ts +16 -0
  64. package/dist/deployment/dockerLabels.js +32 -0
  65. package/dist/deployment/dockerLogs.d.ts +24 -0
  66. package/dist/deployment/dockerLogs.js +96 -0
  67. package/dist/deployment/dockerManager.d.ts +59 -0
  68. package/dist/deployment/dockerManager.js +76 -0
  69. package/dist/deployment/dockerProbeGateway.d.ts +16 -0
  70. package/dist/deployment/dockerProbeGateway.js +48 -0
  71. package/dist/deployment/dockerRecover.d.ts +17 -0
  72. package/dist/deployment/dockerRecover.js +129 -0
  73. package/dist/deployment/homeStore.d.ts +19 -0
  74. package/dist/deployment/homeStore.js +130 -0
  75. package/dist/deployment/index.d.ts +10 -0
  76. package/dist/deployment/index.js +10 -0
  77. package/dist/deployment/names.d.ts +3 -0
  78. package/dist/deployment/names.js +12 -0
  79. package/dist/deployment/record.d.ts +76 -0
  80. package/dist/deployment/record.js +174 -0
  81. package/dist/deployment/target.d.ts +35 -0
  82. package/dist/deployment/target.js +96 -0
  83. package/dist/dev/activity.d.ts +14 -0
  84. package/dist/dev/activity.js +48 -0
  85. package/dist/dev/docker.d.ts +16 -0
  86. package/dist/dev/docker.js +71 -0
  87. package/dist/dev/index.d.ts +2 -0
  88. package/dist/dev/index.js +2 -0
  89. package/dist/dev/project.d.ts +35 -0
  90. package/dist/dev/project.js +164 -0
  91. package/dist/distribution/buildDistributionReport.d.ts +22 -0
  92. package/dist/distribution/buildDistributionReport.js +51 -0
  93. package/dist/distribution/consumeImage.d.ts +29 -0
  94. package/dist/distribution/consumeImage.js +254 -0
  95. package/dist/distribution/consumeImageSupport.d.ts +18 -0
  96. package/dist/distribution/consumeImageSupport.js +63 -0
  97. package/dist/distribution/distributionReportSchema.d.ts +84 -0
  98. package/dist/distribution/distributionReportSchema.js +107 -0
  99. package/dist/distribution/dockerRunner.d.ts +5 -0
  100. package/dist/distribution/dockerRunner.js +21 -0
  101. package/dist/distribution/extractImage.d.ts +25 -0
  102. package/dist/distribution/extractImage.js +96 -0
  103. package/dist/distribution/fingerprint.d.ts +2 -0
  104. package/dist/distribution/fingerprint.js +14 -0
  105. package/dist/distribution/imageRef.d.ts +9 -0
  106. package/dist/distribution/imageRef.js +65 -0
  107. package/dist/distribution/imageRuntimeAuth.d.ts +19 -0
  108. package/dist/distribution/imageRuntimeAuth.js +71 -0
  109. package/dist/distribution/index.d.ts +24 -0
  110. package/dist/distribution/index.js +16 -0
  111. package/dist/distribution/preflight.d.ts +11 -0
  112. package/dist/distribution/preflight.js +65 -0
  113. package/dist/distribution/projectImageView.d.ts +8 -0
  114. package/dist/distribution/projectImageView.js +83 -0
  115. package/dist/distribution/projectName.d.ts +1 -0
  116. package/dist/distribution/projectName.js +12 -0
  117. package/dist/distribution/renderImageInterface.d.ts +30 -0
  118. package/dist/distribution/renderImageInterface.js +86 -0
  119. package/dist/distribution/tarReader.d.ts +10 -0
  120. package/dist/distribution/tarReader.js +69 -0
  121. package/dist/distribution/types.d.ts +80 -0
  122. package/dist/distribution/types.js +4 -0
  123. package/dist/distribution/verifyDistributionReport.d.ts +12 -0
  124. package/dist/distribution/verifyDistributionReport.js +29 -0
  125. package/dist/filesystem/paths.d.ts +6 -0
  126. package/dist/filesystem/paths.js +11 -0
  127. package/dist/manifest/loadManifest.js +22 -2
  128. package/dist/manifest/renderSpawnfile.js +2 -4
  129. package/dist/manifest/schemas.d.ts +17 -41
  130. package/dist/manifest/surfaceSchemas.d.ts +19 -47
  131. package/dist/manifest/surfaceSchemas.js +3 -6
  132. package/dist/report/createReport.d.ts +7 -1
  133. package/dist/report/createReport.js +28 -1
  134. package/dist/report/types.d.ts +43 -0
  135. package/dist/runtime/common.d.ts +2 -0
  136. package/dist/runtime/common.js +1 -1
  137. package/dist/runtime/container.d.ts +1 -0
  138. package/dist/runtime/container.js +82 -15
  139. package/dist/runtime/index.d.ts +1 -0
  140. package/dist/runtime/index.js +1 -0
  141. package/dist/runtime/install.d.ts +1 -11
  142. package/dist/runtime/install.js +27 -30
  143. package/dist/runtime/openclaw/adapter.js +9 -1
  144. package/dist/runtime/openclaw/configAuth.d.ts +15 -0
  145. package/dist/runtime/openclaw/configAuth.js +46 -0
  146. package/dist/runtime/openclaw/runAuth.d.ts +6 -0
  147. package/dist/runtime/openclaw/runAuth.js +7 -55
  148. package/dist/runtime/openclaw/statusProbes.d.ts +2 -0
  149. package/dist/runtime/openclaw/statusProbes.js +31 -0
  150. package/dist/runtime/pi/adapter.d.ts +4 -0
  151. package/dist/runtime/pi/adapter.js +198 -0
  152. package/dist/runtime/pi/appActivitySource.d.ts +1 -0
  153. package/dist/runtime/pi/appActivitySource.js +87 -0
  154. package/dist/runtime/pi/appCoreSource.d.ts +1 -0
  155. package/dist/runtime/pi/appCoreSource.js +354 -0
  156. package/dist/runtime/pi/appPreludeSource.d.ts +1 -0
  157. package/dist/runtime/pi/appPreludeSource.js +103 -0
  158. package/dist/runtime/pi/appSource.d.ts +1 -0
  159. package/dist/runtime/pi/appSource.js +8 -0
  160. package/dist/runtime/pi/appTemplate.d.ts +28 -0
  161. package/dist/runtime/pi/appTemplate.js +144 -0
  162. package/dist/runtime/pi/runAuth.d.ts +2 -0
  163. package/dist/runtime/pi/runAuth.js +68 -0
  164. package/dist/runtime/picoclaw/adapter.js +10 -0
  165. package/dist/runtime/picoclaw/runAuth.d.ts +7 -0
  166. package/dist/runtime/picoclaw/runAuth.js +8 -68
  167. package/dist/runtime/picoclaw/statusProbes.d.ts +2 -0
  168. package/dist/runtime/picoclaw/statusProbes.js +100 -0
  169. package/dist/runtime/registry.d.ts +0 -5
  170. package/dist/runtime/registry.js +5 -12
  171. package/dist/runtime/statusProbes.d.ts +16 -0
  172. package/dist/runtime/statusProbes.js +41 -0
  173. package/dist/runtime/types.d.ts +53 -1
  174. package/dist/shared/errors.d.ts +6 -0
  175. package/dist/shared/errors.js +9 -0
  176. package/dist/status/buildStatus.d.ts +13 -0
  177. package/dist/status/buildStatus.js +266 -0
  178. package/dist/status/compileReport.d.ts +96 -0
  179. package/dist/status/compileReport.js +230 -0
  180. package/dist/status/compiledContainerObservations.d.ts +5 -0
  181. package/dist/status/compiledContainerObservations.js +63 -0
  182. package/dist/status/deploymentLogs.d.ts +12 -0
  183. package/dist/status/deploymentLogs.js +45 -0
  184. package/dist/status/deployments.d.ts +14 -0
  185. package/dist/status/deployments.js +121 -0
  186. package/dist/status/imageStatusReport.d.ts +10 -0
  187. package/dist/status/imageStatusReport.js +73 -0
  188. package/dist/status/index.d.ts +14 -0
  189. package/dist/status/index.js +14 -0
  190. package/dist/status/moltnetProbes.d.ts +21 -0
  191. package/dist/status/moltnetProbes.js +240 -0
  192. package/dist/status/registryDrift.d.ts +28 -0
  193. package/dist/status/registryDrift.js +119 -0
  194. package/dist/status/renderStatus.d.ts +2 -0
  195. package/dist/status/renderStatus.js +230 -0
  196. package/dist/status/runtimeProbes.d.ts +13 -0
  197. package/dist/status/runtimeProbes.js +111 -0
  198. package/dist/status/selectionSubjects.d.ts +8 -0
  199. package/dist/status/selectionSubjects.js +77 -0
  200. package/dist/status/selectors.d.ts +14 -0
  201. package/dist/status/selectors.js +90 -0
  202. package/dist/status/traversal.d.ts +6 -0
  203. package/dist/status/traversal.js +15 -0
  204. package/dist/status/types.d.ts +113 -0
  205. package/dist/status/types.js +1 -0
  206. package/package.json +9 -2
  207. package/runtimes.yaml +44 -21
  208. package/dist/runtime/tinyclaw/adapter.d.ts +0 -2
  209. package/dist/runtime/tinyclaw/adapter.js +0 -284
  210. package/dist/runtime/tinyclaw/runAuth.d.ts +0 -2
  211. package/dist/runtime/tinyclaw/runAuth.js +0 -49
  212. package/dist/runtime/tinyclaw/scaffold-assets/AGENTS.md +0 -160
  213. package/dist/runtime/tinyclaw/scaffold-assets/CLAUDE.md +0 -5
  214. package/dist/runtime/tinyclaw/scaffold-assets/SOUL.md +0 -177
  215. package/dist/runtime/tinyclaw/scaffold.d.ts +0 -2
  216. package/dist/runtime/tinyclaw/scaffold.js +0 -24
  217. package/dist/runtime/tinyclaw/schedules.d.ts +0 -9
  218. package/dist/runtime/tinyclaw/schedules.js +0 -62
  219. package/dist/runtime/tinyclaw/surfaces.d.ts +0 -8
  220. package/dist/runtime/tinyclaw/surfaces.js +0 -94
package/README.md CHANGED
@@ -37,12 +37,25 @@ spawnfile init # scaffold an agent (defaults t
37
37
  spawnfile validate # check the graph
38
38
  spawnfile view . # read-only graph view; writes no files
39
39
  spawnfile compile # lower to runtime-native output
40
+ spawnfile status . # read declared/compiled status
40
41
  spawnfile auth sync --profile dev --env-file .env
41
42
  spawnfile build --tag my-agent # compile + docker build
42
- spawnfile run --tag my-agent --auth-profile dev
43
+ spawnfile up . --context gpu-4090 --detach # build and run on a Docker context
44
+ spawnfile dev up . --auth-profile dev # detached dev loop in .spawn-dev
45
+ spawnfile dev apply . --agent researcher # hot-add/reload one Pi agent
46
+ spawnfile dev activity . --agent researcher # inspect buffered Pi activity events
47
+ spawnfile run --tag my-agent --auth-profile dev --detach
48
+ spawnfile status . --live # inspect the detached deployment
49
+ spawnfile publish . --tag you/my-agent:1.0.0 # compile + build + verify + push
43
50
  ```
44
51
 
45
- Compiled output lands under `.spawn/` by default, including a `Dockerfile`, `entrypoint.sh`, `.env.example`, and a prebuilt `container/rootfs/` tree. `spawnfile build` uses the pinned runtime artifacts from `runtimes.yaml`; it does not rebuild runtimes from source.
52
+ Compiled output lands under `.spawn/` by default, including a `Dockerfile`, `entrypoint.sh`, `.env.example`, and a prebuilt `container/rootfs/` tree. `spawnfile build` uses the pinned runtime artifacts from `runtimes.yaml`; it does not rebuild runtimes from source. Daimon, OpenClaw, and PicoClaw use published copyable artifact images by default, so normal prompt/config edits reuse their dependency layers. To test a local runtime artifact instead, set `SPAWNFILE_DAIMON_RUNTIME_IMAGE`, `SPAWNFILE_OPENCLAW_RUNTIME_IMAGE`, or `SPAWNFILE_PICOCLAW_RUNTIME_IMAGE` to a local image tag. For Pi-heavy orgs, build a reusable runtime base with `npm run runtime:pi-base -- noopolis/spawnfile-pi-runtime:0.79.9-node24`, then set `SPAWNFILE_PI_RUNTIME_BASE_IMAGE` during `spawnfile up`. For `build`/`up` on a docker `--context`, Moltnet release assets are staged for that context's architecture (`amd64` or `arm64`); for local-only manual compile targeting a fixed architecture, set `SPAWNFILE_MOLTNET_TARGET_ARCH=amd64|arm64`.
53
+
54
+ `spawnfile status` is read-only. By default it shows authored and compiled state without Docker, runtime, or Moltnet calls. With `--live`, it reads the selected detached deployment record, inspects the recorded Docker target, runs adapter-owned runtime probes, and checks Moltnet metadata without reading message bodies. Add `--logs` for a redacted Docker log tail, or `--watch` to refresh status continuously. For a remote Docker context where the local record is missing, pass `--context <name>` with `--live` to recover the deployment from Spawnfile container labels.
55
+
56
+ `spawnfile dev` is the source-backed interactive loop. It uses `.spawn-dev/` by default, starts a detached dev deployment with `spawnfile dev up`, and can hot-apply one Daimon runtime agent with `spawnfile dev apply --agent <id>` without restarting the rest of the org. Hot apply recompiles source, copies the selected agent workspace, Daimon config, matching Moltnet node configs, and managed Moltnet server configs into the running container, loads it through the Daimon control endpoint, and starts only that agent's Moltnet bridges when it is new. `spawnfile dev activity` reads the generated Daimon app's bounded activity buffer as JSON lines so operators can see queued wakes, turn starts/completions, runtime event types, output completions, and errors without mixing those diagnostics into Moltnet chat. Running managed Moltnet servers keep their current in-memory room membership until an operator-token `moltnet apply` or server restart reconciles the copied server config.
57
+
58
+ Compiled images are self-describing: `spawnfile publish` pushes one to any OCI registry, and anyone can run it with no source — `spawnfile up you/my-agent:1.0.0 --deployment prod --detach --auth-profile me` — or inspect what it needs first with `spawnfile status you/my-agent:1.0.0`. See [`specs/DISTRIBUTION.md`](specs/DISTRIBUTION.md).
46
59
 
47
60
  Declare external credentials in `secrets:` and provide values through an ignored env file or the shell environment. `spawnfile auth sync --env-file .env` stores declared model auth and project secrets in a local auth profile; `spawnfile run --env-file .env` can inject the same values directly for a single run. This is the intended pattern for credentials like `GH_TOKEN`, MCP tokens, and provider API keys.
48
61
 
@@ -91,9 +104,12 @@ v0.1 targets autonomous agent runtimes that share a markdown workspace identity
91
104
  |-----------|---------------|---------|-----------------------------------------------|
92
105
  | OpenClaw | active | ✅ | Discord, Telegram, WhatsApp, Slack |
93
106
  | PicoClaw | active | | Discord, Telegram, Slack (WhatsApp blocked) |
94
- | TinyClaw | active | | Discord, Telegram, WhatsApp (pairing-gated) |
107
+ | Pi | active | | Embedded org app, Moltnet client config |
95
108
  | NullClaw | exploratory | | No active adapter yet |
96
109
  | ZeroClaw | exploratory | | No active adapter yet |
110
+ | OpenFang | exploratory | | No active adapter yet |
111
+ | Hermes Agent | exploratory | | No active adapter yet |
112
+ | OpenCode | exploratory | | No active adapter yet |
97
113
 
98
114
  Each adapter maps the portable schema into its native forms. The compiler reports a machine-readable `spawnfile-report.json` with the resolved graph, chosen runtimes, and capability outcomes (`supported`, `degraded`, `unsupported`). See [`specs/RUNTIMES.md`](specs/RUNTIMES.md) for the live matrix and pinned versions, or [`runtimes.yaml`](runtimes.yaml) for the registry source of truth.
99
115
 
@@ -113,6 +129,8 @@ The source-of-truth specs live in this repo:
113
129
  - [`specs/CONTAINERS.md`](specs/CONTAINERS.md) — container compilation
114
130
  - [`specs/RUNTIMES.md`](specs/RUNTIMES.md) — runtime registry and version pinning
115
131
  - [`specs/SURFACES.md`](specs/SURFACES.md) — messaging surface model
132
+ - [`specs/STATUS.md`](specs/STATUS.md) — static and live operational status
133
+ - [`specs/DISTRIBUTION.md`](specs/DISTRIBUTION.md) — image distribution, publish, and sourceless run
116
134
  - [`fixtures/`](fixtures/) — canonical example projects
117
135
 
118
136
  ## From source
@@ -14,6 +14,21 @@ const parseJson = (filePath, content) => {
14
14
  throw new SpawnfileError("validation_error", `Invalid imported auth file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
15
15
  }
16
16
  };
17
+ const decodeJwtExpiry = (token) => {
18
+ const payload = token.split(".")[1];
19
+ if (!payload) {
20
+ return null;
21
+ }
22
+ try {
23
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
24
+ return typeof decoded.exp === "number" && Number.isFinite(decoded.exp)
25
+ ? decoded.exp * 1000
26
+ : null;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ };
17
32
  export const loadImportedClaudeCodeCredential = async (sourceDirectory) => {
18
33
  const credentialsPath = path.join(sourceDirectory, ".credentials.json");
19
34
  if (!(await fileExists(credentialsPath))) {
@@ -66,11 +81,12 @@ export const loadImportedCodexCredential = async (sourceDirectory) => {
66
81
  refresh.length === 0) {
67
82
  return null;
68
83
  }
69
- const fileStats = await stat(authPath);
84
+ const jwtExpiry = decodeJwtExpiry(access);
85
+ const expires = jwtExpiry ?? (await stat(authPath)).mtimeMs + 60 * 60 * 1000;
70
86
  return {
71
87
  access,
72
88
  ...(typeof accountId === "string" && accountId.length > 0 ? { accountId } : {}),
73
- expires: fileStats.mtimeMs + 60 * 60 * 1000,
89
+ expires,
74
90
  refresh
75
91
  };
76
92
  };
@@ -0,0 +1,3 @@
1
+ import { type Command } from "commander";
2
+ import type { CliHandlers, CliStreams } from "./runCli.js";
3
+ export declare const registerDevCommands: (program: Command, handlers: CliHandlers, streams: CliStreams) => void;
@@ -0,0 +1,115 @@
1
+ import { InvalidArgumentError } from "commander";
2
+ const parsePositiveIntegerOption = (value) => {
3
+ if (!/^[1-9]\d*$/u.test(value)) {
4
+ throw new InvalidArgumentError("must be a positive integer");
5
+ }
6
+ return Number(value);
7
+ };
8
+ export const registerDevCommands = (program, handlers, streams) => {
9
+ const dev = program.command("dev").description("Run an interactive Spawnfile development deployment");
10
+ dev
11
+ .command("up")
12
+ .description("Compile, build, and start a detached dev deployment under .spawn-dev")
13
+ .argument("[path]", "Project directory or Spawnfile path", process.cwd())
14
+ .option("--auth-profile <name>", "Local Spawnfile auth profile")
15
+ .option("--context <name>", "Docker context for the deployment target")
16
+ .option("--deployment <name>", "Detached deployment record name")
17
+ .option("--docker-command <command>", "Docker command")
18
+ .option("--env-file <file>", "Path to an env file for runtime secrets")
19
+ .option("--name <container>", "Docker container name")
20
+ .option("-o, --out <directory>", "Dev output directory")
21
+ .option("-t, --tag <image>", "Docker image tag")
22
+ .action(async (inputPath, options) => {
23
+ const result = await handlers.devUpProject(inputPath, {
24
+ authProfile: options.authProfile,
25
+ containerName: options.name,
26
+ deploymentName: options.deployment,
27
+ dockerCommand: options.dockerCommand,
28
+ dockerContext: options.context,
29
+ envFilePath: options.envFile,
30
+ imageTag: options.tag,
31
+ outputDirectory: options.out
32
+ });
33
+ streams.stdout(`dev deployment: ${result.containerName ?? "unknown"}`);
34
+ streams.stdout(`image: ${result.imageTag}`);
35
+ streams.stdout(`compiled to ${result.outputDirectory}`);
36
+ if (result.deploymentRecordPath) {
37
+ streams.stdout(`record: ${result.deploymentRecordPath}`);
38
+ }
39
+ });
40
+ dev
41
+ .command("apply")
42
+ .description("Hot-apply one agent into a running dev deployment")
43
+ .argument("[path]", "Project directory or Spawnfile path", process.cwd())
44
+ .requiredOption("--agent <id>", "Agent id, slug, or name to hot-apply")
45
+ .option("--deployment <name>", "Detached deployment record name")
46
+ .option("--docker-command <command>", "Docker command")
47
+ .option("-o, --out <directory>", "Dev output directory")
48
+ .action(async (inputPath, options) => {
49
+ const result = await handlers.devApplyProject(inputPath, {
50
+ agent: options.agent,
51
+ deploymentName: options.deployment,
52
+ dockerCommand: options.dockerCommand,
53
+ outputDirectory: options.out
54
+ });
55
+ streams.stdout(`applied agent ${result.agentId}`);
56
+ streams.stdout(`container: ${result.containerName}`);
57
+ streams.stdout(`bridge: ${result.bridgeStarted ? "started" : "unchanged"}`);
58
+ });
59
+ dev
60
+ .command("restart")
61
+ .description("Reload one agent in a running dev deployment")
62
+ .argument("[path]", "Project directory or Spawnfile path", process.cwd())
63
+ .requiredOption("--agent <id>", "Agent id, slug, or name to reload")
64
+ .option("--deployment <name>", "Detached deployment record name")
65
+ .option("--docker-command <command>", "Docker command")
66
+ .option("-o, --out <directory>", "Dev output directory")
67
+ .action(async (inputPath, options) => {
68
+ const result = await handlers.devRestartProject(inputPath, {
69
+ agent: options.agent,
70
+ deploymentName: options.deployment,
71
+ dockerCommand: options.dockerCommand,
72
+ outputDirectory: options.out
73
+ });
74
+ streams.stdout(`restarted agent ${result.agentId}`);
75
+ streams.stdout(`container: ${result.containerName}`);
76
+ streams.stdout(`bridge: ${result.bridgeStarted ? "started" : "unchanged"}`);
77
+ });
78
+ dev
79
+ .command("activity")
80
+ .description("Print the current Pi agent activity buffer from a running dev deployment")
81
+ .argument("[path]", "Project directory or Spawnfile path", process.cwd())
82
+ .option("--agent <id>", "Only show events for one agent id, slug, or name")
83
+ .option("--deployment <name>", "Detached deployment record name")
84
+ .option("--docker-command <command>", "Docker command")
85
+ .option("-o, --out <directory>", "Dev output directory")
86
+ .option("--tail <count>", "Maximum buffered events to print", parsePositiveIntegerOption)
87
+ .action(async (inputPath, options) => {
88
+ const result = await handlers.devActivityProject(inputPath, {
89
+ agent: options.agent,
90
+ deploymentName: options.deployment,
91
+ dockerCommand: options.dockerCommand,
92
+ outputDirectory: options.out,
93
+ tail: options.tail
94
+ });
95
+ for (const event of result.events) {
96
+ streams.stdout(JSON.stringify(event));
97
+ }
98
+ });
99
+ dev
100
+ .command("stop")
101
+ .description("Stop the recorded dev deployment container")
102
+ .argument("[path]", "Project directory or Spawnfile path", process.cwd())
103
+ .option("--deployment <name>", "Detached deployment record name")
104
+ .option("--docker-command <command>", "Docker command")
105
+ .option("-o, --out <directory>", "Dev output directory")
106
+ .action(async (inputPath, options) => {
107
+ const result = await handlers.devStopProject(inputPath, {
108
+ deploymentName: options.deployment,
109
+ dockerCommand: options.dockerCommand,
110
+ outputDirectory: options.out
111
+ });
112
+ streams.stdout(`stopped dev deployment ${result.deploymentName}`);
113
+ streams.stdout(`container: ${result.containerName}`);
114
+ });
115
+ };
@@ -1,6 +1,27 @@
1
+ import { readRunEnvFile } from "../compiler/runProjectAuth.js";
2
+ import { SpawnfileError } from "../shared/index.js";
3
+ import { resolveCommandInput } from "./resolveCommandInput.js";
4
+ const resolveAuthProfileForImage = async (handlers, authProfile) => {
5
+ if (!authProfile || !handlers.requireAuthProfile) {
6
+ return null;
7
+ }
8
+ return handlers.requireAuthProfile(authProfile);
9
+ };
10
+ const shortDigest = (digest) => digest ? digest.replace(/^sha256:/, "").slice(0, 12) : "unknown";
11
+ /** Summarizes a redeploy as previous → new ref/digest, per specs/DISTRIBUTION.md. */
12
+ const formatRedeploySummary = (previous, newRef, newDigest) => {
13
+ // Only claim "unchanged" when we actually resolved a matching digest; two null
14
+ // digests (local-only images) are unknown, not provably identical.
15
+ if (previous.ref === newRef && newDigest !== null && previous.digest === newDigest) {
16
+ return `redeployed image ${newRef} (digest unchanged ${shortDigest(newDigest)})`;
17
+ }
18
+ return (`redeployed image: ${previous.ref} (${shortDigest(previous.digest)}) ` +
19
+ `→ ${newRef} (${shortDigest(newDigest)})`);
20
+ };
1
21
  export const registerLifecycleCommands = (program, handlers, streams) => {
2
22
  program
3
23
  .command("compile")
24
+ .description("Compile a project into runtime-native output under .spawn")
4
25
  .argument("[path]", "Project directory or Spawnfile path", process.cwd())
5
26
  .option("-o, --out <directory>", "Output directory")
6
27
  .action(async (inputPath, options) => {
@@ -10,12 +31,15 @@ export const registerLifecycleCommands = (program, handlers, streams) => {
10
31
  });
11
32
  program
12
33
  .command("build")
34
+ .description("Compile and build the organization Docker image")
13
35
  .argument("[path]", "Project directory or Spawnfile path", process.cwd())
36
+ .option("--context <name>", "Docker context for the build target")
14
37
  .option("--docker-command <command>", "Docker command")
15
38
  .option("-o, --out <directory>", "Output directory")
16
39
  .option("-t, --tag <image>", "Docker image tag")
17
40
  .action(async (inputPath, options) => {
18
41
  const result = await handlers.buildProject(inputPath, {
42
+ dockerContext: options.context,
19
43
  dockerCommand: options.dockerCommand,
20
44
  imageTag: options.tag,
21
45
  outputDirectory: options.out
@@ -26,20 +50,36 @@ export const registerLifecycleCommands = (program, handlers, streams) => {
26
50
  });
27
51
  program
28
52
  .command("run")
53
+ .description("Compile, build, and run the organization container locally")
29
54
  .argument("[path]", "Project directory or Spawnfile path", process.cwd())
30
55
  .option("-o, --out <directory>", "Output directory")
31
56
  .option("-t, --tag <image>", "Docker image tag")
32
57
  .option("--auth-profile <name>", "Local Spawnfile auth profile")
58
+ .option("--context <name>", "Docker context for the deployment target")
59
+ .option("--deployment <name>", "Detached deployment record name")
33
60
  .option("--docker-command <command>", "Docker command")
34
61
  .option("--name <container>", "Docker container name")
35
62
  .option("--env-file <file>", "Path to an env file for runtime secrets")
63
+ .option("--image", "Interpret the argument as an image reference")
36
64
  .option("-d, --detach", "Run the container in detached mode")
37
65
  .action(async (inputPath, options) => {
66
+ const runInput = resolveCommandInput(inputPath, { forceImage: options.image });
67
+ if (runInput.kind === "image" || (runInput.kind === "invalid" && options.image)) {
68
+ throw new SpawnfileError("validation_error", `Image-mode run is not supported. Deploy the image with: ` +
69
+ `spawnfile up ${inputPath} --auth-profile <profile> ` +
70
+ "(add --env-file for any extra secrets; image deployments always detach).");
71
+ }
72
+ if (runInput.kind === "invalid") {
73
+ throw new SpawnfileError("validation_error", `Cannot resolve "${inputPath}" as a project path or image reference. ` +
74
+ "Pass a project directory, a Spawnfile path, or an image like 'name:tag' (use --image to force image mode).");
75
+ }
38
76
  const result = await handlers.runProject(inputPath, {
39
77
  authProfile: options.authProfile,
40
78
  containerName: options.name,
41
79
  detach: options.detach,
80
+ deploymentName: options.deployment,
42
81
  dockerCommand: options.dockerCommand,
82
+ dockerContext: options.context,
43
83
  envFilePath: options.envFile,
44
84
  imageTag: options.tag,
45
85
  outputDirectory: options.out
@@ -50,21 +90,83 @@ export const registerLifecycleCommands = (program, handlers, streams) => {
50
90
  }
51
91
  });
52
92
  program
53
- .command("up")
93
+ .command("publish")
94
+ .description("Compile, build, verify, and push the organization image to a registry")
54
95
  .argument("[path]", "Project directory or Spawnfile path", process.cwd())
96
+ .requiredOption("-t, --tag <image>", "Registry image reference to push")
97
+ .option("-o, --out <directory>", "Output directory")
98
+ .option("--docker-command <command>", "Docker command")
99
+ .action(async (inputPath, options) => {
100
+ const publishInput = resolveCommandInput(inputPath);
101
+ if (publishInput.kind !== "project") {
102
+ throw new SpawnfileError("validation_error", "publish operates on a project path, not an image reference");
103
+ }
104
+ const result = await handlers.publishProject(inputPath, {
105
+ dockerCommand: options.dockerCommand,
106
+ imageTag: options.tag,
107
+ outputDirectory: options.out
108
+ });
109
+ streams.stdout(`published ${result.imageTag}`);
110
+ streams.stdout(`digest: ${result.digest ?? "unknown"}`);
111
+ streams.stdout(`next: spawnfile up ${result.imageTag} --detach --auth-profile <profile>`);
112
+ });
113
+ program
114
+ .command("up")
115
+ .description("Deploy an organization from a project or a published image reference")
116
+ .argument("[path]", "Project directory, Spawnfile path, or image reference", process.cwd())
55
117
  .option("-o, --out <directory>", "Output directory")
56
118
  .option("-t, --tag <image>", "Docker image tag")
57
119
  .option("--auth-profile <name>", "Local Spawnfile auth profile")
120
+ .option("--context <name>", "Docker context for the deployment target")
121
+ .option("--deployment <name>", "Detached deployment record name")
58
122
  .option("--docker-command <command>", "Docker command")
59
123
  .option("--name <container>", "Docker container name")
60
124
  .option("--env-file <file>", "Path to an env file for runtime secrets")
125
+ .option("--image", "Interpret the argument as an image reference")
126
+ .option("--pull", "Pull the image before deploying")
61
127
  .option("-d, --detach", "Run the container in detached mode")
62
128
  .action(async (inputPath, options) => {
129
+ const upInput = resolveCommandInput(inputPath, { forceImage: options.image });
130
+ if (upInput.kind === "invalid") {
131
+ throw new SpawnfileError("validation_error", `Cannot resolve "${inputPath}" as a project path or image reference. ` +
132
+ "Pass a project directory, a Spawnfile path, or an image like 'name:tag' (use --image to force image mode).");
133
+ }
134
+ if (upInput.kind === "image") {
135
+ const [authProfile, envFileEnv] = await Promise.all([
136
+ resolveAuthProfileForImage(handlers, options.authProfile),
137
+ readRunEnvFile(options.envFile)
138
+ ]);
139
+ const consumed = await handlers.consumeImageUp(upInput.ref, {
140
+ authProfile,
141
+ authProfileName: options.authProfile ?? null,
142
+ authValues: authProfile?.env ?? {},
143
+ deploymentName: options.deployment,
144
+ dockerCommand: options.dockerCommand,
145
+ dockerContext: options.context,
146
+ envFileEnv,
147
+ envFilePath: options.envFile ?? null,
148
+ pull: options.pull
149
+ });
150
+ if (consumed.previous) {
151
+ const newDigest = consumed.record.source.kind === "image" ? consumed.record.source.digest : null;
152
+ streams.stdout(formatRedeploySummary(consumed.previous, consumed.imageRef, newDigest));
153
+ }
154
+ else {
155
+ streams.stdout(`deployed image ${consumed.imageRef}`);
156
+ }
157
+ streams.stdout(`deployment: ${consumed.deploymentName}`);
158
+ streams.stdout(`running container ${consumed.containerName}`);
159
+ streams.stdout(`record: ${consumed.recordPath}`);
160
+ streams.stdout(`next: spawnfile status --deployment ${consumed.deploymentName} --live`);
161
+ return;
162
+ }
63
163
  const result = await handlers.upProject(inputPath, {
64
164
  authProfile: options.authProfile,
65
165
  containerName: options.name,
66
166
  detach: options.detach,
167
+ deploymentName: options.deployment,
67
168
  dockerCommand: options.dockerCommand,
169
+ dockerContext: options.context,
68
170
  envFilePath: options.envFile,
69
171
  imageTag: options.tag,
70
172
  outputDirectory: options.out
@@ -4,6 +4,7 @@ export const registerModelCommands = (program, handlers, streams) => {
4
4
  .description("Edit primary and fallback model declarations");
5
5
  modelCommand
6
6
  .command("set")
7
+ .description("Set an agent's primary model")
7
8
  .argument("<provider>", "Model provider")
8
9
  .argument("<name>", "Model name")
9
10
  .argument("[path]", "Project directory or Spawnfile path", process.cwd())
@@ -29,6 +30,7 @@ export const registerModelCommands = (program, handlers, streams) => {
29
30
  });
30
31
  modelCommand
31
32
  .command("add-fallback")
33
+ .description("Append a fallback model to an agent")
32
34
  .argument("<provider>", "Model provider")
33
35
  .argument("<name>", "Model name")
34
36
  .argument("[path]", "Project directory or Spawnfile path", process.cwd())
@@ -54,6 +56,7 @@ export const registerModelCommands = (program, handlers, streams) => {
54
56
  });
55
57
  modelCommand
56
58
  .command("clear-fallbacks")
59
+ .description("Remove all fallback models from an agent")
57
60
  .argument("[path]", "Project directory or Spawnfile path", process.cwd())
58
61
  .option("--recursive", "Update the target project and its descendants")
59
62
  .action(async (inputPath, options) => {
@@ -0,0 +1,19 @@
1
+ export type CommandInput = {
2
+ kind: "project";
3
+ path: string;
4
+ } | {
5
+ kind: "image";
6
+ ref: string;
7
+ } | {
8
+ kind: "invalid";
9
+ value: string;
10
+ };
11
+ export interface ResolveCommandInputOptions {
12
+ forceImage?: boolean;
13
+ }
14
+ /**
15
+ * Resolves a positional argument that may be a project path or an image
16
+ * reference. An existing directory or file always wins unless --image forces
17
+ * image mode. Implicit image refs require a tag, digest, or registry component.
18
+ */
19
+ export declare const resolveCommandInput: (value: string, options?: ResolveCommandInputOptions) => CommandInput;
@@ -0,0 +1,25 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { parseImplicitImageReference } from "../distribution/index.js";
4
+ /**
5
+ * Resolves a positional argument that may be a project path or an image
6
+ * reference. An existing directory or file always wins unless --image forces
7
+ * image mode. Implicit image refs require a tag, digest, or registry component.
8
+ */
9
+ export const resolveCommandInput = (value, options = {}) => {
10
+ if (!options.forceImage) {
11
+ const resolved = path.resolve(value);
12
+ if (existsSync(resolved)) {
13
+ return { kind: "project", path: value };
14
+ }
15
+ }
16
+ if (options.forceImage) {
17
+ // --image accepts Docker's full reference grammar, including bare names.
18
+ return { kind: "image", ref: value };
19
+ }
20
+ const parsed = parseImplicitImageReference(value);
21
+ if (parsed) {
22
+ return { kind: "image", ref: value };
23
+ }
24
+ return { kind: "invalid", value };
25
+ };
@@ -1,5 +1,7 @@
1
1
  import { importClaudeCodeAuth, importCodexAuth, importEnvFile, requireAuthProfile } from "../auth/index.js";
2
- import { addAgentProject, addProjectSurface, addProjectModelFallback, addSubagentProject, addTeamProject, buildOrganizationView, buildCompilePlan, buildProject, clearProjectModelFallbacks, compileProject, initProject, upProject, removeProjectSurface, runProject, setProjectPrimaryModel, setProjectRuntime, setProjectSurfaceAccess, showProjectSurfaces, syncProjectAuth } from "../compiler/index.js";
2
+ import { addAgentProject, addProjectSurface, addProjectModelFallback, addSubagentProject, addTeamProject, buildOrganizationView, buildCompilePlan, buildProject, clearProjectModelFallbacks, compileProject, initProject, publishProject, upProject, removeProjectSurface, runProject, setProjectPrimaryModel, setProjectRuntime, setProjectSurfaceAccess, showProjectSurfaces, syncProjectAuth } from "../compiler/index.js";
3
+ import { devActivityProject, devApplyProject, devRestartProject, devStopProject, devUpProject } from "../dev/index.js";
4
+ import { consumeImageUp } from "../distribution/index.js";
3
5
  import { listRuntimeAdapters } from "../runtime/index.js";
4
6
  export interface CliStreams {
5
7
  stderr: (message: string) => void;
@@ -15,6 +17,7 @@ export interface CliHandlers {
15
17
  buildOrganizationView: typeof buildOrganizationView;
16
18
  buildProject: typeof buildProject;
17
19
  compileProject: typeof compileProject;
20
+ publishProject: typeof publishProject;
18
21
  addAgentProject: typeof addAgentProject;
19
22
  addProjectModelFallback: typeof addProjectModelFallback;
20
23
  addProjectSurface: typeof addProjectSurface;
@@ -30,6 +33,12 @@ export interface CliHandlers {
30
33
  requireAuthProfile: typeof requireAuthProfile;
31
34
  runProject: typeof runProject;
32
35
  upProject: typeof upProject;
36
+ consumeImageUp: typeof consumeImageUp;
37
+ devActivityProject: typeof devActivityProject;
38
+ devApplyProject: typeof devApplyProject;
39
+ devRestartProject: typeof devRestartProject;
40
+ devStopProject: typeof devStopProject;
41
+ devUpProject: typeof devUpProject;
33
42
  setProjectPrimaryModel: typeof setProjectPrimaryModel;
34
43
  setProjectRuntime: typeof setProjectRuntime;
35
44
  setProjectSurfaceAccess: typeof setProjectSurfaceAccess;
@@ -1,13 +1,17 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { Command } from "commander";
3
3
  import { importClaudeCodeAuth, importCodexAuth, importEnvFile, requireAuthProfile } from "../auth/index.js";
4
- import { addAgentProject, addProjectSurface, addProjectModelFallback, addSubagentProject, addTeamProject, buildOrganizationView, buildCompilePlan, buildProject, clearProjectModelFallbacks, compileProject, initProject, upProject, removeProjectSurface, runProject, setProjectPrimaryModel, setProjectRuntime, setProjectSurfaceAccess, showProjectSurfaces, syncProjectAuth } from "../compiler/index.js";
5
- import { isSpawnfileError } from "../shared/index.js";
4
+ import { addAgentProject, addProjectSurface, addProjectModelFallback, addSubagentProject, addTeamProject, buildOrganizationView, buildCompilePlan, buildProject, clearProjectModelFallbacks, compileProject, initProject, publishProject, upProject, removeProjectSurface, runProject, setProjectPrimaryModel, setProjectRuntime, setProjectSurfaceAccess, showProjectSurfaces, syncProjectAuth } from "../compiler/index.js";
5
+ import { devActivityProject, devApplyProject, devRestartProject, devStopProject, devUpProject } from "../dev/index.js";
6
+ import { consumeImageUp } from "../distribution/index.js";
7
+ import { errorExitCode, isSpawnfileError } from "../shared/index.js";
6
8
  import { listRuntimeAdapters } from "../runtime/index.js";
9
+ import { registerDevCommands } from "./devCommands.js";
7
10
  import { registerLifecycleCommands } from "./lifecycleCommands.js";
8
11
  import { registerModelCommands } from "./modelCommands.js";
9
12
  import { registerRuntimeCommands } from "./runtimeCommands.js";
10
13
  import { registerSurfaceCommands } from "./surfaceCommands.js";
14
+ import { registerStatusCommand } from "./statusCommand.js";
11
15
  import { registerViewCommand } from "./viewCommand.js";
12
16
  const packageJsonPath = new URL("../../package.json", import.meta.url);
13
17
  const readPackageVersion = () => {
@@ -24,12 +28,13 @@ const createDefaultRenderEnvironment = () => ({
24
28
  stdoutIsTty: process.stdout.isTTY === true
25
29
  });
26
30
  const createDefaultHandlers = () => ({
27
- buildCompilePlan, buildOrganizationView, buildProject, compileProject,
31
+ buildCompilePlan, buildOrganizationView, buildProject, compileProject, publishProject,
28
32
  addAgentProject, addProjectModelFallback, addProjectSurface,
29
33
  addSubagentProject, addTeamProject, clearProjectModelFallbacks,
30
34
  importClaudeCodeAuth, importCodexAuth, importEnvFile,
31
35
  initProject, listRuntimeAdapters, removeProjectSurface, requireAuthProfile,
32
- runProject, setProjectPrimaryModel, setProjectRuntime, upProject,
36
+ runProject, setProjectPrimaryModel, setProjectRuntime, upProject, consumeImageUp,
37
+ devActivityProject, devApplyProject, devRestartProject, devStopProject, devUpProject,
33
38
  setProjectSurfaceAccess, showProjectSurfaces, syncProjectAuth
34
39
  });
35
40
  const isCliStreams = (value) => {
@@ -62,6 +67,16 @@ const isCommanderError = (error) => {
62
67
  && candidate.code.startsWith("commander.")
63
68
  && typeof candidate.exitCode === "number";
64
69
  };
70
+ const formatCliErrorMessage = (error) => {
71
+ if (isSpawnfileError(error)) {
72
+ // Surface the human message only; the internal code is not user-facing.
73
+ return error.message;
74
+ }
75
+ if (error instanceof Error) {
76
+ return error.message;
77
+ }
78
+ return String(error);
79
+ };
65
80
  const formatPlanSummary = (plan) => [
66
81
  `root: ${plan.root}`,
67
82
  `nodes: ${plan.nodes.length}`,
@@ -82,6 +97,7 @@ export const runCli = async (argv, optionsOrStreams, handlerOverrides = {}) => {
82
97
  const cliOptions = normalizeRunCliOptions(optionsOrStreams, handlerOverrides);
83
98
  const streams = cliOptions.streams;
84
99
  const handlers = { ...createDefaultHandlers(), ...cliOptions.handlers };
100
+ let commandExitCode = 0;
85
101
  const program = new Command();
86
102
  program.name("spawnfile").description("Spawnfile v0.1 compiler").version(readPackageVersion());
87
103
  program.exitOverride();
@@ -91,8 +107,10 @@ export const runCli = async (argv, optionsOrStreams, handlerOverrides = {}) => {
91
107
  writeOut: (message) => writeCommanderOutput(streams.stdout, message)
92
108
  });
93
109
  registerLifecycleCommands(program, handlers, streams);
110
+ registerDevCommands(program, handlers, streams);
94
111
  program
95
112
  .command("init")
113
+ .description("Scaffold a new Spawnfile project or team in a directory")
96
114
  .argument("[path]", "Directory to initialize", process.cwd())
97
115
  .option("--team", "Initialize a team project")
98
116
  .option("--runtime <name>", "Runtime for agent scaffolds")
@@ -108,6 +126,7 @@ export const runCli = async (argv, optionsOrStreams, handlerOverrides = {}) => {
108
126
  const addCommand = program.command("add").description("Add children to an existing Spawnfile project");
109
127
  addCommand
110
128
  .command("agent")
129
+ .description("Add an agent member to a team project")
111
130
  .argument("<id>", "Agent member id")
112
131
  .argument("[path]", "Team project directory or Spawnfile path", process.cwd())
113
132
  .option("--runtime <name>", "Runtime for the new agent member")
@@ -122,6 +141,7 @@ export const runCli = async (argv, optionsOrStreams, handlerOverrides = {}) => {
122
141
  });
123
142
  addCommand
124
143
  .command("subagent")
144
+ .description("Add a subagent to an agent project")
125
145
  .argument("<id>", "Subagent id")
126
146
  .argument("[path]", "Agent project directory or Spawnfile path", process.cwd())
127
147
  .action(async (id, inputPath) => {
@@ -134,6 +154,7 @@ export const runCli = async (argv, optionsOrStreams, handlerOverrides = {}) => {
134
154
  });
135
155
  addCommand
136
156
  .command("team")
157
+ .description("Add a nested team to a team project")
137
158
  .argument("<id>", "Nested team id")
138
159
  .argument("[path]", "Team project directory or Spawnfile path", process.cwd())
139
160
  .action(async (id, inputPath) => {
@@ -147,9 +168,13 @@ export const runCli = async (argv, optionsOrStreams, handlerOverrides = {}) => {
147
168
  registerModelCommands(program, handlers, streams);
148
169
  registerRuntimeCommands(program, handlers, streams);
149
170
  registerSurfaceCommands(program, handlers, streams);
171
+ registerStatusCommand(program, handlers, streams, (exitCode) => {
172
+ commandExitCode = exitCode;
173
+ });
150
174
  registerViewCommand(program, handlers, streams, cliOptions.renderEnvironment);
151
175
  program
152
176
  .command("validate")
177
+ .description("Validate a Spawnfile and report the resolved compile plan")
153
178
  .argument("[path]", "Project directory or Spawnfile path", process.cwd())
154
179
  .action(async (inputPath) => {
155
180
  const plan = await handlers.buildCompilePlan(inputPath);
@@ -170,6 +195,7 @@ export const runCli = async (argv, optionsOrStreams, handlerOverrides = {}) => {
170
195
  .description("Import auth material into a local auth profile");
171
196
  authImportCommand
172
197
  .command("env")
198
+ .description("Import secrets from an env file into a profile")
173
199
  .argument("<file>", "Path to an env file")
174
200
  .option("-p, --profile <name>", "Auth profile name", "default")
175
201
  .action(async (filePath, options) => {
@@ -178,6 +204,7 @@ export const runCli = async (argv, optionsOrStreams, handlerOverrides = {}) => {
178
204
  });
179
205
  authImportCommand
180
206
  .command("claude-code")
207
+ .description("Import Claude Code subscription credentials into a profile")
181
208
  .option("-p, --profile <name>", "Auth profile name", "default")
182
209
  .option("--from <directory>", "Source Claude Code config directory")
183
210
  .action(async (options) => {
@@ -186,6 +213,7 @@ export const runCli = async (argv, optionsOrStreams, handlerOverrides = {}) => {
186
213
  });
187
214
  authImportCommand
188
215
  .command("codex")
216
+ .description("Import Codex subscription credentials into a profile")
189
217
  .option("-p, --profile <name>", "Auth profile name", "default")
190
218
  .option("--from <directory>", "Source Codex config directory")
191
219
  .action(async (options) => {
@@ -194,6 +222,7 @@ export const runCli = async (argv, optionsOrStreams, handlerOverrides = {}) => {
194
222
  });
195
223
  authCommand
196
224
  .command("sync")
225
+ .description("Provision auth material a project requires into a profile")
197
226
  .argument("[path]", "Project directory or Spawnfile path", process.cwd())
198
227
  .option("-p, --profile <name>", "Auth profile name", "default")
199
228
  .option("--env-file <file>", "Path to an env file with model keys and runtime secrets")
@@ -210,6 +239,7 @@ export const runCli = async (argv, optionsOrStreams, handlerOverrides = {}) => {
210
239
  });
211
240
  authCommand
212
241
  .command("show")
242
+ .description("Show the contents of a local auth profile")
213
243
  .option("-p, --profile <name>", "Auth profile name", "default")
214
244
  .action(async (options) => {
215
245
  const profile = await handlers.requireAuthProfile(options.profile);
@@ -217,18 +247,16 @@ export const runCli = async (argv, optionsOrStreams, handlerOverrides = {}) => {
217
247
  });
218
248
  try {
219
249
  await program.parseAsync(argv, { from: "user" });
220
- return 0;
250
+ return commandExitCode;
221
251
  }
222
252
  catch (error) {
223
253
  if (isCommanderError(error)) {
224
- return error.exitCode === 0 ? 0 : 1;
254
+ // Commander prints its own usage message; usage errors exit 2.
255
+ return error.exitCode === 0 ? 0 : 2;
225
256
  }
226
- const message = isSpawnfileError(error)
227
- ? `${error.code}: ${error.message}`
228
- : error instanceof Error
229
- ? error.message
230
- : String(error);
231
- streams.stderr(message);
232
- return 1;
257
+ streams.stderr(`error: ${formatCliErrorMessage(error)}`);
258
+ // Usage/input errors exit 2; runtime failures exit 1, matching the
259
+ // documented status exit-code contract across all commands.
260
+ return errorExitCode(error);
233
261
  }
234
262
  };