akm-cli 0.9.0-beta.9 → 0.9.0-rc.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 (381) hide show
  1. package/CHANGELOG.md +715 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/help/help-improve.md +9 -6
  6. package/dist/assets/hints/cli-hints-full.md +6 -5
  7. package/dist/assets/profiles/default.json +9 -4
  8. package/dist/assets/profiles/frequent.json +1 -1
  9. package/dist/assets/profiles/memory-focus.json +1 -1
  10. package/dist/assets/profiles/proactive-maintenance.json +25 -0
  11. package/dist/assets/profiles/quick.json +1 -1
  12. package/dist/assets/profiles/recombine-only.json +21 -0
  13. package/dist/assets/profiles/reflect-distill.json +30 -0
  14. package/dist/assets/profiles/synthesize.json +15 -0
  15. package/dist/assets/profiles/thorough.json +1 -1
  16. package/dist/assets/prompts/consolidate-system.md +23 -0
  17. package/dist/assets/prompts/contradiction-judge.md +33 -0
  18. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  19. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  20. package/dist/assets/prompts/extract-session.md +11 -3
  21. package/dist/assets/prompts/graph-extract-system.md +1 -0
  22. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  23. package/dist/assets/prompts/memory-infer-system.md +1 -0
  24. package/dist/assets/prompts/memory-infer-user.md +5 -0
  25. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  26. package/dist/assets/prompts/procedural-system.md +44 -0
  27. package/dist/assets/prompts/recombine-system.md +40 -0
  28. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  29. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  30. package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  39. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  40. package/dist/assets/templates/html/health.html +281 -111
  41. package/dist/assets/wiki/ingest-workflow-template.md +45 -16
  42. package/dist/assets/wiki/schema-template.md +4 -4
  43. package/dist/cli/clack.js +56 -0
  44. package/dist/cli/config-migrate.js +7 -1
  45. package/dist/cli/confirm.js +1 -1
  46. package/dist/cli/parse-args.js +46 -1
  47. package/dist/cli/shared.js +28 -0
  48. package/dist/cli.js +25 -21
  49. package/dist/commands/agent/agent-dispatch.js +3 -2
  50. package/dist/commands/agent/agent-support.js +0 -7
  51. package/dist/commands/agent/contribute-cli.js +26 -7
  52. package/dist/commands/config-cli.js +26 -13
  53. package/dist/commands/env/child-env.js +47 -0
  54. package/dist/commands/env/env-binding.js +95 -0
  55. package/dist/commands/env/env-cli.js +228 -292
  56. package/dist/commands/env/env.js +14 -67
  57. package/dist/commands/env/secret-cli.js +140 -138
  58. package/dist/commands/feedback-cli.js +156 -155
  59. package/dist/commands/graph/graph-cli.js +5 -13
  60. package/dist/commands/graph/graph.js +3 -3
  61. package/dist/commands/health/advisories.js +151 -0
  62. package/dist/commands/health/checks.js +103 -16
  63. package/dist/commands/health/html-report.js +447 -81
  64. package/dist/commands/health/improve-metrics.js +771 -0
  65. package/dist/commands/health/llm-usage.js +65 -0
  66. package/dist/commands/health/md-report.js +103 -0
  67. package/dist/commands/health/metrics.js +278 -0
  68. package/dist/commands/health/stash-exposure.js +46 -0
  69. package/dist/commands/health/surfaces.js +216 -0
  70. package/dist/commands/health/task-runs.js +135 -0
  71. package/dist/commands/health/types.js +26 -0
  72. package/dist/commands/health/windows.js +195 -0
  73. package/dist/commands/health.js +91 -1091
  74. package/dist/commands/improve/anti-collapse.js +170 -0
  75. package/dist/commands/improve/calibration.js +161 -0
  76. package/dist/commands/improve/collapse-detector.js +421 -0
  77. package/dist/commands/improve/consolidate/chunking.js +141 -0
  78. package/dist/commands/improve/consolidate/eligibility.js +64 -0
  79. package/dist/commands/improve/consolidate/merge.js +145 -0
  80. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  81. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  82. package/dist/commands/improve/consolidate.js +1295 -1277
  83. package/dist/commands/improve/dedup.js +482 -0
  84. package/dist/commands/improve/distill/content-repair.js +202 -0
  85. package/dist/commands/improve/distill/promote-memory.js +229 -0
  86. package/dist/commands/improve/distill/quality-gate.js +236 -0
  87. package/dist/commands/improve/distill-guards.js +127 -0
  88. package/dist/commands/improve/distill-promotion-policy.js +826 -167
  89. package/dist/commands/improve/distill.js +228 -605
  90. package/dist/commands/improve/eligibility.js +434 -0
  91. package/dist/commands/improve/encoding-salience.js +205 -0
  92. package/dist/commands/improve/extract-cli.js +179 -59
  93. package/dist/commands/improve/extract-prompt.js +54 -3
  94. package/dist/commands/improve/extract-watch.js +140 -0
  95. package/dist/commands/improve/extract.js +409 -43
  96. package/dist/commands/improve/feedback-valence.js +54 -0
  97. package/dist/commands/improve/hot-probation.js +45 -0
  98. package/dist/commands/improve/improve-auto-accept.js +157 -10
  99. package/dist/commands/improve/improve-cli.js +115 -73
  100. package/dist/commands/improve/improve-profiles.js +28 -8
  101. package/dist/commands/improve/improve-result-file.js +15 -25
  102. package/dist/commands/improve/improve-session.js +58 -0
  103. package/dist/commands/improve/improve.js +485 -2764
  104. package/dist/commands/improve/locks.js +154 -0
  105. package/dist/commands/improve/loop-stages.js +1100 -0
  106. package/dist/commands/improve/memory/memory-belief.js +14 -15
  107. package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
  108. package/dist/commands/improve/memory/memory-improve.js +27 -27
  109. package/dist/commands/improve/outcome-loop.js +270 -0
  110. package/dist/commands/improve/preparation.js +2002 -0
  111. package/dist/commands/improve/proactive-maintenance.js +37 -35
  112. package/dist/commands/improve/procedural.js +398 -0
  113. package/dist/commands/improve/recombine.js +818 -0
  114. package/dist/commands/improve/reflect-noise.js +0 -0
  115. package/dist/commands/improve/reflect.js +206 -45
  116. package/dist/commands/improve/salience.js +455 -0
  117. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  118. package/dist/commands/improve/shared.js +51 -0
  119. package/dist/commands/improve/triage.js +93 -0
  120. package/dist/commands/lint/agent-linter.js +19 -24
  121. package/dist/commands/lint/base-linter.js +173 -60
  122. package/dist/commands/lint/command-linter.js +19 -24
  123. package/dist/commands/lint/env-key-rules.js +38 -1
  124. package/dist/commands/lint/fact-linter.js +39 -0
  125. package/dist/commands/lint/index.js +31 -13
  126. package/dist/commands/lint/memory-linter.js +1 -1
  127. package/dist/commands/lint/registry.js +7 -2
  128. package/dist/commands/lint/task-linter.js +3 -3
  129. package/dist/commands/lint/workflow-linter.js +26 -1
  130. package/dist/commands/observability-cli.js +4 -4
  131. package/dist/commands/proposal/drain-policies.js +13 -4
  132. package/dist/commands/proposal/drain.js +45 -51
  133. package/dist/commands/proposal/legacy-import.js +115 -0
  134. package/dist/commands/proposal/proposal-cli.js +24 -34
  135. package/dist/commands/proposal/proposal.js +2 -1
  136. package/dist/commands/proposal/propose.js +8 -3
  137. package/dist/commands/proposal/repository.js +829 -0
  138. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  139. package/dist/commands/proposal/validators/proposals.js +93 -895
  140. package/dist/commands/read/curate.js +410 -111
  141. package/dist/commands/read/knowledge.js +10 -3
  142. package/dist/commands/read/remember-cli.js +133 -138
  143. package/dist/commands/read/search-cli.js +15 -8
  144. package/dist/commands/read/search.js +22 -11
  145. package/dist/commands/read/show.js +106 -14
  146. package/dist/commands/registry-cli.js +76 -87
  147. package/dist/commands/remember.js +11 -12
  148. package/dist/commands/sources/add-cli.js +91 -95
  149. package/dist/commands/sources/history.js +1 -1
  150. package/dist/commands/sources/init.js +66 -18
  151. package/dist/commands/sources/installed-stashes.js +11 -3
  152. package/dist/commands/sources/migration-help.js +7 -4
  153. package/dist/commands/sources/schema-repair.js +44 -46
  154. package/dist/commands/sources/self-update.js +2 -2
  155. package/dist/commands/sources/source-add.js +7 -3
  156. package/dist/commands/sources/sources-cli.js +3 -3
  157. package/dist/commands/sources/stash-cli.js +19 -39
  158. package/dist/commands/sources/stash-skeleton.js +57 -8
  159. package/dist/commands/tasks/default-tasks.js +15 -2
  160. package/dist/commands/tasks/tasks-cli.js +20 -29
  161. package/dist/commands/tasks/tasks.js +39 -11
  162. package/dist/commands/wiki-cli.js +23 -38
  163. package/dist/commands/workflow-cli.js +291 -13
  164. package/dist/core/asset/asset-registry.js +3 -1
  165. package/dist/core/asset/asset-spec.js +79 -5
  166. package/dist/core/asset/frontmatter.js +188 -167
  167. package/dist/core/asset/markdown.js +8 -0
  168. package/dist/core/authoring-rules.js +92 -0
  169. package/dist/core/common.js +4 -23
  170. package/dist/core/concurrent.js +10 -1
  171. package/dist/core/config/config-io.js +10 -1
  172. package/dist/core/config/config-migration.js +18 -40
  173. package/dist/core/config/config-schema.js +403 -62
  174. package/dist/core/config/config-types.js +3 -3
  175. package/dist/core/config/config.js +67 -22
  176. package/dist/core/deep-merge.js +38 -0
  177. package/dist/core/errors.js +1 -0
  178. package/dist/core/eval/rank-metrics.js +113 -0
  179. package/dist/core/events.js +4 -7
  180. package/dist/core/improve-types.js +47 -8
  181. package/dist/core/json-schema.js +142 -0
  182. package/dist/core/logs-db.js +14 -75
  183. package/dist/core/parse.js +36 -16
  184. package/dist/core/paths.js +18 -18
  185. package/dist/core/standards/resolve-standards-context.js +87 -0
  186. package/dist/core/standards/resolve-stash-standards.js +99 -0
  187. package/dist/core/standards/resolve-type-conventions.js +66 -0
  188. package/dist/core/state/migrations.js +770 -0
  189. package/dist/core/state-db.js +132 -1126
  190. package/dist/core/structured.js +69 -0
  191. package/dist/core/time.js +53 -0
  192. package/dist/core/warn.js +21 -0
  193. package/dist/core/write-source.js +37 -0
  194. package/dist/indexer/db/db.js +261 -770
  195. package/dist/indexer/db/entry-mapper.js +41 -0
  196. package/dist/indexer/db/graph-db.js +129 -86
  197. package/dist/indexer/db/llm-cache.js +2 -2
  198. package/dist/indexer/db/schema.js +516 -0
  199. package/dist/indexer/ensure-index.js +36 -92
  200. package/dist/indexer/feedback/utility-policy.js +75 -0
  201. package/dist/indexer/graph/graph-boost.js +51 -41
  202. package/dist/indexer/graph/graph-extraction.js +207 -4
  203. package/dist/indexer/index-writer-lock.js +18 -11
  204. package/dist/indexer/index-written-assets.js +105 -0
  205. package/dist/indexer/indexer.js +182 -204
  206. package/dist/indexer/passes/dir-staleness.js +114 -0
  207. package/dist/indexer/passes/memory-inference.js +13 -5
  208. package/dist/indexer/passes/metadata.js +20 -0
  209. package/dist/indexer/read-preflight.js +23 -0
  210. package/dist/indexer/search/db-search.js +89 -13
  211. package/dist/indexer/search/fts-query.js +51 -0
  212. package/dist/indexer/search/ranking-contributors.js +95 -9
  213. package/dist/indexer/search/ranking.js +79 -3
  214. package/dist/indexer/search/search-fields.js +6 -0
  215. package/dist/indexer/search/search-source.js +32 -21
  216. package/dist/indexer/search/semantic-status.js +4 -0
  217. package/dist/indexer/walk/matchers.js +48 -0
  218. package/dist/indexer/walk/walker.js +21 -13
  219. package/dist/integrations/agent/builders.js +41 -13
  220. package/dist/integrations/agent/config.js +20 -59
  221. package/dist/integrations/agent/detect.js +9 -0
  222. package/dist/integrations/agent/index.js +3 -19
  223. package/dist/integrations/agent/model-aliases.js +16 -2
  224. package/dist/integrations/agent/profiles.js +79 -6
  225. package/dist/integrations/agent/prompts.js +75 -9
  226. package/dist/integrations/agent/runner-dispatch.js +83 -0
  227. package/dist/integrations/agent/runner.js +13 -9
  228. package/dist/integrations/agent/spawn.js +206 -81
  229. package/dist/integrations/harnesses/aider/agent-builder.js +113 -0
  230. package/dist/integrations/harnesses/aider/index.js +58 -0
  231. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  232. package/dist/integrations/harnesses/amazonq/agent-builder.js +153 -0
  233. package/dist/integrations/harnesses/amazonq/index.js +59 -0
  234. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  235. package/dist/integrations/harnesses/claude/agent-builder.js +46 -7
  236. package/dist/integrations/harnesses/claude/index.js +27 -23
  237. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  238. package/dist/integrations/harnesses/claude/session-log.js +10 -0
  239. package/dist/integrations/harnesses/codex/agent-builder.js +137 -0
  240. package/dist/integrations/harnesses/codex/index.js +63 -0
  241. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  242. package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
  243. package/dist/integrations/harnesses/copilot/index.js +60 -0
  244. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  245. package/dist/integrations/harnesses/gemini/agent-builder.js +121 -0
  246. package/dist/integrations/harnesses/gemini/index.js +60 -0
  247. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  248. package/dist/integrations/harnesses/index.js +28 -7
  249. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
  250. package/dist/integrations/harnesses/opencode/index.js +17 -16
  251. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  252. package/dist/integrations/harnesses/opencode-sdk/harness.js +65 -0
  253. package/dist/integrations/harnesses/opencode-sdk/index.js +10 -34
  254. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +642 -71
  255. package/dist/integrations/harnesses/openhands/agent-builder.js +126 -0
  256. package/dist/integrations/harnesses/openhands/index.js +58 -0
  257. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  258. package/dist/integrations/harnesses/pi/agent-builder.js +104 -0
  259. package/dist/integrations/harnesses/pi/index.js +58 -0
  260. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  261. package/dist/integrations/harnesses/types.js +8 -0
  262. package/dist/integrations/session-logs/index.js +40 -11
  263. package/dist/llm/call-ai.js +2 -2
  264. package/dist/llm/client.js +34 -11
  265. package/dist/llm/embedder.js +67 -4
  266. package/dist/llm/embedders/cache.js +3 -1
  267. package/dist/llm/embedders/deterministic.js +66 -0
  268. package/dist/llm/embedders/local.js +73 -3
  269. package/dist/llm/feature-gate.js +16 -15
  270. package/dist/llm/graph-extract.js +67 -44
  271. package/dist/llm/memory-infer-impl.js +138 -0
  272. package/dist/llm/memory-infer.js +1 -127
  273. package/dist/llm/metadata-enhance.js +44 -31
  274. package/dist/llm/structured-call.js +49 -0
  275. package/dist/migrate-storage-node.mjs +8 -0
  276. package/dist/output/context.js +5 -5
  277. package/dist/output/renderers.js +87 -15
  278. package/dist/output/shapes/curate.js +14 -2
  279. package/dist/output/shapes/helpers.js +0 -3
  280. package/dist/output/shapes/passthrough.js +6 -1
  281. package/dist/output/text/helpers.js +241 -2
  282. package/dist/output/text/workflow.js +4 -1
  283. package/dist/registry/providers/skills-sh.js +21 -147
  284. package/dist/registry/providers/static-index.js +15 -157
  285. package/dist/registry/resolve.js +27 -9
  286. package/dist/runtime.js +25 -1
  287. package/dist/schemas/akm-config.json +14225 -0
  288. package/dist/schemas/akm-workflow.json +328 -0
  289. package/dist/scripts/migrate-storage.js +2743 -8390
  290. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +1652 -607
  291. package/dist/setup/detect.js +9 -0
  292. package/dist/setup/legacy-config.js +106 -0
  293. package/dist/setup/prompt.js +57 -0
  294. package/dist/setup/providers.js +14 -0
  295. package/dist/setup/registry-stash-loader.js +12 -0
  296. package/dist/setup/semantic-assets.js +124 -0
  297. package/dist/setup/setup.js +52 -1614
  298. package/dist/setup/steps/connection.js +734 -0
  299. package/dist/setup/steps/output.js +31 -0
  300. package/dist/setup/steps/platforms.js +124 -0
  301. package/dist/setup/steps/semantic.js +27 -0
  302. package/dist/setup/steps/sources.js +222 -0
  303. package/dist/setup/steps/stashdir.js +42 -0
  304. package/dist/setup/steps/tasks.js +152 -0
  305. package/dist/sources/include.js +6 -2
  306. package/dist/sources/providers/filesystem.js +0 -1
  307. package/dist/sources/providers/git-install.js +210 -0
  308. package/dist/sources/providers/git-provider.js +234 -0
  309. package/dist/sources/providers/git-stash.js +248 -0
  310. package/dist/sources/providers/git.js +10 -661
  311. package/dist/sources/providers/npm.js +2 -6
  312. package/dist/sources/providers/provider-utils.js +13 -7
  313. package/dist/sources/providers/sync-from-ref.js +9 -1
  314. package/dist/sources/providers/website.js +9 -5
  315. package/dist/sources/website-ingest.js +187 -29
  316. package/dist/sources/wiki-fetchers/registry.js +53 -0
  317. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  318. package/dist/storage/database.js +45 -10
  319. package/dist/storage/managed-db.js +82 -0
  320. package/dist/storage/repositories/canaries-repository.js +107 -0
  321. package/dist/storage/repositories/consolidation-repository.js +38 -0
  322. package/dist/storage/repositories/embeddings-repository.js +72 -0
  323. package/dist/storage/repositories/events-repository.js +187 -0
  324. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  325. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  326. package/dist/storage/repositories/index-db.js +14 -8
  327. package/dist/storage/repositories/proposals-repository.js +220 -0
  328. package/dist/storage/repositories/recombine-repository.js +213 -0
  329. package/dist/storage/repositories/registry-cache.js +93 -0
  330. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  331. package/dist/storage/repositories/task-history-repository.js +93 -0
  332. package/dist/storage/repositories/workflow-runs-repository.js +189 -1
  333. package/dist/storage/sqlite-pragmas.js +146 -0
  334. package/dist/tasks/backends/cron.js +1 -1
  335. package/dist/tasks/backends/index.js +9 -0
  336. package/dist/tasks/backends/launchd.js +1 -1
  337. package/dist/tasks/backends/schtasks.js +1 -1
  338. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  339. package/dist/tasks/runner.js +15 -13
  340. package/dist/text-import-hook.mjs +1 -1
  341. package/dist/wiki/wiki.js +52 -11
  342. package/dist/workflows/authoring/authoring.js +123 -10
  343. package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
  344. package/dist/workflows/cli.js +5 -0
  345. package/dist/workflows/db.js +138 -4
  346. package/dist/workflows/exec/brief.js +484 -0
  347. package/dist/workflows/exec/native-executor.js +975 -0
  348. package/dist/workflows/exec/param-secrets.js +115 -0
  349. package/dist/workflows/exec/report.js +1295 -0
  350. package/dist/workflows/exec/run-workflow.js +596 -0
  351. package/dist/workflows/exec/scheduler.js +100 -0
  352. package/dist/workflows/exec/step-work.js +1156 -0
  353. package/dist/workflows/exec/unit-writer.js +23 -0
  354. package/dist/workflows/exec/watch.js +116 -0
  355. package/dist/workflows/exec/worktree.js +171 -0
  356. package/dist/workflows/ir/compile.js +388 -0
  357. package/dist/workflows/ir/params.js +54 -0
  358. package/dist/workflows/ir/plan-hash.js +33 -0
  359. package/dist/workflows/ir/schema.js +4 -0
  360. package/dist/workflows/parser.js +3 -1
  361. package/dist/workflows/program/expressions.js +369 -0
  362. package/dist/workflows/program/parser.js +760 -0
  363. package/dist/workflows/program/project.js +105 -0
  364. package/dist/workflows/program/schema.js +54 -0
  365. package/dist/workflows/renderer.js +82 -5
  366. package/dist/workflows/runtime/agent-identity.js +59 -14
  367. package/dist/workflows/runtime/runs.js +248 -153
  368. package/dist/workflows/runtime/unit-checkin.js +45 -0
  369. package/dist/workflows/runtime/workflow-asset-loader.js +188 -0
  370. package/dist/workflows/validate-summary.js +26 -10
  371. package/dist/workflows/validator.js +1 -1
  372. package/docs/README.md +69 -18
  373. package/docs/data-and-telemetry.md +7 -5
  374. package/docs/migration/release-notes/0.7.0.md +1 -1
  375. package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
  376. package/docs/migration/release-notes/0.9.0.md +39 -0
  377. package/package.json +10 -10
  378. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  379. package/dist/commands/db-cli.js +0 -23
  380. package/dist/indexer/db/db-backup.js +0 -376
  381. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -10,28 +10,28 @@
