akm-cli 0.7.4 → 0.8.0-rc.10
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 +224 -1
- package/README.md +22 -6
- package/SECURITY.md +93 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +133 -0
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2631 -1440
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +110 -0
- package/dist/commands/agent-support.js +68 -0
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +130 -534
- package/dist/commands/consolidate.js +2122 -0
- package/dist/commands/curate.js +45 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +660 -0
- package/dist/commands/distill.js +1081 -73
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +43 -0
- package/dist/commands/events.js +15 -24
- package/dist/commands/extract-cli.js +127 -0
- package/dist/commands/extract-prompt.js +204 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/feedback-cli.js +331 -0
- package/dist/commands/graph.js +477 -0
- package/dist/commands/health.js +1302 -0
- package/dist/commands/help/help-accept.md +12 -0
- package/dist/commands/help/help-improve.md +69 -0
- package/dist/commands/help/help-proposals.md +18 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +11 -0
- package/dist/commands/history.js +54 -46
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +217 -0
- package/dist/commands/improve-profiles.js +166 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +2373 -0
- package/dist/commands/info.js +5 -2
- package/dist/commands/init.js +50 -2
- package/dist/commands/installed-stashes.js +102 -139
- package/dist/commands/knowledge.js +136 -0
- package/dist/commands/lint/agent-linter.js +49 -0
- package/dist/commands/lint/base-linter.js +479 -0
- package/dist/commands/lint/command-linter.js +49 -0
- package/dist/commands/lint/default-linter.js +16 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +196 -0
- package/dist/commands/lint/knowledge-linter.js +16 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +61 -0
- package/dist/commands/lint/registry.js +36 -0
- package/dist/commands/lint/skill-linter.js +45 -0
- package/dist/commands/lint/task-linter.js +50 -0
- package/dist/commands/lint/types.js +4 -0
- package/dist/commands/lint/workflow-linter.js +56 -0
- package/dist/commands/lint.js +4 -0
- package/dist/commands/migration-help.js +3 -0
- package/dist/commands/proposal.js +67 -12
- package/dist/commands/propose.js +120 -45
- package/dist/commands/reflect.js +1104 -60
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +5 -2
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +70 -7
- package/dist/commands/schema-repair.js +203 -0
- package/dist/commands/search.js +115 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +158 -60
- package/dist/commands/source-add.js +17 -45
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +14 -19
- package/dist/commands/tasks.js +437 -0
- package/dist/commands/url-checker.js +42 -0
- package/dist/core/action-contributors.js +28 -0
- package/dist/core/asset-ref.js +17 -2
- package/dist/core/asset-registry.js +12 -17
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +67 -1
- package/dist/core/common.js +182 -0
- package/dist/core/concurrent.js +25 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +534 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +364 -968
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +105 -135
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -8
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +20 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +806 -0
- package/dist/core/parse.js +158 -0
- package/dist/core/paths.js +280 -14
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +69 -0
- package/dist/core/proposals.js +512 -42
- package/dist/core/state-db.js +1068 -0
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +54 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +64 -1
- package/dist/core/write-source.js +3 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +198 -489
- package/dist/indexer/db.js +990 -108
- package/dist/indexer/ensure-index.js +136 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +376 -101
- package/dist/indexer/graph-db.js +391 -0
- package/dist/indexer/graph-dedup.js +95 -0
- package/dist/indexer/graph-extraction.js +550 -114
- package/dist/indexer/index-context.js +4 -0
- package/dist/indexer/indexer.js +547 -309
- package/dist/indexer/llm-cache.js +52 -0
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +167 -160
- package/dist/indexer/memory-inference.js +152 -74
- package/dist/indexer/metadata-contributors.js +29 -0
- package/dist/indexer/metadata.js +275 -196
- package/dist/indexer/path-resolver.js +92 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +331 -0
- package/dist/indexer/ranking.js +81 -0
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +111 -0
- package/dist/indexer/search-source.js +44 -10
- package/dist/indexer/semantic-status.js +6 -17
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +28 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +122 -230
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +7 -13
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +70 -5
- package/dist/integrations/agent/prompts.js +250 -36
- package/dist/integrations/agent/runner.js +151 -0
- package/dist/integrations/agent/sdk-runner.js +126 -0
- package/dist/integrations/agent/spawn.js +183 -35
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +32 -69
- package/dist/integrations/session-logs/index.js +69 -0
- package/dist/integrations/session-logs/inline-refs.js +35 -0
- package/dist/integrations/session-logs/pre-filter.js +152 -0
- package/dist/integrations/session-logs/providers/claude-code.js +282 -0
- package/dist/integrations/session-logs/providers/opencode.js +258 -0
- package/dist/integrations/session-logs/types.js +4 -0
- package/dist/llm/call-ai.js +62 -0
- package/dist/llm/client.js +79 -88
- package/dist/llm/embedder.js +20 -29
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +42 -1
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +95 -48
- package/dist/llm/graph-extract.js +676 -72
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +80 -71
- package/dist/llm/metadata-enhance.js +42 -29
- package/dist/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/output/cli-hints-full.md +292 -0
- package/dist/output/cli-hints-short.md +66 -0
- package/dist/output/cli-hints.js +7 -311
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +306 -258
- package/dist/output/shapes/curate.js +56 -0
- package/dist/output/shapes/distill.js +10 -0
- package/dist/output/shapes/env-list.js +19 -0
- package/dist/output/shapes/events.js +11 -0
- package/dist/output/shapes/helpers.js +424 -0
- package/dist/output/shapes/history.js +7 -0
- package/dist/output/shapes/passthrough.js +102 -0
- package/dist/output/shapes/proposal-accept.js +7 -0
- package/dist/output/shapes/proposal-diff.js +7 -0
- package/dist/output/shapes/proposal-list.js +7 -0
- package/dist/output/shapes/proposal-producer.js +11 -0
- package/dist/output/shapes/proposal-reject.js +7 -0
- package/dist/output/shapes/proposal-show.js +7 -0
- package/dist/output/shapes/registry-search.js +6 -0
- package/dist/output/shapes/registry.js +30 -0
- package/dist/output/shapes/search.js +6 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes/show.js +6 -0
- package/dist/output/shapes/vault-list.js +19 -0
- package/dist/output/shapes.js +51 -511
- package/dist/output/text/add.js +6 -0
- package/dist/output/text/clone.js +6 -0
- package/dist/output/text/config.js +6 -0
- package/dist/output/text/curate.js +6 -0
- package/dist/output/text/distill.js +7 -0
- package/dist/output/text/enable-disable.js +7 -0
- package/dist/output/text/events.js +10 -0
- package/dist/output/text/feedback.js +6 -0
- package/dist/output/text/helpers.js +1039 -0
- package/dist/output/text/history.js +7 -0
- package/dist/output/text/import.js +6 -0
- package/dist/output/text/index.js +6 -0
- package/dist/output/text/info.js +6 -0
- package/dist/output/text/init.js +6 -0
- package/dist/output/text/list.js +6 -0
- package/dist/output/text/proposal-producer.js +8 -0
- package/dist/output/text/proposal.js +11 -0
- package/dist/output/text/registry-commands.js +11 -0
- package/dist/output/text/registry.js +30 -0
- package/dist/output/text/remember.js +6 -0
- package/dist/output/text/remove.js +6 -0
- package/dist/output/text/save.js +6 -0
- package/dist/output/text/search.js +6 -0
- package/dist/output/text/show.js +6 -0
- package/dist/output/text/update.js +6 -0
- package/dist/output/text/upgrade.js +6 -0
- package/dist/output/text/vault.js +16 -0
- package/dist/output/text/wiki.js +15 -0
- package/dist/output/text/workflow.js +14 -0
- package/dist/output/text.js +44 -1093
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +4 -1
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +71 -50
- package/dist/registry/providers/static-index.js +53 -48
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17750 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +775 -37
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +5 -12
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +179 -20
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +7 -0
- package/dist/tasks/backends/cron.js +203 -0
- package/dist/tasks/backends/exec-utils.js +28 -0
- package/dist/tasks/backends/index.js +24 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +187 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +215 -0
- package/dist/tasks/parser.js +211 -0
- package/dist/tasks/resolveAkmBin.js +87 -0
- package/dist/tasks/runner.js +458 -0
- package/dist/tasks/schedule.js +227 -0
- package/dist/tasks/schema.js +15 -0
- package/dist/tasks/validator.js +62 -0
- package/dist/version.js +3 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +15 -0
- package/dist/wiki/wiki.js +13 -61
- package/dist/workflows/authoring.js +8 -25
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +141 -2
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +11 -3
- package/dist/workflows/runs.js +91 -89
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +79 -0
- package/dist/workflows/validator.js +4 -8
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +10 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.7.4.md +1 -1
- package/docs/migration/release-notes/0.7.5.md +20 -0
- package/docs/migration/release-notes/0.8.0.md +48 -0
- package/docs/migration/v0.7-to-v0.8.md +1307 -0
- package/package.json +29 -11
- package/dist/commands/install-audit.js +0 -381
- package/dist/commands/vault.js +0 -333
- package/dist/templates/wiki-templates.js +0 -100
|
@@ -0,0 +1,347 @@
|
|
|
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
|
+
/**
|
|
5
|
+
* Pure I/O helpers for AKM config files.
|
|
6
|
+
*
|
|
7
|
+
* No knowledge of the AkmConfig shape — these functions just read JSON(C) text
|
|
8
|
+
* from disk and write JSON text back atomically. Validation and migration live
|
|
9
|
+
* in `./config.ts` and `./config-migrate.ts`.
|
|
10
|
+
*
|
|
11
|
+
* Split out so the load path is testable without touching the filesystem
|
|
12
|
+
* (`parseConfigText` is pure), and so a single atomic write path serves
|
|
13
|
+
* `saveConfig`, the migrate command, and the setup wizard (#464.c).
|
|
14
|
+
*/
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { writeFileAtomic } from "./common";
|
|
18
|
+
import { ConfigError } from "./errors";
|
|
19
|
+
import { probeLock, releaseLock, tryAcquireLockSync } from "./file-lock";
|
|
20
|
+
import { getCacheDir, getConfigDir } from "./paths";
|
|
21
|
+
/**
|
|
22
|
+
* Read the raw text of a config file. Returns `undefined` when the file does
|
|
23
|
+
* not exist (legitimate cold-start). Other I/O errors propagate.
|
|
24
|
+
*/
|
|
25
|
+
export function readConfigText(configPath) {
|
|
26
|
+
try {
|
|
27
|
+
return fs.readFileSync(configPath, "utf8");
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (err.code === "ENOENT")
|
|
31
|
+
return undefined;
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Parse JSON(C) config text into a plain object. Strips `//` and `/* */`
|
|
37
|
+
* comments before parsing.
|
|
38
|
+
*
|
|
39
|
+
* Throws {@link ConfigError} when the text is unparseable or when the root is
|
|
40
|
+
* not a JSON object. Per #458, malformed config text is NOT silently rescued —
|
|
41
|
+
* the caller must surface the parse error.
|
|
42
|
+
*/
|
|
43
|
+
export function parseConfigText(text, sourcePath) {
|
|
44
|
+
const stripped = stripJsonComments(text);
|
|
45
|
+
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
46
|
+
let parsed;
|
|
47
|
+
try {
|
|
48
|
+
parsed = JSON.parse(stripped);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
52
|
+
throw new ConfigError(`Failed to parse config JSON${where}: ${detail}`, "INVALID_CONFIG_FILE", "Edit the file to fix the JSON syntax error. Comments (// and /* */) are allowed; trailing commas are not.");
|
|
53
|
+
}
|
|
54
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
55
|
+
throw new ConfigError(`Config file${where} must contain a JSON object at the root, got ${describeJsonRoot(parsed)}.`, "INVALID_CONFIG_FILE");
|
|
56
|
+
}
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
function describeJsonRoot(value) {
|
|
60
|
+
if (value === null)
|
|
61
|
+
return "null";
|
|
62
|
+
if (Array.isArray(value))
|
|
63
|
+
return "an array";
|
|
64
|
+
if (typeof value === "string")
|
|
65
|
+
return "a string";
|
|
66
|
+
if (typeof value === "number")
|
|
67
|
+
return "a number";
|
|
68
|
+
if (typeof value === "boolean")
|
|
69
|
+
return "a boolean";
|
|
70
|
+
return typeof value;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Atomically write a config object to disk as pretty-printed JSON. Routes
|
|
74
|
+
* through {@link writeFileAtomic} so partial writes can never corrupt the
|
|
75
|
+
* config file (#464.c).
|
|
76
|
+
*/
|
|
77
|
+
export function writeConfigAtomic(configPath, config) {
|
|
78
|
+
writeFileAtomic(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
79
|
+
}
|
|
80
|
+
/** Maximum number of timestamped config backups to retain (#459). */
|
|
81
|
+
const MAX_CONFIG_BACKUPS = 5;
|
|
82
|
+
/**
|
|
83
|
+
* Snapshot the current config file to `<cacheDir>/config-backups/`. Writes
|
|
84
|
+
* both a timestamped copy and a `config.latest.json` pointer, then prunes the
|
|
85
|
+
* timestamped set to {@link MAX_CONFIG_BACKUPS} most-recent entries.
|
|
86
|
+
*
|
|
87
|
+
* No-op when the source file does not exist (cold-start safe).
|
|
88
|
+
*/
|
|
89
|
+
export function backupExistingConfig(configPath) {
|
|
90
|
+
if (!fs.existsSync(configPath))
|
|
91
|
+
return;
|
|
92
|
+
const backupDir = path.join(getCacheDir(), "config-backups");
|
|
93
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
94
|
+
const timestamp = new Date().toISOString().replace(/[.:]/g, "-");
|
|
95
|
+
fs.copyFileSync(configPath, path.join(backupDir, `config-${timestamp}.json`));
|
|
96
|
+
fs.copyFileSync(configPath, path.join(backupDir, "config.latest.json"));
|
|
97
|
+
pruneOldBackups(backupDir);
|
|
98
|
+
}
|
|
99
|
+
function pruneOldBackups(backupDir) {
|
|
100
|
+
let entries;
|
|
101
|
+
try {
|
|
102
|
+
entries = fs.readdirSync(backupDir);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const timestamped = entries
|
|
108
|
+
.filter((n) => n.startsWith("config-") && n.endsWith(".json") && n !== "config.latest.json")
|
|
109
|
+
.map((name) => {
|
|
110
|
+
const full = path.join(backupDir, name);
|
|
111
|
+
let mtime = 0;
|
|
112
|
+
try {
|
|
113
|
+
mtime = fs.statSync(full).mtimeMs;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Unreadable — sorts to the end via mtime 0.
|
|
117
|
+
}
|
|
118
|
+
return { path: full, mtime };
|
|
119
|
+
})
|
|
120
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
121
|
+
for (const stale of timestamped.slice(MAX_CONFIG_BACKUPS)) {
|
|
122
|
+
try {
|
|
123
|
+
fs.unlinkSync(stale.path);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Best-effort prune; next save will retry.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// ── Config write lock ────────────────────────────────────────────────────────
|
|
131
|
+
/**
|
|
132
|
+
* Path to the config write sentinel (`config.json.lck` in $CONFIG).
|
|
133
|
+
*
|
|
134
|
+
* Placed next to config.json so the lock scope is obvious and the path is
|
|
135
|
+
* predictable for debugging. Uses $CONFIG (not $DATA) because config.json
|
|
136
|
+
* itself lives in $CONFIG — they should fail together if the dir is read-only.
|
|
137
|
+
*/
|
|
138
|
+
function getConfigLockPath() {
|
|
139
|
+
return path.join(getConfigDir(), "config.json.lck");
|
|
140
|
+
}
|
|
141
|
+
const CONFIG_LOCK_MAX_RETRIES = 10;
|
|
142
|
+
const CONFIG_LOCK_RETRY_DELAY_MS = 50;
|
|
143
|
+
/**
|
|
144
|
+
* Acquire an exclusive sentinel around config writes.
|
|
145
|
+
*
|
|
146
|
+
* Returns a release function. Best-effort: when all retries are exhausted the
|
|
147
|
+
* write proceeds unlocked rather than erroring (same posture as lockfile.ts).
|
|
148
|
+
*/
|
|
149
|
+
export function acquireConfigLock() {
|
|
150
|
+
const lockPath = getConfigLockPath();
|
|
151
|
+
try {
|
|
152
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Directory already exists or unwritable — let the write fail naturally.
|
|
156
|
+
}
|
|
157
|
+
for (let attempt = 0; attempt < CONFIG_LOCK_MAX_RETRIES; attempt++) {
|
|
158
|
+
try {
|
|
159
|
+
if (tryAcquireLockSync(lockPath, String(process.pid))) {
|
|
160
|
+
return () => releaseLock(lockPath);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Non-EEXIST error (permissions, etc.) — bail out and proceed unlocked.
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
if (probeLock(lockPath).state === "stale") {
|
|
168
|
+
releaseLock(lockPath);
|
|
169
|
+
continue; // Reclaimed — retry immediately.
|
|
170
|
+
}
|
|
171
|
+
if (attempt < CONFIG_LOCK_MAX_RETRIES - 1) {
|
|
172
|
+
// Busy spin (synchronous) — config writes are fast.
|
|
173
|
+
const deadline = Date.now() + CONFIG_LOCK_RETRY_DELAY_MS;
|
|
174
|
+
while (Date.now() < deadline) {
|
|
175
|
+
// spin
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Best-effort: proceed without lock.
|
|
180
|
+
return () => { };
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Run `fn` inside the config write lock. Always releases the lock.
|
|
184
|
+
*/
|
|
185
|
+
export function withConfigLock(fn) {
|
|
186
|
+
const release = acquireConfigLock();
|
|
187
|
+
try {
|
|
188
|
+
return fn();
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
release();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// ── Unified diff helper ──────────────────────────────────────────────────────
|
|
195
|
+
/**
|
|
196
|
+
* Produce a minimal unified diff between `before` and `after` text.
|
|
197
|
+
* Uses LCS-based diff with 2-line context. Returns an empty string when the
|
|
198
|
+
* inputs are identical. `label` is used as the path in the diff header.
|
|
199
|
+
*
|
|
200
|
+
* Designed for config files (typically < 200 lines). O(m*n) in line count.
|
|
201
|
+
*/
|
|
202
|
+
export function unifiedDiff(before, after, label) {
|
|
203
|
+
if (before === after)
|
|
204
|
+
return "";
|
|
205
|
+
const a = before.split("\n");
|
|
206
|
+
const b = after.split("\n");
|
|
207
|
+
const eqPairs = lcsLinePairs(a, b);
|
|
208
|
+
const CONTEXT = 2;
|
|
209
|
+
const ops = [];
|
|
210
|
+
let ai = 0;
|
|
211
|
+
let bi = 0;
|
|
212
|
+
let pi = 0;
|
|
213
|
+
while (ai < a.length || bi < b.length) {
|
|
214
|
+
const eq = eqPairs[pi];
|
|
215
|
+
if (eq && eq.ai === ai && eq.bi === bi) {
|
|
216
|
+
ops.push({ type: "eq", line: a[ai], ai, bi });
|
|
217
|
+
ai++;
|
|
218
|
+
bi++;
|
|
219
|
+
pi++;
|
|
220
|
+
}
|
|
221
|
+
else if (ai < a.length && (!eq || ai < eq.ai)) {
|
|
222
|
+
ops.push({ type: "del", line: a[ai], ai, bi });
|
|
223
|
+
ai++;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
ops.push({ type: "add", line: b[bi], ai, bi });
|
|
227
|
+
bi++;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Find changed op indices
|
|
231
|
+
const changed = new Set(ops.map((o, i) => (o.type !== "eq" ? i : -1)).filter((i) => i >= 0));
|
|
232
|
+
if (changed.size === 0)
|
|
233
|
+
return "";
|
|
234
|
+
// Determine which equal lines to include as context
|
|
235
|
+
const include = new Set();
|
|
236
|
+
for (const ci of changed) {
|
|
237
|
+
for (let k = Math.max(0, ci - CONTEXT); k <= Math.min(ops.length - 1, ci + CONTEXT); k++) {
|
|
238
|
+
include.add(k);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Collect hunks
|
|
242
|
+
const header = [`--- ${label} (before)`, `+++ ${label} (after)`];
|
|
243
|
+
const out = [];
|
|
244
|
+
let hunkOps = [];
|
|
245
|
+
let prevIncluded = false;
|
|
246
|
+
function flushHunk() {
|
|
247
|
+
if (hunkOps.length === 0)
|
|
248
|
+
return;
|
|
249
|
+
const delStart = hunkOps.find((o) => o.type !== "add")?.ai ?? 0;
|
|
250
|
+
const addStart = hunkOps.find((o) => o.type !== "del")?.bi ?? 0;
|
|
251
|
+
const countA = hunkOps.filter((o) => o.type !== "add").length;
|
|
252
|
+
const countB = hunkOps.filter((o) => o.type !== "del").length;
|
|
253
|
+
out.push(`@@ -${delStart + 1},${countA} +${addStart + 1},${countB} @@`);
|
|
254
|
+
for (const op of hunkOps) {
|
|
255
|
+
const ch = op.type === "eq" ? " " : op.type === "del" ? "-" : "+";
|
|
256
|
+
out.push(`${ch}${op.line}`);
|
|
257
|
+
}
|
|
258
|
+
hunkOps = [];
|
|
259
|
+
}
|
|
260
|
+
for (let k = 0; k < ops.length; k++) {
|
|
261
|
+
if (include.has(k)) {
|
|
262
|
+
hunkOps.push(ops[k]);
|
|
263
|
+
prevIncluded = true;
|
|
264
|
+
}
|
|
265
|
+
else if (prevIncluded) {
|
|
266
|
+
flushHunk();
|
|
267
|
+
prevIncluded = false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
flushHunk();
|
|
271
|
+
return out.length > 0 ? [...header, ...out].join("\n") : "";
|
|
272
|
+
}
|
|
273
|
+
function lcsLinePairs(a, b) {
|
|
274
|
+
const m = a.length;
|
|
275
|
+
const n = b.length;
|
|
276
|
+
if (m === 0 || n === 0)
|
|
277
|
+
return [];
|
|
278
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
279
|
+
for (let i = 1; i <= m; i++) {
|
|
280
|
+
for (let j = 1; j <= n; j++) {
|
|
281
|
+
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const result = [];
|
|
285
|
+
let i = m;
|
|
286
|
+
let j = n;
|
|
287
|
+
while (i > 0 && j > 0) {
|
|
288
|
+
if (a[i - 1] === b[j - 1]) {
|
|
289
|
+
result.unshift({ ai: i - 1, bi: j - 1 });
|
|
290
|
+
i--;
|
|
291
|
+
j--;
|
|
292
|
+
}
|
|
293
|
+
else if (dp[i - 1][j] > dp[i][j - 1]) {
|
|
294
|
+
i--;
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
j--;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Strip JavaScript-style comments from a JSON string (JSONC support).
|
|
304
|
+
* Handles `//` line comments and `/* */` block comments while preserving
|
|
305
|
+
* comment-like sequences inside quoted strings.
|
|
306
|
+
*/
|
|
307
|
+
export function stripJsonComments(text) {
|
|
308
|
+
let result = "";
|
|
309
|
+
let i = 0;
|
|
310
|
+
let inString = false;
|
|
311
|
+
while (i < text.length) {
|
|
312
|
+
if (inString) {
|
|
313
|
+
if (text[i] === "\\") {
|
|
314
|
+
result += text[i] + (text[i + 1] ?? "");
|
|
315
|
+
i += 2;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (text[i] === '"') {
|
|
319
|
+
inString = false;
|
|
320
|
+
}
|
|
321
|
+
result += text[i];
|
|
322
|
+
i++;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (text[i] === '"') {
|
|
326
|
+
inString = true;
|
|
327
|
+
result += text[i];
|
|
328
|
+
i++;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (text[i] === "/" && text[i + 1] === "/") {
|
|
332
|
+
while (i < text.length && text[i] !== "\n")
|
|
333
|
+
i++;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (text[i] === "/" && text[i + 1] === "*") {
|
|
337
|
+
i += 2;
|
|
338
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
|
|
339
|
+
i++;
|
|
340
|
+
i += 2;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
result += text[i];
|
|
344
|
+
i++;
|
|
345
|
+
}
|
|
346
|
+
return result;
|
|
347
|
+
}
|