spawnfile 0.1.11 → 0.1.12

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 (182) hide show
  1. package/README.md +13 -2
  2. package/dist/cli/lifecycleCommands.js +101 -1
  3. package/dist/cli/modelCommands.js +3 -0
  4. package/dist/cli/resolveCommandInput.d.ts +19 -0
  5. package/dist/cli/resolveCommandInput.js +25 -0
  6. package/dist/cli/runCli.d.ts +4 -1
  7. package/dist/cli/runCli.js +37 -13
  8. package/dist/cli/runtimeCommands.js +1 -0
  9. package/dist/cli/statusCommand.d.ts +42 -0
  10. package/dist/cli/statusCommand.js +394 -0
  11. package/dist/cli/surfaceCommands.js +4 -0
  12. package/dist/compiler/agentSurfaces.js +2 -4
  13. package/dist/compiler/compileProject.js +19 -10
  14. package/dist/compiler/containerArtifacts.d.ts +1 -0
  15. package/dist/compiler/containerArtifacts.js +208 -32
  16. package/dist/compiler/containerArtifactsPlans.js +7 -4
  17. package/dist/compiler/containerArtifactsRender.d.ts +7 -1
  18. package/dist/compiler/containerArtifactsRender.js +6 -0
  19. package/dist/compiler/containerArtifactsTypes.d.ts +9 -0
  20. package/dist/compiler/containerEntrypointRender.js +18 -31
  21. package/dist/compiler/index.d.ts +1 -0
  22. package/dist/compiler/index.js +1 -0
  23. package/dist/compiler/moltnetArtifacts.js +5 -0
  24. package/dist/compiler/moltnetClientConfig.d.ts +1 -1
  25. package/dist/compiler/moltnetClientConfig.js +3 -16
  26. package/dist/compiler/moltnetNodeConfig.js +2 -4
  27. package/dist/compiler/moltnetResolution.js +1 -1
  28. package/dist/compiler/moltnetRoomMemberships.js +1 -2
  29. package/dist/compiler/moltnetRuntimeConfig.d.ts +1 -1
  30. package/dist/compiler/moltnetRuntimeConfig.js +1 -13
  31. package/dist/compiler/networkBinding.d.ts +14 -0
  32. package/dist/compiler/networkBinding.js +34 -0
  33. package/dist/compiler/publishProject.d.ts +13 -0
  34. package/dist/compiler/publishProject.js +72 -0
  35. package/dist/compiler/runProject.d.ts +26 -12
  36. package/dist/compiler/runProject.js +102 -125
  37. package/dist/compiler/runProjectAuth.d.ts +5 -0
  38. package/dist/compiler/runProjectAuth.js +98 -0
  39. package/dist/compiler/runProjectDocker.d.ts +19 -0
  40. package/dist/compiler/runProjectDocker.js +57 -0
  41. package/dist/compiler/types.d.ts +1 -2
  42. package/dist/compiler/upProject.d.ts +6 -0
  43. package/dist/compiler/upProject.js +42 -8
  44. package/dist/compiler/view/buildOrganizationView.js +4 -1
  45. package/dist/compiler/view/declarations.d.ts +4 -0
  46. package/dist/compiler/view/declarations.js +104 -0
  47. package/dist/compiler/view/renderNetworks.js +1 -2
  48. package/dist/compiler/view/renderTree.js +6 -2
  49. package/dist/compiler/view/types.d.ts +64 -1
  50. package/dist/deployment/dockerInspect.d.ts +29 -0
  51. package/dist/deployment/dockerInspect.js +130 -0
  52. package/dist/deployment/dockerLabels.d.ts +16 -0
  53. package/dist/deployment/dockerLabels.js +32 -0
  54. package/dist/deployment/dockerLogs.d.ts +24 -0
  55. package/dist/deployment/dockerLogs.js +96 -0
  56. package/dist/deployment/dockerManager.d.ts +59 -0
  57. package/dist/deployment/dockerManager.js +76 -0
  58. package/dist/deployment/dockerProbeGateway.d.ts +16 -0
  59. package/dist/deployment/dockerProbeGateway.js +48 -0
  60. package/dist/deployment/homeStore.d.ts +19 -0
  61. package/dist/deployment/homeStore.js +130 -0
  62. package/dist/deployment/index.d.ts +9 -0
  63. package/dist/deployment/index.js +9 -0
  64. package/dist/deployment/names.d.ts +3 -0
  65. package/dist/deployment/names.js +12 -0
  66. package/dist/deployment/record.d.ts +76 -0
  67. package/dist/deployment/record.js +174 -0
  68. package/dist/deployment/target.d.ts +35 -0
  69. package/dist/deployment/target.js +96 -0
  70. package/dist/distribution/buildDistributionReport.d.ts +22 -0
  71. package/dist/distribution/buildDistributionReport.js +51 -0
  72. package/dist/distribution/consumeImage.d.ts +29 -0
  73. package/dist/distribution/consumeImage.js +254 -0
  74. package/dist/distribution/consumeImageSupport.d.ts +18 -0
  75. package/dist/distribution/consumeImageSupport.js +63 -0
  76. package/dist/distribution/distributionReportSchema.d.ts +84 -0
  77. package/dist/distribution/distributionReportSchema.js +107 -0
  78. package/dist/distribution/dockerRunner.d.ts +5 -0
  79. package/dist/distribution/dockerRunner.js +21 -0
  80. package/dist/distribution/extractImage.d.ts +25 -0
  81. package/dist/distribution/extractImage.js +96 -0
  82. package/dist/distribution/fingerprint.d.ts +2 -0
  83. package/dist/distribution/fingerprint.js +14 -0
  84. package/dist/distribution/imageRef.d.ts +9 -0
  85. package/dist/distribution/imageRef.js +65 -0
  86. package/dist/distribution/imageRuntimeAuth.d.ts +19 -0
  87. package/dist/distribution/imageRuntimeAuth.js +71 -0
  88. package/dist/distribution/index.d.ts +24 -0
  89. package/dist/distribution/index.js +16 -0
  90. package/dist/distribution/preflight.d.ts +11 -0
  91. package/dist/distribution/preflight.js +65 -0
  92. package/dist/distribution/projectImageView.d.ts +8 -0
  93. package/dist/distribution/projectImageView.js +83 -0
  94. package/dist/distribution/projectName.d.ts +1 -0
  95. package/dist/distribution/projectName.js +12 -0
  96. package/dist/distribution/renderImageInterface.d.ts +30 -0
  97. package/dist/distribution/renderImageInterface.js +86 -0
  98. package/dist/distribution/tarReader.d.ts +10 -0
  99. package/dist/distribution/tarReader.js +69 -0
  100. package/dist/distribution/types.d.ts +80 -0
  101. package/dist/distribution/types.js +4 -0
  102. package/dist/distribution/verifyDistributionReport.d.ts +12 -0
  103. package/dist/distribution/verifyDistributionReport.js +29 -0
  104. package/dist/filesystem/paths.d.ts +6 -0
  105. package/dist/filesystem/paths.js +11 -0
  106. package/dist/manifest/loadManifest.js +22 -2
  107. package/dist/manifest/renderSpawnfile.js +2 -4
  108. package/dist/manifest/schemas.d.ts +17 -41
  109. package/dist/manifest/surfaceSchemas.d.ts +19 -47
  110. package/dist/manifest/surfaceSchemas.js +3 -6
  111. package/dist/report/createReport.d.ts +7 -1
  112. package/dist/report/createReport.js +28 -1
  113. package/dist/report/types.d.ts +43 -0
  114. package/dist/runtime/container.js +0 -16
  115. package/dist/runtime/index.d.ts +1 -0
  116. package/dist/runtime/index.js +1 -0
  117. package/dist/runtime/install.d.ts +0 -10
  118. package/dist/runtime/install.js +0 -45
  119. package/dist/runtime/openclaw/adapter.js +9 -1
  120. package/dist/runtime/openclaw/configAuth.d.ts +15 -0
  121. package/dist/runtime/openclaw/configAuth.js +46 -0
  122. package/dist/runtime/openclaw/runAuth.d.ts +6 -0
  123. package/dist/runtime/openclaw/runAuth.js +7 -55
  124. package/dist/runtime/openclaw/statusProbes.d.ts +2 -0
  125. package/dist/runtime/openclaw/statusProbes.js +31 -0
  126. package/dist/runtime/picoclaw/adapter.js +10 -0
  127. package/dist/runtime/picoclaw/runAuth.d.ts +7 -0
  128. package/dist/runtime/picoclaw/runAuth.js +8 -68
  129. package/dist/runtime/picoclaw/statusProbes.d.ts +2 -0
  130. package/dist/runtime/picoclaw/statusProbes.js +100 -0
  131. package/dist/runtime/registry.d.ts +0 -5
  132. package/dist/runtime/registry.js +2 -12
  133. package/dist/runtime/statusProbes.d.ts +16 -0
  134. package/dist/runtime/statusProbes.js +41 -0
  135. package/dist/runtime/types.d.ts +52 -1
  136. package/dist/shared/errors.d.ts +6 -0
  137. package/dist/shared/errors.js +9 -0
  138. package/dist/status/buildStatus.d.ts +13 -0
  139. package/dist/status/buildStatus.js +266 -0
  140. package/dist/status/compileReport.d.ts +96 -0
  141. package/dist/status/compileReport.js +230 -0
  142. package/dist/status/compiledContainerObservations.d.ts +5 -0
  143. package/dist/status/compiledContainerObservations.js +63 -0
  144. package/dist/status/deploymentLogs.d.ts +12 -0
  145. package/dist/status/deploymentLogs.js +45 -0
  146. package/dist/status/deployments.d.ts +14 -0
  147. package/dist/status/deployments.js +128 -0
  148. package/dist/status/imageStatusReport.d.ts +10 -0
  149. package/dist/status/imageStatusReport.js +73 -0
  150. package/dist/status/index.d.ts +14 -0
  151. package/dist/status/index.js +14 -0
  152. package/dist/status/moltnetProbes.d.ts +21 -0
  153. package/dist/status/moltnetProbes.js +240 -0
  154. package/dist/status/registryDrift.d.ts +28 -0
  155. package/dist/status/registryDrift.js +119 -0
  156. package/dist/status/renderStatus.d.ts +2 -0
  157. package/dist/status/renderStatus.js +230 -0
  158. package/dist/status/runtimeProbes.d.ts +13 -0
  159. package/dist/status/runtimeProbes.js +111 -0
  160. package/dist/status/selectionSubjects.d.ts +8 -0
  161. package/dist/status/selectionSubjects.js +77 -0
  162. package/dist/status/selectors.d.ts +14 -0
  163. package/dist/status/selectors.js +90 -0
  164. package/dist/status/traversal.d.ts +6 -0
  165. package/dist/status/traversal.js +15 -0
  166. package/dist/status/types.d.ts +113 -0
  167. package/dist/status/types.js +1 -0
  168. package/package.json +4 -2
  169. package/runtimes.yaml +22 -15
  170. package/dist/runtime/tinyclaw/adapter.d.ts +0 -2
  171. package/dist/runtime/tinyclaw/adapter.js +0 -284
  172. package/dist/runtime/tinyclaw/runAuth.d.ts +0 -2
  173. package/dist/runtime/tinyclaw/runAuth.js +0 -49
  174. package/dist/runtime/tinyclaw/scaffold-assets/AGENTS.md +0 -160
  175. package/dist/runtime/tinyclaw/scaffold-assets/CLAUDE.md +0 -5
  176. package/dist/runtime/tinyclaw/scaffold-assets/SOUL.md +0 -177
  177. package/dist/runtime/tinyclaw/scaffold.d.ts +0 -2
  178. package/dist/runtime/tinyclaw/scaffold.js +0 -24
  179. package/dist/runtime/tinyclaw/schedules.d.ts +0 -9
  180. package/dist/runtime/tinyclaw/schedules.js +0 -62
  181. package/dist/runtime/tinyclaw/surfaces.d.ts +0 -8
  182. package/dist/runtime/tinyclaw/surfaces.js +0 -94
