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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import fs from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { buildWorkflowAction } from "../../output/renderers.js";
|
|
6
7
|
import { registerActionBuilder, registerTypeRenderer } from "./asset-registry.js";
|
|
@@ -8,6 +9,59 @@ function toPosix(input) {
|
|
|
8
9
|
return input.replace(/\\/g, "/");
|
|
9
10
|
}
|
|
10
11
|
const buildTaskAction = (ref) => `akm tasks show ${ref.replace(/^task:/, "")} -> inspect; akm tasks run <id> -> run now; akm tasks remove <id> -> unschedule`;
|
|
12
|
+
/**
|
|
13
|
+
* Recognized workflow asset extensions, in resolution-priority order.
|
|
14
|
+
* `.md` (classic linear markdown workflows — the stable contract) stays
|
|
15
|
+
* FIRST for back-compat; `.yaml`/`.yml` hold YAML workflow *programs*
|
|
16
|
+
* (redesign addendum, R1). `workflow:<name>` refs resolve against this list.
|
|
17
|
+
*/
|
|
18
|
+
export const WORKFLOW_EXTENSIONS = [".md", ".yaml", ".yml"];
|
|
19
|
+
/**
|
|
20
|
+
* Strip a recognized workflow extension (`.md`/`.yaml`/`.yml`) from a workflow
|
|
21
|
+
* asset *name* so `foo`, `foo.yaml`, `foo.yml`, and `foo.md` collapse to one
|
|
22
|
+
* canonical identity — the same collapse `workflowSpec.toCanonicalName`
|
|
23
|
+
* performs on a resolved file path. Callers that turn a `workflow:<name>` ref
|
|
24
|
+
* into run identity (the active-run guard, list/status filters) MUST route the
|
|
25
|
+
* name through this so an aliased spelling (`workflow:foo.yaml`) and the
|
|
26
|
+
* canonical `workflow:foo` cannot start or hide parallel runs of the same
|
|
27
|
+
* workflow. Names without a recognized workflow extension pass through
|
|
28
|
+
* unchanged.
|
|
29
|
+
*/
|
|
30
|
+
export function canonicalizeWorkflowName(name) {
|
|
31
|
+
const lower = name.toLowerCase();
|
|
32
|
+
for (const ext of WORKFLOW_EXTENSIONS) {
|
|
33
|
+
if (lower.endsWith(ext))
|
|
34
|
+
return name.slice(0, -ext.length);
|
|
35
|
+
}
|
|
36
|
+
return name;
|
|
37
|
+
}
|
|
38
|
+
const workflowSpec = {
|
|
39
|
+
isRelevantFile: (fileName) => WORKFLOW_EXTENSIONS.includes(path.extname(fileName).toLowerCase()),
|
|
40
|
+
toCanonicalName: (typeRoot, filePath) => {
|
|
41
|
+
const rel = toPosix(path.relative(typeRoot, filePath));
|
|
42
|
+
for (const ext of WORKFLOW_EXTENSIONS) {
|
|
43
|
+
if (rel.toLowerCase().endsWith(ext))
|
|
44
|
+
return rel.slice(0, -ext.length);
|
|
45
|
+
}
|
|
46
|
+
return rel;
|
|
47
|
+
},
|
|
48
|
+
toAssetPath: (typeRoot, name) => {
|
|
49
|
+
// Explicit extension wins (accepts refs like "release/ship.yaml").
|
|
50
|
+
const lower = name.toLowerCase();
|
|
51
|
+
for (const ext of WORKFLOW_EXTENSIONS) {
|
|
52
|
+
if (lower.endsWith(ext))
|
|
53
|
+
return path.join(typeRoot, name);
|
|
54
|
+
}
|
|
55
|
+
// Probe in priority order — `.md` first for back-compat — and fall back
|
|
56
|
+
// to the markdown path so error messages keep naming the canonical file.
|
|
57
|
+
for (const ext of WORKFLOW_EXTENSIONS) {
|
|
58
|
+
const candidate = path.join(typeRoot, `${name}${ext}`);
|
|
59
|
+
if (fs.existsSync(candidate))
|
|
60
|
+
return candidate;
|
|
61
|
+
}
|
|
62
|
+
return path.join(typeRoot, `${name}.md`);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
11
65
|
const markdownSpec = {
|
|
12
66
|
isRelevantFile: (fileName) => path.extname(fileName).toLowerCase() === ".md",
|
|
13
67
|
toCanonicalName: (typeRoot, filePath) => {
|
|
@@ -62,7 +116,10 @@ const ASSET_SPECS_INTERNAL = {
|
|
|
62
116
|
knowledge: { stashDir: "knowledge", ...markdownSpec },
|
|
63
117
|
workflow: {
|
|
64
118
|
stashDir: "workflows",
|
|
65
|
-
...
|
|
119
|
+
...workflowSpec,
|
|
120
|
+
// Type-level renderer for markdown workflows; YAML programs are claimed by
|
|
121
|
+
// the dedicated `workflowProgramMatcher`, which names the
|
|
122
|
+
// "workflow-program-yaml" renderer directly on its MatchResult.
|
|
66
123
|
rendererName: "workflow-md",
|
|
67
124
|
actionBuilder: (ref) => buildWorkflowAction(ref),
|
|
68
125
|
},
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import fs from "node:fs";
|
|
12
12
|
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
|
|
13
|
-
import { assembleAsset } from "./asset-serialize.js";
|
|
13
|
+
import { assembleAsset, serializeFrontmatter } from "./asset-serialize.js";
|
|
14
14
|
/**
|
|
15
15
|
* Parse YAML frontmatter from a Markdown (or similar) string.
|
|
16
16
|
*
|
|
@@ -118,6 +118,13 @@ function parseFrontmatterLenient(frontmatter) {
|
|
|
118
118
|
* or `null` to skip the write entirely (e.g. for idempotent no-ops). The body
|
|
119
119
|
* content is preserved from the parse.
|
|
120
120
|
*
|
|
121
|
+
* A frontmatter mutation is a METADATA edit, not a content edit: when the file
|
|
122
|
+
* already has a frontmatter block, only that block is replaced and the body
|
|
123
|
+
* bytes are kept verbatim (routing through `assembleAsset` would strip the
|
|
124
|
+
* body's leading blank lines and force a trailing newline, silently reshaping
|
|
125
|
+
* assets whose writer used a different separator style). A file gaining its
|
|
126
|
+
* FIRST frontmatter block goes through the canonical `assembleAsset` shape.
|
|
127
|
+
*
|
|
121
128
|
* @returns `true` if a write occurred, `false` if the mutator returned `null`.
|
|
122
129
|
*/
|
|
123
130
|
export function mutateFrontmatter(filePath, mutator) {
|
|
@@ -126,7 +133,10 @@ export function mutateFrontmatter(filePath, mutator) {
|
|
|
126
133
|
const nextFrontmatter = mutator(parsed);
|
|
127
134
|
if (nextFrontmatter === null)
|
|
128
135
|
return false;
|
|
129
|
-
|
|
136
|
+
const next = parsed.frontmatter !== null
|
|
137
|
+
? `---\n${serializeFrontmatter(nextFrontmatter)}\n---\n${parsed.content}`
|
|
138
|
+
: assembleAsset(nextFrontmatter, parsed.content);
|
|
139
|
+
fs.writeFileSync(filePath, next, "utf8");
|
|
130
140
|
return true;
|
|
131
141
|
}
|
|
132
142
|
export function parseFrontmatterBlock(raw) {
|
package/dist/core/common.js
CHANGED
|
@@ -243,11 +243,12 @@ function normalizeFsPathForComparison(value) {
|
|
|
243
243
|
*/
|
|
244
244
|
export async function fetchWithTimeout(url, opts, timeoutMs = 30_000, signal) {
|
|
245
245
|
const controller = new AbortController();
|
|
246
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
246
|
+
const timer = timeoutMs === null ? undefined : setTimeout(() => controller.abort(), timeoutMs);
|
|
247
247
|
const abortExternal = () => controller.abort(signal?.reason);
|
|
248
248
|
if (signal) {
|
|
249
249
|
if (signal.aborted) {
|
|
250
|
-
|
|
250
|
+
if (timer)
|
|
251
|
+
clearTimeout(timer);
|
|
251
252
|
controller.abort(signal.reason);
|
|
252
253
|
}
|
|
253
254
|
else {
|
|
@@ -269,7 +270,8 @@ export async function fetchWithTimeout(url, opts, timeoutMs = 30_000, signal) {
|
|
|
269
270
|
finally {
|
|
270
271
|
if (signal)
|
|
271
272
|
signal.removeEventListener("abort", abortExternal);
|
|
272
|
-
|
|
273
|
+
if (timer)
|
|
274
|
+
clearTimeout(timer);
|
|
273
275
|
}
|
|
274
276
|
}
|
|
275
277
|
/**
|
|
@@ -17,7 +17,7 @@ import path from "node:path";
|
|
|
17
17
|
import { sleepSync } from "../../runtime.js";
|
|
18
18
|
import { writeFileAtomic } from "../common.js";
|
|
19
19
|
import { ConfigError } from "../errors.js";
|
|
20
|
-
import { probeLock, releaseLock, tryAcquireLockSync } from "../file-lock.js";
|
|
20
|
+
import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync } from "../file-lock.js";
|
|
21
21
|
import { getCacheDir, getConfigDir } from "../paths.js";
|
|
22
22
|
/**
|
|
23
23
|
* Read the raw text of a config file. Returns `undefined` when the file does
|
|
@@ -80,7 +80,7 @@ export function writeConfigAtomic(configPath, config) {
|
|
|
80
80
|
}
|
|
81
81
|
/** Maximum number of timestamped config backups to retain (#459). */
|
|
82
82
|
const MAX_CONFIG_BACKUPS = 5;
|
|
83
|
-
export function backupExistingConfig(configPath) {
|
|
83
|
+
export function backupExistingConfig(configPath, now = new Date()) {
|
|
84
84
|
if (!fs.existsSync(configPath))
|
|
85
85
|
return undefined;
|
|
86
86
|
const backupDir = path.join(getCacheDir(), "config-backups");
|
|
@@ -89,10 +89,22 @@ export function backupExistingConfig(configPath) {
|
|
|
89
89
|
// the copy→chmod window. chmod again to tighten a dir from an older version.
|
|
90
90
|
fs.mkdirSync(backupDir, { recursive: true, mode: 0o700 });
|
|
91
91
|
fs.chmodSync(backupDir, 0o700);
|
|
92
|
-
const timestamp =
|
|
93
|
-
|
|
92
|
+
const timestamp = now.toISOString().replace(/[.:]/g, "-");
|
|
93
|
+
let sequence = 0;
|
|
94
|
+
let timestamped;
|
|
95
|
+
while (true) {
|
|
96
|
+
timestamped = path.join(backupDir, `config-${timestamp}${sequence === 0 ? "" : `-${sequence}`}.json`);
|
|
97
|
+
try {
|
|
98
|
+
fs.copyFileSync(configPath, timestamped, fs.constants.COPYFILE_EXCL);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error.code !== "EEXIST")
|
|
103
|
+
throw error;
|
|
104
|
+
sequence++;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
94
107
|
const latest = path.join(backupDir, "config.latest.json");
|
|
95
|
-
fs.copyFileSync(configPath, timestamped);
|
|
96
108
|
fs.copyFileSync(configPath, latest);
|
|
97
109
|
// 08-F4: a config backup carries the same sensitive fields as the live config
|
|
98
110
|
// (endpoints, tokens). `copyFileSync` inherits the source's (often 0644) mode,
|
|
@@ -141,7 +153,7 @@ function pruneOldBackups(backupDir) {
|
|
|
141
153
|
* predictable for debugging. Uses $CONFIG (not $DATA) because config.json
|
|
142
154
|
* itself lives in $CONFIG — they should fail together if the dir is read-only.
|
|
143
155
|
*/
|
|
144
|
-
function getConfigLockPath() {
|
|
156
|
+
export function getConfigLockPath() {
|
|
145
157
|
return path.join(getConfigDir(), "config.json.lck");
|
|
146
158
|
}
|
|
147
159
|
const CONFIG_LOCK_MAX_RETRIES = 10;
|
|
@@ -157,8 +169,8 @@ function sleepSyncMs(ms) {
|
|
|
157
169
|
/**
|
|
158
170
|
* Acquire an exclusive sentinel around config writes.
|
|
159
171
|
*
|
|
160
|
-
* Returns a release function.
|
|
161
|
-
*
|
|
172
|
+
* Returns a release function. Acquisition is fail-closed: config mutation may
|
|
173
|
+
* never continue without owning the lock that protects its read/merge/write.
|
|
162
174
|
*/
|
|
163
175
|
export function acquireConfigLock() {
|
|
164
176
|
const lockPath = getConfigLockPath();
|
|
@@ -170,16 +182,16 @@ export function acquireConfigLock() {
|
|
|
170
182
|
}
|
|
171
183
|
for (let attempt = 0; attempt < CONFIG_LOCK_MAX_RETRIES; attempt++) {
|
|
172
184
|
try {
|
|
173
|
-
|
|
174
|
-
|
|
185
|
+
const ownership = tryAcquireLockSync(lockPath, createLockPayload());
|
|
186
|
+
if (ownership) {
|
|
187
|
+
return () => releaseLock(ownership);
|
|
175
188
|
}
|
|
176
189
|
}
|
|
177
|
-
catch {
|
|
178
|
-
|
|
179
|
-
break;
|
|
190
|
+
catch (error) {
|
|
191
|
+
throw new ConfigError(`Unable to acquire config lock at ${lockPath}: ${error instanceof Error ? error.message : String(error)}`, "INVALID_CONFIG_FILE");
|
|
180
192
|
}
|
|
181
|
-
|
|
182
|
-
|
|
193
|
+
const probe = probeLock(lockPath);
|
|
194
|
+
if (probe.state === "stale" && reclaimStaleLock(lockPath, probe)) {
|
|
183
195
|
continue; // Reclaimed — retry immediately.
|
|
184
196
|
}
|
|
185
197
|
if (attempt < CONFIG_LOCK_MAX_RETRIES - 1) {
|
|
@@ -195,8 +207,7 @@ export function acquireConfigLock() {
|
|
|
195
207
|
sleepSyncMs(CONFIG_LOCK_RETRY_DELAY_MS);
|
|
196
208
|
}
|
|
197
209
|
}
|
|
198
|
-
|
|
199
|
-
return () => { };
|
|
210
|
+
throw new ConfigError(`Timed out waiting for config lock at ${lockPath}. Another AKM process may be updating config.`, "INVALID_CONFIG_FILE");
|
|
200
211
|
}
|
|
201
212
|
/**
|
|
202
213
|
* Run `fn` inside the config write lock. Always releases the lock.
|