akm-cli 0.9.0-rc.0 → 0.9.0-rc.2
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 +339 -2
- package/SECURITY.md +23 -24
- package/dist/assets/help/help-improve.md +10 -10
- package/dist/assets/hints/cli-hints-full.md +44 -10
- package/dist/assets/hints/cli-hints-short.md +6 -2
- package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
- package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
- package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
- package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
- package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
- package/dist/assets/stash-skeleton/README.md +28 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
- package/dist/assets/tasks/core/backup.yml +1 -0
- package/dist/assets/tasks/core/extract.yml +1 -0
- package/dist/assets/tasks/core/improve.yml +1 -0
- package/dist/assets/tasks/core/index-refresh.yml +1 -0
- package/dist/assets/tasks/core/sync.yml +1 -0
- package/dist/assets/tasks/core/version-check.yml +1 -0
- package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
- package/dist/assets/templates/html/health.html +5 -1
- package/dist/cli/config-migrate.js +31 -138
- package/dist/cli/config-validate.js +10 -8
- package/dist/cli.js +48 -14
- package/dist/commands/agent/agent-dispatch.js +17 -16
- package/dist/commands/agent/agent-support.js +0 -24
- package/dist/commands/agent/contribute-cli.js +5 -15
- package/dist/commands/backup-cli.js +54 -0
- package/dist/commands/config-cli.js +45 -159
- package/dist/commands/env/env-binding.js +95 -0
- package/dist/commands/env/env-cli.js +8 -65
- package/dist/commands/env/secret.js +8 -5
- package/dist/commands/health/checks.js +130 -83
- package/dist/commands/health/html-report.js +4 -0
- package/dist/commands/health/improve-metrics.js +30 -32
- package/dist/commands/health/llm-usage.js +19 -19
- package/dist/commands/health/md-report.js +4 -0
- package/dist/commands/health/metrics.js +2 -1
- package/dist/commands/health/surfaces.js +5 -4
- package/dist/commands/health.js +1 -1
- package/dist/commands/improve/consolidate/chunking.js +2 -2
- package/dist/commands/improve/consolidate.js +28 -25
- package/dist/commands/improve/distill/promote-memory.js +5 -12
- package/dist/commands/improve/distill/quality-gate.js +5 -7
- package/dist/commands/improve/distill.js +16 -5
- package/dist/commands/improve/eligibility.js +22 -12
- package/dist/commands/improve/extract-cli.js +47 -19
- package/dist/commands/improve/extract.js +110 -62
- package/dist/commands/improve/improve-cli.js +38 -16
- package/dist/commands/improve/improve-result-file.js +30 -24
- package/dist/commands/improve/improve-strategies.js +137 -0
- package/dist/commands/improve/improve.js +60 -30
- package/dist/commands/improve/locks.js +66 -45
- package/dist/commands/improve/loop-stages.js +75 -33
- package/dist/commands/improve/memory/memory-belief.js +79 -7
- package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
- package/dist/commands/improve/preparation.js +71 -73
- package/dist/commands/improve/procedural.js +3 -2
- package/dist/commands/improve/recombine.js +2 -1
- package/dist/commands/improve/reflect.js +119 -214
- package/dist/commands/improve/shared.js +11 -5
- package/dist/commands/lint/base-linter.js +152 -42
- package/dist/commands/mv-cli.js +809 -0
- package/dist/commands/proposal/proposal-cli.js +18 -8
- package/dist/commands/proposal/propose.js +64 -69
- package/dist/commands/read/knowledge.js +436 -4
- package/dist/commands/read/remember-cli.js +39 -2
- package/dist/commands/read/search-cli.js +6 -1
- package/dist/commands/registry-cli.js +29 -14
- package/dist/commands/remember.js +2 -0
- package/dist/commands/sources/init.js +13 -14
- package/dist/commands/sources/migration-help.js +7 -4
- package/dist/commands/sources/schema-repair.js +2 -4
- package/dist/commands/sources/source-add.js +62 -73
- package/dist/commands/sources/source-manage.js +50 -46
- package/dist/commands/sources/stash-cli.js +41 -4
- package/dist/commands/tasks/default-tasks.js +12 -12
- package/dist/commands/tasks/tasks-cli.js +7 -3
- package/dist/commands/tasks/tasks.js +113 -18
- package/dist/commands/wiki-cli.js +9 -10
- package/dist/commands/workflow-cli.js +276 -12
- package/dist/core/asset/asset-spec.js +58 -1
- package/dist/core/asset/frontmatter.js +12 -2
- package/dist/core/common.js +5 -3
- package/dist/core/config/config-io.js +28 -17
- package/dist/core/config/config-schema.js +379 -66
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config-version.js +29 -0
- package/dist/core/config/config-walker.js +98 -27
- package/dist/core/config/config.js +132 -266
- package/dist/core/config/deep-merge.js +41 -0
- package/dist/core/config/engine-semantics.js +32 -0
- package/dist/core/errors.js +2 -2
- package/dist/core/extra-params.js +61 -0
- package/dist/core/file-lock.js +201 -56
- package/dist/core/improve-result.js +178 -0
- package/dist/core/json-schema.js +142 -0
- package/dist/core/maintenance-barrier.js +119 -0
- package/dist/core/migration-backup.js +416 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/redaction.js +358 -0
- package/dist/core/state/migrations.js +17 -2
- package/dist/core/state-db.js +44 -1
- package/dist/indexer/db/db.js +118 -2
- package/dist/indexer/graph/graph-extraction.js +28 -16
- package/dist/indexer/index-writer-lock.js +31 -24
- package/dist/indexer/index-written-assets.js +15 -6
- package/dist/indexer/indexer.js +47 -2
- package/dist/indexer/passes/memory-inference.js +10 -6
- package/dist/indexer/passes/metadata.js +250 -0
- package/dist/indexer/search/db-search.js +111 -44
- package/dist/indexer/search/fts-query.js +41 -0
- package/dist/indexer/search/ranking-contributors.js +48 -0
- package/dist/indexer/search/ranking.js +36 -23
- package/dist/indexer/search/search-fields.js +11 -1
- package/dist/indexer/walk/matchers.js +39 -0
- package/dist/integrations/agent/builder-shared.js +7 -0
- package/dist/integrations/agent/builders.js +5 -50
- package/dist/integrations/agent/config.js +3 -143
- package/dist/integrations/agent/detect.js +17 -2
- package/dist/integrations/agent/engine-resolution.js +202 -0
- package/dist/integrations/agent/index.js +1 -2
- package/dist/integrations/agent/model-aliases.js +16 -2
- package/dist/integrations/agent/profiles.js +36 -62
- package/dist/integrations/agent/runner-dispatch.js +91 -4
- package/dist/integrations/agent/runner.js +76 -207
- package/dist/integrations/agent/spawn.js +141 -20
- package/dist/integrations/harnesses/aider/agent-builder.js +112 -0
- package/dist/integrations/harnesses/aider/index.js +57 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +152 -0
- package/dist/integrations/harnesses/amazonq/index.js +58 -0
- package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
- package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
- package/dist/integrations/harnesses/claude/index.js +25 -25
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/codex/agent-builder.js +136 -0
- package/dist/integrations/harnesses/codex/index.js +62 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +121 -0
- package/dist/integrations/harnesses/copilot/index.js +59 -0
- package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
- package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
- package/dist/integrations/harnesses/gemini/index.js +59 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/index.js +27 -28
- package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
- package/dist/integrations/harnesses/opencode/index.js +15 -22
- package/dist/integrations/harnesses/opencode-sdk/harness.js +60 -0
- package/dist/integrations/harnesses/opencode-sdk/index.js +8 -32
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
- package/dist/integrations/harnesses/openhands/agent-builder.js +125 -0
- package/dist/integrations/harnesses/openhands/index.js +57 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +103 -0
- package/dist/integrations/harnesses/pi/index.js +57 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/types.js +8 -32
- package/dist/integrations/lockfile.js +32 -21
- package/dist/integrations/session-logs/index.js +24 -11
- package/dist/llm/client.js +48 -14
- package/dist/llm/feature-gate.js +15 -47
- package/dist/llm/graph-extract.js +1 -1
- package/dist/llm/index-passes.js +8 -42
- package/dist/llm/memory-infer-impl.js +1 -1
- package/dist/llm/usage-persist.js +4 -0
- package/dist/llm/usage-telemetry.js +35 -5
- package/dist/output/renderers.js +3 -2
- package/dist/output/shapes/helpers.js +2 -1
- package/dist/output/shapes/passthrough.js +6 -0
- package/dist/output/text/helpers.js +215 -2
- package/dist/output/text/workflow.js +3 -1
- package/dist/schemas/akm-config.json +16638 -0
- package/dist/schemas/akm-task.json +87 -0
- package/dist/schemas/akm-workflow.json +372 -0
- package/dist/scripts/migrate-storage.js +10944 -8801
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9247 -350
- package/dist/setup/detected-engines.js +142 -0
- package/dist/setup/engine-config.js +89 -0
- package/dist/setup/setup.js +236 -132
- package/dist/setup/steps/connection.js +61 -32
- package/dist/setup/steps/platforms.js +4 -4
- package/dist/setup/steps.js +3 -2
- package/dist/storage/database.js +13 -1
- package/dist/storage/engines/sqlite-migrations.js +1 -0
- package/dist/storage/repositories/improve-runs-repository.js +5 -5
- package/dist/storage/repositories/task-history-repository.js +78 -0
- package/dist/storage/repositories/workflow-runs-repository.js +190 -1
- package/dist/tasks/parser.js +138 -52
- package/dist/tasks/runner.js +71 -75
- package/dist/tasks/schema.js +1 -1
- package/dist/tasks/validator.js +11 -6
- package/dist/text-import-hook.mjs +1 -1
- package/dist/wiki/wiki.js +9 -8
- package/dist/workflows/authoring/authoring.js +123 -10
- package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
- package/dist/workflows/cli.js +4 -0
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/db.js +200 -13
- package/dist/workflows/exec/brief.js +478 -0
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +1034 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1355 -0
- package/dist/workflows/exec/run-workflow.js +609 -0
- package/dist/workflows/exec/scheduler.js +71 -0
- package/dist/workflows/exec/step-work.js +1212 -0
- package/dist/workflows/exec/unit-writer.js +23 -0
- package/dist/workflows/exec/watch.js +116 -0
- package/dist/workflows/exec/worktree.js +171 -0
- package/dist/workflows/ir/compile.js +375 -0
- package/dist/workflows/ir/freeze.js +243 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +68 -0
- package/dist/workflows/ir/schema.js +545 -0
- package/dist/workflows/parser.js +10 -1
- package/dist/workflows/program/expressions.js +369 -0
- package/dist/workflows/program/parser.js +869 -0
- package/dist/workflows/program/project.js +104 -0
- package/dist/workflows/program/schema.js +54 -0
- package/dist/workflows/renderer.js +82 -5
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/plan-classifier.js +187 -0
- package/dist/workflows/runtime/runs.js +246 -69
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +42 -1
- package/dist/workflows/validate-summary.js +24 -3
- package/dist/workflows/validator.js +26 -1
- package/docs/data-and-telemetry.md +4 -3
- package/docs/migration/release-notes/0.6.0.md +1 -1
- package/docs/migration/release-notes/0.7.0.md +5 -4
- package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
- package/docs/migration/v0.8-to-v0.9.md +401 -0
- package/package.json +4 -2
- package/schemas/akm-config.json +16638 -0
- package/schemas/akm-task.json +87 -0
- package/schemas/akm-workflow.json +372 -0
- package/dist/commands/improve/improve-profiles.js +0 -168
- package/dist/core/config/config-migration.js +0 -602
- package/dist/core/deep-merge.js +0 -38
- package/dist/llm/call-ai.js +0 -62
- package/dist/setup/legacy-config.js +0 -106
- package/docs/README.md +0 -104
- /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { ConfigError } from "./errors.js";
|
|
9
|
+
import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync } from "./file-lock.js";
|
|
10
|
+
import { getMaintenanceBarrierPath } from "./paths.js";
|
|
11
|
+
const heldBarrierContext = new AsyncLocalStorage();
|
|
12
|
+
/**
|
|
13
|
+
* Serialize restore with the short critical section that creates every
|
|
14
|
+
* long-lived AKM lock or workflow lease. The long operation keeps its own
|
|
15
|
+
* lock/lease; this barrier is released immediately after acquisition.
|
|
16
|
+
*/
|
|
17
|
+
export function tryAcquireMaintenanceBarrier() {
|
|
18
|
+
const lockPath = getMaintenanceBarrierPath();
|
|
19
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
20
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
21
|
+
const ownership = tryAcquireLockSync(lockPath, createLockPayload({ purpose: "maintenance-start" }));
|
|
22
|
+
if (ownership) {
|
|
23
|
+
return () => releaseLock(ownership);
|
|
24
|
+
}
|
|
25
|
+
const probe = probeLock(lockPath);
|
|
26
|
+
if (probe.state !== "stale" || !reclaimStaleLock(lockPath, probe))
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
export function acquireMaintenanceBarrier() {
|
|
32
|
+
const release = tryAcquireMaintenanceBarrier();
|
|
33
|
+
if (release)
|
|
34
|
+
return release;
|
|
35
|
+
throw new ConfigError(`AKM maintenance is in progress (barrier ${getMaintenanceBarrierPath()}); retry after it completes.`, "INVALID_CONFIG_FILE");
|
|
36
|
+
}
|
|
37
|
+
export function withMaintenanceStartBarrier(run) {
|
|
38
|
+
if (heldBarrierContext.getStore()?.active)
|
|
39
|
+
return run();
|
|
40
|
+
const release = acquireMaintenanceBarrier();
|
|
41
|
+
const ownership = { active: true };
|
|
42
|
+
try {
|
|
43
|
+
return heldBarrierContext.run(ownership, run);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
ownership.active = false;
|
|
47
|
+
release();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function acquireMaintenanceBarrierAsync() {
|
|
51
|
+
const deadline = Date.now() + 5_000;
|
|
52
|
+
while (true) {
|
|
53
|
+
const release = tryAcquireMaintenanceBarrier();
|
|
54
|
+
if (release)
|
|
55
|
+
return release;
|
|
56
|
+
if (Date.now() >= deadline)
|
|
57
|
+
return acquireMaintenanceBarrier();
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function withMaintenanceStartBarrierAsync(run) {
|
|
62
|
+
if (heldBarrierContext.getStore()?.active)
|
|
63
|
+
return run();
|
|
64
|
+
const release = await acquireMaintenanceBarrierAsync();
|
|
65
|
+
const ownership = { active: true };
|
|
66
|
+
try {
|
|
67
|
+
return await heldBarrierContext.run(ownership, run);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
ownership.active = false;
|
|
71
|
+
release();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function withMaintenanceStartBarrierSyncWait(run) {
|
|
75
|
+
if (heldBarrierContext.getStore()?.active)
|
|
76
|
+
return run();
|
|
77
|
+
const deadline = Date.now() + 5_000;
|
|
78
|
+
let release = tryAcquireMaintenanceBarrier();
|
|
79
|
+
while (!release && Date.now() < deadline) {
|
|
80
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
81
|
+
release = tryAcquireMaintenanceBarrier();
|
|
82
|
+
}
|
|
83
|
+
if (!release)
|
|
84
|
+
release = acquireMaintenanceBarrier();
|
|
85
|
+
const ownership = { active: true };
|
|
86
|
+
try {
|
|
87
|
+
return heldBarrierContext.run(ownership, run);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
ownership.active = false;
|
|
91
|
+
release();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Register a long-lived operation atomically with restore's blocker scan. */
|
|
95
|
+
export async function acquireMaintenanceActivity(name) {
|
|
96
|
+
return withMaintenanceStartBarrierAsync(async () => {
|
|
97
|
+
const directory = path.join(path.dirname(getMaintenanceBarrierPath()), "maintenance-activities");
|
|
98
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
99
|
+
const lockPath = path.join(directory, `${name}-${process.pid}-${randomUUID()}.lock`);
|
|
100
|
+
const ownership = tryAcquireLockSync(lockPath, createLockPayload({ purpose: name }));
|
|
101
|
+
if (!ownership) {
|
|
102
|
+
throw new ConfigError(`Could not register AKM maintenance activity at ${lockPath}.`, "INVALID_CONFIG_FILE");
|
|
103
|
+
}
|
|
104
|
+
return () => releaseLock(ownership);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** Synchronous activity registration for synchronous database handle lifetimes. */
|
|
108
|
+
export function acquireMaintenanceActivitySync(name) {
|
|
109
|
+
return withMaintenanceStartBarrierSyncWait(() => {
|
|
110
|
+
const directory = path.join(path.dirname(getMaintenanceBarrierPath()), "maintenance-activities");
|
|
111
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
112
|
+
const lockPath = path.join(directory, `${name}-${process.pid}-${randomUUID()}.lock`);
|
|
113
|
+
const ownership = tryAcquireLockSync(lockPath, createLockPayload({ purpose: name }));
|
|
114
|
+
if (!ownership) {
|
|
115
|
+
throw new ConfigError(`Could not register AKM maintenance activity at ${lockPath}.`, "INVALID_CONFIG_FILE");
|
|
116
|
+
}
|
|
117
|
+
return () => releaseLock(ownership);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { openDatabase } from "../storage/database.js";
|
|
8
|
+
import { writeFileAtomic } from "./common.js";
|
|
9
|
+
import { parseConfigText, withConfigLock } from "./config/config-io.js";
|
|
10
|
+
import { CURRENT_CONFIG_VERSION } from "./config/config-schema.js";
|
|
11
|
+
import { ConfigError } from "./errors.js";
|
|
12
|
+
import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync } from "./file-lock.js";
|
|
13
|
+
import { acquireMaintenanceActivitySync, withMaintenanceStartBarrier } from "./maintenance-barrier.js";
|
|
14
|
+
import { getCacheDir, getConfigPath, getDataDir, getIndexWriterLockPath, getLockfileLockPath, getStateDbPathInDataDir, getWorkflowDbPath, } from "./paths.js";
|
|
15
|
+
export const MIGRATION_BACKUP_VERSION = "0.9.0";
|
|
16
|
+
const ARTIFACT_NAMES = ["config.json", "state.db", "workflow.db"];
|
|
17
|
+
export function getMigrationBackupDir() {
|
|
18
|
+
return path.join(getCacheDir(), "migration-backups", MIGRATION_BACKUP_VERSION);
|
|
19
|
+
}
|
|
20
|
+
function migrationBackupLockPath() {
|
|
21
|
+
return path.join(getCacheDir(), "migration-backups", `${MIGRATION_BACKUP_VERSION}.lock`);
|
|
22
|
+
}
|
|
23
|
+
function expectedSourcePaths() {
|
|
24
|
+
return {
|
|
25
|
+
"config.json": getConfigPath(),
|
|
26
|
+
"state.db": getStateDbPathInDataDir(),
|
|
27
|
+
"workflow.db": getWorkflowDbPath(),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function sha256File(filePath) {
|
|
31
|
+
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
32
|
+
}
|
|
33
|
+
function ownerOnlyMode(filePath, directory) {
|
|
34
|
+
if (process.platform === "win32")
|
|
35
|
+
return true;
|
|
36
|
+
const mode = fs.statSync(filePath).mode & 0o777;
|
|
37
|
+
return mode === (directory ? 0o700 : 0o600);
|
|
38
|
+
}
|
|
39
|
+
function fsyncDirectory(directory) {
|
|
40
|
+
try {
|
|
41
|
+
const fd = fs.openSync(directory, "r");
|
|
42
|
+
try {
|
|
43
|
+
fs.fsyncSync(fd);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
fs.closeSync(fd);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const code = error.code;
|
|
51
|
+
if (code !== "EINVAL" && code !== "ENOTSUP" && code !== "EISDIR" && code !== "EPERM")
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function parseManifest(bundlePath) {
|
|
56
|
+
const manifestPath = path.join(bundlePath, "manifest.json");
|
|
57
|
+
let value;
|
|
58
|
+
try {
|
|
59
|
+
value = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw new ConfigError(`Migration backup at ${bundlePath} is incomplete or unreadable: ${error instanceof Error ? error.message : String(error)}. Remove it only after preserving any recoverable files, then retry backup creation.`, "INVALID_CONFIG_FILE");
|
|
63
|
+
}
|
|
64
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
65
|
+
throw new ConfigError(`Migration backup manifest at ${manifestPath} is invalid.`, "INVALID_CONFIG_FILE");
|
|
66
|
+
}
|
|
67
|
+
const manifest = value;
|
|
68
|
+
if (manifest.version !== MIGRATION_BACKUP_VERSION || typeof manifest.createdAt !== "string") {
|
|
69
|
+
throw new ConfigError(`Migration backup manifest at ${manifestPath} has an unsupported version.`, "INVALID_CONFIG_FILE");
|
|
70
|
+
}
|
|
71
|
+
const expected = expectedSourcePaths();
|
|
72
|
+
for (const name of ARTIFACT_NAMES) {
|
|
73
|
+
const artifact = manifest.artifacts?.[name];
|
|
74
|
+
if (!artifact ||
|
|
75
|
+
artifact.sourcePath !== expected[name] ||
|
|
76
|
+
typeof artifact.present !== "boolean" ||
|
|
77
|
+
!Number.isSafeInteger(artifact.byteSize) ||
|
|
78
|
+
artifact.byteSize < 0 ||
|
|
79
|
+
typeof artifact.createdAt !== "string" ||
|
|
80
|
+
(artifact.present
|
|
81
|
+
? typeof artifact.sha256 !== "string" || artifact.sha256.length !== 64
|
|
82
|
+
: artifact.sha256 !== null)) {
|
|
83
|
+
throw new ConfigError(`Migration backup manifest has an invalid ${name} entry.`, "INVALID_CONFIG_FILE");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return manifest;
|
|
87
|
+
}
|
|
88
|
+
/** Verify the complete bundle, including source-path binding, modes, sizes, and hashes. */
|
|
89
|
+
export function verifyMigrationBackup(bundlePath = getMigrationBackupDir()) {
|
|
90
|
+
if (!fs.existsSync(bundlePath) || !fs.statSync(bundlePath).isDirectory()) {
|
|
91
|
+
throw new ConfigError(`Migration backup does not exist at ${bundlePath}.`, "INVALID_CONFIG_FILE");
|
|
92
|
+
}
|
|
93
|
+
if (!ownerOnlyMode(bundlePath, true)) {
|
|
94
|
+
throw new ConfigError(`Migration backup directory ${bundlePath} must have mode 0700.`, "INVALID_CONFIG_FILE");
|
|
95
|
+
}
|
|
96
|
+
const manifest = parseManifest(bundlePath);
|
|
97
|
+
const expectedFiles = new Set(["manifest.json"]);
|
|
98
|
+
for (const name of ARTIFACT_NAMES) {
|
|
99
|
+
const artifact = manifest.artifacts[name];
|
|
100
|
+
const artifactPath = path.join(bundlePath, name);
|
|
101
|
+
if (!artifact.present) {
|
|
102
|
+
if (fs.existsSync(artifactPath)) {
|
|
103
|
+
throw new ConfigError(`Migration backup contains ${name}, but its manifest records it absent.`, "INVALID_CONFIG_FILE");
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
expectedFiles.add(name);
|
|
108
|
+
if (!fs.existsSync(artifactPath) || !fs.statSync(artifactPath).isFile()) {
|
|
109
|
+
throw new ConfigError(`Migration backup is missing ${artifactPath}.`, "INVALID_CONFIG_FILE");
|
|
110
|
+
}
|
|
111
|
+
if (!ownerOnlyMode(artifactPath, false)) {
|
|
112
|
+
throw new ConfigError(`Migration backup artifact ${artifactPath} must have mode 0600.`, "INVALID_CONFIG_FILE");
|
|
113
|
+
}
|
|
114
|
+
const stat = fs.statSync(artifactPath);
|
|
115
|
+
if (stat.size !== artifact.byteSize || sha256File(artifactPath) !== artifact.sha256) {
|
|
116
|
+
throw new ConfigError(`Migration backup artifact ${artifactPath} failed checksum verification.`, "INVALID_CONFIG_FILE");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!ownerOnlyMode(path.join(bundlePath, "manifest.json"), false)) {
|
|
120
|
+
throw new ConfigError(`Migration backup manifest must have mode 0600.`, "INVALID_CONFIG_FILE");
|
|
121
|
+
}
|
|
122
|
+
const extras = fs.readdirSync(bundlePath).filter((name) => !expectedFiles.has(name));
|
|
123
|
+
if (extras.length > 0) {
|
|
124
|
+
throw new ConfigError(`Migration backup contains unexpected files: ${extras.join(", ")}.`, "INVALID_CONFIG_FILE");
|
|
125
|
+
}
|
|
126
|
+
return manifest;
|
|
127
|
+
}
|
|
128
|
+
function acquireMigrationBackupLock() {
|
|
129
|
+
const lockPath = migrationBackupLockPath();
|
|
130
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
131
|
+
fs.chmodSync(path.dirname(lockPath), 0o700);
|
|
132
|
+
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
133
|
+
const ownership = tryAcquireLockSync(lockPath, createLockPayload());
|
|
134
|
+
if (ownership)
|
|
135
|
+
return () => releaseLock(ownership);
|
|
136
|
+
const probe = probeLock(lockPath);
|
|
137
|
+
if (probe.state === "stale" && reclaimStaleLock(lockPath, probe)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);
|
|
141
|
+
}
|
|
142
|
+
throw new ConfigError(`Timed out waiting for migration backup lock at ${lockPath}.`, "INVALID_CONFIG_FILE");
|
|
143
|
+
}
|
|
144
|
+
function assertNotAlreadyCutOver() {
|
|
145
|
+
const configPath = getConfigPath();
|
|
146
|
+
let text;
|
|
147
|
+
try {
|
|
148
|
+
text = fs.readFileSync(configPath, "utf8");
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (error.code === "ENOENT")
|
|
152
|
+
return;
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
const raw = parseConfigText(text, configPath);
|
|
156
|
+
if (raw.configVersion === CURRENT_CONFIG_VERSION) {
|
|
157
|
+
throw new ConfigError(`Refusing to create a pre-0.9 migration backup from an existing ${CURRENT_CONFIG_VERSION} config at ${configPath}. Restore the original migration bundle or pre-cutover config before retrying.`, "INVALID_CONFIG_FILE");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function copyRegularArtifact(source, destination) {
|
|
161
|
+
const stat = fs.statSync(source);
|
|
162
|
+
if (!stat.isFile())
|
|
163
|
+
throw new ConfigError(`Backup source is not a regular file: ${source}`, "INVALID_CONFIG_FILE");
|
|
164
|
+
writeFileAtomic(destination, fs.readFileSync(source), 0o600);
|
|
165
|
+
fs.chmodSync(destination, 0o600);
|
|
166
|
+
}
|
|
167
|
+
function sqliteQuote(value) {
|
|
168
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
169
|
+
}
|
|
170
|
+
function backupSqlite(source, destination) {
|
|
171
|
+
const stat = fs.statSync(source);
|
|
172
|
+
if (!stat.isFile())
|
|
173
|
+
throw new ConfigError(`SQLite backup source is not a regular file: ${source}`, "INVALID_CONFIG_FILE");
|
|
174
|
+
const resolvedSource = path.resolve(source);
|
|
175
|
+
const activityName = resolvedSource === path.resolve(getWorkflowDbPath())
|
|
176
|
+
? "workflow-db"
|
|
177
|
+
: resolvedSource === path.resolve(getStateDbPathInDataDir())
|
|
178
|
+
? "state-db"
|
|
179
|
+
: undefined;
|
|
180
|
+
const releaseActivity = activityName ? acquireMaintenanceActivitySync(activityName) : undefined;
|
|
181
|
+
// The source was stat-verified above. Opening without Bun's `create:false`
|
|
182
|
+
// avoids bun:sqlite's SQLITE_MISUSE for that unsupported false option while
|
|
183
|
+
// retaining Node's ordinary existing-file behavior.
|
|
184
|
+
let db;
|
|
185
|
+
try {
|
|
186
|
+
db = openDatabase(source);
|
|
187
|
+
db.exec("PRAGMA busy_timeout = 10000");
|
|
188
|
+
db.exec("PRAGMA wal_checkpoint(FULL)");
|
|
189
|
+
db.exec(`VACUUM INTO ${sqliteQuote(destination)}`);
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
try {
|
|
193
|
+
db?.close();
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
releaseActivity?.();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
fs.chmodSync(destination, 0o600);
|
|
200
|
+
const fd = fs.openSync(destination, "r");
|
|
201
|
+
try {
|
|
202
|
+
fs.fsyncSync(fd);
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
fs.closeSync(fd);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function createMigrationBackupUnlocked() {
|
|
209
|
+
const bundlePath = getMigrationBackupDir();
|
|
210
|
+
if (fs.existsSync(bundlePath)) {
|
|
211
|
+
return { path: bundlePath, created: false, manifest: verifyMigrationBackup(bundlePath) };
|
|
212
|
+
}
|
|
213
|
+
assertNotAlreadyCutOver();
|
|
214
|
+
const parent = path.dirname(bundlePath);
|
|
215
|
+
fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
216
|
+
fs.chmodSync(parent, 0o700);
|
|
217
|
+
const temporary = path.join(parent, `.0.9.0.tmp.${process.pid}.${crypto.randomBytes(8).toString("hex")}`);
|
|
218
|
+
fs.mkdirSync(temporary, { mode: 0o700 });
|
|
219
|
+
const createdAt = new Date().toISOString();
|
|
220
|
+
const sources = expectedSourcePaths();
|
|
221
|
+
const artifacts = {};
|
|
222
|
+
try {
|
|
223
|
+
for (const name of ARTIFACT_NAMES) {
|
|
224
|
+
const sourcePath = sources[name];
|
|
225
|
+
const destination = path.join(temporary, name);
|
|
226
|
+
let present = true;
|
|
227
|
+
try {
|
|
228
|
+
fs.statSync(sourcePath);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (error.code === "ENOENT")
|
|
232
|
+
present = false;
|
|
233
|
+
else
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
if (present) {
|
|
237
|
+
if (name === "config.json")
|
|
238
|
+
copyRegularArtifact(sourcePath, destination);
|
|
239
|
+
else
|
|
240
|
+
backupSqlite(sourcePath, destination);
|
|
241
|
+
}
|
|
242
|
+
const byteSize = present ? fs.statSync(destination).size : 0;
|
|
243
|
+
artifacts[name] = {
|
|
244
|
+
sourcePath,
|
|
245
|
+
present,
|
|
246
|
+
byteSize,
|
|
247
|
+
sha256: present ? sha256File(destination) : null,
|
|
248
|
+
createdAt,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const manifest = { version: MIGRATION_BACKUP_VERSION, createdAt, artifacts };
|
|
252
|
+
writeFileAtomic(path.join(temporary, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, 0o600);
|
|
253
|
+
fs.chmodSync(path.join(temporary, "manifest.json"), 0o600);
|
|
254
|
+
fsyncDirectory(temporary);
|
|
255
|
+
fs.renameSync(temporary, bundlePath);
|
|
256
|
+
fsyncDirectory(parent);
|
|
257
|
+
return { path: bundlePath, created: true, manifest: verifyMigrationBackup(bundlePath) };
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
fs.rmSync(temporary, { recursive: true, force: true });
|
|
261
|
+
if (fs.existsSync(bundlePath)) {
|
|
262
|
+
throw new ConfigError(`Migration backup creation raced with another writer and left ${bundlePath}. Verify or preserve it before retrying.`, "INVALID_CONFIG_FILE");
|
|
263
|
+
}
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function withMigrationBackupLock(fn) {
|
|
268
|
+
const release = acquireMigrationBackupLock();
|
|
269
|
+
try {
|
|
270
|
+
return fn();
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
release();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/** Create or verify the immutable pre-cutover bundle. Safe for raw legacy config. */
|
|
277
|
+
export function createMigrationBackup() {
|
|
278
|
+
return withConfigLock(() => withMigrationBackupLock(createMigrationBackupUnlocked));
|
|
279
|
+
}
|
|
280
|
+
/** Used while a config mutation already owns config.json.lck. */
|
|
281
|
+
export function ensureMigrationBackupWithConfigLockHeld() {
|
|
282
|
+
return withMigrationBackupLock(createMigrationBackupUnlocked);
|
|
283
|
+
}
|
|
284
|
+
/** Used by database migration hooks before applying the first 0.9 migration. */
|
|
285
|
+
export function ensureMigrationBackup() {
|
|
286
|
+
return createMigrationBackup();
|
|
287
|
+
}
|
|
288
|
+
function activeRestoreLocks() {
|
|
289
|
+
const lockPaths = [getLockfileLockPath(), getIndexWriterLockPath()];
|
|
290
|
+
const configLock = path.join(path.dirname(getConfigPath()), "config.json.lck");
|
|
291
|
+
const dataDir = getDataDir();
|
|
292
|
+
for (const name of ["improve.lock", "consolidate.lock", "reflect-distill.lock", "triage.lock"]) {
|
|
293
|
+
lockPaths.push(path.join(dataDir, name));
|
|
294
|
+
}
|
|
295
|
+
const stashDirs = new Set();
|
|
296
|
+
for (const configPath of [getConfigPath(), path.join(getMigrationBackupDir(), "config.json")]) {
|
|
297
|
+
try {
|
|
298
|
+
const raw = parseConfigText(fs.readFileSync(configPath, "utf8"), configPath);
|
|
299
|
+
if (typeof raw.stashDir === "string" && raw.stashDir)
|
|
300
|
+
stashDirs.add(raw.stashDir);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// Restore still verifies the bundle. A malformed live config must not
|
|
304
|
+
// prevent recovery; it simply cannot contribute an extra stash lock path.
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
for (const stashDir of stashDirs) {
|
|
308
|
+
for (const name of ["improve.lock", "consolidate.lock", "reflect-distill.lock", "triage.lock"]) {
|
|
309
|
+
lockPaths.push(path.join(stashDir, ".akm", name));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (const baseDir of [dataDir, ...[...stashDirs].map((stashDir) => path.join(stashDir, ".akm"))]) {
|
|
313
|
+
const extractLockDir = path.join(baseDir, "extract-locks");
|
|
314
|
+
try {
|
|
315
|
+
for (const entry of fs.readdirSync(extractLockDir, { withFileTypes: true })) {
|
|
316
|
+
if (entry.isFile() && entry.name.endsWith(".lock"))
|
|
317
|
+
lockPaths.push(path.join(extractLockDir, entry.name));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
if (error.code !== "ENOENT")
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const activityDir = path.join(path.dirname(getLockfileLockPath()), "maintenance-activities");
|
|
326
|
+
try {
|
|
327
|
+
for (const entry of fs.readdirSync(activityDir, { withFileTypes: true })) {
|
|
328
|
+
if (entry.isFile() && entry.name.endsWith(".lock"))
|
|
329
|
+
lockPaths.push(path.join(activityDir, entry.name));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
if (error.code !== "ENOENT")
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
return [configLock, ...lockPaths].filter((lockPath) => {
|
|
337
|
+
const probe = probeLock(lockPath);
|
|
338
|
+
return probe.state === "held" && (lockPath !== configLock || probe.holderPid !== process.pid);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
function activeWorkflowClaims() {
|
|
342
|
+
const workflowPath = getWorkflowDbPath();
|
|
343
|
+
if (!fs.existsSync(workflowPath))
|
|
344
|
+
return [];
|
|
345
|
+
// Restore already owns the maintenance barrier for this handle's complete
|
|
346
|
+
// lifetime. Registering a normal workflow-db activity here would re-enter
|
|
347
|
+
// that barrier and deadlock against ourselves.
|
|
348
|
+
const db = openDatabase(workflowPath, { readonly: true });
|
|
349
|
+
try {
|
|
350
|
+
const runsTable = db
|
|
351
|
+
.prepare("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'workflow_runs'")
|
|
352
|
+
.get();
|
|
353
|
+
const blockers = [];
|
|
354
|
+
if (runsTable) {
|
|
355
|
+
const columns = new Set(db.prepare("PRAGMA table_info(workflow_runs)").all().map((row) => row.name));
|
|
356
|
+
if (columns.has("engine_lease_holder") && columns.has("engine_lease_until")) {
|
|
357
|
+
const now = new Date().toISOString();
|
|
358
|
+
blockers.push(...db
|
|
359
|
+
.prepare(`SELECT id, engine_lease_holder AS holder, engine_lease_until AS expires
|
|
360
|
+
FROM workflow_runs
|
|
361
|
+
WHERE engine_lease_holder IS NOT NULL AND engine_lease_until IS NOT NULL AND engine_lease_until >= ?`)
|
|
362
|
+
.all(now).map((lease) => `${workflowPath}#run=${lease.id},holder=${lease.holder},expires=${lease.expires}`));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const unitsTable = db
|
|
366
|
+
.prepare("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'workflow_run_units'")
|
|
367
|
+
.get();
|
|
368
|
+
if (!unitsTable)
|
|
369
|
+
return blockers;
|
|
370
|
+
const unitColumns = new Set(db.prepare("PRAGMA table_info(workflow_run_units)").all().map((row) => row.name));
|
|
371
|
+
if (!unitColumns.has("claim_holder") || !unitColumns.has("claim_expires_at"))
|
|
372
|
+
return blockers;
|
|
373
|
+
const now = new Date().toISOString();
|
|
374
|
+
blockers.push(...db
|
|
375
|
+
.prepare(`SELECT run_id AS runId, unit_id AS unitId, claim_holder AS holder, claim_expires_at AS expires
|
|
376
|
+
FROM workflow_run_units
|
|
377
|
+
WHERE status = 'running' AND claim_holder IS NOT NULL
|
|
378
|
+
AND claim_expires_at IS NOT NULL AND claim_expires_at >= ?`)
|
|
379
|
+
.all(now).map((claim) => `${workflowPath}#run=${claim.runId},unit=${claim.unitId},holder=${claim.holder},expires=${claim.expires}`));
|
|
380
|
+
return blockers;
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
db.close();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function restoreArtifact(source, destination) {
|
|
387
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
388
|
+
writeFileAtomic(destination, fs.readFileSync(source), 0o600);
|
|
389
|
+
fs.chmodSync(destination, 0o600);
|
|
390
|
+
}
|
|
391
|
+
/** Restore exactly the manifest's original presence/absence state. */
|
|
392
|
+
export function restoreMigrationBackup(confirm) {
|
|
393
|
+
if (!confirm) {
|
|
394
|
+
throw new ConfigError("Migration backup restore requires --confirm.", "INVALID_CONFIG_FILE");
|
|
395
|
+
}
|
|
396
|
+
return withConfigLock(() => withMigrationBackupLock(() => {
|
|
397
|
+
return withMaintenanceStartBarrier(() => {
|
|
398
|
+
const blockers = [...activeRestoreLocks(), ...activeWorkflowClaims()];
|
|
399
|
+
if (blockers.length > 0) {
|
|
400
|
+
throw new ConfigError(`Refusing restore while AKM locks or workflow leases are active: ${blockers.join(", ")}.`, "INVALID_CONFIG_FILE");
|
|
401
|
+
}
|
|
402
|
+
const bundlePath = getMigrationBackupDir();
|
|
403
|
+
const manifest = verifyMigrationBackup(bundlePath);
|
|
404
|
+
for (const name of ARTIFACT_NAMES) {
|
|
405
|
+
const artifact = manifest.artifacts[name];
|
|
406
|
+
if (artifact.present)
|
|
407
|
+
restoreArtifact(path.join(bundlePath, name), artifact.sourcePath);
|
|
408
|
+
else
|
|
409
|
+
fs.rmSync(artifact.sourcePath, { force: true });
|
|
410
|
+
fs.rmSync(`${artifact.sourcePath}-wal`, { force: true });
|
|
411
|
+
fs.rmSync(`${artifact.sourcePath}-shm`, { force: true });
|
|
412
|
+
}
|
|
413
|
+
return { path: bundlePath, created: false, manifest };
|
|
414
|
+
});
|
|
415
|
+
}));
|
|
416
|
+
}
|
package/dist/core/paths.js
CHANGED
|
@@ -218,6 +218,9 @@ export function getDbPath(env = process.env) {
|
|
|
218
218
|
export function getIndexWriterLockPath() {
|
|
219
219
|
return path.join(getDataDir(), "index.db.write.lock");
|
|
220
220
|
}
|
|
221
|
+
export function getMaintenanceBarrierPath() {
|
|
222
|
+
return path.join(getDataDir(), "maintenance.barrier.lock");
|
|
223
|
+
}
|
|
221
224
|
export function getWorkflowDbPath() {
|
|
222
225
|
return path.join(getDataDir(), "workflow.db");
|
|
223
226
|
}
|