@@ -1,6 +1,5 @@
1
1
  import { getRuntimeAdapter } from "../runtime/index.js";
2
2
  import { SpawnfileError } from "../shared/index.js";
3
- const DEFAULT_TINYCLAW_PORT = 3777;
4
3
  const INSTANCE_ROOT_PLACEHOLDER = "<instance-root>";
5
4
  const CONFIG_FILE_PLACEHOLDER = "<config-file>";
6
5
  const resolveSequentialRuntimePort = (plan, runtimeName, slug) => {
@@ -16,7 +15,6 @@ const resolveSequentialRuntimePort = (plan, runtimeName, slug) => {
16
15
  }
17
16
  return basePort + (index * (adapter.container.portStride ?? 1));
18
17
  };
19
- const createTinyClawChannel = (networkId, agentId) => `moltnet:${networkId}:${agentId}`;
20
18
  const replaceContainerPathTemplate = (template, instanceRoot, configFileName) => template
21
19
  .replaceAll(INSTANCE_ROOT_PLACEHOLDER, instanceRoot)
22
20
  .replaceAll(CONFIG_FILE_PLACEHOLDER, configFileName);
@@ -30,7 +28,7 @@ const resolveRuntimeInstancePaths = (runtimeName, slug) => {
30
28
  : undefined
31
29
  };
