spawnfile 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +9 -2
  2. package/dist/auth/runtimeCredentials.js +18 -2
  3. package/dist/cli/devCommands.d.ts +3 -0
  4. package/dist/cli/devCommands.js +115 -0
  5. package/dist/cli/lifecycleCommands.js +2 -0
  6. package/dist/cli/runCli.d.ts +6 -0
  7. package/dist/cli/runCli.js +4 -0
  8. package/dist/cli/statusCommand.d.ts +2 -1
  9. package/dist/cli/statusCommand.js +50 -16
  10. package/dist/compiler/buildProject.d.ts +9 -1
  11. package/dist/compiler/buildProject.js +44 -5
  12. package/dist/compiler/compileProject.d.ts +2 -0
  13. package/dist/compiler/compileProject.js +3 -1
  14. package/dist/compiler/containerArtifactsRender.js +25 -69
  15. package/dist/compiler/containerEntrypointRender.js +4 -1
  16. package/dist/compiler/containerPackageRender.d.ts +15 -0
  17. package/dist/compiler/containerPackageRender.js +63 -0
  18. package/dist/compiler/containerTargetResources.js +3 -1
  19. package/dist/compiler/moltnetArtifacts.js +16 -1
  20. package/dist/compiler/moltnetBinaries.d.ts +4 -1
  21. package/dist/compiler/moltnetBinaries.js +24 -3
  22. package/dist/compiler/moltnetClientConfig.js +11 -0
  23. package/dist/compiler/moltnetRuntimeConfig.js +11 -0
  24. package/dist/compiler/runProject.js +7 -1
  25. package/dist/compiler/runProjectDocker.js +157 -12
  26. package/dist/compiler/upProject.js +1 -0
  27. package/dist/deployment/dockerRecover.d.ts +17 -0
  28. package/dist/deployment/dockerRecover.js +129 -0
  29. package/dist/deployment/index.d.ts +1 -0
  30. package/dist/deployment/index.js +1 -0
  31. package/dist/dev/activity.d.ts +14 -0
  32. package/dist/dev/activity.js +48 -0
  33. package/dist/dev/docker.d.ts +16 -0
  34. package/dist/dev/docker.js +71 -0
  35. package/dist/dev/index.d.ts +2 -0
  36. package/dist/dev/index.js +2 -0
  37. package/dist/dev/project.d.ts +35 -0
  38. package/dist/dev/project.js +164 -0
  39. package/dist/runtime/common.d.ts +2 -0
  40. package/dist/runtime/common.js +1 -1
  41. package/dist/runtime/container.d.ts +1 -0
  42. package/dist/runtime/container.js +93 -10
  43. package/dist/runtime/install.d.ts +1 -1
  44. package/dist/runtime/install.js +43 -1
  45. package/dist/runtime/pi/adapter.d.ts +4 -0
  46. package/dist/runtime/pi/adapter.js +198 -0
  47. package/dist/runtime/pi/appActivitySource.d.ts +1 -0
  48. package/dist/runtime/pi/appActivitySource.js +87 -0
  49. package/dist/runtime/pi/appCoreSource.d.ts +1 -0
  50. package/dist/runtime/pi/appCoreSource.js +354 -0
  51. package/dist/runtime/pi/appPreludeSource.d.ts +1 -0
  52. package/dist/runtime/pi/appPreludeSource.js +103 -0
  53. package/dist/runtime/pi/appSource.d.ts +1 -0
  54. package/dist/runtime/pi/appSource.js +8 -0
  55. package/dist/runtime/pi/appTemplate.d.ts +28 -0
  56. package/dist/runtime/pi/appTemplate.js +144 -0
  57. package/dist/runtime/pi/runAuth.d.ts +2 -0
  58. package/dist/runtime/pi/runAuth.js +68 -0
  59. package/dist/runtime/registry.js +3 -0
  60. package/dist/runtime/types.d.ts +1 -0
  61. package/dist/status/deployments.js +5 -12
  62. package/package.json +7 -2
  63. package/runtimes.yaml +27 -11
