spawnfile 0.1.1 → 0.1.3
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/README.md +80 -396
- package/dist/cli/index.js +0 -0
- package/dist/cli/modelCommands.d.ts +3 -0
- package/dist/cli/modelCommands.js +68 -0
- package/dist/cli/runCli.d.ts +23 -2
- package/dist/cli/runCli.js +78 -122
- package/dist/cli/runtimeCommands.d.ts +3 -0
- package/dist/cli/runtimeCommands.js +20 -0
- package/dist/cli/surfaceCommands.d.ts +3 -0
- package/dist/cli/surfaceCommands.js +98 -0
- package/dist/cli/viewCommand.d.ts +3 -0
- package/dist/cli/viewCommand.js +87 -0
- package/dist/compiler/agentSurfaces.js +51 -5
- package/dist/compiler/buildCompilePlan.js +38 -40
- package/dist/compiler/buildCompilePlanRuntime.d.ts +14 -0
- package/dist/compiler/buildCompilePlanRuntime.js +39 -0
- package/dist/compiler/buildCompilePlanTeams.d.ts +5 -0
- package/dist/compiler/buildCompilePlanTeams.js +38 -0
- package/dist/compiler/compilePlanHelpers.js +4 -1
- package/dist/compiler/compileProject.js +62 -13
- package/dist/compiler/compileProjectSupport.d.ts +17 -0
- package/dist/compiler/compileProjectSupport.js +136 -0
- package/dist/compiler/containerArtifacts.d.ts +6 -1
- package/dist/compiler/containerArtifacts.js +26 -4
- package/dist/compiler/containerArtifactsPlans.js +16 -1
- package/dist/compiler/containerArtifactsRender.d.ts +4 -2
- package/dist/compiler/containerArtifactsRender.js +21 -126
- package/dist/compiler/containerArtifactsTypes.d.ts +7 -0
- package/dist/compiler/containerEntrypointRender.d.ts +12 -0
- package/dist/compiler/containerEntrypointRender.js +186 -0
- package/dist/compiler/index.d.ts +4 -0
- package/dist/compiler/index.js +4 -0
- package/dist/compiler/interactiveSurfaceScopes.d.ts +2 -0
- package/dist/compiler/interactiveSurfaceScopes.js +21 -0
- package/dist/compiler/moltnetArtifacts.d.ts +27 -0
- package/dist/compiler/moltnetArtifacts.js +208 -0
- package/dist/compiler/moltnetBinaries.d.ts +4 -0
- package/dist/compiler/moltnetBinaries.js +103 -0
- package/dist/compiler/moltnetClientConfig.d.ts +11 -0
- package/dist/compiler/moltnetClientConfig.js +89 -0
- package/dist/compiler/moltnetRepresentativeResolution.d.ts +16 -0
- package/dist/compiler/moltnetRepresentativeResolution.js +86 -0
- package/dist/compiler/moltnetResolution.d.ts +3 -0
- package/dist/compiler/moltnetResolution.js +182 -0
- package/dist/compiler/moltnetRoomMemberships.d.ts +3 -0
- package/dist/compiler/moltnetRoomMemberships.js +140 -0
- package/dist/compiler/runProject.js +1 -1
- package/dist/compiler/surfaceDefinitions.d.ts +55 -0
- package/dist/compiler/surfaceDefinitions.js +204 -0
- package/dist/compiler/teamContextHelpers.d.ts +18 -0
- package/dist/compiler/teamContextHelpers.js +112 -0
- package/dist/compiler/teamContextSupport.d.ts +4 -0
- package/dist/compiler/teamContextSupport.js +264 -0
- package/dist/compiler/teamContextSupport.testHelpers.d.ts +16 -0
- package/dist/compiler/teamContextSupport.testHelpers.js +68 -0
- package/dist/compiler/teamContextTypes.d.ts +28 -0
- package/dist/compiler/teamContextTypes.js +1 -0
- package/dist/compiler/teamRoster.d.ts +12 -0
- package/dist/compiler/teamRoster.js +48 -0
- package/dist/compiler/teamRosterEntries.d.ts +13 -0
- package/dist/compiler/teamRosterEntries.js +230 -0
- package/dist/compiler/teamRosterTypes.d.ts +45 -0
- package/dist/compiler/teamRosterTypes.js +1 -0
- package/dist/compiler/types.d.ts +90 -6
- package/dist/compiler/updateProjectRuntime.d.ts +9 -0
- package/dist/compiler/updateProjectRuntime.js +67 -0
- package/dist/compiler/updateProjectSurfaces.d.ts +8 -0
- package/dist/compiler/updateProjectSurfaces.js +106 -0
- package/dist/compiler/view/buildOrganizationView.d.ts +2 -0
- package/dist/compiler/view/buildOrganizationView.js +180 -0
- package/dist/compiler/view/index.d.ts +4 -0
- package/dist/compiler/view/index.js +4 -0
- package/dist/compiler/view/renderNetworks.d.ts +2 -0
- package/dist/compiler/view/renderNetworks.js +93 -0
- package/dist/compiler/view/renderTree.d.ts +2 -0
- package/dist/compiler/view/renderTree.js +59 -0
- package/dist/compiler/view/sourcePaths.d.ts +2 -0
- package/dist/compiler/view/sourcePaths.js +19 -0
- package/dist/compiler/view/types.d.ts +80 -0
- package/dist/compiler/view/types.js +1 -0
- package/dist/manifest/loadManifest.js +4 -4
- package/dist/manifest/renderSpawnfile.js +74 -8
- package/dist/manifest/scaffold.js +1 -3
- package/dist/manifest/schemas.d.ts +227 -17
- package/dist/manifest/schemas.js +62 -20
- package/dist/manifest/surfaceSchemas.d.ts +154 -0
- package/dist/manifest/surfaceSchemas.js +77 -5
- package/dist/runtime/common.js +3 -0
- package/dist/runtime/openclaw/adapter.js +38 -5
- package/dist/runtime/openclaw/moltnet.d.ts +12 -0
- package/dist/runtime/openclaw/moltnet.js +124 -0
- package/dist/runtime/openclaw/surfaces.js +3 -0
- package/dist/runtime/picoclaw/adapter.js +27 -8
- package/dist/runtime/picoclaw/pico.d.ts +2 -0
- package/dist/runtime/picoclaw/pico.js +2 -0
- package/dist/runtime/picoclaw/surfaces.js +11 -0
- package/dist/runtime/tinyclaw/adapter.js +22 -8
- package/dist/runtime/tinyclaw/runAuth.js +28 -1
- package/dist/runtime/tinyclaw/surfaces.js +8 -0
- package/dist/runtime/types.d.ts +11 -0
- package/package.json +5 -3
- package/runtimes.yaml +4 -4
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { createRuntimeInstallRecipe } from "../runtime/index.js";
|
|
3
3
|
import { SpawnfileError } from "../shared/index.js";
|
|
4
|
+
export { renderEntrypoint } from "./containerEntrypointRender.js";
|
|
5
|
+
import { MOLTNET_BIN_DIRECTORY, MOLTNET_BINARY_NAMES } from "./moltnetBinaries.js";
|
|
4
6
|
const CONTAINER_ROOTFS_ROOT = "container/rootfs";
|
|
5
7
|
const GATEWAY_PORT_PLACEHOLDER = "<gateway-port>";
|
|
8
|
+
const MOLTNET_INSTALL_SCRIPT_URL = "https://moltnet.dev/install.sh";
|
|
6
9
|
const WORKSPACE_PLACEHOLDER = "<workspace-path>";
|
|
7
10
|
const shellQuote = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
8
11
|
const extractNodeMajorVersion = (image) => Number(image.match(/^node:(\d+)/)?.[1] ?? "0");
|
|
@@ -44,7 +47,7 @@ export const renderEnvExample = (variables) => {
|
|
|
44
47
|
}
|
|
45
48
|
return `${lines.join("\n").trimEnd()}\n`;
|
|
46
49
|
};
|
|
47
|
-
export const renderDockerfile = async (runtimePlans) => {
|
|
50
|
+
export const renderDockerfile = async (runtimePlans, options = {}) => {
|
|
48
51
|
const runtimeNames = [...new Set(runtimePlans.map((plan) => plan.runtimeName))];
|
|
49
52
|
const runtimeRecipes = await Promise.all(runtimeNames.map((runtimeName) => createRuntimeInstallRecipe(runtimeName)));
|
|
50
53
|
const baseImage = selectBaseImage(runtimePlans);
|
|
@@ -52,20 +55,33 @@ export const renderDockerfile = async (runtimePlans) => {
|
|
|
52
55
|
const systemDeps = [
|
|
53
56
|
...new Set([
|
|
54
57
|
...runtimePlans.flatMap((plan) => plan.meta.systemDeps),
|
|
58
|
+
...(options.hasMoltnet && !options.hasStagedMoltnetBinaries
|
|
59
|
+
? ["ca-certificates", "curl", "tar"]
|
|
60
|
+
: []),
|
|
55
61
|
...(needsJsonEnvWriter ? ["python3"] : [])
|
|
56
62
|
])
|
|
57
63
|
].sort();
|
|
58
64
|
const globalNpmPackages = [
|
|
59
65
|
...new Set(runtimePlans.flatMap((plan) => plan.meta.globalNpmPackages ?? []))
|
|
60
66
|
].sort();
|
|
61
|
-
const
|
|
62
|
-
const
|
|
67
|
+
const runtimePorts = runtimePlans.flatMap((plan) => plan.publishedPort ? [plan.publishedPort] : []);
|
|
68
|
+
const moltnetPorts = options.moltnetPublishedPorts ?? [];
|
|
69
|
+
const exposedPorts = [...new Set([...runtimePorts, ...moltnetPorts])].sort((left, right) => left - right);
|
|
70
|
+
const lines = [];
|
|
71
|
+
lines.push(`FROM ${baseImage}`);
|
|
72
|
+
lines.push("USER root", "", "WORKDIR /opt/spawnfile");
|
|
63
73
|
if (systemDeps.length > 0) {
|
|
64
74
|
lines.push(createPackageInstallCommand(systemDeps), "");
|
|
65
75
|
}
|
|
66
76
|
if (globalNpmPackages.length > 0) {
|
|
67
77
|
lines.push(createNpmPackageInstallCommand(globalNpmPackages), "");
|
|
68
78
|
}
|
|
79
|
+
if (options.hasMoltnet && options.hasStagedMoltnetBinaries) {
|
|
80
|
+
lines.push(`COPY ${MOLTNET_BIN_DIRECTORY}/ /usr/local/bin/`, `RUN chmod +x ${MOLTNET_BINARY_NAMES.map((binaryName) => `/usr/local/bin/${binaryName}`).join(" ")}`, "");
|
|
81
|
+
}
|
|
82
|
+
else if (options.hasMoltnet) {
|
|
83
|
+
lines.push(`RUN MOLTNET_INSTALL_DIR=/usr/local/bin sh -c ${shellQuote(`curl -fsSL ${MOLTNET_INSTALL_SCRIPT_URL} | sh`)}`, "");
|
|
84
|
+
}
|
|
69
85
|
for (const recipe of runtimeRecipes) {
|
|
70
86
|
for (const copyCommand of recipe.copyCommands) {
|
|
71
87
|
lines.push(copyCommand);
|
|
@@ -76,7 +92,8 @@ export const renderDockerfile = async (runtimePlans) => {
|
|
|
76
92
|
lines.push("");
|
|
77
93
|
}
|
|
78
94
|
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"
|
|
95
|
+
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");
|
|
96
|
+
lines.push("RUN mkdir -p /var/lib/spawnfile && chown -R spawnfile:spawnfile /var/lib/spawnfile /opt/spawnfile");
|
|
80
97
|
if (exposedPorts.length > 0) {
|
|
81
98
|
lines.push(`EXPOSE ${exposedPorts.join(" ")}`);
|
|
82
99
|
}
|
|
@@ -84,32 +101,6 @@ export const renderDockerfile = async (runtimePlans) => {
|
|
|
84
101
|
lines.push('ENTRYPOINT ["/opt/spawnfile/entrypoint.sh"]');
|
|
85
102
|
return `${lines.join("\n").trimEnd()}\n`;
|
|
86
103
|
};
|
|
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
104
|
export const createRootfsFiles = (runtimePlans) => runtimePlans.flatMap((plan) => plan.targetFiles.map((file) => {
|
|
114
105
|
if (file.path === plan.meta.configFileName) {
|
|
115
106
|
return {
|
|
@@ -139,99 +130,3 @@ export const createRootfsFiles = (runtimePlans) => runtimePlans.flatMap((plan) =
|
|
|
139
130
|
}
|
|
140
131
|
throw new SpawnfileError("runtime_error", `Container target ${plan.id} for ${plan.runtimeName} emitted unsupported path ${file.path}`);
|
|
141
132
|
}));
|
|
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
|
-
};
|
|
@@ -2,6 +2,7 @@ import type { ContainerReport } from "../report/index.js";
|
|
|
2
2
|
import type { EmittedFile, RuntimeContainerConfigEnvBinding, RuntimeContainerMeta } from "../runtime/index.js";
|
|
3
3
|
import type { ModelAuthMethod } from "../shared/index.js";
|
|
4
4
|
import type { ResolvedAgentNode, ResolvedTeamNode } from "./types.js";
|
|
5
|
+
import type { MoltnetBridgePlan, MoltnetServerPlan } from "./moltnetArtifacts.js";
|
|
5
6
|
export interface ContainerEnvVariable {
|
|
6
7
|
categories: Array<"model" | "project" | "runtime" | "surface">;
|
|
7
8
|
description: string;
|
|
@@ -24,8 +25,10 @@ export interface RuntimeTargetPlan {
|
|
|
24
25
|
modelAuthMethods: Record<string, ModelAuthMethod>;
|
|
25
26
|
modelSecretsRequired: string[];
|
|
26
27
|
port?: number;
|
|
28
|
+
publishedPort?: number;
|
|
27
29
|
runtimeName: string;
|
|
28
30
|
runtimeRoot: string;
|
|
31
|
+
targetConfigEnvBindings?: RuntimeContainerConfigEnvBinding[];
|
|
29
32
|
targetFiles: EmittedFile[];
|
|
30
33
|
}
|
|
31
34
|
export interface CompiledNodeArtifact {
|
|
@@ -38,5 +41,9 @@ export interface CompiledNodeArtifact {
|
|
|
38
41
|
export interface GeneratedContainerArtifacts {
|
|
39
42
|
executablePaths: string[];
|
|
40
43
|
files: EmittedFile[];
|
|
44
|
+
moltnet?: {
|
|
45
|
+
bridgePlans: MoltnetBridgePlan[];
|
|
46
|
+
serverPlans: MoltnetServerPlan[];
|
|
47
|
+
};
|
|
41
48
|
report: ContainerReport;
|
|
42
49
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RuntimeTargetPlan } from "./containerArtifactsTypes.js";
|
|
2
|
+
import type { MoltnetArtifacts } from "./moltnetArtifacts.js";
|
|
3
|
+
export interface EntrypointOptions {
|
|
4
|
+
hasMoltnet?: boolean;
|
|
5
|
+
hasStagedMoltnetBinaries?: boolean;
|
|
6
|
+
moltnet?: {
|
|
7
|
+
bridgePlans: MoltnetArtifacts["bridgePlans"];
|
|
8
|
+
serverPlans: MoltnetArtifacts["serverPlans"];
|
|
9
|
+
};
|
|
10
|
+
moltnetPublishedPorts?: number[];
|
|
11
|
+
}
|
|
12
|
+
export declare const renderEntrypoint: (runtimePlans: RuntimeTargetPlan[], requiredSecrets: string[], options?: EntrypointOptions) => string;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
const MOLTNET_SERVER_DATA_DIRECTORY = "/var/lib/spawnfile/moltnet/servers";
|
|
3
|
+
const shellQuote = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
4
|
+
const pathSafeSegment = (value) => value.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "server";
|
|
5
|
+
const createMoltnetServerDataPath = (serverId) => `${MOLTNET_SERVER_DATA_DIRECTORY}/${pathSafeSegment(serverId)}.db`;
|
|
6
|
+
const createEnvironmentAssignments = (plan) => {
|
|
7
|
+
const envAssignments = [];
|
|
8
|
+
if (plan.instancePaths.homePath) {
|
|
9
|
+
envAssignments.push(`HOME=${shellQuote(plan.instancePaths.homePath)}`);
|
|
10
|
+
}
|
|
11
|
+
if (plan.runtimeName === "tinyclaw" &&
|
|
12
|
+
plan.instancePaths.homePath &&
|
|
13
|
+
(plan.modelAuthMethods.openai === "api_key" || plan.modelAuthMethods.openai === "codex")) {
|
|
14
|
+
envAssignments.push(`CODEX_HOME=${shellQuote(path.posix.join(plan.instancePaths.homePath, ".codex"))}`);
|
|
15
|
+
}
|
|
16
|
+
if (plan.meta.homeEnv && plan.instancePaths.homePath) {
|
|
17
|
+
envAssignments.push(`${plan.meta.homeEnv}=${shellQuote(plan.instancePaths.homePath)}`);
|
|
18
|
+
}
|
|
19
|
+
if (plan.meta.configPathEnv) {
|
|
20
|
+
envAssignments.push(`${plan.meta.configPathEnv}=${shellQuote(plan.instancePaths.configPath)}`);
|
|
21
|
+
}
|
|
22
|
+
if (plan.meta.portEnv && plan.port) {
|
|
23
|
+
envAssignments.push(`${plan.meta.portEnv}=${shellQuote(String(plan.port))}`);
|
|
24
|
+
}
|
|
25
|
+
for (const [name, value] of Object.entries(plan.meta.staticEnv ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
|
|
26
|
+
envAssignments.push(`${name}=${shellQuote(value)}`);
|
|
27
|
+
}
|
|
28
|
+
return envAssignments;
|
|
29
|
+
};
|
|
30
|
+
const createEnvFileWrites = (plan) => plan.envFiles.map((binding) => `write_env_file ${shellQuote(binding.envName)} ${shellQuote(binding.filePath)}`);
|
|
31
|
+
const createConfigEnvWrites = (plan) => (plan.configEnvBindings ?? []).map((binding) => `apply_json_env_value ${shellQuote(plan.instancePaths.configPath)} ${shellQuote(binding.envName)} ${shellQuote(binding.jsonPath)}`);
|
|
32
|
+
const createAuthSetupCommands = (plan) => {
|
|
33
|
+
if (plan.runtimeName !== "tinyclaw" ||
|
|
34
|
+
!plan.instancePaths.homePath ||
|
|
35
|
+
plan.modelAuthMethods.openai !== "api_key") {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
return [`configure_codex_api_key_auth ${shellQuote(plan.instancePaths.homePath)}`];
|
|
39
|
+
};
|
|
40
|
+
const resolveStartCommand = (plan) => plan.meta.startCommand
|
|
41
|
+
.map((token) => token
|
|
42
|
+
.replaceAll("<runtime-root>", plan.runtimeRoot)
|
|
43
|
+
.replaceAll("<port>", plan.port ? String(plan.port) : ""))
|
|
44
|
+
.filter((token) => token.length > 0);
|
|
45
|
+
const createRuntimeReadinessWait = (plan) => {
|
|
46
|
+
if (plan.runtimeName !== "openclaw" || !plan.port) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
return [
|
|
50
|
+
"attempts=0",
|
|
51
|
+
`until curl -sf ${shellQuote(`http://127.0.0.1:${plan.port}/healthz`)} >/dev/null; do`,
|
|
52
|
+
" attempts=$((attempts + 1))",
|
|
53
|
+
' if [ "$attempts" -ge 180 ]; then',
|
|
54
|
+
` echo ${shellQuote(`Timed out waiting for ${plan.runtimeName} on port ${plan.port}`)} >&2`,
|
|
55
|
+
" exit 1",
|
|
56
|
+
" fi",
|
|
57
|
+
" sleep 1",
|
|
58
|
+
"done",
|
|
59
|
+
""
|
|
60
|
+
];
|
|
61
|
+
};
|
|
62
|
+
export const renderEntrypoint = (runtimePlans, requiredSecrets, options = {}) => {
|
|
63
|
+
const lines = [
|
|
64
|
+
"#!/usr/bin/env bash",
|
|
65
|
+
"set -euo pipefail",
|
|
66
|
+
"",
|
|
67
|
+
"require_env() {",
|
|
68
|
+
' local name=\"$1\"',
|
|
69
|
+
' if [ -z \"${!name:-}\" ]; then',
|
|
70
|
+
' echo \"Missing required env: $name\" >&2',
|
|
71
|
+
" exit 1",
|
|
72
|
+
" fi",
|
|
73
|
+
"}",
|
|
74
|
+
"",
|
|
75
|
+
"require_file() {",
|
|
76
|
+
' local target=\"$1\"',
|
|
77
|
+
' if [ ! -f \"$target\" ]; then',
|
|
78
|
+
' echo \"Missing required file: $target\" >&2',
|
|
79
|
+
" exit 1",
|
|
80
|
+
" fi",
|
|
81
|
+
"}",
|
|
82
|
+
"",
|
|
83
|
+
"write_env_file() {",
|
|
84
|
+
' local name=\"$1\"',
|
|
85
|
+
' local target=\"$2\"',
|
|
86
|
+
' if [ -z \"${!name:-}\" ]; then',
|
|
87
|
+
" return",
|
|
88
|
+
" fi",
|
|
89
|
+
' mkdir -p \"$(dirname \"$target\")\"',
|
|
90
|
+
' printf %s \"${!name:-}\" > \"$target\"',
|
|
91
|
+
"}",
|
|
92
|
+
"",
|
|
93
|
+
"apply_json_env_value() {",
|
|
94
|
+
' local target=\"$1\"',
|
|
95
|
+
' local name=\"$2\"',
|
|
96
|
+
' local json_path=\"$3\"',
|
|
97
|
+
' if [ -z \"${!name:-}\" ]; then',
|
|
98
|
+
" return",
|
|
99
|
+
" fi",
|
|
100
|
+
" python3 - \"$target\" \"$name\" \"$json_path\" <<'PY'",
|
|
101
|
+
"import json",
|
|
102
|
+
"import os",
|
|
103
|
+
"import sys",
|
|
104
|
+
"",
|
|
105
|
+
"target_path = sys.argv[1]",
|
|
106
|
+
"env_name = sys.argv[2]",
|
|
107
|
+
"json_path = sys.argv[3].split('.')",
|
|
108
|
+
"value = os.environ.get(env_name)",
|
|
109
|
+
"if value is None:",
|
|
110
|
+
" raise SystemExit(0)",
|
|
111
|
+
"",
|
|
112
|
+
"with open(target_path, encoding='utf-8') as handle:",
|
|
113
|
+
" data = json.load(handle)",
|
|
114
|
+
"",
|
|
115
|
+
"cursor = data",
|
|
116
|
+
"for part in json_path[:-1]:",
|
|
117
|
+
" child = cursor.get(part)",
|
|
118
|
+
" if not isinstance(child, dict):",
|
|
119
|
+
" child = {}",
|
|
120
|
+
" cursor[part] = child",
|
|
121
|
+
" cursor = child",
|
|
122
|
+
"",
|
|
123
|
+
"cursor[json_path[-1]] = value",
|
|
124
|
+
"",
|
|
125
|
+
"with open(target_path, 'w', encoding='utf-8') as handle:",
|
|
126
|
+
" json.dump(data, handle, indent=2)",
|
|
127
|
+
" handle.write('\\n')",
|
|
128
|
+
"PY",
|
|
129
|
+
"}",
|
|
130
|
+
"",
|
|
131
|
+
"configure_codex_api_key_auth() {",
|
|
132
|
+
' local home_path="$1"',
|
|
133
|
+
' if [ -z "${OPENAI_API_KEY:-}" ]; then',
|
|
134
|
+
" return",
|
|
135
|
+
" fi",
|
|
136
|
+
' mkdir -p "$home_path/.codex"',
|
|
137
|
+
' printf "%s\\n" "${OPENAI_API_KEY:-}" | HOME="$home_path" CODEX_HOME="$home_path/.codex" codex login --with-api-key >/dev/null',
|
|
138
|
+
"}",
|
|
139
|
+
""
|
|
140
|
+
];
|
|
141
|
+
lines.push('if [ -n "${OPENCLAW_GATEWAY_TOKEN:-}" ] && [ -z "${OPENCLAW_HOOKS_TOKEN:-}" ]; then', ' export OPENCLAW_HOOKS_TOKEN="hooks-${OPENCLAW_GATEWAY_TOKEN}"', "fi", "");
|
|
142
|
+
for (const secretName of requiredSecrets) {
|
|
143
|
+
lines.push(`require_env ${shellQuote(secretName)}`);
|
|
144
|
+
}
|
|
145
|
+
if (requiredSecrets.length > 0) {
|
|
146
|
+
lines.push("");
|
|
147
|
+
}
|
|
148
|
+
const moltnetServerPlans = options.moltnet?.serverPlans ?? [];
|
|
149
|
+
const moltnetBridgePlans = options.moltnet?.bridgePlans ?? [];
|
|
150
|
+
if (runtimePlans.length === 1 &&
|
|
151
|
+
moltnetServerPlans.length === 0 &&
|
|
152
|
+
moltnetBridgePlans.length === 0) {
|
|
153
|
+
const plan = runtimePlans[0];
|
|
154
|
+
const commandTokens = resolveStartCommand(plan);
|
|
155
|
+
const envAssignments = createEnvironmentAssignments(plan);
|
|
156
|
+
lines.push(`mkdir -p ${shellQuote(plan.instancePaths.workspacePath)}`, `require_file ${shellQuote(plan.instancePaths.configPath)}`, ...createEnvFileWrites(plan), ...createConfigEnvWrites(plan), ...createAuthSetupCommands(plan), `${envAssignments.join(" ")} exec ${commandTokens.map(shellQuote).join(" ")}`);
|
|
157
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
158
|
+
}
|
|
159
|
+
lines.push("PIDS=()", "", "terminate_children() {", ' for pid in "${PIDS[@]:-}"; do', ' kill "$pid" 2>/dev/null || true', " done", "}", "", "trap terminate_children INT TERM EXIT", "");
|
|
160
|
+
if (moltnetServerPlans.length > 0) {
|
|
161
|
+
lines.push(`mkdir -p ${shellQuote(MOLTNET_SERVER_DATA_DIRECTORY)}`, "");
|
|
162
|
+
}
|
|
163
|
+
for (const serverPlan of moltnetServerPlans) {
|
|
164
|
+
lines.push(`MOLTNET_DATA_PATH=${shellQuote(createMoltnetServerDataPath(serverPlan.id))} MOLTNET_LISTEN_ADDR=${shellQuote(`:${serverPlan.port}`)} MOLTNET_NETWORK_ID=${shellQuote(serverPlan.networkId)} MOLTNET_NETWORK_NAME=${shellQuote(serverPlan.name)} /usr/local/bin/moltnet &`, 'PIDS+=("$!")', "");
|
|
165
|
+
}
|
|
166
|
+
for (const plan of runtimePlans) {
|
|
167
|
+
const commandTokens = resolveStartCommand(plan);
|
|
168
|
+
const envAssignments = createEnvironmentAssignments(plan);
|
|
169
|
+
lines.push(`mkdir -p ${shellQuote(plan.instancePaths.workspacePath)}`, `require_file ${shellQuote(plan.instancePaths.configPath)}`, ...createEnvFileWrites(plan), ...createConfigEnvWrites(plan), ...createAuthSetupCommands(plan), `${envAssignments.join(" ")} ${commandTokens.map(shellQuote).join(" ")} &`, 'PIDS+=("$!")', "", ...createRuntimeReadinessWait(plan));
|
|
170
|
+
}
|
|
171
|
+
for (const serverPlan of moltnetServerPlans) {
|
|
172
|
+
lines.push(`until curl -sf ${shellQuote(`http://127.0.0.1:${serverPlan.port}/healthz`)} >/dev/null; do sleep 1; done`);
|
|
173
|
+
for (const room of serverPlan.rooms) {
|
|
174
|
+
lines.push(`curl -sf -X POST -H 'Content-Type: application/json' -d ${shellQuote(JSON.stringify({
|
|
175
|
+
id: room.id,
|
|
176
|
+
members: room.members
|
|
177
|
+
}))} ${shellQuote(`http://127.0.0.1:${serverPlan.port}/v1/rooms`)} >/dev/null || true`);
|
|
178
|
+
}
|
|
179
|
+
lines.push("");
|
|
180
|
+
}
|
|
181
|
+
for (const bridgePlan of moltnetBridgePlans) {
|
|
182
|
+
lines.push(`/usr/local/bin/moltnet bridge ${shellQuote(bridgePlan.configPath)} &`, 'PIDS+=("$!")', "");
|
|
183
|
+
}
|
|
184
|
+
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"');
|
|
185
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
186
|
+
};
|
package/dist/compiler/index.d.ts
CHANGED
|
@@ -3,7 +3,11 @@ export * from "./buildCompilePlan.js";
|
|
|
3
3
|
export * from "./buildProject.js";
|
|
4
4
|
export * from "./compileProject.js";
|
|
5
5
|
export * from "./initProject.js";
|
|
6
|
+
export * from "./moltnetRoomMemberships.js";
|
|
6
7
|
export * from "./runProject.js";
|
|
7
8
|
export * from "./syncProjectAuth.js";
|
|
8
9
|
export * from "./types.js";
|
|
9
10
|
export * from "./updateProjectModels.js";
|
|
11
|
+
export * from "./updateProjectRuntime.js";
|
|
12
|
+
export * from "./updateProjectSurfaces.js";
|
|
13
|
+
export * from "./view/index.js";
|
package/dist/compiler/index.js
CHANGED
|
@@ -3,7 +3,11 @@ export * from "./buildCompilePlan.js";
|
|
|
3
3
|
export * from "./buildProject.js";
|
|
4
4
|
export * from "./compileProject.js";
|
|
5
5
|
export * from "./initProject.js";
|
|
6
|
+
export * from "./moltnetRoomMemberships.js";
|
|
6
7
|
export * from "./runProject.js";
|
|
7
8
|
export * from "./syncProjectAuth.js";
|
|
8
9
|
export * from "./types.js";
|
|
9
10
|
export * from "./updateProjectModels.js";
|
|
11
|
+
export * from "./updateProjectRuntime.js";
|
|
12
|
+
export * from "./updateProjectSurfaces.js";
|
|
13
|
+
export * from "./view/index.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const listMoltnetAttachmentScopes = (attachment) => {
|
|
2
|
+
const scopes = Object.keys(attachment.rooms ?? {})
|
|
3
|
+
.sort((left, right) => left.localeCompare(right))
|
|
4
|
+
.map((roomId) => `moltnet:${attachment.network}:room:${roomId}`);
|
|
5
|
+
if (attachment.dms?.enabled) {
|
|
6
|
+
scopes.push(`moltnet:${attachment.network}:dms`);
|
|
7
|
+
}
|
|
8
|
+
return scopes;
|
|
9
|
+
};
|
|
10
|
+
export const listInteractiveSurfaceScopes = (surfaces) => {
|
|
11
|
+
if (!surfaces) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
return [
|
|
15
|
+
...(surfaces.discord ? ["discord"] : []),
|
|
16
|
+
...(surfaces.moltnet?.flatMap(listMoltnetAttachmentScopes) ?? []),
|
|
17
|
+
...(surfaces.slack ? ["slack"] : []),
|
|
18
|
+
...(surfaces.telegram ? ["telegram"] : []),
|
|
19
|
+
...(surfaces.whatsapp ? ["whatsapp"] : [])
|
|
20
|
+
];
|
|
21
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { EmittedFile } from "../runtime/index.js";
|
|
2
|
+
import type { CompilePlan } from "./types.js";
|
|
3
|
+
export interface MoltnetServerPlan {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
networkId: string;
|
|
7
|
+
port: number;
|
|
8
|
+
rooms: Array<{
|
|
9
|
+
id: string;
|
|
10
|
+
members: string[];
|
|
11
|
+
}>;
|
|
12
|
+
teamSource: string;
|
|
13
|
+
}
|
|
14
|
+
export interface MoltnetBridgePlan {
|
|
15
|
+
agentId: string;
|
|
16
|
+
configPath: string;
|
|
17
|
+
networkId: string;
|
|
18
|
+
runtime: string;
|
|
19
|
+
}
|
|
20
|
+
export interface MoltnetArtifacts {
|
|
21
|
+
bridgePlans: MoltnetBridgePlan[];
|
|
22
|
+
files: EmittedFile[];
|
|
23
|
+
ports: number[];
|
|
24
|
+
publishedPorts: number[];
|
|
25
|
+
serverPlans: MoltnetServerPlan[];
|
|
26
|
+
}
|
|
27
|
+
export declare const generateMoltnetArtifacts: (plan: CompilePlan) => Promise<MoltnetArtifacts | null>;
|