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
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { resolveMoltnetStorePath } from "./moltnetConfigLowering.js";
3
+ import { networkUrlEnvName } from "./networkBinding.js";
3
4
  import { createWorkspaceResourceCommands, createWorkspaceResourceShellFunctions } from "./containerWorkspaceResourceRender.js";
4
5
  const MOLTNET_SERVER_DATA_DIRECTORY = "/var/lib/spawnfile/moltnet/servers";
5
6
  const shellQuote = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
@@ -8,9 +9,9 @@ const createEnvironmentAssignments = (plan) => {
8
9
  if (plan.instancePaths.homePath) {
9
10
  envAssignments.push(`HOME=${shellQuote(plan.instancePaths.homePath)}`);
10
11
  }
11
- if (plan.instancePaths.homePath && ((plan.runtimeName === "tinyclaw" &&
12
- (plan.modelAuthMethods.openai === "api_key" || plan.modelAuthMethods.openai === "codex")) ||
13
- (plan.runtimeName === "picoclaw" && plan.modelAuthMethods.openai === "codex"))) {
12
+ if (plan.instancePaths.homePath &&
13
+ plan.runtimeName === "picoclaw" &&
14
+ plan.modelAuthMethods.openai === "codex") {
14
15
  envAssignments.push(`CODEX_HOME=${shellQuote(path.posix.join(plan.instancePaths.homePath, ".codex"))}`);
15
16
  }
16
17
  if (plan.meta.homeEnv && plan.instancePaths.homePath) {
@@ -29,14 +30,6 @@ const createEnvironmentAssignments = (plan) => {
29
30
  };
30
31
  const createEnvFileWrites = (plan) => plan.envFiles.map((binding) => `write_env_file ${shellQuote(binding.envName)} ${shellQuote(binding.filePath)}`);
31
32
  const createConfigEnvWrites = (plan) => (plan.configEnvBindings ?? []).map((binding) => `apply_json_env_value ${shellQuote(plan.instancePaths.configPath)} ${shellQuote(binding.envName)} ${shellQuote(binding.jsonPath)}`);
32
- const createAuthSetupCommands = (plan) => {
33
- if (plan.runtimeName !== "tinyclaw" ||
34
- !plan.instancePaths.homePath ||
35
- plan.modelAuthMethods.openai !== "api_key") {
36
- return [];
37
- }
38
- return [`configure_codex_api_key_auth ${shellQuote(plan.instancePaths.homePath)}`];
39
- };
40
33
  const createMoltnetStorePrepareCommands = (serverPlan) => {
41
34
  const server = serverPlan.server;
42
35
  if (server.mode !== "managed" || server.store.kind === "memory" || server.store.kind === "postgres") {
@@ -50,11 +43,14 @@ const createMoltnetStorePrepareCommands = (serverPlan) => {
50
43
  };
51
44
  const resolveStartCommand = (plan) => plan.meta.startCommand
52
45
  .map((token) => token
46
+ .replaceAll("<config-path>", plan.instancePaths.configPath)
47
+ .replaceAll("<home-path>", plan.instancePaths.homePath ?? "")
53
48
  .replaceAll("<runtime-root>", plan.runtimeRoot)
49
+ .replaceAll("<workspace-path>", plan.instancePaths.workspacePath)
54
50
  .replaceAll("<port>", plan.port ? String(plan.port) : ""))
55
51
  .filter((token) => token.length > 0);
56
52
  const createRuntimeReadinessWait = (plan) => {
57
- if (plan.runtimeName !== "openclaw" || !plan.port) {
53
+ if (!["openclaw", "pi"].includes(plan.runtimeName) || !plan.port) {
58
54
  return [];
59
55
  }
60
56
  return [
@@ -145,15 +141,6 @@ export const renderEntrypoint = (runtimePlans, requiredSecrets, options = {}) =>
145
141
  "PY",
146
142
  "}",
147
143
  "",
148
- "configure_codex_api_key_auth() {",
149
- ' local home_path="$1"',
150
- ' if [ -z "${OPENAI_API_KEY:-}" ]; then',
151
- " return",
152
- " fi",
153
- ' mkdir -p "$home_path/.codex"',
154
- ' printf "%s\\n" "${OPENAI_API_KEY:-}" | HOME="$home_path" CODEX_HOME="$home_path/.codex" codex login --with-api-key >/dev/null',
155
- "}",
156
- "",
157
144
  ...createWorkspaceResourceShellFunctions()
158
145
  ];
159
146
  lines.push('if [ -n "${OPENCLAW_GATEWAY_TOKEN:-}" ] && [ -z "${OPENCLAW_HOOKS_TOKEN:-}" ]; then', ' export OPENCLAW_HOOKS_TOKEN="hooks-${OPENCLAW_GATEWAY_TOKEN}"', "fi", "");
@@ -171,7 +158,7 @@ export const renderEntrypoint = (runtimePlans, requiredSecrets, options = {}) =>
171
158
  const plan = runtimePlans[0];
172
159
  const commandTokens = resolveStartCommand(plan);
173
160
  const envAssignments = createEnvironmentAssignments(plan);
174
- lines.push(`mkdir -p ${shellQuote(plan.instancePaths.workspacePath)}`, ...createWorkspaceResourceCommands(plan), `require_file ${shellQuote(plan.instancePaths.configPath)}`, ...createEnvFileWrites(plan), ...createConfigEnvWrites(plan), ...createAuthSetupCommands(plan), `${envAssignments.join(" ")} exec ${commandTokens.map(shellQuote).join(" ")}`);
161
+ lines.push(`mkdir -p ${shellQuote(plan.instancePaths.workspacePath)}`, ...createWorkspaceResourceCommands(plan), `require_file ${shellQuote(plan.instancePaths.configPath)}`, ...createEnvFileWrites(plan), ...createConfigEnvWrites(plan), `${envAssignments.join(" ")} exec ${commandTokens.map(shellQuote).join(" ")}`);
175
162
  return `${lines.join("\n").trimEnd()}\n`;
176
163
  }
177
164
  lines.push("PIDS=()", "", "terminate_children() {", ' for pid in "${PIDS[@]:-}"; do', ' kill "$pid" 2>/dev/null || true', " done", "}", "", "trap terminate_children INT TERM EXIT", "");
@@ -183,25 +170,28 @@ export const renderEntrypoint = (runtimePlans, requiredSecrets, options = {}) =>
183
170
  if (!serverPlan.configPath) {
184
171
  continue;
185
172
  }
173
+ const urlEnv = networkUrlEnvName(serverPlan.networkId);
174
+ const serverLines = [];
186
175
  for (const patch of serverPlan.secretPatches) {
187
- lines.push(`apply_json_env_value ${shellQuote(serverPlan.configPath)} ${shellQuote(patch.envName)} ${shellQuote(patch.jsonPath)}`);
176
+ serverLines.push(`apply_json_env_value ${shellQuote(serverPlan.configPath)} ${shellQuote(patch.envName)} ${shellQuote(patch.jsonPath)}`);
188
177
  }
189
- lines.push(...createMoltnetStorePrepareCommands(serverPlan), `MOLTNET_CONFIG=${shellQuote(serverPlan.configPath)} /usr/local/bin/moltnet &`, 'PIDS+=("$!")', "");
190
- }
191
- for (const serverPlan of managedMoltnetServerPlans) {
192
- if (!serverPlan.port) {
193
- continue;
178
+ serverLines.push(...createMoltnetStorePrepareCommands(serverPlan), `MOLTNET_CONFIG=${shellQuote(serverPlan.configPath)} /usr/local/bin/moltnet &`, 'PIDS+=("$!")');
179
+ if (serverPlan.port) {
180
+ serverLines.push(`until curl -sf ${shellQuote(`http://127.0.0.1:${serverPlan.port}/healthz`)} >/dev/null; do sleep 1; done`);
194
181
  }
195
- lines.push(`until curl -sf ${shellQuote(`http://127.0.0.1:${serverPlan.port}/healthz`)} >/dev/null; do sleep 1; done`);
196
- lines.push("");
182
+ // Suppress the in-image managed server when an external endpoint is bound.
183
+ lines.push(`if [ -z "\${${urlEnv}:-}" ]; then`, ...serverLines.map((line) => ` ${line}`), "fi", "");
197
184
  }
198
185
  for (const plan of runtimePlans) {
199
186
  const commandTokens = resolveStartCommand(plan);
200
187
  const envAssignments = createEnvironmentAssignments(plan);
201
- lines.push(`mkdir -p ${shellQuote(plan.instancePaths.workspacePath)}`, ...createWorkspaceResourceCommands(plan), `require_file ${shellQuote(plan.instancePaths.configPath)}`, ...createEnvFileWrites(plan), ...createConfigEnvWrites(plan), ...createAuthSetupCommands(plan), `${envAssignments.join(" ")} ${commandTokens.map(shellQuote).join(" ")} &`, 'PIDS+=("$!")', "", ...createRuntimeReadinessWait(plan));
188
+ lines.push(`mkdir -p ${shellQuote(plan.instancePaths.workspacePath)}`, ...createWorkspaceResourceCommands(plan), `require_file ${shellQuote(plan.instancePaths.configPath)}`, ...createEnvFileWrites(plan), ...createConfigEnvWrites(plan), `${envAssignments.join(" ")} ${commandTokens.map(shellQuote).join(" ")} &`, 'PIDS+=("$!")', "", ...createRuntimeReadinessWait(plan));
202
189
  }
203
190
  for (const nodePlan of moltnetNodePlans) {
204
- lines.push(`/usr/local/bin/moltnet node ${shellQuote(nodePlan.configPath)} &`, 'PIDS+=("$!")', "");
191
+ const urlEnv = networkUrlEnvName(nodePlan.networkId);
192
+ lines.push(
193
+ // Rebind the bridge endpoint when an external network URL is provided.
194
+ `if [ -n "\${${urlEnv}:-}" ]; then`, ` apply_json_env_value ${shellQuote(nodePlan.configPath)} ${shellQuote(urlEnv)} ${shellQuote("moltnet.base_url")}`, "fi", `/usr/local/bin/moltnet node ${shellQuote(nodePlan.configPath)} &`, 'PIDS+=("$!")', "");
205
195
  }
206
196
  lines.push('if [ "${#PIDS[@]}" -eq 0 ]; then', ' echo "No runtime targets were generated for this compile output" >&2', " exit 1", "fi", "", "status=0", 'for pid in "${PIDS[@]}"; do', ' if ! wait "$pid"; then', " status=1", " fi", "done", "", 'exit "$status"');
207
197
  return `${lines.join("\n").trimEnd()}\n`;
@@ -0,0 +1,15 @@
1
+ import type { RuntimeTargetPlan } from "./containerArtifactsTypes.js";
2
+ import type { ResolvedPackage } from "./types.js";
3
+ export interface PackagesByManager {
4
+ apt: ResolvedPackage[];
5
+ npm: ResolvedPackage[];
6
+ pipx: ResolvedPackage[];
7
+ }
8
+ export declare const createPackageInstallCommand: (packages: string[]) => string;
9
+ export declare const createNpmPackageInstallCommand: (packages: string[]) => string;
10
+ export declare const createPipxPackageInstallCommand: (packages: string[]) => string;
11
+ export declare const createAptPackageInstallItem: (pkg: ResolvedPackage) => string;
12
+ export declare const createNpmPackageInstallItem: (pkg: ResolvedPackage) => string;
13
+ export declare const createPipxPackageInstallItem: (pkg: ResolvedPackage) => string;
14
+ export declare const getNpmInstallItemName: (item: string) => string;
15
+ export declare const collectPackagesByManager: (runtimePlans: RuntimeTargetPlan[]) => PackagesByManager;
@@ -0,0 +1,63 @@
1
+ import { SpawnfileError } from "../shared/index.js";
2
+ export const createPackageInstallCommand = (packages) => `RUN apt-get update && apt-get install -y --no-install-recommends ${packages.join(" ")} && rm -rf /var/lib/apt/lists/*`;
3
+ export const createNpmPackageInstallCommand = (packages) => `RUN npm install -g --omit=dev --no-fund --no-audit ${packages.join(" ")}`;
4
+ export const createPipxPackageInstallCommand = (packages) => `RUN PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install ${packages.join(" ")}`;
5
+ const packageIdentity = (pkg) => `${pkg.manager}\u0000${pkg.name}\u0000${pkg.version ?? ""}\u0000${pkg.scope ?? ""}`;
6
+ const packageConflictIdentity = (pkg) => `${pkg.manager}\u0000${pkg.name}`;
7
+ const dedupePackages = (packages) => {
8
+ const seen = new Map();
9
+ for (const currentPackage of packages) {
10
+ seen.set(packageIdentity(currentPackage), currentPackage);
11
+ }
12
+ return [...seen.values()].sort((left, right) => packageIdentity(left).localeCompare(packageIdentity(right)));
13
+ };
14
+ const assertImagePackageCompatibility = (packages) => {
15
+ const byName = new Map();
16
+ for (const currentPackage of packages) {
17
+ const conflictIdentity = packageConflictIdentity(currentPackage);
18
+ const existingPackage = byName.get(conflictIdentity);
19
+ if (!existingPackage) {
20
+ byName.set(conflictIdentity, currentPackage);
21
+ continue;
22
+ }
23
+ if (packageIdentity(existingPackage) !== packageIdentity(currentPackage)) {
24
+ throw new SpawnfileError("validation_error", `Generated container declares conflicting package definitions for ${currentPackage.manager} package ${currentPackage.name}`);
25
+ }
26
+ }
27
+ };
28
+ export const createAptPackageInstallItem = (pkg) => pkg.version ? `${pkg.name}=${pkg.version}` : pkg.name;
29
+ export const createNpmPackageInstallItem = (pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name;
30
+ export const createPipxPackageInstallItem = (pkg) => pkg.version ? `${pkg.name}==${pkg.version}` : pkg.name;
31
+ export const getNpmInstallItemName = (item) => {
32
+ if (!item.startsWith("@")) {
33
+ const versionMarker = item.indexOf("@");
34
+ return versionMarker === -1 ? item : item.slice(0, versionMarker);
35
+ }
36
+ const slashIndex = item.indexOf("/");
37
+ if (slashIndex === -1) {
38
+ return item;
39
+ }
40
+ const versionMarker = item.indexOf("@", slashIndex);
41
+ return versionMarker === -1 ? item : item.slice(0, versionMarker);
42
+ };
43
+ export const collectPackagesByManager = (runtimePlans) => {
44
+ const packagesByManager = {
45
+ apt: [],
46
+ npm: [],
47
+ pipx: []
48
+ };
49
+ const imagePackages = runtimePlans.flatMap((plan) => plan.packages ?? []);
50
+ assertImagePackageCompatibility(imagePackages);
51
+ for (const packageConfig of dedupePackages(imagePackages)) {
52
+ if (packageConfig.manager === "apt") {
53
+ packagesByManager.apt.push(packageConfig);
54
+ continue;
55
+ }
56
+ if (packageConfig.manager === "npm") {
57
+ packagesByManager.npm.push(packageConfig);
58
+ continue;
59
+ }
60
+ packagesByManager.pipx.push(packageConfig);
61
+ }
62
+ return packagesByManager;
63
+ };
@@ -4,13 +4,15 @@ import { mergeWorkspaceResourcePlans } from "./workspaceResources.js";
4
4
  const CONFIG_FILE_PLACEHOLDER = "<config-file>";
5
5
  const INSTANCE_ROOT_PLACEHOLDER = "<instance-root>";
6
6
  const SOURCE_AGENT_PLACEHOLDER = "<agent-name>";
7
+ const SOURCE_SLUG_PLACEHOLDER = "<source-slug>";
7
8
  const replaceSourceWorkspacePathTemplate = (template, input, instancePaths, meta) => {
8
9
  const agentName = input.value.kind === "agent" ? input.value.name : input.slug;
9
10
  const instanceRoot = instancePaths.instanceRoot ?? path.posix.dirname(instancePaths.workspacePath);
10
11
  return template
11
12
  .replaceAll(INSTANCE_ROOT_PLACEHOLDER, instanceRoot)
12
13
  .replaceAll(CONFIG_FILE_PLACEHOLDER, meta.configFileName)
13
- .replaceAll(SOURCE_AGENT_PLACEHOLDER, agentName);
14
+ .replaceAll(SOURCE_AGENT_PLACEHOLDER, agentName)
15
+ .replaceAll(SOURCE_SLUG_PLACEHOLDER, input.slug);
14
16
  };
15
17
  const resolveSourceWorkspacePath = (input, instancePaths, meta) => meta.instancePaths.sourceWorkspacePathTemplate
16
18
  ? replaceSourceWorkspacePathTemplate(meta.instancePaths.sourceWorkspacePathTemplate, input, instancePaths, meta)
@@ -1,6 +1,7 @@
1
1
  export * from "./addProjectNode.js";
2
2
  export * from "./buildCompilePlan.js";
3
3
  export * from "./buildProject.js";
4
+ export * from "./publishProject.js";
4
5
  export * from "./upProject.js";
5
6
  export * from "./compileProject.js";
6
7
  export * from "./initProject.js";
@@ -1,6 +1,7 @@
1
1
  export * from "./addProjectNode.js";
2
2
  export * from "./buildCompilePlan.js";
3
3
  export * from "./buildProject.js";
4
+ export * from "./publishProject.js";
4
5
  export * from "./upProject.js";
5
6
  export * from "./compileProject.js";
6
7
  export * from "./initProject.js";
@@ -1,5 +1,6 @@
1
1
  import { SpawnfileError } from "../shared/index.js";
2
- import { createMoltnetOpenTokenDirectory, createMoltnetNativeServerConfig, createMoltnetNodeConfigPath, createMoltnetServerConfigPath, resolveMoltnetBaseUrl, resolveMoltnetStorePersistenceMountPath } from "./moltnetConfigLowering.js";
2
+ import { createMoltnetOpenTokenDirectory, createMoltnetNativeServerConfig, createMoltnetNodeConfigPath, createMoltnetServerConfigPath, resolveMoltnetClientAuth, resolveMoltnetBaseUrl, resolveMoltnetStorePersistenceMountPath } from "./moltnetConfigLowering.js";
3
+ import { assertNetworkBindingEnvUniqueness } from "./networkBinding.js";
3
4
  import { listConcreteMoltnetRoomMemberIds } from "./moltnetRoomMemberships.js";
4
5
  import { assertCompatibleMoltnetNetworkName, assertCompatibleMoltnetRoomPolicy, assertCompatibleMoltnetServer } from "./moltnetRoomPolicyCompatibility.js";
5
6
  import { createMoltnetNodeConfigContent } from "./moltnetNodeConfig.js";
@@ -177,6 +178,21 @@ export const generateMoltnetArtifacts = async (plan) => {
177
178
  throw new SpawnfileError("validation_error", `Duplicate Moltnet node attachment for ${attachment.network}/${attachment.memberId}`);
178
179
  }
179
180
  nodePlanKeys.add(nodePlanKey);
181
+ if (agentNode.runtime.name === "pi") {
182
+ const clientAuth = resolveMoltnetClientAuth(serverPlan.server, attachment.network, attachment.memberId, node.slug);
183
+ const usesPerAttachmentOpenToken = clientAuth.mode === "open" &&
184
+ clientAuth.staticToken !== true &&
185
+ Boolean(clientAuth.tokenEnv || clientAuth.tokenPath);
186
+ if (usesPerAttachmentOpenToken && clientAuth.tokenPath) {
187
+ const mountId = `agent-${node.slug}-moltnet-tokens`;
188
+ addPersistentMount({
189
+ id: mountId,
190
+ mountPath: createMoltnetOpenTokenDirectory(node.slug),
191
+ reason: `Moltnet open-mode generated agent tokens for ${agentNode.name}`,
192
+ volumeName: createPersistentVolumeName(plan.root, mountId)
193
+ });
194
+ }
195
+ }
180
196
  const nodeConfig = createMoltnetNodeConfigContent({
181
197
  agentNode,
182
198
  attachment: { ...attachment, memberId: attachment.memberId },
@@ -207,6 +223,10 @@ export const generateMoltnetArtifacts = async (plan) => {
207
223
  }
208
224
  }
209
225
  const managedServerPlans = [...serverPlans.values()].filter((serverPlan) => serverPlan.mode === "managed");
226
+ assertNetworkBindingEnvUniqueness([...serverPlans.values()].map((serverPlan) => ({
227
+ id: serverPlan.networkId,
228
+ members: [...new Set(serverPlan.rooms.flatMap((room) => room.members))]
229
+ })));
210
230
  return {
211
231
  files: configFiles,
212
232
  nodePlans: nodePlans.sort((left, right) => left.configPath.localeCompare(right.configPath)),
@@ -1,4 +1,7 @@
1
1
  export declare const MOLTNET_BIN_DIRECTORY = "moltnet-bin";
2
2
  export declare const MOLTNET_BINARY_NAMES: readonly ["moltnet"];
3
+ export type MoltnetTargetArchitecture = "amd64" | "arm64";
3
4
  export declare const resolveMoltnetCliCommand: () => Promise<string>;
4
- export declare const stageMoltnetBinaries: (outputDirectory: string) => Promise<boolean>;
5
+ export declare const stageMoltnetBinaries: (outputDirectory: string, options?: {
6
+ architecture?: MoltnetTargetArchitecture;
7
+ }) => Promise<boolean>;
@@ -7,11 +7,32 @@ import { SpawnfileError } from "../shared/index.js";
7
7
  const execFile = promisify(execFileCallback);
8
8
  const MOLTNET_CLI_ENV = "SPAWNFILE_MOLTNET_CLI";
9
9
  const MOLTNET_RELEASE_DIR_ENV = "SPAWNFILE_MOLTNET_RELEASE_DIR";
10
+ const MOLTNET_TARGET_ARCH_ENV = "SPAWNFILE_MOLTNET_TARGET_ARCH";
10
11
  const LOCAL_MOLTNET_CLI_PATH = path.resolve(process.cwd(), "moltnet", "bin", "moltnet");
11
12
  const MOLTNET_TARGET_OS = "linux";
12
13
  export const MOLTNET_BIN_DIRECTORY = "moltnet-bin";
13
14
  export const MOLTNET_BINARY_NAMES = ["moltnet"];
14
- const resolveTargetArchitecture = () => {
15
+ const normalizeTargetArchitecture = (architecture) => {
16
+ switch (architecture) {
17
+ case "amd64":
18
+ case "x86_64":
19
+ case "x64":
20
+ return "amd64";
21
+ case "aarch64":
22
+ case "arm64":
23
+ return "arm64";
24
+ default:
25
+ throw new SpawnfileError("compile_error", `Moltnet container installs do not support target architecture ${architecture}`);
26
+ }
27
+ };
28
+ const resolveTargetArchitecture = (architecture) => {
29
+ if (architecture) {
30
+ return architecture;
31
+ }
32
+ const configuredArchitecture = process.env[MOLTNET_TARGET_ARCH_ENV]?.trim();
33
+ if (configuredArchitecture) {
34
+ return normalizeTargetArchitecture(configuredArchitecture);
35
+ }
15
36
  switch (process.arch) {
16
37
  case "arm64":
17
38
  return "arm64";
@@ -73,12 +94,12 @@ export const resolveMoltnetCliCommand = async () => {
73
94
  }
74
95
  return validateMoltnetCli("moltnet", "PATH");
75
96
  };
76
- export const stageMoltnetBinaries = async (outputDirectory) => {
97
+ export const stageMoltnetBinaries = async (outputDirectory, options = {}) => {
77
98
  const releaseDirectory = await resolveConfiguredReleaseDirectory();
78
99
  if (!releaseDirectory) {
79
100
  return false;
80
101
  }
81
- const architecture = resolveTargetArchitecture();
102
+ const architecture = resolveTargetArchitecture(options.architecture);
82
103
  const releaseAssetPath = path.join(releaseDirectory, createReleaseAssetName(architecture));
83
104
  if (!(await fileExists(releaseAssetPath))) {
84
105
  throw new SpawnfileError("compile_error", `Moltnet release asset ${releaseAssetPath} does not exist. Build it with \`cd moltnet && make release-assets\` or set ${MOLTNET_RELEASE_DIR_ENV}.`);
@@ -7,5 +7,5 @@ export interface MoltnetWorkspaceLayout {
7
7
  skillPaths: string[];
8
8
  workspaceRootPath: string;
9
9
  }
10
- export declare const resolveMoltnetWorkspaceLayout: (runtimeName: string, agentName: string) => MoltnetWorkspaceLayout;
10
+ export declare const resolveMoltnetWorkspaceLayout: (runtimeName: string, _agentName: string) => MoltnetWorkspaceLayout;
11
11
  export declare const createMoltnetClientConfigFiles: (node: ResolvedAgentNode, artifacts: MoltnetArtifacts, agentSlug?: string) => EmittedFile[];
@@ -38,8 +38,7 @@ const createAttachmentConfig = (node, artifacts, attachment, agentSlug) => {
38
38
  ? {
39
39
  dms: {
40
40
  enabled: attachment.dms.enabled,
41
- ...(attachment.dms.read ? { read: attachment.dms.read } : {}),
42
- ...(attachment.dms.reply ? { reply: attachment.dms.reply } : {})
41
+ ...(attachment.dms.wake ? { wake: attachment.dms.wake } : {})
43
42
  }
44
43
  }
45
44
  : {}),
@@ -56,32 +55,31 @@ const createAttachmentConfig = (node, artifacts, attachment, agentSlug) => {
56
55
  id: roomId,
57
56
  ...(room?.visibility ? { visibility: room.visibility } : {}),
58
57
  ...(room?.write_policy ? { write_policy: room.write_policy } : {}),
59
- ...(policy.read ? { read: policy.read } : {}),
60
- ...(policy.reply ? { reply: policy.reply } : {})
58
+ ...(policy.wake ? { wake: policy.wake } : {})
61
59
  };
62
60
  })
63
61
  }
64
62
  : {})
65
63
  };
66
64
  };
67
- export const resolveMoltnetWorkspaceLayout = (runtimeName, agentName) => {
68
- if (runtimeName === "openclaw" || runtimeName === "picoclaw") {
65
+ export const resolveMoltnetWorkspaceLayout = (runtimeName, _agentName) => {
66
+ if (runtimeName === "daimon" || runtimeName === "pi") {
69
67
  return {
70
- clientConfigPath: `workspace/${GENERATED_CONFIG_PATH}`,
71
- cliRuntime: runtimeName,
72
- skillPaths: [`workspace/skills/${GENERATED_SKILL_NAME}/SKILL.md`],
68
+ clientConfigPath: `workspace/.moltnet/config.json`,
69
+ cliRuntime: "codex",
70
+ skillPaths: [
71
+ `workspace/.agents/skills/${GENERATED_SKILL_NAME}/SKILL.md`,
72
+ `workspace/.codex/skills/${GENERATED_SKILL_NAME}/SKILL.md`
73
+ ],
73
74
  workspaceRootPath: "workspace"
74
75
  };
75
76
  }
76
- if (runtimeName === "tinyclaw") {
77
+ if (runtimeName === "openclaw" || runtimeName === "picoclaw") {
77
78
  return {
78
- clientConfigPath: `workspace/${agentName}/${GENERATED_CONFIG_PATH}`,
79
+ clientConfigPath: `workspace/${GENERATED_CONFIG_PATH}`,
79
80
  cliRuntime: runtimeName,
80
- skillPaths: [
81
- `workspace/${agentName}/.agents/skills/${GENERATED_SKILL_NAME}/SKILL.md`,
82
- `workspace/${agentName}/.claude/skills/${GENERATED_SKILL_NAME}/SKILL.md`
83
- ],
84
- workspaceRootPath: `workspace/${agentName}`
81
+ skillPaths: [`workspace/skills/${GENERATED_SKILL_NAME}/SKILL.md`],
82
+ workspaceRootPath: "workspace"
85
83
  };
86
84
  }
87
85
  throw new SpawnfileError("compile_error", `Moltnet client config does not know how to emit files for runtime ${runtimeName}`);
@@ -47,8 +47,7 @@ export const createMoltnetNodeConfigContent = ({ agentNode, attachment, networkS
47
47
  id: roomId,
48
48
  ...(room?.visibility ? { visibility: room.visibility } : {}),
49
49
  ...(room?.write_policy ? { write_policy: room.write_policy } : {}),
50
- ...(policy.read ? { read: policy.read } : {}),
51
- ...(policy.reply ? { reply: policy.reply } : {})
50
+ ...(policy.wake ? { wake: policy.wake } : {})
52
51
  };
53
52
  })
54
53
  }
@@ -57,8 +56,7 @@ export const createMoltnetNodeConfigContent = ({ agentNode, attachment, networkS
57
56
  ? {
58
57
  dms: {
59
58
  enabled: attachment.dms.enabled,
60
- ...(attachment.dms.read ? { read: attachment.dms.read } : {}),
61
- ...(attachment.dms.reply ? { reply: attachment.dms.reply } : {})
59
+ ...(attachment.dms.wake ? { wake: attachment.dms.wake } : {})
62
60
  }
63
61
  }
64
62
  : {})
@@ -52,7 +52,7 @@ const synthesizeRepresentativeAttachments = (memberships) => memberships
52
52
  teamSource: membership.declaringTeamSource
53
53
  }));
54
54
  const roomPolicyKey = (policy) => JSON.stringify(policy ?? {});
55
- const hasRoomPolicy = (policy) => policy.read !== undefined || policy.reply !== undefined;
55
+ const hasRoomPolicy = (policy) => policy.wake !== undefined;
56
56
  const mergeAttachment = (target, next, nodeName) => {
57
57
  if (target.dms &&
58
58
  next.dms &&
@@ -16,8 +16,7 @@ const findTeamBySource = (plan, source) => {
16
16
  return node.value;
17
17
  };
18
18
  const clonePolicy = (policy) => ({
19
- ...(policy.read ? { read: policy.read } : {}),
20
- ...(policy.reply ? { reply: policy.reply } : {})
19
+ ...(policy.wake ? { wake: policy.wake } : {})
21
20
  });
22
21
  const findDirectRoomPolicy = (plan, teamNode, agentSource, memberId, networkId, roomId) => {
23
22
  const agentNode = findAgentBySource(plan, agentSource);
@@ -1,2 +1,2 @@
1
1
  import type { CompilePlan, ResolvedAgentNode } from "./types.js";
2
- export declare const resolveRuntimeConfig: (plan: CompilePlan, agentNode: ResolvedAgentNode, nodeSlug: string, networkId: string, agentId: string) => Record<string, string>;
2
+ export declare const resolveRuntimeConfig: (plan: CompilePlan, agentNode: ResolvedAgentNode, nodeSlug: string, _networkId: string, _agentId: string) => Record<string, string>;
@@ -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,14 +51,15 @@ 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);
54
+ case "daimon":
55
+ case "pi": {
56
+ const port = getRuntimeAdapter(agentNode.runtime.name).container.port;
57
+ if (!port) {
58
+ throw new SpawnfileError("compile_error", `Unable to resolve ${agentNode.runtime.name} control port for Moltnet agent ${agentNode.name}`);
59
+ }
58
60
  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)}`
61
+ control_url: `http://127.0.0.1:${port}/agents/${nodeSlug}/wake`,
62
+ kind: "pi"
64
63
  };
65
64
  }
66
65
  default:
@@ -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
+ };