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.
- package/README.md +9 -2
- package/dist/auth/runtimeCredentials.js +18 -2
- package/dist/cli/devCommands.d.ts +3 -0
- package/dist/cli/devCommands.js +115 -0
- package/dist/cli/lifecycleCommands.js +2 -0
- package/dist/cli/runCli.d.ts +6 -0
- package/dist/cli/runCli.js +4 -0
- package/dist/cli/statusCommand.d.ts +2 -1
- package/dist/cli/statusCommand.js +50 -16
- package/dist/compiler/buildProject.d.ts +9 -1
- package/dist/compiler/buildProject.js +44 -5
- package/dist/compiler/compileProject.d.ts +2 -0
- package/dist/compiler/compileProject.js +3 -1
- package/dist/compiler/containerArtifactsRender.js +25 -69
- package/dist/compiler/containerEntrypointRender.js +4 -1
- package/dist/compiler/containerPackageRender.d.ts +15 -0
- package/dist/compiler/containerPackageRender.js +63 -0
- package/dist/compiler/containerTargetResources.js +3 -1
- package/dist/compiler/moltnetArtifacts.js +16 -1
- package/dist/compiler/moltnetBinaries.d.ts +4 -1
- package/dist/compiler/moltnetBinaries.js +24 -3
- package/dist/compiler/moltnetClientConfig.js +11 -0
- package/dist/compiler/moltnetRuntimeConfig.js +11 -0
- package/dist/compiler/runProject.js +7 -1
- package/dist/compiler/runProjectDocker.js +157 -12
- package/dist/compiler/upProject.js +1 -0
- package/dist/deployment/dockerRecover.d.ts +17 -0
- package/dist/deployment/dockerRecover.js +129 -0
- package/dist/deployment/index.d.ts +1 -0
- package/dist/deployment/index.js +1 -0
- package/dist/dev/activity.d.ts +14 -0
- package/dist/dev/activity.js +48 -0
- package/dist/dev/docker.d.ts +16 -0
- package/dist/dev/docker.js +71 -0
- package/dist/dev/index.d.ts +2 -0
- package/dist/dev/index.js +2 -0
- package/dist/dev/project.d.ts +35 -0
- package/dist/dev/project.js +164 -0
- package/dist/runtime/common.d.ts +2 -0
- package/dist/runtime/common.js +1 -1
- package/dist/runtime/container.d.ts +1 -0
- package/dist/runtime/container.js +93 -10
- package/dist/runtime/install.d.ts +1 -1
- package/dist/runtime/install.js +43 -1
- package/dist/runtime/pi/adapter.d.ts +4 -0
- package/dist/runtime/pi/adapter.js +198 -0
- package/dist/runtime/pi/appActivitySource.d.ts +1 -0
- package/dist/runtime/pi/appActivitySource.js +87 -0
- package/dist/runtime/pi/appCoreSource.d.ts +1 -0
- package/dist/runtime/pi/appCoreSource.js +354 -0
- package/dist/runtime/pi/appPreludeSource.d.ts +1 -0
- package/dist/runtime/pi/appPreludeSource.js +103 -0
- package/dist/runtime/pi/appSource.d.ts +1 -0
- package/dist/runtime/pi/appSource.js +8 -0
- package/dist/runtime/pi/appTemplate.d.ts +28 -0
- package/dist/runtime/pi/appTemplate.js +144 -0
- package/dist/runtime/pi/runAuth.d.ts +2 -0
- package/dist/runtime/pi/runAuth.js +68 -0
- package/dist/runtime/registry.js +3 -0
- package/dist/runtime/types.d.ts +1 -0
- package/dist/status/deployments.js +5 -12
- package/package.json +7 -2
- package/runtimes.yaml +27 -11
|
@@ -4,16 +4,15 @@ import { createRuntimeInstallRecipe } from "../runtime/index.js";
|
|
|
4
4
|
import { SpawnfileError } from "../shared/index.js";
|
|
5
5
|
export { renderEntrypoint } from "./containerEntrypointRender.js";
|
|
6
6
|
import { MOLTNET_BIN_DIRECTORY, MOLTNET_BINARY_NAMES } from "./moltnetBinaries.js";
|
|
7
|
+
import { collectPackagesByManager, createAptPackageInstallItem, createNpmPackageInstallCommand, createNpmPackageInstallItem, createPackageInstallCommand, createPipxPackageInstallCommand, createPipxPackageInstallItem, getNpmInstallItemName } from "./containerPackageRender.js";
|
|
7
8
|
const CONTAINER_ROOTFS_ROOT = "container/rootfs";
|
|
8
9
|
const GATEWAY_PORT_PLACEHOLDER = "<gateway-port>";
|
|
9
10
|
const MOLTNET_INSTALL_SCRIPT_URL = "https://moltnet.dev/install.sh";
|
|
10
11
|
const MOLTNET_RELEASE_METADATA_URL = "https://api.github.com/repos/noopolis/moltnet/releases/latest";
|
|
11
12
|
const WORKSPACE_PLACEHOLDER = "<workspace-path>";
|
|
13
|
+
const RUNTIME_ROOT_PLACEHOLDER = "<runtime-root>";
|
|
12
14
|
const shellQuote = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
13
15
|
const extractNodeMajorVersion = (image) => Number(image.match(/^node:(\d+)/)?.[1] ?? "0");
|
|
14
|
-
const createPackageInstallCommand = (packages) => `RUN apt-get update && apt-get install -y --no-install-recommends ${packages.join(" ")} && rm -rf /var/lib/apt/lists/*`;
|
|
15
|
-
const createNpmPackageInstallCommand = (packages) => `RUN npm install -g --omit=dev --no-fund --no-audit ${packages.join(" ")}`;
|
|
16
|
-
const createPipxPackageInstallCommand = (packages) => `RUN PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install ${packages.join(" ")}`;
|
|
17
16
|
const createStateOwnershipCommand = (persistentMountPaths = []) => {
|
|
18
17
|
const mountPaths = [...new Set(persistentMountPaths)].sort();
|
|
19
18
|
const mkdirPaths = [...new Set(["/var/lib/spawnfile", ...mountPaths])].sort();
|
|
@@ -21,7 +20,6 @@ const createStateOwnershipCommand = (persistentMountPaths = []) => {
|
|
|
21
20
|
const chownPaths = [
|
|
22
21
|
...new Set([
|
|
23
22
|
"/var/lib/spawnfile",
|
|
24
|
-
"/opt/spawnfile",
|
|
25
23
|
...mountPaths.filter((mountPath) => !mountPath.startsWith("/var/lib/spawnfile/"))
|
|
26
24
|
])
|
|
27
25
|
].sort();
|
|
@@ -31,70 +29,13 @@ const createStateOwnershipCommand = (persistentMountPaths = []) => {
|
|
|
31
29
|
`chown -R spawnfile:spawnfile ${chownPaths.map(shellQuote).join(" ")}`
|
|
32
30
|
].join(" && ");
|
|
33
31
|
};
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
};
|
|
41
|
-
const createPackageIdentity = (pkg) => `${pkg.manager}\u0000${pkg.name}\u0000${pkg.version ?? ""}\u0000${pkg.scope ?? ""}`;
|
|
42
|
-
const createPackageConflictIdentity = (pkg) => `${pkg.manager}\u0000${pkg.name}`;
|
|
43
|
-
const assertImagePackageCompatibility = (packages) => {
|
|
44
|
-
const byName = new Map();
|
|
45
|
-
for (const currentPackage of packages) {
|
|
46
|
-
const conflictIdentity = createPackageConflictIdentity(currentPackage);
|
|
47
|
-
const existingPackage = byName.get(conflictIdentity);
|
|
48
|
-
if (!existingPackage) {
|
|
49
|
-
byName.set(conflictIdentity, currentPackage);
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
if (createPackageIdentity(existingPackage) !== createPackageIdentity(currentPackage)) {
|
|
53
|
-
throw new SpawnfileError("validation_error", `Generated container declares conflicting package definitions for ${currentPackage.manager} package ${currentPackage.name}`);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
const createAptPackageInstallItem = (pkg) => pkg.version ? `${pkg.name}=${pkg.version}` : pkg.name;
|
|
58
|
-
const createNpmPackageInstallItem = (pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name;
|
|
59
|
-
const createPipxPackageInstallItem = (pkg) => pkg.version ? `${pkg.name}==${pkg.version}` : pkg.name;
|
|
60
|
-
const getNpmInstallItemName = (item) => {
|
|
61
|
-
if (!item.startsWith("@")) {
|
|
62
|
-
const versionMarker = item.indexOf("@");
|
|
63
|
-
return versionMarker === -1 ? item : item.slice(0, versionMarker);
|
|
64
|
-
}
|
|
65
|
-
const slashIndex = item.indexOf("/");
|
|
66
|
-
if (slashIndex === -1) {
|
|
67
|
-
return item;
|
|
68
|
-
}
|
|
69
|
-
const versionMarker = item.indexOf("@", slashIndex);
|
|
70
|
-
return versionMarker === -1 ? item : item.slice(0, versionMarker);
|
|
71
|
-
};
|
|
72
|
-
const collectPackagesByManager = (runtimePlans) => {
|
|
73
|
-
const packagesByManager = {
|
|
74
|
-
apt: [],
|
|
75
|
-
npm: [],
|
|
76
|
-
pipx: []
|
|
77
|
-
};
|
|
78
|
-
const imagePackages = runtimePlans.flatMap((plan) => plan.packages ?? []);
|
|
79
|
-
assertImagePackageCompatibility(imagePackages);
|
|
80
|
-
const resolvedPackages = dedupePackages(imagePackages);
|
|
81
|
-
for (const packageConfig of resolvedPackages) {
|
|
82
|
-
if (packageConfig.manager === "apt") {
|
|
83
|
-
packagesByManager.apt.push(packageConfig);
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
if (packageConfig.manager === "npm") {
|
|
87
|
-
packagesByManager.npm.push(packageConfig);
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (packageConfig.manager === "pipx") {
|
|
91
|
-
packagesByManager.pipx.push(packageConfig);
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
32
|
+
const selectBaseImage = (runtimePlans, runtimeRecipes) => {
|
|
33
|
+
const recipeBaseImages = [
|
|
34
|
+
...new Set(runtimeRecipes.flatMap((recipe) => recipe.baseImage ? [recipe.baseImage] : []))
|
|
35
|
+
];
|
|
36
|
+
if (recipeBaseImages.length === 1 && runtimePlans.length === runtimeRecipes.length) {
|
|
37
|
+
return recipeBaseImages[0];
|
|
94
38
|
}
|
|
95
|
-
return packagesByManager;
|
|
96
|
-
};
|
|
97
|
-
const selectBaseImage = (runtimePlans) => {
|
|
98
39
|
const firstRuntimeMeta = runtimePlans[0]?.meta;
|
|
99
40
|
if (runtimePlans.length <= 1) {
|
|
100
41
|
return firstRuntimeMeta?.standaloneBaseImage ?? "debian:bookworm-slim";
|
|
@@ -133,12 +74,13 @@ export const renderEnvExample = (variables) => {
|
|
|
133
74
|
export const renderDockerfile = async (runtimePlans, options = {}) => {
|
|
134
75
|
const runtimeNames = [...new Set(runtimePlans.map((plan) => plan.runtimeName))];
|
|
135
76
|
const runtimeRecipes = await Promise.all(runtimeNames.map((runtimeName) => createRuntimeInstallRecipe(runtimeName)));
|
|
136
|
-
const
|
|
77
|
+
const recipeByRuntimeName = new Map(runtimeRecipes.map((recipe) => [recipe.runtimeName, recipe]));
|
|
78
|
+
const baseImage = selectBaseImage(runtimePlans, runtimeRecipes);
|
|
137
79
|
const needsJsonEnvWriter = runtimePlans.some((plan) => (plan.configEnvBindings?.length ?? 0) > 0);
|
|
138
80
|
const needsGit = runtimePlans.some((plan) => (plan.resources ?? []).some((resource) => resource.kind === "git"));
|
|
139
81
|
const systemDeps = [
|
|
140
82
|
...new Set([
|
|
141
|
-
...runtimePlans.flatMap((plan) => plan.meta.systemDeps),
|
|
83
|
+
...runtimePlans.flatMap((plan) => recipeByRuntimeName.get(plan.runtimeName)?.baseImage ? [] : plan.meta.systemDeps),
|
|
142
84
|
...(needsGit ? ["git"] : []),
|
|
143
85
|
...(options.hasMoltnet && !options.hasStagedMoltnetBinaries
|
|
144
86
|
? ["ca-certificates", "curl", "tar"]
|
|
@@ -195,6 +137,12 @@ export const renderDockerfile = async (runtimePlans, options = {}) => {
|
|
|
195
137
|
}
|
|
196
138
|
lines.push('RUN if ! id -u spawnfile >/dev/null 2>&1; then useradd --create-home --home-dir /home/spawnfile --shell /bin/bash spawnfile; fi', "");
|
|
197
139
|
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");
|
|
140
|
+
const postRootfsCommands = [
|
|
141
|
+
...new Set(runtimePlans.flatMap((plan) => plan.meta.postRootfsCommands ?? []))
|
|
142
|
+
];
|
|
143
|
+
for (const command of postRootfsCommands) {
|
|
144
|
+
lines.push(`RUN ${command}`);
|
|
145
|
+
}
|
|
198
146
|
if (options.distribution) {
|
|
199
147
|
lines.push("", `COPY ${options.distribution.reportOutputFile} ${DISTRIBUTION_REPORT_IMAGE_PATH}`, ...Object.entries(options.distribution.labels)
|
|
200
148
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
@@ -213,11 +161,19 @@ export const createRootfsFiles = (runtimePlans) => runtimePlans.flatMap((plan) =
|
|
|
213
161
|
return {
|
|
214
162
|
content: file.content
|
|
215
163
|
.replaceAll(WORKSPACE_PLACEHOLDER, plan.instancePaths.workspacePath)
|
|
164
|
+
.replaceAll(RUNTIME_ROOT_PLACEHOLDER, plan.runtimeRoot)
|
|
216
165
|
.replaceAll(`"${GATEWAY_PORT_PLACEHOLDER}"`, plan.port ? String(plan.port) : "0")
|
|
217
166
|
.replaceAll(GATEWAY_PORT_PLACEHOLDER, plan.port ? String(plan.port) : ""),
|
|
218
167
|
path: `${CONTAINER_ROOTFS_ROOT}${plan.instancePaths.configPath}`
|
|
219
168
|
};
|
|
220
169
|
}
|
|
170
|
+
if (file.path.startsWith("runtime/")) {
|
|
171
|
+
const relativeRuntimePath = file.path.slice("runtime/".length);
|
|
172
|
+
return {
|
|
173
|
+
content: file.content,
|
|
174
|
+
path: `${CONTAINER_ROOTFS_ROOT}${path.posix.join(plan.runtimeRoot, relativeRuntimePath)}`
|
|
175
|
+
};
|
|
176
|
+
}
|
|
221
177
|
if (file.path.startsWith("workspace/")) {
|
|
222
178
|
const relativeWorkspacePath = file.path.slice("workspace/".length);
|
|
223
179
|
return {
|
|
@@ -43,11 +43,14 @@ const createMoltnetStorePrepareCommands = (serverPlan) => {
|
|
|
43
43
|
};
|
|
44
44
|
const resolveStartCommand = (plan) => plan.meta.startCommand
|
|
45
45
|
.map((token) => token
|
|
46
|
+
.replaceAll("<config-path>", plan.instancePaths.configPath)
|
|
47
|
+
.replaceAll("<home-path>", plan.instancePaths.homePath ?? "")
|
|
46
48
|
.replaceAll("<runtime-root>", plan.runtimeRoot)
|
|
49
|
+
.replaceAll("<workspace-path>", plan.instancePaths.workspacePath)
|
|
47
50
|
.replaceAll("<port>", plan.port ? String(plan.port) : ""))
|
|
48
51
|
.filter((token) => token.length > 0);
|
|
49
52
|
const createRuntimeReadinessWait = (plan) => {
|
|
50
|
-
if (plan.runtimeName
|
|
53
|
+
if (!["openclaw", "pi"].includes(plan.runtimeName) || !plan.port) {
|
|
51
54
|
return [];
|
|
52
55
|
}
|
|
53
56
|
return [
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RuntimeTargetPlan } from "./containerArtifactsTypes.js";
|
|
2
|
+
import type { ResolvedPackage } from "./types.js";
|
|
3
|
+
export interface PackagesByManager {
|
|
4
|
+
apt: ResolvedPackage[];
|
|
5
|
+
npm: ResolvedPackage[];
|
|
6
|
+
pipx: ResolvedPackage[];
|
|
7
|
+
}
|
|
8
|
+
export declare const createPackageInstallCommand: (packages: string[]) => string;
|
|
9
|
+
export declare const createNpmPackageInstallCommand: (packages: string[]) => string;
|
|
10
|
+
export declare const createPipxPackageInstallCommand: (packages: string[]) => string;
|
|
11
|
+
export declare const createAptPackageInstallItem: (pkg: ResolvedPackage) => string;
|
|
12
|
+
export declare const createNpmPackageInstallItem: (pkg: ResolvedPackage) => string;
|
|
13
|
+
export declare const createPipxPackageInstallItem: (pkg: ResolvedPackage) => string;
|
|
14
|
+
export declare const getNpmInstallItemName: (item: string) => string;
|
|
15
|
+
export declare const collectPackagesByManager: (runtimePlans: RuntimeTargetPlan[]) => PackagesByManager;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
2
|
+
export const createPackageInstallCommand = (packages) => `RUN apt-get update && apt-get install -y --no-install-recommends ${packages.join(" ")} && rm -rf /var/lib/apt/lists/*`;
|
|
3
|
+
export const createNpmPackageInstallCommand = (packages) => `RUN npm install -g --omit=dev --no-fund --no-audit ${packages.join(" ")}`;
|
|
4
|
+
export const createPipxPackageInstallCommand = (packages) => `RUN PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install ${packages.join(" ")}`;
|
|
5
|
+
const packageIdentity = (pkg) => `${pkg.manager}\u0000${pkg.name}\u0000${pkg.version ?? ""}\u0000${pkg.scope ?? ""}`;
|
|
6
|
+
const packageConflictIdentity = (pkg) => `${pkg.manager}\u0000${pkg.name}`;
|
|
7
|
+
const dedupePackages = (packages) => {
|
|
8
|
+
const seen = new Map();
|
|
9
|
+
for (const currentPackage of packages) {
|
|
10
|
+
seen.set(packageIdentity(currentPackage), currentPackage);
|
|
11
|
+
}
|
|
12
|
+
return [...seen.values()].sort((left, right) => packageIdentity(left).localeCompare(packageIdentity(right)));
|
|
13
|
+
};
|
|
14
|
+
const assertImagePackageCompatibility = (packages) => {
|
|
15
|
+
const byName = new Map();
|
|
16
|
+
for (const currentPackage of packages) {
|
|
17
|
+
const conflictIdentity = packageConflictIdentity(currentPackage);
|
|
18
|
+
const existingPackage = byName.get(conflictIdentity);
|
|
19
|
+
if (!existingPackage) {
|
|
20
|
+
byName.set(conflictIdentity, currentPackage);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (packageIdentity(existingPackage) !== packageIdentity(currentPackage)) {
|
|
24
|
+
throw new SpawnfileError("validation_error", `Generated container declares conflicting package definitions for ${currentPackage.manager} package ${currentPackage.name}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
export const createAptPackageInstallItem = (pkg) => pkg.version ? `${pkg.name}=${pkg.version}` : pkg.name;
|
|
29
|
+
export const createNpmPackageInstallItem = (pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name;
|
|
30
|
+
export const createPipxPackageInstallItem = (pkg) => pkg.version ? `${pkg.name}==${pkg.version}` : pkg.name;
|
|
31
|
+
export const getNpmInstallItemName = (item) => {
|
|
32
|
+
if (!item.startsWith("@")) {
|
|
33
|
+
const versionMarker = item.indexOf("@");
|
|
34
|
+
return versionMarker === -1 ? item : item.slice(0, versionMarker);
|
|
35
|
+
}
|
|
36
|
+
const slashIndex = item.indexOf("/");
|
|
37
|
+
if (slashIndex === -1) {
|
|
38
|
+
return item;
|
|
39
|
+
}
|
|
40
|
+
const versionMarker = item.indexOf("@", slashIndex);
|
|
41
|
+
return versionMarker === -1 ? item : item.slice(0, versionMarker);
|
|
42
|
+
};
|
|
43
|
+
export const collectPackagesByManager = (runtimePlans) => {
|
|
44
|
+
const packagesByManager = {
|
|
45
|
+
apt: [],
|
|
46
|
+
npm: [],
|
|
47
|
+
pipx: []
|
|
48
|
+
};
|
|
49
|
+
const imagePackages = runtimePlans.flatMap((plan) => plan.packages ?? []);
|
|
50
|
+
assertImagePackageCompatibility(imagePackages);
|
|
51
|
+
for (const packageConfig of dedupePackages(imagePackages)) {
|
|
52
|
+
if (packageConfig.manager === "apt") {
|
|
53
|
+
packagesByManager.apt.push(packageConfig);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (packageConfig.manager === "npm") {
|
|
57
|
+
packagesByManager.npm.push(packageConfig);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
packagesByManager.pipx.push(packageConfig);
|
|
61
|
+
}
|
|
62
|
+
return packagesByManager;
|
|
63
|
+
};
|
|
@@ -4,13 +4,15 @@ import { mergeWorkspaceResourcePlans } from "./workspaceResources.js";
|
|
|
4
4
|
const CONFIG_FILE_PLACEHOLDER = "<config-file>";
|
|
5
5
|
const INSTANCE_ROOT_PLACEHOLDER = "<instance-root>";
|
|
6
6
|
const SOURCE_AGENT_PLACEHOLDER = "<agent-name>";
|
|
7
|
+
const SOURCE_SLUG_PLACEHOLDER = "<source-slug>";
|
|
7
8
|
const replaceSourceWorkspacePathTemplate = (template, input, instancePaths, meta) => {
|
|
8
9
|
const agentName = input.value.kind === "agent" ? input.value.name : input.slug;
|
|
9
10
|
const instanceRoot = instancePaths.instanceRoot ?? path.posix.dirname(instancePaths.workspacePath);
|
|
10
11
|
return template
|
|
11
12
|
.replaceAll(INSTANCE_ROOT_PLACEHOLDER, instanceRoot)
|
|
12
13
|
.replaceAll(CONFIG_FILE_PLACEHOLDER, meta.configFileName)
|
|
13
|
-
.replaceAll(SOURCE_AGENT_PLACEHOLDER, agentName)
|
|
14
|
+
.replaceAll(SOURCE_AGENT_PLACEHOLDER, agentName)
|
|
15
|
+
.replaceAll(SOURCE_SLUG_PLACEHOLDER, input.slug);
|
|
14
16
|
};
|
|
15
17
|
const resolveSourceWorkspacePath = (input, instancePaths, meta) => meta.instancePaths.sourceWorkspacePathTemplate
|
|
16
18
|
? replaceSourceWorkspacePathTemplate(meta.instancePaths.sourceWorkspacePathTemplate, input, instancePaths, meta)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SpawnfileError } from "../shared/index.js";
|
|
2
|
-
import { createMoltnetOpenTokenDirectory, createMoltnetNativeServerConfig, createMoltnetNodeConfigPath, createMoltnetServerConfigPath, resolveMoltnetBaseUrl, resolveMoltnetStorePersistenceMountPath } from "./moltnetConfigLowering.js";
|
|
2
|
+
import { createMoltnetOpenTokenDirectory, createMoltnetNativeServerConfig, createMoltnetNodeConfigPath, createMoltnetServerConfigPath, resolveMoltnetClientAuth, resolveMoltnetBaseUrl, resolveMoltnetStorePersistenceMountPath } from "./moltnetConfigLowering.js";
|
|
3
3
|
import { assertNetworkBindingEnvUniqueness } from "./networkBinding.js";
|
|
4
4
|
import { listConcreteMoltnetRoomMemberIds } from "./moltnetRoomMemberships.js";
|
|
5
5
|
import { assertCompatibleMoltnetNetworkName, assertCompatibleMoltnetRoomPolicy, assertCompatibleMoltnetServer } from "./moltnetRoomPolicyCompatibility.js";
|
|
@@ -178,6 +178,21 @@ export const generateMoltnetArtifacts = async (plan) => {
|
|
|
178
178
|
throw new SpawnfileError("validation_error", `Duplicate Moltnet node attachment for ${attachment.network}/${attachment.memberId}`);
|
|
179
179
|
}
|
|
180
180
|
nodePlanKeys.add(nodePlanKey);
|
|
181
|
+
if (agentNode.runtime.name === "pi") {
|
|
182
|
+
const clientAuth = resolveMoltnetClientAuth(serverPlan.server, attachment.network, attachment.memberId, node.slug);
|
|
183
|
+
const usesPerAttachmentOpenToken = clientAuth.mode === "open" &&
|
|
184
|
+
clientAuth.staticToken !== true &&
|
|
185
|
+
Boolean(clientAuth.tokenEnv || clientAuth.tokenPath);
|
|
186
|
+
if (usesPerAttachmentOpenToken && clientAuth.tokenPath) {
|
|
187
|
+
const mountId = `agent-${node.slug}-moltnet-tokens`;
|
|
188
|
+
addPersistentMount({
|
|
189
|
+
id: mountId,
|
|
190
|
+
mountPath: createMoltnetOpenTokenDirectory(node.slug),
|
|
191
|
+
reason: `Moltnet open-mode generated agent tokens for ${agentNode.name}`,
|
|
192
|
+
volumeName: createPersistentVolumeName(plan.root, mountId)
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
181
196
|
const nodeConfig = createMoltnetNodeConfigContent({
|
|
182
197
|
agentNode,
|
|
183
198
|
attachment: { ...attachment, memberId: attachment.memberId },
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export declare const MOLTNET_BIN_DIRECTORY = "moltnet-bin";
|
|
2
2
|
export declare const MOLTNET_BINARY_NAMES: readonly ["moltnet"];
|
|
3
|
+
export type MoltnetTargetArchitecture = "amd64" | "arm64";
|
|
3
4
|
export declare const resolveMoltnetCliCommand: () => Promise<string>;
|
|
4
|
-
export declare const stageMoltnetBinaries: (outputDirectory: string
|
|
5
|
+
export declare const stageMoltnetBinaries: (outputDirectory: string, options?: {
|
|
6
|
+
architecture?: MoltnetTargetArchitecture;
|
|
7
|
+
}) => Promise<boolean>;
|
|
@@ -7,11 +7,32 @@ import { SpawnfileError } from "../shared/index.js";
|
|
|
7
7
|
const execFile = promisify(execFileCallback);
|
|
8
8
|
const MOLTNET_CLI_ENV = "SPAWNFILE_MOLTNET_CLI";
|
|
9
9
|
const MOLTNET_RELEASE_DIR_ENV = "SPAWNFILE_MOLTNET_RELEASE_DIR";
|
|
10
|
+
const MOLTNET_TARGET_ARCH_ENV = "SPAWNFILE_MOLTNET_TARGET_ARCH";
|
|
10
11
|
const LOCAL_MOLTNET_CLI_PATH = path.resolve(process.cwd(), "moltnet", "bin", "moltnet");
|
|
11
12
|
const MOLTNET_TARGET_OS = "linux";
|
|
12
13
|
export const MOLTNET_BIN_DIRECTORY = "moltnet-bin";
|
|
13
14
|
export const MOLTNET_BINARY_NAMES = ["moltnet"];
|
|
14
|
-
const
|
|
15
|
+
const normalizeTargetArchitecture = (architecture) => {
|
|
16
|
+
switch (architecture) {
|
|
17
|
+
case "amd64":
|
|
18
|
+
case "x86_64":
|
|
19
|
+
case "x64":
|
|
20
|
+
return "amd64";
|
|
21
|
+
case "aarch64":
|
|
22
|
+
case "arm64":
|
|
23
|
+
return "arm64";
|
|
24
|
+
default:
|
|
25
|
+
throw new SpawnfileError("compile_error", `Moltnet container installs do not support target architecture ${architecture}`);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const resolveTargetArchitecture = (architecture) => {
|
|
29
|
+
if (architecture) {
|
|
30
|
+
return architecture;
|
|
31
|
+
}
|
|
32
|
+
const configuredArchitecture = process.env[MOLTNET_TARGET_ARCH_ENV]?.trim();
|
|
33
|
+
if (configuredArchitecture) {
|
|
34
|
+
return normalizeTargetArchitecture(configuredArchitecture);
|
|
35
|
+
}
|
|
15
36
|
switch (process.arch) {
|
|
16
37
|
case "arm64":
|
|
17
38
|
return "arm64";
|
|
@@ -73,12 +94,12 @@ export const resolveMoltnetCliCommand = async () => {
|
|
|
73
94
|
}
|
|
74
95
|
return validateMoltnetCli("moltnet", "PATH");
|
|
75
96
|
};
|
|
76
|
-
export const stageMoltnetBinaries = async (outputDirectory) => {
|
|
97
|
+
export const stageMoltnetBinaries = async (outputDirectory, options = {}) => {
|
|
77
98
|
const releaseDirectory = await resolveConfiguredReleaseDirectory();
|
|
78
99
|
if (!releaseDirectory) {
|
|
79
100
|
return false;
|
|
80
101
|
}
|
|
81
|
-
const architecture = resolveTargetArchitecture();
|
|
102
|
+
const architecture = resolveTargetArchitecture(options.architecture);
|
|
82
103
|
const releaseAssetPath = path.join(releaseDirectory, createReleaseAssetName(architecture));
|
|
83
104
|
if (!(await fileExists(releaseAssetPath))) {
|
|
84
105
|
throw new SpawnfileError("compile_error", `Moltnet release asset ${releaseAssetPath} does not exist. Build it with \`cd moltnet && make release-assets\` or set ${MOLTNET_RELEASE_DIR_ENV}.`);
|
|
@@ -63,6 +63,17 @@ const createAttachmentConfig = (node, artifacts, attachment, agentSlug) => {
|
|
|
63
63
|
};
|
|
64
64
|
};
|
|
65
65
|
export const resolveMoltnetWorkspaceLayout = (runtimeName, _agentName) => {
|
|
66
|
+
if (runtimeName === "daimon" || runtimeName === "pi") {
|
|
67
|
+
return {
|
|
68
|
+
clientConfigPath: `workspace/.moltnet/config.json`,
|
|
69
|
+
cliRuntime: "codex",
|
|
70
|
+
skillPaths: [
|
|
71
|
+
`workspace/.agents/skills/${GENERATED_SKILL_NAME}/SKILL.md`,
|
|
72
|
+
`workspace/.codex/skills/${GENERATED_SKILL_NAME}/SKILL.md`
|
|
73
|
+
],
|
|
74
|
+
workspaceRootPath: "workspace"
|
|
75
|
+
};
|
|
76
|
+
}
|
|
66
77
|
if (runtimeName === "openclaw" || runtimeName === "picoclaw") {
|
|
67
78
|
return {
|
|
68
79
|
clientConfigPath: `workspace/${GENERATED_CONFIG_PATH}`,
|
|
@@ -51,6 +51,17 @@ export const resolveRuntimeConfig = (plan, agentNode, nodeSlug, _networkId, _age
|
|
|
51
51
|
kind: "picoclaw"
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
case "daimon":
|
|
55
|
+
case "pi": {
|
|
56
|
+
const port = getRuntimeAdapter(agentNode.runtime.name).container.port;
|
|
57
|
+
if (!port) {
|
|
58
|
+
throw new SpawnfileError("compile_error", `Unable to resolve ${agentNode.runtime.name} control port for Moltnet agent ${agentNode.name}`);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
control_url: `http://127.0.0.1:${port}/agents/${nodeSlug}/wake`,
|
|
62
|
+
kind: "pi"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
54
65
|
default:
|
|
55
66
|
throw new SpawnfileError("compile_error", `Moltnet does not know how to attach runtime ${agentNode.runtime.name} directly`);
|
|
56
67
|
}
|
|
@@ -6,7 +6,7 @@ import { ensureDirectory, fileExists, removeDirectory, writeUtf8File } from "../
|
|
|
6
6
|
import { appendDockerLabelArgs, createDockerDeploymentLabels, createDockerDeploymentUnitId, createDockerProjectLabel, dockerContextNameForTarget, dockerHostValueForTarget, normalizeDeploymentName, readDeploymentRecordFromOutput, resolveDeploymentRecordPath, verifyDockerDeploymentTarget, writeDockerDeploymentRecordForRun } from "../deployment/index.js";
|
|
7
7
|
import { DEFAULT_OUTPUT_DIRECTORY, SpawnfileError } from "../shared/index.js";
|
|
8
8
|
import { compileProject } from "./compileProject.js";
|
|
9
|
-
import { createDefaultImageTag } from "./buildProject.js";
|
|
9
|
+
import { createDefaultImageTag, resolveDockerBuildArchitecture } from "./buildProject.js";
|
|
10
10
|
import { slugify } from "./helpers.js";
|
|
11
11
|
import { runDockerContainer } from "./runProjectDocker.js";
|
|
12
12
|
import { assertDeclaredModelAuthSatisfied, assertRunEnvironmentSatisfied, prepareRuntimeAuthMounts, readRunEnvFile, renderDockerEnvFile, resolveAuthMountArgs, resolveRunEnvironment } from "./runProjectAuth.js";
|
|
@@ -142,8 +142,14 @@ export const runProject = async (inputPath, options = {}) => {
|
|
|
142
142
|
imageTag: options.imageTag,
|
|
143
143
|
targetExecFile: options.targetExecFile
|
|
144
144
|
});
|
|
145
|
+
const targetArchitecture = options.containerArchitecture ??
|
|
146
|
+
await resolveDockerBuildArchitecture({
|
|
147
|
+
dockerCommand: options.dockerCommand,
|
|
148
|
+
dockerContext: resolvedOptions.dockerContext
|
|
149
|
+
});
|
|
145
150
|
const compileResult = await compileProject(inputPath, {
|
|
146
151
|
clean: options.clean,
|
|
152
|
+
containerArchitecture: targetArchitecture,
|
|
147
153
|
outputDirectory: options.outputDirectory
|
|
148
154
|
});
|
|
149
155
|
const imageTag = resolvedOptions.imageTag ?? createDefaultImageTag(resolveImageTagRoot(inputPath));
|
|
@@ -1,7 +1,137 @@
|
|
|
1
1
|
import { execFile as execFileCallback, spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import { promisify } from "node:util";
|
|
3
4
|
import { SpawnfileError } from "../shared/index.js";
|
|
4
5
|
const execFile = promisify(execFileCallback);
|
|
6
|
+
const parseDockerContextHost = (stdout) => {
|
|
7
|
+
const trimmed = stdout.trim();
|
|
8
|
+
if (!trimmed) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(trimmed);
|
|
13
|
+
return typeof parsed === "string" ? parsed : null;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return trimmed;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const resolveSshDockerContext = async (invocation) => {
|
|
20
|
+
if (!invocation.dockerContext) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const { stdout } = await execFile(invocation.command, [
|
|
24
|
+
"context",
|
|
25
|
+
"inspect",
|
|
26
|
+
invocation.dockerContext,
|
|
27
|
+
"--format",
|
|
28
|
+
"{{json .Endpoints.docker.Host}}"
|
|
29
|
+
], { cwd: invocation.cwd, timeout: 10_000 });
|
|
30
|
+
const host = parseDockerContextHost(stdout);
|
|
31
|
+
if (!host?.startsWith("ssh://")) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const url = new URL(host);
|
|
35
|
+
const user = decodeURIComponent(url.username);
|
|
36
|
+
const targetHost = url.hostname;
|
|
37
|
+
return {
|
|
38
|
+
host,
|
|
39
|
+
...(url.port ? { port: url.port } : {}),
|
|
40
|
+
target: user ? `${user}@${targetHost}` : targetHost
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const sshArgs = (context, command) => [
|
|
44
|
+
...(context.port ? ["-p", context.port] : []),
|
|
45
|
+
context.target,
|
|
46
|
+
command
|
|
47
|
+
];
|
|
48
|
+
const scpArgs = (context, source, target) => [
|
|
49
|
+
...(context.port ? ["-P", context.port] : []),
|
|
50
|
+
"-r",
|
|
51
|
+
source,
|
|
52
|
+
`${context.target}:${target}`
|
|
53
|
+
];
|
|
54
|
+
const shellQuote = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
55
|
+
const createRemoteDirectory = async (context) => {
|
|
56
|
+
const { stdout } = await execFile("ssh", sshArgs(context, "mktemp -d /tmp/spawnfile-run-XXXXXX"), {
|
|
57
|
+
timeout: 10_000
|
|
58
|
+
});
|
|
59
|
+
return stdout.trim();
|
|
60
|
+
};
|
|
61
|
+
const copyPathToRemote = async (context, sourcePath, remotePath) => {
|
|
62
|
+
await execFile("scp", scpArgs(context, sourcePath, remotePath), { timeout: 120_000 });
|
|
63
|
+
};
|
|
64
|
+
const removeRemoteDirectory = async (context, remoteDirectory) => {
|
|
65
|
+
await execFile("ssh", sshArgs(context, `rm -rf ${shellQuote(remoteDirectory)}`), {
|
|
66
|
+
timeout: 10_000
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
const splitVolumeSpec = (spec) => {
|
|
70
|
+
const delimiter = spec.indexOf(":");
|
|
71
|
+
if (delimiter <= 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
rest: spec.slice(delimiter),
|
|
76
|
+
source: spec.slice(0, delimiter)
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
const collectBindMountSources = (args) => {
|
|
80
|
+
const sources = new Set();
|
|
81
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
82
|
+
if (args[index] !== "-v" && args[index] !== "--volume") {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const volume = args[index + 1];
|
|
86
|
+
if (!volume) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const parsed = splitVolumeSpec(volume);
|
|
90
|
+
if (parsed && path.isAbsolute(parsed.source)) {
|
|
91
|
+
sources.add(parsed.source);
|
|
92
|
+
}
|
|
93
|
+
index += 1;
|
|
94
|
+
}
|
|
95
|
+
return [...sources].sort();
|
|
96
|
+
};
|
|
97
|
+
const rewriteBindMountSources = (args, replacements) => args.map((arg, index) => {
|
|
98
|
+
const previous = args[index - 1];
|
|
99
|
+
if (previous !== "-v" && previous !== "--volume") {
|
|
100
|
+
return arg;
|
|
101
|
+
}
|
|
102
|
+
const parsed = splitVolumeSpec(arg);
|
|
103
|
+
if (!parsed) {
|
|
104
|
+
return arg;
|
|
105
|
+
}
|
|
106
|
+
const replacement = replacements.get(parsed.source);
|
|
107
|
+
return replacement ? `${replacement}${parsed.rest}` : arg;
|
|
108
|
+
});
|
|
109
|
+
const prepareRemoteBindMounts = async (invocation) => {
|
|
110
|
+
const context = await resolveSshDockerContext(invocation);
|
|
111
|
+
if (!context) {
|
|
112
|
+
return { cleanup: async () => undefined, invocation };
|
|
113
|
+
}
|
|
114
|
+
const sources = collectBindMountSources(invocation.args);
|
|
115
|
+
if (sources.length === 0) {
|
|
116
|
+
return { cleanup: async () => undefined, invocation };
|
|
117
|
+
}
|
|
118
|
+
const remoteDirectory = await createRemoteDirectory(context);
|
|
119
|
+
const replacements = new Map();
|
|
120
|
+
for (const [index, source] of sources.entries()) {
|
|
121
|
+
const remotePath = `${remoteDirectory}/${index}-${path.basename(source) || "mount"}`;
|
|
122
|
+
await copyPathToRemote(context, source, remotePath);
|
|
123
|
+
replacements.set(source, remotePath);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
cleanup: invocation.detach
|
|
127
|
+
? async () => undefined
|
|
128
|
+
: async () => removeRemoteDirectory(context, remoteDirectory),
|
|
129
|
+
invocation: {
|
|
130
|
+
...invocation,
|
|
131
|
+
args: rewriteBindMountSources(invocation.args, replacements)
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
};
|
|
5
135
|
const inspectImageArgs = (invocation, containerId) => {
|
|
6
136
|
const base = invocation.dockerContext
|
|
7
137
|
? ["--context", invocation.dockerContext, "inspect"]
|
|
@@ -23,35 +153,50 @@ const inspectDetachedImageId = async (invocation, containerId) => {
|
|
|
23
153
|
throw new SpawnfileError("runtime_error", `Unable to inspect detached container ${containerId} image id: ${message}`);
|
|
24
154
|
}
|
|
25
155
|
};
|
|
26
|
-
|
|
156
|
+
const runPreparedDockerContainer = (prepared) => new Promise((resolve, reject) => {
|
|
27
157
|
let stdout = "";
|
|
28
|
-
const child = spawn(invocation.command, invocation.args, {
|
|
29
|
-
cwd: invocation.cwd,
|
|
30
|
-
stdio: invocation.detach ? ["ignore", "pipe", "inherit"] : "inherit"
|
|
158
|
+
const child = spawn(prepared.invocation.command, prepared.invocation.args, {
|
|
159
|
+
cwd: prepared.invocation.cwd,
|
|
160
|
+
stdio: prepared.invocation.detach ? ["ignore", "pipe", "inherit"] : "inherit"
|
|
31
161
|
});
|
|
32
|
-
|
|
162
|
+
const settle = (finish) => {
|
|
163
|
+
prepared.cleanup().then(finish, reject);
|
|
164
|
+
};
|
|
165
|
+
const rejectAfterCleanup = (error) => {
|
|
166
|
+
prepared.cleanup().then(() => reject(error), reject);
|
|
167
|
+
};
|
|
168
|
+
if (prepared.invocation.detach && child.stdout) {
|
|
33
169
|
child.stdout.setEncoding("utf8");
|
|
34
170
|
child.stdout.on("data", (chunk) => {
|
|
35
171
|
stdout += chunk;
|
|
36
172
|
});
|
|
37
173
|
}
|
|
38
174
|
child.once("error", (error) => {
|
|
39
|
-
|
|
175
|
+
rejectAfterCleanup(new SpawnfileError("runtime_error", `Unable to start docker run for ${prepared.invocation.imageTag}: ${error.message}`));
|
|
40
176
|
});
|
|
41
177
|
child.once("exit", (code, signal) => {
|
|
42
178
|
if (code === 0) {
|
|
43
179
|
const containerId = stdout.trim().split(/\s+/)[0];
|
|
44
|
-
if (!invocation.detach || !containerId) {
|
|
45
|
-
resolve(undefined);
|
|
180
|
+
if (!prepared.invocation.detach || !containerId) {
|
|
181
|
+
settle(() => resolve(undefined));
|
|
46
182
|
return;
|
|
47
183
|
}
|
|
48
|
-
inspectDetachedImageId(invocation, containerId)
|
|
184
|
+
inspectDetachedImageId(prepared.invocation, containerId)
|
|
49
185
|
.then((imageId) => resolve({ containerId, ...(imageId ? { imageId } : {}) }))
|
|
50
186
|
.catch(reject);
|
|
51
187
|
return;
|
|
52
188
|
}
|
|
53
|
-
|
|
54
|
-
? `Docker run for ${invocation.imageTag} exited from signal ${signal}`
|
|
55
|
-
: `Docker run for ${invocation.imageTag} failed with exit code ${code ?? "unknown"}`));
|
|
189
|
+
rejectAfterCleanup(new SpawnfileError("runtime_error", signal
|
|
190
|
+
? `Docker run for ${prepared.invocation.imageTag} exited from signal ${signal}`
|
|
191
|
+
: `Docker run for ${prepared.invocation.imageTag} failed with exit code ${code ?? "unknown"}`));
|
|
56
192
|
});
|
|
57
193
|
});
|
|
194
|
+
export const runDockerContainer = (invocation) => {
|
|
195
|
+
if (!invocation.dockerContext || collectBindMountSources(invocation.args).length === 0) {
|
|
196
|
+
return runPreparedDockerContainer({
|
|
197
|
+
cleanup: async () => undefined,
|
|
198
|
+
invocation
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return prepareRemoteBindMounts(invocation).then(runPreparedDockerContainer);
|
|
202
|
+
};
|
|
@@ -21,6 +21,7 @@ export const upProject = async (inputPath, options = {}) => {
|
|
|
21
21
|
const buildResult = await buildProject(inputPath, {
|
|
22
22
|
buildRunner: options.buildRunner,
|
|
23
23
|
clean: options.clean,
|
|
24
|
+
dockerContext: resolvedOptions.dockerContext,
|
|
24
25
|
dockerCommand: options.dockerCommand,
|
|
25
26
|
imageTag: resolvedOptions.imageTag,
|
|
26
27
|
outputDirectory: options.outputDirectory
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DeploymentRecord } from "./record.js";
|
|
2
|
+
import { type DockerTargetExecFile } from "./target.js";
|
|
3
|
+
export interface RecoverDockerDeploymentRecordsOptions {
|
|
4
|
+
contains?: DeploymentRecord["units"][number]["contains"];
|
|
5
|
+
context: string;
|
|
6
|
+
dockerCommand?: string;
|
|
7
|
+
execFile?: DockerTargetExecFile;
|
|
8
|
+
outputDirectory: string;
|
|
9
|
+
projectLabel: string;
|
|
10
|
+
runtimeInstanceIds?: string[];
|
|
11
|
+
sourceRoot: string;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare const recoverDockerDeploymentRecords: (options: RecoverDockerDeploymentRecordsOptions) => Promise<Array<{
|
|
15
|
+
path: string;
|
|
16
|
+
record: DeploymentRecord;
|
|
17
|
+
}>>;
|