10
10
  * copy.
11
11
  *
12
12
  * `akm env` manages whole `.env` files under each stash's env/ directory.
13
- * Values are NEVER written to stdout or structured output — only key NAMES and
14
- * start-of-line comments are surfaced. akm does not manage individual entries;
13
+ * Values and comment text are NEVER written to stdout or structured output —
14
+ * only key NAMES are surfaced (comments routinely contain commented-out
15
+ * credentials). akm does not manage individual entries;
15
16
  * you edit the `.env` file yourself and akm loads it. Replaced the deprecated
16
17
  * `vault` type (removed in 0.9.0).
17
18
  */
18
19
  import { spawnSync } from "node:child_process";
19
20
  import fs from "node:fs";
20
21
  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";
22
+ import { getStringArg } from "../../cli/parse-args.js";
23
+ import { defineGroupCommand, defineJsonCommand, output } from "../../cli/shared.js";
24
24
  import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "../../core/asset/asset-create.js";
25
25
  import { deriveCanonicalAssetName, resolveAssetPathFromName } from "../../core/asset/asset-spec.js";
26
26
  import { isWithin, writeFileAtomic } from "../../core/common.js";
27
27
  import { loadConfig } from "../../core/config/config.js";
28
28
  import { findEnvSource, makeEnvRef, parseEnvRef, resolveEnvPath } from "../../core/env-secret-ref.js";
