spawnfile 0.1.12 → 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 (63) hide show
  1. package/README.md +9 -2
  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 +2 -0
  6. package/dist/cli/runCli.d.ts +6 -0
  7. package/dist/cli/runCli.js +4 -0
  8. package/dist/cli/statusCommand.d.ts +2 -1
  9. package/dist/cli/statusCommand.js +50 -16
  10. package/dist/compiler/buildProject.d.ts +9 -1
  11. package/dist/compiler/buildProject.js +44 -5
  12. package/dist/compiler/compileProject.d.ts +2 -0
  13. package/dist/compiler/compileProject.js +3 -1
  14. package/dist/compiler/containerArtifactsRender.js +25 -69
  15. package/dist/compiler/containerEntrypointRender.js +4 -1
  16. package/dist/compiler/containerPackageRender.d.ts +15 -0
  17. package/dist/compiler/containerPackageRender.js +63 -0
  18. package/dist/compiler/containerTargetResources.js +3 -1
  19. package/dist/compiler/moltnetArtifacts.js +16 -1
  20. package/dist/compiler/moltnetBinaries.d.ts +4 -1
  21. package/dist/compiler/moltnetBinaries.js +24 -3
  22. package/dist/compiler/moltnetClientConfig.js +11 -0
  23. package/dist/compiler/moltnetRuntimeConfig.js +11 -0
  24. package/dist/compiler/runProject.js +7 -1
  25. package/dist/compiler/runProjectDocker.js +157 -12
  26. package/dist/compiler/upProject.js +1 -0
  27. package/dist/deployment/dockerRecover.d.ts +17 -0
  28. package/dist/deployment/dockerRecover.js +129 -0
  29. package/dist/deployment/index.d.ts +1 -0
  30. package/dist/deployment/index.js +1 -0
  31. package/dist/dev/activity.d.ts +14 -0
  32. package/dist/dev/activity.js +48 -0
  33. package/dist/dev/docker.d.ts +16 -0
  34. package/dist/dev/docker.js +71 -0
  35. package/dist/dev/index.d.ts +2 -0
  36. package/dist/dev/index.js +2 -0
  37. package/dist/dev/project.d.ts +35 -0
  38. package/dist/dev/project.js +164 -0
  39. package/dist/runtime/common.d.ts +2 -0
  40. package/dist/runtime/common.js +1 -1
  41. package/dist/runtime/container.d.ts +1 -0
  42. package/dist/runtime/container.js +93 -10
  43. package/dist/runtime/install.d.ts +1 -1
  44. package/dist/runtime/install.js +43 -1
  45. package/dist/runtime/pi/adapter.d.ts +4 -0
  46. package/dist/runtime/pi/adapter.js +198 -0
  47. package/dist/runtime/pi/appActivitySource.d.ts +1 -0
  48. package/dist/runtime/pi/appActivitySource.js +87 -0
  49. package/dist/runtime/pi/appCoreSource.d.ts +1 -0
  50. package/dist/runtime/pi/appCoreSource.js +354 -0
  51. package/dist/runtime/pi/appPreludeSource.d.ts +1 -0
  52. package/dist/runtime/pi/appPreludeSource.js +103 -0
  53. package/dist/runtime/pi/appSource.d.ts +1 -0
  54. package/dist/runtime/pi/appSource.js +8 -0
  55. package/dist/runtime/pi/appTemplate.d.ts +28 -0
  56. package/dist/runtime/pi/appTemplate.js +144 -0
  57. package/dist/runtime/pi/runAuth.d.ts +2 -0
  58. package/dist/runtime/pi/runAuth.js +68 -0
  59. package/dist/runtime/registry.js +3 -0
  60. package/dist/runtime/types.d.ts +1 -0
  61. package/dist/status/deployments.js +5 -12
  62. package/package.json +7 -2
  63. package/runtimes.yaml +27 -11
package/README.md CHANGED
@@ -40,14 +40,20 @@ spawnfile compile # lower to runtime-native outpu
40
40
  spawnfile status . # read declared/compiled status
41
41
  spawnfile auth sync --profile dev --env-file .env
42
42
  spawnfile build --tag my-agent # compile + docker build
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
43
47
  spawnfile run --tag my-agent --auth-profile dev --detach
44
48
  spawnfile status . --live # inspect the detached deployment
