akm-cli 0.8.6 → 0.8.14

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 (324) hide show
  1. package/CHANGELOG.md +442 -0
  2. package/dist/assets/help/help-proposals.md +1 -2
  3. package/dist/assets/hints/cli-hints-full.md +34 -19
  4. package/dist/assets/hints/cli-hints-short.md +1 -1
  5. package/dist/assets/profiles/catchup.json +13 -0
  6. package/dist/assets/profiles/consolidate.json +13 -0
  7. package/dist/assets/profiles/frequent.json +13 -0
  8. package/dist/assets/tasks/core/backup.yml +4 -0
  9. package/dist/assets/tasks/core/extract.yml +4 -0
  10. package/dist/assets/tasks/core/improve.yml +4 -0
  11. package/dist/assets/tasks/core/index-refresh.yml +4 -0
  12. package/dist/assets/tasks/core/sync.yml +4 -0
  13. package/dist/assets/tasks/core/update-stashes.yml +4 -0
  14. package/dist/assets/tasks/core/version-check.yml +4 -0
  15. package/dist/assets/templates/html/default.html +78 -0
  16. package/dist/assets/templates/html/health.html +560 -0
  17. package/dist/assets/templates/html/vendor/echarts.min.js +45 -0
  18. package/dist/cli/config-migrate.js +6 -6
  19. package/dist/cli/config-validate.js +4 -4
  20. package/dist/cli/confirm.js +3 -3
  21. package/dist/cli/parse-args.js +1 -1
  22. package/dist/cli/shared.js +72 -19
  23. package/dist/cli-node.mjs +26 -0
  24. package/dist/cli.js +206 -3866
  25. package/dist/commands/{agent-dispatch.js → agent/agent-dispatch.js} +6 -6
  26. package/dist/commands/{agent-support.js → agent/agent-support.js} +2 -2
  27. package/dist/commands/agent/contribute-cli.js +200 -0
  28. package/dist/commands/completions.js +1 -1
  29. package/dist/commands/config-cli.js +230 -3
  30. package/dist/commands/db-cli.js +2 -2
  31. package/dist/commands/env/env-cli.js +529 -0
  32. package/dist/commands/env/env.js +410 -0
  33. package/dist/commands/env/secret-cli.js +259 -0
  34. package/dist/commands/{secret.js → env/secret.js} +6 -47
  35. package/dist/commands/events.js +4 -4
  36. package/dist/commands/feedback-cli.js +18 -34
  37. package/dist/commands/graph/graph-cli.js +132 -0
  38. package/dist/commands/{graph.js → graph/graph.js} +22 -16
  39. package/dist/commands/health/checks.js +279 -0
  40. package/dist/commands/health/html-report.js +448 -0
  41. package/dist/commands/health.js +189 -266
  42. package/dist/commands/{consolidate.js → improve/consolidate.js} +63 -38
  43. package/dist/commands/{distill-promotion-policy.js → improve/distill-promotion-policy.js} +3 -3
  44. package/dist/commands/{distill.js → improve/distill.js} +39 -18
  45. package/dist/commands/{eval-cases.js → improve/eval-cases.js} +1 -1
  46. package/dist/commands/{extract-cli.js → improve/extract-cli.js} +4 -4
  47. package/dist/commands/{extract-prompt.js → improve/extract-prompt.js} +2 -2
  48. package/dist/commands/{extract.js → improve/extract.js} +221 -26
  49. package/dist/commands/{improve-auto-accept.js → improve/improve-auto-accept.js} +30 -4
  50. package/dist/commands/{improve-cli.js → improve/improve-cli.js} +44 -22
  51. package/dist/commands/{improve-profiles.js → improve/improve-profiles.js} +13 -7
  52. package/dist/commands/{improve-result-file.js → improve/improve-result-file.js} +1 -1
  53. package/dist/commands/{improve.js → improve/improve.js} +672 -292
  54. package/dist/{core → commands/improve/memory}/memory-belief.js +2 -2
  55. package/dist/{core → commands/improve/memory}/memory-contradiction-detect.js +5 -5
  56. package/dist/{core → commands/improve/memory}/memory-improve.js +4 -4
  57. package/dist/commands/improve/reflect-noise.js +0 -0
  58. package/dist/commands/{reflect.js → improve/reflect.js} +58 -28
  59. package/dist/commands/improve/session-asset.js +248 -0
  60. package/dist/commands/lint/agent-linter.js +1 -1
  61. package/dist/commands/lint/base-linter.js +55 -37
  62. package/dist/commands/lint/command-linter.js +1 -1
  63. package/dist/commands/lint/default-linter.js +1 -1
  64. package/dist/commands/lint/env-key-rules.js +1 -1
  65. package/dist/commands/lint/index.js +19 -25
  66. package/dist/commands/lint/knowledge-linter.js +1 -1
  67. package/dist/commands/lint/memory-linter.js +1 -1
  68. package/dist/commands/lint/registry.js +8 -8
  69. package/dist/commands/lint/skill-linter.js +1 -1
  70. package/dist/commands/lint/task-linter.js +1 -1
  71. package/dist/commands/lint/workflow-linter.js +1 -1
  72. package/dist/commands/lint.js +1 -1
  73. package/dist/commands/observability-cli.js +244 -0
  74. package/dist/commands/proposal/drain-policies.js +3 -3
  75. package/dist/commands/proposal/drain.js +87 -15
  76. package/dist/commands/proposal/proposal-cli.js +490 -0
  77. package/dist/commands/{proposal.js → proposal/proposal.js} +17 -6
  78. package/dist/commands/{propose.js → proposal/propose.js} +11 -11
  79. package/dist/{core → commands/proposal/validators}/proposal-quality-validators.js +8 -3
  80. package/dist/{core → commands/proposal/validators}/proposal-validators.js +5 -5
  81. package/dist/{core → commands/proposal/validators}/proposals.js +374 -345
  82. package/dist/commands/{curate.js → read/curate.js} +7 -7
  83. package/dist/commands/{knowledge.js → read/knowledge.js} +22 -9
  84. package/dist/commands/{registry-search.js → read/registry-search.js} +5 -5
  85. package/dist/commands/{remember-cli.js → read/remember-cli.js} +15 -7
  86. package/dist/commands/read/search-cli.js +207 -0
  87. package/dist/commands/{search.js → read/search.js} +22 -27
  88. package/dist/commands/{show.js → read/show.js} +31 -45
  89. package/dist/commands/registry-cli.js +8 -8
  90. package/dist/commands/remember.js +14 -10
  91. package/dist/commands/sources/add-cli.js +293 -0
  92. package/dist/commands/{history.js → sources/history.js} +27 -25
  93. package/dist/commands/{info.js → sources/info.js} +6 -6
  94. package/dist/commands/{init.js → sources/init.js} +6 -6
  95. package/dist/commands/{installed-stashes.js → sources/installed-stashes.js} +12 -12
  96. package/dist/commands/{migration-help.js → sources/migration-help.js} +3 -2
  97. package/dist/commands/{schema-repair.js → sources/schema-repair.js} +8 -8
  98. package/dist/commands/{self-update.js → sources/self-update.js} +10 -9
  99. package/dist/commands/{source-add.js → sources/source-add.js} +10 -10
  100. package/dist/commands/{source-clone.js → sources/source-clone.js} +7 -7
  101. package/dist/commands/{source-manage.js → sources/source-manage.js} +4 -4
  102. package/dist/commands/sources/sources-cli.js +305 -0
  103. package/dist/commands/sources/stash-cli.js +219 -0
  104. package/dist/commands/{stash-skeleton.js → sources/stash-skeleton.js} +2 -1
  105. package/dist/commands/tasks/default-tasks.js +173 -0
  106. package/dist/commands/tasks/tasks-cli.js +210 -0
  107. package/dist/commands/{tasks.js → tasks/tasks.js} +14 -14
  108. package/dist/commands/wiki-cli.js +307 -0
  109. package/dist/commands/workflow-cli.js +329 -0
  110. package/dist/core/action-contributors.js +1 -1
  111. package/dist/core/assert.js +40 -0
  112. package/dist/core/asset/asset-create.js +54 -0
  113. package/dist/core/{asset-ref.js → asset/asset-ref.js} +21 -4
  114. package/dist/core/{asset-registry.js → asset/asset-registry.js} +3 -3
  115. package/dist/core/{asset-spec.js → asset/asset-spec.js} +17 -31
  116. package/dist/core/{markdown.js → asset/markdown.js} +1 -1
  117. package/dist/core/{stash-meta.js → asset/stash-meta.js} +1 -1
  118. package/dist/core/best-effort.js +64 -0
  119. package/dist/core/common.js +32 -18
  120. package/dist/core/{config-io.js → config/config-io.js} +29 -19
  121. package/dist/core/{config-migration.js → config/config-migration.js} +11 -9
  122. package/dist/core/{config-schema.js → config/config-schema.js} +50 -7
  123. package/dist/core/config/config-types.js +16 -0
  124. package/dist/core/{config-walker.js → config/config-walker.js} +2 -2
  125. package/dist/core/{config.js → config/config.js} +10 -8
  126. package/dist/core/env-secret-ref.js +90 -0
  127. package/dist/core/errors.js +13 -3
  128. package/dist/core/events.js +27 -4
  129. package/dist/core/file-lock.js +1 -1
  130. package/dist/core/improve-types.js +48 -0
  131. package/dist/core/lesson-lint.js +2 -2
  132. package/dist/core/logs-db.js +304 -0
  133. package/dist/core/paths.js +2 -2
  134. package/dist/core/ripgrep/install.js +2 -2
  135. package/dist/core/ripgrep/resolve.js +2 -2
  136. package/dist/core/state-db.js +195 -60
  137. package/dist/core/text-truncation.js +148 -0
  138. package/dist/core/time.js +1 -1
  139. package/dist/core/write-source.js +98 -85
  140. package/dist/indexer/{db-backup.js → db/db-backup.js} +9 -24
  141. package/dist/indexer/{db.js → db/db.js} +128 -118
  142. package/dist/indexer/{graph-db.js → db/graph-db.js} +9 -4
  143. package/dist/indexer/{llm-cache.js → db/llm-cache.js} +15 -12
  144. package/dist/indexer/ensure-index.js +4 -4
  145. package/dist/indexer/{graph-boost.js → graph/graph-boost.js} +1 -1
  146. package/dist/indexer/{graph-extraction.js → graph/graph-extraction.js} +55 -13
  147. package/dist/indexer/indexer.js +37 -30
  148. package/dist/indexer/init.js +54 -0
  149. package/dist/indexer/manifest.js +10 -10
  150. package/dist/indexer/{memory-inference.js → passes/memory-inference.js} +141 -33
  151. package/dist/indexer/{metadata-contributors.js → passes/metadata-contributors.js} +10 -8
  152. package/dist/indexer/{metadata.js → passes/metadata.js} +15 -19
  153. package/dist/indexer/{staleness-detect.js → passes/staleness-detect.js} +53 -12
  154. package/dist/indexer/{db-search.js → search/db-search.js} +28 -16
  155. package/dist/indexer/{ranking-contributors.js → search/ranking-contributors.js} +1 -1
  156. package/dist/indexer/{ranking.js → search/ranking.js} +2 -2
  157. package/dist/indexer/{search-hit-enrichers.js → search/search-hit-enrichers.js} +3 -3
  158. package/dist/indexer/{search-source.js → search/search-source.js} +8 -8
  159. package/dist/indexer/{semantic-status.js → search/semantic-status.js} +3 -3
  160. package/dist/indexer/usage/unmigrated-vaults-guard.js +94 -0
  161. package/dist/indexer/{usage-events.js → usage/usage-events.js} +32 -0
  162. package/dist/indexer/{file-context.js → walk/file-context.js} +10 -15
  163. package/dist/indexer/{matchers.js → walk/matchers.js} +13 -9
  164. package/dist/indexer/{path-resolver.js → walk/path-resolver.js} +6 -6
  165. package/dist/indexer/{project-context.js → walk/project-context.js} +1 -1
  166. package/dist/indexer/{walker.js → walk/walker.js} +4 -3
  167. package/dist/integrations/agent/builder-shared.js +39 -0
  168. package/dist/integrations/agent/builders.js +14 -81
  169. package/dist/integrations/agent/config.js +6 -4
  170. package/dist/integrations/agent/detect.js +1 -1
  171. package/dist/integrations/agent/index.js +23 -8
  172. package/dist/integrations/agent/prompts.js +2 -3
  173. package/dist/integrations/agent/runner.js +22 -3
  174. package/dist/integrations/agent/spawn.js +9 -10
  175. package/dist/integrations/harnesses/claude/agent-builder.js +48 -0
  176. package/dist/integrations/harnesses/claude/config-import.js +70 -0
  177. package/dist/integrations/harnesses/claude/index.js +64 -0
  178. package/dist/integrations/{session-logs/providers/claude-code.js → harnesses/claude/session-log.js} +32 -5
  179. package/dist/integrations/harnesses/index.js +144 -0
  180. package/dist/integrations/harnesses/opencode/agent-builder.js +43 -0
  181. package/dist/integrations/harnesses/opencode/config-import.js +82 -0
  182. package/dist/integrations/harnesses/opencode/index.js +59 -0
  183. package/dist/integrations/{session-logs/providers/opencode.js → harnesses/opencode/session-log.js} +1 -1
  184. package/dist/integrations/harnesses/opencode-sdk/index.js +49 -0
  185. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +234 -0
  186. package/dist/integrations/harnesses/types.js +43 -0
  187. package/dist/integrations/lockfile.js +7 -16
  188. package/dist/integrations/session-logs/index.js +82 -9
  189. package/dist/llm/call-ai.js +4 -4
  190. package/dist/llm/client.js +146 -6
  191. package/dist/llm/embedder.js +6 -6
  192. package/dist/llm/embedders/local.js +9 -22
  193. package/dist/llm/embedders/remote.js +2 -2
  194. package/dist/llm/embedders/types.js +1 -1
  195. package/dist/llm/graph-extract.js +31 -12
  196. package/dist/llm/index-passes.js +1 -1
  197. package/dist/llm/memory-infer.js +12 -5
  198. package/dist/llm/metadata-enhance.js +2 -2
  199. package/dist/llm/usage-persist.js +77 -0
  200. package/dist/llm/usage-telemetry.js +103 -0
  201. package/dist/output/context.js +9 -46
  202. package/dist/output/html-render.js +73 -0
  203. package/dist/output/renderers.js +88 -58
  204. package/dist/output/shapes/curate.js +7 -3
  205. package/dist/output/shapes/distill.js +7 -3
  206. package/dist/output/shapes/env-list.js +18 -16
  207. package/dist/output/shapes/events.js +5 -4
  208. package/dist/output/shapes/helpers.js +19 -5
  209. package/dist/output/shapes/history.js +7 -3
  210. package/dist/output/shapes/passthrough.js +8 -11
  211. package/dist/output/shapes/{proposal-accept.js → proposal/accept.js} +7 -3
  212. package/dist/output/shapes/{proposal-diff.js → proposal/diff.js} +7 -3
  213. package/dist/output/shapes/{proposal-list.js → proposal/list.js} +7 -3
  214. package/dist/output/shapes/{proposal-producer.js → proposal/producer.js} +5 -4
  215. package/dist/output/shapes/{proposal-reject.js → proposal/reject.js} +7 -3
  216. package/dist/output/shapes/{proposal-show.js → proposal/show.js} +7 -3
  217. package/dist/output/shapes/registry-search.js +7 -3
  218. package/dist/output/shapes/registry.js +12 -0
  219. package/dist/output/shapes/search.js +7 -3
  220. package/dist/output/shapes/secret-list.js +18 -16
  221. package/dist/output/shapes/show.js +7 -3
  222. package/dist/output/shapes.js +55 -30
  223. package/dist/output/text/add.js +2 -3
  224. package/dist/output/text/clone.js +2 -3
  225. package/dist/output/text/config.js +2 -3
  226. package/dist/output/text/curate.js +4 -3
  227. package/dist/output/text/distill.js +2 -3
  228. package/dist/output/text/enable-disable.js +5 -4
  229. package/dist/output/text/env.js +13 -0
  230. package/dist/output/text/events.js +5 -4
  231. package/dist/output/text/feedback.js +4 -3
  232. package/dist/output/text/helpers.js +123 -40
  233. package/dist/output/text/history.js +2 -3
  234. package/dist/output/text/import.js +2 -3
  235. package/dist/output/text/index.js +2 -3
  236. package/dist/output/text/info.js +2 -3
  237. package/dist/output/text/init.js +2 -3
  238. package/dist/output/text/list.js +2 -3
  239. package/dist/output/text/proposal/producer.js +9 -0
  240. package/dist/output/text/proposal/proposal.js +13 -0
  241. package/dist/output/text/registry-commands.js +8 -7
  242. package/dist/output/text/registry.js +12 -0
  243. package/dist/output/text/remember.js +4 -3
  244. package/dist/output/text/remove.js +2 -3
  245. package/dist/output/text/save.js +2 -3
  246. package/dist/output/text/search.js +4 -3
  247. package/dist/output/text/show.js +4 -3
  248. package/dist/output/text/update.js +2 -3
  249. package/dist/output/text/upgrade.js +2 -3
  250. package/dist/output/text/wiki.js +12 -11
  251. package/dist/output/text/workflow.js +12 -10
  252. package/dist/output/text.js +66 -32
  253. package/dist/registry/build-index.js +11 -10
  254. package/dist/registry/factory.js +1 -1
  255. package/dist/registry/origin-resolve.js +1 -1
  256. package/dist/registry/providers/index.js +2 -2
  257. package/dist/registry/providers/skills-sh.js +91 -72
  258. package/dist/registry/providers/static-index.js +75 -52
  259. package/dist/registry/resolve.js +3 -3
  260. package/dist/runtime.js +242 -0
  261. package/dist/scripts/migrate-storage.js +1654 -683
  262. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +254 -168
  263. package/dist/setup/detect.js +311 -9
  264. package/dist/setup/harness-config-import.js +6 -120
  265. package/dist/setup/setup.js +454 -43
  266. package/dist/sources/include.js +1 -1
  267. package/dist/sources/provider-factory.js +2 -2
  268. package/dist/sources/providers/filesystem.js +3 -3
  269. package/dist/sources/providers/git.js +9 -9
  270. package/dist/sources/providers/index.js +4 -4
  271. package/dist/sources/providers/npm.js +6 -6
  272. package/dist/sources/providers/provider-utils.js +13 -20
  273. package/dist/sources/providers/sync-from-ref.js +5 -5
  274. package/dist/sources/providers/tar-utils.js +2 -2
  275. package/dist/sources/providers/website.js +2 -2
  276. package/dist/sources/resolve.js +5 -5
  277. package/dist/sources/website-ingest.js +5 -5
  278. package/dist/storage/database.js +102 -0
  279. package/dist/storage/engines/sqlite-migrations.js +42 -0
  280. package/dist/storage/locations.js +25 -0
  281. package/dist/storage/repositories/index-db.js +43 -0
  282. package/dist/storage/repositories/workflow-runs-repository.js +141 -0
  283. package/dist/tasks/backends/cron.js +4 -4
  284. package/dist/tasks/backends/exec-utils.js +32 -0
  285. package/dist/tasks/backends/index.js +3 -3
  286. package/dist/tasks/backends/launchd.js +7 -14
  287. package/dist/tasks/backends/schtasks.js +7 -16
  288. package/dist/tasks/embedded.js +71 -0
  289. package/dist/tasks/parser.js +2 -2
  290. package/dist/tasks/resolveAkmBin.js +1 -1
  291. package/dist/tasks/runner.js +127 -31
  292. package/dist/tasks/schedule.js +1 -1
  293. package/dist/tasks/validator.js +7 -7
  294. package/dist/text-import-hook.mjs +51 -0
  295. package/dist/version.js +2 -1
  296. package/dist/wiki/wiki.js +7 -7
  297. package/dist/workflows/{authoring.js → authoring/authoring.js} +6 -6
  298. package/dist/workflows/{scope-key.js → authoring/scope-key.js} +1 -1
  299. package/dist/workflows/cli.js +1 -1
  300. package/dist/workflows/db.js +54 -32
  301. package/dist/workflows/parser.js +4 -4
  302. package/dist/workflows/renderer.js +5 -5
  303. package/dist/workflows/runtime/agent-identity.js +56 -0
  304. package/dist/workflows/runtime/checkin.js +57 -0
  305. package/dist/workflows/{runs.js → runtime/runs.js} +197 -101
  306. package/dist/workflows/validate-summary.js +82 -0
  307. package/docs/README.md +1 -1
  308. package/docs/data-and-telemetry.md +6 -6
  309. package/package.json +17 -8
  310. package/dist/commands/add-cli.js +0 -279
  311. package/dist/commands/env.js +0 -213
  312. package/dist/integrations/agent/sdk-runner.js +0 -126
  313. package/dist/output/shapes/vault-list.js +0 -19
  314. package/dist/output/text/proposal-producer.js +0 -8
  315. package/dist/output/text/proposal.js +0 -12
  316. package/dist/output/text/vault.js +0 -16
  317. /package/dist/core/{asset-serialize.js → asset/asset-serialize.js} +0 -0
  318. /package/dist/core/{frontmatter.js → asset/frontmatter.js} +0 -0
  319. /package/dist/core/{config-sources.js → config/config-sources.js} +0 -0
  320. /package/dist/indexer/{graph-dedup.js → graph/graph-dedup.js} +0 -0
  321. /package/dist/{core/config-types.js → indexer/passes/pass-context.js} +0 -0
  322. /package/dist/indexer/{search-fields.js → search/search-fields.js} +0 -0
  323. /package/dist/indexer/{index-context.js → walk/index-context.js} +0 -0
  324. /package/dist/workflows/{document-cache.js → runtime/document-cache.js} +0 -0
