@deksden-com/dd-flow-cli 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -4
- package/README.md +2 -2
- package/dist/build-info.json +6 -6
- package/dist/cli/help.js +16 -4
- package/dist/cli/run-cli.js +42 -17
- package/dist/domain/flow-contract.js +82 -3
- package/dist/domain/session-coverage.js +88 -0
- package/dist/domain/validation.js +56 -28
- package/dist/protocol/local-files.js +1 -16
- package/dist/schemas/code-stage-report.schema.json +2 -2
- package/dist/schemas/flow-contract.schema.json +152 -94
- package/dist/schemas/flow-run.schema.json +129 -23
- package/dist/schemas/mb-upgrade-review-data.schema.json +2 -2
- package/dist/schemas/memorybank-permissions-preflight.schema.json +13 -73
- package/dist/schemas/merge-stage-report.schema.json +2 -2
- package/dist/schemas/plan-stage-report.schema.json +38 -335
- package/dist/schemas/project-flow-pack-manifest.schema.json +4 -4
- package/dist/schemas/protocol-plan.schema.json +197 -0
- package/dist/schemas/release-impact.schema.json +9 -5
- package/dist/schemas/session-usage.schema.json +16 -0
- package/dist/schemas/stage-finish-input.schema.json +20 -0
- package/dist/schemas/stage-prompt.schema.json +35 -0
- package/dist/schemas/stage-report.schema.json +20 -0
- package/dist/schemas/stage-start-response.schema.json +30 -0
- package/dist/schemas/timeline-event.schema.json +29 -0
- package/dist/schemas/worktrunk-workspace.schema.json +19 -0
- package/dist/services/branch-context.js +9 -4
- package/dist/services/cleanup.js +77 -0
- package/dist/services/dashboard.js +51 -26
- package/dist/services/engines.js +84 -18
- package/dist/services/hooks.js +101 -250
- package/dist/services/memory-permissions.js +77 -69
- package/dist/services/migrations.js +1 -1
- package/dist/services/plan-runtime.js +124 -0
- package/dist/services/plans.js +22 -84
- package/dist/services/projects.js +2 -1
- package/dist/services/prompts.js +26 -21
- package/dist/services/protocols.js +29 -25
- package/dist/services/run-projection.js +93 -13
- package/dist/services/runs.js +128 -61
- package/dist/services/schema-validation.js +168 -7
- package/dist/services/sessions.js +97 -73
- package/dist/services/stage-lifecycle.js +737 -0
- package/dist/services/tooling.js +285 -0
- package/dist/services/usage.js +183 -30
- package/dist/services/version-status.js +1 -1
- package/dist/services/worktrees.js +88 -39
- package/dist/storage/database.js +72 -30
- package/dist/storage/paths.js +0 -9
- package/package.json +14 -13
- package/tools/worktrunk-manifest.json +34 -0
- package/dist/schemas/flow-run-index-v3.schema.json +0 -203
- package/dist/schemas/flow-run-index.schema.json +0 -175
|
@@ -12,6 +12,7 @@ import { protocolRunDiagnostics, protocolSetBoardForProtocol, protocolSetBoardsF
|
|
|
12
12
|
import { activeFlowSessionsForProject } from "./sessions.js";
|
|
13
13
|
import { usageForRun } from "./usage.js";
|
|
14
14
|
import { normalizeProtocolLifecycle } from "./protocol-lifecycle.js";
|
|
15
|
+
import { planSummary, planWithProgress } from "./plan-runtime.js";
|
|
15
16
|
export function getCmuxStatus(context, input) {
|
|
16
17
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
17
18
|
return { ok: true, project_root: project.root, cmux: detectCmux(context) };
|
|
@@ -626,14 +627,20 @@ function buildProtocolDashboardData(context, project, protocolId, htmlPath) {
|
|
|
626
627
|
const runs = context.db.all(`SELECT id, short_id, flow_kind, subject_type, subject_id, status, verdict, next_action, run_index_path, index_json, created_at, updated_at, completed_at
|
|
627
628
|
FROM flow_runs WHERE project_id = ? AND subject_type = 'protocol' AND subject_id = ?
|
|
628
629
|
ORDER BY updated_at DESC, id DESC`, [project.id, protocolId]);
|
|
629
|
-
const
|
|
630
|
+
const warnings = [...runDiagnostics.diagnostics];
|
|
631
|
+
const canonicalRuns = runs.filter((run) => {
|
|
632
|
+
if (isCanonicalRun(run))
|
|
633
|
+
return true;
|
|
634
|
+
warnings.push({ code: "legacy_run_ignored", run_id: run.id });
|
|
635
|
+
return false;
|
|
636
|
+
});
|
|
637
|
+
const primaryRun = lifecycle.terminal ? canonicalRuns[0] : canonicalRuns.find((run) => run.status === "running") ?? canonicalRuns[0];
|
|
630
638
|
const observability = primaryRun
|
|
631
639
|
? usageForRun(context, { projectId: project.id, runId: primaryRun.id, groupBy: "role" })
|
|
632
640
|
: { status: "not_observable", reason: "no_protocol_run" };
|
|
633
|
-
const warnings = [...runDiagnostics.diagnostics];
|
|
634
641
|
const protocolSetBoard = protocolSetBoardForProtocol(context, project.id, project.root, protocolId);
|
|
635
642
|
const reviewRuns = recentReviewRunsForProtocol(context, project.id, protocolId, 8);
|
|
636
|
-
const runHistory =
|
|
643
|
+
const runHistory = canonicalRuns.map((run) => {
|
|
637
644
|
const index = safeRunIndex(run.index_json, warnings, run.id);
|
|
638
645
|
return {
|
|
639
646
|
id: run.id,
|
|
@@ -707,10 +714,10 @@ function buildProtocolDashboardData(context, project, protocolId, htmlPath) {
|
|
|
707
714
|
run_history: runHistory,
|
|
708
715
|
stage_pipeline: latestStages(runHistory),
|
|
709
716
|
metrics: [
|
|
710
|
-
metric("runs",
|
|
717
|
+
metric("runs", canonicalRuns.length, "known", "flow_runs"),
|
|
711
718
|
metric("open_defs", jsonArray(protocol.active_def_json).length, "known", "protocols.active_def_json"),
|
|
712
719
|
metric("blockers", jsonArray(protocol.blockers_json).length, "known", "protocols.blockers_json"),
|
|
713
|
-
metric("completed_runs",
|
|
720
|
+
metric("completed_runs", canonicalRuns.filter((run) => run.status === "done").length, "known", "flow_runs"),
|
|
714
721
|
metric("review_runs", reviewRuns.length, "known", "flow_runs.flow_kind=mb-sdlc-review|review")
|
|
715
722
|
],
|
|
716
723
|
warnings
|
|
@@ -881,34 +888,39 @@ function protocolsForProject(context, projectId) {
|
|
|
881
888
|
FROM protocols WHERE project_id = ? ORDER BY updated_at DESC`, [projectId]);
|
|
882
889
|
}
|
|
883
890
|
function planSummaryForProtocol(context, projectId, protocolId) {
|
|
884
|
-
const
|
|
885
|
-
if (!
|
|
891
|
+
const protocol = context.db.get("SELECT plan_path FROM protocols WHERE project_id = ? AND id = ?", [projectId, protocolId]);
|
|
892
|
+
if (!protocol) {
|
|
886
893
|
return { plan_id: "none", total: 0, done: 0, blocked: 0 };
|
|
887
894
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
};
|
|
895
|
+
try {
|
|
896
|
+
const summary = planSummary(context, { projectId, protocolId, planPath: protocol.plan_path });
|
|
897
|
+
return { plan_id: summary.plan_id, total: summary.total, done: summary.done, blocked: summary.blocked };
|
|
898
|
+
}
|
|
899
|
+
catch {
|
|
900
|
+
return { plan_id: "stale", total: 0, done: 0, blocked: 0 };
|
|
901
|
+
}
|
|
896
902
|
}
|
|
897
903
|
function activePlanItems(context, projectId, protocols) {
|
|
898
904
|
const items = [];
|
|
899
905
|
for (const protocol of protocols) {
|
|
900
|
-
const
|
|
901
|
-
if (!
|
|
906
|
+
const record = context.db.get("SELECT plan_path FROM protocols WHERE project_id = ? AND id = ?", [projectId, protocol.id]);
|
|
907
|
+
if (!record) {
|
|
902
908
|
continue;
|
|
903
909
|
}
|
|
904
|
-
|
|
905
|
-
|
|
910
|
+
let plan;
|
|
911
|
+
try {
|
|
912
|
+
plan = planWithProgress(context, { projectId, protocolId: protocol.id, planPath: record.plan_path }).plan;
|
|
913
|
+
}
|
|
914
|
+
catch {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
for (const item of plan.items) {
|
|
906
918
|
if (["in_progress", "blocked"].includes(item.status ?? "")) {
|
|
907
919
|
items.push({
|
|
908
920
|
protocol: protocol.id,
|
|
909
|
-
id: item.id
|
|
910
|
-
status: item.status
|
|
911
|
-
summary: item.block_reason ?? item.summary ?? item.title
|
|
921
|
+
id: item.id,
|
|
922
|
+
status: item.status,
|
|
923
|
+
summary: item.block_reason ?? item.summary ?? item.title
|
|
912
924
|
});
|
|
913
925
|
}
|
|
914
926
|
}
|
|
@@ -1040,9 +1052,10 @@ function buildProtocolCard(context, project, protocol, generatePage) {
|
|
|
1040
1052
|
const summary = planSummaryForProtocol(context, project.id, protocol.id);
|
|
1041
1053
|
const activeDefCount = jsonArray(protocol.active_def_json).length;
|
|
1042
1054
|
const blockerCount = jsonArray(protocol.blockers_json).length;
|
|
1043
|
-
const
|
|
1055
|
+
const runs = context.db.all(`SELECT id, short_id, flow_kind, subject_type, subject_id, status, verdict, next_action, run_index_path, index_json, created_at, updated_at, completed_at
|
|
1044
1056
|
FROM flow_runs WHERE project_id = ? AND subject_type = 'protocol' AND subject_id = ?
|
|
1045
|
-
ORDER BY updated_at DESC, id DESC
|
|
1057
|
+
ORDER BY updated_at DESC, id DESC`, [project.id, protocol.id]);
|
|
1058
|
+
const latestRun = runs.find((run) => isCanonicalRun(run));
|
|
1046
1059
|
let diagnostics = [];
|
|
1047
1060
|
let lifecycle = normalizeProtocolLifecycle({ rawStage: protocol.stage, rawStatus: protocol.status });
|
|
1048
1061
|
try {
|
|
@@ -1242,10 +1255,19 @@ function safeRunIndex(text, warnings, runId) {
|
|
|
1242
1255
|
return null;
|
|
1243
1256
|
}
|
|
1244
1257
|
}
|
|
1258
|
+
function isCanonicalRun(run) {
|
|
1259
|
+
try {
|
|
1260
|
+
const parsed = JSON.parse(run.index_json);
|
|
1261
|
+
return parsed?.schema_id === "dd-flow/flow-run@2";
|
|
1262
|
+
}
|
|
1263
|
+
catch {
|
|
1264
|
+
return false;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1245
1267
|
function stageLink(projectRoot, run, stage) {
|
|
1246
1268
|
const item = stage && typeof stage === "object" && !Array.isArray(stage) ? stage : {};
|
|
1247
1269
|
const report = typeof item.stage_report === "string" ? item.stage_report : undefined;
|
|
1248
|
-
const href = report ?
|
|
1270
|
+
const href = report ? runArtifactPath(run.run_index_path, report) : "";
|
|
1249
1271
|
return {
|
|
1250
1272
|
order: typeof item.order === "number" ? item.order : 0,
|
|
1251
1273
|
stage: String(item.stage ?? "unknown"),
|
|
@@ -1253,10 +1275,13 @@ function stageLink(projectRoot, run, stage) {
|
|
|
1253
1275
|
status: String(item.status ?? "unknown"),
|
|
1254
1276
|
display_status: normalizeDisplayStatus(String(item.status ?? "unknown")),
|
|
1255
1277
|
report: report ? link("Stage report", href, fs.existsSync(href) ? "available" : "missing", fs.existsSync(href) ? undefined : "stage_report_missing") : link("Stage report", "", "pending", "stage_report_not_created_yet"),
|
|
1256
|
-
data: typeof item.data === "string" ?
|
|
1278
|
+
data: typeof item.data === "string" ? runArtifactPath(run.run_index_path, item.data) : null,
|
|
1257
1279
|
run_index: path.relative(projectRoot, run.run_index_path)
|
|
1258
1280
|
};
|
|
1259
1281
|
}
|
|
1282
|
+
function runArtifactPath(runIndexPath, artifact) {
|
|
1283
|
+
return path.isAbsolute(artifact) ? artifact : path.resolve(path.dirname(runIndexPath), artifact);
|
|
1284
|
+
}
|
|
1260
1285
|
function latestStages(runHistory) {
|
|
1261
1286
|
const byStage = new Map();
|
|
1262
1287
|
for (const run of runHistory) {
|
package/dist/services/engines.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
5
6
|
import { createRequire } from "node:module";
|
|
6
7
|
import { getCliBuildInfo } from "./build-info.js";
|
|
7
8
|
import { AppError } from "../shared/errors.js";
|
|
@@ -34,12 +35,12 @@ export function installCurrentEngine(context, input = {}) {
|
|
|
34
35
|
const target = engineVersionRoot(context.ddFlowHome, build.package_name, build.version);
|
|
35
36
|
const manifestPath = path.join(target, "engine.json");
|
|
36
37
|
const existing = readManifest(manifestPath);
|
|
37
|
-
if (existing && !input.force && manifestHealthy(existing)) {
|
|
38
|
+
if (existing && !input.force && (input.forRouting || manifestHealthy(existing))) {
|
|
38
39
|
return {
|
|
39
40
|
ok: true,
|
|
40
41
|
action: "engine_install",
|
|
41
42
|
changed: false,
|
|
42
|
-
engine: manifestSummary(existing),
|
|
43
|
+
...(input.forRouting ? {} : { engine: manifestSummary(existing) }),
|
|
43
44
|
path: target
|
|
44
45
|
};
|
|
45
46
|
}
|
|
@@ -47,12 +48,12 @@ export function installCurrentEngine(context, input = {}) {
|
|
|
47
48
|
acquireInstallLock(lockDir);
|
|
48
49
|
try {
|
|
49
50
|
const afterLock = readManifest(manifestPath);
|
|
50
|
-
if (afterLock && !input.force && manifestHealthy(afterLock)) {
|
|
51
|
+
if (afterLock && !input.force && (input.forRouting || manifestHealthy(afterLock))) {
|
|
51
52
|
return {
|
|
52
53
|
ok: true,
|
|
53
54
|
action: "engine_install",
|
|
54
55
|
changed: false,
|
|
55
|
-
engine: manifestSummary(afterLock),
|
|
56
|
+
...(input.forRouting ? {} : { engine: manifestSummary(afterLock) }),
|
|
56
57
|
path: target
|
|
57
58
|
};
|
|
58
59
|
}
|
|
@@ -64,14 +65,21 @@ export function installCurrentEngine(context, input = {}) {
|
|
|
64
65
|
copyPackageSnapshot(packageRoot, tmp);
|
|
65
66
|
const manifest = buildManifest(context, build.package_name, build.version, packageRoot, target, tmp);
|
|
66
67
|
fs.writeFileSync(path.join(tmp, "engine.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
const stagedManifest = requireManifest(path.join(tmp, "engine.json"));
|
|
69
|
+
const stagedDiagnostics = manifestDiagnostics({ ...stagedManifest, package_root: tmp, snapshot_root: tmp }, { checkEntrypoint: !input.forRouting });
|
|
70
|
+
if (stagedDiagnostics.length > 0) {
|
|
71
|
+
throw new AppError("engine_install_failed", "Staged dd-flow engine failed its integrity or dependency health check", 1, {
|
|
72
|
+
diagnostics: stagedDiagnostics,
|
|
73
|
+
path: tmp
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
activateEngineAtomically(tmp, target);
|
|
69
77
|
const installed = requireManifest(path.join(target, "engine.json"));
|
|
70
78
|
return {
|
|
71
79
|
ok: true,
|
|
72
80
|
action: "engine_install",
|
|
73
81
|
changed: true,
|
|
74
|
-
engine: manifestSummary(installed),
|
|
82
|
+
...(input.forRouting ? {} : { engine: manifestSummary(installed) }),
|
|
75
83
|
path: target
|
|
76
84
|
};
|
|
77
85
|
}
|
|
@@ -121,8 +129,8 @@ export function doctorEngines(context, input = {}) {
|
|
|
121
129
|
export function routeArgsThroughEngine(context, args, io, stdin, env, resolvedProjectRoot) {
|
|
122
130
|
if (isEngineMode(env) || isRouterNativeCommand(args))
|
|
123
131
|
return Promise.resolve(null);
|
|
124
|
-
installCurrentEngine(context);
|
|
125
|
-
const selection = selectEngine(context, { projectRoot: resolvedProjectRoot ?? resolveOperationProjectRoot(context, args) ?? undefined });
|
|
132
|
+
installCurrentEngine(context, { forRouting: true });
|
|
133
|
+
const selection = selectEngine(context, { projectRoot: resolvedProjectRoot ?? resolveOperationProjectRoot(context, args) ?? undefined }, { allowCurrentInProcess: true });
|
|
126
134
|
if (selection.status === "missing") {
|
|
127
135
|
const classification = classifyCliOperation(args, env);
|
|
128
136
|
if (classification.mode === "read_only_diagnostics" || classification.mode === "mb_upgrade") {
|
|
@@ -148,7 +156,7 @@ export function engineRoutingMetadata(env) {
|
|
|
148
156
|
routed_from: env.DD_FLOW_ROUTED_FROM ?? null
|
|
149
157
|
};
|
|
150
158
|
}
|
|
151
|
-
export function selectEngine(context, input) {
|
|
159
|
+
export function selectEngine(context, input, options = {}) {
|
|
152
160
|
const compatibility = readCompatibilityForProject(input.projectRoot);
|
|
153
161
|
const build = getCliBuildInfo();
|
|
154
162
|
const packageName = stringValue(compatibility?.engine?.package_name) ?? build.package_name;
|
|
@@ -158,7 +166,11 @@ export function selectEngine(context, input) {
|
|
|
158
166
|
const manifests = readInstalledManifests(engineStoreRoot(context.ddFlowHome))
|
|
159
167
|
.filter((manifest) => manifest.package_name === packageName)
|
|
160
168
|
.filter((manifest) => satisfiesRange(manifest.package_version, requiredRange))
|
|
161
|
-
.filter((manifest) =>
|
|
169
|
+
.filter((manifest) => options.allowCurrentInProcess
|
|
170
|
+
&& manifest.package_name === build.package_name
|
|
171
|
+
&& manifest.package_version === build.version
|
|
172
|
+
? true
|
|
173
|
+
: manifestHealthy(manifest))
|
|
162
174
|
.sort((a, b) => compareSemverStrings(b.package_version, a.package_version));
|
|
163
175
|
const selected = manifests[0] ?? null;
|
|
164
176
|
return {
|
|
@@ -275,13 +287,13 @@ function buildManifest(context, packageName, version, packageRoot, snapshotRoot,
|
|
|
275
287
|
},
|
|
276
288
|
integrity: {
|
|
277
289
|
source: "package_snapshot",
|
|
278
|
-
checksum:
|
|
279
|
-
mode: "
|
|
290
|
+
checksum: engineSnapshotChecksum(checksumRoot),
|
|
291
|
+
mode: "full_content"
|
|
280
292
|
}
|
|
281
293
|
};
|
|
282
294
|
}
|
|
283
295
|
function copyPackageSnapshot(packageRoot, target) {
|
|
284
|
-
for (const item of ["dist", "package.json", "README.md", "CHANGELOG.md"]) {
|
|
296
|
+
for (const item of ["dist", "package.json", "README.md", "CHANGELOG.md", "tools"]) {
|
|
285
297
|
const from = path.join(packageRoot, item);
|
|
286
298
|
if (!fs.existsSync(from))
|
|
287
299
|
continue;
|
|
@@ -441,7 +453,7 @@ function readManifest(file) {
|
|
|
441
453
|
function manifestHealthy(manifest) {
|
|
442
454
|
return manifestDiagnostics(manifest).length === 0;
|
|
443
455
|
}
|
|
444
|
-
function manifestDiagnostics(manifest) {
|
|
456
|
+
function manifestDiagnostics(manifest, options = {}) {
|
|
445
457
|
const diagnostics = [];
|
|
446
458
|
if (manifest.schema_id !== engineManifestSchemaId)
|
|
447
459
|
diagnostics.push("schema_id_mismatch");
|
|
@@ -455,6 +467,34 @@ function manifestDiagnostics(manifest) {
|
|
|
455
467
|
diagnostics.push("entrypoint_outside_snapshot");
|
|
456
468
|
else if (!fs.existsSync(entrypoint))
|
|
457
469
|
diagnostics.push("entrypoint_missing");
|
|
470
|
+
if (!manifest.integrity || manifest.integrity.mode !== "full_content" || !manifest.integrity.checksum) {
|
|
471
|
+
diagnostics.push("content_checksum_missing");
|
|
472
|
+
}
|
|
473
|
+
else if (fs.existsSync(snapshotRoot)) {
|
|
474
|
+
try {
|
|
475
|
+
if (engineSnapshotChecksum(snapshotRoot) !== manifest.integrity.checksum)
|
|
476
|
+
diagnostics.push("content_checksum_mismatch");
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
diagnostics.push("snapshot_unreadable");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (options.checkEntrypoint !== false && entrypoint && fs.existsSync(entrypoint) && !diagnostics.includes("entrypoint_outside_snapshot")) {
|
|
483
|
+
const health = spawnSync(process.execPath, [entrypoint, "--version", "--json"], {
|
|
484
|
+
cwd: snapshotRoot,
|
|
485
|
+
env: {
|
|
486
|
+
...process.env,
|
|
487
|
+
DD_FLOW_ENGINE_MODE: "1",
|
|
488
|
+
DD_FLOW_ENGINE_HOME: snapshotRoot,
|
|
489
|
+
DD_FLOW_ENGINE_HEALTHCHECK: "1"
|
|
490
|
+
},
|
|
491
|
+
encoding: "utf8",
|
|
492
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
493
|
+
timeout: 15_000
|
|
494
|
+
});
|
|
495
|
+
if (health.status !== 0)
|
|
496
|
+
diagnostics.push("entrypoint_dependency_load_failed");
|
|
497
|
+
}
|
|
458
498
|
return diagnostics;
|
|
459
499
|
}
|
|
460
500
|
function manifestSummary(manifest) {
|
|
@@ -592,10 +632,17 @@ function findPackageRoot(start) {
|
|
|
592
632
|
current = parent;
|
|
593
633
|
}
|
|
594
634
|
}
|
|
595
|
-
function
|
|
635
|
+
export function engineSnapshotChecksum(root) {
|
|
596
636
|
const files = [];
|
|
597
637
|
collectFiles(root, root, files);
|
|
598
|
-
|
|
638
|
+
const checksum = crypto.createHash("sha256");
|
|
639
|
+
for (const file of files.sort()) {
|
|
640
|
+
checksum.update(file);
|
|
641
|
+
checksum.update("\0");
|
|
642
|
+
checksum.update(fs.readFileSync(path.join(root, file)));
|
|
643
|
+
checksum.update("\0");
|
|
644
|
+
}
|
|
645
|
+
return checksum.digest("hex");
|
|
599
646
|
}
|
|
600
647
|
function collectFiles(root, current, files) {
|
|
601
648
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
@@ -608,6 +655,25 @@ function collectFiles(root, current, files) {
|
|
|
608
655
|
files.push(path.relative(root, full));
|
|
609
656
|
}
|
|
610
657
|
}
|
|
658
|
+
function activateEngineAtomically(staging, target) {
|
|
659
|
+
const previous = `${target}.previous-${process.pid}-${Date.now()}`;
|
|
660
|
+
const hadPrevious = fs.existsSync(target);
|
|
661
|
+
if (hadPrevious)
|
|
662
|
+
fs.renameSync(target, previous);
|
|
663
|
+
try {
|
|
664
|
+
fs.renameSync(staging, target);
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
if (hadPrevious && !fs.existsSync(target) && fs.existsSync(previous))
|
|
668
|
+
fs.renameSync(previous, target);
|
|
669
|
+
throw new AppError("engine_install_failed", "Atomic engine activation failed; previous engine was preserved", 1, {
|
|
670
|
+
target,
|
|
671
|
+
cause: String(error)
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
if (hadPrevious)
|
|
675
|
+
fs.rmSync(previous, { recursive: true, force: true });
|
|
676
|
+
}
|
|
611
677
|
function stringValue(value) {
|
|
612
678
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
613
679
|
}
|