akm-cli 0.9.1 → 0.9.2-alpha.1

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 (341) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +3 -1
  3. package/SECURITY.md +1 -1
  4. package/STABILITY.md +1 -1
  5. package/dist/akm +2 -2
  6. package/dist/akm-migrate +2 -2
  7. package/dist/assets/hints/cli-hints-full.md +14 -9
  8. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
  9. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  10. package/dist/assets/models.json +35 -0
  11. package/dist/assets/tasks/core/extract.yml +6 -5
  12. package/dist/assets/tasks/core/improve.yml +6 -5
  13. package/dist/assets/tasks/core/index-refresh.yml +6 -5
  14. package/dist/assets/tasks/core/sync.yml +6 -5
  15. package/dist/assets/tasks/core/version-check.yml +6 -5
  16. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
  17. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
  18. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
  19. package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
  20. package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
  21. package/dist/cli/confirm.js +2 -2
  22. package/dist/cli/parse-args.js +3 -24
  23. package/dist/cli/retired-commands.js +1 -1
  24. package/dist/cli/shared.js +2 -2
  25. package/dist/cli.js +11 -9
  26. package/dist/commands/agent/agent-dispatch.js +55 -89
  27. package/dist/commands/agent/contribute-cli.js +12 -45
  28. package/dist/commands/command/builtin-action.js +32 -0
  29. package/dist/commands/command/command-cli.js +99 -0
  30. package/dist/commands/command/command-execution.js +308 -0
  31. package/dist/commands/command/execution-source-loader.js +176 -0
  32. package/dist/commands/command/portable-template.js +60 -0
  33. package/dist/commands/config-cli.js +10 -4
  34. package/dist/commands/env/env.js +4 -2
  35. package/dist/commands/feedback-cli.js +1 -1
  36. package/dist/commands/health/checks.js +241 -29
  37. package/dist/commands/health/html-report.js +0 -14
  38. package/dist/commands/health/report-view-model.js +0 -1
  39. package/dist/commands/health/surfaces.js +6 -7
  40. package/dist/commands/health/types.js +0 -2
  41. package/dist/commands/health.js +63 -18
  42. package/dist/commands/improve/collapse-detector.js +2 -2
  43. package/dist/commands/improve/consolidate.js +251 -214
  44. package/dist/commands/improve/distill/promote-memory.js +71 -34
  45. package/dist/commands/improve/distill/quality-gate.js +17 -5
  46. package/dist/commands/improve/distill.js +232 -155
  47. package/dist/commands/improve/eligibility.js +112 -79
  48. package/dist/commands/improve/execution.js +57 -0
  49. package/dist/commands/improve/extract-cli.js +5 -5
  50. package/dist/commands/improve/extract.js +521 -342
  51. package/dist/commands/improve/improve-strategies.js +43 -14
  52. package/dist/commands/improve/improve.js +249 -29
  53. package/dist/commands/improve/loop-stages.js +11 -17
  54. package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
  55. package/dist/commands/improve/outcome-loop.js +22 -38
  56. package/dist/commands/improve/planner.js +134 -0
  57. package/dist/commands/improve/preparation.js +730 -409
  58. package/dist/commands/improve/reflect.js +386 -223
  59. package/dist/commands/improve/run-context.js +3 -4
  60. package/dist/commands/improve/salience.js +6 -58
  61. package/dist/commands/improve/session-asset.js +12 -12
  62. package/dist/commands/lint/index.js +101 -29
  63. package/dist/commands/migrate-cli.js +11 -69
  64. package/dist/commands/migration-tool.js +6 -9
  65. package/dist/commands/models-cli.js +27 -0
  66. package/dist/commands/proposal/drain.js +258 -186
  67. package/dist/commands/proposal/proposal-cli.js +32 -10
  68. package/dist/commands/proposal/proposal.js +2 -5
  69. package/dist/commands/proposal/propose.js +192 -172
  70. package/dist/commands/proposal/repository.js +54 -91
  71. package/dist/commands/proposal/validators/proposal-validators.js +9 -7
  72. package/dist/commands/read/curate.js +10 -0
  73. package/dist/commands/read/registry-search.js +25 -9
  74. package/dist/commands/read/remember-cli.js +14 -2
  75. package/dist/commands/read/search.js +10 -4
  76. package/dist/commands/read/show.js +139 -153
  77. package/dist/commands/registry-cli.js +16 -7
  78. package/dist/commands/remember.js +33 -18
  79. package/dist/commands/sources/add-cli.js +19 -178
  80. package/dist/commands/sources/bundle-cli.js +15 -3
  81. package/dist/commands/sources/dangerous-env-audit.js +135 -0
  82. package/dist/commands/sources/info.js +2 -1
  83. package/dist/commands/sources/installed-stashes.js +835 -152
  84. package/dist/commands/sources/schema-repair.js +174 -95
  85. package/dist/commands/sources/self-update.js +30 -74
  86. package/dist/commands/sources/source-add.js +3 -5
  87. package/dist/commands/sources/sources-cli.js +2 -15
  88. package/dist/commands/sources/update-transaction.js +220 -0
  89. package/dist/commands/tasks/tasks-cli.js +3 -3
  90. package/dist/commands/tasks/tasks.js +736 -317
  91. package/dist/commands/workflow-cli.js +2 -2
  92. package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
  93. package/dist/core/adapter/adapters/akm-adapter.js +83 -34
  94. package/dist/core/adapter/adapters/akm-lint.js +54 -39
  95. package/dist/core/adapter/adapters/akm-metadata.js +45 -45
  96. package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
  97. package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
  98. package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
  99. package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
  100. package/dist/core/adapter/adapters/index.js +0 -9
  101. package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
  102. package/dist/core/adapter/adapters/okf-adapter.js +4 -0
  103. package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
  104. package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
  105. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
  106. package/dist/core/adapter/execution-source.js +308 -0
  107. package/dist/core/adapter/recognize-match.js +36 -13
  108. package/dist/core/adapter/registry.js +0 -9
  109. package/dist/core/asset/stash-meta.js +94 -4
  110. package/dist/core/common.js +6 -11
  111. package/dist/core/config/config-io.js +3 -3
  112. package/dist/core/config/config-schema.js +18 -40
  113. package/dist/core/config/config-sources.js +11 -21
  114. package/dist/core/config/config-walker.js +31 -13
  115. package/dist/core/config/config.js +21 -20
  116. package/dist/core/config/schema/engines.js +8 -7
  117. package/dist/core/config/schema/improve-processes.js +29 -5
  118. package/dist/core/config/schema/primitives.js +1 -23
  119. package/dist/core/config/schema/sources-bundles.js +13 -16
  120. package/dist/core/errors.js +2 -0
  121. package/dist/core/events.js +68 -32
  122. package/dist/core/extra-params.js +1 -0
  123. package/dist/core/improve-result.js +315 -0
  124. package/dist/core/lesson-lint.js +0 -6
  125. package/dist/core/maintenance-barrier.js +4 -4
  126. package/dist/core/network-policy.js +152 -0
  127. package/dist/core/paths.js +1 -1
  128. package/dist/core/recognition-util.js +4 -4
  129. package/dist/core/registry-url.js +456 -0
  130. package/dist/core/state/migrations.js +161 -47
  131. package/dist/core/state-db.js +453 -80
  132. package/dist/core/system-error.js +32 -0
  133. package/dist/core/time.js +2 -12
  134. package/dist/core/write-source.js +0 -18
  135. package/dist/execution/directory-identity.js +52 -0
  136. package/dist/execution/executable-identity.js +107 -0
  137. package/dist/execution/guarded-source.js +398 -0
  138. package/dist/execution/json.js +95 -0
  139. package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
  140. package/dist/execution/record.js +55 -0
  141. package/dist/execution/resolved-request.js +730 -0
  142. package/dist/execution/source.js +320 -0
  143. package/dist/indexer/bundle-identity-guard.js +5 -4
  144. package/dist/indexer/db/graph-db.js +33 -0
  145. package/dist/indexer/graph/graph-boost.js +3 -4
  146. package/dist/indexer/graph/graph-extraction.js +562 -373
  147. package/dist/indexer/index-written-assets.js +65 -34
  148. package/dist/indexer/indexer.js +443 -222
  149. package/dist/indexer/installations.js +6 -0
  150. package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
  151. package/dist/indexer/passes/memory-inference.js +227 -174
  152. package/dist/indexer/scan/doc-to-entry.js +7 -9
  153. package/dist/indexer/scan/drain-dir.js +51 -23
  154. package/dist/indexer/search/db-search.js +98 -22
  155. package/dist/indexer/search/search-fields.js +1 -1
  156. package/dist/indexer/search/search-hit-enrichers.js +1 -1
  157. package/dist/indexer/search/search-source.js +7 -16
  158. package/dist/indexer/search/semantic-status.js +10 -1
  159. package/dist/indexer/usage/show-usage.js +105 -0
  160. package/dist/indexer/usage/usage-events.js +7 -2
  161. package/dist/indexer/walk/matchers.js +40 -10
  162. package/dist/indexer/walk/path-resolver.js +5 -2
  163. package/dist/indexer/walk/walker.js +20 -2
  164. package/dist/integrations/agent/builder-shared.js +3 -6
  165. package/dist/integrations/agent/conversation-fallback.js +16 -0
  166. package/dist/integrations/agent/engine-resolution.js +87 -87
  167. package/dist/integrations/agent/execution-cascade.js +566 -0
  168. package/dist/integrations/agent/execution-definitions.js +211 -0
  169. package/dist/integrations/agent/execution-lowering.js +811 -0
  170. package/dist/integrations/agent/execution-preparation.js +67 -0
  171. package/dist/integrations/agent/index.js +0 -2
  172. package/dist/integrations/agent/inline-execution.js +74 -0
  173. package/dist/integrations/agent/model-map.js +515 -0
  174. package/dist/integrations/agent/persona-fallback.js +30 -0
  175. package/dist/integrations/agent/request-lowering.js +186 -0
  176. package/dist/integrations/agent/runner-dispatch.js +230 -37
  177. package/dist/integrations/agent/runner.js +12 -83
  178. package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
  179. package/dist/integrations/harnesses/aider/index.js +0 -1
  180. package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
  181. package/dist/integrations/harnesses/amazonq/index.js +0 -1
  182. package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
  183. package/dist/integrations/harnesses/claude/index.js +1 -5
  184. package/dist/integrations/harnesses/claude/session-log.js +3 -33
  185. package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
  186. package/dist/integrations/harnesses/codex/index.js +0 -1
  187. package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
  188. package/dist/integrations/harnesses/copilot/index.js +0 -1
  189. package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
  190. package/dist/integrations/harnesses/gemini/index.js +0 -1
  191. package/dist/integrations/harnesses/index.js +4 -44
  192. package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
  193. package/dist/integrations/harnesses/opencode/index.js +0 -2
  194. package/dist/integrations/harnesses/opencode/session-log.js +14 -204
  195. package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
  196. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
  197. package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
  198. package/dist/integrations/harnesses/openhands/index.js +0 -1
  199. package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
  200. package/dist/integrations/harnesses/pi/index.js +0 -1
  201. package/dist/integrations/harnesses/shared.js +0 -1
  202. package/dist/integrations/harnesses/types.js +1 -3
  203. package/dist/integrations/lockfile.js +82 -79
  204. package/dist/integrations/session-logs/index.js +6 -17
  205. package/dist/integrations/session-logs/provider-base.js +1 -29
  206. package/dist/llm/client.js +10 -5
  207. package/dist/llm/embedder.js +6 -7
  208. package/dist/llm/embedders/local.js +37 -88
  209. package/dist/llm/embedders/types.js +1 -1
  210. package/dist/llm/graph-extract.js +75 -50
  211. package/dist/llm/index-passes.js +43 -5
  212. package/dist/llm/memory-infer.js +8 -6
  213. package/dist/llm/metadata-enhance.js +5 -3
  214. package/dist/llm/structured-call.js +122 -25
  215. package/dist/output/format-exempt.js +1 -1
  216. package/dist/output/render-registry.js +0 -16
  217. package/dist/output/renderers.js +12 -7
  218. package/dist/output/shapes/curate.js +1 -0
  219. package/dist/output/shapes/helpers.js +10 -2
  220. package/dist/output/shapes/passthrough.js +2 -0
  221. package/dist/output/text/command-format.js +28 -32
  222. package/dist/output/text/health-format.js +1 -29
  223. package/dist/output/text/migrate.js +6 -56
  224. package/dist/output/text/proposal-format.js +16 -1
  225. package/dist/output/text/workflow-format.js +16 -0
  226. package/dist/registry/network.js +279 -0
  227. package/dist/registry/pinned-request-helper.js +247 -0
  228. package/dist/registry/pinned-transport.js +717 -0
  229. package/dist/registry/providers/skills-sh.js +18 -6
  230. package/dist/registry/providers/static-index.js +20 -7
  231. package/dist/registry/resolve.js +53 -28
  232. package/dist/scripts/akm-migrate-node.js +19122 -52218
  233. package/dist/scripts/akm-migrate.js +19064 -51567
  234. package/dist/setup/registry-stash-loader.js +64 -20
  235. package/dist/setup/semantic-assets.js +9 -34
  236. package/dist/setup/setup.js +12 -30
  237. package/dist/setup/source-identity.js +17 -0
  238. package/dist/setup/steps/sources.js +36 -15
  239. package/dist/setup/steps/tasks.js +39 -11
  240. package/dist/sources/providers/git-provider.js +3 -3
  241. package/dist/sources/providers/npm.js +2 -2
  242. package/dist/sources/providers/provider-utils.js +4 -3
  243. package/dist/sources/providers/website.js +11 -7
  244. package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
  245. package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
  246. package/dist/sources/website-url.js +73 -0
  247. package/dist/storage/engines/sqlite-migrations.js +81 -26
  248. package/dist/storage/managed-db.js +27 -24
  249. package/dist/storage/repositories/events-repository.js +3 -0
  250. package/dist/storage/repositories/index-connection.js +42 -10
  251. package/dist/storage/repositories/index-entries-repository.js +167 -207
  252. package/dist/storage/repositories/index-entry-mapper.js +8 -12
  253. package/dist/storage/repositories/index-entry-schema.js +255 -0
  254. package/dist/storage/repositories/index-fts-repository.js +10 -10
  255. package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
  256. package/dist/storage/repositories/index-meta-repository.js +0 -11
  257. package/dist/storage/repositories/index-schema.js +71 -342
  258. package/dist/storage/repositories/index-utility-repository.js +12 -17
  259. package/dist/storage/repositories/index-vec-repository.js +1 -1
  260. package/dist/storage/repositories/proposals-repository.js +4 -127
  261. package/dist/storage/repositories/registry-cache.js +2 -1
  262. package/dist/storage/repositories/task-history-repository.js +20 -40
  263. package/dist/storage/repositories/workflow-runs-repository.js +228 -129
  264. package/dist/storage/sqlite-read-snapshot.js +148 -0
  265. package/dist/tasks/backends/cron.js +170 -42
  266. package/dist/tasks/backends/index.js +1 -1
  267. package/dist/tasks/backends/launchd.js +787 -202
  268. package/dist/tasks/backends/schtasks.js +282 -83
  269. package/dist/tasks/embedded.js +7 -7
  270. package/dist/tasks/frozen-script.js +50 -0
  271. package/dist/tasks/resolve-akm-bin.js +5 -1
  272. package/dist/tasks/runner.js +239 -251
  273. package/dist/tasks/runtime-v3.js +281 -0
  274. package/dist/tasks/scheduler-binding.js +272 -0
  275. package/dist/tasks/scheduler-invocation.js +57 -43
  276. package/dist/tasks/scheduler-sync.js +654 -0
  277. package/dist/tasks/source-v3.js +752 -0
  278. package/dist/tasks/standalone-script-entry.js +5 -0
  279. package/dist/tasks/task-id.js +29 -0
  280. package/dist/workflows/authoring/authoring.js +15 -32
  281. package/dist/workflows/exec/dispatch-redaction.js +14 -8
  282. package/dist/workflows/exec/exec-unit.js +7 -28
  283. package/dist/workflows/exec/frozen-judge.js +57 -89
  284. package/dist/workflows/exec/lowering-notices.js +23 -0
  285. package/dist/workflows/exec/native-executor.js +301 -458
  286. package/dist/workflows/exec/param-secrets.js +4 -3
  287. package/dist/workflows/exec/run-workflow.js +26 -32
  288. package/dist/workflows/exec/step-work.js +105 -109
  289. package/dist/workflows/exec/unit-dispatch.js +103 -27
  290. package/dist/workflows/exec/unit-writer.js +3 -3
  291. package/dist/workflows/exec/worktree.js +2 -2
  292. package/dist/workflows/ir/compile.js +86 -72
  293. package/dist/workflows/ir/environment-v4.js +328 -0
  294. package/dist/workflows/ir/freeze-v4.js +122 -0
  295. package/dist/workflows/ir/plan-hash.js +13 -7
  296. package/dist/workflows/ir/schema-v4.js +525 -0
  297. package/dist/workflows/ir/schema.js +25 -284
  298. package/dist/workflows/ir/source-freeze-v4.js +506 -0
  299. package/dist/workflows/parser.js +27 -24
  300. package/dist/workflows/program/schema.js +1 -2
  301. package/dist/workflows/renderer.js +42 -29
  302. package/dist/workflows/resource-limits.js +4 -5
  303. package/dist/workflows/runtime/agent-identity.js +11 -13
  304. package/dist/workflows/runtime/plan-classifier.js +8 -8
  305. package/dist/workflows/runtime/runs.js +27 -43
  306. package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
  307. package/dist/workflows/source-files.js +373 -0
  308. package/dist/workflows/source-ir/compile.js +196 -0
  309. package/dist/workflows/source-ir/github-yaml.js +577 -0
  310. package/dist/workflows/source-ir/ordering.js +38 -0
  311. package/dist/workflows/source-ir/program.js +50 -0
  312. package/dist/workflows/source-ir/result.js +26 -0
  313. package/dist/workflows/source-ir/schema.js +772 -0
  314. package/dist/workflows/source-ir/semantics.js +242 -0
  315. package/dist/workflows/source-ir/uses.js +14 -0
  316. package/docs/README.md +2 -0
  317. package/docs/migration/README.md +3 -1
  318. package/docs/migration/release-notes/0.9.2.md +55 -0
  319. package/docs/migration/release-notes/README.md +5 -0
  320. package/docs/migration/v0.8-to-v0.9.md +76 -1077
  321. package/docs/migration/v0.9.0-troubleshooting.md +104 -516
  322. package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
  323. package/docs/reference/README.md +1 -0
  324. package/docs/reference/cli.md +230 -98
  325. package/docs/reference/configuration.md +153 -27
  326. package/docs/reference/data-and-telemetry.md +19 -1
  327. package/docs/reference/supported-formats.md +23 -3
  328. package/docs/reference/tasks.md +182 -0
  329. package/docs/reference/workflow-schema.md +91 -40
  330. package/docs/reference/workflows.md +33 -6
  331. package/package.json +10 -6
  332. package/schemas/akm-config.json +372 -216
  333. package/schemas/akm-task.json +324 -80
  334. package/schemas/akm-workflow.json +6 -9
  335. package/dist/core/migration-operation.js +0 -75
  336. package/dist/integrations/agent/model-aliases.js +0 -74
  337. package/dist/tasks/parser.js +0 -380
  338. package/dist/tasks/schema.js +0 -123
  339. package/dist/tasks/validator.js +0 -80
  340. package/dist/workflows/ir/freeze.js +0 -320
  341. package/dist/workflows/runtime/document-cache.js +0 -13