@@ -0,0 +1,410 @@
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
+ * Environment asset type (`env`) — whole `.env` file storage.
6
+ *
7
+ * An `env` asset holds a GROUP of related CONFIGURATION for an app or service
8
+ * (URLs, feature flags, and any credentials it needs) in a single `.env` file,
9
+ * sourced/injected wholesale. Values may or may not be sensitive — akm protects
10
+ * them all the same. For a single sensitive value used on its own for
11
+ * authentication (a token, key, or cert), use the `secret` type instead.
12
+ *
13
+ * Single keys can be managed with `akm env set <ref> KEY` (value read from stdin
14
+ * or `--from-env`/`--from-file`, never argv) and `akm env unset <ref> KEY...`,
15
+ * which do a minimal line-level edit that preserves existing comments and key
16
+ * order (see `setEnvKey` / `unsetEnvKeys`). You can also just edit the `.env`
17
+ * file with your own editor. Values are quoted/escaped only when necessary and
18
+ * round-trip through `dotenv`; the shell-load safety guarantee still lives on
19
+ * the READ path (see `buildShellExportScript` + `akm env export`).
20
+ *
21
+ * Invariant: env values must never be written to stdout, returned through the
22
+ * indexer, the `akm show` renderer, or any structured output channel. Key
23
+ * NAMES and start-of-line comments ARE surfaced by design (discoverability) —
24
+ * only values are secret. The supported value-load paths are:
25
+ *
26
+ * - `akm env run <ref> -- <command>` — values injected into the child
27
+ * process env (never via a shell), see `injectIntoEnv` / `loadEnv`. This is
28
+ * the primary path and the only one safe for AI agents (no values ever
29
+ * reach stdout). For an interactive shell, `akm env run <ref> -- $SHELL`.
30
+ * - `akm env export <ref> --out <file>` — write parse-then-reserialized safe
31
+ * `export KEY='value'` lines to a file (mode 0600) for `source`-ing. Values
32
+ * are re-emitted single-quoted so a raw `.env` containing `X=$(cmd)` cannot
33
+ * execute on load. `export` never prints values to stdout (would leak into
34
+ * an agent's context); `path` prints only the file path.
35
+ *
36
+ * Value parsing is delegated to the `dotenv` package, and `dotenv` is also the
37
+ * serialisation oracle for `env set` (`setEnvKey`): a written value is only
38
+ * committed if `dotenv.parse` reads it back exactly, and the whole edit is
39
+ * re-parsed to confirm no other key was disturbed. We never hand-roll a
40
+ * quoting representation we cannot read back.
41
+ *
42
+ * Secret-token substitution: env VALUES may embed `${secret:NAME}` tokens, which
43
+ * are replaced at `env run` time with the value of the sibling `secret:NAME`
44
+ * asset in the SAME stash (see `resolveSecretTokens`). Substitution applies to
45
+ * values only, never keys; only the `${secret:...}` form is recognised —
46
+ * shell-style `${VAR}` / `$VAR` are left untouched. The secret lookup is
47
+ * injected so this module keeps its narrow dependency surface (dotenv +
48
+ * core/common) and never reaches into the secret resolver/source machinery.
49
+ */
50
+ import fs from "node:fs";
51
+ import path from "node:path";
52
+ import dotenv from "dotenv";
53
+ import { writeFileAtomic } from "../../core/common.js";
54
+ import { UsageError } from "../../core/errors.js";
55
+ /** Matches a KEY=value assignment line, capturing only the key. */
56
+ const ASSIGN_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
57
+ /** Scan lines and return KEY names in file order, without duplicates. */
58
+ function scanKeys(text) {
59
+ const keys = [];
60
+ const seen = new Set();
61
+ for (const line of text.split(/\r?\n/)) {
62
+ const m = line.match(ASSIGN_RE);
63
+ if (!m)
64
+ continue;
65
+ const key = m[1];
66
+ if (seen.has(key))
67
+ continue;
68
+ seen.add(key);
69
+ keys.push(key);
70
+ }
71
+ return keys;
72
+ }
73
+ /**
74
+ * Scan lines and return start-of-line `#` comments (with the leading `#` and
75
+ * any leading whitespace stripped). Inline/trailing `#` after an assignment is
76
+ * never extracted.
77
+ */
78
+ function scanComments(text) {
79
+ const comments = [];
80
+ for (const line of text.split(/\r?\n/)) {
81
+ const trimmed = line.trimStart();
82
+ if (trimmed.startsWith("#")) {
83
+ comments.push(trimmed.slice(1).trimStart());
84
+ }
85
+ }
86
+ return comments;
87
+ }
88
+ /**
89
+ * Read and return ONLY non-secret metadata (keys + start-of-line comments).
90
+ *
91
+ * The function reads the whole file into memory (same as any dotenv parser)
92
+ * but deliberately does not parse values — the LHS-only regex scanners above
93
+ * ensure no value content is retained or returned. The guarantee is that
94
+ * values never leave this function.
95
+ */
96
+ export function listKeys(envPath) {
97
+ if (!fs.existsSync(envPath))
98
+ return { keys: [], comments: [] };
99
+ const text = fs.readFileSync(envPath, "utf8");
100
+ return { keys: scanKeys(text), comments: scanComments(text) };
101
+ }
102
+ /**
103
+ * Return structured `entries` pairing each key with the nearest preceding
104
+ * comment line (if any). This is an easier-to-consume shape than the parallel
105
+ * `keys[]` + `comments[]` of `listKeys` (QA #35).
106
+ *
107
+ * Values are never included — the same privacy guarantee as `listKeys`.
108
+ */
109
+ export function listEntries(envPath) {
110
+ if (!fs.existsSync(envPath))
111
+ return [];
112
+ const text = fs.readFileSync(envPath, "utf8");
113
+ const lines = text.split(/\r?\n/);
114
+ const seen = new Set();
115
+ const entries = [];
116
+ let pendingComment;
117
+ for (const line of lines) {
118
+ const trimmed = line.trimStart();
119
+ if (trimmed.startsWith("#")) {
120
+ // Capture the most recent comment before a key
121
+ pendingComment = trimmed.slice(1).trimStart() || undefined;
122
+ continue;
123
+ }
124
+ const m = line.match(ASSIGN_RE);
125
+ if (m) {
126
+ const key = m[1];
127
+ if (!seen.has(key)) {
128
+ seen.add(key);
129
+ const entry = { key };
130
+ if (pendingComment)
131
+ entry.comment = pendingComment;
132
+ entries.push(entry);
133
+ }
134
+ pendingComment = undefined;
135
+ }
136
+ else {
137
+ // Any non-comment, non-assignment line (including blank lines)
138
+ // breaks "nearest preceding comment line" association.
139
+ pendingComment = undefined;
140
+ }
141
+ }
142
+ return entries;
143
+ }
144
+ /**
145
+ * Read all KEY=value pairs from an env file. Intended for programmatic callers
146
+ * that need to inject values into a process environment. Callers MUST NOT write
147
+ * the returned values to stdout or any logged output.
148
+ *
149
+ * Value parsing (quoting, escapes, multi-line, etc.) is delegated to dotenv.
150
+ */
151
+ export function loadEnv(envPath) {
152
+ if (!fs.existsSync(envPath))
153
+ return {};
154
+ const buf = fs.readFileSync(envPath);
155
+ return dotenv.parse(buf);
156
+ }
157
+ /**
158
+ * Load an env file and assign its values into `target` (defaults to
159
+ * `process.env`). Returns the list of keys that were set so the caller can
160
+ * log/observe without touching values.
161
+ *
162
+ * Existing keys in `target` are overwritten — callers who want to preserve
163
+ * pre-existing environment variables should filter before calling.
164
+ */
165
+ export function injectIntoEnv(envPath, target = process.env) {
166
+ const env = loadEnv(envPath);
167
+ for (const [key, value] of Object.entries(env)) {
168
+ target[key] = value;
169
+ }
170
+ return Object.keys(env);
171
+ }
172
+ /**
173
+ * Serialise an env file's values as a POSIX shell script of `export KEY='value'`
174
+ * lines, with single-quote escaping (`'\''`). Every line is an assignment of a
175
+ * literal string — there is no expansion, command substitution, or
176
+ * non-assignment content, so `eval`-ing the output is safe regardless of what
177
+ * the source file contains.
178
+ *
179
+ * This is the trust boundary for shell loading: a raw `.env` may contain
180
+ * `X=$(rm -rf ~)`, which would execute if `source`d directly, but dotenv parses
181
+ * it to the literal string `$(rm -rf ~)` and we re-emit it single-quoted. This
182
+ * backs `akm env export <ref> --out <file>` (file-only; never printed to stdout).
183
+ *
184
+ * NOTE: `${secret:NAME}` token substitution is intentionally NOT applied here.
185
+ * The export path emits values single-quoted as literals, so an unsubstituted
186
+ * `${secret:NAME}` is written verbatim (it would expand to nothing under POSIX
187
+ * shells, never to the secret). Secret-token resolution is scoped to the
188
+ * `env run` value-injection path only; see `resolveSecretTokens`.
189
+ */
190
+ export function buildShellExportScript(envPath) {
191
+ const env = loadEnv(envPath);
192
+ const lines = [];
193
+ for (const [key, value] of Object.entries(env)) {
194
+ // Defence in depth: dotenv already validates key shape, but reject any
195
+ // key we wouldn't be able to export safely.
196
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
197
+ continue;
198
+ const escaped = value.replace(/'/g, "'\\''");
199
+ lines.push(`export ${key}='${escaped}'`);
200
+ }
201
+ return lines.length > 0 ? `${lines.join("\n")}\n` : "";
202
+ }
203
+ /**
204
+ * Matches a `${secret:NAME}` substitution token in an env value. The captured
205
+ * NAME accepts the same character set as a secret asset name (letters, digits,
206
+ * `_`, `.`, `/`, `-`). Only this exact form is recognised — shell-style
207
+ * `${VAR}` and `$VAR` deliberately do not match and are left untouched.
208
+ */
209
+ const SECRET_TOKEN_RE = /\$\{secret:([A-Za-z0-9_./-]+)\}/g;
210
+ /**
211
+ * Replace every `${secret:NAME}` token in each value with the corresponding
212
+ * secret value, looked up via the injected `resolveSecret`. Keys are never
213
+ * touched. Multiple tokens per value and tokens embedded in larger strings
214
+ * (e.g. `Bearer ${secret:a}:${secret:b}`) are all substituted.
215
+ *
216
+ * `resolveSecret` returns `undefined` for an unknown secret name; such names are
217
+ * collected into `missing` (de-duplicated, in first-seen order) and their tokens
218
+ * are left unsubstituted in the returned values. Callers MUST treat a non-empty
219
+ * `missing` as a hard error and inject NOTHING — never partially inject.
220
+ *
221
+ * The lookup is injected so this module does not import the secret
222
+ * resolver/source machinery directly, preserving its narrow dependency surface.
223
+ * Resolved secret values must never be logged or printed by callers.
224
+ */
225
+ export function resolveSecretTokens(values, resolveSecret) {
226
+ const missing = [];
227
+ const missingSeen = new Set();
228
+ const out = {};
229
+ const cache = new Map();
230
+ for (const [key, value] of Object.entries(values)) {
231
+ out[key] = value.replace(SECRET_TOKEN_RE, (match, name) => {
232
+ let resolved = cache.get(name);
233
+ if (!cache.has(name)) {
234
+ resolved = resolveSecret(name);
235
+ cache.set(name, resolved);
236
+ }
237
+ if (resolved === undefined) {
238
+ if (!missingSeen.has(name)) {
239
+ missingSeen.add(name);
240
+ missing.push(name);
241
+ }
242
+ return match;
243
+ }
244
+ return resolved;
245
+ });
246
+ }
247
+ return { values: out, missing };
248
+ }
249
+ /** Create an empty env file (does nothing if it already exists). */
250
+ export function createEnv(envPath) {
251
+ ensureParentDir(envPath);
252
+ if (fs.existsSync(envPath))
253
+ return;
254
+ writeFileAtomic(envPath, "", 0o600);
255
+ }
256
+ /**
257
+ * Write (create or overwrite) an env file with the given text content,
258
+ * atomically at mode 0600. Used to ingest an existing `.env` file
259
+ * (`env create --from-file` / `--from-stdin`).
260
+ */
261
+ export function writeEnv(envPath, content) {
262
+ ensureParentDir(envPath);
263
+ writeFileAtomic(envPath, content, 0o600);
264
+ }
265
+ /** Remove an env file (and its `.sensitive` marker, if present). Returns true if it existed. */
266
+ export function removeEnv(envPath) {
267
+ if (!fs.existsSync(envPath))
268
+ return false;
269
+ fs.rmSync(envPath);
270
+ const marker = `${envPath}.sensitive`;
271
+ if (fs.existsSync(marker))
272
+ fs.rmSync(marker);
273
+ return true;
274
+ }
275
+ /** A valid env KEY name (same grammar as the assignment scanner). */
276
+ export const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
277
+ /**
278
+ * Build a `KEY=value` assignment line whose value is GUARANTEED to round-trip
279
+ * through `dotenv.parse` — dotenv is the serialisation oracle, so we never
280
+ * write a representation we cannot read back. Candidate representations are
281
+ * tried in order of readability (bare → double-quoted → single-quoted) and the
282
+ * first one `dotenv.parse` recovers exactly is used. If a value contains
283
+ * characters no inline representation can round-trip (e.g. both quote styles),
284
+ * we throw rather than silently corrupt the file.
285
+ */
286
+ function serializeEnvAssignment(key, value) {
287
+ const candidates = [];
288
+ // Bare — only for simple values (no whitespace/quotes/#/$/control chars).
289
+ if (/^[A-Za-z0-9_@%+=:,./-]*$/.test(value))
290
+ candidates.push(`${key}=${value}`);
291
+ // Double-quoted — dotenv expands \n \r \t inside double quotes.
292
+ const dq = value
293
+ .replace(/\\/g, "\\\\")
294
+ .replace(/"/g, '\\"')
295
+ .replace(/\n/g, "\\n")
296
+ .replace(/\r/g, "\\r")
297
+ .replace(/\t/g, "\\t");
298
+ candidates.push(`${key}="${dq}"`);
299
+ // Single-quoted — dotenv takes the content literally (no escapes/expansion).
300
+ candidates.push(`${key}='${value}'`);
301
+ for (const line of candidates) {
302
+ try {
303
+ if (dotenv.parse(line)[key] === value)
304
+ return line;
305
+ }
306
+ catch {
307
+ // Not parseable as written; try the next representation.
308
+ }
309
+ }
310
+ throw new UsageError(`Value for "${key}" cannot be stored inline in a .env file (it contains characters dotenv cannot round-trip). ` +
311
+ "Edit the .env file directly, or choose a different value.");
312
+ }
313
+ /**
314
+ * Assert (using `dotenv.parse` as the oracle) that `after` set `key` to
315
+ * `expected` and left every other key from `before` byte-identical. This
316
+ * catches a line-level edit accidentally disturbing a quoted/multiline value.
317
+ */
318
+ function assertEnvEditSafe(before, after, key) {
319
+ for (const [k, v] of Object.entries(before)) {
320
+ if (k !== key && after[k] !== v) {
321
+ throw new UsageError(`Editing "${key}" would disturb "${k}" (the .env file has a value layout dotenv could not safely round-trip). ` +
322
+ "Edit the .env file directly.");
323
+ }
324
+ }
325
+ }
326
+ /**
327
+ * Set (create or update) a single `KEY=value` entry in an env file, preserving
328
+ * the file's existing lines, comments, and key order. The first existing
329
+ * assignment of `key` is replaced in place; otherwise the entry is appended.
330
+ * Creates the file (and parent dirs) if absent. The value is never logged.
331
+ *
332
+ * The serialised value and the whole resulting file are verified with
333
+ * `dotenv.parse` before the write commits.
334
+ */
335
+ export function setEnvKey(envPath, key, value) {
336
+ ensureParentDir(envPath);
337
+ const existing = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
338
+ const assignment = serializeEnvAssignment(key, value);
339
+ const keyLineRe = new RegExp(`^\\s*(?:export\\s+)?${key}\\s*=`);
340
+ const lines = existing.split(/\r?\n/);
341
+ let replaced = false;
342
+ const out = lines.map((line) => {
343
+ if (!replaced && keyLineRe.test(line)) {
344
+ replaced = true;
345
+ return assignment;
346
+ }
347
+ return line;
348
+ });
349
+ if (!replaced) {
350
+ while (out.length > 0 && out[out.length - 1] === "")
351
+ out.pop();
352
+ out.push(assignment);
353
+ }
354
+ let content = out.join("\n");
355
+ if (!content.endsWith("\n"))
356
+ content += "\n";
357
+ // Verify the edit with dotenv before committing it.
358
+ const after = dotenv.parse(content);
359
+ if (after[key] !== value) {
360
+ throw new UsageError(`Could not set "${key}" reliably (the .env file has a value layout dotenv could not round-trip). ` +
361
+ "Edit the .env file directly.");
362
+ }
363
+ assertEnvEditSafe(dotenv.parse(existing), after, key);
364
+ writeFileAtomic(envPath, content, 0o600);
365
+ }
366
+ /**
367
+ * Remove one or more `KEY=value` entries from an env file, preserving all other
368
+ * lines and comments. Returns which keys were present (removed) vs. absent.
369
+ *
370
+ * The result is verified with `dotenv.parse`: the removed keys are gone and
371
+ * every surviving key is byte-identical to before.
372
+ */
373
+ export function unsetEnvKeys(envPath, keys) {
374
+ if (!fs.existsSync(envPath))
375
+ return { removed: [], missing: keys };
376
+ const text = fs.readFileSync(envPath, "utf8");
377
+ const before = dotenv.parse(text);
378
+ const present = new Set(Object.keys(before));
379
+ const toRemove = new Set(keys);
380
+ const out = text.split(/\r?\n/).filter((line) => {
381
+ const m = line.match(ASSIGN_RE);
382
+ return !(m && toRemove.has(m[1]));
383
+ });
384
+ let content = out.join("\n");
385
+ if (content.length > 0 && !content.endsWith("\n"))
386
+ content += "\n";
387
+ // Verify with dotenv: removed keys gone, survivors unchanged.
388
+ const after = dotenv.parse(content);
389
+ for (const k of toRemove) {
390
+ if (k in after) {
391
+ throw new UsageError(`Could not remove "${k}" reliably (the .env file has a value layout dotenv could not round-trip). ` +
392
+ "Edit the .env file directly.");
393
+ }
394
+ }
395
+ for (const [k, v] of Object.entries(before)) {
396
+ if (!toRemove.has(k) && after[k] !== v) {
397
+ throw new UsageError(`Removing those keys would disturb "${k}" (multiline/quoted value). Edit the .env file directly.`);
398
+ }
399
+ }
400
+ writeFileAtomic(envPath, content, 0o600);
401
+ return {
402
+ removed: keys.filter((k) => present.has(k)),
403
+ missing: keys.filter((k) => !present.has(k)),
404
+ };
405
+ }
406
+ function ensureParentDir(filePath) {
407
+ const dir = path.dirname(filePath);
408
+ if (!fs.existsSync(dir))
409
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
410
+ }
@@ -0,0 +1,259 @@
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
+ * `akm secret` command family. Extracted verbatim from src/cli.ts (WS6) so the
6
+ * God Module shrinks; the `main.subCommands.secret` key and every subcommand's
7
+ * args/output shape are byte-identical. The ref-resolution helpers
8
+ * (parseSecretRef / makeSecretRef / resolveSecretPath, plus the shared
9
+ * findEnvSource) live in src/core/env-secret-ref.ts so env + secret share one
10
+ * copy.
11
+ *
12
+ * `akm secret` manages whole-file secrets under each stash's secrets/ directory.
13
+ * Unlike env files (.env key/value), the ENTIRE file is the secret value. The bytes
14
+ * are NEVER written to stdout or structured output. Values reach a command only
15
+ * via `akm secret run` (injected into a child env var) or `akm secret path`
16
+ * (the Docker /run/secrets + `_FILE` convention).
17
+ */
18
+ import { spawnSync } from "node:child_process";
19
+ import fs from "node:fs";
20
+ import path from "node:path";
21
+ import { defineCommand } from "citty";
22
+ import { getStringArg, hasSubcommand } from "../../cli/parse-args.js";
23
+ import { output, runWithJsonErrors } from "../../cli/shared.js";
24
+ import { deriveCanonicalAssetName } from "../../core/asset/asset-spec.js";
25
+ import { loadConfig } from "../../core/config/config.js";
26
+ import { makeSecretRef, resolveSecretPath } from "../../core/env-secret-ref.js";
27
+ import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
28
+ import { appendEvent } from "../../core/events.js";
29
+ import { resolveSourceEntries } from "../../indexer/search/search-source.js";
30
+ import { getHyphenatedArg } from "../../output/context.js";
31
+ import { readStdin } from "../../runtime.js";
32
+ /** Walk `secrets/` across all stashes, returning one entry per secret file. */
33
+ function listSecretsRecursive() {
34
+ const result = [];
35
+ for (const source of resolveSourceEntries(undefined, loadConfig())) {
36
+ const secretsDir = path.join(source.path, "secrets");
37
+ if (!fs.existsSync(secretsDir))
38
+ continue;
39
+ const walk = (dir) => {
40
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
41
+ const full = path.join(dir, entry.name);
42
+ if (entry.isDirectory()) {
43
+ walk(full);
44
+ continue;
45
+ }
46
+ if (!entry.isFile())
47
+ continue;
48
+ if (entry.name.endsWith(".lock") || entry.name.endsWith(".sensitive"))
49
+ continue;
50
+ // A sibling `<name>.sensitive` marker suppresses listing.
51
+ if (fs.existsSync(`${full}.sensitive`))
52
+ continue;
53
+ const canonical = deriveCanonicalAssetName("secret", secretsDir, full);
54
+ if (!canonical)
55
+ continue;
56
+ result.push({ ref: makeSecretRef(canonical, source), path: full });
57
+ }
58
+ };
59
+ walk(secretsDir);
60
+ }
61
+ return result;
62
+ }
63
+ const secretListCommand = defineCommand({
64
+ meta: {
65
+ name: "list",
66
+ description: "List all secrets across all stashes by name (the file contents are never shown)",
67
+ },
68
+ run() {
69
+ return runWithJsonErrors(async () => {
70
+ output("secret-list", { secrets: listSecretsRecursive() });
71
+ });
72
+ },
73
+ });
74
+ const secretSetCommand = defineCommand({
75
+ meta: {
76
+ name: "set",
77
+ description: "Create or overwrite a secret. The value is read from stdin by default (never via argv). Use --from-file <path> to import an existing file byte-exact, or --from-env <VAR> to read from an environment variable. Multi-line values are allowed.",
78
+ },
79
+ args: {
80
+ ref: {
81
+ type: "positional",
82
+ description: "Secret ref (flat name, e.g. secret:deploy-key or just deploy-key; use --path for a subdirectory)",
83
+ required: true,
84
+ },
85
+ path: {
86
+ type: "string",
87
+ description: "Relative subdirectory under secrets/ to place the secret in (e.g. 'team'). The filename comes from the name.",
88
+ },
89
+ "from-file": { type: "string", description: "Read the value from this file (stored byte-exact)" },
90
+ "from-env": { type: "string", description: "Read the value from the named environment variable" },
91
+ },
92
+ run({ args }) {
93
+ return runWithJsonErrors(async () => {
94
+ const { setSecret } = await import("./secret.js");
95
+ const { name, absPath, source } = resolveSecretPath(args.ref, { subPath: getStringArg(args, "path") });
96
+ const fromEnv = getHyphenatedArg(args, "from-env");
97
+ const fromFile = getHyphenatedArg(args, "from-file");
98
+ if (fromEnv !== undefined && fromFile !== undefined) {
99
+ throw new UsageError("Pass only one of --from-file or --from-env (or use stdin).", "INVALID_FLAG_VALUE");
100
+ }
101
+ const MAX_SECRET_BYTES = 5 * 1024 * 1024; // 5 MB
102
+ let value;
103
+ if (fromFile !== undefined) {
104
+ if (!fs.existsSync(fromFile)) {
105
+ throw new NotFoundError(`File not found: ${fromFile}`, "FILE_NOT_FOUND");
106
+ }
107
+ value = fs.readFileSync(fromFile);
108
+ if (value.byteLength > MAX_SECRET_BYTES) {
109
+ throw new UsageError("Secret exceeds the 5 MB limit.");
110
+ }
111
+ }
112
+ else if (fromEnv !== undefined) {
113
+ const envVal = process.env[fromEnv];
114
+ if (envVal === undefined) {
115
+ throw new UsageError(`Environment variable "${fromEnv}" is not set.`, "INVALID_FLAG_VALUE");
116
+ }
117
+ value = Buffer.from(envVal, "utf8");
118
+ }
119
+ else {
120
+ if (process.stdin.isTTY) {
121
+ process.stderr.write(`Enter value for secret "${name}" (Ctrl-D when done):\n`);
122
+ }
123
+ const stdinBuf = await readStdin(MAX_SECRET_BYTES, () => new UsageError("Secret exceeds the 5 MB limit."));
124
+ // Strip a single trailing newline so `echo "$TOKEN" | akm secret set`
125
+ // stores the token without the shell-added newline. Use --from-file for
126
+ // byte-exact storage of multi-line material (PEM keys, certs).
127
+ value = Buffer.from(stdinBuf.toString("utf8").replace(/\n$/, ""), "utf8");
128
+ }
129
+ setSecret(absPath, value);
130
+ output("secret-set", { ref: makeSecretRef(name, source) });
131
+ });
132
+ },
133
+ });
134
+ const secretPathCommand = defineCommand({
135
+ meta: {
136
+ name: "path",
137
+ description: "Print the absolute secret file path for the Docker `_FILE` convention, e.g. `MY_SECRET_FILE=$(akm secret path secret:deploy-key)`.",
138
+ },
139
+ args: {
140
+ ref: { type: "positional", description: "Secret ref", required: true },
141
+ },
142
+ run({ args }) {
143
+ return runWithJsonErrors(async () => {
144
+ const { name, absPath, source } = resolveSecretPath(args.ref);
145
+ if (!fs.existsSync(absPath)) {
146
+ throw new NotFoundError(`Secret not found: ${makeSecretRef(name, source)}`);
147
+ }
148
+ process.stdout.write(`${absPath}\n`);
149
+ });
150
+ },
151
+ });
152
+ const secretRunCommand = defineCommand({
153
+ meta: {
154
+ name: "run",
155
+ description: "Run a command with a secret's value injected into an env var: `akm secret run <ref> <VAR> -- <command>`. The value is set as $VAR in the child process only.",
156
+ },
157
+ args: {
158
+ ref: { type: "positional", description: "Secret ref", required: true },
159
+ var: { type: "positional", description: "Environment variable name to inject the value into", required: true },
160
+ },
161
+ run({ args }) {
162
+ return runWithJsonErrors(async () => {
163
+ // Validate the target env var name FIRST (before the command split) so a
164
+ // dangerous/invalid name is rejected regardless of how the command is
165
+ // supplied — and so the failure does not depend on argv parsing.
166
+ const varName = args.var;
167
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(varName)) {
168
+ throw new UsageError(`"${varName}" is not a valid environment variable name.`, "INVALID_FLAG_VALUE");
169
+ }
170
+ const { isDangerousEnvKey } = await import("../lint/env-key-rules.js");
171
+ if (isDangerousEnvKey(varName)) {
172
+ throw new UsageError(`Refusing to inject a secret into "${varName}": it is a known process-hijacking variable (e.g. LD_PRELOAD, PATH).`, "INVALID_FLAG_VALUE");
173
+ }
174
+ const dashIndex = process.argv.indexOf("--");
175
+ if (dashIndex < 0 || dashIndex === process.argv.length - 1) {
176
+ throw new UsageError("Missing command. Usage: akm secret run <ref> <VAR> -- <command>");
177
+ }
178
+ const command = process.argv.slice(dashIndex + 1);
179
+ const { name, absPath, source } = resolveSecretPath(args.ref);
180
+ if (!fs.existsSync(absPath)) {
181
+ throw new NotFoundError(`Secret not found: ${makeSecretRef(name, source)}`);
182
+ }
183
+ const { readValue } = await import("./secret.js");
184
+ const mergedEnv = { ...process.env };
185
+ mergedEnv[varName] = readValue(absPath).toString("utf8");
186
+ // Audit trail: record access by ref + var name only — never the value.
187
+ appendEvent({
188
+ eventType: "secret_access",
189
+ ref: makeSecretRef(name, source),
190
+ metadata: { var: varName },
191
+ });
192
+ const result = spawnSync(command[0], command.slice(1), {
193
+ stdio: "inherit",
194
+ env: mergedEnv,
195
+ });
196
+ if (result.error) {
197
+ const err = result.error;
198
+ if (err.code === "ENOENT") {
199
+ throw new NotFoundError(`Command not found: ${command[0]}`, "FILE_NOT_FOUND", `Install '${command[0]}' or add its directory to PATH before invoking 'akm secret run'.`);
200
+ }
201
+ if (err.code === "EACCES") {
202
+ throw new ConfigError(`Command not executable: ${command[0]}`, "STASH_DIR_UNREADABLE", `Add execute permission ('chmod +x ${command[0]}') or invoke via an interpreter.`);
203
+ }
204
+ throw err;
205
+ }
206
+ process.exit(result.status ?? 0);
207
+ });
208
+ },
209
+ });
210
+ const secretRemoveCommand = defineCommand({
211
+ meta: { name: "remove", description: "Remove a secret (and its .sensitive marker, if any)" },
212
+ args: {
213
+ ref: { type: "positional", description: "Secret ref", required: true },
214
+ yes: { type: "boolean", alias: "y", description: "Skip confirmation prompt", default: false },
215
+ },
216
+ run({ args }) {
217
+ return runWithJsonErrors(async () => {
218
+ const { name, absPath, source } = resolveSecretPath(args.ref);
219
+ const { confirmDestructive } = await import("../../cli/confirm.js");
220
+ const confirmed = await confirmDestructive(`Remove secret "${args.ref}"? This cannot be undone.`, {
221
+ yes: args.yes === true,
222
+ });
223
+ if (!confirmed) {
224
+ process.stderr.write("Aborted.\n");
225
+ return;
226
+ }
227
+ const { removeSecret } = await import("./secret.js");
228
+ if (!fs.existsSync(absPath)) {
229
+ throw new NotFoundError(`Secret not found: ${makeSecretRef(name, source)}`);
230
+ }
231
+ const removed = removeSecret(absPath);
232
+ output("secret-remove", { ref: makeSecretRef(name, source), removed });
233
+ });
234
+ },
235
+ });
236
+ // Single source of truth: the routing set is derived from the subCommands keys
237
+ // (M10) so adding a subcommand can never silently desync from `hasSubcommand`.
238
+ const secretSubCommands = {
239
+ list: secretListCommand,
240
+ path: secretPathCommand,
241
+ run: secretRunCommand,
242
+ set: secretSetCommand,
243
+ remove: secretRemoveCommand,
244
+ };
245
+ const SECRET_SUBCOMMAND_SET = new Set(Object.keys(secretSubCommands));
246
+ export const secretCommand = defineCommand({
247
+ meta: {
248
+ name: "secret",
249
+ description: "Manage secrets — a single sensitive value used on its own for authentication (an API token, a PEM private key, a TLS cert), one value per file. Names are visible; the file contents are the value and never appear in structured output. For a group of related configuration loaded together, use `akm env`.",
250
+ },
251
+ subCommands: secretSubCommands,
252
+ run({ args }) {
253
+ return runWithJsonErrors(async () => {
254
+ if (hasSubcommand(args, SECRET_SUBCOMMAND_SET))
255
+ return;
256
+ output("secret-list", { secrets: listSecretsRecursive() });
257
+ });
258
+ },
259
+ });