32
30
  };
33
- export const resolveRuntimeConfig = (plan, agentNode, nodeSlug, networkId, agentId) => {
31
+ export const resolveRuntimeConfig = (plan, agentNode, nodeSlug, _networkId, _agentId) => {
34
32
  switch (agentNode.runtime.name) {
35
33
  case "openclaw": {
36
34
  const port = resolveSequentialRuntimePort(plan, "openclaw", nodeSlug);
@@ -53,16 +51,6 @@ export const resolveRuntimeConfig = (plan, agentNode, nodeSlug, networkId, agent
53
51
  kind: "picoclaw"
54
52
  };
55
53
  }
56
- case "tinyclaw": {
57
- const channel = createTinyClawChannel(networkId, agentId);
58
- return {
59
- ack_url: `http://127.0.0.1:${DEFAULT_TINYCLAW_PORT}/api/responses`,
60
- channel,
61
- inbound_url: `http://127.0.0.1:${DEFAULT_TINYCLAW_PORT}/api/message`,
62
- kind: "tinyclaw",
63
- outbound_url: `http://127.0.0.1:${DEFAULT_TINYCLAW_PORT}/api/responses/pending?channel=${encodeURIComponent(channel)}`
64
- };
65
- }
66
54
  default:
67
55
  throw new SpawnfileError("compile_error", `Moltnet does not know how to attach runtime ${agentNode.runtime.name} directly`);
68
56
  }
@@ -0,0 +1,14 @@
1
+ /** Uppercases an id and replaces every non-[A-Z0-9] character with `_`. */
2
+ export declare const normalizeNetworkEnvSegment: (id: string) => string;
3
+ export declare const networkUrlEnvName: (networkId: string) => string;
4
+ export declare const networkTokenEnvName: (networkId: string, memberId?: string) => string;
5
+ export interface NetworkBindingInput {
6
+ id: string;
7
+ members: string[];
8
+ }
9
+ /**
10
+ * Asserts that every generated network binding env name (URL and token vars,
11
+ * across all networks and members) is globally unique after normalization.
12
+ * Collisions are baked into immutable images, so they must fail at compile.
13
+ */
14
+ export declare const assertNetworkBindingEnvUniqueness: (networks: NetworkBindingInput[]) => void;
@@ -0,0 +1,34 @@
1
+ import { SpawnfileError } from "../shared/index.js";
2
+ const ENV_PREFIX = "SPAWNFILE_NETWORK_";
3
+ /** Uppercases an id and replaces every non-[A-Z0-9] character with `_`. */
4
+ export const normalizeNetworkEnvSegment = (id) => id.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
5
+ export const networkUrlEnvName = (networkId) => `${ENV_PREFIX}${normalizeNetworkEnvSegment(networkId)}_URL`;
6
+ export const networkTokenEnvName = (networkId, memberId) => memberId === undefined
7
+ ? `${ENV_PREFIX}${normalizeNetworkEnvSegment(networkId)}_TOKEN`
8
+ : `${ENV_PREFIX}${normalizeNetworkEnvSegment(networkId)}_TOKEN_${normalizeNetworkEnvSegment(memberId)}`;
9
+ /**
10
+ * Asserts that every generated network binding env name (URL and token vars,
11
+ * across all networks and members) is globally unique after normalization.
12
+ * Collisions are baked into immutable images, so they must fail at compile.
13
+ */
14
+ export const assertNetworkBindingEnvUniqueness = (networks) => {
15
+ const seen = new Map();
16
+ const claim = (envName, owner) => {
17
+ const existing = seen.get(envName);
18
+ if (existing && existing !== owner) {
19
+ throw new SpawnfileError("validation_error", `Network binding env name ${envName} collides between ${existing} and ${owner}. ` +
20
+ "Rename a network or member so their normalized env names stay unique.");
21
+ }
22
+ seen.set(envName, owner);
23
+ };
24
+ for (const network of networks) {
25
+ claim(networkUrlEnvName(network.id), `network ${network.id} url`);
26
+ const members = [...new Set(network.members)];
27
+ if (members.length === 1) {
28
+ claim(networkTokenEnvName(network.id), `network ${network.id} token`);
29
+ }
30
+ for (const member of members) {
31
+ claim(networkTokenEnvName(network.id, member), `network ${network.id} member ${member}`);
32
+ }
33
+ }
34
+ };
@@ -0,0 +1,13 @@
1
+ import { type BuildProjectOptions, type BuildProjectResult } from "./buildProject.js";
2
+ export type DockerPushRunner = (command: string, args: string[]) => Promise<string>;
3
+ export interface PublishProjectOptions extends BuildProjectOptions {
4
+ pushRunner?: DockerPushRunner;
5
+ }
6
+ export interface PublishProjectResult extends BuildProjectResult {
7
+ digest: string | null;
8
+ }
9
+ /**
10
+ * Compiles, builds, verifies, and pushes a project image. Pre-push verification
11
+ * refuses to publish a report that leaks creator paths or omits secret markers.
12
+ */
13
+ export declare const publishProject: (inputPath: string, options?: PublishProjectOptions) => Promise<PublishProjectResult>;
@@ -0,0 +1,72 @@
1
+ import path from "node:path";
2
+ import { spawn } from "node:child_process";
3
+ import { readFile } from "node:fs/promises";
4
+ import { DISTRIBUTION_REPORT_OUTPUT_FILE, verifyDistributionReport } from "../distribution/index.js";
5
+ import { SpawnfileError } from "../shared/index.js";
6
+ import { buildProject } from "./buildProject.js";
7
+ /* v8 ignore start -- docker push runner is covered by distribution E2E */
8
+ const runDockerCapture = async (command, args) => new Promise((resolve, reject) => {
9
+ const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
10
+ const stdout = [];
11
+ const stderr = [];
12
+ child.stdout.on("data", (chunk) => stdout.push(String(chunk)));
13
+ child.stderr.on("data", (chunk) => stderr.push(String(chunk)));
14
+ child.on("error", reject);
15
+ child.on("close", (code) => {
16
+ if (code === 0) {
17
+ resolve(stdout.join(""));
18
+ return;
19
+ }
20
+ reject(new SpawnfileError("runtime_error", `docker ${args[0]} failed (${code}): ${stderr.join("").trim()}`));
21
+ });
22
+ });
23
+ /* v8 ignore stop */
24
+ const resolvePushedDigest = async (command, imageTag, runDocker) => {
25
+ try {
26
+ const raw = (await runDocker(command, ["image", "inspect", "--format", "{{json .RepoDigests}}", imageTag])).trim();
27
+ const digests = JSON.parse(raw);
28
+ const match = digests
29
+ .map((entry) => (entry.includes("@") ? entry.slice(entry.indexOf("@") + 1) : null))
30
+ .find((entry) => Boolean(entry));
31
+ return match ?? null;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ };
37
+ /**
38
+ * Compiles, builds, verifies, and pushes a project image. Pre-push verification
39
+ * refuses to publish a report that leaks creator paths or omits secret markers.
40
+ */
41
+ export const publishProject = async (inputPath, options = {}) => {
42
+ if (!options.imageTag) {
43
+ throw new SpawnfileError("validation_error", "publish requires --tag with a registry image reference");
44
+ }
45
+ const buildResult = await buildProject(inputPath, options);
46
+ const dockerCommand = options.dockerCommand ?? "docker";
47
+ const runDocker = options.pushRunner ?? runDockerCapture;
48
+ const reportPath = path.join(buildResult.outputDirectory, DISTRIBUTION_REPORT_OUTPUT_FILE);
49
+ let parsedReport;
50
+ try {
51
+ parsedReport = JSON.parse(await readFile(reportPath, "utf8"));
52
+ }
53
+ catch (error) {
54
+ /* v8 ignore next 2 -- compile always emits the report; defensive only */
55
+ const reason = error instanceof Error ? error.message : String(error);
56
+ throw new SpawnfileError("validation_error", `Unable to read distribution report for publish: ${reason}`);
57
+ }
58
+ verifyDistributionReport({
59
+ forbiddenPathFragments: [
60
+ path.resolve(inputPath),
61
+ buildResult.outputDirectory,
62
+ buildResult.report.root
63
+ ],
64
+ report: parsedReport
65
+ });
66
+ await runDocker(dockerCommand, ["push", buildResult.imageTag]);
67
+ const digest = await resolvePushedDigest(dockerCommand, buildResult.imageTag, runDocker);
68
+ return {
69
+ ...buildResult,
70
+ digest
71
+ };
72
+ };
@@ -1,36 +1,50 @@
1
1
  import { type ResolvedAuthProfile } from "../auth/index.js";
2
+ import { type DockerTargetExecFile } from "../deployment/index.js";
2
3
  import { type CompileProjectOptions, type CompileProjectResult } from "./compileProject.js";
3
- export interface DockerRunInvocation {
4
- args: string[];
5
- command: string;
6
- containerName: string | null;
7
- cwd: string;
8
- detach: boolean;
9
- envFilePath: string;
10
- imageTag: string;
11
- supportDirectory: string;
12
- }
13
- export type DockerRunRunner = (invocation: DockerRunInvocation) => Promise<void>;
4
+ import { runDockerContainer, type DockerRunInvocation, type DockerRunResult, type DockerRunRunner } from "./runProjectDocker.js";
5
+ export { runDockerContainer };
6
+ export type { DockerRunInvocation, DockerRunResult, DockerRunRunner };
14
7
  export interface RunProjectOptions extends CompileProjectOptions {
15
8
  authProfile?: string;
16
9
  containerName?: string;
17
10
  detach?: boolean;
11
+ deploymentName?: string;
18
12
  dockerCommand?: string;
13
+ dockerContext?: string;
14
+ dockerHost?: string;
19
15
  envFilePath?: string;
20
16
  imageTag?: string;
21
17
  runRunner?: DockerRunRunner;
18
+ targetExecFile?: DockerTargetExecFile;
22
19
  }
23
20
  export interface RunProjectResult extends CompileProjectResult {
24
21
  authProfileName: string | null;
25
22
  containerName: string | null;
23
+ deploymentRecordPath?: string | null;
26
24
  imageTag: string;
27
25
  }
28
26
  export declare const createDockerRunInvocation: (compileResult: CompileProjectResult, imageTag: string, options?: {
29
27
  authProfile?: ResolvedAuthProfile | null;
30
28
  containerName?: string;
31
29
  detach?: boolean;
30
+ deploymentName?: string;
32
31
  dockerCommand?: string;
32
+ dockerContext?: string;
33
+ dockerHost?: string;
33
34
  envFilePath?: string;
34
35
  }) => Promise<DockerRunInvocation>;
35
- export declare const runDockerContainer: DockerRunRunner;
36
+ interface ResolvedRunDeploymentOptions {
37
+ authProfile?: string;
38
+ containerName?: string;
39
+ deploymentName?: string;
40
+ dockerCommand?: string;
41
+ dockerContext?: string;
42
+ dockerHost?: string;
43
+ envFilePath?: string;
44
+ imageTag?: string;
45
+ targetExecFile?: DockerTargetExecFile;
46
+ }
47
+ export declare const resolveDetachedDeploymentOptions: (outputDirectory: string, options: ResolvedRunDeploymentOptions & {
48
+ detach?: boolean;
49
+ }) => Promise<ResolvedRunDeploymentOptions>;
36
50
  export declare const runProject: (inputPath: string, options?: RunProjectOptions) => Promise<RunProjectResult>;
@@ -1,15 +1,16 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
3
  import { mkdtemp } from "node:fs/promises";
4
- import { randomBytes } from "node:crypto";
5
- import { spawn } from "node:child_process";
6
- import { parseEnvFile, requireAuthProfile } from "../auth/index.js";
7
- import { ensureDirectory, fileExists, readUtf8File, removeDirectory, writeUtf8File } from "../filesystem/index.js";
8
- import { SpawnfileError } from "../shared/index.js";
4
+ import { requireAuthProfile } from "../auth/index.js";
5
+ import { ensureDirectory, fileExists, removeDirectory, writeUtf8File } from "../filesystem/index.js";
6
+ import { appendDockerLabelArgs, createDockerDeploymentLabels, createDockerDeploymentUnitId, createDockerProjectLabel, dockerContextNameForTarget, dockerHostValueForTarget, normalizeDeploymentName, readDeploymentRecordFromOutput, resolveDeploymentRecordPath, verifyDockerDeploymentTarget, writeDockerDeploymentRecordForRun } from "../deployment/index.js";
7
+ import { DEFAULT_OUTPUT_DIRECTORY, SpawnfileError } from "../shared/index.js";
9
8
  import { compileProject } from "./compileProject.js";
10
9
  import { createDefaultImageTag } from "./buildProject.js";
11
10
  import { slugify } from "./helpers.js";
12
- import { assertDeclaredModelAuthSatisfied, prepareRuntimeAuthMounts } from "./runProjectAuth.js";
11
+ import { runDockerContainer } from "./runProjectDocker.js";
12
+ import { assertDeclaredModelAuthSatisfied, assertRunEnvironmentSatisfied, prepareRuntimeAuthMounts, readRunEnvFile, renderDockerEnvFile, resolveAuthMountArgs, resolveRunEnvironment } from "./runProjectAuth.js";
13
+ export { runDockerContainer };
13
14
  const resolveImageTagRoot = (inputPath) => {
14
15
  const resolvedPath = path.resolve(inputPath);
15
16
  return path.basename(resolvedPath).toLowerCase() === "spawnfile"
@@ -20,100 +21,6 @@ const createDefaultContainerName = (imageTag) => {
20
21
  const containerName = slugify(imageTag.replaceAll("/", "-").replaceAll(":", "-"));
21
22
  return containerName || "spawnfile-run";
22
23
  };
23
- const getImportMountTargetName = (kind) => kind === "claude-code" ? ".claude" : ".codex";
24
- const createGeneratedRuntimeSecret = (secretName) => {
25
- if (secretName === "OPENCLAW_GATEWAY_TOKEN" || secretName === "OPENCLAW_HOOKS_TOKEN") {
26
- return randomBytes(24).toString("hex");
27
- }
28
- return null;
29
- };
30
- const hasEnvValue = (env, name) => typeof env[name] === "string" && env[name].length > 0;
31
- const collectMissingRequiredSecrets = (containerReport, env, coveredModelSecrets) => {
32
- const missing = new Set();
33
- for (const secretName of containerReport.secrets_required) {
34
- if (hasEnvValue(env, secretName)) {
35
- continue;
36
- }
37
- if (!containerReport.model_secrets_required.includes(secretName)) {
38
- missing.add(secretName);
39
- continue;
40
- }
41
- const requiredInstances = (containerReport.runtime_instances ?? []).filter((instance) => instance.model_secrets_required.includes(secretName));
42
- const isCoveredEverywhere = requiredInstances.length > 0 &&
43
- requiredInstances.every((instance) => coveredModelSecrets.has(`${instance.id}:${secretName}`));
44
- if (!isCoveredEverywhere) {
45
- missing.add(secretName);
46
- }
47
- }
48
- return [...missing].sort();
49
- };
50
- const resolveRunEnvironment = (containerReport, authProfile, envFileEnv = {}) => {
51
- const env = {
52
- ...(authProfile?.env ?? {}),
53
- ...envFileEnv
54
- };
55
- for (const name of new Set([...Object.keys(env), ...containerReport.secrets_required])) {
56
- const processValue = process.env[name];
57
- if (typeof processValue === "string" && processValue.length > 0) {
58
- env[name] = processValue;
59
- }
60
- }
61
- for (const name of containerReport.runtime_secrets_required) {
62
- if (hasEnvValue(env, name)) {
63
- continue;
64
- }
65
- const generatedValue = createGeneratedRuntimeSecret(name);
66
- if (generatedValue) {
67
- env[name] = generatedValue;
68
- }
69
- }
70
- for (const [name, value] of Object.entries(env)) {
71
- if (value.includes("\n")) {
72
- throw new SpawnfileError("validation_error", `Env value for ${name} contains a newline and cannot be written to a Docker env file`);
73
- }
74
- }
75
- return env;
76
- };
77
- const assertRunEnvironmentSatisfied = (containerReport, env, coveredModelSecrets) => {
78
- const missing = collectMissingRequiredSecrets(containerReport, env, coveredModelSecrets);
79
- if (missing.length > 0) {
80
- throw new SpawnfileError("validation_error", `Missing required runtime env: ${missing.sort().join(", ")}`);
81
- }
82
- };
83
- const renderDockerEnvFile = (env) => `${Object.entries(env)
84
- .sort(([left], [right]) => left.localeCompare(right))
85
- .map(([name, value]) => `${name}=${value}`)
86
- .join("\n")}\n`;
87
- const readRunEnvFile = async (envFilePath) => {
88
- if (!envFilePath) {
89
- return {};
90
- }
91
- try {
92
- return parseEnvFile(await readUtf8File(envFilePath));
93
- }
94
- catch (error) {
95
- const reason = error instanceof Error ? error.message : String(error);
96
- throw new SpawnfileError("validation_error", `Unable to read env file ${envFilePath}: ${reason}`);
97
- }
98
- };
99
- const resolveAuthMountArgs = async (containerReport, authProfile) => {
100
- if (!authProfile || containerReport.runtime_homes.length === 0) {
101
- return [];
102
- }
103
- const args = [];
104
- for (const [kind, entry] of Object.entries(authProfile.imports)) {
105
- if (!entry) {
106
- continue;
107
- }
108
- if (!(await fileExists(entry.path))) {
109
- throw new SpawnfileError("validation_error", `Imported auth path for ${kind} does not exist: ${entry.path}`);
110
- }
111
- for (const runtimeHome of containerReport.runtime_homes) {
112
- args.push("-v", `${entry.path}:${path.posix.join(runtimeHome, getImportMountTargetName(kind))}`);
113
- }
114
- }
115
- return args;
116
- };
117
24
  export const createDockerRunInvocation = async (compileResult, imageTag, options = {}) => {
118
25
  const containerReport = compileResult.report.container;
119
26
  if (!containerReport) {
@@ -128,8 +35,17 @@ export const createDockerRunInvocation = async (compileResult, imageTag, options
128
35
  assertRunEnvironmentSatisfied(containerReport, env, preparedRuntimeAuth.coveredModelSecrets);
129
36
  await ensureDirectory(supportDirectory);
130
37
  await writeUtf8File(envFilePath, renderDockerEnvFile(env));
38
+ const deploymentName = options.deploymentName
39
+ ? normalizeDeploymentName(options.deploymentName)
40
+ : options.detach
41
+ ? normalizeDeploymentName(undefined)
42
+ : null;
131
43
  const containerName = options.containerName ?? createDefaultContainerName(imageTag);
132
- const args = ["run"];
44
+ const args = options.dockerContext
45
+ ? ["--context", options.dockerContext, "run"]
46
+ : options.dockerHost
47
+ ? ["--host", options.dockerHost, "run"]
48
+ : ["run"];
133
49
  if (options.detach) {
134
50
  args.push("-d");
135
51
  }
@@ -143,6 +59,16 @@ export const createDockerRunInvocation = async (compileResult, imageTag, options
143
59
  for (const mount of containerReport.persistent_mounts ?? []) {
144
60
  args.push("-v", `${mount.volume_name}:${mount.mount_path}`);
145
61
  }
62
+ if (options.detach && deploymentName) {
63
+ const compileFingerprint = compileResult.report.compile_fingerprint ?? "sf1:unknown";
64
+ appendDockerLabelArgs(args, createDockerDeploymentLabels({
65
+ compileFingerprint,
66
+ deployment: deploymentName,
67
+ project: createDockerProjectLabel(compileResult.report.root, compileResult.report.project_name),
68
+ unit: createDockerDeploymentUnitId(deploymentName),
69
+ version: compileResult.report.spawnfile_version
70
+ }));
71
+ }
146
72
  args.push("--env-file", envFilePath);
147
73
  args.push(...(await resolveAuthMountArgs(containerReport, options.authProfile ?? null)));
148
74
  args.push(...preparedRuntimeAuth.mountArgs);
@@ -153,6 +79,9 @@ export const createDockerRunInvocation = async (compileResult, imageTag, options
153
79
  containerName,
154
80
  cwd: compileResult.outputDirectory,
155
81
  detach: options.detach ?? false,
82
+ deploymentName,
83
+ dockerContext: options.dockerContext ?? null,
84
+ dockerHost: options.dockerHost ?? null,
156
85
  envFilePath,
157
86
  imageTag,
158
87
  supportDirectory
@@ -163,52 +92,100 @@ export const createDockerRunInvocation = async (compileResult, imageTag, options
163
92
  throw error;
164
93
  }
165
94
  };
166
- export const runDockerContainer = async (invocation) => new Promise((resolve, reject) => {
167
- const child = spawn(invocation.command, invocation.args, {
168
- cwd: invocation.cwd,
169
- stdio: "inherit"
170
- });
171
- child.once("error", (error) => {
172
- reject(new SpawnfileError("runtime_error", `Unable to start docker run for ${invocation.imageTag}: ${error.message}`));
173
- });
174
- child.once("exit", (code, signal) => {
175
- if (code === 0) {
176
- resolve();
177
- return;
178
- }
179
- reject(new SpawnfileError("runtime_error", signal
180
- ? `Docker run for ${invocation.imageTag} exited from signal ${signal}`
181
- : `Docker run for ${invocation.imageTag} failed with exit code ${code ?? "unknown"}`));
182
- });
183
- });
95
+ const readExistingDeploymentRecord = async (outputDirectory, deploymentName) => {
96
+ const recordPath = resolveDeploymentRecordPath(outputDirectory, deploymentName);
97
+ if (!await fileExists(recordPath)) {
98
+ return null;
99
+ }
100
+ return readDeploymentRecordFromOutput(outputDirectory, deploymentName);
101
+ };
102
+ const firstDeploymentUnit = (record) => record.units[0] ?? null;
103
+ export const resolveDetachedDeploymentOptions = async (outputDirectory, options) => {
104
+ if (!options.detach) {
105
+ return options;
106
+ }
107
+ const deploymentName = normalizeDeploymentName(options.deploymentName);
108
+ const record = await readExistingDeploymentRecord(outputDirectory, deploymentName);
109
+ if (!record) {
110
+ return {
111
+ ...options,
112
+ deploymentName
113
+ };
114
+ }
115
+ const unit = firstDeploymentUnit(record);
116
+ if (!options.dockerContext && !options.dockerHost) {
117
+ await verifyDockerDeploymentTarget(record.target, {
118
+ dockerCommand: options.dockerCommand,
119
+ execFile: options.targetExecFile
120
+ });
121
+ }
122
+ return {
123
+ authProfile: options.authProfile ?? record.auth_profile ?? undefined,
124
+ containerName: options.containerName ?? unit?.container_name ?? undefined,
125
+ deploymentName,
126
+ dockerContext: options.dockerContext ?? dockerContextNameForTarget(record.target) ?? undefined,
127
+ dockerHost: options.dockerHost ?? dockerHostValueForTarget(record.target) ?? undefined,
128
+ envFilePath: options.envFilePath ?? record.env_file,
129
+ imageTag: options.imageTag ?? unit?.image_tag
130
+ };
131
+ };
184
132
  export const runProject = async (inputPath, options = {}) => {
133
+ const resolvedOptions = await resolveDetachedDeploymentOptions(path.resolve(options.outputDirectory ?? DEFAULT_OUTPUT_DIRECTORY), {
134
+ authProfile: options.authProfile,
135
+ containerName: options.containerName,
136
+ deploymentName: options.deploymentName,
137
+ detach: options.detach,
138
+ dockerCommand: options.dockerCommand,
139
+ dockerContext: options.dockerContext,
140
+ dockerHost: options.dockerHost,
141
+ envFilePath: options.envFilePath,
142
+ imageTag: options.imageTag,
143
+ targetExecFile: options.targetExecFile
144
+ });
185
145
  const compileResult = await compileProject(inputPath, {
186
146
  clean: options.clean,
187
147
  outputDirectory: options.outputDirectory
188
148
  });
189
- const imageTag = options.imageTag ?? createDefaultImageTag(resolveImageTagRoot(inputPath));
190
- const authProfile = options.authProfile
191
- ? await requireAuthProfile(options.authProfile)
149
+ const imageTag = resolvedOptions.imageTag ?? createDefaultImageTag(resolveImageTagRoot(inputPath));
150
+ const authProfile = resolvedOptions.authProfile
151
+ ? await requireAuthProfile(resolvedOptions.authProfile)
192
152
  : null;
193
153
  const invocation = await createDockerRunInvocation(compileResult, imageTag, {
194
154
  authProfile,
195
- containerName: options.containerName,
155
+ containerName: resolvedOptions.containerName,
196
156
  detach: options.detach,
157
+ deploymentName: resolvedOptions.deploymentName,
197
158
  dockerCommand: options.dockerCommand,
198
- envFilePath: options.envFilePath
159
+ dockerContext: resolvedOptions.dockerContext,
160
+ dockerHost: resolvedOptions.dockerHost,
161
+ envFilePath: resolvedOptions.envFilePath
199
162
  });
163
+ let runMetadata;
200
164
  try {
201
- await (options.runRunner ?? runDockerContainer)(invocation);
165
+ runMetadata = await (options.runRunner ?? runDockerContainer)(invocation);
202
166
  }
203
167
  finally {
204
168
  if (!invocation.detach) {
205
169
  await removeDirectory(invocation.supportDirectory);
206
170
  }
207
171
  }
172
+ const deploymentRecordPath = invocation.detach && invocation.deploymentName
173
+ ? await writeDockerDeploymentRecordForRun({
174
+ authProfileName: authProfile?.name ?? null,
175
+ envFilePath: resolvedOptions.envFilePath,
176
+ imageTag,
177
+ invocation,
178
+ outputDirectory: compileResult.outputDirectory,
179
+ report: compileResult.report,
180
+ runMetadata: runMetadata ?? undefined,
181
+ targetExecFile: options.targetExecFile
182
+ })
183
+ : null;
208
184
  return {
209
185
  ...compileResult,
210
186
  authProfileName: authProfile?.name ?? null,
211
187
  containerName: invocation.containerName,
188
+ deploymentRecordPath,
212
189
  imageTag
213
190
  };
214
191
  };
@@ -6,4 +6,9 @@ interface PreparedRunAuth {
6
6
  }
7
7
  export declare const prepareRuntimeAuthMounts: (outputDirectory: string, containerReport: ContainerReport, authProfile: ResolvedAuthProfile | null, env: Record<string, string>, tempRoot: string) => Promise<PreparedRunAuth>;
8
8
  export declare const assertDeclaredModelAuthSatisfied: (containerReport: ContainerReport, authProfile: ResolvedAuthProfile | null) => void;
9
+ export declare const resolveRunEnvironment: (containerReport: ContainerReport, authProfile: ResolvedAuthProfile | null, envFileEnv?: Record<string, string>) => Record<string, string>;
10
+ export declare const assertRunEnvironmentSatisfied: (containerReport: ContainerReport, env: Record<string, string>, coveredModelSecrets: Set<string>) => void;
11
+ export declare const renderDockerEnvFile: (env: Record<string, string>) => string;
12
+ export declare const readRunEnvFile: (envFilePath: string | undefined) => Promise<Record<string, string>>;
13
+ export declare const resolveAuthMountArgs: (containerReport: ContainerReport, authProfile: ResolvedAuthProfile | null) => Promise<string[]>;
9
14
  export {};
@@ -1,3 +1,7 @@
1
+ import path from "node:path";
2
+ import { randomBytes } from "node:crypto";
3
+ import { parseEnvFile } from "../auth/index.js";
4
+ import { fileExists, readUtf8File } from "../filesystem/index.js";
1
5
  import { SpawnfileError } from "../shared/index.js";
2
6
  import { getRuntimeAdapter } from "../runtime/index.js";
3
7
  const MODEL_AUTH_IMPORT_KINDS = {
@@ -57,3 +61,97 @@ export const assertDeclaredModelAuthSatisfied = (containerReport, authProfile) =
57
61
  throw new SpawnfileError("validation_error", `Auth profile ${authProfile.name} is missing required auth imports: ${missingImportKinds.join(", ")}`);
58
62
  }
59
63
  };
64
+ const getImportMountTargetName = (kind) => kind === "claude-code" ? ".claude" : ".codex";
65
+ const createGeneratedRuntimeSecret = (secretName) => {
66
+ if (secretName === "OPENCLAW_GATEWAY_TOKEN" || secretName === "OPENCLAW_HOOKS_TOKEN") {
67
+ return randomBytes(24).toString("hex");
68
+ }
69
+ return null;
70
+ };
71
+ const hasEnvValue = (env, name) => typeof env[name] === "string" && env[name].length > 0;
72
+ const collectMissingRequiredSecrets = (containerReport, env, coveredModelSecrets) => {
73
+ const missing = new Set();
74
+ for (const secretName of containerReport.secrets_required) {
75
+ if (hasEnvValue(env, secretName)) {
76
+ continue;
77
+ }
78
+ if (!containerReport.model_secrets_required.includes(secretName)) {
79
+ missing.add(secretName);
80
+ continue;
81
+ }
82
+ const requiredInstances = (containerReport.runtime_instances ?? []).filter((instance) => instance.model_secrets_required.includes(secretName));
83
+ const isCoveredEverywhere = requiredInstances.length > 0 &&
84
+ requiredInstances.every((instance) => coveredModelSecrets.has(`${instance.id}:${secretName}`));
85
+ if (!isCoveredEverywhere) {
86
+ missing.add(secretName);
87
+ }
88
+ }
89
+ return [...missing].sort();
90
+ };
91
+ export const resolveRunEnvironment = (containerReport, authProfile, envFileEnv = {}) => {
92
+ const env = {
93
+ ...(authProfile?.env ?? {}),
94
+ ...envFileEnv
95
+ };
96
+ for (const name of new Set([...Object.keys(env), ...containerReport.secrets_required])) {
97
+ const processValue = process.env[name];
98
+ if (typeof processValue === "string" && processValue.length > 0) {
99
+ env[name] = processValue;
100
+ }
101
+ }
102
+ for (const name of containerReport.runtime_secrets_required) {
103
+ if (hasEnvValue(env, name)) {
104
+ continue;
105
+ }
106
+ const generatedValue = createGeneratedRuntimeSecret(name);
107
+ if (generatedValue) {
108
+ env[name] = generatedValue;
109
+ }
110
+ }
111
+ for (const [name, value] of Object.entries(env)) {
112
+ if (value.includes("\n")) {
113
+ throw new SpawnfileError("validation_error", `Env value for ${name} contains a newline and cannot be written to a Docker env file`);
114
+ }
115
+ }
116
+ return env;
117
+ };
118
+ export const assertRunEnvironmentSatisfied = (containerReport, env, coveredModelSecrets) => {
119
+ const missing = collectMissingRequiredSecrets(containerReport, env, coveredModelSecrets);
120
+ if (missing.length > 0) {
121
+ throw new SpawnfileError("validation_error", `Missing required runtime env: ${missing.sort().join(", ")}`);
122
+ }
123
+ };
124
+ export const renderDockerEnvFile = (env) => `${Object.entries(env)
125
+ .sort(([left], [right]) => left.localeCompare(right))
126
+ .map(([name, value]) => `${name}=${value}`)
127
+ .join("\n")}\n`;
128
+ export const readRunEnvFile = async (envFilePath) => {
129
+ if (!envFilePath) {
130
+ return {};
131
+ }
132
+ try {
133
+ return parseEnvFile(await readUtf8File(envFilePath));
134
+ }
135
+ catch (error) {
136
+ const reason = error instanceof Error ? error.message : String(error);
137
+ throw new SpawnfileError("validation_error", `Unable to read env file ${envFilePath}: ${reason}`);
138
+ }
139
+ };
140
+ export const resolveAuthMountArgs = async (containerReport, authProfile) => {
141
+ if (!authProfile || containerReport.runtime_homes.length === 0) {
142
+ return [];
143
+ }
144
+ const args = [];
145
+ for (const [kind, entry] of Object.entries(authProfile.imports)) {
146
+ if (!entry) {
147
+ continue;
148
+ }
149
+ if (!(await fileExists(entry.path))) {
150
+ throw new SpawnfileError("validation_error", `Imported auth path for ${kind} does not exist: ${entry.path}`);
151
+ }
152
+ for (const runtimeHome of containerReport.runtime_homes) {
153
+ args.push("-v", `${entry.path}:${path.posix.join(runtimeHome, getImportMountTargetName(kind))}`);
154
+ }
155
+ }
156
+ return args;
157
+ };