akm-cli 0.7.5 → 0.8.0-rc.11

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.
Files changed (300) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +192 -2
  2. package/README.md +22 -6
  3. package/SECURITY.md +93 -0
  4. package/dist/cli/config-migrate.js +144 -0
  5. package/dist/cli/config-validate.js +39 -0
  6. package/dist/cli/confirm.js +73 -0
  7. package/dist/cli/parse-args.js +133 -0
  8. package/dist/cli/shared.js +129 -0
  9. package/dist/cli.js +2569 -1449
  10. package/dist/commands/add-cli.js +279 -0
  11. package/dist/commands/agent-dispatch.js +110 -0
  12. package/dist/commands/agent-support.js +68 -0
  13. package/dist/commands/completions.js +3 -0
  14. package/dist/commands/config-cli.js +130 -534
  15. package/dist/commands/consolidate.js +2122 -0
  16. package/dist/commands/curate.js +44 -3
  17. package/dist/commands/db-cli.js +23 -0
  18. package/dist/commands/distill-promotion-policy.js +660 -0
  19. package/dist/commands/distill.js +1075 -77
  20. package/dist/commands/env.js +213 -0
  21. package/dist/commands/eval-cases.js +43 -0
  22. package/dist/commands/events.js +5 -23
  23. package/dist/commands/extract-cli.js +127 -0
  24. package/dist/commands/extract-prompt.js +204 -0
  25. package/dist/commands/extract.js +477 -0
  26. package/dist/commands/feedback-cli.js +331 -0
  27. package/dist/commands/graph.js +477 -0
  28. package/dist/commands/health.js +1302 -0
  29. package/dist/commands/help/help-accept.md +12 -0
  30. package/dist/commands/help/help-improve.md +69 -0
  31. package/dist/commands/help/help-proposals.md +18 -0
  32. package/dist/commands/help/help-propose.md +17 -0
  33. package/dist/commands/help/help-reject.md +11 -0
  34. package/dist/commands/history.js +54 -46
  35. package/dist/commands/improve-auto-accept.js +97 -0
  36. package/dist/commands/improve-cli.js +217 -0
  37. package/dist/commands/improve-profiles.js +166 -0
  38. package/dist/commands/improve-result-file.js +167 -0
  39. package/dist/commands/improve.js +2373 -0
  40. package/dist/commands/info.js +5 -2
  41. package/dist/commands/init.js +50 -2
  42. package/dist/commands/installed-stashes.js +102 -139
  43. package/dist/commands/knowledge.js +136 -0
  44. package/dist/commands/lint/agent-linter.js +49 -0
  45. package/dist/commands/lint/base-linter.js +479 -0
  46. package/dist/commands/lint/command-linter.js +49 -0
  47. package/dist/commands/lint/default-linter.js +16 -0
  48. package/dist/commands/lint/env-key-rules.js +154 -0
  49. package/dist/commands/lint/index.js +196 -0
  50. package/dist/commands/lint/knowledge-linter.js +16 -0
  51. package/dist/commands/lint/markdown-insertion.js +343 -0
  52. package/dist/commands/lint/memory-linter.js +61 -0
  53. package/dist/commands/lint/registry.js +36 -0
  54. package/dist/commands/lint/skill-linter.js +45 -0
  55. package/dist/commands/lint/task-linter.js +50 -0
  56. package/dist/commands/lint/types.js +4 -0
  57. package/dist/commands/lint/workflow-linter.js +56 -0
  58. package/dist/commands/lint.js +4 -0
  59. package/dist/commands/migration-help.js +5 -2
  60. package/dist/commands/proposal.js +67 -12
  61. package/dist/commands/propose.js +86 -31
  62. package/dist/commands/reflect.js +1091 -73
  63. package/dist/commands/registry-cli.js +150 -0
  64. package/dist/commands/registry-search.js +5 -2
  65. package/dist/commands/remember-cli.js +257 -0
  66. package/dist/commands/remember.js +69 -6
  67. package/dist/commands/schema-repair.js +203 -0
  68. package/dist/commands/search.js +115 -14
  69. package/dist/commands/secret.js +173 -0
  70. package/dist/commands/self-update.js +3 -0
  71. package/dist/commands/show.js +148 -25
  72. package/dist/commands/source-add.js +17 -45
  73. package/dist/commands/source-clone.js +3 -0
  74. package/dist/commands/source-manage.js +14 -19
  75. package/dist/commands/tasks.js +437 -0
  76. package/dist/commands/url-checker.js +42 -0
  77. package/dist/core/action-contributors.js +28 -0
  78. package/dist/core/asset-ref.js +17 -2
  79. package/dist/core/asset-registry.js +12 -17
  80. package/dist/core/asset-serialize.js +88 -0
  81. package/dist/core/asset-spec.js +67 -1
  82. package/dist/core/common.js +182 -0
  83. package/dist/core/concurrent.js +25 -0
  84. package/dist/core/config-io.js +347 -0
  85. package/dist/core/config-migration.js +622 -0
  86. package/dist/core/config-schema.js +534 -0
  87. package/dist/core/config-sources.js +108 -0
  88. package/dist/core/config-types.js +4 -0
  89. package/dist/core/config-walker.js +337 -0
  90. package/dist/core/config.js +364 -981
  91. package/dist/core/errors.js +42 -20
  92. package/dist/core/events.js +91 -138
  93. package/dist/core/file-lock.js +104 -0
  94. package/dist/core/frontmatter.js +75 -8
  95. package/dist/core/lesson-lint.js +3 -0
  96. package/dist/core/markdown.js +20 -0
  97. package/dist/core/memory-belief.js +62 -0
  98. package/dist/core/memory-contradiction-detect.js +274 -0
  99. package/dist/core/memory-improve.js +806 -0
  100. package/dist/core/parse.js +158 -0
  101. package/dist/core/paths.js +280 -14
  102. package/dist/core/proposal-quality-validators.js +380 -0
  103. package/dist/core/proposal-validators.js +69 -0
  104. package/dist/core/proposals.js +512 -42
  105. package/dist/core/state-db.js +1068 -0
  106. package/dist/core/text-truncation.js +107 -0
  107. package/dist/core/time.js +54 -0
  108. package/dist/core/tty.js +59 -0
  109. package/dist/core/warn.js +64 -1
  110. package/dist/core/write-source.js +3 -0
  111. package/dist/indexer/db-backup.js +391 -0
  112. package/dist/indexer/db-search.js +178 -256
  113. package/dist/indexer/db.js +975 -103
  114. package/dist/indexer/ensure-index.js +64 -0
  115. package/dist/indexer/file-context.js +3 -0
  116. package/dist/indexer/graph-boost.js +376 -101
  117. package/dist/indexer/graph-db.js +391 -0
  118. package/dist/indexer/graph-dedup.js +95 -0
  119. package/dist/indexer/graph-extraction.js +550 -124
  120. package/dist/indexer/index-context.js +4 -0
  121. package/dist/indexer/indexer.js +523 -301
  122. package/dist/indexer/llm-cache.js +52 -0
  123. package/dist/indexer/manifest.js +3 -0
  124. package/dist/indexer/matchers.js +167 -160
  125. package/dist/indexer/memory-inference.js +152 -74
  126. package/dist/indexer/metadata-contributors.js +29 -0
  127. package/dist/indexer/metadata.js +275 -196
  128. package/dist/indexer/path-resolver.js +92 -0
  129. package/dist/indexer/project-context.js +192 -0
  130. package/dist/indexer/ranking-contributors.js +331 -0
  131. package/dist/indexer/ranking.js +81 -0
  132. package/dist/indexer/search-fields.js +5 -9
  133. package/dist/indexer/search-hit-enrichers.js +111 -0
  134. package/dist/indexer/search-source.js +44 -10
  135. package/dist/indexer/semantic-status.js +6 -17
  136. package/dist/indexer/staleness-detect.js +447 -0
  137. package/dist/indexer/usage-events.js +12 -9
  138. package/dist/indexer/walker.js +28 -0
  139. package/dist/integrations/agent/builders.js +135 -0
  140. package/dist/integrations/agent/config.js +122 -230
  141. package/dist/integrations/agent/detect.js +3 -0
  142. package/dist/integrations/agent/index.js +7 -13
  143. package/dist/integrations/agent/model-aliases.js +55 -0
  144. package/dist/integrations/agent/profiles.js +70 -5
  145. package/dist/integrations/agent/prompts.js +214 -80
  146. package/dist/integrations/agent/runner.js +151 -0
  147. package/dist/integrations/agent/sdk-runner.js +126 -0
  148. package/dist/integrations/agent/spawn.js +118 -23
  149. package/dist/integrations/github.js +3 -0
  150. package/dist/integrations/lockfile.js +32 -69
  151. package/dist/integrations/session-logs/index.js +69 -0
  152. package/dist/integrations/session-logs/inline-refs.js +35 -0
  153. package/dist/integrations/session-logs/pre-filter.js +152 -0
  154. package/dist/integrations/session-logs/providers/claude-code.js +282 -0
  155. package/dist/integrations/session-logs/providers/opencode.js +258 -0
  156. package/dist/integrations/session-logs/types.js +4 -0
  157. package/dist/llm/call-ai.js +62 -0
  158. package/dist/llm/client.js +77 -124
  159. package/dist/llm/embedder.js +20 -29
  160. package/dist/llm/embedders/cache.js +3 -7
  161. package/dist/llm/embedders/local.js +42 -1
  162. package/dist/llm/embedders/remote.js +20 -8
  163. package/dist/llm/embedders/types.js +3 -7
  164. package/dist/llm/feature-gate.js +95 -48
  165. package/dist/llm/graph-extract.js +676 -70
  166. package/dist/llm/index-passes.js +44 -29
  167. package/dist/llm/memory-infer.js +77 -71
  168. package/dist/llm/metadata-enhance.js +42 -29
  169. package/dist/llm/prompts/extract-session.md +80 -0
  170. package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
  171. package/dist/output/cli-hints-full.md +292 -0
  172. package/dist/output/cli-hints-short.md +66 -0
  173. package/dist/output/cli-hints.js +7 -320
  174. package/dist/output/context.js +60 -8
  175. package/dist/output/renderers.js +300 -257
  176. package/dist/output/shapes/curate.js +56 -0
  177. package/dist/output/shapes/distill.js +10 -0
  178. package/dist/output/shapes/env-list.js +19 -0
  179. package/dist/output/shapes/events.js +11 -0
  180. package/dist/output/shapes/helpers.js +424 -0
  181. package/dist/output/shapes/history.js +7 -0
  182. package/dist/output/shapes/passthrough.js +102 -0
  183. package/dist/output/shapes/proposal-accept.js +7 -0
  184. package/dist/output/shapes/proposal-diff.js +7 -0
  185. package/dist/output/shapes/proposal-list.js +7 -0
  186. package/dist/output/shapes/proposal-producer.js +11 -0
  187. package/dist/output/shapes/proposal-reject.js +7 -0
  188. package/dist/output/shapes/proposal-show.js +7 -0
  189. package/dist/output/shapes/registry-search.js +6 -0
  190. package/dist/output/shapes/registry.js +30 -0
  191. package/dist/output/shapes/search.js +6 -0
  192. package/dist/output/shapes/secret-list.js +19 -0
  193. package/dist/output/shapes/show.js +6 -0
  194. package/dist/output/shapes/vault-list.js +19 -0
  195. package/dist/output/shapes.js +51 -516
  196. package/dist/output/text/add.js +6 -0
  197. package/dist/output/text/clone.js +6 -0
  198. package/dist/output/text/config.js +6 -0
  199. package/dist/output/text/curate.js +6 -0
  200. package/dist/output/text/distill.js +7 -0
  201. package/dist/output/text/enable-disable.js +7 -0
  202. package/dist/output/text/events.js +10 -0
  203. package/dist/output/text/feedback.js +6 -0
  204. package/dist/output/text/helpers.js +1039 -0
  205. package/dist/output/text/history.js +7 -0
  206. package/dist/output/text/import.js +6 -0
  207. package/dist/output/text/index.js +6 -0
  208. package/dist/output/text/info.js +6 -0
  209. package/dist/output/text/init.js +6 -0
  210. package/dist/output/text/list.js +6 -0
  211. package/dist/output/text/proposal-producer.js +8 -0
  212. package/dist/output/text/proposal.js +11 -0
  213. package/dist/output/text/registry-commands.js +11 -0
  214. package/dist/output/text/registry.js +30 -0
  215. package/dist/output/text/remember.js +6 -0
  216. package/dist/output/text/remove.js +6 -0
  217. package/dist/output/text/save.js +6 -0
  218. package/dist/output/text/search.js +6 -0
  219. package/dist/output/text/show.js +6 -0
  220. package/dist/output/text/update.js +6 -0
  221. package/dist/output/text/upgrade.js +6 -0
  222. package/dist/output/text/vault.js +16 -0
  223. package/dist/output/text/wiki.js +15 -0
  224. package/dist/output/text/workflow.js +14 -0
  225. package/dist/output/text.js +44 -1092
  226. package/dist/registry/build-index.js +3 -0
  227. package/dist/registry/create-provider-registry.js +3 -0
  228. package/dist/registry/factory.js +4 -1
  229. package/dist/registry/origin-resolve.js +3 -0
  230. package/dist/registry/providers/index.js +3 -0
  231. package/dist/registry/providers/skills-sh.js +71 -50
  232. package/dist/registry/providers/static-index.js +53 -48
  233. package/dist/registry/providers/types.js +3 -24
  234. package/dist/registry/resolve.js +11 -16
  235. package/dist/registry/types.js +3 -0
  236. package/dist/scripts/migrate-storage.js +17750 -0
  237. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  238. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  239. package/dist/setup/detect.js +3 -0
  240. package/dist/setup/ripgrep-install.js +3 -0
  241. package/dist/setup/ripgrep-resolve.js +3 -0
  242. package/dist/setup/setup.js +775 -37
  243. package/dist/setup/steps.js +3 -15
  244. package/dist/sources/include.js +3 -0
  245. package/dist/sources/provider-factory.js +5 -12
  246. package/dist/sources/provider.js +3 -20
  247. package/dist/sources/providers/filesystem.js +19 -23
  248. package/dist/sources/providers/git.js +138 -21
  249. package/dist/sources/providers/index.js +3 -0
  250. package/dist/sources/providers/install-types.js +3 -13
  251. package/dist/sources/providers/npm.js +3 -4
  252. package/dist/sources/providers/provider-utils.js +3 -0
  253. package/dist/sources/providers/sync-from-ref.js +3 -11
  254. package/dist/sources/providers/tar-utils.js +3 -0
  255. package/dist/sources/providers/website.js +18 -22
  256. package/dist/sources/resolve.js +3 -0
  257. package/dist/sources/types.js +3 -0
  258. package/dist/sources/website-ingest.js +7 -0
  259. package/dist/tasks/backends/cron.js +203 -0
  260. package/dist/tasks/backends/exec-utils.js +28 -0
  261. package/dist/tasks/backends/index.js +24 -0
  262. package/dist/tasks/backends/launchd-template.xml +19 -0
  263. package/dist/tasks/backends/launchd.js +187 -0
  264. package/dist/tasks/backends/schtasks-template.xml +29 -0
  265. package/dist/tasks/backends/schtasks.js +215 -0
  266. package/dist/tasks/parser.js +211 -0
  267. package/dist/tasks/resolveAkmBin.js +87 -0
  268. package/dist/tasks/runner.js +458 -0
  269. package/dist/tasks/schedule.js +227 -0
  270. package/dist/tasks/schema.js +15 -0
  271. package/dist/tasks/validator.js +62 -0
  272. package/dist/version.js +3 -0
  273. package/dist/wiki/index-template.md +12 -0
  274. package/dist/wiki/ingest-workflow-template.md +54 -0
  275. package/dist/wiki/log-template.md +8 -0
  276. package/dist/wiki/schema-template.md +61 -0
  277. package/dist/wiki/wiki-templates.js +15 -0
  278. package/dist/wiki/wiki.js +13 -61
  279. package/dist/workflows/authoring.js +8 -25
  280. package/dist/workflows/cli.js +3 -0
  281. package/dist/workflows/db.js +140 -10
  282. package/dist/workflows/document-cache.js +3 -10
  283. package/dist/workflows/parser.js +3 -0
  284. package/dist/workflows/renderer.js +11 -3
  285. package/dist/workflows/runs.js +77 -92
  286. package/dist/workflows/schema.js +3 -0
  287. package/dist/workflows/scope-key.js +3 -0
  288. package/dist/workflows/validator.js +4 -8
  289. package/dist/workflows/workflow-template.md +24 -0
  290. package/docs/README.md +10 -2
  291. package/docs/data-and-telemetry.md +225 -0
  292. package/docs/migration/release-notes/0.7.0.md +1 -1
  293. package/docs/migration/release-notes/0.7.5.md +2 -2
  294. package/docs/migration/release-notes/0.8.0.md +48 -0
  295. package/docs/migration/v0.7-to-v0.8.md +1307 -0
  296. package/package.json +30 -12
  297. package/.github/LICENSE +0 -374
  298. package/dist/commands/install-audit.js +0 -381
  299. package/dist/commands/vault.js +0 -328
  300. 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
+ }