45
49
  spawnfile publish . --tag you/my-agent:1.0.0 # compile + build + verify + push
46
50
  ```
47
51
 
48
- 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`.
49
53
 
50
- `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.
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.
51
57
 
52
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).
53
59
 
@@ -98,6 +104,7 @@ v0.1 targets autonomous agent runtimes that share a markdown workspace identity
98
104
  |-----------|---------------|---------|-----------------------------------------------|
99
105
  | OpenClaw | active | ✅ | Discord, Telegram, WhatsApp, Slack |
100
106
  | PicoClaw | active | | Discord, Telegram, Slack (WhatsApp blocked) |
107
+ | Pi | active | | Embedded org app, Moltnet client config |
101
108
  | NullClaw | exploratory | | No active adapter yet |
102
109
  | ZeroClaw | exploratory | | No active adapter yet |
103
110
  | OpenFang | exploratory | | No active adapter yet |
@@ -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
+ };
@@ -33,11 +33,13 @@ export const registerLifecycleCommands = (program, handlers, streams) => {
33
33
  .command("build")
34
34
  .description("Compile and build the organization Docker image")
35
35
  .argument("[path]", "Project directory or Spawnfile path", process.cwd())
36
+ .option("--context <name>", "Docker context for the build target")
36
37
  .option("--docker-command <command>", "Docker command")
37
38
  .option("-o, --out <directory>", "Output directory")
38
39
  .option("-t, --tag <image>", "Docker image tag")
39
40
  .action(async (inputPath, options) => {
40
41
  const result = await handlers.buildProject(inputPath, {
42
+ dockerContext: options.context,
41
43
  dockerCommand: options.dockerCommand,
42
44
  imageTag: options.tag,
43
45
  outputDirectory: options.out
@@ -1,5 +1,6 @@
1
1
  import { importClaudeCodeAuth, importCodexAuth, importEnvFile, requireAuthProfile } from "../auth/index.js";
2
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";
3
4
  import { consumeImageUp } from "../distribution/index.js";
4
5
  import { listRuntimeAdapters } from "../runtime/index.js";
5
6
  export interface CliStreams {
@@ -33,6 +34,11 @@ export interface CliHandlers {
33
34
  runProject: typeof runProject;
34
35
  upProject: typeof upProject;
35
36
  consumeImageUp: typeof consumeImageUp;
37
+ devActivityProject: typeof devActivityProject;
38
+ devApplyProject: typeof devApplyProject;
39
+ devRestartProject: typeof devRestartProject;
40
+ devStopProject: typeof devStopProject;
41
+ devUpProject: typeof devUpProject;
36
42
  setProjectPrimaryModel: typeof setProjectPrimaryModel;
37
43
  setProjectRuntime: typeof setProjectRuntime;
38
44
  setProjectSurfaceAccess: typeof setProjectSurfaceAccess;
@@ -2,9 +2,11 @@ import { readFileSync } from "node:fs";
2
2
  import { Command } from "commander";
3
3
  import { importClaudeCodeAuth, importCodexAuth, importEnvFile, requireAuthProfile } from "../auth/index.js";
4
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";
5
6
  import { consumeImageUp } from "../distribution/index.js";
6
7
  import { errorExitCode, isSpawnfileError } from "../shared/index.js";
7
8
  import { listRuntimeAdapters } from "../runtime/index.js";
9
+ import { registerDevCommands } from "./devCommands.js";
8
10
  import { registerLifecycleCommands } from "./lifecycleCommands.js";
9
11
  import { registerModelCommands } from "./modelCommands.js";
10
12
  import { registerRuntimeCommands } from "./runtimeCommands.js";
@@ -32,6 +34,7 @@ const createDefaultHandlers = () => ({
32
34
  importClaudeCodeAuth, importCodexAuth, importEnvFile,
33
35
  initProject, listRuntimeAdapters, removeProjectSurface, requireAuthProfile,
34
36
  runProject, setProjectPrimaryModel, setProjectRuntime, upProject, consumeImageUp,
37
+ devActivityProject, devApplyProject, devRestartProject, devStopProject, devUpProject,
35
38
  setProjectSurfaceAccess, showProjectSurfaces, syncProjectAuth
36
39
  });
37
40
  const isCliStreams = (value) => {
@@ -104,6 +107,7 @@ export const runCli = async (argv, optionsOrStreams, handlerOverrides = {}) => {
104
107
  writeOut: (message) => writeCommanderOutput(streams.stdout, message)
105
108
  });
106
109
  registerLifecycleCommands(program, handlers, streams);
110
+ registerDevCommands(program, handlers, streams);
107
111
  program
108
112
  .command("init")
109
113
  .description("Scaffold a new Spawnfile project or team in a directory")
@@ -1,5 +1,5 @@
1
1
  import { Command } from "commander";
2
- import { inspectDockerDeployment } from "../deployment/index.js";
2
+ import { inspectDockerDeployment, recoverDockerDeploymentRecords } from "../deployment/index.js";
3
3
  import { collectDeploymentLogObservations, collectMoltnetProbeObservations, collectRuntimeProbeObservations, type StatusCommandResult, type StatusExitCode } from "../status/index.js";
4
4
  import type { CliHandlers, CliStreams } from "./runCli.js";
5
5
  type StatusCommandHandlers = Pick<CliHandlers, "buildOrganizationView"> & Partial<Pick<CliHandlers, "requireAuthProfile">>;
@@ -8,6 +8,7 @@ type StatusCommandLiveHandlers = {
8
8
  collectMoltnetProbeObservations?: typeof collectMoltnetProbeObservations;
9
9
  collectRuntimeProbeObservations?: typeof collectRuntimeProbeObservations;
10
10
  inspectDockerDeployment?: typeof inspectDockerDeployment;
11
+ recoverDockerDeploymentRecords?: typeof recoverDockerDeploymentRecords;
11
12
  };
12
13
  type StatusCommandHandlersWithLive = StatusCommandHandlers & StatusCommandLiveHandlers;
13
14
  export interface StatusCommandOptions {
@@ -1,11 +1,11 @@
1
- import { inspectDockerDeployment, listDeploymentRecords, listHomeDeploymentRecords, readHomeDeploymentRecord, readHomeDeploymentReport } from "../deployment/index.js";
2
- import { extractImageReport, parseDistributionReport, projectImageOrganizationView, renderImageInterface } from "../distribution/index.js";
1
+ import { inspectDockerDeployment, listDeploymentRecords, listHomeDeploymentRecords, readHomeDeploymentRecord, readHomeDeploymentReport, recoverDockerDeploymentRecords } from "../deployment/index.js";
2
+ import { extractImageReport, normalizeProjectLabelSlug, parseDistributionReport, projectImageOrganizationView, renderImageInterface } from "../distribution/index.js";
3
3
  import { collectRegistryDriftObservations } from "../status/index.js";
4
4
  import { resolveProjectOutputDirectory } from "../filesystem/index.js";
5
5
  import { DEFAULT_OUTPUT_DIRECTORY, errorExitCode, SpawnfileError } from "../shared/index.js";
6
6
  import { resolveCommandInput } from "./resolveCommandInput.js";
7
7
  import { loadedImageCompileReport } from "../status/index.js";
8
- import { createStaticStatus, createDeploymentSummaries, collectDeploymentLogObservations, collectMoltnetProbeObservations, collectRuntimeProbeObservations, exitCodeForStatus, loadCompileReport, renderStatus, resolveStatusSelector } from "../status/index.js";
8
+ import { createStaticStatus, createDeploymentSummaries, collectDeploymentLogObservations, collectMoltnetProbeObservations, collectRuntimeProbeObservations, exitCodeForStatus, flattenOrganizationNodes, loadCompileReport, renderStatus, resolveStatusSelector } from "../status/index.js";
9
9
  const inputFailure = (message) => ({
10
10
  error: message,
11
11
  exitCode: 2
@@ -56,16 +56,36 @@ const resolveDeploymentRecords = (records, options) => {
56
56
  return records;
57
57
  };
58
58
  const inspectDeployments = async (records, handlers, options, timeoutMs) => {
59
- if (!options.live || options.recover) {
59
+ if (!options.live) {
60
60
  return new Map();
61
61
  }
62
62
  const inspect = handlers.inspectDockerDeployment ?? inspectDockerDeployment;
63
63
  const inspections = await Promise.all(records.map(async ({ record }) => [
64
64
  record.name,
65
- await inspect(record, { timeoutMs })
65
+ await inspect(record, {
66
+ dockerCommand: options.dockerCommand,
67
+ timeoutMs
68
+ })
66
69
  ]));
67
70
  return new Map(inspections);
68
71
  };
72
+ const containsFromView = (view) => flattenOrganizationNodes(view).map((node) => ({ id: node.id, kind: node.kind }));
73
+ const runtimeInstanceIdsFromReport = (loadedReport) => loadedReport.kind === "loaded"
74
+ ? loadedReport.report.runtimeInstances.map((instance) => instance.id).sort()
75
+ : [];
76
+ const recoverContextDeployments = async (input) => {
77
+ const recover = input.handlers.recoverDockerDeploymentRecords ?? recoverDockerDeploymentRecords;
78
+ return recover({
79
+ contains: containsFromView(input.view),
80
+ context: input.options.context,
81
+ dockerCommand: input.options.dockerCommand,
82
+ outputDirectory: input.outputDirectory,
83
+ projectLabel: input.projectLabel,
84
+ runtimeInstanceIds: runtimeInstanceIdsFromReport(input.loadedReport),
85
+ sourceRoot: input.sourceRoot,
86
+ timeoutMs: input.timeoutMs
87
+ });
88
+ };
69
89
  const resolveAuthValues = async (records, handlers) => {
70
90
  const values = {};
71
91
  if (!handlers.requireAuthProfile) {
@@ -204,7 +224,12 @@ const runHomeDeploymentStatus = async (options, handlers, mode, timeoutMs) => {
204
224
  timeoutMs
205
225
  }),
206
226
  ...(options.logs
207
- ? await collectDeploymentLogs({ deployments: recordValues, loadedReport, timeoutMs })
227
+ ? await collectDeploymentLogs({
228
+ deployments: recordValues,
229
+ dockerCommand: options.dockerCommand,
230
+ loadedReport,
231
+ timeoutMs
232
+ })
208
233
  : []),
209
234
  ...(options.pullCheck
210
235
  ? await collectRegistryDriftObservations({
@@ -250,22 +275,19 @@ export const executeStatusCommand = async (inputPath, options, handlers) => {
250
275
  if (commandInput.kind === "image" && !options.deployment) {
251
276
  return runStaticImageStatus(commandInput.ref, options, mode === "json");
252
277
  }
253
- if (options.deployment && (commandInput.kind === "image" || usedDefaultPath)) {
278
+ if (options.deployment && !options.context && (commandInput.kind === "image" || usedDefaultPath)) {
254
279
  const timeoutForHome = resolveTimeoutMs(options);
255
280
  if (typeof timeoutForHome !== "number" && timeoutForHome !== undefined) {
256
281
  return timeoutForHome;
257
282
  }
258
283
  return runHomeDeploymentStatus(options, handlers, mode, timeoutForHome);
259
284
  }
260
- if (options.context && !options.recover) {
261
- return inputFailure("status accepts --context only with --recover");
285
+ if (options.context && !options.live) {
286
+ return inputFailure("status --context requires --live");
262
287
  }
263
288
  if (options.logs && !options.live) {
264
289
  return inputFailure("status --logs requires --live");
265
290
  }
266
- if (options.logs && options.recover) {
267
- return inputFailure("status --logs is not available with --recover");
268
- }
269
291
  if (options.recover && !options.context) {
270
292
  return inputFailure("status --recover requires --context");
271
293
  }
@@ -289,12 +311,23 @@ export const executeStatusCommand = async (inputPath, options, handlers) => {
289
311
  }
290
312
  let deploymentRecords;
291
313
  try {
292
- deploymentRecords = await listDeploymentRecords(outputDirectory);
314
+ deploymentRecords = options.context
315
+ ? await recoverContextDeployments({
316
+ handlers,
317
+ loadedReport,
318
+ options,
319
+ outputDirectory,
320
+ projectLabel: normalizeProjectLabelSlug(view.root.name),
321
+ sourceRoot: view.root.source,
322
+ timeoutMs,
323
+ view
324
+ })
325
+ : await listDeploymentRecords(outputDirectory);
293
326
  }
294
327
  catch (error) {
295
328
  return {
296
329
  error: error instanceof Error ? error.message : String(error),
297
- exitCode: 2
330
+ exitCode: errorExitCode(error)
298
331
  };
299
332
  }
300
333
  const selectedDeploymentRecords = resolveDeploymentRecords(deploymentRecords, options);
@@ -327,6 +360,7 @@ export const executeStatusCommand = async (inputPath, options, handlers) => {
327
360
  ...(options.logs
328
361
  ? await collectDeploymentLogs({
329
362
  deployments: selectedDeploymentRecordValues,
363
+ dockerCommand: options.dockerCommand,
330
364
  loadedReport,
331
365
  timeoutMs
332
366
  })
@@ -340,7 +374,7 @@ export const executeStatusCommand = async (inputPath, options, handlers) => {
340
374
  context: options.context ?? null,
341
375
  deploymentName: options.deployment ?? null,
342
376
  logs: options.logs ?? false,
343
- recover: options.recover ?? false,
377
+ recover: options.recover === true || Boolean(options.context),
344
378
  requested: options.live ?? false
345
379
  },
346
380
  liveObservations,
@@ -368,7 +402,7 @@ export const registerStatusCommand = (program, handlers, streams, setExitCode) =
368
402
  .option("--pull", "Pull the image before inspecting (image references only)")
369
403
  .option("--pull-check", "Check the registry for a newer image digest (networked)")
370
404
  .option("--docker-command <command>", "Docker command")
371
- .option("--context <name>", "Docker context for label recovery only")
405
+ .option("--context <name>", "Docker context for live remote deployment recovery")
372
406
  .option("--recover", "Recover live status from labels instead of a deployment record")
373
407
  .option("--logs", "Include redacted logs when supported")
374
408
  .option("--timeout <ms>", "Bound live Docker/runtime checks in milliseconds")
@@ -1,20 +1,28 @@
1
1
  import { type CompileProjectOptions, type CompileProjectResult } from "./compileProject.js";
2
+ import type { MoltnetTargetArchitecture } from "./moltnetBinaries.js";
2
3
  export interface DockerBuildInvocation {
3
4
  args: string[];
4
5
  command: string;
5
6
  cwd: string;
7
+ dockerContext?: string | null;
6
8
  imageTag: string;
7
9
  }
8
10
  export type DockerBuildRunner = (invocation: DockerBuildInvocation) => Promise<void>;
9
11
  export interface BuildProjectOptions extends CompileProjectOptions {
10
12
  buildRunner?: DockerBuildRunner;
11
13
  dockerCommand?: string;
14
+ dockerContext?: string;
12
15
  imageTag?: string;
13
16
  }
14
17
  export interface BuildProjectResult extends CompileProjectResult {
15
18
  imageTag: string;
16
19
  }
17
20
  export declare const createDefaultImageTag: (projectRoot: string) => string;
18
- export declare const createDockerBuildInvocation: (outputDirectory: string, imageTag: string, dockerCommand?: string) => DockerBuildInvocation;
21
+ export declare const createDockerBuildInvocation: (outputDirectory: string, imageTag: string, options?: string | {
22
+ dockerCommand?: string;
23
+ dockerContext?: string;
24
+ }) => DockerBuildInvocation;
25
+ export declare const normalizeDockerArchitecture: (architecture: string) => MoltnetTargetArchitecture;
26
+ export declare const resolveDockerBuildArchitecture: (options: Pick<BuildProjectOptions, "dockerCommand" | "dockerContext">) => Promise<MoltnetTargetArchitecture | undefined>;
19
27
  export declare const runDockerBuild: DockerBuildRunner;
20
28
  export declare const buildProject: (inputPath: string, options?: BuildProjectOptions) => Promise<BuildProjectResult>;
@@ -1,8 +1,10 @@
1
1
  import path from "node:path";
2
- import { spawn } from "node:child_process";
2
+ import { execFile as execFileCallback, spawn } from "node:child_process";
3
+ import { promisify } from "node:util";
3
4
  import { SpawnfileError } from "../shared/index.js";
4
5
  import { compileProject } from "./compileProject.js";
5
6
  import { slugify } from "./helpers.js";
7
+ const execFile = promisify(execFileCallback);
6
8
  export const createDefaultImageTag = (projectRoot) => {
7
9
  const baseName = slugify(path.basename(projectRoot));
8
10
  return `spawnfile-${baseName || "project"}`;
@@ -13,12 +15,44 @@ const resolveImageTagRoot = (inputPath) => {
13
15
  ? path.dirname(resolvedPath)
14
16
  : resolvedPath;
15
17
  };
16
- export const createDockerBuildInvocation = (outputDirectory, imageTag, dockerCommand = "docker") => ({
17
- args: ["build", "-t", imageTag, "."],
18
- command: dockerCommand,
18
+ export const createDockerBuildInvocation = (outputDirectory, imageTag, options = "docker") => ({
19
+ args: typeof options === "string" || !options.dockerContext
20
+ ? ["build", "-t", imageTag, "."]
21
+ : ["--context", options.dockerContext, "build", "-t", imageTag, "."],
22
+ command: typeof options === "string" ? options : options.dockerCommand ?? "docker",
19
23
  cwd: outputDirectory,
24
+ dockerContext: typeof options === "string" ? null : options.dockerContext ?? null,
20
25
  imageTag
21
26
  });
27
+ export const normalizeDockerArchitecture = (architecture) => {
28
+ switch (architecture.trim()) {
29
+ case "amd64":
30
+ case "x64":
31
+ case "x86_64":
32
+ return "amd64";
33
+ case "aarch64":
34
+ case "arm64":
35
+ return "arm64";
36
+ default:
37
+ throw new SpawnfileError("compile_error", `Docker target architecture ${architecture.trim() || "unknown"} is not supported by staged Moltnet binaries`);
38
+ }
39
+ };
40
+ export const resolveDockerBuildArchitecture = async (options) => {
41
+ if (!options.dockerContext) {
42
+ return undefined;
43
+ }
44
+ try {
45
+ const { stdout } = await execFile(options.dockerCommand ?? "docker", ["--context", options.dockerContext, "info", "--format", "{{.Architecture}}"], { timeout: 10_000 });
46
+ return normalizeDockerArchitecture(stdout);
47
+ }
48
+ catch (error) {
49
+ if (error instanceof SpawnfileError) {
50
+ throw error;
51
+ }
52
+ const reason = error instanceof Error ? error.message : String(error);
53
+ throw new SpawnfileError("compile_error", `Unable to resolve Docker context ${options.dockerContext} architecture: ${reason}`);
54
+ }
55
+ };
22
56
  export const runDockerBuild = async (invocation) => new Promise((resolve, reject) => {
23
57
  const child = spawn(invocation.command, invocation.args, {
24
58
  cwd: invocation.cwd,
@@ -38,12 +72,17 @@ export const runDockerBuild = async (invocation) => new Promise((resolve, reject
38
72
  });
39
73
  });
40
74
  export const buildProject = async (inputPath, options = {}) => {
75
+ const targetArchitecture = options.containerArchitecture ?? await resolveDockerBuildArchitecture(options);
41
76
  const compileResult = await compileProject(inputPath, {
42
77
  clean: options.clean,
78
+ containerArchitecture: targetArchitecture,
43
79
  outputDirectory: options.outputDirectory
44
80
  });
45
81
  const imageTag = options.imageTag ?? createDefaultImageTag(resolveImageTagRoot(inputPath));
46
- const invocation = createDockerBuildInvocation(compileResult.outputDirectory, imageTag, options.dockerCommand);
82
+ const invocation = createDockerBuildInvocation(compileResult.outputDirectory, imageTag, {
83
+ dockerCommand: options.dockerCommand,
84
+ dockerContext: options.dockerContext
85
+ });
47
86
  await (options.buildRunner ?? runDockerBuild)(invocation);
48
87
  return {
49
88
  ...compileResult,
@@ -1,6 +1,8 @@
1
1
  import { CompileReport } from "../report/index.js";
2
+ import { type MoltnetTargetArchitecture } from "./moltnetBinaries.js";
2
3
  export interface CompileProjectOptions {
3
4
  clean?: boolean;
5
+ containerArchitecture?: MoltnetTargetArchitecture;
4
6
  outputDirectory?: string;
5
7
  }
6
8
  export interface CompileProjectResult {
@@ -232,7 +232,9 @@ export const compileProject = async (inputPath, options = {}) => {
232
232
  }
233
233
  const moltnetArtifacts = await generateMoltnetArtifacts(plan);
234
234
  const hasStagedMoltnetBinaries = moltnetArtifacts
235
- ? await stageMoltnetBinaries(outputDirectory)
235
+ ? await stageMoltnetBinaries(outputDirectory, {
236
+ architecture: options.containerArchitecture
237
+ })
236
238
  : false;
237
239
  await injectMoltnetWorkspaceFiles(outputDirectory, compiledNodes, moltnetArtifacts);
238
240
  const generatedAt = new Date().toISOString();