@@ -0,0 +1,129 @@
1
+ import { execFile as execFileCallback } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { SpawnfileError } from "../shared/index.js";
4
+ import { dockerDeploymentLabelKeys } from "./dockerLabels.js";
5
+ import { resolveDockerDeploymentTarget } from "./target.js";
6
+ const execFile = promisify(execFileCallback);
7
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
8
+ const toStringOrNull = (value) => typeof value === "string" && value.length > 0 ? value : null;
9
+ const labelsFrom = (inspection) => {
10
+ const labels = inspection.Config?.Labels;
11
+ if (!isRecord(labels)) {
12
+ return {};
13
+ }
14
+ return Object.fromEntries(Object.entries(labels).filter((entry) => typeof entry[1] === "string"));
15
+ };
16
+ const withContext = (context, args) => [
17
+ "--context",
18
+ context,
19
+ ...args
20
+ ];
21
+ const containerNameFrom = (inspection) => {
22
+ const name = toStringOrNull(inspection.Name);
23
+ return name ? name.replace(/^\//u, "") : null;
24
+ };
25
+ const parseInspections = (stdout) => {
26
+ let parsed;
27
+ try {
28
+ parsed = JSON.parse(stdout);
29
+ }
30
+ catch (error) {
31
+ throw new SpawnfileError("runtime_error", `Unable to parse recovered Docker container inspection: ${error instanceof Error ? error.message : String(error)}`);
32
+ }
33
+ return Array.isArray(parsed) ? parsed.filter(isRecord) : [];
34
+ };
35
+ const deploymentNameFor = (labels) => labels[dockerDeploymentLabelKeys.deployment] ?? null;
36
+ const buildUnit = (inspection, labels, contains, runtimeInstanceIds) => {
37
+ const unitId = labels[dockerDeploymentLabelKeys.unit];
38
+ const imageTag = toStringOrNull(inspection.Config?.Image);
39
+ if (!unitId || !imageTag) {
40
+ return null;
41
+ }
42
+ return {
43
+ container_id: toStringOrNull(inspection.Id),
44
+ container_name: containerNameFrom(inspection),
45
+ contains,
46
+ id: unitId,
47
+ image_id: toStringOrNull(inspection.Image),
48
+ image_tag: imageTag,
49
+ kind: "container",
50
+ runtime_instances: runtimeInstanceIds
51
+ };
52
+ };
53
+ const groupByDeployment = (inspections, options, target) => {
54
+ const records = new Map();
55
+ const unitsByDeployment = new Map();
56
+ const contains = options.contains ?? [];
57
+ const runtimeInstanceIds = options.runtimeInstanceIds ?? [];
58
+ for (const inspection of inspections) {
59
+ const labels = labelsFrom(inspection);
60
+ if (labels[dockerDeploymentLabelKeys.project] !== options.projectLabel) {
61
+ continue;
62
+ }
63
+ const name = deploymentNameFor(labels);
64
+ const compileFingerprint = labels[dockerDeploymentLabelKeys.compileFingerprint];
65
+ if (!name || !compileFingerprint) {
66
+ continue;
67
+ }
68
+ const unit = buildUnit(inspection, labels, contains, runtimeInstanceIds);
69
+ if (!unit) {
70
+ continue;
71
+ }
72
+ const seenUnits = unitsByDeployment.get(name) ?? new Set();
73
+ if (seenUnits.has(unit.id)) {
74
+ throw new SpawnfileError("validation_error", `Recovered deployment "${name}" has multiple containers for unit "${unit.id}"`);
75
+ }
76
+ seenUnits.add(unit.id);
77
+ unitsByDeployment.set(name, seenUnits);
78
+ const record = records.get(name) ?? {
79
+ auth_profile: null,
80
+ compile_fingerprint: compileFingerprint,
81
+ created_at: toStringOrNull(inspection.Created) ?? new Date().toISOString(),
82
+ manager: "docker",
83
+ name,
84
+ output_directory: options.outputDirectory,
85
+ source: { kind: "project", root: options.sourceRoot },
86
+ target,
87
+ units: [],
88
+ version: "spawnfile.deployment.v2"
89
+ };
90
+ record.units.push(unit);
91
+ records.set(name, record);
92
+ }
93
+ return [...records.values()]
94
+ .sort((left, right) => left.name.localeCompare(right.name))
95
+ .map((record) => ({
96
+ path: `docker-context://${options.context}/${record.name}`,
97
+ record: {
98
+ ...record,
99
+ units: [...record.units].sort((left, right) => left.id.localeCompare(right.id))
100
+ }
101
+ }));
102
+ };
103
+ export const recoverDockerDeploymentRecords = async (options) => {
104
+ const resolved = {
105
+ dockerCommand: options.dockerCommand ?? "docker",
106
+ execFile: options.execFile ?? execFile,
107
+ timeoutMs: options.timeoutMs ?? 10_000
108
+ };
109
+ const target = await resolveDockerDeploymentTarget({
110
+ context: options.context,
111
+ dockerCommand: resolved.dockerCommand,
112
+ execFile: resolved.execFile,
113
+ timeoutMs: resolved.timeoutMs
114
+ });
115
+ const { stdout: listStdout } = await resolved.execFile(resolved.dockerCommand, withContext(options.context, [
116
+ "ps",
117
+ "-a",
118
+ "--filter",
119
+ `label=${dockerDeploymentLabelKeys.version}`,
120
+ "--format",
121
+ "{{.ID}}"
122
+ ]), { timeout: resolved.timeoutMs });
123
+ const ids = listStdout.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
124
+ if (ids.length === 0) {
125
+ return [];
126
+ }
127
+ const { stdout } = await resolved.execFile(resolved.dockerCommand, withContext(options.context, ["inspect", ...ids]), { timeout: resolved.timeoutMs });
128
+ return groupByDeployment(parseInspections(stdout), options, target);
129
+ };
@@ -3,6 +3,7 @@ export * from "./dockerInspect.js";
3
3
  export * from "./dockerLogs.js";
4
4
  export * from "./dockerManager.js";
5
5
  export * from "./dockerProbeGateway.js";
6
+ export * from "./dockerRecover.js";
6
7
  export * from "./homeStore.js";
7
8
  export * from "./names.js";
8
9
  export * from "./record.js";
@@ -3,6 +3,7 @@ export * from "./dockerInspect.js";
3
3
  export * from "./dockerLogs.js";
4
4
  export * from "./dockerManager.js";
5
5
  export * from "./dockerProbeGateway.js";
6
+ export * from "./dockerRecover.js";
6
7
  export * from "./homeStore.js";
7
8
  export * from "./names.js";
8
9
  export * from "./record.js";
@@ -0,0 +1,14 @@
1
+ import { type DevExecFile } from "./docker.js";
2
+ import { type DevProjectOptions } from "./project.js";
3
+ export interface DevActivityProjectOptions extends DevProjectOptions {
4
+ agent?: string;
5
+ execFile?: DevExecFile;
6
+ tail?: number;
7
+ }
8
+ export interface DevActivityProjectResult {
9
+ containerName: string;
10
+ deploymentName: string;
11
+ events: unknown[];
12
+ outputDirectory: string;
13
+ }
14
+ export declare const devActivityProject: (inputPath: string, options?: DevActivityProjectOptions) => Promise<DevActivityProjectResult>;
@@ -0,0 +1,48 @@
1
+ import { defaultExecFile, dockerArgsForRecord, firstContainerName, runDocker } from "./docker.js";
2
+ import { PI_CONTROL_URL, resolveDevOutputDirectory, selectDeploymentRecord } from "./project.js";
3
+ import { SpawnfileError } from "../shared/index.js";
4
+ const activityPath = (options) => {
5
+ const params = new URLSearchParams();
6
+ if (options.agent) {
7
+ params.set("agent", options.agent);
8
+ }
9
+ if (typeof options.tail === "number" && Number.isInteger(options.tail) && options.tail > 0) {
10
+ params.set("tail", String(options.tail));
11
+ }
12
+ const query = params.toString();
13
+ return `${PI_CONTROL_URL}/spawnfile/activity${query ? `?${query}` : ""}`;
14
+ };
15
+ const parseActivityEvents = (stdout) => {
16
+ let parsed;
17
+ try {
18
+ parsed = JSON.parse(stdout);
19
+ }
20
+ catch {
21
+ throw new SpawnfileError("runtime_error", "Pi activity endpoint returned invalid JSON");
22
+ }
23
+ if (typeof parsed !== "object" ||
24
+ parsed === null ||
25
+ !Array.isArray(parsed.events)) {
26
+ throw new SpawnfileError("runtime_error", "Pi activity endpoint response did not include an events array");
27
+ }
28
+ return parsed.events;
29
+ };
30
+ export const devActivityProject = async (inputPath, options = {}) => {
31
+ const outputDirectory = await resolveDevOutputDirectory(inputPath, options.outputDirectory);
32
+ const record = await selectDeploymentRecord(outputDirectory, options.deploymentName);
33
+ const containerName = firstContainerName(record);
34
+ const stdout = await runDocker(options.dockerCommand ?? "docker", [
35
+ ...dockerArgsForRecord(record),
36
+ "exec",
37
+ containerName,
38
+ "curl",
39
+ "-fsS",
40
+ activityPath(options)
41
+ ], options.execFile ?? defaultExecFile);
42
+ return {
43
+ containerName,
44
+ deploymentName: record.name,
45
+ events: parseActivityEvents(stdout),
46
+ outputDirectory
47
+ };
48
+ };
@@ -0,0 +1,16 @@
1
+ import { type CompileProjectResult } from "../compiler/index.js";
2
+ import { type DeploymentRecord } from "../deployment/index.js";
3
+ export type DevExecFile = (file: string, args: string[], options: {
4
+ timeout: number;
5
+ }) => Promise<{
6
+ stderr: string;
7
+ stdout: string;
8
+ }>;
9
+ export declare const defaultExecFile: DevExecFile;
10
+ export declare const dockerArgsForRecord: (record: DeploymentRecord) => string[];
11
+ export declare const firstContainerName: (record: DeploymentRecord) => string;
12
+ export declare const compileForDevApply: (inputPath: string, outputDirectory: string, record: DeploymentRecord, dockerCommand: string, execFileImpl: DevExecFile) => Promise<CompileProjectResult>;
13
+ export declare const runDocker: (command: string, args: string[], execFileImpl: DevExecFile, timeout?: number) => Promise<string>;
14
+ export declare const copyIntoContainer: (dockerCommand: string, dockerPrefix: string[], source: string, destination: string, execFileImpl: DevExecFile) => Promise<void>;
15
+ export declare const ensureContainerDirectories: (dockerCommand: string, dockerPrefix: string[], containerName: string, paths: string[], execFileImpl: DevExecFile) => Promise<void>;
16
+ export declare const chownContainerPaths: (dockerCommand: string, dockerPrefix: string[], containerName: string, paths: string[], execFileImpl: DevExecFile) => Promise<void>;
@@ -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
+ };
@@ -7,6 +7,8 @@ export declare const createDocumentFiles: (baseDirectory: string, documents: Res
7
7
  export declare const createSkillFiles: (baseDirectory: string, skills: ResolvedSkill[]) => EmittedFile[];
8
8
  export declare const createAgentCapabilities: (node: ResolvedAgentNode, options?: {
9
9
  mcpOutcome?: CapabilityReport["outcome"];
10
+ moltnetMessage?: string;
11
+ moltnetOutcome?: CapabilityReport["outcome"];
10
12
  sandboxOutcome?: CapabilityReport["outcome"];
11
13
  scheduleMessage?: string;
12
14
  scheduleOutcome?: CapabilityReport["outcome"];
@@ -48,7 +48,7 @@ export const createAgentCapabilities = (node, options = {}) => {
48
48
  capabilities.push(createCapability("surfaces.discord", "supported"));
49
49
  }
50
50
  if (node.surfaces?.moltnet) {
51
- capabilities.push(createCapability("surfaces.moltnet", "supported"));
51
+ capabilities.push(createCapability("surfaces.moltnet", options.moltnetOutcome ?? "supported", options.moltnetMessage));
52
52
  }
53
53
  if (node.surfaces?.telegram) {
54
54
  capabilities.push(createCapability("surfaces.telegram", "supported"));
@@ -1,5 +1,6 @@
1
1
  export declare const RUNTIME_INSTALL_ROOT = "/opt/spawnfile/runtime-installs";
2
2
  export interface RuntimeInstallRecipe {
3
+ baseImage?: string;
3
4
  commands: string[];
4
5
  copyCommands: string[];
5
6
  runtimeName: string;