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
@@ -0,0 +1,71 @@
1
+ import { execFile as execFileCallback } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { compileProject, normalizeDockerArchitecture } from "../compiler/index.js";
4
+ import { dockerContextNameForTarget, dockerHostValueForTarget } from "../deployment/index.js";
5
+ import { SpawnfileError } from "../shared/index.js";
6
+ export const defaultExecFile = promisify(execFileCallback);
7
+ export const dockerArgsForRecord = (record) => {
8
+ const context = dockerContextNameForTarget(record.target);
9
+ if (context) {
10
+ return ["--context", context];
11
+ }
12
+ const host = dockerHostValueForTarget(record.target);
13
+ return host ? ["--host", host] : [];
14
+ };
15
+ export const firstContainerName = (record) => {
16
+ const unit = record.units.find((candidate) => candidate.kind === "container") ?? record.units[0];
17
+ const name = unit?.container_name;
18
+ if (!name) {
19
+ throw new SpawnfileError("runtime_error", `Deployment "${record.name}" has no recorded container name`);
20
+ }
21
+ return name;
22
+ };
23
+ export const compileForDevApply = async (inputPath, outputDirectory, record, dockerCommand, execFileImpl) => {
24
+ const dockerContext = dockerContextNameForTarget(record.target) ?? undefined;
25
+ const architecture = dockerContext
26
+ ? normalizeDockerArchitecture((await execFileImpl(dockerCommand, ["--context", dockerContext, "info", "--format", "{{.Architecture}}"], { timeout: 10_000 })).stdout)
27
+ : undefined;
28
+ return compileProject(inputPath, {
29
+ clean: false,
30
+ containerArchitecture: architecture,
31
+ outputDirectory
32
+ });
33
+ };
34
+ export const runDocker = async (command, args, execFileImpl, timeout = 30_000) => {
35
+ const { stdout } = await execFileImpl(command, args, { timeout });
36
+ return stdout;
37
+ };
38
+ export const copyIntoContainer = async (dockerCommand, dockerPrefix, source, destination, execFileImpl) => {
39
+ await runDocker(dockerCommand, [...dockerPrefix, "cp", source, destination], execFileImpl, 60_000);
40
+ };
41
+ export const ensureContainerDirectories = async (dockerCommand, dockerPrefix, containerName, paths, execFileImpl) => {
42
+ if (paths.length === 0) {
43
+ return;
44
+ }
45
+ await runDocker(dockerCommand, [
46
+ ...dockerPrefix,
47
+ "exec",
48
+ "--user",
49
+ "root",
50
+ containerName,
51
+ "mkdir",
52
+ "-p",
53
+ ...paths
54
+ ], execFileImpl);
55
+ };
56
+ export const chownContainerPaths = async (dockerCommand, dockerPrefix, containerName, paths, execFileImpl) => {
57
+ if (paths.length === 0) {
58
+ return;
59
+ }
60
+ await runDocker(dockerCommand, [
61
+ ...dockerPrefix,
62
+ "exec",
63
+ "--user",
64
+ "root",
65
+ containerName,
66
+ "chown",
67
+ "-R",
68
+ "spawnfile:spawnfile",
69
+ ...paths
70
+ ], execFileImpl);
71
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./activity.js";
2
+ export * from "./project.js";
@@ -0,0 +1,2 @@
1
+ export * from "./activity.js";
2
+ export * from "./project.js";
@@ -0,0 +1,35 @@
1
+ import { type UpProjectOptions, type UpProjectResult } from "../compiler/index.js";
2
+ import { type DeploymentRecord } from "../deployment/index.js";
3
+ import { type DevExecFile } from "./docker.js";
4
+ export declare const PI_CONTROL_URL = "http://127.0.0.1:19690";
5
+ export interface DevProjectOptions {
6
+ deploymentName?: string;
7
+ dockerCommand?: string;
8
+ outputDirectory?: string;
9
+ }
10
+ export interface DevApplyProjectOptions extends DevProjectOptions {
11
+ agent: string;
12
+ execFile?: DevExecFile;
13
+ restart?: boolean;
14
+ }
15
+ export interface DevApplyProjectResult {
16
+ agentId: string;
17
+ bridgeStarted: boolean;
18
+ containerName: string;
19
+ deploymentName: string;
20
+ existingAgent: boolean;
21
+ outputDirectory: string;
22
+ }
23
+ export interface DevStopProjectResult {
24
+ containerName: string;
25
+ deploymentName: string;
26
+ outputDirectory: string;
27
+ }
28
+ export declare const resolveDevOutputDirectory: (inputPath: string, outputDirectory?: string) => Promise<string>;
29
+ export declare const devUpProject: (inputPath: string, options?: UpProjectOptions) => Promise<UpProjectResult>;
30
+ export declare const selectDeploymentRecord: (outputDirectory: string, deploymentName?: string) => Promise<DeploymentRecord>;
31
+ export declare const devApplyProject: (inputPath: string, options: DevApplyProjectOptions) => Promise<DevApplyProjectResult>;
32
+ export declare const devRestartProject: (inputPath: string, options: DevApplyProjectOptions) => Promise<DevApplyProjectResult>;
33
+ export declare const devStopProject: (inputPath: string, options?: DevProjectOptions & {
34
+ execFile?: DevExecFile;
35
+ }) => Promise<DevStopProjectResult>;
@@ -0,0 +1,164 @@
1
+ import path from "node:path";
2
+ import { readFile } from "node:fs/promises";
3
+ import { buildCompilePlan, upProject } from "../compiler/index.js";
4
+ import { listDeploymentRecords, normalizeDeploymentName } from "../deployment/index.js";
5
+ import { SpawnfileError } from "../shared/index.js";
6
+ import { chownContainerPaths, compileForDevApply, copyIntoContainer, defaultExecFile, dockerArgsForRecord, ensureContainerDirectories, firstContainerName, runDocker } from "./docker.js";
7
+ const DEV_OUTPUT_DIRECTORY = ".spawn-dev";
8
+ const PI_APP_CONFIG_PATH = "/var/lib/spawnfile/instances/pi/pi-app/pi/pi-app.json";
9
+ export const PI_CONTROL_URL = "http://127.0.0.1:19690";
10
+ export const resolveDevOutputDirectory = async (inputPath, outputDirectory) => {
11
+ if (outputDirectory) {
12
+ return path.resolve(outputDirectory);
13
+ }
14
+ const plan = await buildCompilePlan(inputPath);
15
+ return path.join(path.dirname(plan.root), DEV_OUTPUT_DIRECTORY);
16
+ };
17
+ export const devUpProject = async (inputPath, options = {}) => upProject(inputPath, {
18
+ ...options,
19
+ detach: true,
20
+ outputDirectory: await resolveDevOutputDirectory(inputPath, options.outputDirectory)
21
+ });
22
+ export const selectDeploymentRecord = async (outputDirectory, deploymentName) => {
23
+ const records = await listDeploymentRecords(outputDirectory);
24
+ if (records.length === 0) {
25
+ throw new SpawnfileError("validation_error", `No dev deployment records found in ${outputDirectory}; run spawnfile dev up first`);
26
+ }
27
+ if (deploymentName) {
28
+ const normalized = normalizeDeploymentName(deploymentName);
29
+ const selected = records.find((entry) => entry.record.name === normalized);
30
+ if (!selected) {
31
+ throw new SpawnfileError("validation_error", `Unknown dev deployment "${normalized}". Valid deployments: ${records.map((entry) => entry.record.name).join(", ")}`);
32
+ }
33
+ return selected.record;
34
+ }
35
+ if (records.length > 1) {
36
+ throw new SpawnfileError("validation_error", `--deployment is required: ${records.map((entry) => entry.record.name).join(", ")}`);
37
+ }
38
+ return records[0].record;
39
+ };
40
+ const readJson = async (filePath) => JSON.parse(await readFile(filePath, "utf8"));
41
+ const rootfsPath = (outputDirectory, containerPath) => path.join(outputDirectory, "container", "rootfs", containerPath.replace(/^\/+/u, ""));
42
+ const matchesAgent = (agent, input) => agent.slug === input || agent.id === input || agent.name === input || agent.id === `agent:${input}`;
43
+ const selectPiAgent = async (outputDirectory, agentInput) => {
44
+ const configPath = rootfsPath(outputDirectory, PI_APP_CONFIG_PATH);
45
+ const config = await readJson(configPath);
46
+ const agent = (config.agents ?? []).find((candidate) => matchesAgent(candidate, agentInput));
47
+ if (!agent) {
48
+ throw new SpawnfileError("validation_error", `Pi agent "${agentInput}" was not found in compiled output`);
49
+ }
50
+ return {
51
+ agent,
52
+ configPath,
53
+ workspacePath: rootfsPath(outputDirectory, `/var/lib/spawnfile/instances/pi/pi-app/workspace/agents/${agent.slug}`)
54
+ };
55
+ };
56
+ const selectMoltnetNodes = async (result, agent) => {
57
+ const nodePlans = result.report.container?.moltnet?.node_plans ?? [];
58
+ const matches = [];
59
+ for (const plan of nodePlans) {
60
+ const configPath = rootfsPath(result.outputDirectory, plan.config_path);
61
+ const config = await readJson(configPath);
62
+ const attachment = (config.attachments ?? []).find((candidate) => {
63
+ const id = candidate.agent?.id ?? "";
64
+ const name = candidate.agent?.name ?? "";
65
+ return id === agent.slug || id === agent.id || name === agent.name;
66
+ });
67
+ if (attachment?.agent?.id) {
68
+ matches.push({
69
+ configPath,
70
+ remotePath: plan.config_path,
71
+ ...(attachment.moltnet?.token_path
72
+ ? { tokenDirectory: path.posix.dirname(attachment.moltnet.token_path) }
73
+ : {})
74
+ });
75
+ }
76
+ }
77
+ return matches;
78
+ };
79
+ const selectManagedMoltnetServers = (result) => (result.report.container?.moltnet?.server_plans ?? [])
80
+ .filter((plan) => plan.mode === "managed" && Boolean(plan.config_path))
81
+ .map((plan) => ({
82
+ configPath: rootfsPath(result.outputDirectory, plan.config_path),
83
+ remotePath: plan.config_path
84
+ }));
85
+ const shellQuote = (value) => `'${value.replaceAll("'", "'\"'\"'")}'`;
86
+ const listRunningPiAgents = async (dockerCommand, dockerPrefix, containerName, execFileImpl) => {
87
+ const stdout = await runDocker(dockerCommand, [...dockerPrefix, "exec", containerName, "curl", "-fsS", `${PI_CONTROL_URL}/spawnfile/agents`], execFileImpl);
88
+ const parsed = JSON.parse(stdout);
89
+ return parsed.agents ?? [];
90
+ };
91
+ const loadPiAgent = async (dockerCommand, dockerPrefix, containerName, slug, action, execFileImpl) => {
92
+ await runDocker(dockerCommand, [
93
+ ...dockerPrefix,
94
+ "exec",
95
+ containerName,
96
+ "curl",
97
+ "-fsS",
98
+ "-X",
99
+ "POST",
100
+ "-H",
101
+ "content-type: application/json",
102
+ "--data",
103
+ JSON.stringify({ slug }),
104
+ `${PI_CONTROL_URL}/spawnfile/agents/${action}`
105
+ ], execFileImpl, 120_000);
106
+ };
107
+ export const devApplyProject = async (inputPath, options) => {
108
+ const outputDirectory = await resolveDevOutputDirectory(inputPath, options.outputDirectory);
109
+ const record = await selectDeploymentRecord(outputDirectory, options.deploymentName);
110
+ const dockerCommand = options.dockerCommand ?? "docker";
111
+ const dockerPrefix = dockerArgsForRecord(record);
112
+ const containerName = firstContainerName(record);
113
+ const execFileImpl = options.execFile ?? defaultExecFile;
114
+ const existingAgents = await listRunningPiAgents(dockerCommand, dockerPrefix, containerName, execFileImpl);
115
+ const compileResult = await compileForDevApply(inputPath, outputDirectory, record, dockerCommand, execFileImpl);
116
+ const { agent, configPath, workspacePath } = await selectPiAgent(outputDirectory, options.agent);
117
+ const nodes = await selectMoltnetNodes(compileResult, agent);
118
+ const serverConfigs = selectManagedMoltnetServers(compileResult);
119
+ const existingAgent = existingAgents.some((candidate) => matchesAgent(candidate, agent.slug));
120
+ await copyIntoContainer(dockerCommand, dockerPrefix, configPath, `${containerName}:${PI_APP_CONFIG_PATH}`, execFileImpl);
121
+ await copyIntoContainer(dockerCommand, dockerPrefix, workspacePath, `${containerName}:/var/lib/spawnfile/instances/pi/pi-app/workspace/agents`, execFileImpl);
122
+ for (const node of nodes) {
123
+ await copyIntoContainer(dockerCommand, dockerPrefix, node.configPath, `${containerName}:${node.remotePath}`, execFileImpl);
124
+ }
125
+ for (const serverConfig of serverConfigs) {
126
+ await copyIntoContainer(dockerCommand, dockerPrefix, serverConfig.configPath, `${containerName}:${serverConfig.remotePath}`, execFileImpl);
127
+ }
128
+ const tokenDirectories = nodes
129
+ .map((node) => node.tokenDirectory)
130
+ .filter((directory) => Boolean(directory));
131
+ await ensureContainerDirectories(dockerCommand, dockerPrefix, containerName, tokenDirectories, execFileImpl);
132
+ await chownContainerPaths(dockerCommand, dockerPrefix, containerName, [
133
+ "/var/lib/spawnfile/instances/pi/pi-app",
134
+ ...nodes.flatMap((node) => [
135
+ node.remotePath,
136
+ ...(node.tokenDirectory ? [node.tokenDirectory] : [])
137
+ ]),
138
+ ...serverConfigs.map((serverConfig) => serverConfig.remotePath)
139
+ ], execFileImpl);
140
+ await loadPiAgent(dockerCommand, dockerPrefix, containerName, agent.slug, options.restart === true ? "restart" : "load", execFileImpl);
141
+ if (!existingAgent) {
142
+ for (const node of nodes) {
143
+ const prepare = node.tokenDirectory ? `mkdir -p ${shellQuote(node.tokenDirectory)} && ` : "";
144
+ const command = `${prepare}/usr/local/bin/moltnet node ${shellQuote(node.remotePath)}`;
145
+ await runDocker(dockerCommand, [...dockerPrefix, "exec", "-d", "--user", "spawnfile", containerName, "sh", "-lc", command], execFileImpl);
146
+ }
147
+ }
148
+ return {
149
+ agentId: agent.id,
150
+ bridgeStarted: !existingAgent && nodes.length > 0,
151
+ containerName,
152
+ deploymentName: record.name,
153
+ existingAgent,
154
+ outputDirectory
155
+ };
156
+ };
157
+ export const devRestartProject = async (inputPath, options) => devApplyProject(inputPath, { ...options, restart: true });
158
+ export const devStopProject = async (inputPath, options = {}) => {
159
+ const outputDirectory = await resolveDevOutputDirectory(inputPath, options.outputDirectory);
160
+ const record = await selectDeploymentRecord(outputDirectory, options.deploymentName);
161
+ const containerName = firstContainerName(record);
162
+ await runDocker(options.dockerCommand ?? "docker", [...dockerArgsForRecord(record), "rm", "-f", containerName], options.execFile ?? defaultExecFile);
163
+ return { containerName, deploymentName: record.name, outputDirectory };
164
+ };
@@ -0,0 +1,22 @@
1
+ import type { DistributionImageLabels, DistributionMoltnetNetwork, DistributionOrganizationSummary, DistributionPersistentMount, DistributionPortMapping, DistributionReport, DistributionRuntimeInstance, DistributionSecretCategory, DistributionWorkspaceResource } from "./types.js";
2
+ export interface DistributionEnvVariableInput {
3
+ categories: DistributionSecretCategory[];
4
+ generated: boolean;
5
+ name: string;
6
+ required: boolean;
7
+ }
8
+ export interface BuildDistributionReportInput {
9
+ envVariables: DistributionEnvVariableInput[];
10
+ generatedAt: string;
11
+ internalPorts: number[];
12
+ modelAuthMethods: Record<string, DistributionRuntimeInstance["model_auth_methods"][string]>;
13
+ moltnetNetworks: DistributionMoltnetNetwork[];
14
+ organization: DistributionOrganizationSummary;
15
+ persistentMounts: DistributionPersistentMount[];
16
+ portMappings: DistributionPortMapping[];
17
+ publishedPorts: number[];
18
+ resources: DistributionWorkspaceResource[];
19
+ runtimeInstances: DistributionRuntimeInstance[];
20
+ }
21
+ export declare const buildDistributionReport: (input: BuildDistributionReportInput) => DistributionReport;
22
+ export declare const createDistributionImageLabels: (projectSlug: string, compileFingerprint: string) => DistributionImageLabels;
@@ -0,0 +1,51 @@
1
+ import { createDistributionFingerprint } from "./fingerprint.js";
2
+ import { DISTRIBUTION_REPORT_IMAGE_PATH, DISTRIBUTION_REPORT_VERSION, IMAGE_CONTRACT_VERSION } from "./types.js";
3
+ const createSecretCategoryMap = (envVariables) => {
4
+ const secrets = {
5
+ model: [],
6
+ project: [],
7
+ runtime: [],
8
+ surface: []
9
+ };
10
+ for (const variable of envVariables) {
11
+ for (const category of variable.categories) {
12
+ secrets[category].push({
13
+ generated: variable.generated,
14
+ name: variable.name,
15
+ required: variable.required
16
+ });
17
+ }
18
+ }
19
+ for (const category of Object.keys(secrets)) {
20
+ secrets[category].sort((left, right) => left.name.localeCompare(right.name));
21
+ }
22
+ return secrets;
23
+ };
24
+ export const buildDistributionReport = (input) => {
25
+ const body = {
26
+ internal_ports: [...input.internalPorts].sort((left, right) => left - right),
27
+ model_auth_methods: input.modelAuthMethods,
28
+ moltnet: {
29
+ networks: [...input.moltnetNetworks].sort((left, right) => left.id.localeCompare(right.id))
30
+ },
31
+ organization: input.organization,
32
+ persistent_mounts: [...input.persistentMounts].sort((left, right) => left.id.localeCompare(right.id)),
33
+ port_mappings: input.portMappings,
34
+ ports: [...input.publishedPorts].sort((left, right) => left - right),
35
+ resources: input.resources,
36
+ runtime_instances: [...input.runtimeInstances].sort((left, right) => left.id.localeCompare(right.id)),
37
+ secrets: createSecretCategoryMap(input.envVariables),
38
+ version: DISTRIBUTION_REPORT_VERSION
39
+ };
40
+ return {
41
+ compile_fingerprint: createDistributionFingerprint(body),
42
+ generated_at: input.generatedAt,
43
+ ...body
44
+ };
45
+ };
46
+ export const createDistributionImageLabels = (projectSlug, compileFingerprint) => ({
47
+ "com.spawnfile.compile_fingerprint": compileFingerprint,
48
+ "com.spawnfile.image_contract": IMAGE_CONTRACT_VERSION,
49
+ "com.spawnfile.project": projectSlug,
50
+ "com.spawnfile.report": DISTRIBUTION_REPORT_IMAGE_PATH
51
+ });
@@ -0,0 +1,29 @@
1
+ import { type DeploymentRecord } from "../deployment/index.js";
2
+ import type { DockerCommandRunner } from "./dockerRunner.js";
3
+ import type { ResolvedAuthProfile } from "../auth/index.js";
4
+ export interface ConsumeImageUpOptions {
5
+ authProfile?: ResolvedAuthProfile | null;
6
+ authValues?: Record<string, string>;
7
+ authProfileName?: string | null;
8
+ deploymentName?: string;
9
+ dockerCommand?: string;
10
+ dockerContext?: string;
11
+ dockerHost?: string;
12
+ envFileEnv?: Record<string, string>;
13
+ envFilePath?: string | null;
14
+ pull?: boolean;
15
+ runDocker?: DockerCommandRunner;
16
+ }
17
+ export interface ConsumeImageUpResult {
18
+ containerName: string;
19
+ deploymentName: string;
20
+ imageRef: string;
21
+ /** The ref/digest this redeploy replaced, or null for a first deploy. */
22
+ previous: {
23
+ digest: string | null;
24
+ ref: string;
25
+ } | null;
26
+ record: DeploymentRecord;
27
+ recordPath: string;
28
+ }
29
+ export declare const consumeImageUp: (imageRef: string, options?: ConsumeImageUpOptions) => Promise<ConsumeImageUpResult>;
@@ -0,0 +1,254 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import { acquireHomeDeploymentLock, createDockerDeploymentLabels, homeDeploymentExists, normalizeDeploymentName, readHomeDeploymentRecord, resolveDockerDeploymentTarget, verifyDockerDeploymentTarget, writeHomeDeployment } from "../deployment/index.js";
5
+ import { SpawnfileError } from "../shared/index.js";
6
+ import { deriveDeploymentName, deriveVolumeName, renderEnvFileContent, resolveImageEnvironment } from "./consumeImageSupport.js";
7
+ import { createConsumerDockerRunner } from "./dockerRunner.js";
8
+ import { extractImageReport, resolveDockerBaseArgs } from "./extractImage.js";
9
+ import { parseImageReference } from "./imageRef.js";
10
+ import { prepareImageRuntimeAuthMounts } from "./imageRuntimeAuth.js";
11
+ import { runImagePreflight } from "./preflight.js";
12
+ import { normalizeProjectLabelSlug } from "./projectName.js";
13
+ const containerNameFor = (deploymentName) => `spawnfile-${deploymentName}`;
14
+ const unitIdFor = (deploymentName) => `${deploymentName}-container`;
15
+ const resolveRegistryDigest = async (imageRef, runDocker) => {
16
+ const fromRef = imageRef.includes("@") ? imageRef.slice(imageRef.indexOf("@") + 1) : null;
17
+ if (fromRef) {
18
+ return fromRef;
19
+ }
20
+ try {
21
+ const raw = (await runDocker([
22
+ "image",
23
+ "inspect",
24
+ "--format",
25
+ "{{json .RepoDigests}}",
26
+ imageRef
27
+ ])).toString("utf8").trim();
28
+ const digests = JSON.parse(raw);
29
+ const match = digests
30
+ .map((entry) => (entry.includes("@") ? entry.slice(entry.indexOf("@") + 1) : null))
31
+ .find((entry) => Boolean(entry));
32
+ return match ?? null;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ };
38
+ const containerExists = async (runDocker, containerName) => {
39
+ try {
40
+ await runDocker(["container", "inspect", containerName]);
41
+ return true;
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ };
47
+ const resolveLocalImageId = async (imageRef, runDocker) => {
48
+ try {
49
+ const raw = (await runDocker([
50
+ "image",
51
+ "inspect",
52
+ "--format",
53
+ "{{.Id}}",
54
+ imageRef
55
+ ])).toString("utf8").trim();
56
+ return raw.length > 0 ? raw : null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ };
62
+ const networkIdsFrom = (report) => report.moltnet.networks.map((network) => network.id).sort();
63
+ const buildContainsEntries = (report) => [
64
+ ...report.organization.agents.map((agent) => ({ id: agent.id, kind: "agent" })),
65
+ ...report.organization.teams.map((team) => ({ id: team.id, kind: "team" })),
66
+ ...networkIdsFrom(report).map((id) => ({ id, kind: "network" }))
67
+ ];
68
+ export const consumeImageUp = async (imageRef, options = {}) => {
69
+ const parsedRef = parseImageReference(imageRef);
70
+ if (!parsedRef) {
71
+ throw new SpawnfileError("validation_error", `Invalid image reference: ${imageRef}`);
72
+ }
73
+ const deploymentName = normalizeDeploymentName(options.deploymentName ?? deriveDeploymentName(parsedRef));
74
+ const isExplicitName = Boolean(options.deploymentName);
75
+ // Hold an exclusive lock for the whole operation so a concurrent `up` for the
76
+ // same deployment cannot race on the record or orphan a container.
77
+ const releaseLock = await acquireHomeDeploymentLock(deploymentName);
78
+ try {
79
+ return await consumeImageUpLocked(imageRef, parsedRef, deploymentName, isExplicitName, options);
80
+ }
81
+ finally {
82
+ await releaseLock();
83
+ }
84
+ };
85
+ const consumeImageUpLocked = async (imageRef, parsedRef, deploymentName, isExplicitName, options) => {
86
+ void parsedRef;
87
+ const alreadyExists = await homeDeploymentExists(deploymentName);
88
+ if (alreadyExists && !isExplicitName) {
89
+ throw new SpawnfileError("validation_error", `Deployment "${deploymentName}" already exists (derived from image ${imageRef}). ` +
90
+ `To redeploy it, re-run with --deployment ${deploymentName}; ` +
91
+ "to create a separate one, pass a different --deployment <name>.");
92
+ }
93
+ const dockerCommand = options.dockerCommand ?? "docker";
94
+ const baseArgs = resolveDockerBaseArgs({
95
+ dockerContext: options.dockerContext,
96
+ dockerHost: options.dockerHost
97
+ });
98
+ const inspection = await extractImageReport(imageRef, {
99
+ dockerCommand,
100
+ dockerContext: options.dockerContext,
101
+ dockerHost: options.dockerHost,
102
+ pull: options.pull,
103
+ runDocker: options.runDocker
104
+ });
105
+ const report = inspection.report;
106
+ // Resolve the full environment first (auth profile, env file, process-env
107
+ // overrides, and generated runtime tokens), then preflight against it so
108
+ // required secrets supplied via any of those sources satisfy the check.
109
+ const env = resolveImageEnvironment({
110
+ authValues: options.authValues ?? {},
111
+ envFileEnv: options.envFileEnv,
112
+ report
113
+ });
114
+ const availableImports = options.authProfile
115
+ ? ["claude-code", "codex"].filter((kind) => options.authProfile.imports[kind])
116
+ : [];
117
+ runImagePreflight({ authValues: env, availableImports, report });
118
+ const target = await resolveExistingOrNewTarget(deploymentName, alreadyExists, options);
119
+ // Capture what this redeploy replaces so the caller can show previous → new
120
+ // ref/digest, per specs/DISTRIBUTION.md. Null on a first deploy.
121
+ const previous = alreadyExists ? await readPreviousSource(deploymentName) : null;
122
+ const runDocker = options.runDocker ?? createConsumerDockerRunner(dockerCommand, baseArgs);
123
+ const containerName = containerNameFor(deploymentName);
124
+ const projectSlug = normalizeProjectLabelSlug(report.organization.project);
125
+ const workDir = await mkdtemp(path.join(os.tmpdir(), "spawnfile-consume-"));
126
+ const envFilePath = path.join(workDir, "container.env");
127
+ // A redeploy reuses the live container's published host ports, and Docker
128
+ // cannot bind a host port twice. So when a live container exists we move it
129
+ // aside (rename + stop) to free its name and ports, start the new container,
130
+ // and on failure restore the old one — non-destructive without needing a free
131
+ // port for two containers at once. There is a brief restart gap, unavoidable
132
+ // when reusing host ports without a proxy.
133
+ const backupName = `${containerName}-previous-${Date.now().toString(36)}`;
134
+ try {
135
+ await writeFile(envFilePath, renderEnvFileContent(env), "utf8");
136
+ // Prepare import-based model auth (the consumer's Claude/Codex login) and
137
+ // validate its credentials BEFORE the destructive swap below. The OAuth-mode
138
+ // config is already baked into the image; this only resolves the credential
139
+ // mounts. Doing it first guarantees an unusable import fails while the live
140
+ // container is still untouched, never mid-swap.
141
+ const authMountArgs = options.authProfile && availableImports.length > 0
142
+ ? (await prepareImageRuntimeAuthMounts({
143
+ authProfile: options.authProfile,
144
+ report,
145
+ tempRoot: workDir
146
+ })).mountArgs
147
+ : [];
148
+ const liveExists = await containerExists(runDocker, containerName);
149
+ if (liveExists) {
150
+ await runDocker(["rename", containerName, backupName]);
151
+ await runDocker(["stop", backupName]).catch(() => undefined);
152
+ }
153
+ const runArgs = ["run", "-d", "--name", containerName, "--env-file", envFilePath];
154
+ for (const port of report.ports) {
155
+ runArgs.push("-p", `${port}:${port}`);
156
+ }
157
+ for (const mount of report.persistent_mounts) {
158
+ runArgs.push("-v", `${deriveVolumeName(deploymentName, mount.id)}:${mount.target}`);
159
+ }
160
+ runArgs.push(...authMountArgs);
161
+ const labels = createDockerDeploymentLabels({
162
+ compileFingerprint: inspection.compileFingerprint,
163
+ deployment: deploymentName,
164
+ project: projectSlug,
165
+ unit: unitIdFor(deploymentName),
166
+ version: "0.1"
167
+ });
168
+ for (const [name, value] of Object.entries(labels)) {
169
+ runArgs.push("--label", `${name}=${value}`);
170
+ }
171
+ runArgs.push(imageRef);
172
+ let runOutput;
173
+ try {
174
+ runOutput = (await runDocker(runArgs)).toString("utf8").trim();
175
+ }
176
+ catch (error) {
177
+ // The new container failed to start; remove the failed attempt and restore
178
+ // the previous deployment so a failed redeploy never loses the live one.
179
+ await runDocker(["rm", "-f", containerName]).catch(() => undefined);
180
+ if (liveExists) {
181
+ await runDocker(["rename", backupName, containerName]).catch(() => undefined);
182
+ await runDocker(["start", containerName]).catch(() => undefined);
183
+ }
184
+ throw error;
185
+ }
186
+ // The new container is up — discard the previous one.
187
+ if (liveExists) {
188
+ await runDocker(["rm", "-f", backupName]).catch(() => undefined);
189
+ }
190
+ const containerId = runOutput.split("\n").pop()?.trim() || null;
191
+ const imageId = await resolveLocalImageId(imageRef, runDocker);
192
+ const digest = await resolveRegistryDigest(imageRef, runDocker);
193
+ const record = {
194
+ auth_profile: options.authProfileName ?? null,
195
+ compile_fingerprint: inspection.compileFingerprint,
196
+ created_at: new Date().toISOString(),
197
+ ...(options.envFilePath ? { env_file: path.resolve(options.envFilePath) } : {}),
198
+ manager: "docker",
199
+ name: deploymentName,
200
+ output_directory: null,
201
+ source: { digest, kind: "image", ref: imageRef },
202
+ target,
203
+ units: [
204
+ {
205
+ container_id: containerId,
206
+ container_name: containerName,
207
+ contains: buildContainsEntries(report),
208
+ id: unitIdFor(deploymentName),
209
+ image_id: imageId,
210
+ image_tag: imageRef,
211
+ kind: "container",
212
+ runtime_instances: report.runtime_instances.map((instance) => instance.id).sort()
213
+ }
214
+ ],
215
+ version: "spawnfile.deployment.v2"
216
+ };
217
+ const written = await writeHomeDeployment(record, report);
218
+ return {
219
+ containerName,
220
+ deploymentName,
221
+ imageRef,
222
+ previous,
223
+ record,
224
+ recordPath: written.recordPath
225
+ };
226
+ }
227
+ finally {
228
+ await rm(workDir, { force: true, recursive: true }).catch(() => undefined);
229
+ }
230
+ };
231
+ const readPreviousSource = async (deploymentName) => {
232
+ try {
233
+ const existing = await readHomeDeploymentRecord(deploymentName);
234
+ if (existing.source.kind !== "image") {
235
+ return null;
236
+ }
237
+ return { digest: existing.source.digest, ref: existing.source.ref };
238
+ }
239
+ catch {
240
+ return null;
241
+ }
242
+ };
243
+ const resolveExistingOrNewTarget = async (deploymentName, alreadyExists, options) => {
244
+ if (alreadyExists && !options.dockerContext && !options.dockerHost) {
245
+ const existing = await readHomeDeploymentRecord(deploymentName);
246
+ await verifyDockerDeploymentTarget(existing.target, { dockerCommand: options.dockerCommand });
247
+ return existing.target;
248
+ }
249
+ return resolveDockerDeploymentTarget({
250
+ context: options.dockerContext ?? null,
251
+ dockerCommand: options.dockerCommand,
252
+ dockerHost: options.dockerHost ?? null
253
+ });
254
+ };
@@ -0,0 +1,18 @@
1
+ import type { ParsedImageReference } from "./imageRef.js";
2
+ import type { DistributionReport } from "./types.js";
3
+ /** Derives a kebab-case deployment name from the image repository component. */
4
+ export declare const deriveDeploymentName: (ref: ParsedImageReference) => string;
5
+ /** Per-deployment volume name so two deployments never share a store. */
6
+ export declare const deriveVolumeName: (deploymentName: string, mountId: string) => string;
7
+ export interface ResolveImageEnvironmentInput {
8
+ authValues: Record<string, string>;
9
+ envFileEnv?: Record<string, string>;
10
+ report: DistributionReport;
11
+ }
12
+ /**
13
+ * Builds the container environment for a sourceless deployment: auth-profile
14
+ * and env-file values, process-env overrides for declared secrets, and
15
+ * generated runtime tokens that the user must never supply.
16
+ */
17
+ export declare const resolveImageEnvironment: (input: ResolveImageEnvironmentInput) => Record<string, string>;
18
+ export declare const renderEnvFileContent: (env: Record<string, string>) => string;