spawnfile 0.1.10 → 0.1.12
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 +13 -2
- package/dist/cli/lifecycleCommands.js +101 -1
- package/dist/cli/modelCommands.js +3 -0
- package/dist/cli/resolveCommandInput.d.ts +19 -0
- package/dist/cli/resolveCommandInput.js +25 -0
- package/dist/cli/runCli.d.ts +4 -1
- package/dist/cli/runCli.js +37 -13
- package/dist/cli/runtimeCommands.js +1 -0
- package/dist/cli/statusCommand.d.ts +42 -0
- package/dist/cli/statusCommand.js +394 -0
- package/dist/cli/surfaceCommands.js +4 -0
- package/dist/compiler/agentSurfaces.js +2 -4
- package/dist/compiler/buildCompilePlanTeams.js +3 -1
- package/dist/compiler/compileProject.js +19 -10
- package/dist/compiler/compileProjectSupport.d.ts +1 -0
- package/dist/compiler/compileProjectSupport.js +1 -1
- package/dist/compiler/containerArtifacts.d.ts +1 -0
- package/dist/compiler/containerArtifacts.js +208 -32
- package/dist/compiler/containerArtifactsPlans.js +7 -4
- package/dist/compiler/containerArtifactsRender.d.ts +7 -1
- package/dist/compiler/containerArtifactsRender.js +6 -0
- package/dist/compiler/containerArtifactsTypes.d.ts +9 -0
- package/dist/compiler/containerEntrypointRender.js +18 -31
- package/dist/compiler/index.d.ts +1 -0
- package/dist/compiler/index.js +1 -0
- package/dist/compiler/moltnetArtifacts.d.ts +2 -0
- package/dist/compiler/moltnetArtifacts.js +58 -99
- package/dist/compiler/moltnetClientConfig.d.ts +2 -2
- package/dist/compiler/moltnetClientConfig.js +16 -23
- package/dist/compiler/moltnetConfigLowering.d.ts +3 -0
- package/dist/compiler/moltnetConfigLowering.js +13 -2
- package/dist/compiler/moltnetNodeConfig.d.ts +27 -0
- package/dist/compiler/moltnetNodeConfig.js +68 -0
- package/dist/compiler/moltnetResolution.js +1 -1
- package/dist/compiler/moltnetRoomMemberships.js +1 -2
- package/dist/compiler/moltnetRoomPolicyCompatibility.d.ts +4 -0
- package/dist/compiler/moltnetRoomPolicyCompatibility.js +28 -0
- package/dist/compiler/moltnetRuntimeConfig.d.ts +1 -1
- package/dist/compiler/moltnetRuntimeConfig.js +1 -13
- package/dist/compiler/networkBinding.d.ts +14 -0
- package/dist/compiler/networkBinding.js +34 -0
- package/dist/compiler/publishProject.d.ts +13 -0
- package/dist/compiler/publishProject.js +72 -0
- package/dist/compiler/runProject.d.ts +26 -12
- package/dist/compiler/runProject.js +102 -125
- package/dist/compiler/runProjectAuth.d.ts +5 -0
- package/dist/compiler/runProjectAuth.js +98 -0
- package/dist/compiler/runProjectDocker.d.ts +19 -0
- package/dist/compiler/runProjectDocker.js +57 -0
- package/dist/compiler/types.d.ts +3 -2
- package/dist/compiler/upProject.d.ts +6 -0
- package/dist/compiler/upProject.js +42 -8
- package/dist/compiler/view/buildOrganizationView.js +27 -3
- package/dist/compiler/view/declarations.d.ts +4 -0
- package/dist/compiler/view/declarations.js +104 -0
- package/dist/compiler/view/renderNetworks.js +12 -3
- package/dist/compiler/view/renderTree.js +16 -3
- package/dist/compiler/view/types.d.ts +77 -1
- package/dist/deployment/dockerInspect.d.ts +29 -0
- package/dist/deployment/dockerInspect.js +130 -0
- package/dist/deployment/dockerLabels.d.ts +16 -0
- package/dist/deployment/dockerLabels.js +32 -0
- package/dist/deployment/dockerLogs.d.ts +24 -0
- package/dist/deployment/dockerLogs.js +96 -0
- package/dist/deployment/dockerManager.d.ts +59 -0
- package/dist/deployment/dockerManager.js +76 -0
- package/dist/deployment/dockerProbeGateway.d.ts +16 -0
- package/dist/deployment/dockerProbeGateway.js +48 -0
- package/dist/deployment/homeStore.d.ts +19 -0
- package/dist/deployment/homeStore.js +130 -0
- package/dist/deployment/index.d.ts +9 -0
- package/dist/deployment/index.js +9 -0
- package/dist/deployment/names.d.ts +3 -0
- package/dist/deployment/names.js +12 -0
- package/dist/deployment/record.d.ts +76 -0
- package/dist/deployment/record.js +174 -0
- package/dist/deployment/target.d.ts +35 -0
- package/dist/deployment/target.js +96 -0
- package/dist/distribution/buildDistributionReport.d.ts +22 -0
- package/dist/distribution/buildDistributionReport.js +51 -0
- package/dist/distribution/consumeImage.d.ts +29 -0
- package/dist/distribution/consumeImage.js +254 -0
- package/dist/distribution/consumeImageSupport.d.ts +18 -0
- package/dist/distribution/consumeImageSupport.js +63 -0
- package/dist/distribution/distributionReportSchema.d.ts +84 -0
- package/dist/distribution/distributionReportSchema.js +107 -0
- package/dist/distribution/dockerRunner.d.ts +5 -0
- package/dist/distribution/dockerRunner.js +21 -0
- package/dist/distribution/extractImage.d.ts +25 -0
- package/dist/distribution/extractImage.js +96 -0
- package/dist/distribution/fingerprint.d.ts +2 -0
- package/dist/distribution/fingerprint.js +14 -0
- package/dist/distribution/imageRef.d.ts +9 -0
- package/dist/distribution/imageRef.js +65 -0
- package/dist/distribution/imageRuntimeAuth.d.ts +19 -0
- package/dist/distribution/imageRuntimeAuth.js +71 -0
- package/dist/distribution/index.d.ts +24 -0
- package/dist/distribution/index.js +16 -0
- package/dist/distribution/preflight.d.ts +11 -0
- package/dist/distribution/preflight.js +65 -0
- package/dist/distribution/projectImageView.d.ts +8 -0
- package/dist/distribution/projectImageView.js +83 -0
- package/dist/distribution/projectName.d.ts +1 -0
- package/dist/distribution/projectName.js +12 -0
- package/dist/distribution/renderImageInterface.d.ts +30 -0
- package/dist/distribution/renderImageInterface.js +86 -0
- package/dist/distribution/tarReader.d.ts +10 -0
- package/dist/distribution/tarReader.js +69 -0
- package/dist/distribution/types.d.ts +80 -0
- package/dist/distribution/types.js +4 -0
- package/dist/distribution/verifyDistributionReport.d.ts +12 -0
- package/dist/distribution/verifyDistributionReport.js +29 -0
- package/dist/filesystem/paths.d.ts +6 -0
- package/dist/filesystem/paths.js +11 -0
- package/dist/manifest/loadManifest.js +22 -2
- package/dist/manifest/renderSpawnfile.js +2 -4
- package/dist/manifest/renderSpawnfileNetworks.js +5 -0
- package/dist/manifest/schemas.d.ts +71 -41
- package/dist/manifest/surfaceSchemas.d.ts +19 -47
- package/dist/manifest/surfaceSchemas.js +3 -6
- package/dist/manifest/teamNetworkAccessSchemas.d.ts +15 -0
- package/dist/manifest/teamNetworkAccessSchemas.js +8 -0
- package/dist/manifest/teamNetworkConsoleSchemas.d.ts +7 -0
- package/dist/manifest/teamNetworkConsoleSchemas.js +19 -0
- package/dist/manifest/teamNetworkSchemas.d.ts +60 -0
- package/dist/manifest/teamNetworkSchemas.js +20 -7
- package/dist/report/createReport.d.ts +7 -1
- package/dist/report/createReport.js +28 -1
- package/dist/report/types.d.ts +43 -0
- package/dist/runtime/container.js +0 -16
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/install.d.ts +0 -10
- package/dist/runtime/install.js +0 -45
- package/dist/runtime/openclaw/adapter.js +9 -1
- package/dist/runtime/openclaw/configAuth.d.ts +15 -0
- package/dist/runtime/openclaw/configAuth.js +46 -0
- package/dist/runtime/openclaw/runAuth.d.ts +6 -0
- package/dist/runtime/openclaw/runAuth.js +7 -55
- package/dist/runtime/openclaw/statusProbes.d.ts +2 -0
- package/dist/runtime/openclaw/statusProbes.js +31 -0
- package/dist/runtime/picoclaw/adapter.js +10 -0
- package/dist/runtime/picoclaw/runAuth.d.ts +7 -0
- package/dist/runtime/picoclaw/runAuth.js +8 -68
- package/dist/runtime/picoclaw/statusProbes.d.ts +2 -0
- package/dist/runtime/picoclaw/statusProbes.js +100 -0
- package/dist/runtime/registry.d.ts +0 -5
- package/dist/runtime/registry.js +2 -12
- package/dist/runtime/statusProbes.d.ts +16 -0
- package/dist/runtime/statusProbes.js +41 -0
- package/dist/runtime/types.d.ts +52 -1
- package/dist/shared/errors.d.ts +6 -0
- package/dist/shared/errors.js +9 -0
- package/dist/status/buildStatus.d.ts +13 -0
- package/dist/status/buildStatus.js +266 -0
- package/dist/status/compileReport.d.ts +96 -0
- package/dist/status/compileReport.js +230 -0
- package/dist/status/compiledContainerObservations.d.ts +5 -0
- package/dist/status/compiledContainerObservations.js +63 -0
- package/dist/status/deploymentLogs.d.ts +12 -0
- package/dist/status/deploymentLogs.js +45 -0
- package/dist/status/deployments.d.ts +14 -0
- package/dist/status/deployments.js +128 -0
- package/dist/status/imageStatusReport.d.ts +10 -0
- package/dist/status/imageStatusReport.js +73 -0
- package/dist/status/index.d.ts +14 -0
- package/dist/status/index.js +14 -0
- package/dist/status/moltnetProbes.d.ts +21 -0
- package/dist/status/moltnetProbes.js +240 -0
- package/dist/status/registryDrift.d.ts +28 -0
- package/dist/status/registryDrift.js +119 -0
- package/dist/status/renderStatus.d.ts +2 -0
- package/dist/status/renderStatus.js +230 -0
- package/dist/status/runtimeProbes.d.ts +13 -0
- package/dist/status/runtimeProbes.js +111 -0
- package/dist/status/selectionSubjects.d.ts +8 -0
- package/dist/status/selectionSubjects.js +77 -0
- package/dist/status/selectors.d.ts +14 -0
- package/dist/status/selectors.js +90 -0
- package/dist/status/traversal.d.ts +6 -0
- package/dist/status/traversal.js +15 -0
- package/dist/status/types.d.ts +113 -0
- package/dist/status/types.js +1 -0
- package/package.json +4 -2
- package/runtimes.yaml +22 -15
- package/dist/runtime/tinyclaw/adapter.d.ts +0 -2
- package/dist/runtime/tinyclaw/adapter.js +0 -284
- package/dist/runtime/tinyclaw/runAuth.d.ts +0 -2
- package/dist/runtime/tinyclaw/runAuth.js +0 -49
- package/dist/runtime/tinyclaw/scaffold-assets/AGENTS.md +0 -160
- package/dist/runtime/tinyclaw/scaffold-assets/CLAUDE.md +0 -5
- package/dist/runtime/tinyclaw/scaffold-assets/SOUL.md +0 -177
- package/dist/runtime/tinyclaw/scaffold.d.ts +0 -2
- package/dist/runtime/tinyclaw/scaffold.js +0 -24
- package/dist/runtime/tinyclaw/schedules.d.ts +0 -9
- package/dist/runtime/tinyclaw/schedules.js +0 -62
- package/dist/runtime/tinyclaw/surfaces.d.ts +0 -8
- package/dist/runtime/tinyclaw/surfaces.js +0 -94
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { dockerContextNameForTarget, verifyDockerDeploymentTarget } from "./target.js";
|
|
4
|
+
const execFile = promisify(execFileCallback);
|
|
5
|
+
const toStringOrNull = (value) => typeof value === "string" && value.length > 0 ? value : null;
|
|
6
|
+
const toNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
7
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
const targetRefForUnit = (unit) => unit.container_id ?? unit.container_name;
|
|
9
|
+
const argsForInspect = (record, targetRef) => {
|
|
10
|
+
const context = dockerContextNameForTarget(record.target);
|
|
11
|
+
if (context) {
|
|
12
|
+
return ["--context", context, "inspect", targetRef];
|
|
13
|
+
}
|
|
14
|
+
if (record.target.kind === "host") {
|
|
15
|
+
return ["--host", record.target.value, "inspect", targetRef];
|
|
16
|
+
}
|
|
17
|
+
return ["inspect", targetRef];
|
|
18
|
+
};
|
|
19
|
+
const matchesDockerId = (recorded, live) => !recorded
|
|
20
|
+
|| !live
|
|
21
|
+
|| recorded === live
|
|
22
|
+
|| live.startsWith(recorded)
|
|
23
|
+
|| recorded.startsWith(live);
|
|
24
|
+
const createDriftMessages = (unit, inspected) => {
|
|
25
|
+
const containerId = toStringOrNull(inspected.Id);
|
|
26
|
+
const imageId = toStringOrNull(inspected.Image);
|
|
27
|
+
const drift = [];
|
|
28
|
+
if (!matchesDockerId(unit.container_id, containerId)) {
|
|
29
|
+
drift.push(`container id drift: recorded ${unit.container_id}, live ${containerId}`);
|
|
30
|
+
}
|
|
31
|
+
if (!matchesDockerId(unit.image_id, imageId)) {
|
|
32
|
+
drift.push(`image id drift: recorded ${unit.image_id}, live ${imageId}`);
|
|
33
|
+
}
|
|
34
|
+
return { containerId, drift, imageId };
|
|
35
|
+
};
|
|
36
|
+
const inspectionFromState = (unitId, unit, inspected, state) => {
|
|
37
|
+
const running = typeof state.Running === "boolean" ? state.Running : null;
|
|
38
|
+
const status = toStringOrNull(state.Status);
|
|
39
|
+
const { containerId, drift, imageId } = createDriftMessages(unit, inspected);
|
|
40
|
+
const baseMessage = running === true
|
|
41
|
+
? `container is running (${status ?? "running"})`
|
|
42
|
+
: `container is not running (${status ?? "unknown"})`;
|
|
43
|
+
return {
|
|
44
|
+
containerId,
|
|
45
|
+
drift,
|
|
46
|
+
exists: true,
|
|
47
|
+
exitCode: toNumberOrNull(state.ExitCode),
|
|
48
|
+
finishedAt: toStringOrNull(state.FinishedAt),
|
|
49
|
+
imageId,
|
|
50
|
+
message: drift.length > 0 ? `${baseMessage}; ${drift.join("; ")}` : baseMessage,
|
|
51
|
+
restartCount: toNumberOrNull(state.RestartCount),
|
|
52
|
+
running,
|
|
53
|
+
severity: drift.length > 0 ? "warn" : running === false ? "error" : "ok",
|
|
54
|
+
startedAt: toStringOrNull(state.StartedAt),
|
|
55
|
+
status,
|
|
56
|
+
unitId
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
const missingInspection = (unitId, message) => ({
|
|
60
|
+
containerId: null,
|
|
61
|
+
drift: [],
|
|
62
|
+
exists: false,
|
|
63
|
+
exitCode: null,
|
|
64
|
+
finishedAt: null,
|
|
65
|
+
imageId: null,
|
|
66
|
+
message,
|
|
67
|
+
restartCount: null,
|
|
68
|
+
running: false,
|
|
69
|
+
severity: "warn",
|
|
70
|
+
startedAt: null,
|
|
71
|
+
status: null,
|
|
72
|
+
unitId
|
|
73
|
+
});
|
|
74
|
+
const unknownInspection = (unitId, message, severity = "unknown") => ({
|
|
75
|
+
containerId: null,
|
|
76
|
+
drift: [],
|
|
77
|
+
exists: null,
|
|
78
|
+
exitCode: null,
|
|
79
|
+
finishedAt: null,
|
|
80
|
+
imageId: null,
|
|
81
|
+
message,
|
|
82
|
+
restartCount: null,
|
|
83
|
+
running: null,
|
|
84
|
+
severity,
|
|
85
|
+
startedAt: null,
|
|
86
|
+
status: null,
|
|
87
|
+
unitId
|
|
88
|
+
});
|
|
89
|
+
const inspectUnit = async (record, unit, options) => {
|
|
90
|
+
const targetRef = targetRefForUnit(unit);
|
|
91
|
+
if (!targetRef) {
|
|
92
|
+
return unknownInspection(unit.id, "deployment unit has no recorded container id or name");
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const { stdout } = await options.execFile(options.dockerCommand, argsForInspect(record, targetRef), { timeout: options.timeoutMs });
|
|
96
|
+
const parsed = JSON.parse(stdout);
|
|
97
|
+
const [first] = Array.isArray(parsed) ? parsed : [];
|
|
98
|
+
const state = isRecord(first) && isRecord(first.State) ? first.State : null;
|
|
99
|
+
return state
|
|
100
|
+
? inspectionFromState(unit.id, unit, first, state)
|
|
101
|
+
: unknownInspection(unit.id, `docker inspect returned no state for ${targetRef}`);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
const stderr = isRecord(error) ? toStringOrNull(error.stderr) : null;
|
|
105
|
+
const message = stderr ?? (error instanceof Error ? error.message : String(error));
|
|
106
|
+
if (/No such object/i.test(message)) {
|
|
107
|
+
return missingInspection(unit.id, `recorded container ${targetRef} is missing`);
|
|
108
|
+
}
|
|
109
|
+
return unknownInspection(unit.id, `unable to inspect container ${targetRef}: ${message}`, "error");
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
export const inspectDockerDeployment = async (record, options = {}) => {
|
|
113
|
+
const resolvedOptions = {
|
|
114
|
+
dockerCommand: options.dockerCommand ?? "docker",
|
|
115
|
+
execFile: options.execFile ?? execFile,
|
|
116
|
+
timeoutMs: options.timeoutMs ?? 10_000
|
|
117
|
+
};
|
|
118
|
+
try {
|
|
119
|
+
await verifyDockerDeploymentTarget(record.target, resolvedOptions);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
+
return new Map(record.units.map((unit) => [
|
|
124
|
+
unit.id,
|
|
125
|
+
unknownInspection(unit.id, `unable to verify recorded Docker target: ${message}`, "error")
|
|
126
|
+
]));
|
|
127
|
+
}
|
|
128
|
+
const inspections = await Promise.all(record.units.map(async (unit) => inspectUnit(record, unit, resolvedOptions)));
|
|
129
|
+
return new Map(inspections.map((inspection) => [inspection.unitId, inspection]));
|
|
130
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const dockerDeploymentLabelKeys: {
|
|
2
|
+
readonly compileFingerprint: "com.spawnfile.compile_fingerprint";
|
|
3
|
+
readonly deployment: "com.spawnfile.deployment";
|
|
4
|
+
readonly project: "com.spawnfile.project";
|
|
5
|
+
readonly unit: "com.spawnfile.unit";
|
|
6
|
+
readonly version: "com.spawnfile.version";
|
|
7
|
+
};
|
|
8
|
+
export interface DockerDeploymentLabelInput {
|
|
9
|
+
compileFingerprint: string;
|
|
10
|
+
deployment: string;
|
|
11
|
+
project: string;
|
|
12
|
+
unit: string;
|
|
13
|
+
version: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const createDockerDeploymentLabels: (input: DockerDeploymentLabelInput) => Record<string, string>;
|
|
16
|
+
export declare const appendDockerLabelArgs: (args: string[], labels: Record<string, string>) => void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
2
|
+
export const dockerDeploymentLabelKeys = {
|
|
3
|
+
compileFingerprint: "com.spawnfile.compile_fingerprint",
|
|
4
|
+
deployment: "com.spawnfile.deployment",
|
|
5
|
+
project: "com.spawnfile.project",
|
|
6
|
+
unit: "com.spawnfile.unit",
|
|
7
|
+
version: "com.spawnfile.version"
|
|
8
|
+
};
|
|
9
|
+
const dockerLabelValuePattern = /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/;
|
|
10
|
+
const assertIdentifierLabelValue = (key, value) => {
|
|
11
|
+
if (!dockerLabelValuePattern.test(value)) {
|
|
12
|
+
throw new SpawnfileError("validation_error", `Docker deployment label ${key} must be an identifier`);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
export const createDockerDeploymentLabels = (input) => {
|
|
16
|
+
const labels = {
|
|
17
|
+
[dockerDeploymentLabelKeys.compileFingerprint]: input.compileFingerprint,
|
|
18
|
+
[dockerDeploymentLabelKeys.deployment]: input.deployment,
|
|
19
|
+
[dockerDeploymentLabelKeys.project]: input.project,
|
|
20
|
+
[dockerDeploymentLabelKeys.unit]: input.unit,
|
|
21
|
+
[dockerDeploymentLabelKeys.version]: input.version
|
|
22
|
+
};
|
|
23
|
+
for (const [key, value] of Object.entries(labels)) {
|
|
24
|
+
assertIdentifierLabelValue(key, value);
|
|
25
|
+
}
|
|
26
|
+
return labels;
|
|
27
|
+
};
|
|
28
|
+
export const appendDockerLabelArgs = (args, labels) => {
|
|
29
|
+
for (const [key, value] of Object.entries(labels).sort(([left], [right]) => left.localeCompare(right))) {
|
|
30
|
+
args.push("--label", `${key}=${value}`);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DeploymentRecord } from "./record.js";
|
|
2
|
+
export type DockerLogsExecFile = (file: string, args: string[], options: {
|
|
3
|
+
timeout: number;
|
|
4
|
+
}) => Promise<{
|
|
5
|
+
stderr: string;
|
|
6
|
+
stdout: string;
|
|
7
|
+
}>;
|
|
8
|
+
export interface DockerUnitLogs {
|
|
9
|
+
containerRef: string | null;
|
|
10
|
+
message: string;
|
|
11
|
+
severity: "error" | "ok" | "unknown" | "warn";
|
|
12
|
+
text: string;
|
|
13
|
+
unitId: string;
|
|
14
|
+
}
|
|
15
|
+
export type DockerLogsResult = Map<string, DockerUnitLogs>;
|
|
16
|
+
export interface DockerLogsOptions {
|
|
17
|
+
dockerCommand?: string;
|
|
18
|
+
execFile?: DockerLogsExecFile;
|
|
19
|
+
secretValues?: string[];
|
|
20
|
+
tail?: number;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare const redactDockerLogText: (text: string, secretValues?: string[]) => string;
|
|
24
|
+
export declare const readDockerDeploymentLogs: (record: DeploymentRecord, options?: DockerLogsOptions) => Promise<DockerLogsResult>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { dockerContextNameForTarget } from "./target.js";
|
|
4
|
+
const execFile = promisify(execFileCallback);
|
|
5
|
+
const targetRefForUnit = (unit) => unit.container_id ?? unit.container_name;
|
|
6
|
+
const withDockerTarget = (record, args) => {
|
|
7
|
+
const context = dockerContextNameForTarget(record.target);
|
|
8
|
+
if (context) {
|
|
9
|
+
return ["--context", context, ...args];
|
|
10
|
+
}
|
|
11
|
+
if (record.target.kind === "host") {
|
|
12
|
+
return ["--host", record.target.value, ...args];
|
|
13
|
+
}
|
|
14
|
+
return args;
|
|
15
|
+
};
|
|
16
|
+
const normalizeTail = (tail) => typeof tail === "number" && Number.isInteger(tail) && tail > 0 ? tail : 100;
|
|
17
|
+
const redactKnownSecrets = (text, secretValues) => secretValues
|
|
18
|
+
.filter((secret) => secret.length > 0)
|
|
19
|
+
.reduce((redacted, secret) => redacted.replaceAll(secret, "[REDACTED]"), text);
|
|
20
|
+
const redactTokenLikeText = (text) => {
|
|
21
|
+
let redacted = text;
|
|
22
|
+
redacted = redacted.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, "Bearer [REDACTED]");
|
|
23
|
+
redacted = redacted.replace(/\bmagt_v1_[A-Za-z0-9_-]{16,}\b/g, "[REDACTED]");
|
|
24
|
+
redacted = redacted.replace(/\b(?:sk|sk-proj)-[A-Za-z0-9_-]{20,}\b/g, "[REDACTED]");
|
|
25
|
+
redacted = redacted.replace(/\b([A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD)[A-Z0-9_]*=)([^\s"'`]+)/gi, "$1[REDACTED]");
|
|
26
|
+
redacted = redacted.replace(/("([^"]*(?:api[_-]?key|token|secret|password)[^"]*)"\s*:\s*")([^"]+)(")/gi, "$1[REDACTED]$4");
|
|
27
|
+
return redacted;
|
|
28
|
+
};
|
|
29
|
+
export const redactDockerLogText = (text, secretValues = []) => redactTokenLikeText(redactKnownSecrets(text, secretValues));
|
|
30
|
+
const combineLogStreams = (stdout, stderr) => {
|
|
31
|
+
if (!stdout) {
|
|
32
|
+
return stderr;
|
|
33
|
+
}
|
|
34
|
+
if (!stderr) {
|
|
35
|
+
return stdout;
|
|
36
|
+
}
|
|
37
|
+
return `${stdout}${stdout.endsWith("\n") ? "" : "\n"}${stderr}`;
|
|
38
|
+
};
|
|
39
|
+
const missingUnitLogs = (unitId, message) => ({
|
|
40
|
+
containerRef: null,
|
|
41
|
+
message,
|
|
42
|
+
severity: "unknown",
|
|
43
|
+
text: "",
|
|
44
|
+
unitId
|
|
45
|
+
});
|
|
46
|
+
const failedUnitLogs = (unitId, containerRef, message, severity = "error") => ({
|
|
47
|
+
containerRef,
|
|
48
|
+
message,
|
|
49
|
+
severity,
|
|
50
|
+
text: "",
|
|
51
|
+
unitId
|
|
52
|
+
});
|
|
53
|
+
const unitLogs = (unitId, containerRef, text) => ({
|
|
54
|
+
containerRef,
|
|
55
|
+
message: text.length > 0 ? "logs collected" : "logs collected; no output",
|
|
56
|
+
severity: "ok",
|
|
57
|
+
text,
|
|
58
|
+
unitId
|
|
59
|
+
});
|
|
60
|
+
const toErrorMessage = (error) => {
|
|
61
|
+
if (typeof error === "object" && error !== null && "stderr" in error) {
|
|
62
|
+
const stderr = error.stderr;
|
|
63
|
+
if (typeof stderr === "string" && stderr.length > 0) {
|
|
64
|
+
return stderr.trim();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return error instanceof Error ? error.message : String(error);
|
|
68
|
+
};
|
|
69
|
+
const readUnitLogs = async (record, unit, options) => {
|
|
70
|
+
const containerRef = targetRefForUnit(unit);
|
|
71
|
+
if (!containerRef) {
|
|
72
|
+
return missingUnitLogs(unit.id, "deployment unit has no recorded container id or name");
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const result = await options.execFile(options.dockerCommand, withDockerTarget(record, ["logs", "--tail", String(options.tail), containerRef]), { timeout: options.timeoutMs });
|
|
76
|
+
return unitLogs(unit.id, containerRef, redactDockerLogText(combineLogStreams(result.stdout, result.stderr), options.secretValues));
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const message = redactDockerLogText(toErrorMessage(error), options.secretValues);
|
|
80
|
+
if (/No such (?:object|container)/i.test(message)) {
|
|
81
|
+
return failedUnitLogs(unit.id, containerRef, `recorded container ${containerRef} is missing`, "warn");
|
|
82
|
+
}
|
|
83
|
+
return failedUnitLogs(unit.id, containerRef, `unable to collect logs for ${containerRef}: ${message}`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
export const readDockerDeploymentLogs = async (record, options = {}) => {
|
|
87
|
+
const resolvedOptions = {
|
|
88
|
+
dockerCommand: options.dockerCommand ?? "docker",
|
|
89
|
+
execFile: options.execFile ?? execFile,
|
|
90
|
+
secretValues: options.secretValues ?? [],
|
|
91
|
+
tail: normalizeTail(options.tail),
|
|
92
|
+
timeoutMs: options.timeoutMs ?? 10_000
|
|
93
|
+
};
|
|
94
|
+
const results = await Promise.all(record.units.map(async (unit) => readUnitLogs(record, unit, resolvedOptions)));
|
|
95
|
+
return new Map(results.map((result) => [result.unitId, result]));
|
|
96
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { DeploymentRecordSource } from "./record.js";
|
|
2
|
+
import { type DockerDeploymentTarget, type DockerTargetExecFile } from "./target.js";
|
|
3
|
+
import { type DeploymentRecord } from "./record.js";
|
|
4
|
+
export interface DockerDeploymentNodeInput {
|
|
5
|
+
id: string;
|
|
6
|
+
kind: "agent" | "team";
|
|
7
|
+
}
|
|
8
|
+
export interface DockerDeploymentRunMetadata {
|
|
9
|
+
containerId?: string;
|
|
10
|
+
imageId?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DockerDeploymentRunInvocationInput {
|
|
13
|
+
command: string;
|
|
14
|
+
containerName: string | null;
|
|
15
|
+
deploymentName?: string | null;
|
|
16
|
+
detach: boolean;
|
|
17
|
+
dockerContext?: string | null;
|
|
18
|
+
dockerHost?: string | null;
|
|
19
|
+
}
|
|
20
|
+
export interface DockerDeploymentReportInput {
|
|
21
|
+
compile_fingerprint?: string;
|
|
22
|
+
container?: {
|
|
23
|
+
runtime_instances: Array<{
|
|
24
|
+
id: string;
|
|
25
|
+
}>;
|
|
26
|
+
};
|
|
27
|
+
nodes: DockerDeploymentNodeInput[];
|
|
28
|
+
root: string;
|
|
29
|
+
}
|
|
30
|
+
export interface WriteDockerDeploymentRecordInput {
|
|
31
|
+
authProfileName: string | null;
|
|
32
|
+
compileFingerprint: string;
|
|
33
|
+
containerName: string | null;
|
|
34
|
+
deploymentName?: string;
|
|
35
|
+
envFilePath?: string;
|
|
36
|
+
imageTag: string;
|
|
37
|
+
nodes: DockerDeploymentNodeInput[];
|
|
38
|
+
outputDirectory: string;
|
|
39
|
+
projectRoot: string;
|
|
40
|
+
runMetadata?: DockerDeploymentRunMetadata;
|
|
41
|
+
runtimeInstanceIds: string[];
|
|
42
|
+
source?: DeploymentRecordSource;
|
|
43
|
+
target: DockerDeploymentTarget;
|
|
44
|
+
}
|
|
45
|
+
export declare const createDockerDeploymentUnitId: (deploymentName: string) => string;
|
|
46
|
+
export declare const createDockerDeploymentRecord: (input: WriteDockerDeploymentRecordInput) => DeploymentRecord;
|
|
47
|
+
export declare const createDockerProjectLabel: (projectRoot: string, projectName?: string) => string;
|
|
48
|
+
export declare const writeDockerDeploymentRecord: (input: WriteDockerDeploymentRecordInput) => Promise<string>;
|
|
49
|
+
export declare const writeDockerDeploymentRecordForRun: (input: {
|
|
50
|
+
authProfileName: string | null;
|
|
51
|
+
envFilePath?: string;
|
|
52
|
+
imageTag: string;
|
|
53
|
+
invocation: DockerDeploymentRunInvocationInput;
|
|
54
|
+
outputDirectory: string;
|
|
55
|
+
report: DockerDeploymentReportInput;
|
|
56
|
+
runMetadata?: DockerDeploymentRunMetadata;
|
|
57
|
+
targetExecFile?: DockerTargetExecFile;
|
|
58
|
+
targetTimeoutMs?: number;
|
|
59
|
+
}) => Promise<string | null>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { normalizeProjectLabelSlug } from "../distribution/index.js";
|
|
3
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
4
|
+
import { normalizeDeploymentName } from "./names.js";
|
|
5
|
+
import { resolveDockerDeploymentTarget } from "./target.js";
|
|
6
|
+
import { writeDeploymentRecord } from "./record.js";
|
|
7
|
+
const resolveProjectSlug = (projectRoot) => {
|
|
8
|
+
const base = path.basename(projectRoot).toLowerCase() === "spawnfile"
|
|
9
|
+
? path.basename(path.dirname(projectRoot))
|
|
10
|
+
: path.basename(projectRoot);
|
|
11
|
+
return base
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
14
|
+
.replace(/^-+|-+$/g, "") || "project";
|
|
15
|
+
};
|
|
16
|
+
export const createDockerDeploymentUnitId = (deploymentName) => `${deploymentName}-container`;
|
|
17
|
+
export const createDockerDeploymentRecord = (input) => {
|
|
18
|
+
const name = normalizeDeploymentName(input.deploymentName);
|
|
19
|
+
return {
|
|
20
|
+
auth_profile: input.authProfileName,
|
|
21
|
+
compile_fingerprint: input.compileFingerprint,
|
|
22
|
+
created_at: new Date().toISOString(),
|
|
23
|
+
...(input.envFilePath ? { env_file: path.resolve(input.envFilePath) } : {}),
|
|
24
|
+
manager: "docker",
|
|
25
|
+
name,
|
|
26
|
+
output_directory: path.resolve(input.outputDirectory),
|
|
27
|
+
source: input.source ?? { kind: "project", root: path.resolve(input.projectRoot) },
|
|
28
|
+
target: input.target,
|
|
29
|
+
units: [
|
|
30
|
+
{
|
|
31
|
+
container_id: input.runMetadata?.containerId ?? null,
|
|
32
|
+
container_name: input.containerName,
|
|
33
|
+
contains: input.nodes.map((node) => ({ id: node.id, kind: node.kind })),
|
|
34
|
+
id: createDockerDeploymentUnitId(name),
|
|
35
|
+
image_id: input.runMetadata?.imageId ?? null,
|
|
36
|
+
image_tag: input.imageTag,
|
|
37
|
+
kind: "container",
|
|
38
|
+
runtime_instances: [...input.runtimeInstanceIds].sort()
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
version: "spawnfile.deployment.v2"
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export const createDockerProjectLabel = (projectRoot, projectName) => projectName ? normalizeProjectLabelSlug(projectName) : resolveProjectSlug(projectRoot);
|
|
45
|
+
const requireCompileFingerprint = (fingerprint) => {
|
|
46
|
+
if (!fingerprint) {
|
|
47
|
+
throw new SpawnfileError("validation_error", "Detached deployment records require compile_fingerprint in the compile report");
|
|
48
|
+
}
|
|
49
|
+
return fingerprint;
|
|
50
|
+
};
|
|
51
|
+
export const writeDockerDeploymentRecord = async (input) => writeDeploymentRecord(input.outputDirectory, createDockerDeploymentRecord(input));
|
|
52
|
+
export const writeDockerDeploymentRecordForRun = async (input) => {
|
|
53
|
+
if (!input.invocation.detach || !input.invocation.deploymentName) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return writeDockerDeploymentRecord({
|
|
57
|
+
authProfileName: input.authProfileName,
|
|
58
|
+
compileFingerprint: requireCompileFingerprint(input.report.compile_fingerprint),
|
|
59
|
+
containerName: input.invocation.containerName,
|
|
60
|
+
deploymentName: input.invocation.deploymentName,
|
|
61
|
+
envFilePath: input.envFilePath,
|
|
62
|
+
imageTag: input.imageTag,
|
|
63
|
+
nodes: input.report.nodes,
|
|
64
|
+
outputDirectory: input.outputDirectory,
|
|
65
|
+
projectRoot: input.report.root,
|
|
66
|
+
runMetadata: input.runMetadata,
|
|
67
|
+
runtimeInstanceIds: input.report.container?.runtime_instances.map((instance) => instance.id) ?? [],
|
|
68
|
+
target: await resolveDockerDeploymentTarget({
|
|
69
|
+
context: input.invocation.dockerContext ?? undefined,
|
|
70
|
+
dockerCommand: input.invocation.command,
|
|
71
|
+
dockerHost: input.invocation.dockerHost ?? undefined,
|
|
72
|
+
execFile: input.targetExecFile,
|
|
73
|
+
timeoutMs: input.targetTimeoutMs
|
|
74
|
+
})
|
|
75
|
+
});
|
|
76
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RuntimeProbeGateway } from "../runtime/index.js";
|
|
2
|
+
import type { DeploymentRecord } from "./record.js";
|
|
3
|
+
import type { DockerUnitInspection } from "./dockerInspect.js";
|
|
4
|
+
export type DockerProbeExecFile = (file: string, args: string[], options: {
|
|
5
|
+
timeout: number;
|
|
6
|
+
}) => Promise<{
|
|
7
|
+
stderr: string;
|
|
8
|
+
stdout: string;
|
|
9
|
+
}>;
|
|
10
|
+
export interface DockerProbeGatewayOptions {
|
|
11
|
+
dockerCommand?: string;
|
|
12
|
+
execFile?: DockerProbeExecFile;
|
|
13
|
+
inspection: DockerUnitInspection;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare const createDockerProbeGateway: (record: DeploymentRecord, unit: DeploymentRecord["units"][number], options: DockerProbeGatewayOptions) => RuntimeProbeGateway;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { dockerContextNameForTarget } from "./target.js";
|
|
4
|
+
const execFile = promisify(execFileCallback);
|
|
5
|
+
const targetRefForUnit = (unit) => {
|
|
6
|
+
const targetRef = unit.container_id ?? unit.container_name;
|
|
7
|
+
if (!targetRef) {
|
|
8
|
+
throw new Error(`deployment unit ${unit.id} has no recorded container id or name`);
|
|
9
|
+
}
|
|
10
|
+
return targetRef;
|
|
11
|
+
};
|
|
12
|
+
const withDockerTarget = (record, args) => {
|
|
13
|
+
const context = dockerContextNameForTarget(record.target);
|
|
14
|
+
if (context) {
|
|
15
|
+
return ["--context", context, ...args];
|
|
16
|
+
}
|
|
17
|
+
if (record.target.kind === "host") {
|
|
18
|
+
return ["--host", record.target.value, ...args];
|
|
19
|
+
}
|
|
20
|
+
return args;
|
|
21
|
+
};
|
|
22
|
+
export const createDockerProbeGateway = (record, unit, options) => {
|
|
23
|
+
const dockerCommand = options.dockerCommand ?? "docker";
|
|
24
|
+
const runExec = options.execFile ?? execFile;
|
|
25
|
+
const timeout = options.timeoutMs ?? 10_000;
|
|
26
|
+
const exec = async (command) => {
|
|
27
|
+
const targetRef = targetRefForUnit(unit);
|
|
28
|
+
return runExec(dockerCommand, withDockerTarget(record, ["exec", targetRef, ...command]), { timeout });
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
exec,
|
|
32
|
+
async httpGet(port, requestPath, headers = {}) {
|
|
33
|
+
const url = `http://127.0.0.1:${port}${requestPath.startsWith("/") ? requestPath : `/${requestPath}`}`;
|
|
34
|
+
try {
|
|
35
|
+
const headerArgs = Object.entries(headers).flatMap(([name, value]) => ["-H", `${name}: ${value}`]);
|
|
36
|
+
const result = await exec(["curl", "-fsS", ...headerArgs, url]);
|
|
37
|
+
return { body: result.stdout, ok: true };
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
41
|
+
return { body: "", error: message, ok: false };
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
async inspectUnit() {
|
|
45
|
+
return options.inspection;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DistributionReport } from "../distribution/index.js";
|
|
2
|
+
import { type DeploymentRecord } from "./record.js";
|
|
3
|
+
export declare const resolveHomeDeploymentsDirectory: () => string;
|
|
4
|
+
export declare const resolveHomeDeploymentDirectory: (deploymentName: string) => string;
|
|
5
|
+
export declare const resolveHomeRecordPath: (deploymentName: string) => string;
|
|
6
|
+
export declare const resolveHomeReportPath: (deploymentName: string) => string;
|
|
7
|
+
export declare const acquireHomeDeploymentLock: (deploymentName: string) => Promise<() => Promise<void>>;
|
|
8
|
+
/** Writes the record and the cached distribution report together, record last. */
|
|
9
|
+
export declare const writeHomeDeployment: (record: DeploymentRecord, report: DistributionReport) => Promise<{
|
|
10
|
+
recordPath: string;
|
|
11
|
+
reportPath: string;
|
|
12
|
+
}>;
|
|
13
|
+
export declare const readHomeDeploymentRecord: (deploymentName: string) => Promise<DeploymentRecord>;
|
|
14
|
+
export declare const readHomeDeploymentReport: (deploymentName: string) => Promise<string>;
|
|
15
|
+
export declare const homeDeploymentExists: (deploymentName: string) => Promise<boolean>;
|
|
16
|
+
export declare const listHomeDeploymentRecords: () => Promise<Array<{
|
|
17
|
+
path: string;
|
|
18
|
+
record: DeploymentRecord;
|
|
19
|
+
}>>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { open, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { resolveSpawnfileHome } from "../auth/index.js";
|
|
4
|
+
import { ensureDirectory, readUtf8File } from "../filesystem/index.js";
|
|
5
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
6
|
+
import { normalizeDeploymentName } from "./names.js";
|
|
7
|
+
import { parseDeploymentRecord } from "./record.js";
|
|
8
|
+
export const resolveHomeDeploymentsDirectory = () => path.join(resolveSpawnfileHome(), "deployments");
|
|
9
|
+
export const resolveHomeDeploymentDirectory = (deploymentName) => path.join(resolveHomeDeploymentsDirectory(), normalizeDeploymentName(deploymentName));
|
|
10
|
+
export const resolveHomeRecordPath = (deploymentName) => path.join(resolveHomeDeploymentDirectory(deploymentName), "record.json");
|
|
11
|
+
export const resolveHomeReportPath = (deploymentName) => path.join(resolveHomeDeploymentDirectory(deploymentName), "spawnfile-report.json");
|
|
12
|
+
/**
|
|
13
|
+
* Acquires an exclusive lock for a home deployment so concurrent `up`
|
|
14
|
+
* invocations cannot race on the same record (and orphan each other's
|
|
15
|
+
* containers). Returns a release function. Throws if the deployment is already
|
|
16
|
+
* locked by another in-flight operation.
|
|
17
|
+
*/
|
|
18
|
+
const isProcessAlive = (pid) => {
|
|
19
|
+
try {
|
|
20
|
+
process.kill(pid, 0);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
// ESRCH means no such process (stale); EPERM means it exists but is ours to
|
|
25
|
+
// not signal — treat as alive to stay safe.
|
|
26
|
+
return error.code === "EPERM";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
export const acquireHomeDeploymentLock = async (deploymentName) => {
|
|
30
|
+
const directory = resolveHomeDeploymentDirectory(deploymentName);
|
|
31
|
+
await ensureDirectory(directory);
|
|
32
|
+
const lockPath = path.join(directory, ".lock");
|
|
33
|
+
const write = async () => {
|
|
34
|
+
const handle = await open(lockPath, "wx");
|
|
35
|
+
try {
|
|
36
|
+
await handle.write(JSON.stringify({ pid: process.pid }));
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await handle.close();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
try {
|
|
43
|
+
await write();
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error.code !== "EEXIST") {
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
// A lock exists. Reclaim it only if the owning process is gone (a crashed
|
|
50
|
+
// deploy must not lock the deployment forever); otherwise it is genuinely busy.
|
|
51
|
+
let ownerPid = null;
|
|
52
|
+
try {
|
|
53
|
+
ownerPid = JSON.parse(await readUtf8File(lockPath)).pid ?? null;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
ownerPid = null;
|
|
57
|
+
}
|
|
58
|
+
if (ownerPid !== null && isProcessAlive(ownerPid)) {
|
|
59
|
+
throw new SpawnfileError("runtime_error", `Deployment "${normalizeDeploymentName(deploymentName)}" is already being modified by another operation`);
|
|
60
|
+
}
|
|
61
|
+
await rm(lockPath, { force: true });
|
|
62
|
+
await write();
|
|
63
|
+
}
|
|
64
|
+
return async () => {
|
|
65
|
+
await rm(lockPath, { force: true });
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
const writeAtomic = async (filePath, content) => {
|
|
69
|
+
const temporaryPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
70
|
+
await writeFile(temporaryPath, content, "utf8");
|
|
71
|
+
await rename(temporaryPath, filePath);
|
|
72
|
+
};
|
|
73
|
+
/** Writes the record and the cached distribution report together, record last. */
|
|
74
|
+
export const writeHomeDeployment = async (record, report) => {
|
|
75
|
+
const parsed = parseDeploymentRecord(record);
|
|
76
|
+
const directory = resolveHomeDeploymentDirectory(parsed.name);
|
|
77
|
+
await ensureDirectory(directory);
|
|
78
|
+
const reportPath = resolveHomeReportPath(parsed.name);
|
|
79
|
+
const recordPath = resolveHomeRecordPath(parsed.name);
|
|
80
|
+
await writeAtomic(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
81
|
+
await writeAtomic(recordPath, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
82
|
+
return { recordPath, reportPath };
|
|
83
|
+
};
|
|
84
|
+
export const readHomeDeploymentRecord = async (deploymentName) => {
|
|
85
|
+
const recordPath = resolveHomeRecordPath(deploymentName);
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(await readUtf8File(recordPath));
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
92
|
+
throw new SpawnfileError("validation_error", `Unable to read home deployment record ${recordPath}: ${reason}`);
|
|
93
|
+
}
|
|
94
|
+
return parseDeploymentRecord(parsed, recordPath);
|
|
95
|
+
};
|
|
96
|
+
export const readHomeDeploymentReport = async (deploymentName) => readUtf8File(resolveHomeReportPath(deploymentName));
|
|
97
|
+
export const homeDeploymentExists = async (deploymentName) => {
|
|
98
|
+
const records = await listHomeDeploymentRecords();
|
|
99
|
+
const target = normalizeDeploymentName(deploymentName);
|
|
100
|
+
return records.some((entry) => entry.record.name === target);
|
|
101
|
+
};
|
|
102
|
+
export const listHomeDeploymentRecords = async () => {
|
|
103
|
+
const directory = resolveHomeDeploymentsDirectory();
|
|
104
|
+
let entries;
|
|
105
|
+
try {
|
|
106
|
+
entries = await readdir(directory);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error.code === "ENOENT") {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
113
|
+
throw new SpawnfileError("validation_error", `Unable to read home deployments directory ${directory}: ${reason}`);
|
|
114
|
+
}
|
|
115
|
+
const records = await Promise.all(entries
|
|
116
|
+
.sort()
|
|
117
|
+
.map(async (entry) => {
|
|
118
|
+
const recordPath = path.join(directory, entry, "record.json");
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(await readUtf8File(recordPath));
|
|
121
|
+
return { path: recordPath, record: parseDeploymentRecord(parsed, recordPath) };
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}));
|
|
127
|
+
return records
|
|
128
|
+
.filter((entry) => entry !== null)
|
|
129
|
+
.sort((left, right) => left.record.name.localeCompare(right.record.name));
|
|
130
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./dockerLabels.js";
|
|
2
|
+
export * from "./dockerInspect.js";
|
|
3
|
+
export * from "./dockerLogs.js";
|
|
4
|
+
export * from "./dockerManager.js";
|
|
5
|
+
export * from "./dockerProbeGateway.js";
|
|
6
|
+
export * from "./homeStore.js";
|
|
7
|
+
export * from "./names.js";
|
|
8
|
+
export * from "./record.js";
|
|
9
|
+
export * from "./target.js";
|