akm-cli 0.9.0-beta.9 → 0.9.0-rc.0

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