29
29
  import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
30
- import { appendEvent } from "../../core/events.js";
31
30
  import { isQuiet } from "../../core/warn.js";
32
31
  import { resolveSourceEntries } from "../../indexer/search/search-source.js";
33
- import { getHyphenatedArg, parseFlagValue } from "../../output/context.js";
32
+ import { parseFlagValue } from "../../output/context.js";
34
33
  import { readStdin } from "../../runtime.js";
34
+ import { buildChildEnv } from "./child-env.js";
35
35
  /**
36
36
  * Walk each stash's env files and return one entry per `.env` file, using the
37
37
  * env asset spec's canonical-name logic (e.g. `env/team/prod.env` →
@@ -69,16 +69,14 @@ function listEnvsRecursive(listKeysFn) {
69
69
  }
70
70
  return result;
71
71
  }
72
- const envListCommand = defineCommand({
72
+ const envListCommand = defineJsonCommand({
73
73
  meta: { name: "list", description: "List all env files across all stashes with their key names (no values)" },
74
- run() {
75
- return runWithJsonErrors(async () => {
76
- const { listKeys } = await import("./env.js");
77
- output("env-list", { envs: listEnvsRecursive(listKeys) });
78
- });
74
+ async run() {
75
+ const { listKeys } = await import("./env.js");
76
+ output("env-list", { envs: listEnvsRecursive(listKeys) });
79
77
  },
80
78
  });
81
- const envCreateCommand = defineCommand({
79
+ const envCreateCommand = defineJsonCommand({
82
80
  meta: {
83
81
  name: "create",
84
82
  description: "Create an env file (empty by default; seed an existing `.env` with --from-file or --from-stdin). No-op if it already exists and no source is given.",
@@ -101,58 +99,56 @@ const envCreateCommand = defineCommand({
101
99
  default: false,
102
100
  },
103
101
  },
104
- run({ args }) {
105
- return runWithJsonErrors(async () => {
106
- const { createEnv, writeEnv } = await import("./env.js");
107
- // `create` always targets env/, never the frozen vaults/ copy.
108
- const parsed = parseEnvRef(args.name);
109
- // `name` is flat; subdirectory placement is `--path`'s job.
110
- assertFlatAssetName(parsed.name);
111
- parsed.name = combineCreatePath(normalizeCreateSubPath(getStringArg(args, "path")), parsed.name);
112
- const source = findEnvSource(parsed.origin);
113
- const envRoot = path.join(source.path, "env");
114
- const absPath = resolveAssetPathFromName("env", envRoot, parsed.name);
115
- if (!isWithin(absPath, envRoot)) {
116
- throw new UsageError(`Env name "${parsed.name}" escapes the env directory.`);
117
- }
118
- const fromFile = getHyphenatedArg(args, "from-file");
119
- const fromStdin = getHyphenatedArg(args, "from-stdin") === true;
120
- if (fromFile !== undefined && fromStdin) {
121
- throw new UsageError("Pass only one of --from-file or --from-stdin.", "INVALID_FLAG_VALUE");
102
+ async run({ args }) {
103
+ const { createEnv, writeEnv } = await import("./env.js");
104
+ // `create` always targets env/, never the frozen vaults/ copy.
105
+ const parsed = parseEnvRef(args.name);
106
+ // `name` is flat; subdirectory placement is `--path`'s job.
107
+ assertFlatAssetName(parsed.name);
108
+ parsed.name = combineCreatePath(normalizeCreateSubPath(getStringArg(args, "path")), parsed.name);
109
+ const source = findEnvSource(parsed.origin);
110
+ const envRoot = path.join(source.path, "env");
111
+ const absPath = resolveAssetPathFromName("env", envRoot, parsed.name);
112
+ if (!isWithin(absPath, envRoot)) {
113
+ throw new UsageError(`Env name "${parsed.name}" escapes the env directory.`);
114
+ }
115
+ const fromFile = args["from-file"];
116
+ const fromStdin = args["from-stdin"] === true;
117
+ if (fromFile !== undefined && fromStdin) {
118
+ throw new UsageError("Pass only one of --from-file or --from-stdin.", "INVALID_FLAG_VALUE");
119
+ }
120
+ if (fromFile !== undefined || fromStdin) {
121
+ // Ingest path: never silently clobber an existing env file.
122
+ if (fs.existsSync(absPath)) {
123
+ throw new UsageError(`Env "${makeEnvRef(parsed.name, source)}" already exists. Remove it first (\`akm env remove\`) or edit the file directly.`, "RESOURCE_ALREADY_EXISTS");
122
124
  }
123
- if (fromFile !== undefined || fromStdin) {
124
- // Ingest path: never silently clobber an existing env file.
125
- if (fs.existsSync(absPath)) {
126
- throw new UsageError(`Env "${makeEnvRef(parsed.name, source)}" already exists. Remove it first (\`akm env remove\`) or edit the file directly.`, "RESOURCE_ALREADY_EXISTS");
127
- }
128
- let content;
129
- if (fromFile !== undefined) {
130
- if (!fs.existsSync(fromFile)) {
131
- throw new NotFoundError(`Source file not found: ${fromFile}`, "FILE_NOT_FOUND");
132
- }
133
- content = fs.readFileSync(fromFile, "utf8");
134
- }
135
- else {
136
- const MAX_ENV_BYTES = 1024 * 1024; // 1 MB
137
- const buf = await readStdin(MAX_ENV_BYTES, () => new UsageError("Env file exceeds 1 MB limit.", "INVALID_FLAG_VALUE"));
138
- content = buf.toString("utf8");
125
+ let content;
126
+ if (fromFile !== undefined) {
127
+ if (!fs.existsSync(fromFile)) {
128
+ throw new NotFoundError(`Source file not found: ${fromFile}`, "FILE_NOT_FOUND");
139
129
  }
140
- writeEnv(absPath, content);
130
+ content = fs.readFileSync(fromFile, "utf8");
141
131
  }
142
132
  else {
143
- createEnv(absPath);
133
+ const MAX_ENV_BYTES = 1024 * 1024; // 1 MB
134
+ const buf = await readStdin(MAX_ENV_BYTES, () => new UsageError("Env file exceeds 1 MB limit.", "INVALID_FLAG_VALUE"));
135
+ content = buf.toString("utf8");
144
136
  }
145
- if (args.sensitive) {
146
- const markerPath = absPath.replace(/\.env$/, ".sensitive");
147
- if (!fs.existsSync(markerPath)) {
148
- fs.writeFileSync(markerPath, "", { mode: 0o600 });
149
- }
137
+ writeEnv(absPath, content);
138
+ }
139
+ else {
140
+ createEnv(absPath);
141
+ }
142
+ if (args.sensitive) {
143
+ const markerPath = absPath.replace(/\.env$/, ".sensitive");
144
+ if (!fs.existsSync(markerPath)) {
145
+ fs.writeFileSync(markerPath, "", { mode: 0o600 });
150
146
  }
151
- output("env-create", { ref: makeEnvRef(parsed.name, source) });
152
- });
147
+ }
148
+ output("env-create", { ref: makeEnvRef(parsed.name, source) });
153
149
  },
154
150
  });
155
- const envPathCommand = defineCommand({
151
+ const envPathCommand = defineJsonCommand({
156
152
  meta: {
157
153
  name: "path",
158
154
  description: "Print the absolute env file path (Docker `_FILE` convention / `--env-file`). To inject values, use `akm env run <ref> -- <cmd>` — do NOT `source` the raw file.",
@@ -161,24 +157,22 @@ const envPathCommand = defineCommand({
161
157
  ref: { type: "positional", description: "Env ref", required: true },
162
158
  quiet: { type: "boolean", alias: "q", description: "Suppress the unsafe-source warning", default: false },
163
159
  },
164
- run({ args }) {
165
- return runWithJsonErrors(async () => {
166
- const { name, absPath, source } = resolveEnvPath(args.ref);
167
- if (!fs.existsSync(absPath)) {
168
- throw new NotFoundError(`Env not found: ${makeEnvRef(name, source)}`);
169
- }
170
- // The raw `.env` may contain `X=$(cmd)`, which executes if `source`d.
171
- // Warning goes to stderr (never contaminates the path on stdout) and is
172
- // suppressed with --quiet for the legitimate `_FILE` / `--env-file` use.
173
- if (args.quiet !== true) {
174
- process.stderr.write(`warning: this is the raw file path. Do NOT \`source\` it (shell substitutions in the file would execute).\n` +
175
- ` To inject values run: akm env run ${args.ref} -- <command>\n`);
176
- }
177
- process.stdout.write(`${absPath}\n`);
178
- });
160
+ async run({ args }) {
161
+ const { name, absPath, source } = resolveEnvPath(args.ref);
162
+ if (!fs.existsSync(absPath)) {
163
+ throw new NotFoundError(`Env not found: ${makeEnvRef(name, source)}`);
164
+ }
165
+ // The raw `.env` may contain `X=$(cmd)`, which executes if `source`d.
166
+ // Warning goes to stderr (never contaminates the path on stdout) and is
167
+ // suppressed with --quiet for the legitimate `_FILE` / `--env-file` use.
168
+ if (args.quiet !== true) {
169
+ process.stderr.write(`warning: this is the raw file path. Do NOT \`source\` it (shell substitutions in the file would execute).\n` +
170
+ ` To inject values run: akm env run ${args.ref} -- <command>\n`);
171
+ }
172
+ process.stdout.write(`${absPath}\n`);
179
173
  },
180
174
  });
181
- const envExportCommand = defineCommand({
175
+ const envExportCommand = defineJsonCommand({
182
176
  meta: {
183
177
  name: "export",
184
178
  description: "Write safe `export KEY='value'` lines to a file (mode 0600) for `source`-ing — requires --out <path>. Values are re-serialised single-quoted so a raw `.env` cannot execute on load, and are NEVER printed to stdout. To use values directly, prefer `akm env run <ref> -- <command>`.",
@@ -187,23 +181,21 @@ const envExportCommand = defineCommand({
187
181
  ref: { type: "positional", description: "Env ref", required: true },
188
182
  out: { type: "string", alias: "o", description: "Destination file (required). Written at mode 0600." },
189
183
  },
190
- run({ args }) {
191
- return runWithJsonErrors(async () => {
192
- const outPath = getHyphenatedArg(args, "out");
193
- if (!outPath) {
194
- throw new UsageError("`akm env export` writes to a file pass --out <path>.\n" +
195
- " To use values directly, run `akm env run <ref> -- <command>` (or `-- $SHELL` for an interactive\n" +
196
- " session). export never prints values to stdout, to avoid leaking them into a captured context.", "MISSING_REQUIRED_ARGUMENT");
197
- }
198
- const { name, absPath, source } = resolveEnvPath(args.ref);
199
- if (!fs.existsSync(absPath)) {
200
- throw new NotFoundError(`Env not found: ${makeEnvRef(name, source)}`);
201
- }
202
- const { buildShellExportScript } = await import("./env.js");
203
- const resolvedOut = path.resolve(outPath);
204
- writeFileAtomic(resolvedOut, buildShellExportScript(absPath), 0o600);
205
- output("env-export", { ref: makeEnvRef(name, source), out: resolvedOut });
206
- });
184
+ async run({ args }) {
185
+ const outPath = args.out;
186
+ if (!outPath) {
187
+ throw new UsageError("`akm env export` writes to a file — pass --out <path>.\n" +
188
+ " To use values directly, run `akm env run <ref> -- <command>` (or `-- $SHELL` for an interactive\n" +
189
+ " session). export never prints values to stdout, to avoid leaking them into a captured context.", "MISSING_REQUIRED_ARGUMENT");
190
+ }
191
+ const { name, absPath, source } = resolveEnvPath(args.ref);
192
+ if (!fs.existsSync(absPath)) {
193
+ throw new NotFoundError(`Env not found: ${makeEnvRef(name, source)}`);
194
+ }
195
+ const { buildShellExportScript } = await import("./env.js");
196
+ const resolvedOut = path.resolve(outPath);
197
+ writeFileAtomic(resolvedOut, buildShellExportScript(absPath), 0o600);
198
+ output("env-export", { ref: makeEnvRef(name, source), out: resolvedOut });
207
199
  },
208
200
  });
209
201
  /**
@@ -239,74 +231,21 @@ async function runEnvInjected(target, opts) {
239
231
  }
240
232
  throw new NotFoundError(`Env not found: ${makeEnvRef(name, source)}`);
241
233
  }
242
- const { loadEnv } = await import("./env.js");
243
- const allValues = loadEnv(absPath);
244
- // Value-safe key filtering (--only / --except operate on key NAMES only).
245
- let envValues = allValues;
246
- if (opts.only && opts.except) {
247
- throw new UsageError("Pass only one of --only or --except.", "INVALID_FLAG_VALUE");
248
- }
249
- if (opts.only) {
250
- const wanted = new Set(opts.only);
251
- const missing = opts.only.filter((k) => !(k in allValues));
252
- if (missing.length > 0) {
253
- process.stderr.write(`warning: --only key(s) not present in ${makeEnvRef(name, source)}: ${missing.join(", ")}\n`);
254
- }
255
- envValues = Object.fromEntries(Object.entries(allValues).filter(([k]) => wanted.has(k)));
256
- }
257
- else if (opts.except) {
258
- const excluded = new Set(opts.except);
259
- envValues = Object.fromEntries(Object.entries(allValues).filter(([k]) => !excluded.has(k)));
260
- }
261
- // Substitute `${secret:NAME}` tokens in values with the value of the sibling
262
- // secret asset in the SAME stash. The lookup is injected so commands/env.ts
263
- // keeps its narrow dependency surface; we resolve each name against this env's
264
- // own `source`. A missing secret is a hard error — inject NOTHING (no partial
265
- // injection). Resolved values are never logged or printed.
266
- const { resolveSecretTokens } = await import("./env.js");
267
- const { readValue } = await import("./secret.js");
268
- const secretsRoot = path.join(source.path, "secrets");
269
- const resolveSecret = (secretName) => {
270
- const secretPath = resolveAssetPathFromName("secret", secretsRoot, secretName);
271
- // Defense-in-depth: ensure the resolved path stays inside the secrets dir.
272
- if (!isWithin(secretPath, secretsRoot)) {
273
- throw new UsageError(`Secret name "${secretName}" escapes the secrets directory.`);
274
- }
275
- if (!fs.existsSync(secretPath))
276
- return undefined;
277
- // Match `secret run`: read utf8, do not trim (stay consistent with that path).
278
- return readValue(secretPath).toString("utf8");
279
- };
280
- const { values: substituted, missing } = resolveSecretTokens(envValues, resolveSecret);
281
- if (missing.length > 0) {
282
- const envRef = makeEnvRef(name, source);
283
- throw new NotFoundError(`Env "${envRef}" references secret(s) not found in its stash: ${missing.map((n) => `secret:${n}`).join(", ")}. Nothing was injected.`, "FILE_NOT_FOUND", `Create the missing secret, e.g. \`akm secret set secret:${missing[0]}\`.`);
284
- }
285
- envValues = substituted;
286
- const keys = Object.keys(envValues);
287
- // Scan injected keys for known process-hijacking variables (LD_PRELOAD,
288
- // PATH, ...). Block for third-party-sourced stashes (origin has a registryId);
289
- // warn for the operator's own first-party stash, where they own the file.
290
- const { isDangerousEnvKey } = await import("../lint/env-key-rules.js");
291
- const dangerous = keys.filter(isDangerousEnvKey);
292
- if (dangerous.length > 0) {
293
- const detail = `Env "${makeEnvRef(name, source)}" injects process-hijacking variable(s): ${dangerous.join(", ")}.`;
294
- if (source.registryId) {
295
- throw new UsageError(`Refusing to inject env from a third-party stash. ${detail}\n` +
296
- ` Review the file, then copy the values into a first-party env if you trust them.`, "INVALID_FLAG_VALUE");
297
- }
298
- process.stderr.write(`warning: ${detail} Injecting anyway (first-party stash).\n`);
299
- }
300
- const mergedEnv = { ...process.env };
234
+ // Load filter secret-substitute → dangerous-key policy → keys-only
235
+ // audit event. Shared with the workflow engine's per-unit env bindings —
236
+ // see env-binding.ts for the extracted core and its safety invariants.
237
+ const { resolveEnvBinding } = await import("./env-binding.js");
238
+ const { values: envValues } = resolveEnvBinding(target, {
239
+ only: opts.only,
240
+ except: opts.except,
241
+ });
242
+ const mergedEnv = buildChildEnv(process.env, {
243
+ clean: opts.clean === true,
244
+ inherit: opts.inherit ?? [],
245
+ });
301
246
  for (const [envKey, envValue] of Object.entries(envValues)) {
302
247
  mergedEnv[envKey] = envValue;
303
248
  }
304
- // Audit trail: keys only, never values.
305
- appendEvent({
306
- eventType: "env_access",
307
- ref: makeEnvRef(name, source),
308
- metadata: { keys },
309
- });
310
249
  const result = spawnSync(command[0], command.slice(1), {
311
250
  stdio: "inherit",
312
251
  env: mergedEnv,
@@ -336,12 +275,12 @@ function parseKeyListFlag(raw) {
336
275
  .filter(Boolean);
337
276
  return keys.length > 0 ? keys : undefined;
338
277
  }
339
- const envRunCommand = defineCommand({
278
+ const envRunCommand = defineJsonCommand({
340
279
  meta: {
341
280
  name: "run",
342
281
  description:
343
282
  // biome-ignore lint/suspicious/noTemplateCurlyInString: literal `${secret:NAME}` token syntax documented for users, not interpolation
344
- "Run a command with the env file injected into its environment: `akm env run <ref> -- <command>`. Use `-- $SHELL` for an interactive session. Restrict which variables are injected with --only / --except. Values may embed `${secret:NAME}` tokens, replaced at run time with the sibling `secret:NAME` value from the same stash.",
283
+ "Run a command with the env file injected into its environment: `akm env run <ref> -- <command>`. Use `-- $SHELL` for an interactive session. Restrict which variables are injected with --only / --except. Values may embed `${secret:NAME}` tokens, replaced at run time with the sibling `secret:NAME` value from the same stash. Pass --clean to start the child with a minimal inherited environment instead of the full parent environment.",
345
284
  },
346
285
  args: {
347
286
  target: { type: "positional", description: "Env ref", required: true },
@@ -350,47 +289,56 @@ const envRunCommand = defineCommand({
350
289
  description: "Inject ONLY these keys (comma-separated). Mutually exclusive with --except.",
351
290
  },
352
291
  except: { type: "string", description: "Inject all keys EXCEPT these (comma-separated)." },
292
+ clean: {
293
+ type: "boolean",
294
+ description: "Start the child with a minimal inherited environment (PATH/HOME/locale/terminal basics) instead of the full parent environment.",
295
+ default: false,
296
+ },
297
+ inherit: {
298
+ type: "string",
299
+ description: "When used with --clean, also inherit these parent env vars (comma-separated). Ignored without --clean.",
300
+ },
353
301
  },
354
- run({ args }) {
355
- return runWithJsonErrors(() => runEnvInjected(args.target, {
356
- only: parseKeyListFlag(getHyphenatedArg(args, "only")),
357
- except: parseKeyListFlag(getHyphenatedArg(args, "except")),
358
- }));
302
+ async run({ args }) {
303
+ await runEnvInjected(args.target, {
304
+ only: parseKeyListFlag(args.only),
305
+ except: parseKeyListFlag(args.except),
306
+ clean: args.clean === true,
307
+ inherit: parseKeyListFlag(args.inherit) ?? [],
308
+ });
359
309
  },
360
310
  });
361
- const envRemoveCommand = defineCommand({
311
+ const envRemoveCommand = defineJsonCommand({
362
312
  meta: { name: "remove", description: "Remove an env file (and its .sensitive marker, if any)" },
363
313
  args: {
364
314
  ref: { type: "positional", description: "Env ref", required: true },
365
315
  yes: { type: "boolean", alias: "y", description: "Skip confirmation prompt", default: false },
366
316
  },
367
- run({ args }) {
368
- return runWithJsonErrors(async () => {
369
- const parsed = parseEnvRef(args.ref);
370
- const source = findEnvSource(parsed.origin);
371
- const envRoot = path.join(source.path, "env");
372
- const absPath = resolveAssetPathFromName("env", envRoot, parsed.name);
373
- if (!isWithin(absPath, envRoot)) {
374
- throw new UsageError(`Env name "${parsed.name}" escapes the env directory.`);
375
- }
376
- const { confirmDestructive } = await import("../../cli/confirm.js");
377
- const confirmed = await confirmDestructive(`Remove env "${args.ref}"? This cannot be undone.`, {
378
- yes: args.yes === true,
379
- });
380
- if (!confirmed) {
381
- process.stderr.write("Aborted.\n");
382
- return;
383
- }
384
- if (!fs.existsSync(absPath)) {
385
- throw new NotFoundError(`Env not found: ${makeEnvRef(parsed.name, source)}`);
386
- }
387
- const { removeEnv } = await import("./env.js");
388
- const removed = removeEnv(absPath);
389
- output("env-remove", { ref: makeEnvRef(parsed.name, source), removed });
317
+ async run({ args }) {
318
+ const parsed = parseEnvRef(args.ref);
319
+ const source = findEnvSource(parsed.origin);
320
+ const envRoot = path.join(source.path, "env");
321
+ const absPath = resolveAssetPathFromName("env", envRoot, parsed.name);
322
+ if (!isWithin(absPath, envRoot)) {
323
+ throw new UsageError(`Env name "${parsed.name}" escapes the env directory.`);
324
+ }
325
+ const { confirmDestructive } = await import("../../cli/confirm.js");
326
+ const confirmed = await confirmDestructive(`Remove env "${args.ref}"? This cannot be undone.`, {
327
+ yes: args.yes === true,
390
328
  });
329
+ if (!confirmed) {
330
+ process.stderr.write("Aborted.\n");
331
+ return;
332
+ }
333
+ if (!fs.existsSync(absPath)) {
334
+ throw new NotFoundError(`Env not found: ${makeEnvRef(parsed.name, source)}`);
335
+ }
336
+ const { removeEnv } = await import("./env.js");
337
+ const removed = removeEnv(absPath);
338
+ output("env-remove", { ref: makeEnvRef(parsed.name, source), removed });
391
339
  },
392
340
  });
393
- const envSetCommand = defineCommand({
341
+ const envSetCommand = defineJsonCommand({
394
342
  meta: {
395
343
  name: "set",
396
344
  description: "Set (create or update) a single KEY in an env file: `akm env set <ref> <KEY>`. The value is read from stdin by default (never via argv); use --from-env <VAR> or --from-file <path>. Preserves existing comments and key order; the value is never printed. Creates the env file if it does not exist.",
@@ -401,59 +349,57 @@ const envSetCommand = defineCommand({
401
349
  "from-env": { type: "string", description: "Read the value from the named environment variable" },
402
350
  "from-file": { type: "string", description: "Read the value from this file" },
403
351
  },
404
- run({ args }) {
405
- return runWithJsonErrors(async () => {
406
- const parsed = parseEnvRef(args.ref);
407
- const source = findEnvSource(parsed.origin);
408
- const envRoot = path.join(source.path, "env");
409
- const absPath = resolveAssetPathFromName("env", envRoot, parsed.name);
410
- if (!isWithin(absPath, envRoot)) {
411
- throw new UsageError(`Env name "${parsed.name}" escapes the env directory.`);
412
- }
413
- const key = String(args.key);
414
- const { ENV_KEY_RE, setEnvKey } = await import("./env.js");
415
- if (!ENV_KEY_RE.test(key)) {
416
- throw new UsageError(`Invalid env key "${key}". Keys match [A-Za-z_][A-Za-z0-9_]*.`, "INVALID_FLAG_VALUE");
417
- }
418
- const fromEnv = getHyphenatedArg(args, "from-env");
419
- const fromFile = getHyphenatedArg(args, "from-file");
420
- if (fromEnv !== undefined && fromFile !== undefined) {
421
- throw new UsageError("Pass only one of --from-file or --from-env (or use stdin).", "INVALID_FLAG_VALUE");
422
- }
423
- const MAX_ENV_VALUE_BYTES = 1024 * 1024; // 1 MB
424
- let value;
425
- if (fromFile !== undefined) {
426
- if (!fs.existsSync(fromFile)) {
427
- throw new NotFoundError(`File not found: ${fromFile}`, "FILE_NOT_FOUND");
428
- }
429
- const buf = fs.readFileSync(fromFile);
430
- if (buf.byteLength > MAX_ENV_VALUE_BYTES)
431
- throw new UsageError("Value exceeds the 1 MB limit.");
432
- value = buf.toString("utf8");
433
- }
434
- else if (fromEnv !== undefined) {
435
- const v = process.env[fromEnv];
436
- if (v === undefined) {
437
- throw new UsageError(`Environment variable "${fromEnv}" is not set.`, "INVALID_FLAG_VALUE");
438
- }
439
- value = v;
440
- }
441
- else {
442
- const buf = await readStdin(MAX_ENV_VALUE_BYTES, () => new UsageError("Value exceeds the 1 MB limit."));
443
- // Strip a single trailing newline so `echo "$VAL" | akm env set` is exact.
444
- value = buf.toString("utf8").replace(/\n$/, "");
352
+ async run({ args }) {
353
+ const parsed = parseEnvRef(args.ref);
354
+ const source = findEnvSource(parsed.origin);
355
+ const envRoot = path.join(source.path, "env");
356
+ const absPath = resolveAssetPathFromName("env", envRoot, parsed.name);
357
+ if (!isWithin(absPath, envRoot)) {
358
+ throw new UsageError(`Env name "${parsed.name}" escapes the env directory.`);
359
+ }
360
+ const key = String(args.key);
361
+ const { ENV_KEY_RE, setEnvKey } = await import("./env.js");
362
+ if (!ENV_KEY_RE.test(key)) {
363
+ throw new UsageError(`Invalid env key "${key}". Keys match [A-Za-z_][A-Za-z0-9_]*.`, "INVALID_FLAG_VALUE");
364
+ }
365
+ const fromEnv = args["from-env"];
366
+ const fromFile = args["from-file"];
367
+ if (fromEnv !== undefined && fromFile !== undefined) {
368
+ throw new UsageError("Pass only one of --from-file or --from-env (or use stdin).", "INVALID_FLAG_VALUE");
369
+ }
370
+ const MAX_ENV_VALUE_BYTES = 1024 * 1024; // 1 MB
371
+ let value;
372
+ if (fromFile !== undefined) {
373
+ if (!fs.existsSync(fromFile)) {
374
+ throw new NotFoundError(`File not found: ${fromFile}`, "FILE_NOT_FOUND");
445
375
  }
446
- setEnvKey(absPath, key, value);
447
- // Warn (never block) on process-hijacking key names, matching the env-run audit.
448
- const { isDangerousEnvKey } = await import("../lint/env-key-rules.js");
449
- if (isDangerousEnvKey(key) && !isQuiet()) {
450
- process.stderr.write(`warning: "${key}" can influence process execution when this env is loaded via 'akm env run'.\n`);
376
+ const buf = fs.readFileSync(fromFile);
377
+ if (buf.byteLength > MAX_ENV_VALUE_BYTES)
378
+ throw new UsageError("Value exceeds the 1 MB limit.");
379
+ value = buf.toString("utf8");
380
+ }
381
+ else if (fromEnv !== undefined) {
382
+ const v = process.env[fromEnv];
383
+ if (v === undefined) {
384
+ throw new UsageError(`Environment variable "${fromEnv}" is not set.`, "INVALID_FLAG_VALUE");
451
385
  }
452
- output("env-set", { ref: makeEnvRef(parsed.name, source), key });
453
- });
386
+ value = v;
387
+ }
388
+ else {
389
+ const buf = await readStdin(MAX_ENV_VALUE_BYTES, () => new UsageError("Value exceeds the 1 MB limit."));
390
+ // Strip a single trailing newline so `echo "$VAL" | akm env set` is exact.
391
+ value = buf.toString("utf8").replace(/\n$/, "");
392
+ }
393
+ setEnvKey(absPath, key, value);
394
+ // Warn (never block) on process-hijacking key names, matching the env-run audit.
395
+ const { isDangerousEnvKey } = await import("../lint/env-key-rules.js");
396
+ if (isDangerousEnvKey(key) && !isQuiet()) {
397
+ process.stderr.write(`warning: "${key}" can influence process execution when this env is loaded via 'akm env run'.\n`);
398
+ }
399
+ output("env-set", { ref: makeEnvRef(parsed.name, source), key });
454
400
  },
455
401
  });
456
- const envUnsetCommand = defineCommand({
402
+ const envUnsetCommand = defineJsonCommand({
457
403
  meta: {
458
404
  name: "unset",
459
405
  description: "Remove one or more KEYs from an env file: `akm env unset <ref> <KEY...>`. Preserves other keys and comments. To remove the whole file, use `akm env remove`.",
@@ -464,66 +410,56 @@ const envUnsetCommand = defineCommand({
464
410
  // non-required so citty doesn't block before we emit a structured error.
465
411
  key: { type: "positional", description: "Key name(s) to remove (one or more)", required: false },
466
412
  },
467
- run({ args }) {
468
- return runWithJsonErrors(async () => {
469
- const parsed = parseEnvRef(args.ref);
470
- const source = findEnvSource(parsed.origin);
471
- const envRoot = path.join(source.path, "env");
472
- const absPath = resolveAssetPathFromName("env", envRoot, parsed.name);
473
- if (!isWithin(absPath, envRoot)) {
474
- throw new UsageError(`Env name "${parsed.name}" escapes the env directory.`);
475
- }
476
- if (!fs.existsSync(absPath)) {
477
- throw new NotFoundError(`Env not found: ${makeEnvRef(parsed.name, source)}`);
478
- }
479
- // citty puts every positional in `args._` (incl. the ref at [0]); the keys
480
- // are the remaining positionals. citty also mis-captures the space-separated
481
- // value of a global flag (`--format json`) as a positional, so drop any
482
- // token that is actually a global flag's value (cli.ts:1335 documents this).
483
- const globalFlagValues = new Set(["--format", "--shape", "--detail", "--scope", "--filter", "--target"]
484
- .map((flag) => parseFlagValue(process.argv, flag))
485
- .filter((v) => typeof v === "string"));
486
- const keys = (Array.isArray(args._) ? args._.map(String) : [])
487
- .slice(1)
488
- .filter((k) => !globalFlagValues.has(k));
489
- if (keys.length === 0) {
490
- throw new UsageError("Usage: akm env unset <ref> <KEY...> (one or more keys).", "MISSING_REQUIRED_ARGUMENT");
491
- }
492
- const { ENV_KEY_RE, unsetEnvKeys } = await import("./env.js");
493
- const invalid = keys.filter((k) => !ENV_KEY_RE.test(k));
494
- if (invalid.length > 0) {
495
- throw new UsageError(`Invalid env key(s): ${invalid.join(", ")}.`, "INVALID_FLAG_VALUE");
496
- }
497
- const { removed, missing } = unsetEnvKeys(absPath, keys);
498
- output("env-unset", { ref: makeEnvRef(parsed.name, source), removed, missing });
499
- });
413
+ async run({ args }) {
414
+ const parsed = parseEnvRef(args.ref);
415
+ const source = findEnvSource(parsed.origin);
416
+ const envRoot = path.join(source.path, "env");
417
+ const absPath = resolveAssetPathFromName("env", envRoot, parsed.name);
418
+ if (!isWithin(absPath, envRoot)) {
419
+ throw new UsageError(`Env name "${parsed.name}" escapes the env directory.`);
420
+ }
421
+ if (!fs.existsSync(absPath)) {
422
+ throw new NotFoundError(`Env not found: ${makeEnvRef(parsed.name, source)}`);
423
+ }
424
+ // citty puts every positional in `args._` (incl. the ref at [0]); the keys
425
+ // are the remaining positionals. citty also mis-captures the space-separated
426
+ // value of a global flag (`--format json`) as a positional, so drop any
427
+ // token that is actually a global flag's value (cli.ts:1335 documents this).
428
+ const globalFlagValues = new Set(["--format", "--shape", "--detail", "--scope", "--filter", "--target"]
429
+ .map((flag) => parseFlagValue(process.argv, flag))
430
+ .filter((v) => typeof v === "string"));
431
+ const keys = (Array.isArray(args._) ? args._.map(String) : [])
432
+ .slice(1)
433
+ .filter((k) => !globalFlagValues.has(k));
434
+ if (keys.length === 0) {
435
+ throw new UsageError("Usage: akm env unset <ref> <KEY...> (one or more keys).", "MISSING_REQUIRED_ARGUMENT");
436
+ }
437
+ const { ENV_KEY_RE, unsetEnvKeys } = await import("./env.js");
438
+ const invalid = keys.filter((k) => !ENV_KEY_RE.test(k));
439
+ if (invalid.length > 0) {
440
+ throw new UsageError(`Invalid env key(s): ${invalid.join(", ")}.`, "INVALID_FLAG_VALUE");
441
+ }
442
+ const { removed, missing } = unsetEnvKeys(absPath, keys);
443
+ output("env-unset", { ref: makeEnvRef(parsed.name, source), removed, missing });
500
444
  },
501
445
  });
502
- // Single source of truth: the routing set is derived from the subCommands keys
503
- // (M10) so adding a subcommand can never silently desync from `hasSubcommand`.
504
- const envSubCommands = {
505
- list: envListCommand,
506
- path: envPathCommand,
507
- export: envExportCommand,
508
- run: envRunCommand,
509
- create: envCreateCommand,
510
- set: envSetCommand,
511
- unset: envUnsetCommand,
512
- remove: envRemoveCommand,
513
- };
514
- const ENV_SUBCOMMAND_SET = new Set(Object.keys(envSubCommands));
515
- export const envCommand = defineCommand({
446
+ export const envCommand = defineGroupCommand({
516
447
  meta: {
517
448
  name: "env",
518
449
  description: "Manage `.env` files — a group of related CONFIGURATION values for an app or service (URLs, flags, plus any credentials it needs), loaded together. Values may or may not be sensitive; akm protects them all the same (key names visible, values never in structured output). For a single sensitive value used on its own (an auth token, key, or cert), use `akm secret`.",
519
450
  },
520
- subCommands: envSubCommands,
521
- run({ args }) {
522
- return runWithJsonErrors(async () => {
523
- if (hasSubcommand(args, ENV_SUBCOMMAND_SET))
524
- return;
525
- const { listKeys } = await import("./env.js");
526
- output("env-list", { envs: listEnvsRecursive(listKeys) });
527
- });
451
+ subCommands: {
452
+ list: envListCommand,
453
+ path: envPathCommand,
454
+ export: envExportCommand,
455
+ run: envRunCommand,
456
+ create: envCreateCommand,
457
+ set: envSetCommand,
458
+ unset: envUnsetCommand,
459
+ remove: envRemoveCommand,
460
+ },
461
+ async defaultRun() {
462
+ const { listKeys } = await import("./env.js");
463
+ output("env-list", { envs: listEnvsRecursive(listKeys) });
528
464
  },
529
465
  });