spawnfile 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +464 -0
- package/dist/.env.example +5 -0
- package/dist/Dockerfile +21 -0
- package/dist/auth/importers.d.ts +8 -0
- package/dist/auth/importers.js +93 -0
- package/dist/auth/index.d.ts +5 -0
- package/dist/auth/index.js +5 -0
- package/dist/auth/paths.d.ts +6 -0
- package/dist/auth/paths.js +18 -0
- package/dist/auth/profileStore.d.ts +10 -0
- package/dist/auth/profileStore.js +125 -0
- package/dist/auth/runtimeCredentials.d.ts +14 -0
- package/dist/auth/runtimeCredentials.js +76 -0
- package/dist/auth/types.d.ts +22 -0
- package/dist/auth/types.js +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/runCli.d.ts +27 -0
- package/dist/cli/runCli.js +314 -0
- package/dist/compiler/addProjectNode.d.ts +21 -0
- package/dist/compiler/addProjectNode.js +126 -0
- package/dist/compiler/agentSurfaces.d.ts +4 -0
- package/dist/compiler/agentSurfaces.js +70 -0
- package/dist/compiler/buildCompilePlan.d.ts +2 -0
- package/dist/compiler/buildCompilePlan.js +258 -0
- package/dist/compiler/buildProject.d.ts +20 -0
- package/dist/compiler/buildProject.js +52 -0
- package/dist/compiler/compilePlanHelpers.d.ts +7 -0
- package/dist/compiler/compilePlanHelpers.js +39 -0
- package/dist/compiler/compileProject.d.ts +11 -0
- package/dist/compiler/compileProject.js +182 -0
- package/dist/compiler/containerArtifacts.d.ts +4 -0
- package/dist/compiler/containerArtifacts.js +64 -0
- package/dist/compiler/containerArtifactsPlans.d.ts +4 -0
- package/dist/compiler/containerArtifactsPlans.js +154 -0
- package/dist/compiler/containerArtifactsRender.d.ts +6 -0
- package/dist/compiler/containerArtifactsRender.js +237 -0
- package/dist/compiler/containerArtifactsTypes.d.ts +42 -0
- package/dist/compiler/containerArtifactsTypes.js +1 -0
- package/dist/compiler/discordSurface.d.ts +4 -0
- package/dist/compiler/discordSurface.js +28 -0
- package/dist/compiler/executionDefaults.d.ts +2 -0
- package/dist/compiler/executionDefaults.js +9 -0
- package/dist/compiler/helpers.d.ts +7 -0
- package/dist/compiler/helpers.js +35 -0
- package/dist/compiler/index.d.ts +9 -0
- package/dist/compiler/index.js +9 -0
- package/dist/compiler/initProject.d.ts +9 -0
- package/dist/compiler/initProject.js +46 -0
- package/dist/compiler/modelAuth.d.ts +2 -0
- package/dist/compiler/modelAuth.js +17 -0
- package/dist/compiler/modelEnv.d.ts +10 -0
- package/dist/compiler/modelEnv.js +97 -0
- package/dist/compiler/runProject.d.ts +34 -0
- package/dist/compiler/runProject.js +197 -0
- package/dist/compiler/runProjectAuth.d.ts +9 -0
- package/dist/compiler/runProjectAuth.js +59 -0
- package/dist/compiler/surfaceSupport.d.ts +2 -0
- package/dist/compiler/surfaceSupport.js +13 -0
- package/dist/compiler/surfaces.d.ts +21 -0
- package/dist/compiler/surfaces.js +59 -0
- package/dist/compiler/syncProjectAuth.d.ts +7 -0
- package/dist/compiler/syncProjectAuth.js +65 -0
- package/dist/compiler/types.d.ts +134 -0
- package/dist/compiler/types.js +1 -0
- package/dist/compiler/updateProjectModels.d.ts +20 -0
- package/dist/compiler/updateProjectModels.js +181 -0
- package/dist/container/rootfs/var/lib/spawnfile/instances/picoclaw/agent-assistant/picoclaw/config.json +16 -0
- package/dist/container/rootfs/var/lib/spawnfile/instances/picoclaw/agent-assistant/picoclaw/workspace/AGENTS.md +1 -0
- package/dist/e2e/cli.d.ts +1 -0
- package/dist/e2e/cli.js +40 -0
- package/dist/e2e/dockerAuth.d.ts +18 -0
- package/dist/e2e/dockerAuth.js +212 -0
- package/dist/e2e/fixtures.d.ts +2 -0
- package/dist/e2e/fixtures.js +49 -0
- package/dist/e2e/index.d.ts +4 -0
- package/dist/e2e/index.js +4 -0
- package/dist/e2e/runtimePrompts.d.ts +13 -0
- package/dist/e2e/runtimePrompts.js +132 -0
- package/dist/e2e/scenarios.d.ts +3 -0
- package/dist/e2e/scenarios.js +84 -0
- package/dist/e2e/types.d.ts +35 -0
- package/dist/e2e/types.js +1 -0
- package/dist/entrypoint.sh +71 -0
- package/dist/filesystem/index.d.ts +2 -0
- package/dist/filesystem/index.js +2 -0
- package/dist/filesystem/io.d.ts +11 -0
- package/dist/filesystem/io.js +57 -0
- package/dist/filesystem/paths.d.ts +6 -0
- package/dist/filesystem/paths.js +30 -0
- package/dist/manifest/index.d.ts +5 -0
- package/dist/manifest/index.js +5 -0
- package/dist/manifest/loadManifest.d.ts +12 -0
- package/dist/manifest/loadManifest.js +208 -0
- package/dist/manifest/renderSpawnfile.d.ts +2 -0
- package/dist/manifest/renderSpawnfile.js +211 -0
- package/dist/manifest/scaffold.d.ts +16 -0
- package/dist/manifest/scaffold.js +41 -0
- package/dist/manifest/schemas.d.ts +989 -0
- package/dist/manifest/schemas.js +314 -0
- package/dist/manifest/skillFrontmatter.d.ts +5 -0
- package/dist/manifest/skillFrontmatter.js +32 -0
- package/dist/manifest/surfaceSchemas.d.ts +148 -0
- package/dist/manifest/surfaceSchemas.js +162 -0
- package/dist/report/createDiagnostic.d.ts +2 -0
- package/dist/report/createDiagnostic.js +4 -0
- package/dist/report/createReport.d.ts +2 -0
- package/dist/report/createReport.js +7 -0
- package/dist/report/index.d.ts +4 -0
- package/dist/report/index.js +4 -0
- package/dist/report/types.d.ts +50 -0
- package/dist/report/types.js +1 -0
- package/dist/report/writeReport.d.ts +2 -0
- package/dist/report/writeReport.js +9 -0
- package/dist/runtime/common.d.ts +13 -0
- package/dist/runtime/common.js +63 -0
- package/dist/runtime/container.d.ts +8 -0
- package/dist/runtime/container.js +67 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/install.d.ts +51 -0
- package/dist/runtime/install.js +167 -0
- package/dist/runtime/openclaw/adapter.d.ts +2 -0
- package/dist/runtime/openclaw/adapter.js +194 -0
- package/dist/runtime/openclaw/runAuth.d.ts +2 -0
- package/dist/runtime/openclaw/runAuth.js +125 -0
- package/dist/runtime/openclaw/scaffold-assets/AGENTS.md +120 -0
- package/dist/runtime/openclaw/scaffold-assets/CLAUDE.md +5 -0
- package/dist/runtime/openclaw/scaffold-assets/IDENTITY.md +23 -0
- package/dist/runtime/openclaw/scaffold-assets/SOUL.md +36 -0
- package/dist/runtime/openclaw/scaffold.d.ts +2 -0
- package/dist/runtime/openclaw/scaffold.js +28 -0
- package/dist/runtime/openclaw/surfaces.d.ts +5 -0
- package/dist/runtime/openclaw/surfaces.js +253 -0
- package/dist/runtime/picoclaw/adapter.d.ts +2 -0
- package/dist/runtime/picoclaw/adapter.js +204 -0
- package/dist/runtime/picoclaw/runAuth.d.ts +2 -0
- package/dist/runtime/picoclaw/runAuth.js +117 -0
- package/dist/runtime/picoclaw/scaffold-assets/AGENTS.md +3 -0
- package/dist/runtime/picoclaw/scaffold-assets/CLAUDE.md +5 -0
- package/dist/runtime/picoclaw/scaffold-assets/IDENTITY.md +3 -0
- package/dist/runtime/picoclaw/scaffold-assets/SOUL.md +3 -0
- package/dist/runtime/picoclaw/scaffold.d.ts +2 -0
- package/dist/runtime/picoclaw/scaffold.js +28 -0
- package/dist/runtime/picoclaw/surfaces.d.ts +5 -0
- package/dist/runtime/picoclaw/surfaces.js +111 -0
- package/dist/runtime/registry.d.ts +41 -0
- package/dist/runtime/registry.js +134 -0
- package/dist/runtime/scaffoldAssets.d.ts +1 -0
- package/dist/runtime/scaffoldAssets.js +2 -0
- package/dist/runtime/tinyclaw/adapter.d.ts +2 -0
- package/dist/runtime/tinyclaw/adapter.js +263 -0
- package/dist/runtime/tinyclaw/runAuth.d.ts +2 -0
- package/dist/runtime/tinyclaw/runAuth.js +22 -0
- package/dist/runtime/tinyclaw/scaffold-assets/AGENTS.md +160 -0
- package/dist/runtime/tinyclaw/scaffold-assets/CLAUDE.md +5 -0
- package/dist/runtime/tinyclaw/scaffold-assets/SOUL.md +177 -0
- package/dist/runtime/tinyclaw/scaffold.d.ts +2 -0
- package/dist/runtime/tinyclaw/scaffold.js +24 -0
- package/dist/runtime/tinyclaw/surfaces.d.ts +8 -0
- package/dist/runtime/tinyclaw/surfaces.js +86 -0
- package/dist/runtime/types.d.ts +87 -0
- package/dist/runtime/types.js +1 -0
- package/dist/runtimes/picoclaw/agents/assistant/config.json +16 -0
- package/dist/runtimes/picoclaw/agents/assistant/workspace/AGENTS.md +1 -0
- package/dist/shared/constants.d.ts +7 -0
- package/dist/shared/constants.js +7 -0
- package/dist/shared/errors.d.ts +6 -0
- package/dist/shared/errors.js +9 -0
- package/dist/shared/index.d.ts +3 -0
- package/dist/shared/index.js +3 -0
- package/dist/shared/types.d.ts +9 -0
- package/dist/shared/types.js +1 -0
- package/dist/spawnfile-report.json +71 -0
- package/package.json +41 -0
- package/runtimes.yaml +62 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { chmod } from "node:fs/promises";
|
|
3
|
+
import { ensureDirectory, removeDirectory, writeUtf8File } from "../filesystem/index.js";
|
|
4
|
+
import { createCompileReport, createDiagnostic, writeCompileReport } from "../report/index.js";
|
|
5
|
+
import { DEFAULT_OUTPUT_DIRECTORY } from "../shared/index.js";
|
|
6
|
+
import { assertRuntimeCanCompile, createRuntimeLifecycleDiagnostics, getRuntimeAdapter } from "../runtime/index.js";
|
|
7
|
+
import { buildCompilePlan } from "./buildCompilePlan.js";
|
|
8
|
+
import { createContainerArtifacts } from "./containerArtifacts.js";
|
|
9
|
+
const writeEmittedFiles = async (outputDirectory, files) => {
|
|
10
|
+
await Promise.all(files.map(async (file) => {
|
|
11
|
+
const targetPath = path.join(outputDirectory, file.path);
|
|
12
|
+
await ensureDirectory(path.dirname(targetPath));
|
|
13
|
+
await writeUtf8File(targetPath, file.content);
|
|
14
|
+
}));
|
|
15
|
+
};
|
|
16
|
+
const createTeamCapabilities = (outcome, message) => [
|
|
17
|
+
{ key: "team.members", message, outcome },
|
|
18
|
+
{ key: "team.structure.mode", message, outcome },
|
|
19
|
+
{ key: "team.structure.leader", message, outcome },
|
|
20
|
+
{ key: "team.structure.external", message, outcome },
|
|
21
|
+
{ key: "team.shared", message, outcome },
|
|
22
|
+
{ key: "team.nested", message, outcome }
|
|
23
|
+
];
|
|
24
|
+
const createAgentOutputDirectory = (baseDirectory, node) => path.join(baseDirectory, "runtimes", node.runtimeName ?? "unknown", "agents", node.slug);
|
|
25
|
+
const createTeamOutputDirectory = (baseDirectory, runtimeName, node) => path.join(baseDirectory, "runtimes", runtimeName, "teams", node.slug);
|
|
26
|
+
const compileAgentNode = async (baseDirectory, node) => {
|
|
27
|
+
const runtime = await assertRuntimeCanCompile(node.runtimeName ?? node.value.runtime.name);
|
|
28
|
+
const adapter = getRuntimeAdapter(runtime.name);
|
|
29
|
+
const diagnostics = [
|
|
30
|
+
...createRuntimeLifecycleDiagnostics(runtime)
|
|
31
|
+
];
|
|
32
|
+
for (const diagnostic of adapter.validateRuntimeOptions?.(node.value.runtime.options) ?? []) {
|
|
33
|
+
diagnostics.push(diagnostic);
|
|
34
|
+
}
|
|
35
|
+
const errorDiagnostic = diagnostics.find((diagnostic) => diagnostic.level === "error");
|
|
36
|
+
if (errorDiagnostic) {
|
|
37
|
+
throw new Error(errorDiagnostic.message);
|
|
38
|
+
}
|
|
39
|
+
const result = await adapter.compileAgent(node.value);
|
|
40
|
+
const outputDirectory = createAgentOutputDirectory(baseDirectory, node);
|
|
41
|
+
await writeEmittedFiles(outputDirectory, result.files);
|
|
42
|
+
return {
|
|
43
|
+
emittedFiles: result.files,
|
|
44
|
+
kind: node.kind,
|
|
45
|
+
report: {
|
|
46
|
+
capabilities: result.capabilities,
|
|
47
|
+
diagnostics: [...diagnostics, ...result.diagnostics],
|
|
48
|
+
id: node.id,
|
|
49
|
+
kind: node.kind,
|
|
50
|
+
output_dir: path.relative(baseDirectory, outputDirectory),
|
|
51
|
+
runtime: runtime.name,
|
|
52
|
+
runtime_ref: runtime.ref,
|
|
53
|
+
runtime_status: runtime.status,
|
|
54
|
+
source: node.value.source
|
|
55
|
+
},
|
|
56
|
+
runtimeName: runtime.name,
|
|
57
|
+
slug: node.slug,
|
|
58
|
+
value: node.value
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
const getTeamRuntimeName = (node) => {
|
|
62
|
+
const runtimeNames = [...new Set(node.members.map((member) => member.runtimeName).filter(Boolean))];
|
|
63
|
+
return runtimeNames.length === 1 ? (runtimeNames[0] ?? null) : null;
|
|
64
|
+
};
|
|
65
|
+
const compileTeamNode = async (baseDirectory, node) => {
|
|
66
|
+
const runtimeName = getTeamRuntimeName(node.value);
|
|
67
|
+
if (!runtimeName) {
|
|
68
|
+
return {
|
|
69
|
+
emittedFiles: [],
|
|
70
|
+
kind: node.kind,
|
|
71
|
+
report: {
|
|
72
|
+
capabilities: createTeamCapabilities("degraded", "Team spans multiple runtimes and cannot lower to one native team artifact in v0.1"),
|
|
73
|
+
diagnostics: [
|
|
74
|
+
createDiagnostic("warn", `Team ${node.value.name} spans multiple runtimes and was not emitted as a native team artifact`)
|
|
75
|
+
],
|
|
76
|
+
id: node.id,
|
|
77
|
+
kind: node.kind,
|
|
78
|
+
output_dir: null,
|
|
79
|
+
runtime: null,
|
|
80
|
+
runtime_ref: null,
|
|
81
|
+
runtime_status: null,
|
|
82
|
+
source: node.value.source
|
|
83
|
+
},
|
|
84
|
+
runtimeName: null,
|
|
85
|
+
slug: node.slug,
|
|
86
|
+
value: node.value
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const runtime = await assertRuntimeCanCompile(runtimeName);
|
|
90
|
+
const adapter = getRuntimeAdapter(runtime.name);
|
|
91
|
+
const diagnostics = [...createRuntimeLifecycleDiagnostics(runtime)];
|
|
92
|
+
if (!adapter.compileTeam) {
|
|
93
|
+
return {
|
|
94
|
+
emittedFiles: [],
|
|
95
|
+
kind: node.kind,
|
|
96
|
+
report: {
|
|
97
|
+
capabilities: createTeamCapabilities("degraded", `Runtime ${runtime.name} does not provide native team compilation in v0.1`),
|
|
98
|
+
diagnostics: [
|
|
99
|
+
...diagnostics,
|
|
100
|
+
createDiagnostic("warn", `Runtime ${runtime.name} did not emit a native team artifact for ${node.value.name}`)
|
|
101
|
+
],
|
|
102
|
+
id: node.id,
|
|
103
|
+
kind: node.kind,
|
|
104
|
+
output_dir: null,
|
|
105
|
+
runtime: runtime.name,
|
|
106
|
+
runtime_ref: runtime.ref,
|
|
107
|
+
runtime_status: runtime.status,
|
|
108
|
+
source: node.value.source
|
|
109
|
+
},
|
|
110
|
+
runtimeName: runtime.name,
|
|
111
|
+
slug: node.slug,
|
|
112
|
+
value: node.value
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const result = await adapter.compileTeam(node.value);
|
|
116
|
+
const outputDirectory = createTeamOutputDirectory(baseDirectory, runtime.name, node);
|
|
117
|
+
await writeEmittedFiles(outputDirectory, result.files);
|
|
118
|
+
return {
|
|
119
|
+
emittedFiles: result.files,
|
|
120
|
+
kind: node.kind,
|
|
121
|
+
report: {
|
|
122
|
+
capabilities: result.capabilities,
|
|
123
|
+
diagnostics: [...diagnostics, ...result.diagnostics],
|
|
124
|
+
id: node.id,
|
|
125
|
+
kind: node.kind,
|
|
126
|
+
output_dir: path.relative(baseDirectory, outputDirectory),
|
|
127
|
+
runtime: runtime.name,
|
|
128
|
+
runtime_ref: runtime.ref,
|
|
129
|
+
runtime_status: runtime.status,
|
|
130
|
+
source: node.value.source
|
|
131
|
+
},
|
|
132
|
+
runtimeName: runtime.name,
|
|
133
|
+
slug: node.slug,
|
|
134
|
+
value: node.value
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
const enforcePolicy = (nodeReport, policyMode, onDegrade) => {
|
|
138
|
+
for (const capability of nodeReport.capabilities) {
|
|
139
|
+
if (capability.outcome === "unsupported") {
|
|
140
|
+
if (policyMode === "strict") {
|
|
141
|
+
throw new Error(`Policy violation: ${capability.key} is unsupported for ${nodeReport.id} (strict mode)${capability.message ? `: ${capability.message}` : ""}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (capability.outcome === "degraded") {
|
|
145
|
+
if (onDegrade === "error") {
|
|
146
|
+
throw new Error(`Policy violation: ${capability.key} is degraded for ${nodeReport.id} (on_degrade: error)${capability.message ? `: ${capability.message}` : ""}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
export const compileProject = async (inputPath, options = {}) => {
|
|
152
|
+
const plan = await buildCompilePlan(inputPath);
|
|
153
|
+
const outputDirectory = path.resolve(options.outputDirectory ?? DEFAULT_OUTPUT_DIRECTORY);
|
|
154
|
+
if (options.clean ?? true) {
|
|
155
|
+
await removeDirectory(outputDirectory);
|
|
156
|
+
}
|
|
157
|
+
await ensureDirectory(outputDirectory);
|
|
158
|
+
const nodeReports = [];
|
|
159
|
+
const compiledNodes = [];
|
|
160
|
+
for (const node of plan.nodes) {
|
|
161
|
+
let compiled;
|
|
162
|
+
if (node.kind === "agent") {
|
|
163
|
+
compiled = await compileAgentNode(outputDirectory, node);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
compiled = await compileTeamNode(outputDirectory, node);
|
|
167
|
+
}
|
|
168
|
+
enforcePolicy(compiled.report, node.value.policyMode, node.value.policyOnDegrade);
|
|
169
|
+
nodeReports.push(compiled.report);
|
|
170
|
+
compiledNodes.push(compiled);
|
|
171
|
+
}
|
|
172
|
+
const containerArtifacts = await createContainerArtifacts(plan, compiledNodes);
|
|
173
|
+
await writeEmittedFiles(outputDirectory, containerArtifacts.files);
|
|
174
|
+
await Promise.all(containerArtifacts.executablePaths.map((filePath) => chmod(path.join(outputDirectory, filePath), 0o755)));
|
|
175
|
+
const report = createCompileReport(plan.root, nodeReports, [], containerArtifacts.report);
|
|
176
|
+
const reportPath = await writeCompileReport(outputDirectory, report);
|
|
177
|
+
return {
|
|
178
|
+
outputDirectory,
|
|
179
|
+
report,
|
|
180
|
+
reportPath
|
|
181
|
+
};
|
|
182
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CompiledNodeArtifact, GeneratedContainerArtifacts } from "./containerArtifactsTypes.js";
|
|
2
|
+
import type { CompilePlan } from "./types.js";
|
|
3
|
+
export type { CompiledNodeArtifact, GeneratedContainerArtifacts } from "./containerArtifactsTypes.js";
|
|
4
|
+
export declare const createContainerArtifacts: (plan: CompilePlan, compiledNodes: CompiledNodeArtifact[]) => Promise<GeneratedContainerArtifacts>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createEnvVariableMap, createRuntimeTargetPlans } from "./containerArtifactsPlans.js";
|
|
2
|
+
import { createRootfsFiles, renderDockerfile, renderEntrypoint, renderEnvExample } from "./containerArtifactsRender.js";
|
|
3
|
+
export const createContainerArtifacts = async (plan, compiledNodes) => {
|
|
4
|
+
const runtimePlans = await createRuntimeTargetPlans(plan, compiledNodes);
|
|
5
|
+
const envVariables = [...createEnvVariableMap(compiledNodes, runtimePlans).values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
6
|
+
const requiredSecrets = envVariables
|
|
7
|
+
.filter((variable) => variable.required)
|
|
8
|
+
.map((variable) => variable.name)
|
|
9
|
+
.sort();
|
|
10
|
+
const modelSecretsRequired = envVariables
|
|
11
|
+
.filter((variable) => variable.required && variable.categories.includes("model"))
|
|
12
|
+
.map((variable) => variable.name)
|
|
13
|
+
.sort();
|
|
14
|
+
const runtimeSecretsRequired = envVariables
|
|
15
|
+
.filter((variable) => variable.required && variable.categories.includes("runtime"))
|
|
16
|
+
.map((variable) => variable.name)
|
|
17
|
+
.sort();
|
|
18
|
+
const files = [
|
|
19
|
+
...createRootfsFiles(runtimePlans),
|
|
20
|
+
{
|
|
21
|
+
content: await renderDockerfile(runtimePlans),
|
|
22
|
+
path: "Dockerfile"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
content: renderEntrypoint(runtimePlans, requiredSecrets.filter((secretName) => !modelSecretsRequired.includes(secretName))),
|
|
26
|
+
path: "entrypoint.sh"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
content: renderEnvExample(envVariables),
|
|
30
|
+
path: ".env.example"
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
const ports = [...new Set(runtimePlans.flatMap((plan) => (plan.port ? [plan.port] : [])))].sort((left, right) => left - right);
|
|
34
|
+
const runtimeHomes = [
|
|
35
|
+
...new Set(runtimePlans.flatMap((plan) => (plan.instancePaths.homePath ? [plan.instancePaths.homePath] : [])))
|
|
36
|
+
].sort();
|
|
37
|
+
const runtimeInstances = runtimePlans
|
|
38
|
+
.map((plan) => ({
|
|
39
|
+
config_path: plan.instancePaths.configPath,
|
|
40
|
+
home_path: plan.instancePaths.homePath ?? null,
|
|
41
|
+
id: plan.id,
|
|
42
|
+
model_auth_methods: plan.modelAuthMethods,
|
|
43
|
+
model_secrets_required: plan.modelSecretsRequired,
|
|
44
|
+
runtime: plan.runtimeName
|
|
45
|
+
}))
|
|
46
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
47
|
+
const runtimesInstalled = [...new Set(runtimePlans.map((plan) => plan.runtimeName))].sort();
|
|
48
|
+
return {
|
|
49
|
+
executablePaths: ["entrypoint.sh"],
|
|
50
|
+
files,
|
|
51
|
+
report: {
|
|
52
|
+
dockerfile: "Dockerfile",
|
|
53
|
+
entrypoint: "entrypoint.sh",
|
|
54
|
+
env_example: ".env.example",
|
|
55
|
+
model_secrets_required: modelSecretsRequired,
|
|
56
|
+
ports,
|
|
57
|
+
runtime_instances: runtimeInstances,
|
|
58
|
+
runtime_homes: runtimeHomes,
|
|
59
|
+
runtime_secrets_required: runtimeSecretsRequired,
|
|
60
|
+
runtimes_installed: runtimesInstalled,
|
|
61
|
+
secrets_required: requiredSecrets
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CompilePlan } from "./types.js";
|
|
2
|
+
import type { CompiledNodeArtifact, ContainerEnvVariable, RuntimeTargetPlan } from "./containerArtifactsTypes.js";
|
|
3
|
+
export declare const createEnvVariableMap: (compiledNodes: CompiledNodeArtifact[], runtimePlans: RuntimeTargetPlan[]) => Map<string, ContainerEnvVariable>;
|
|
4
|
+
export declare const createRuntimeTargetPlans: (plan: CompilePlan, compiledNodes: CompiledNodeArtifact[]) => Promise<RuntimeTargetPlan[]>;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createRuntimeInstallRecipe, getRuntimeAdapter } from "../runtime/index.js";
|
|
3
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
4
|
+
import { listAgentSurfaceSecretNames } from "./agentSurfaces.js";
|
|
5
|
+
import { listExecutionModelSecretNames, resolveExecutionModelAuthMethods } from "./modelEnv.js";
|
|
6
|
+
const CONFIG_FILE_PLACEHOLDER = "<config-file>";
|
|
7
|
+
const INSTANCE_ROOT_PLACEHOLDER = "<instance-root>";
|
|
8
|
+
const createDefaultTargets = (inputs) => inputs.map((input) => ({
|
|
9
|
+
files: input.emittedFiles,
|
|
10
|
+
id: `${input.kind}-${input.slug}`,
|
|
11
|
+
sourceIds: [input.id]
|
|
12
|
+
}));
|
|
13
|
+
const resolveTargetEnvFiles = (configPath, target) => (target.envFiles ?? []).map((binding) => ({
|
|
14
|
+
envName: binding.envName,
|
|
15
|
+
filePath: path.posix.join(path.posix.dirname(configPath), binding.relativePath)
|
|
16
|
+
}));
|
|
17
|
+
const resolveTargetConfigEnvBindings = (meta, target) => [...(meta.configEnvBindings ?? []), ...(target.configEnvBindings ?? [])];
|
|
18
|
+
const assertTargetHasConfig = (runtimeName, targetId, meta, files) => {
|
|
19
|
+
if (!files.some((file) => file.path === meta.configFileName)) {
|
|
20
|
+
throw new SpawnfileError("runtime_error", `Container target ${targetId} for ${runtimeName} is missing ${meta.configFileName}`);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const replaceContainerPathTemplate = (template, instanceRoot, configFileName) => template
|
|
24
|
+
.replaceAll(INSTANCE_ROOT_PLACEHOLDER, instanceRoot)
|
|
25
|
+
.replaceAll(CONFIG_FILE_PLACEHOLDER, configFileName);
|
|
26
|
+
const resolveInstancePaths = (runtimeName, targetId, meta) => {
|
|
27
|
+
const instanceRoot = `/var/lib/spawnfile/instances/${runtimeName}/${targetId}`;
|
|
28
|
+
return {
|
|
29
|
+
configPath: replaceContainerPathTemplate(meta.instancePaths.configPathTemplate, instanceRoot, meta.configFileName),
|
|
30
|
+
homePath: meta.instancePaths.homePathTemplate
|
|
31
|
+
? replaceContainerPathTemplate(meta.instancePaths.homePathTemplate, instanceRoot, meta.configFileName)
|
|
32
|
+
: undefined,
|
|
33
|
+
workspacePath: replaceContainerPathTemplate(meta.instancePaths.workspacePathTemplate, instanceRoot, meta.configFileName)
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export const createEnvVariableMap = (compiledNodes, runtimePlans) => {
|
|
37
|
+
const variables = new Map();
|
|
38
|
+
const register = (name, required, description, category) => {
|
|
39
|
+
const current = variables.get(name);
|
|
40
|
+
if (!current) {
|
|
41
|
+
variables.set(name, {
|
|
42
|
+
categories: [category],
|
|
43
|
+
description,
|
|
44
|
+
name,
|
|
45
|
+
required
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
variables.set(name, {
|
|
50
|
+
...current,
|
|
51
|
+
categories: [...new Set([...current.categories, category])],
|
|
52
|
+
required: current.required || required
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
const registerSecret = (secret) => {
|
|
56
|
+
register(secret.name, secret.required, "Declared in Spawnfile secrets", "project");
|
|
57
|
+
};
|
|
58
|
+
for (const node of compiledNodes) {
|
|
59
|
+
if (node.value.kind === "agent") {
|
|
60
|
+
for (const secret of node.value.secrets) {
|
|
61
|
+
registerSecret(secret);
|
|
62
|
+
}
|
|
63
|
+
for (const secretName of listExecutionModelSecretNames(node.value.execution)) {
|
|
64
|
+
register(secretName, true, `Model provider auth for ${secretName}`, "model");
|
|
65
|
+
}
|
|
66
|
+
for (const secretName of listAgentSurfaceSecretNames(node.value.surfaces)) {
|
|
67
|
+
register(secretName, true, "Bot token for declared agent surfaces", "surface");
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
for (const secret of node.value.shared.secrets) {
|
|
72
|
+
registerSecret(secret);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const runtimePlan of runtimePlans) {
|
|
76
|
+
for (const variable of runtimePlan.meta.env ?? []) {
|
|
77
|
+
register(variable.name, variable.required, variable.description, "runtime");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return variables;
|
|
81
|
+
};
|
|
82
|
+
const resolveTargetModelSecrets = (target, inputs) => {
|
|
83
|
+
const sourceIds = new Set(target.sourceIds ?? []);
|
|
84
|
+
if (sourceIds.size === 0) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const secretNames = new Set();
|
|
88
|
+
for (const input of inputs) {
|
|
89
|
+
if (!sourceIds.has(input.id) || input.value.kind !== "agent") {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
for (const secretName of listExecutionModelSecretNames(input.value.execution)) {
|
|
93
|
+
secretNames.add(secretName);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return [...secretNames].sort();
|
|
97
|
+
};
|
|
98
|
+
const resolveTargetModelAuthMethods = (target, inputs) => {
|
|
99
|
+
const sourceIds = new Set(target.sourceIds ?? []);
|
|
100
|
+
if (sourceIds.size === 0) {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
const methods = new Map();
|
|
104
|
+
for (const input of inputs) {
|
|
105
|
+
if (!sourceIds.has(input.id) || input.value.kind !== "agent") {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
for (const [provider, method] of Object.entries(resolveExecutionModelAuthMethods(input.value.execution))) {
|
|
109
|
+
const existingMethod = methods.get(provider);
|
|
110
|
+
if (existingMethod && existingMethod !== method) {
|
|
111
|
+
throw new SpawnfileError("validation_error", `Container target ${target.id} declares conflicting auth methods for provider ${provider}`);
|
|
112
|
+
}
|
|
113
|
+
methods.set(provider, method);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return Object.fromEntries([...methods.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
|
117
|
+
};
|
|
118
|
+
export const createRuntimeTargetPlans = async (plan, compiledNodes) => {
|
|
119
|
+
const runtimeNames = Object.keys(plan.runtimes).sort();
|
|
120
|
+
const runtimePlans = [];
|
|
121
|
+
for (const runtimeName of runtimeNames) {
|
|
122
|
+
const adapter = getRuntimeAdapter(runtimeName);
|
|
123
|
+
const recipe = await createRuntimeInstallRecipe(runtimeName);
|
|
124
|
+
const targetInputs = compiledNodes
|
|
125
|
+
.filter((node) => node.runtimeName === runtimeName && node.emittedFiles.length > 0)
|
|
126
|
+
.map((node) => ({
|
|
127
|
+
emittedFiles: node.emittedFiles,
|
|
128
|
+
id: `${node.kind}:${node.slug}`,
|
|
129
|
+
kind: node.kind,
|
|
130
|
+
slug: node.slug,
|
|
131
|
+
value: node.value
|
|
132
|
+
}));
|
|
133
|
+
const targets = (await adapter.createContainerTargets?.(targetInputs)) ??
|
|
134
|
+
createDefaultTargets(targetInputs);
|
|
135
|
+
targets.forEach((target, index) => {
|
|
136
|
+
assertTargetHasConfig(runtimeName, target.id, adapter.container, target.files);
|
|
137
|
+
const instancePaths = resolveInstancePaths(runtimeName, target.id, adapter.container);
|
|
138
|
+
runtimePlans.push({
|
|
139
|
+
configEnvBindings: resolveTargetConfigEnvBindings(adapter.container, target) ?? [],
|
|
140
|
+
envFiles: resolveTargetEnvFiles(instancePaths.configPath, target),
|
|
141
|
+
id: target.id,
|
|
142
|
+
instancePaths,
|
|
143
|
+
meta: adapter.container,
|
|
144
|
+
modelAuthMethods: resolveTargetModelAuthMethods(target, targetInputs),
|
|
145
|
+
modelSecretsRequired: resolveTargetModelSecrets(target, targetInputs),
|
|
146
|
+
port: adapter.container.port ? adapter.container.port + index : undefined,
|
|
147
|
+
runtimeName,
|
|
148
|
+
runtimeRoot: recipe.runtimeRoot,
|
|
149
|
+
targetFiles: target.files
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return runtimePlans;
|
|
154
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { EmittedFile } from "../runtime/index.js";
|
|
2
|
+
import type { ContainerEnvVariable, RuntimeTargetPlan } from "./containerArtifactsTypes.js";
|
|
3
|
+
export declare const renderEnvExample: (variables: ContainerEnvVariable[]) => string;
|
|
4
|
+
export declare const renderDockerfile: (runtimePlans: RuntimeTargetPlan[]) => Promise<string>;
|
|
5
|
+
export declare const createRootfsFiles: (runtimePlans: RuntimeTargetPlan[]) => EmittedFile[];
|
|
6
|
+
export declare const renderEntrypoint: (runtimePlans: RuntimeTargetPlan[], requiredSecrets: string[]) => string;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createRuntimeInstallRecipe } from "../runtime/index.js";
|
|
3
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
4
|
+
const CONTAINER_ROOTFS_ROOT = "container/rootfs";
|
|
5
|
+
const GATEWAY_PORT_PLACEHOLDER = "<gateway-port>";
|
|
6
|
+
const WORKSPACE_PLACEHOLDER = "<workspace-path>";
|
|
7
|
+
const shellQuote = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
8
|
+
const extractNodeMajorVersion = (image) => Number(image.match(/^node:(\d+)/)?.[1] ?? "0");
|
|
9
|
+
const createPackageInstallCommand = (packages) => `RUN apt-get update && apt-get install -y --no-install-recommends ${packages.join(" ")} && rm -rf /var/lib/apt/lists/*`;
|
|
10
|
+
const createNpmPackageInstallCommand = (packages) => `RUN npm install -g --omit=dev --no-fund --no-audit ${packages.join(" ")}`;
|
|
11
|
+
const selectBaseImage = (runtimePlans) => {
|
|
12
|
+
const firstRuntimeMeta = runtimePlans[0]?.meta;
|
|
13
|
+
if (runtimePlans.length <= 1) {
|
|
14
|
+
return firstRuntimeMeta?.standaloneBaseImage ?? "debian:bookworm-slim";
|
|
15
|
+
}
|
|
16
|
+
const nodeBaseImages = runtimePlans
|
|
17
|
+
.map((plan) => plan.meta.standaloneBaseImage)
|
|
18
|
+
.filter((image) => image.startsWith("node:"))
|
|
19
|
+
.sort((left, right) => extractNodeMajorVersion(right) - extractNodeMajorVersion(left));
|
|
20
|
+
return nodeBaseImages[0] ?? "debian:bookworm-slim";
|
|
21
|
+
};
|
|
22
|
+
export const renderEnvExample = (variables) => {
|
|
23
|
+
if (variables.length === 0) {
|
|
24
|
+
return "# No environment variables were detected during compile.\n";
|
|
25
|
+
}
|
|
26
|
+
const lines = ["# Generated by spawnfile compile", ""];
|
|
27
|
+
const requiredVariables = variables.filter((variable) => variable.required);
|
|
28
|
+
const optionalVariables = variables.filter((variable) => !variable.required);
|
|
29
|
+
if (requiredVariables.length > 0) {
|
|
30
|
+
lines.push("# Required");
|
|
31
|
+
for (const variable of requiredVariables) {
|
|
32
|
+
lines.push(`# ${variable.description}`);
|
|
33
|
+
lines.push(`${variable.name}=`);
|
|
34
|
+
lines.push("");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (optionalVariables.length > 0) {
|
|
38
|
+
lines.push("# Optional");
|
|
39
|
+
for (const variable of optionalVariables) {
|
|
40
|
+
lines.push(`# ${variable.description}`);
|
|
41
|
+
lines.push(`${variable.name}=`);
|
|
42
|
+
lines.push("");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
46
|
+
};
|
|
47
|
+
export const renderDockerfile = async (runtimePlans) => {
|
|
48
|
+
const runtimeNames = [...new Set(runtimePlans.map((plan) => plan.runtimeName))];
|
|
49
|
+
const runtimeRecipes = await Promise.all(runtimeNames.map((runtimeName) => createRuntimeInstallRecipe(runtimeName)));
|
|
50
|
+
const baseImage = selectBaseImage(runtimePlans);
|
|
51
|
+
const needsJsonEnvWriter = runtimePlans.some((plan) => (plan.configEnvBindings?.length ?? 0) > 0);
|
|
52
|
+
const systemDeps = [
|
|
53
|
+
...new Set([
|
|
54
|
+
...runtimePlans.flatMap((plan) => plan.meta.systemDeps),
|
|
55
|
+
...(needsJsonEnvWriter ? ["python3"] : [])
|
|
56
|
+
])
|
|
57
|
+
].sort();
|
|
58
|
+
const globalNpmPackages = [
|
|
59
|
+
...new Set(runtimePlans.flatMap((plan) => plan.meta.globalNpmPackages ?? []))
|
|
60
|
+
].sort();
|
|
61
|
+
const exposedPorts = [...new Set(runtimePlans.flatMap((plan) => (plan.port ? [plan.port] : [])))].sort((left, right) => left - right);
|
|
62
|
+
const lines = [`FROM ${baseImage}`, "USER root", "", "WORKDIR /opt/spawnfile"];
|
|
63
|
+
if (systemDeps.length > 0) {
|
|
64
|
+
lines.push(createPackageInstallCommand(systemDeps), "");
|
|
65
|
+
}
|
|
66
|
+
if (globalNpmPackages.length > 0) {
|
|
67
|
+
lines.push(createNpmPackageInstallCommand(globalNpmPackages), "");
|
|
68
|
+
}
|
|
69
|
+
for (const recipe of runtimeRecipes) {
|
|
70
|
+
for (const copyCommand of recipe.copyCommands) {
|
|
71
|
+
lines.push(copyCommand);
|
|
72
|
+
}
|
|
73
|
+
for (const command of recipe.commands) {
|
|
74
|
+
lines.push(`RUN ${command}`);
|
|
75
|
+
}
|
|
76
|
+
lines.push("");
|
|
77
|
+
}
|
|
78
|
+
lines.push('RUN if ! id -u spawnfile >/dev/null 2>&1; then useradd --create-home --home-dir /home/spawnfile --shell /bin/bash spawnfile; fi', "");
|
|
79
|
+
lines.push("COPY container/rootfs/ /", "COPY .env.example /opt/spawnfile/.env.example", 'COPY entrypoint.sh /opt/spawnfile/entrypoint.sh', "RUN chmod +x /opt/spawnfile/entrypoint.sh", "RUN mkdir -p /var/lib/spawnfile && chown -R spawnfile:spawnfile /var/lib/spawnfile /opt/spawnfile");
|
|
80
|
+
if (exposedPorts.length > 0) {
|
|
81
|
+
lines.push(`EXPOSE ${exposedPorts.join(" ")}`);
|
|
82
|
+
}
|
|
83
|
+
lines.push("USER spawnfile");
|
|
84
|
+
lines.push('ENTRYPOINT ["/opt/spawnfile/entrypoint.sh"]');
|
|
85
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
86
|
+
};
|
|
87
|
+
const createEnvironmentAssignments = (plan) => {
|
|
88
|
+
const envAssignments = [];
|
|
89
|
+
if (plan.instancePaths.homePath) {
|
|
90
|
+
envAssignments.push(`HOME=${shellQuote(plan.instancePaths.homePath)}`);
|
|
91
|
+
}
|
|
92
|
+
if (plan.meta.homeEnv && plan.instancePaths.homePath) {
|
|
93
|
+
envAssignments.push(`${plan.meta.homeEnv}=${shellQuote(plan.instancePaths.homePath)}`);
|
|
94
|
+
}
|
|
95
|
+
if (plan.meta.configPathEnv) {
|
|
96
|
+
envAssignments.push(`${plan.meta.configPathEnv}=${shellQuote(plan.instancePaths.configPath)}`);
|
|
97
|
+
}
|
|
98
|
+
if (plan.meta.portEnv && plan.port) {
|
|
99
|
+
envAssignments.push(`${plan.meta.portEnv}=${shellQuote(String(plan.port))}`);
|
|
100
|
+
}
|
|
101
|
+
for (const [name, value] of Object.entries(plan.meta.staticEnv ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
|
|
102
|
+
envAssignments.push(`${name}=${shellQuote(value)}`);
|
|
103
|
+
}
|
|
104
|
+
return envAssignments;
|
|
105
|
+
};
|
|
106
|
+
const createEnvFileWrites = (plan) => plan.envFiles.map((binding) => `write_env_file ${shellQuote(binding.envName)} ${shellQuote(binding.filePath)}`);
|
|
107
|
+
const createConfigEnvWrites = (plan) => (plan.configEnvBindings ?? []).map((binding) => `apply_json_env_value ${shellQuote(plan.instancePaths.configPath)} ${shellQuote(binding.envName)} ${shellQuote(binding.jsonPath)}`);
|
|
108
|
+
const resolveStartCommand = (plan) => plan.meta.startCommand
|
|
109
|
+
.map((token) => token
|
|
110
|
+
.replaceAll("<runtime-root>", plan.runtimeRoot)
|
|
111
|
+
.replaceAll("<port>", plan.port ? String(plan.port) : ""))
|
|
112
|
+
.filter((token) => token.length > 0);
|
|
113
|
+
export const createRootfsFiles = (runtimePlans) => runtimePlans.flatMap((plan) => plan.targetFiles.map((file) => {
|
|
114
|
+
if (file.path === plan.meta.configFileName) {
|
|
115
|
+
return {
|
|
116
|
+
content: file.content
|
|
117
|
+
.replaceAll(WORKSPACE_PLACEHOLDER, plan.instancePaths.workspacePath)
|
|
118
|
+
.replaceAll(`"${GATEWAY_PORT_PLACEHOLDER}"`, plan.port ? String(plan.port) : "0")
|
|
119
|
+
.replaceAll(GATEWAY_PORT_PLACEHOLDER, plan.port ? String(plan.port) : ""),
|
|
120
|
+
path: `${CONTAINER_ROOTFS_ROOT}${plan.instancePaths.configPath}`
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (file.path.startsWith("workspace/")) {
|
|
124
|
+
const relativeWorkspacePath = file.path.slice("workspace/".length);
|
|
125
|
+
return {
|
|
126
|
+
content: file.content,
|
|
127
|
+
path: `${CONTAINER_ROOTFS_ROOT}${path.posix.join(plan.instancePaths.workspacePath, relativeWorkspacePath)}`
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (file.path.startsWith("home/")) {
|
|
131
|
+
if (!plan.instancePaths.homePath) {
|
|
132
|
+
throw new SpawnfileError("runtime_error", `Container target ${plan.id} for ${plan.runtimeName} emitted home-scoped files without a home path`);
|
|
133
|
+
}
|
|
134
|
+
const relativeHomePath = file.path.slice("home/".length);
|
|
135
|
+
return {
|
|
136
|
+
content: file.content,
|
|
137
|
+
path: `${CONTAINER_ROOTFS_ROOT}${path.posix.join(plan.instancePaths.homePath, relativeHomePath)}`
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
throw new SpawnfileError("runtime_error", `Container target ${plan.id} for ${plan.runtimeName} emitted unsupported path ${file.path}`);
|
|
141
|
+
}));
|
|
142
|
+
export const renderEntrypoint = (runtimePlans, requiredSecrets) => {
|
|
143
|
+
const lines = [
|
|
144
|
+
"#!/usr/bin/env bash",
|
|
145
|
+
"set -euo pipefail",
|
|
146
|
+
"",
|
|
147
|
+
"require_env() {",
|
|
148
|
+
' local name=\"$1\"',
|
|
149
|
+
' if [ -z \"${!name:-}\" ]; then',
|
|
150
|
+
' echo \"Missing required env: $name\" >&2',
|
|
151
|
+
" exit 1",
|
|
152
|
+
" fi",
|
|
153
|
+
"}",
|
|
154
|
+
"",
|
|
155
|
+
"require_file() {",
|
|
156
|
+
' local target=\"$1\"',
|
|
157
|
+
' if [ ! -f \"$target\" ]; then',
|
|
158
|
+
' echo \"Missing required file: $target\" >&2',
|
|
159
|
+
" exit 1",
|
|
160
|
+
" fi",
|
|
161
|
+
"}",
|
|
162
|
+
"",
|
|
163
|
+
"write_env_file() {",
|
|
164
|
+
' local name=\"$1\"',
|
|
165
|
+
' local target=\"$2\"',
|
|
166
|
+
' if [ -z \"${!name:-}\" ]; then',
|
|
167
|
+
" return",
|
|
168
|
+
" fi",
|
|
169
|
+
' mkdir -p \"$(dirname \"$target\")\"',
|
|
170
|
+
' printf %s \"${!name:-}\" > \"$target\"',
|
|
171
|
+
"}",
|
|
172
|
+
"",
|
|
173
|
+
"apply_json_env_value() {",
|
|
174
|
+
' local target=\"$1\"',
|
|
175
|
+
' local name=\"$2\"',
|
|
176
|
+
' local json_path=\"$3\"',
|
|
177
|
+
' if [ -z \"${!name:-}\" ]; then',
|
|
178
|
+
" return",
|
|
179
|
+
" fi",
|
|
180
|
+
" python3 - \"$target\" \"$name\" \"$json_path\" <<'PY'",
|
|
181
|
+
"import json",
|
|
182
|
+
"import os",
|
|
183
|
+
"import sys",
|
|
184
|
+
"",
|
|
185
|
+
"target_path = sys.argv[1]",
|
|
186
|
+
"env_name = sys.argv[2]",
|
|
187
|
+
"json_path = sys.argv[3].split('.')",
|
|
188
|
+
"value = os.environ.get(env_name)",
|
|
189
|
+
"if value is None:",
|
|
190
|
+
" raise SystemExit(0)",
|
|
191
|
+
"",
|
|
192
|
+
"with open(target_path, encoding='utf-8') as handle:",
|
|
193
|
+
" data = json.load(handle)",
|
|
194
|
+
"",
|
|
195
|
+
"cursor = data",
|
|
196
|
+
"for part in json_path[:-1]:",
|
|
197
|
+
" child = cursor.get(part)",
|
|
198
|
+
" if not isinstance(child, dict):",
|
|
199
|
+
" child = {}",
|
|
200
|
+
" cursor[part] = child",
|
|
201
|
+
" cursor = child",
|
|
202
|
+
"",
|
|
203
|
+
"cursor[json_path[-1]] = value",
|
|
204
|
+
"",
|
|
205
|
+
"with open(target_path, 'w', encoding='utf-8') as handle:",
|
|
206
|
+
" json.dump(data, handle, indent=2)",
|
|
207
|
+
" handle.write('\\n')",
|
|
208
|
+
"PY",
|
|
209
|
+
"}",
|
|
210
|
+
""
|
|
211
|
+
];
|
|
212
|
+
for (const secretName of requiredSecrets) {
|
|
213
|
+
lines.push(`require_env ${shellQuote(secretName)}`);
|
|
214
|
+
}
|
|
215
|
+
if (requiredSecrets.length > 0) {
|
|
216
|
+
lines.push("");
|
|
217
|
+
}
|
|
218
|
+
if (runtimePlans.length === 1) {
|
|
219
|
+
const plan = runtimePlans[0];
|
|
220
|
+
const commandTokens = resolveStartCommand(plan);
|
|
221
|
+
const envAssignments = createEnvironmentAssignments(plan);
|
|
222
|
+
const envFileWrites = createEnvFileWrites(plan);
|
|
223
|
+
const configEnvWrites = createConfigEnvWrites(plan);
|
|
224
|
+
lines.push(`mkdir -p ${shellQuote(plan.instancePaths.workspacePath)}`, `require_file ${shellQuote(plan.instancePaths.configPath)}`, ...envFileWrites, ...configEnvWrites, `${envAssignments.join(" ")} exec ${commandTokens.map(shellQuote).join(" ")}`);
|
|
225
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
226
|
+
}
|
|
227
|
+
lines.push("PIDS=()", "", "terminate_children() {", ' for pid in "${PIDS[@]:-}"; do', ' kill "$pid" 2>/dev/null || true', " done", "}", "", "trap terminate_children INT TERM EXIT", "");
|
|
228
|
+
for (const plan of runtimePlans) {
|
|
229
|
+
const commandTokens = resolveStartCommand(plan);
|
|
230
|
+
const envAssignments = createEnvironmentAssignments(plan);
|
|
231
|
+
const envFileWrites = createEnvFileWrites(plan);
|
|
232
|
+
const configEnvWrites = createConfigEnvWrites(plan);
|
|
233
|
+
lines.push(`mkdir -p ${shellQuote(plan.instancePaths.workspacePath)}`, `require_file ${shellQuote(plan.instancePaths.configPath)}`, ...envFileWrites, ...configEnvWrites, `${envAssignments.join(" ")} ${commandTokens.map(shellQuote).join(" ")} &`, 'PIDS+=("$!")', "");
|
|
234
|
+
}
|
|
235
|
+
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"');
|
|
236
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
237
|
+
};
|