@@ -13,26 +13,71 @@
13
13
  * a lock entry is "managed" (installed from a registry and overwritten on
14
14
  * `akm bundle update`); a bundle with no lock is a plain filesystem/git/website source.
15
15
  */
16
+ import { createHash } from "node:crypto";
16
17
  import fs from "node:fs";
17
18
  import path from "node:path";
18
19
  import { isWithin, resolveStashDir } from "../../core/common.js";
19
- import { getSources, loadConfig } from "../../core/config/config.js";
20
- import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
20
+ import { acquireConfigReadFence, bundleComponentConfig, getSources, loadConfig } from "../../core/config/config.js";
21
+ import { AkmError, ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
21
22
  import { isPathAbsent } from "../../core/path-access.js";
22
- import { getDbPath } from "../../core/paths.js";
23
+ import { getDbPath, getRegistryCacheDir } from "../../core/paths.js";
24
+ import { beginImmediateTransaction, getStateDbPath, openStateDatabase } from "../../core/state-db.js";
23
25
  import { warn } from "../../core/warn.js";
26
+ import { resolveGitContentRoot } from "../../core/write-source.js";
24
27
  import { withAssetMutationLease } from "../../indexer/index-writer-lock.js";
25
28
  import { akmIndex } from "../../indexer/indexer.js";
26
- import { compareAndSwapLockfile, readLockfile } from "../../integrations/lockfile.js";
29
+ import { compareAndSwapLockfileSnapshot, publishLockfileUpdate, readLockfile, readLockfileForUpdate, } from "../../integrations/lockfile.js";
27
30
  import { parseRegistryRef } from "../../registry/resolve.js";
28
31
  import { sha256Hex } from "../../runtime.js";
29
- import { parseGitRepoUrl, syncMirroredRepo } from "../../sources/providers/git.js";
32
+ import { getCachePaths, parseGitRepoUrl, runGit, syncMirroredRepo } from "../../sources/providers/git.js";
30
33
  import { syncFromRef } from "../../sources/providers/sync-from-ref.js";
31
- import { ensureWebsiteMirror, shouldAllowPrivateWebsiteUrlForTests, } from "../../sources/snapshot-fetchers/website-ingest.js";
32
- import { closeDatabase, openReadonlyExistingDatabase } from "../../storage/repositories/index-connection.js";
34
+ import { createWebsiteProvider } from "../../sources/providers/website.js";
35
+ import { storeSecretResolver } from "../../sources/snapshot-fetchers/secret-seam.js";
36
+ import { ensureWebsiteMirror } from "../../sources/snapshot-fetchers/website-ingest.js";
37
+ import { getWebsiteCachePaths } from "../../sources/website-url.js";
38
+ import { closeDatabase, openIndexDatabase, openReadonlyExistingDatabase, } from "../../storage/repositories/index-connection.js";
33
39
  import { getAllEntries } from "../../storage/repositories/index-entries-repository.js";
40
+ import { auditStashForDangerousKeys, scanStashForDangerousKeys } from "./dangerous-env-audit.js";
34
41
  import { removeInstalledRegistryEntry } from "./source-add.js";
35
42
  import { removeStash } from "./source-manage.js";
43
+ import { cleanupStagingParent, createStagingParent, prepareDirectoryPublication, prepareWritableGitPublication, } from "./update-transaction.js";
44
+ function canonicalJson(value) {
45
+ if (Array.isArray(value))
46
+ return `[${value.map(canonicalJson).join(",")}]`;
47
+ if (value && typeof value === "object") {
48
+ const entries = Object.entries(value)
49
+ .filter(([, entry]) => entry !== undefined)
50
+ .sort(([left], [right]) => left.localeCompare(right));
51
+ return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
52
+ }
53
+ return JSON.stringify(value);
54
+ }
55
+ function bundleAuditGeneration(entry) {
56
+ return sha256Hex(canonicalJson(entry ?? null));
57
+ }
58
+ function bundleAuditFence(config, id) {
59
+ const bundle = config.bundles?.[id];
60
+ if (!bundle)
61
+ throw new ConfigError(`Source "${id}" disappeared before update staging.`);
62
+ return {
63
+ componentRoot: bundleComponentConfig(bundle)?.root ?? ".",
64
+ generation: bundleAuditGeneration(bundle),
65
+ writable: bundleComponentConfig(bundle)?.writable ?? bundle.writable === true,
66
+ };
67
+ }
68
+ function assertBundleAuditFence(config, id, expectedGeneration) {
69
+ const current = config.bundles?.[id];
70
+ if (bundleAuditGeneration(current) !== expectedGeneration) {
71
+ throw new ConfigError(`Source "${id}" config changed after its staged bytes were audited; refusing to publish without re-staging and re-auditing.`);
72
+ }
73
+ }
74
+ function currentPlainSource(config, id, type) {
75
+ const source = getSources(config).find((candidate) => candidate.name === id && candidate.type === type);
76
+ if (!source) {
77
+ throw new ConfigError(`Source "${id}" changed or disappeared before update staging; retry to re-resolve it.`);
78
+ }
79
+ return source;
80
+ }
36
81
  /** Enumerate the registry-managed installs (lock-backed bundles) in a config. */
37
82
  function listManagedInstalls(config) {
38
83
  const bundles = config.bundles ?? {};
@@ -43,6 +88,7 @@ function listManagedInstalls(config) {
43
88
  if (!lock)
44
89
  continue; // only lock-backed bundles are registry-managed
45
90
  const componentWritable = Object.values(bundle.components ?? {})[0]?.writable;
91
+ const componentRoot = bundleComponentConfig(bundle)?.root ?? ".";
46
92
  out.push({
47
93
  bundleKey: key,
48
94
  installId: bundle.registryId ?? key,
@@ -52,9 +98,9 @@ function listManagedInstalls(config) {
52
98
  resolvedVersion: lock.resolvedVersion,
53
99
  resolvedRevision: lock.resolvedRevision,
54
100
  writable: componentWritable ?? bundle.writable === true,
55
- requiredRoots: lock.localRoot
56
- ? Object.values(bundle.components ?? {}).map((component) => path.resolve(lock.localRoot, component.root ?? "."))
57
- : [],
101
+ componentRoot,
102
+ requiredRoots: lock.localRoot ? [path.resolve(lock.localRoot, componentRoot)] : [],
103
+ auditConfigGeneration: bundleAuditGeneration(bundle),
58
104
  });
59
105
  }
60
106
  return out;
@@ -272,10 +318,26 @@ export async function akmRemove(input) {
272
318
  },
273
319
  };
274
320
  }
275
- // ── akmUpdate helpers ────────────────────────────────────────────────────────
276
- /** Build a standard UpdateResponse summary block from the current config and index run. */
277
- async function buildUpdateResponse(stashDir, target, all, processed, opts) {
278
- const index = opts?.index ?? (await akmIndex({ stashDir, ...(opts?.full ? { full: true } : {}) }));
321
+ /** Read the current index generation without creating or hydrating anything. */
322
+ function readCurrentIndexSummary() {
323
+ const db = openReadonlyExistingDatabase(getDbPath());
324
+ if (!db)
325
+ return { mode: "incremental", totalEntries: 0, directoriesScanned: 0, directoriesSkipped: 0 };
326
+ try {
327
+ return {
328
+ mode: "incremental",
329
+ totalEntries: getAllEntries(db).length,
330
+ directoriesScanned: 0,
331
+ directoriesSkipped: 0,
332
+ };
333
+ }
334
+ finally {
335
+ closeDatabase(db);
336
+ }
337
+ }
338
+ /** Build a standard UpdateResponse summary block from a committed or unchanged index generation. */
339
+ function buildUpdateResponse(stashDir, target, all, processed, opts) {
340
+ const index = opts.index;
279
341
  const finalConfig = loadConfig();
280
342
  return {
281
343
  schemaVersion: 1,
@@ -296,34 +358,540 @@ async function buildUpdateResponse(stashDir, target, all, processed, opts) {
296
358
  },
297
359
  };
298
360
  }
299
- /**
300
- * Sync a git-mirrored (plain) source in place. Returns the {@link UpdatePlainSyncedItem}
301
- * record instead of building the full response, so `--all` can batch several of
302
- * these into one response alongside managed installs (R-015).
303
- */
304
- async function syncGitPlainSource(gitSource) {
305
- await syncMirroredRepo(gitSource, { force: true, writable: gitSource.writable === true });
306
- return { id: gitSource.name ?? gitSource.url ?? "", kind: "git", ref: gitSource.url ?? "" };
307
- }
308
- /** Re-crawl a website (plain) source in place. See {@link syncGitPlainSource}. */
309
- async function syncWebsitePlainSource(websiteSource) {
310
- // TODO: full incremental re-crawl with delta tracking (#19)
311
- await ensureWebsiteMirror(websiteSource, {
312
- requireStashDir: true,
313
- force: true,
314
- ...(shouldAllowPrivateWebsiteUrlForTests(websiteSource.url ?? "") ? { allowPrivateHosts: true } : {}),
361
+ let updateTransactionHookForTests;
362
+ /** TEST-ONLY. Inject faults at source-update transaction boundaries. */
363
+ export function _setUpdateTransactionHookForTests(hook) {
364
+ updateTransactionHookForTests = hook;
365
+ }
366
+ function updateTransactionHook(point, id, context) {
367
+ updateTransactionHookForTests?.(point, id, context);
368
+ }
369
+ function gitHead(repoDir) {
370
+ const result = runGit(["-C", repoDir, "rev-parse", "HEAD"]);
371
+ return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : undefined;
372
+ }
373
+ function invalidWritableGitPublication(repoDir) {
374
+ return {
375
+ publish() {
376
+ throw new ConfigError(`Writable Git source at ${repoDir} is not a valid checkout; refusing to replace its live directory.`);
377
+ },
378
+ rollback() { },
379
+ commit() { },
380
+ };
381
+ }
382
+ function writableGitPublication(opts) {
383
+ const auditedTargetHead = opts.auditedTargetHead ?? gitHead(opts.stagedRepo);
384
+ if (!opts.expectedOldHead || !auditedTargetHead)
385
+ return invalidWritableGitPublication(opts.liveRepo);
386
+ return prepareWritableGitPublication({
387
+ stagedRepo: opts.stagedRepo,
388
+ liveRepo: opts.liveRepo,
389
+ expectedOldHead: opts.expectedOldHead,
390
+ auditedTargetHead,
391
+ });
392
+ }
393
+ const UPDATE_STATE_SCHEMA = "akm_update_state";
394
+ function sqliteStringLiteral(value) {
395
+ return `'${value.replaceAll("'", "''")}'`;
396
+ }
397
+ function openUnifiedUpdateTransaction() {
398
+ // Keep canonical state activity registered for the entire attached
399
+ // transaction so maintenance/migration cannot replace state.db underneath
400
+ // the borrowed index connection.
401
+ const stateActivity = openStateDatabase();
402
+ try {
403
+ const statePath = getStateDbPath();
404
+ const db = openIndexDatabase(undefined, {
405
+ beforeSchema: (candidate) => {
406
+ candidate.exec(`ATTACH DATABASE ${sqliteStringLiteral(statePath)} AS "${UPDATE_STATE_SCHEMA}"`);
407
+ // openIndexDatabase invokes this before ensureSchema, so the outer
408
+ // transaction begins before the first update-owned index mutation.
409
+ beginImmediateTransaction(candidate);
410
+ },
411
+ });
412
+ return {
413
+ db,
414
+ stateActivity,
415
+ deferred: { db, stateSchema: UPDATE_STATE_SCHEMA },
416
+ };
417
+ }
418
+ catch (error) {
419
+ stateActivity.close();
420
+ throw error;
421
+ }
422
+ }
423
+ function rollbackUnifiedUpdateTransaction(transaction) {
424
+ if (!transaction.db.inTransaction)
425
+ return undefined;
426
+ try {
427
+ transaction.db.exec("ROLLBACK");
428
+ return undefined;
429
+ }
430
+ catch (error) {
431
+ return error;
432
+ }
433
+ }
434
+ function commitUnifiedUpdateTransaction(transaction) {
435
+ if (!transaction.db.inTransaction) {
436
+ throw new Error("Source update index returned without its active unified index/state transaction.");
437
+ }
438
+ transaction.db.exec("COMMIT");
439
+ }
440
+ function closeUnifiedUpdateTransaction(transaction, committed) {
441
+ let closeError = rollbackUnifiedUpdateTransaction(transaction);
442
+ try {
443
+ transaction.db.close();
444
+ }
445
+ catch (error) {
446
+ closeError ??= error;
447
+ }
448
+ try {
449
+ transaction.stateActivity.close();
450
+ }
451
+ catch (error) {
452
+ closeError ??= error;
453
+ }
454
+ if (closeError) {
455
+ warn(committed
456
+ ? `[akm bundle update] committed, but closing its database handles failed: ${String(closeError)}`
457
+ : `[akm bundle update] rolled back, but closing its database handles failed: ${String(closeError)}`);
458
+ }
459
+ }
460
+ function pathAtOrBelow(candidate, root) {
461
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
462
+ return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
463
+ }
464
+ function remapStagedPath(candidate, stagedRoot, liveRoot) {
465
+ if (!pathAtOrBelow(candidate, stagedRoot))
466
+ return candidate;
467
+ return path.join(liveRoot, path.relative(stagedRoot, candidate));
468
+ }
469
+ function dangerousFindingGeneration(findings) {
470
+ return JSON.stringify(findings
471
+ .map(({ envRef, keyName, relPath }) => ({ envRef, keyName, relPath }))
472
+ .sort((left, right) => `${left.relPath}\0${left.envRef}\0${left.keyName}`.localeCompare(`${right.relPath}\0${right.envRef}\0${right.keyName}`)));
473
+ }
474
+ async function contentGeneration(root) {
475
+ const hash = createHash("sha256");
476
+ const resolvedRoot = path.resolve(root);
477
+ const walk = async (directory) => {
478
+ const entries = fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
479
+ for (const entry of entries) {
480
+ if (entry.name === ".git")
481
+ continue;
482
+ const absolute = path.join(directory, entry.name);
483
+ const relative = path.relative(resolvedRoot, absolute).replaceAll(path.sep, "/");
484
+ const before = fs.lstatSync(absolute);
485
+ if (before.isDirectory()) {
486
+ hash.update(`D\0${relative}\0${before.mode & 0o777}\0`);
487
+ await walk(absolute);
488
+ }
489
+ else if (before.isSymbolicLink()) {
490
+ hash.update(`L\0${relative}\0${fs.readlinkSync(absolute)}\0`);
491
+ }
492
+ else if (before.isFile()) {
493
+ hash.update(`F\0${relative}\0${before.mode & 0o777}\0${before.size}\0`);
494
+ for await (const chunk of fs.createReadStream(absolute))
495
+ hash.update(chunk);
496
+ const after = fs.lstatSync(absolute);
497
+ if (after.ino !== before.ino || after.size !== before.size || after.mtimeMs !== before.mtimeMs) {
498
+ throw new ConfigError(`Source content changed while ${relative} was being audited; retry the update.`);
499
+ }
500
+ hash.update("\0");
501
+ }
502
+ else {
503
+ throw new ConfigError(`Unsupported filesystem entry ${absolute}; refusing to audit an unstable source tree.`);
504
+ }
505
+ }
506
+ };
507
+ if (!fs.statSync(resolvedRoot).isDirectory())
508
+ throw new ConfigError(`Expected ${resolvedRoot} to be a directory.`);
509
+ await walk(resolvedRoot);
510
+ return hash.digest("hex");
511
+ }
512
+ async function verifyPublishedAudit(prepared, id) {
513
+ const liveRoot = prepared.publishedAuditRoot;
514
+ if (!liveRoot)
515
+ return;
516
+ const resolveLiveAuditRoot = () => {
517
+ const physicalRoot = canonicalWritablePath(liveRoot, `published component root for "${id}"`);
518
+ const containmentRoot = prepared.publishedAuditContainmentRoot;
519
+ if (containmentRoot && !pathAtOrBelow(physicalRoot, containmentRoot)) {
520
+ throw new ConfigError(`Published component root ${liveRoot} resolves outside its physical checkout at ${containmentRoot}.`, "DANGEROUS_ENV_AUDIT_FAILED");
521
+ }
522
+ const expected = prepared.publishedAuditExpectedPhysicalRoot;
523
+ if (expected && path.resolve(physicalRoot) !== path.resolve(expected)) {
524
+ throw new ConfigError(`Published component root ${liveRoot} resolves to ${physicalRoot}, not its audited staged target ${expected}.`, "DANGEROUS_ENV_AUDIT_FAILED");
525
+ }
526
+ return physicalRoot;
527
+ };
528
+ const beforeRoot = resolveLiveAuditRoot();
529
+ const beforeAudit = await contentGeneration(beforeRoot);
530
+ let findings;
531
+ try {
532
+ const scanRoot = resolveLiveAuditRoot();
533
+ if (scanRoot !== beforeRoot)
534
+ throw new Error("configured component symlink changed during re-audit");
535
+ findings = scanStashForDangerousKeys(scanRoot);
536
+ }
537
+ catch (error) {
538
+ throw new ConfigError(`Dangerous environment-key re-audit failed after materializing "${id}"; rolling the update back. ${error instanceof Error ? error.message : String(error)}`, "DANGEROUS_ENV_AUDIT_FAILED");
539
+ }
540
+ const afterRoot = resolveLiveAuditRoot();
541
+ const afterAudit = await contentGeneration(afterRoot);
542
+ if (afterRoot !== beforeRoot ||
543
+ beforeAudit !== prepared.auditedContentGeneration ||
544
+ afterAudit !== prepared.auditedContentGeneration ||
545
+ dangerousFindingGeneration(findings) !== prepared.approvedDangerousFindings) {
546
+ throw new ConfigError(`Writable source "${id}" changed while its audited Git target was materialized; refusing to activate unaudited bytes.`, "DANGEROUS_ENV_AUDIT_FAILED");
547
+ }
548
+ }
549
+ async function auditPreparedUpdate(prepared, id, ref, allowInsecure) {
550
+ updateTransactionHook("staged", id);
551
+ const decision = await auditStashForDangerousKeys({
552
+ stashRoot: prepared.auditRoot,
553
+ ref,
554
+ allowDangerousKeys: allowInsecure,
555
+ isTTY: process.stdin.isTTY === true,
556
+ operation: "update",
557
+ renderBlockedError: false,
315
558
  });
316
- return { id: websiteSource.name ?? websiteSource.url ?? "", kind: "website", ref: websiteSource.url ?? "" };
559
+ if (decision.blocked)
560
+ throw new NotFoundError(decision.error, decision.code);
561
+ prepared.approvedDangerousFindings = dangerousFindingGeneration(decision.findings);
562
+ prepared.auditedContentGeneration = await contentGeneration(prepared.auditRoot);
563
+ updateTransactionHook("audited", id);
564
+ }
565
+ function canonicalWritablePath(candidate, description) {
566
+ try {
567
+ return fs.realpathSync(candidate);
568
+ }
569
+ catch (error) {
570
+ throw new ConfigError(`Cannot resolve ${description} at ${candidate}; refusing writable Git update. ${error instanceof Error ? error.message : String(error)}`);
571
+ }
572
+ }
573
+ function containedWritablePath(candidate, physicalRepo, description) {
574
+ const physicalCandidate = canonicalWritablePath(candidate, description);
575
+ if (!pathAtOrBelow(physicalCandidate, physicalRepo)) {
576
+ throw new ConfigError(`${description} ${candidate} resolves outside its physical checkout at ${physicalRepo}.`);
577
+ }
578
+ return physicalCandidate;
579
+ }
580
+ function prepareWritableManagedUpdate(managed, force) {
581
+ const physicalLocalRoot = canonicalWritablePath(managed.localRoot, "managed content root");
582
+ const topLevel = runGit(["-C", physicalLocalRoot, "rev-parse", "--show-toplevel"]);
583
+ // Production writable installs are Git checkouts. Falling back to the
584
+ // locked root keeps the provider seam testable; the real Git provider still
585
+ // refuses a copied non-checkout before publication.
586
+ const liveRepo = topLevel.status === 0 && topLevel.stdout.trim()
587
+ ? canonicalWritablePath(topLevel.stdout.trim(), "managed Git top-level")
588
+ : physicalLocalRoot;
589
+ if (!pathAtOrBelow(physicalLocalRoot, liveRepo)) {
590
+ throw new ConfigError(`Writable Git content root ${managed.localRoot} resolves outside its physical checkout at ${liveRepo}.`);
591
+ }
592
+ // Keep the configured component path as a lexical identity relative to the
593
+ // content root. Resolving it to today's symlink target here would pin the old
594
+ // target and miss an upstream commit that retargets the symlink.
595
+ const lexicalLocalRoot = path.resolve(managed.localRoot);
596
+ const configuredLiveRoots = managed.requiredRoots.map((root) => {
597
+ const lexicalRoot = path.resolve(root);
598
+ if (!pathAtOrBelow(lexicalRoot, lexicalLocalRoot)) {
599
+ throw new ConfigError(`Configured writable Git component root ${lexicalRoot} escapes content root ${lexicalLocalRoot}.`);
600
+ }
601
+ return path.resolve(physicalLocalRoot, path.relative(lexicalLocalRoot, lexicalRoot));
602
+ });
603
+ const physicalRequiredRoots = configuredLiveRoots.map((root) => containedWritablePath(root, liveRepo, `Configured component root for "${managed.installId}"`));
604
+ // The Git provider must preserve ordinary component directories, but not a
605
+ // symlink's current physical target. The configured lexical path is audited
606
+ // again after sync, so retaining the old target here would reject a valid
607
+ // upstream retarget before the new target can reach that audit.
608
+ const preservedPhysicalRequiredRoots = physicalRequiredRoots.filter((root, index) => configuredLiveRoots[index] === root);
609
+ const expectedOldHead = gitHead(liveRepo);
610
+ const stagingParent = createStagingParent(path.dirname(liveRepo));
611
+ const stagedRepo = path.join(stagingParent, path.basename(liveRepo));
612
+ fs.cpSync(liveRepo, stagedRepo, { recursive: true, preserveTimestamps: true, verbatimSymlinks: true });
613
+ const stagedContentRoot = remapStagedPath(physicalLocalRoot, liveRepo, stagedRepo);
614
+ const stagedPhysicalRequiredRoots = preservedPhysicalRequiredRoots.map((root) => remapStagedPath(root, liveRepo, stagedRepo));
615
+ const stagedConfiguredRoots = configuredLiveRoots.map((root) => remapStagedPath(root, liveRepo, stagedRepo));
616
+ return syncFromRef(managed.ref, {
617
+ force,
618
+ writable: true,
619
+ writableRoot: stagedContentRoot,
620
+ ...(stagedPhysicalRequiredRoots.length > 0 ? { writableRequiredRoots: stagedPhysicalRequiredRoots } : {}),
621
+ })
622
+ .then((staged) => {
623
+ const auditedTargetHead = gitHead(stagedRepo);
624
+ const physicalStagedRepo = canonicalWritablePath(stagedRepo, "staged managed Git checkout");
625
+ const auditedConfiguredRoots = stagedConfiguredRoots.map((root) => containedWritablePath(root, physicalStagedRepo, `Post-sync component root for "${managed.installId}"`));
626
+ const expectedPublishedRoots = auditedConfiguredRoots.map((root) => path.join(liveRepo, path.relative(physicalStagedRepo, root)));
627
+ const synced = {
628
+ ...staged,
629
+ resolvedRevision: auditedTargetHead ?? staged.resolvedRevision,
630
+ contentDir: remapStagedPath(staged.contentDir, stagedRepo, liveRepo),
631
+ cacheDir: remapStagedPath(staged.cacheDir, stagedRepo, liveRepo),
632
+ extractedDir: remapStagedPath(staged.extractedDir, stagedRepo, liveRepo),
633
+ };
634
+ return {
635
+ synced,
636
+ auditRoot: auditedConfiguredRoots[0] ?? staged.contentDir,
637
+ publishedAuditRoot: configuredLiveRoots[0] ?? synced.contentDir,
638
+ publishedAuditContainmentRoot: liveRepo,
639
+ publishedAuditExpectedPhysicalRoot: expectedPublishedRoots[0] ?? synced.contentDir,
640
+ publication: writableGitPublication({ stagedRepo, liveRepo, expectedOldHead, auditedTargetHead }),
641
+ cleanup: () => cleanupStagingParent(stagingParent),
642
+ };
643
+ })
644
+ .catch((error) => {
645
+ cleanupStagingParent(stagingParent);
646
+ throw error;
647
+ });
648
+ }
649
+ async function prepareManagedUpdate(managed, force) {
650
+ if (managed.writable && managed.localRoot)
651
+ return prepareWritableManagedUpdate(managed, force);
652
+ if (managed.source === "local") {
653
+ const synced = await syncFromRef(managed.ref, { force, writable: managed.writable });
654
+ return {
655
+ synced,
656
+ auditRoot: resolveComponentAuditRoot(synced.contentDir, managed.componentRoot, managed.installId),
657
+ cleanup: () => { },
658
+ };
659
+ }
660
+ const canonicalCacheRoot = getRegistryCacheDir();
661
+ const stagingParent = createStagingParent(canonicalCacheRoot);
662
+ try {
663
+ const staged = await syncFromRef(managed.ref, {
664
+ force,
665
+ writable: false,
666
+ cacheRootDir: stagingParent,
667
+ });
668
+ if (!pathAtOrBelow(staged.cacheDir, stagingParent)) {
669
+ // Test/provider seams may return an already-isolated candidate. It remains
670
+ // invisible until the lock points at it, so no directory promotion is needed.
671
+ return {
672
+ synced: staged,
673
+ auditRoot: resolveComponentAuditRoot(staged.contentDir, managed.componentRoot, managed.installId),
674
+ cleanup: () => cleanupStagingParent(stagingParent),
675
+ };
676
+ }
677
+ const liveCacheDir = path.join(canonicalCacheRoot, path.relative(stagingParent, staged.cacheDir));
678
+ const synced = {
679
+ ...staged,
680
+ contentDir: remapStagedPath(staged.contentDir, staged.cacheDir, liveCacheDir),
681
+ cacheDir: liveCacheDir,
682
+ extractedDir: remapStagedPath(staged.extractedDir, staged.cacheDir, liveCacheDir),
683
+ };
684
+ return {
685
+ synced,
686
+ auditRoot: resolveComponentAuditRoot(staged.contentDir, managed.componentRoot, managed.installId),
687
+ publication: prepareDirectoryPublication(staged.cacheDir, liveCacheDir),
688
+ cleanup: () => cleanupStagingParent(stagingParent),
689
+ };
690
+ }
691
+ catch (error) {
692
+ cleanupStagingParent(stagingParent);
693
+ throw error;
694
+ }
695
+ }
696
+ function resolveComponentAuditRoot(contentRoot, componentRoot, id) {
697
+ const resolved = path.resolve(contentRoot, componentRoot);
698
+ if (!pathAtOrBelow(resolved, contentRoot)) {
699
+ throw new ConfigError(`Component root "${componentRoot}" escapes staged bundle "${id}".`);
700
+ }
701
+ return resolved;
702
+ }
703
+ async function publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowInsecure, full, expectedConfigGeneration) {
704
+ try {
705
+ await auditPreparedUpdate(prepared, id, ref, allowInsecure);
706
+ return await withAssetMutationLease("source-update", async () => {
707
+ let transaction;
708
+ let releaseConfigFence;
709
+ let contentPublished = false;
710
+ let committed = false;
711
+ try {
712
+ const configFence = acquireConfigReadFence();
713
+ releaseConfigFence = configFence.release;
714
+ assertBundleAuditFence(configFence.config, id, expectedConfigGeneration);
715
+ updateTransactionHook("fenced", id);
716
+ if (prepared.publication) {
717
+ prepared.publication.publish();
718
+ contentPublished = true;
719
+ }
720
+ await verifyPublishedAudit(prepared, id);
721
+ updateTransactionHook("published", id);
722
+ updateTransactionHook("before-index", id);
723
+ transaction = openUnifiedUpdateTransaction();
724
+ const index = await akmIndex({
725
+ stashDir,
726
+ ...(full ? { full: true } : {}),
727
+ hydrateSources: false,
728
+ persistDetectedAdapters: false,
729
+ deferredUpdateTransaction: transaction.deferred,
730
+ });
731
+ updateTransactionHook("indexed", id, { db: transaction.db });
732
+ // Re-fence the exact published worktree after the potentially long
733
+ // index pass and immediately before the coordinator commit. This
734
+ // catches cooperating or observable external writes that land after
735
+ // the first post-materialization audit.
736
+ await verifyPublishedAudit(prepared, id);
737
+ updateTransactionHook("before-commit", id, { db: transaction.db });
738
+ commitUnifiedUpdateTransaction(transaction);
739
+ committed = true;
740
+ try {
741
+ transaction.deferred.afterCommit?.();
742
+ }
743
+ catch (error) {
744
+ warn(`[akm bundle update] committed, but semantic status refresh failed: ${String(error)}`);
745
+ }
746
+ prepared.publication?.commit();
747
+ return index;
748
+ }
749
+ catch (error) {
750
+ let recoveryError = transaction ? rollbackUnifiedUpdateTransaction(transaction) : undefined;
751
+ try {
752
+ if (contentPublished)
753
+ prepared.publication?.rollback();
754
+ }
755
+ catch (rollbackError) {
756
+ recoveryError ??= rollbackError;
757
+ }
758
+ if (recoveryError)
759
+ throw recoveryError;
760
+ throw error;
761
+ }
762
+ finally {
763
+ try {
764
+ if (transaction)
765
+ closeUnifiedUpdateTransaction(transaction, committed);
766
+ }
767
+ finally {
768
+ releaseConfigFence?.();
769
+ }
770
+ }
771
+ });
772
+ }
773
+ finally {
774
+ prepared.cleanup();
775
+ }
776
+ }
777
+ async function prepareGitPlainUpdate(gitSource, componentRoot, writable) {
778
+ if (!gitSource.url)
779
+ throw new ConfigError(`Git source "${gitSource.name ?? "git"}" has no URL.`);
780
+ const repo = parseGitRepoUrl(gitSource.url);
781
+ const livePaths = getCachePaths(repo.canonicalUrl);
782
+ const stagingParent = createStagingParent(path.dirname(livePaths.rootDir));
783
+ const stagedPaths = getCachePaths(repo.canonicalUrl, stagingParent);
784
+ try {
785
+ const expectedOldHead = writable ? gitHead(livePaths.repoDir) : undefined;
786
+ if (writable && fs.existsSync(livePaths.rootDir)) {
787
+ fs.cpSync(livePaths.rootDir, stagedPaths.rootDir, { recursive: true, preserveTimestamps: true });
788
+ }
789
+ const staged = await syncMirroredRepo(gitSource, {
790
+ force: true,
791
+ writable,
792
+ cacheRootDir: stagingParent,
793
+ });
794
+ const configuredStagedAuditRoot = resolveComponentAuditRoot(resolveGitContentRoot(staged.contentDir), componentRoot, gitSource.name ?? "git");
795
+ if (!pathAtOrBelow(staged.cacheDir, stagingParent)) {
796
+ if (pathAtOrBelow(configuredStagedAuditRoot, livePaths.rootDir)) {
797
+ throw new ConfigError(`Git provider for "${gitSource.name ?? gitSource.url}" wrote into the active cache instead of the update staging root; refusing to index it.`);
798
+ }
799
+ // A provider test seam may hand back an already-isolated candidate.
800
+ // Audit it, but do not move an unowned directory into the canonical
801
+ // cache. Real providers honor cacheRootDir and use the promotion below.
802
+ return {
803
+ auditRoot: configuredStagedAuditRoot,
804
+ cleanup: () => cleanupStagingParent(stagingParent),
805
+ };
806
+ }
807
+ let auditRoot = configuredStagedAuditRoot;
808
+ let publishedAuditRoot;
809
+ let publishedAuditContainmentRoot;
810
+ let publishedAuditExpectedPhysicalRoot;
811
+ if (writable) {
812
+ const physicalStagedRepo = canonicalWritablePath(stagedPaths.repoDir, "staged plain Git checkout");
813
+ auditRoot = containedWritablePath(configuredStagedAuditRoot, physicalStagedRepo, `Post-sync component root for "${gitSource.name ?? "git"}"`);
814
+ publishedAuditRoot = remapStagedPath(configuredStagedAuditRoot, staged.cacheDir, livePaths.rootDir);
815
+ publishedAuditContainmentRoot = canonicalWritablePath(livePaths.repoDir, "plain Git checkout");
816
+ publishedAuditExpectedPhysicalRoot = path.join(publishedAuditContainmentRoot, path.relative(physicalStagedRepo, auditRoot));
817
+ }
818
+ return {
819
+ auditRoot,
820
+ ...(publishedAuditRoot ? { publishedAuditRoot } : {}),
821
+ ...(publishedAuditContainmentRoot ? { publishedAuditContainmentRoot } : {}),
822
+ ...(publishedAuditExpectedPhysicalRoot ? { publishedAuditExpectedPhysicalRoot } : {}),
823
+ publication: writable
824
+ ? writableGitPublication({
825
+ stagedRepo: stagedPaths.repoDir,
826
+ liveRepo: livePaths.repoDir,
827
+ expectedOldHead,
828
+ })
829
+ : prepareDirectoryPublication(staged.cacheDir, livePaths.rootDir),
830
+ cleanup: () => cleanupStagingParent(stagingParent),
831
+ };
832
+ }
833
+ catch (error) {
834
+ cleanupStagingParent(stagingParent);
835
+ throw error;
836
+ }
837
+ }
838
+ async function syncGitPlainSource(gitSource, stashDir, allowInsecure) {
839
+ const id = gitSource.name ?? gitSource.url ?? "";
840
+ const currentConfig = loadConfig();
841
+ const currentSource = currentPlainSource(currentConfig, id, "git");
842
+ const ref = currentSource.url ?? "";
843
+ const fence = bundleAuditFence(currentConfig, id);
844
+ const prepared = await prepareGitPlainUpdate(currentSource, fence.componentRoot, fence.writable);
845
+ const index = await publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowInsecure, true, fence.generation);
846
+ return { item: { id, kind: "git", ref }, index };
847
+ }
848
+ async function prepareWebsitePlainUpdate(websiteSource, componentRoot) {
849
+ if (!websiteSource.url)
850
+ throw new ConfigError(`Website source "${websiteSource.name ?? "website"}" has no URL.`);
851
+ const livePaths = getWebsiteCachePaths(websiteSource.url);
852
+ const stagingParent = createStagingParent(path.dirname(livePaths.rootDir));
853
+ const stagedPaths = getWebsiteCachePaths(websiteSource.url, stagingParent);
854
+ try {
855
+ const provider = createWebsiteProvider(websiteSource);
856
+ await provider.sync({
857
+ force: true,
858
+ secrets: storeSecretResolver,
859
+ ensureWebsiteMirror: (config, options) => ensureWebsiteMirror(config, {
860
+ ...options,
861
+ resolveSecret: options?.resolveSecret ?? storeSecretResolver.resolveSecret,
862
+ cacheRootDir: stagingParent,
863
+ }),
864
+ });
865
+ return {
866
+ auditRoot: resolveComponentAuditRoot(stagedPaths.stashDir, componentRoot, websiteSource.name ?? "website"),
867
+ publication: prepareDirectoryPublication(stagedPaths.rootDir, livePaths.rootDir),
868
+ cleanup: () => cleanupStagingParent(stagingParent),
869
+ };
870
+ }
871
+ catch (error) {
872
+ cleanupStagingParent(stagingParent);
873
+ throw error;
874
+ }
875
+ }
876
+ async function syncWebsitePlainSource(websiteSource, stashDir, allowInsecure) {
877
+ const id = websiteSource.name ?? websiteSource.url ?? "";
878
+ const currentConfig = loadConfig();
879
+ const currentSource = currentPlainSource(currentConfig, id, "website");
880
+ const ref = currentSource.url ?? "";
881
+ const fence = bundleAuditFence(currentConfig, id);
882
+ const prepared = await prepareWebsitePlainUpdate(currentSource, fence.componentRoot);
883
+ const index = await publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowInsecure, true, fence.generation);
884
+ return { item: { id, kind: "website", ref }, index };
317
885
  }
318
886
  /** Sync a git-mirrored (plain) source and return an UpdateResponse (single-target path). */
319
- async function updateGitSource(stashDir, target, all, gitSource) {
320
- const synced = await syncGitPlainSource(gitSource);
321
- return buildUpdateResponse(stashDir, target, all, [], { full: true, plainSynced: [synced] });
887
+ async function updateGitSource(stashDir, target, all, gitSource, allowInsecure) {
888
+ const synced = await syncGitPlainSource(gitSource, stashDir, allowInsecure);
889
+ return buildUpdateResponse(stashDir, target, all, [], { plainSynced: [synced.item], index: synced.index });
322
890
  }
323
891
  /** Re-crawl a website (plain) source and return an UpdateResponse (single-target path). */
324
- async function updateWebsiteSource(stashDir, target, all, websiteSource) {
325
- const synced = await syncWebsitePlainSource(websiteSource);
326
- return buildUpdateResponse(stashDir, target, all, [], { plainSynced: [synced] });
892
+ async function updateWebsiteSource(stashDir, target, all, websiteSource, allowInsecure) {
893
+ const synced = await syncWebsitePlainSource(websiteSource, stashDir, allowInsecure);
894
+ return buildUpdateResponse(stashDir, target, all, [], { plainSynced: [synced.item], index: synced.index });
327
895
  }
328
896
  /**
329
897
  * A plain (lockless) npm bundle has no deterministic content path — unlike
@@ -338,9 +906,12 @@ async function updateWebsiteSource(stashDir, target, all, websiteSource) {
338
906
  * bookkeeping.
339
907
  */
340
908
  function managedInstallViewOfPlainNpm(npmSource) {
341
- const spec = npmSource.path ?? "";
909
+ const id = npmSource.name ?? npmSource.path ?? "";
910
+ const config = loadConfig();
911
+ const currentSource = currentPlainSource(config, id, "npm");
912
+ const spec = currentSource.path ?? "";
342
913
  const ref = spec.startsWith("npm:") ? spec : `npm:${spec}`;
343
- const id = npmSource.name ?? ref;
914
+ const fence = bundleAuditFence(config, id);
344
915
  return {
345
916
  bundleKey: id,
346
917
  installId: id,
@@ -350,7 +921,9 @@ function managedInstallViewOfPlainNpm(npmSource) {
350
921
  resolvedVersion: undefined,
351
922
  resolvedRevision: undefined,
352
923
  writable: false,
924
+ componentRoot: fence.componentRoot,
353
925
  requiredRoots: [],
926
+ auditConfigGeneration: fence.generation,
354
927
  };
355
928
  }
356
929
  /**
@@ -364,101 +937,193 @@ function managedInstallViewOfPlainNpm(npmSource) {
364
937
  * scripts; only the branch that can `rm -rf` needs a gate, not the whole
365
938
  * command).
366
939
  */
367
- async function updateManagedInstall(managed, force, yes, stashDir) {
940
+ async function updateManagedInstall(managed, force, yes, stashDir, allowInsecure) {
368
941
  // No pre-cleanup of the old root, even under --force: the providers already
369
942
  // re-materialize staging-first (git clones into a `.tmp-*` sibling and swaps
370
943
  // only on success), so destroying `managed.localRoot` BEFORE `syncFromRef`
371
944
  // succeeds would turn any sync failure (network down, bad ref) into losing a
372
945
  // previously-working install. The old root is cleaned up below only after
373
946
  // config/lock publication and reindexing both succeed.
374
- const synced = await syncFromRef(managed.ref, {
375
- force,
376
- writable: managed.writable,
377
- ...(managed.writable && managed.localRoot ? { writableRoot: managed.localRoot } : {}),
378
- ...(managed.writable && managed.requiredRoots.length > 0 ? { writableRequiredRoots: managed.requiredRoots } : {}),
379
- });
380
- const installedEntry = {
381
- id: managed.installId,
382
- // Preserve the original source classification. syncFromRef() re-derives the
383
- // source type from the ref scheme (e.g. "github:" → source: "github"), but
384
- // an update should not reclassify an existing entry.
385
- source: managed.source,
386
- ref: synced.ref,
387
- artifactUrl: synced.artifactUrl,
388
- resolvedVersion: synced.resolvedVersion,
389
- resolvedRevision: synced.resolvedRevision,
390
- stashRoot: synced.contentDir,
391
- cacheDir: synced.cacheDir,
392
- installedAt: synced.syncedAt,
393
- writable: synced.writable ?? managed.writable,
394
- };
395
- const movedRoot = managed.localRoot !== "" &&
396
- path.resolve(managed.localRoot) !== path.resolve(synced.contentDir) &&
397
- managed.source !== "local" &&
398
- !managed.writable;
399
- if (movedRoot) {
400
- const { confirmDestructive } = await import("../../cli/confirm.js");
401
- const confirmed = await confirmDestructive(`Update resolved a new content directory for "${managed.installId}" (${synced.contentDir}) and would delete the previous install directory at ${managed.localRoot}. This cannot be undone.`, { yes });
402
- if (!confirmed)
403
- throw new UsageError(`Update cancelled for "${managed.installId}"; no configuration was changed.`);
404
- }
405
- return withAssetMutationLease("source-update", async () => {
406
- const config = loadConfig();
407
- const oldLocks = readLockfile();
408
- if (!config.bundles?.[managed.bundleKey]) {
409
- throw new ConfigError(`Managed source "${managed.installId}" disappeared before update publication.`);
410
- }
411
- const desiredLock = {
412
- id: managed.bundleKey,
413
- // Preserve the STORED install kind: a `github:`-ref entry recorded as
414
- // source "git" must not be reclassified by the sync flow's re-derivation.
947
+ const prepared = await prepareManagedUpdate(managed, force);
948
+ try {
949
+ await auditPreparedUpdate(prepared, managed.installId, managed.ref, allowInsecure);
950
+ const synced = prepared.synced;
951
+ const installedEntry = {
952
+ id: managed.installId,
953
+ // Preserve the original source classification. syncFromRef() re-derives the
954
+ // source type from the ref scheme (e.g. "github:" → source: "github"), but
955
+ // an update should not reclassify an existing entry.
415
956
  source: managed.source,
416
957
  ref: synced.ref,
958
+ artifactUrl: synced.artifactUrl,
417
959
  resolvedVersion: synced.resolvedVersion,
418
960
  resolvedRevision: synced.resolvedRevision,
419
- integrity: synced.integrity ?? (synced.source === "local" ? "local" : undefined),
420
- localRoot: synced.contentDir,
961
+ stashRoot: synced.contentDir,
962
+ cacheDir: synced.cacheDir,
421
963
  installedAt: synced.syncedAt,
964
+ writable: synced.writable ?? managed.writable,
422
965
  };
423
- const desiredLocks = [...oldLocks.filter((entry) => entry.id !== desiredLock.id), desiredLock];
424
- const desiredLockGeneration = generationHash(desiredLocks);
425
- if (!(await compareAndSwapLockfile(oldLocks, desiredLocks)))
426
- throw concurrentGenerationError(managed);
427
- const index = await akmIndex({ stashDir });
428
- if (generationHash(readLockfile()) !== desiredLockGeneration)
429
- throw concurrentGenerationError(managed);
966
+ const movedRoot = managed.localRoot !== "" &&
967
+ path.resolve(managed.localRoot) !== path.resolve(synced.contentDir) &&
968
+ managed.source !== "local" &&
969
+ !managed.writable;
430
970
  if (movedRoot) {
431
- const currentConfig = loadConfig();
432
- const currentLocks = readLockfile();
433
- if (referencesRoot(managed.localRoot, currentConfig, currentLocks)) {
434
- warn(`[akm bundle update] kept previous install directory at ${managed.localRoot} because another configured or locked bundle still references it.`);
435
- }
436
- else {
437
- cleanupDirectoryBestEffort(managed.localRoot, "update");
438
- }
971
+ const { confirmDestructive } = await import("../../cli/confirm.js");
972
+ const confirmed = await confirmDestructive(`Update resolved a new content directory for "${managed.installId}" (${synced.contentDir}) and would delete the previous install directory at ${managed.localRoot}. This cannot be undone.`, { yes });
973
+ if (!confirmed)
974
+ throw new UsageError(`Update cancelled for "${managed.installId}"; no configuration was changed.`);
439
975
  }
440
- const versionChanged = (managed.resolvedVersion ?? "") !== (synced.resolvedVersion ?? "");
441
- const revisionChanged = (managed.resolvedRevision ?? "") !== (synced.resolvedRevision ?? "");
442
- return {
443
- index,
444
- item: {
445
- id: managed.installId,
976
+ return await withAssetMutationLease("source-update", async () => {
977
+ const oldLockSnapshot = readLockfileForUpdate();
978
+ const oldLocks = oldLockSnapshot.entries;
979
+ const desiredLock = {
980
+ id: managed.bundleKey,
981
+ // Preserve the STORED install kind: a `github:`-ref entry recorded as
982
+ // source "git" must not be reclassified by the sync flow's re-derivation.
446
983
  source: managed.source,
447
- ref: managed.ref,
448
- previous: {
449
- resolvedVersion: managed.resolvedVersion,
450
- resolvedRevision: managed.resolvedRevision,
451
- cacheDir: managed.localRoot,
452
- },
453
- installed: { ...installedEntry, extractedDir: synced.extractedDir },
454
- changed: {
455
- version: versionChanged,
456
- revision: revisionChanged,
457
- any: versionChanged || revisionChanged,
984
+ ref: synced.ref,
985
+ resolvedVersion: synced.resolvedVersion,
986
+ resolvedRevision: synced.resolvedRevision,
987
+ integrity: synced.integrity ?? (synced.source === "local" ? "local" : undefined),
988
+ localRoot: synced.contentDir,
989
+ installedAt: synced.syncedAt,
990
+ };
991
+ const desiredLocks = [...oldLocks.filter((entry) => entry.id !== desiredLock.id), desiredLock];
992
+ const desiredLockGeneration = generationHash(desiredLocks);
993
+ let transaction;
994
+ let releaseConfigFence;
995
+ let contentPublished = false;
996
+ let lockPublished = false;
997
+ let publishedLockSnapshot;
998
+ let committed = false;
999
+ let index;
1000
+ try {
1001
+ const configFence = acquireConfigReadFence();
1002
+ releaseConfigFence = configFence.release;
1003
+ assertBundleAuditFence(configFence.config, managed.bundleKey, managed.auditConfigGeneration);
1004
+ updateTransactionHook("fenced", managed.installId);
1005
+ if (prepared.publication) {
1006
+ prepared.publication.publish();
1007
+ contentPublished = true;
1008
+ }
1009
+ await verifyPublishedAudit(prepared, managed.installId);
1010
+ publishedLockSnapshot = await publishLockfileUpdate(oldLockSnapshot, desiredLocks);
1011
+ if (!publishedLockSnapshot)
1012
+ throw concurrentGenerationError(managed);
1013
+ lockPublished = true;
1014
+ updateTransactionHook("published", managed.installId);
1015
+ updateTransactionHook("before-index", managed.installId);
1016
+ transaction = openUnifiedUpdateTransaction();
1017
+ index = await akmIndex({
1018
+ stashDir,
1019
+ full: true,
1020
+ hydrateSources: false,
1021
+ persistDetectedAdapters: false,
1022
+ deferredUpdateTransaction: transaction.deferred,
1023
+ });
1024
+ const currentLockSnapshot = readLockfileForUpdate();
1025
+ if (generationHash(currentLockSnapshot.entries) !== desiredLockGeneration ||
1026
+ currentLockSnapshot.raw !== publishedLockSnapshot.raw ||
1027
+ currentLockSnapshot.mode !== publishedLockSnapshot.mode) {
1028
+ throw concurrentGenerationError(managed);
1029
+ }
1030
+ updateTransactionHook("indexed", managed.installId, { db: transaction.db });
1031
+ // Indexing can be long enough for a local writer to race the first
1032
+ // post-materialization audit. Re-verify the exact bytes/findings at the
1033
+ // final cross-store commit boundary while every AKM fence is held.
1034
+ await verifyPublishedAudit(prepared, managed.installId);
1035
+ updateTransactionHook("before-commit", managed.installId, { db: transaction.db });
1036
+ commitUnifiedUpdateTransaction(transaction);
1037
+ committed = true;
1038
+ try {
1039
+ transaction.deferred.afterCommit?.();
1040
+ }
1041
+ catch (error) {
1042
+ warn(`[akm bundle update] committed, but semantic status refresh failed: ${String(error)}`);
1043
+ }
1044
+ }
1045
+ catch (error) {
1046
+ let recoveryError = transaction ? rollbackUnifiedUpdateTransaction(transaction) : undefined;
1047
+ let concurrentGeneration = false;
1048
+ let concurrentGenerationOwnsPublishedRoot = false;
1049
+ if (lockPublished) {
1050
+ try {
1051
+ if (!publishedLockSnapshot ||
1052
+ !(await compareAndSwapLockfileSnapshot(publishedLockSnapshot, oldLockSnapshot))) {
1053
+ concurrentGeneration = true;
1054
+ const concurrentLocks = readLockfileForUpdate().entries;
1055
+ const concurrentEntry = concurrentLocks.find((entry) => entry.id === desiredLock.id);
1056
+ concurrentGenerationOwnsPublishedRoot =
1057
+ prepared.publication !== undefined &&
1058
+ concurrentEntry?.localRoot !== undefined &&
1059
+ path.resolve(concurrentEntry.localRoot) === path.resolve(synced.contentDir);
1060
+ }
1061
+ }
1062
+ catch (compensationError) {
1063
+ recoveryError ??= compensationError;
1064
+ }
1065
+ }
1066
+ if (!concurrentGenerationOwnsPublishedRoot) {
1067
+ try {
1068
+ if (contentPublished)
1069
+ prepared.publication?.rollback();
1070
+ }
1071
+ catch (rollbackError) {
1072
+ recoveryError ??= rollbackError;
1073
+ }
1074
+ }
1075
+ if (recoveryError)
1076
+ throw recoveryError;
1077
+ if (concurrentGeneration)
1078
+ throw concurrentGenerationError(managed);
1079
+ throw error;
1080
+ }
1081
+ finally {
1082
+ try {
1083
+ if (transaction)
1084
+ closeUnifiedUpdateTransaction(transaction, committed);
1085
+ }
1086
+ finally {
1087
+ releaseConfigFence?.();
1088
+ }
1089
+ }
1090
+ prepared.publication?.commit();
1091
+ if (movedRoot) {
1092
+ const currentConfig = loadConfig();
1093
+ const currentLocks = readLockfile();
1094
+ if (referencesRoot(managed.localRoot, currentConfig, currentLocks)) {
1095
+ warn(`[akm bundle update] kept previous install directory at ${managed.localRoot} because another configured or locked bundle still references it.`);
1096
+ }
1097
+ else {
1098
+ cleanupDirectoryBestEffort(managed.localRoot, "update");
1099
+ }
1100
+ }
1101
+ const versionChanged = (managed.resolvedVersion ?? "") !== (synced.resolvedVersion ?? "");
1102
+ const revisionChanged = (managed.resolvedRevision ?? "") !== (synced.resolvedRevision ?? "");
1103
+ return {
1104
+ index,
1105
+ item: {
1106
+ id: managed.installId,
1107
+ source: managed.source,
1108
+ ref: managed.ref,
1109
+ previous: {
1110
+ resolvedVersion: managed.resolvedVersion,
1111
+ resolvedRevision: managed.resolvedRevision,
1112
+ cacheDir: managed.localRoot,
1113
+ },
1114
+ installed: { ...installedEntry, extractedDir: synced.extractedDir },
1115
+ changed: {
1116
+ version: versionChanged,
1117
+ revision: revisionChanged,
1118
+ any: versionChanged || revisionChanged,
1119
+ },
458
1120
  },
459
- },
460
- };
461
- });
1121
+ };
1122
+ });
1123
+ }
1124
+ finally {
1125
+ prepared.cleanup();
1126
+ }
462
1127
  }
463
1128
  function generationHash(value) {
464
1129
  return sha256Hex(JSON.stringify(value));
@@ -494,13 +1159,14 @@ export async function akmUpdate(input) {
494
1159
  const all = input?.all === true;
495
1160
  const force = input?.force === true;
496
1161
  const yes = input?.yes === true;
1162
+ const allowInsecure = input?.allowInsecure === true;
497
1163
  const config = loadConfig();
498
1164
  const managedInstalls = listManagedInstalls(config);
499
1165
  if (target && !all) {
500
1166
  // Registry-managed install (lock-backed) — re-download from its locator.
501
1167
  const managed = resolveManagedTarget(config, target);
502
1168
  if (managed) {
503
- const updated = await updateManagedInstall(managed, force, yes, stashDir);
1169
+ const updated = await updateManagedInstall(managed, force, yes, stashDir, allowInsecure);
504
1170
  return buildUpdateResponse(stashDir, target, all, [updated.item], { index: updated.index });
505
1171
  }
506
1172
  // Plain git / website source (bundles without a lock) — provider re-sync.
@@ -529,7 +1195,7 @@ export async function akmUpdate(input) {
529
1195
  return false;
530
1196
  });
531
1197
  if (gitMatch)
532
- return updateGitSource(stashDir, target, all, gitMatch);
1198
+ return updateGitSource(stashDir, target, all, gitMatch, allowInsecure);
533
1199
  const websiteMatch = stashes.find((s) => {
534
1200
  if (s.type !== "website")
535
1201
  return false;
@@ -542,7 +1208,7 @@ export async function akmUpdate(input) {
542
1208
  return false;
543
1209
  });
544
1210
  if (websiteMatch)
545
- return updateWebsiteSource(stashDir, target, all, websiteMatch);
1211
+ return updateWebsiteSource(stashDir, target, all, websiteMatch, allowInsecure);
546
1212
  // Plain npm source (bundle without a lock) — sync via the registry
547
1213
  // pipeline and promote to a managed install (see
548
1214
  // managedInstallViewOfPlainNpm's doc comment for why npm can't stay
@@ -557,7 +1223,7 @@ export async function akmUpdate(input) {
557
1223
  return false;
558
1224
  });
559
1225
  if (npmMatch) {
560
- const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(npmMatch), force, yes, stashDir);
1226
+ const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(npmMatch), force, yes, stashDir, allowInsecure);
561
1227
  return buildUpdateResponse(stashDir, target, all, [updated.item], { index: updated.index });
562
1228
  }
563
1229
  }
@@ -566,71 +1232,88 @@ export async function akmUpdate(input) {
566
1232
  : managedInstalls;
567
1233
  const selected = selectManagedTargets(config, enabledManagedInstalls, target, all);
568
1234
  const processed = [];
569
- let latestManagedIndex;
1235
+ const plainSynced = [];
1236
+ const skipped = [];
1237
+ let latestIndex;
570
1238
  for (const managed of selected) {
571
- const updated = await updateManagedInstall(managed, force, yes, stashDir);
572
- processed.push(updated.item);
573
- latestManagedIndex = updated.index;
1239
+ try {
1240
+ const updated = await updateManagedInstall(managed, force, yes, stashDir, allowInsecure);
1241
+ processed.push(updated.item);
1242
+ latestIndex = updated.index;
1243
+ }
1244
+ catch (error) {
1245
+ if (!all)
1246
+ throw error;
1247
+ skipped.push(updateFailureOutcome(managed.installId, sourceKindForInstall(managed.source), error));
1248
+ }
574
1249
  }
575
1250
  // `--all` must account for EVERY configured source, not only the
576
1251
  // registry-managed (lock-backed) ones (R-015) — `selectManagedTargets`
577
1252
  // above returns only `installs` for `all`, so plain sources were
578
1253
  // previously never even looked at. Git/npm plain sources are synced here
579
- // too (git in place; npm promoted to managed, same as the single-target
580
- // path above); website/filesystem sources have no `--all` sync path, so
581
- // they are reported as skipped with the same explanatory wording
582
- // `selectManagedTargets` already uses for a single unmatched target.
583
- let plainSynced;
584
- let skipped;
585
- let sawGitSync = false;
1254
+ // too (git and website through staged promotion; npm promoted to managed,
1255
+ // same as the single-target path above). Filesystem sources reflect their
1256
+ // files in place and are reported as intentionally skipped.
586
1257
  if (all) {
587
1258
  const managedKeys = new Set(managedInstalls.map((m) => m.bundleKey));
588
1259
  const plainSources = getSources(config).filter((source) => source.enabled !== false && !managedKeys.has(source.name ?? ""));
589
- plainSynced = [];
590
- skipped = [];
591
1260
  for (const plain of plainSources) {
592
1261
  const id = plain.name ?? plain.path ?? plain.url ?? "";
593
1262
  try {
594
1263
  if (plain.type === "git") {
595
- plainSynced.push(await syncGitPlainSource(plain));
596
- sawGitSync = true;
1264
+ const updated = await syncGitPlainSource(plain, stashDir, allowInsecure);
1265
+ plainSynced.push(updated.item);
1266
+ latestIndex = updated.index;
597
1267
  }
598
1268
  else if (plain.type === "npm") {
599
- const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(plain), force, yes, stashDir);
1269
+ const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(plain), force, yes, stashDir, allowInsecure);
600
1270
  processed.push(updated.item);
601
- latestManagedIndex = updated.index;
1271
+ latestIndex = updated.index;
602
1272
  }
603
1273
  else if (plain.type === "website") {
604
- skipped.push({
605
- id,
606
- kind: "website",
607
- reason: `website caching not yet implemented for --all; run \`akm bundle update ${id}\` to re-mirror this source individually.`,
608
- });
1274
+ const updated = await syncWebsitePlainSource(plain, stashDir, allowInsecure);
1275
+ plainSynced.push(updated.item);
1276
+ latestIndex = updated.index;
609
1277
  }
610
1278
  else {
611
1279
  skipped.push({
612
1280
  id,
613
1281
  kind: plain.type,
1282
+ status: "skipped",
614
1283
  reason: "reflects your files in place and has no remote to sync; run `akm index` to refresh the search index.",
615
1284
  });
616
1285
  }
617
1286
  }
618
- catch (err) {
619
- skipped.push({
620
- id,
621
- kind: plain.type,
622
- reason: `sync failed: ${err instanceof Error ? err.message : String(err)}`,
623
- });
1287
+ catch (error) {
1288
+ skipped.push(updateFailureOutcome(id, plain.type, error));
624
1289
  }
625
1290
  }
626
1291
  }
627
1292
  return buildUpdateResponse(stashDir, target, all, processed, {
628
- full: sawGitSync,
629
1293
  plainSynced,
630
1294
  skipped,
631
- ...(!sawGitSync && latestManagedIndex ? { index: latestManagedIndex } : {}),
1295
+ index: latestIndex ?? readCurrentIndexSummary(),
632
1296
  });
633
1297
  }
1298
+ function sourceKindForInstall(source) {
1299
+ if (source === "local")
1300
+ return "filesystem";
1301
+ if (source === "github")
1302
+ return "git";
1303
+ return source;
1304
+ }
1305
+ function updateFailureOutcome(id, kind, error) {
1306
+ const code = error instanceof AkmError ? error.code : undefined;
1307
+ const status = code === "DANGEROUS_ENV_KEY" || code === "DANGEROUS_ENV_AUDIT_FAILED" ? "blocked" : "failed";
1308
+ const message = error instanceof Error ? error.message : String(error);
1309
+ return {
1310
+ id,
1311
+ kind,
1312
+ status,
1313
+ ...(code ? { code } : {}),
1314
+ reason: `${status}: ${message}`,
1315
+ };
1316
+ }
634
1317
  function selectManagedTargets(config, installs, target, all) {
635
1318
  if (all && target) {
636
1319
  throw new UsageError("Specify either <target> or --all, not both.", "MISSING_OR_AMBIGUOUS_TARGET");