akm-cli 0.7.0 → 0.7.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 (327) hide show
  1. package/package.json +8 -8
  2. package/dist/tests/add-website-source.test.js +0 -119
  3. package/dist/tests/agent/agent-config-loader.test.js +0 -70
  4. package/dist/tests/agent/agent-config.test.js +0 -221
  5. package/dist/tests/agent/agent-detect.test.js +0 -100
  6. package/dist/tests/agent/agent-spawn.test.js +0 -234
  7. package/dist/tests/agent-output.test.js +0 -186
  8. package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +0 -103
  9. package/dist/tests/architecture/agent-spawn-seam.test.js +0 -193
  10. package/dist/tests/architecture/llm-stateless-seam.test.js +0 -112
  11. package/dist/tests/asset-ref.test.js +0 -192
  12. package/dist/tests/asset-registry.test.js +0 -103
  13. package/dist/tests/asset-spec.test.js +0 -241
  14. package/dist/tests/bench/attribution.test.js +0 -996
  15. package/dist/tests/bench/cleanup-sigint.test.js +0 -83
  16. package/dist/tests/bench/cleanup.js +0 -234
  17. package/dist/tests/bench/cleanup.test.js +0 -166
  18. package/dist/tests/bench/cli.js +0 -1018
  19. package/dist/tests/bench/cli.test.js +0 -445
  20. package/dist/tests/bench/compare.test.js +0 -556
  21. package/dist/tests/bench/corpus.js +0 -317
  22. package/dist/tests/bench/corpus.test.js +0 -258
  23. package/dist/tests/bench/doctor.js +0 -525
  24. package/dist/tests/bench/driver.js +0 -401
  25. package/dist/tests/bench/driver.test.js +0 -584
  26. package/dist/tests/bench/environment.js +0 -233
  27. package/dist/tests/bench/environment.test.js +0 -199
  28. package/dist/tests/bench/evolve-metrics.js +0 -179
  29. package/dist/tests/bench/evolve-metrics.test.js +0 -187
  30. package/dist/tests/bench/evolve.js +0 -647
  31. package/dist/tests/bench/evolve.test.js +0 -624
  32. package/dist/tests/bench/failure-modes.test.js +0 -349
  33. package/dist/tests/bench/feedback-integrity.test.js +0 -457
  34. package/dist/tests/bench/leakage.test.js +0 -228
  35. package/dist/tests/bench/learning-curve.test.js +0 -134
  36. package/dist/tests/bench/metrics.js +0 -2395
  37. package/dist/tests/bench/metrics.test.js +0 -1150
  38. package/dist/tests/bench/no-os-tmpdir-invariant.test.js +0 -43
  39. package/dist/tests/bench/opencode-config.js +0 -194
  40. package/dist/tests/bench/opencode-config.test.js +0 -370
  41. package/dist/tests/bench/report.js +0 -1885
  42. package/dist/tests/bench/report.test.js +0 -1038
  43. package/dist/tests/bench/run-config.js +0 -355
  44. package/dist/tests/bench/run-config.test.js +0 -298
  45. package/dist/tests/bench/run-curate-test.js +0 -32
  46. package/dist/tests/bench/run-failing-tasks.js +0 -56
  47. package/dist/tests/bench/run-full-bench.js +0 -51
  48. package/dist/tests/bench/run-items36-targeted.js +0 -69
  49. package/dist/tests/bench/run-nano-quick.js +0 -42
  50. package/dist/tests/bench/run-waveg-targeted.js +0 -62
  51. package/dist/tests/bench/runner.js +0 -699
  52. package/dist/tests/bench/runner.test.js +0 -958
  53. package/dist/tests/bench/search-bridge.test.js +0 -331
  54. package/dist/tests/bench/tmp.js +0 -131
  55. package/dist/tests/bench/trajectory.js +0 -116
  56. package/dist/tests/bench/trajectory.test.js +0 -127
  57. package/dist/tests/bench/verifier.js +0 -114
  58. package/dist/tests/bench/verifier.test.js +0 -118
  59. package/dist/tests/bench/workflow-evaluator.js +0 -557
  60. package/dist/tests/bench/workflow-evaluator.test.js +0 -421
  61. package/dist/tests/bench/workflow-spec.js +0 -345
  62. package/dist/tests/bench/workflow-spec.test.js +0 -363
  63. package/dist/tests/bench/workflow-trace.js +0 -472
  64. package/dist/tests/bench/workflow-trace.test.js +0 -254
  65. package/dist/tests/benchmark-search-quality.js +0 -536
  66. package/dist/tests/benchmark-suite.js +0 -1441
  67. package/dist/tests/capture-cli.test.js +0 -112
  68. package/dist/tests/cli-errors.test.js +0 -204
  69. package/dist/tests/commands/events.test.js +0 -370
  70. package/dist/tests/commands/history.test.js +0 -418
  71. package/dist/tests/commands/import.test.js +0 -103
  72. package/dist/tests/commands/proposal-cli.test.js +0 -209
  73. package/dist/tests/commands/reflect-propose-cli.test.js +0 -333
  74. package/dist/tests/commands/remember.test.js +0 -97
  75. package/dist/tests/commands/scope-flags.test.js +0 -300
  76. package/dist/tests/commands/search.test.js +0 -537
  77. package/dist/tests/commands/show-indexer-parity.test.js +0 -117
  78. package/dist/tests/commands/show.test.js +0 -294
  79. package/dist/tests/common.test.js +0 -266
  80. package/dist/tests/completions.test.js +0 -142
  81. package/dist/tests/config-cli.test.js +0 -193
  82. package/dist/tests/config-llm-features.test.js +0 -139
  83. package/dist/tests/config.test.js +0 -569
  84. package/dist/tests/contracts/migration-baseline.test.js +0 -43
  85. package/dist/tests/contracts/reflect-propose-envelope.test.js +0 -139
  86. package/dist/tests/contracts/spec-helpers.js +0 -46
  87. package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +0 -228
  88. package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +0 -56
  89. package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +0 -34
  90. package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +0 -94
  91. package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +0 -39
  92. package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +0 -44
  93. package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +0 -47
  94. package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +0 -40
  95. package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +0 -58
  96. package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +0 -34
  97. package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +0 -75
  98. package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +0 -36
  99. package/dist/tests/core/write-source.test.js +0 -366
  100. package/dist/tests/curate-command.test.js +0 -87
  101. package/dist/tests/db-scoring.test.js +0 -201
  102. package/dist/tests/db.test.js +0 -654
  103. package/dist/tests/distill-cli-flag.test.js +0 -208
  104. package/dist/tests/distill.test.js +0 -515
  105. package/dist/tests/docker-install.test.js +0 -120
  106. package/dist/tests/e2e.test.js +0 -1419
  107. package/dist/tests/embedder.test.js +0 -340
  108. package/dist/tests/embedding-model-config.test.js +0 -379
  109. package/dist/tests/feedback-command.test.js +0 -172
  110. package/dist/tests/file-context.test.js +0 -552
  111. package/dist/tests/fixtures/scripts/git/summarize-diff.js +0 -9
  112. package/dist/tests/fixtures/scripts/lint/eslint-check.js +0 -7
  113. package/dist/tests/fixtures/stashes/load.js +0 -166
  114. package/dist/tests/fixtures/stashes/load.test.js +0 -97
  115. package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +0 -12
  116. package/dist/tests/frontmatter.test.js +0 -190
  117. package/dist/tests/fts-field-weighting.test.js +0 -254
  118. package/dist/tests/fuzzy-search.test.js +0 -230
  119. package/dist/tests/git-provider-clone.test.js +0 -45
  120. package/dist/tests/github.test.js +0 -161
  121. package/dist/tests/graph-boost-ranking.test.js +0 -305
  122. package/dist/tests/graph-extraction.test.js +0 -282
  123. package/dist/tests/helpers/usage-events.js +0 -8
  124. package/dist/tests/index-pass-llm.test.js +0 -161
  125. package/dist/tests/indexer.test.js +0 -570
  126. package/dist/tests/info-command.test.js +0 -166
  127. package/dist/tests/init.test.js +0 -69
  128. package/dist/tests/install-script.test.js +0 -246
  129. package/dist/tests/integration/agent-real-profile.test.js +0 -94
  130. package/dist/tests/issue-36-repro.test.js +0 -304
  131. package/dist/tests/issues-191-194.test.js +0 -160
  132. package/dist/tests/lesson-lint.test.js +0 -111
  133. package/dist/tests/llm-client.test.js +0 -115
  134. package/dist/tests/llm-feature-gate.test.js +0 -151
  135. package/dist/tests/llm.test.js +0 -139
  136. package/dist/tests/lockfile.test.js +0 -216
  137. package/dist/tests/manifest.test.js +0 -205
  138. package/dist/tests/markdown.test.js +0 -126
  139. package/dist/tests/matchers-unit.test.js +0 -189
  140. package/dist/tests/memory-inference.test.js +0 -299
  141. package/dist/tests/merge-scoring.test.js +0 -136
  142. package/dist/tests/metadata.test.js +0 -313
  143. package/dist/tests/migration-help.test.js +0 -89
  144. package/dist/tests/origin-resolve.test.js +0 -124
  145. package/dist/tests/output-baseline.test.js +0 -218
  146. package/dist/tests/output-shapes-unit.test.js +0 -478
  147. package/dist/tests/parallel-search.test.js +0 -272
  148. package/dist/tests/parameter-metadata.test.js +0 -365
  149. package/dist/tests/paths.test.js +0 -177
  150. package/dist/tests/progressive-disclosure.test.js +0 -280
  151. package/dist/tests/proposals.test.js +0 -279
  152. package/dist/tests/proposed-quality.test.js +0 -271
  153. package/dist/tests/provider-registry.test.js +0 -32
  154. package/dist/tests/ranking-regression.test.js +0 -548
  155. package/dist/tests/reflect-propose.test.js +0 -455
  156. package/dist/tests/registry-build-index.test.js +0 -394
  157. package/dist/tests/registry-cli.test.js +0 -290
  158. package/dist/tests/registry-index-v2.test.js +0 -430
  159. package/dist/tests/registry-install.test.js +0 -728
  160. package/dist/tests/registry-providers/parity.test.js +0 -189
  161. package/dist/tests/registry-providers/skills-sh.test.js +0 -309
  162. package/dist/tests/registry-providers/static-index.test.js +0 -238
  163. package/dist/tests/registry-resolve.test.js +0 -126
  164. package/dist/tests/registry-search.test.js +0 -923
  165. package/dist/tests/remember-frontmatter.test.js +0 -378
  166. package/dist/tests/remember-unit.test.js +0 -123
  167. package/dist/tests/ripgrep-install.test.js +0 -251
  168. package/dist/tests/ripgrep-resolve.test.js +0 -108
  169. package/dist/tests/ripgrep.test.js +0 -163
  170. package/dist/tests/save-command.test.js +0 -94
  171. package/dist/tests/save-trust-qa-fixes.test.js +0 -270
  172. package/dist/tests/scoring-pipeline.test.js +0 -648
  173. package/dist/tests/search-include-proposed-cli.test.js +0 -118
  174. package/dist/tests/self-update.test.js +0 -442
  175. package/dist/tests/semantic-search-e2e.test.js +0 -512
  176. package/dist/tests/semantic-status.test.js +0 -471
  177. package/dist/tests/setup-run.integration.js +0 -877
  178. package/dist/tests/setup-wizard.test.js +0 -198
  179. package/dist/tests/setup.test.js +0 -131
  180. package/dist/tests/source-add.test.js +0 -11
  181. package/dist/tests/source-clone.test.js +0 -254
  182. package/dist/tests/source-manage.test.js +0 -366
  183. package/dist/tests/source-providers/filesystem.test.js +0 -82
  184. package/dist/tests/source-providers/git.test.js +0 -252
  185. package/dist/tests/source-providers/website.test.js +0 -128
  186. package/dist/tests/source-qa-fixes.test.js +0 -286
  187. package/dist/tests/source-registry.test.js +0 -350
  188. package/dist/tests/source-resolve.test.js +0 -100
  189. package/dist/tests/source-source.test.js +0 -281
  190. package/dist/tests/source.test.js +0 -533
  191. package/dist/tests/tar-utils-scan.test.js +0 -73
  192. package/dist/tests/toggle-components.test.js +0 -73
  193. package/dist/tests/usage-telemetry.test.js +0 -265
  194. package/dist/tests/utility-scoring.test.js +0 -558
  195. package/dist/tests/vault-load-error.test.js +0 -78
  196. package/dist/tests/vault-qa-fixes.test.js +0 -194
  197. package/dist/tests/vault.test.js +0 -429
  198. package/dist/tests/vector-search.test.js +0 -608
  199. package/dist/tests/walker.test.js +0 -252
  200. package/dist/tests/wave2-cluster-bc.test.js +0 -228
  201. package/dist/tests/wave2-cluster-d.test.js +0 -180
  202. package/dist/tests/wave2-cluster-e.test.js +0 -179
  203. package/dist/tests/wiki-qa-fixes.test.js +0 -270
  204. package/dist/tests/wiki.test.js +0 -529
  205. package/dist/tests/workflow-cli.test.js +0 -271
  206. package/dist/tests/workflow-markdown.test.js +0 -171
  207. package/dist/tests/workflow-path-escape.test.js +0 -132
  208. package/dist/tests/workflow-qa-fixes.test.js +0 -395
  209. package/dist/tests/workflows/indexer-rejection.test.js +0 -213
  210. /package/dist/{src/cli.js → cli.js} +0 -0
  211. /package/dist/{src/commands → commands}/completions.js +0 -0
  212. /package/dist/{src/commands → commands}/config-cli.js +0 -0
  213. /package/dist/{src/commands → commands}/curate.js +0 -0
  214. /package/dist/{src/commands → commands}/distill.js +0 -0
  215. /package/dist/{src/commands → commands}/events.js +0 -0
  216. /package/dist/{src/commands → commands}/history.js +0 -0
  217. /package/dist/{src/commands → commands}/info.js +0 -0
  218. /package/dist/{src/commands → commands}/init.js +0 -0
  219. /package/dist/{src/commands → commands}/install-audit.js +0 -0
  220. /package/dist/{src/commands → commands}/installed-stashes.js +0 -0
  221. /package/dist/{src/commands → commands}/migration-help.js +0 -0
  222. /package/dist/{src/commands → commands}/proposal.js +0 -0
  223. /package/dist/{src/commands → commands}/propose.js +0 -0
  224. /package/dist/{src/commands → commands}/reflect.js +0 -0
  225. /package/dist/{src/commands → commands}/registry-search.js +0 -0
  226. /package/dist/{src/commands → commands}/remember.js +0 -0
  227. /package/dist/{src/commands → commands}/search.js +0 -0
  228. /package/dist/{src/commands → commands}/self-update.js +0 -0
  229. /package/dist/{src/commands → commands}/show.js +0 -0
  230. /package/dist/{src/commands → commands}/source-add.js +0 -0
  231. /package/dist/{src/commands → commands}/source-clone.js +0 -0
  232. /package/dist/{src/commands → commands}/source-manage.js +0 -0
  233. /package/dist/{src/commands → commands}/vault.js +0 -0
  234. /package/dist/{src/core → core}/asset-ref.js +0 -0
  235. /package/dist/{src/core → core}/asset-registry.js +0 -0
  236. /package/dist/{src/core → core}/asset-spec.js +0 -0
  237. /package/dist/{src/core → core}/common.js +0 -0
  238. /package/dist/{src/core → core}/config.js +0 -0
  239. /package/dist/{src/core → core}/errors.js +0 -0
  240. /package/dist/{src/core → core}/events.js +0 -0
  241. /package/dist/{src/core → core}/frontmatter.js +0 -0
  242. /package/dist/{src/core → core}/lesson-lint.js +0 -0
  243. /package/dist/{src/core → core}/markdown.js +0 -0
  244. /package/dist/{src/core → core}/paths.js +0 -0
  245. /package/dist/{src/core → core}/proposals.js +0 -0
  246. /package/dist/{src/core → core}/warn.js +0 -0
  247. /package/dist/{src/core → core}/write-source.js +0 -0
  248. /package/dist/{src/indexer → indexer}/db-search.js +0 -0
  249. /package/dist/{src/indexer → indexer}/db.js +0 -0
  250. /package/dist/{src/indexer → indexer}/file-context.js +0 -0
  251. /package/dist/{src/indexer → indexer}/graph-boost.js +0 -0
  252. /package/dist/{src/indexer → indexer}/graph-extraction.js +0 -0
  253. /package/dist/{src/indexer → indexer}/indexer.js +0 -0
  254. /package/dist/{src/indexer → indexer}/manifest.js +0 -0
  255. /package/dist/{src/indexer → indexer}/matchers.js +0 -0
  256. /package/dist/{src/indexer → indexer}/memory-inference.js +0 -0
  257. /package/dist/{src/indexer → indexer}/metadata.js +0 -0
  258. /package/dist/{src/indexer → indexer}/search-fields.js +0 -0
  259. /package/dist/{src/indexer → indexer}/search-source.js +0 -0
  260. /package/dist/{src/indexer → indexer}/semantic-status.js +0 -0
  261. /package/dist/{src/indexer → indexer}/usage-events.js +0 -0
  262. /package/dist/{src/indexer → indexer}/walker.js +0 -0
  263. /package/dist/{src/integrations → integrations}/agent/config.js +0 -0
  264. /package/dist/{src/integrations → integrations}/agent/detect.js +0 -0
  265. /package/dist/{src/integrations → integrations}/agent/index.js +0 -0
  266. /package/dist/{src/integrations → integrations}/agent/profiles.js +0 -0
  267. /package/dist/{src/integrations → integrations}/agent/prompts.js +0 -0
  268. /package/dist/{src/integrations → integrations}/agent/spawn.js +0 -0
  269. /package/dist/{src/integrations → integrations}/github.js +0 -0
  270. /package/dist/{src/integrations → integrations}/lockfile.js +0 -0
  271. /package/dist/{src/llm → llm}/client.js +0 -0
  272. /package/dist/{src/llm → llm}/embedder.js +0 -0
  273. /package/dist/{src/llm → llm}/embedders/cache.js +0 -0
  274. /package/dist/{src/llm → llm}/embedders/local.js +0 -0
  275. /package/dist/{src/llm → llm}/embedders/remote.js +0 -0
  276. /package/dist/{src/llm → llm}/embedders/types.js +0 -0
  277. /package/dist/{src/llm → llm}/feature-gate.js +0 -0
  278. /package/dist/{src/llm → llm}/graph-extract.js +0 -0
  279. /package/dist/{src/llm → llm}/index-passes.js +0 -0
  280. /package/dist/{src/llm → llm}/memory-infer.js +0 -0
  281. /package/dist/{src/llm → llm}/metadata-enhance.js +0 -0
  282. /package/dist/{src/output → output}/cli-hints.js +0 -0
  283. /package/dist/{src/output → output}/context.js +0 -0
  284. /package/dist/{src/output → output}/renderers.js +0 -0
  285. /package/dist/{src/output → output}/shapes.js +0 -0
  286. /package/dist/{src/output → output}/text.js +0 -0
  287. /package/dist/{src/registry → registry}/build-index.js +0 -0
  288. /package/dist/{src/registry → registry}/create-provider-registry.js +0 -0
  289. /package/dist/{src/registry → registry}/factory.js +0 -0
  290. /package/dist/{src/registry → registry}/origin-resolve.js +0 -0
  291. /package/dist/{src/registry → registry}/providers/index.js +0 -0
  292. /package/dist/{src/registry → registry}/providers/skills-sh.js +0 -0
  293. /package/dist/{src/registry → registry}/providers/static-index.js +0 -0
  294. /package/dist/{src/registry → registry}/providers/types.js +0 -0
  295. /package/dist/{src/registry → registry}/resolve.js +0 -0
  296. /package/dist/{src/registry → registry}/types.js +0 -0
  297. /package/dist/{src/setup → setup}/detect.js +0 -0
  298. /package/dist/{src/setup → setup}/ripgrep-install.js +0 -0
  299. /package/dist/{src/setup → setup}/ripgrep-resolve.js +0 -0
  300. /package/dist/{src/setup → setup}/setup.js +0 -0
  301. /package/dist/{src/setup → setup}/steps.js +0 -0
  302. /package/dist/{src/sources → sources}/include.js +0 -0
  303. /package/dist/{src/sources → sources}/provider-factory.js +0 -0
  304. /package/dist/{src/sources → sources}/provider.js +0 -0
  305. /package/dist/{src/sources → sources}/providers/filesystem.js +0 -0
  306. /package/dist/{src/sources → sources}/providers/git.js +0 -0
  307. /package/dist/{src/sources → sources}/providers/index.js +0 -0
  308. /package/dist/{src/sources → sources}/providers/install-types.js +0 -0
  309. /package/dist/{src/sources → sources}/providers/npm.js +0 -0
  310. /package/dist/{src/sources → sources}/providers/provider-utils.js +0 -0
  311. /package/dist/{src/sources → sources}/providers/sync-from-ref.js +0 -0
  312. /package/dist/{src/sources → sources}/providers/tar-utils.js +0 -0
  313. /package/dist/{src/sources → sources}/providers/website.js +0 -0
  314. /package/dist/{src/sources → sources}/resolve.js +0 -0
  315. /package/dist/{src/sources → sources}/types.js +0 -0
  316. /package/dist/{src/templates → templates}/wiki-templates.js +0 -0
  317. /package/dist/{src/version.js → version.js} +0 -0
  318. /package/dist/{src/wiki → wiki}/wiki.js +0 -0
  319. /package/dist/{src/workflows → workflows}/authoring.js +0 -0
  320. /package/dist/{src/workflows → workflows}/cli.js +0 -0
  321. /package/dist/{src/workflows → workflows}/db.js +0 -0
  322. /package/dist/{src/workflows → workflows}/document-cache.js +0 -0
  323. /package/dist/{src/workflows → workflows}/parser.js +0 -0
  324. /package/dist/{src/workflows → workflows}/renderer.js +0 -0
  325. /package/dist/{src/workflows → workflows}/runs.js +0 -0
  326. /package/dist/{src/workflows → workflows}/schema.js +0 -0
  327. /package/dist/{src/workflows → workflows}/validator.js +0 -0
@@ -1,194 +0,0 @@
1
- import { afterAll, describe, expect, test } from "bun:test";
2
- import { spawnSync } from "node:child_process";
3
- import fs from "node:fs";
4
- import os from "node:os";
5
- import path from "node:path";
6
- import { loadEnv, setKey } from "../src/commands/vault";
7
- // ── Helpers ──────────────────────────────────────────────────────────────────
8
- const tempDirs = [];
9
- function makeTempDir(prefix = "akm-vqa-") {
10
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
11
- tempDirs.push(dir);
12
- return dir;
13
- }
14
- afterAll(() => {
15
- for (const dir of tempDirs) {
16
- fs.rmSync(dir, { recursive: true, force: true });
17
- }
18
- });
19
- const xdgCache = makeTempDir("akm-vqa-cache-");
20
- const xdgConfig = makeTempDir("akm-vqa-config-");
21
- const isolatedHome = makeTempDir("akm-vqa-home-");
22
- const repoRoot = path.resolve(import.meta.dir, "..");
23
- const cliPath = path.join(repoRoot, "src", "cli.ts");
24
- function runCli(args, extraEnv = {}) {
25
- const result = spawnSync("bun", [cliPath, ...args], {
26
- encoding: "utf8",
27
- timeout: 30_000,
28
- cwd: repoRoot,
29
- env: {
30
- ...process.env,
31
- HOME: isolatedHome,
32
- XDG_CACHE_HOME: xdgCache,
33
- XDG_CONFIG_HOME: xdgConfig,
34
- AKM_STASH_DIR: undefined,
35
- ...extraEnv,
36
- },
37
- });
38
- return {
39
- stdout: result.stdout ?? "",
40
- stderr: result.stderr ?? "",
41
- status: result.status ?? 1,
42
- };
43
- }
44
- // ── setKey comment parameter (unit tests) ────────────────────────────────────
45
- describe("setKey: comment parameter", () => {
46
- test("1. writes a new key with a leading comment line when comment provided", () => {
47
- const dir = makeTempDir();
48
- const fp = path.join(dir, "v.env");
49
- setKey(fp, "DB_URL", "postgres://localhost/mydb", "database connection string");
50
- const text = fs.readFileSync(fp, "utf8");
51
- const lines = text.split("\n").filter((l) => l.length > 0);
52
- const commentIdx = lines.indexOf("# database connection string");
53
- const keyIdx = lines.findIndex((l) => l.startsWith("DB_URL="));
54
- expect(commentIdx).toBeGreaterThanOrEqual(0);
55
- expect(keyIdx).toBe(commentIdx + 1);
56
- expect(loadEnv(fp).DB_URL).toBe("postgres://localhost/mydb");
57
- });
58
- test("2. updates an existing comment line in-place when the key is overwritten with a new comment", () => {
59
- const dir = makeTempDir();
60
- const fp = path.join(dir, "v.env");
61
- fs.writeFileSync(fp, "# old comment\nDB_URL=postgres://old\n");
62
- setKey(fp, "DB_URL", "postgres://new", "new comment");
63
- const text = fs.readFileSync(fp, "utf8");
64
- expect(text).toContain("# new comment");
65
- expect(text).not.toContain("# old comment");
66
- expect(loadEnv(fp).DB_URL).toBe("postgres://new");
67
- // Comment line should immediately precede the key line
68
- const lines = text.split("\n").filter((l) => l.length > 0);
69
- const commentIdx = lines.indexOf("# new comment");
70
- const keyIdx = lines.findIndex((l) => l.startsWith("DB_URL="));
71
- expect(keyIdx).toBe(commentIdx + 1);
72
- });
73
- test("3. inserts a new comment before an existing key that lacks one", () => {
74
- const dir = makeTempDir();
75
- const fp = path.join(dir, "v.env");
76
- fs.writeFileSync(fp, "FOO=bar\nDB_URL=postgres://old\nBAZ=qux\n");
77
- setKey(fp, "DB_URL", "postgres://new", "injected comment");
78
- const text = fs.readFileSync(fp, "utf8");
79
- expect(text).toContain("# injected comment");
80
- const lines = text.split("\n").filter((l) => l.length > 0);
81
- const commentIdx = lines.indexOf("# injected comment");
82
- const keyIdx = lines.findIndex((l) => l.startsWith("DB_URL="));
83
- expect(keyIdx).toBe(commentIdx + 1);
84
- // Other keys preserved
85
- expect(loadEnv(fp).FOO).toBe("bar");
86
- expect(loadEnv(fp).BAZ).toBe("qux");
87
- });
88
- test("4. without comment does not modify surrounding comments", () => {
89
- const dir = makeTempDir();
90
- const fp = path.join(dir, "v.env");
91
- fs.writeFileSync(fp, "# original comment\nDB_URL=old\n");
92
- setKey(fp, "DB_URL", "new");
93
- const text = fs.readFileSync(fp, "utf8");
94
- expect(text).toContain("# original comment");
95
- expect(loadEnv(fp).DB_URL).toBe("new");
96
- });
97
- });
98
- // ── vault show alias (CLI tests) ─────────────────────────────────────────────
99
- describe("vault show: alias for vault list <ref>", () => {
100
- test("5. vault show vault:prod matches vault list vault:prod output", () => {
101
- const stashDir = makeTempDir("akm-vqa-stash-");
102
- fs.mkdirSync(path.join(stashDir, "vaults"), { recursive: true });
103
- fs.writeFileSync(path.join(stashDir, "vaults", "prod.env"), "# production keys\nAPI_KEY=secret\nDB_PASS=hidden\n", "utf8");
104
- const listResult = runCli(["vault", "list", "vault:prod"], { AKM_STASH_DIR: stashDir });
105
- const showResult = runCli(["vault", "show", "vault:prod"], { AKM_STASH_DIR: stashDir });
106
- expect(listResult.status).toBe(0);
107
- expect(showResult.status).toBe(0);
108
- const listParsed = JSON.parse(listResult.stdout.trim());
109
- const showParsed = JSON.parse(showResult.stdout.trim());
110
- expect(showParsed).toEqual(listParsed);
111
- });
112
- });
113
- describe("vault list: output flags with optional ref", () => {
114
- test("6. vault list --format json returns the vault list, not a vault:json error", () => {
115
- const stashDir = makeTempDir("akm-vqa-stash-");
116
- fs.mkdirSync(path.join(stashDir, "vaults"), { recursive: true });
117
- fs.writeFileSync(path.join(stashDir, "vaults", "prod.env"), "API_KEY=secret\n", "utf8");
118
- const result = runCli(["vault", "list", "--format", "json"], { AKM_STASH_DIR: stashDir });
119
- expect(result.status).toBe(0);
120
- expect(result.stderr).not.toContain("Vault not found: vault:json");
121
- const parsed = JSON.parse(result.stdout.trim());
122
- expect(parsed.vaults).toEqual([
123
- expect.objectContaining({
124
- ref: "vault:prod",
125
- path: path.join(stashDir, "vaults", "prod.env"),
126
- keyCount: 1,
127
- }),
128
- ]);
129
- });
130
- test("7. vault list json --format json still treats json as the ref", () => {
131
- const stashDir = makeTempDir("akm-vqa-stash-");
132
- fs.mkdirSync(path.join(stashDir, "vaults"), { recursive: true });
133
- fs.writeFileSync(path.join(stashDir, "vaults", "json.env"), "# json vault\nAPI_KEY=secret\n", "utf8");
134
- const result = runCli(["vault", "list", "json", "--format", "json"], { AKM_STASH_DIR: stashDir });
135
- expect(result.status).toBe(0);
136
- const parsed = JSON.parse(result.stdout.trim());
137
- expect(parsed).toMatchObject({
138
- ref: "vault:json",
139
- path: path.join(stashDir, "vaults", "json.env"),
140
- entries: [expect.objectContaining({ key: "API_KEY" })],
141
- });
142
- });
143
- });
144
- // ── vault set combined KEY=VALUE form (CLI tests) ────────────────────────────
145
- describe("vault set: KEY=VALUE combined form", () => {
146
- test("8. vault set prod KEY=value succeeds and writes KEY=value", () => {
147
- const stashDir = makeTempDir("akm-vqa-stash-");
148
- fs.mkdirSync(path.join(stashDir, "vaults"), { recursive: true });
149
- fs.writeFileSync(path.join(stashDir, "vaults", "prod.env"), "", "utf8");
150
- const result = runCli(["vault", "set", "prod", "MY_KEY=myvalue"], { AKM_STASH_DIR: stashDir });
151
- expect(result.status).toBe(0);
152
- const vaultPath = path.join(stashDir, "vaults", "prod.env");
153
- expect(loadEnv(vaultPath).MY_KEY).toBe("myvalue");
154
- });
155
- test("9. vault set prod KEY value (3-arg form) still works", () => {
156
- const stashDir = makeTempDir("akm-vqa-stash-");
157
- fs.mkdirSync(path.join(stashDir, "vaults"), { recursive: true });
158
- fs.writeFileSync(path.join(stashDir, "vaults", "prod.env"), "", "utf8");
159
- const result = runCli(["vault", "set", "prod", "ANOTHER_KEY", "anothervalue"], { AKM_STASH_DIR: stashDir });
160
- expect(result.status).toBe(0);
161
- const vaultPath = path.join(stashDir, "vaults", "prod.env");
162
- expect(loadEnv(vaultPath).ANOTHER_KEY).toBe("anothervalue");
163
- });
164
- test("10. vault set prod KEY=val1=val2 writes KEY with value val1=val2 (split on first =)", () => {
165
- const stashDir = makeTempDir("akm-vqa-stash-");
166
- fs.mkdirSync(path.join(stashDir, "vaults"), { recursive: true });
167
- fs.writeFileSync(path.join(stashDir, "vaults", "prod.env"), "", "utf8");
168
- const result = runCli(["vault", "set", "prod", "COMPLEX_KEY=val1=val2"], { AKM_STASH_DIR: stashDir });
169
- expect(result.status).toBe(0);
170
- const vaultPath = path.join(stashDir, "vaults", "prod.env");
171
- expect(loadEnv(vaultPath).COMPLEX_KEY).toBe("val1=val2");
172
- });
173
- });
174
- // ── vault set --comment flag (CLI tests) ─────────────────────────────────────
175
- describe("vault set: --comment flag", () => {
176
- test("11. vault set prod KEY val --comment writes a comment line above the key", () => {
177
- const stashDir = makeTempDir("akm-vqa-stash-");
178
- fs.mkdirSync(path.join(stashDir, "vaults"), { recursive: true });
179
- fs.writeFileSync(path.join(stashDir, "vaults", "prod.env"), "", "utf8");
180
- const result = runCli(["vault", "set", "prod", "AUTH_TOKEN", "tok123", "--comment", "auth secret"], {
181
- AKM_STASH_DIR: stashDir,
182
- });
183
- expect(result.status).toBe(0);
184
- const vaultPath = path.join(stashDir, "vaults", "prod.env");
185
- const text = fs.readFileSync(vaultPath, "utf8");
186
- expect(text).toContain("# auth secret");
187
- const lines = text.split("\n").filter((l) => l.length > 0);
188
- const commentIdx = lines.indexOf("# auth secret");
189
- const keyIdx = lines.findIndex((l) => l.startsWith("AUTH_TOKEN="));
190
- expect(commentIdx).toBeGreaterThanOrEqual(0);
191
- expect(keyIdx).toBe(commentIdx + 1);
192
- expect(loadEnv(vaultPath).AUTH_TOKEN).toBe("tok123");
193
- });
194
- });
@@ -1,429 +0,0 @@
1
- import { afterAll, afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { buildShellExportScript, createVault, injectIntoEnv, listKeys, loadEnv, setKey, unsetKey, } from "../src/commands/vault";
6
- import { getDbPath } from "../src/core/paths";
7
- import { closeDatabase, getAllEntries, openDatabase } from "../src/indexer/db";
8
- import { akmIndex } from "../src/indexer/indexer";
9
- // ── Test fixtures ───────────────────────────────────────────────────────────
10
- const createdTmpDirs = [];
11
- function tmpDir(label = "vault") {
12
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), `akm-${label}-`));
13
- createdTmpDirs.push(dir);
14
- return dir;
15
- }
16
- afterAll(() => {
17
- for (const dir of createdTmpDirs) {
18
- fs.rmSync(dir, { recursive: true, force: true });
19
- }
20
- });
21
- // ── listKeys ────────────────────────────────────────────────────────────────
22
- describe("listKeys", () => {
23
- test("returns keys + comments only, no values", () => {
24
- const dir = tmpDir();
25
- const fp = path.join(dir, "v.env");
26
- fs.writeFileSync(fp, ["# top comment", "DB_URL=postgres://example", "API_TOKEN=secret-value-do-not-leak", "# bottom comment"].join("\n"));
27
- const result = listKeys(fp);
28
- expect(result.keys).toEqual(["DB_URL", "API_TOKEN"]);
29
- expect(result.comments).toEqual(["top comment", "bottom comment"]);
30
- // Sanity: the function's return shape has no value-bearing field
31
- expect(Object.keys(result).sort()).toEqual(["comments", "keys"]);
32
- });
33
- test("captures only start-of-line comments, never trailing/inline", () => {
34
- const dir = tmpDir();
35
- const fp = path.join(dir, "v.env");
36
- fs.writeFileSync(fp, [
37
- "# header comment",
38
- " # indented comment",
39
- "FOO=bar # trailing-comment-not-extracted",
40
- "BAZ=qux",
41
- "# footer comment",
42
- ].join("\n"));
43
- const result = listKeys(fp);
44
- expect(result.comments).toEqual(["header comment", "indented comment", "footer comment"]);
45
- // The trailing-comment text must not leak in via comments
46
- expect(result.comments.join(" ")).not.toContain("trailing-comment-not-extracted");
47
- });
48
- test("preserves key order and de-duplicates", () => {
49
- const dir = tmpDir();
50
- const fp = path.join(dir, "v.env");
51
- fs.writeFileSync(fp, "FOO=first\nBAR=middle\nFOO=second\n");
52
- expect(listKeys(fp).keys).toEqual(["FOO", "BAR"]);
53
- });
54
- test("recognises `export KEY=value`", () => {
55
- const dir = tmpDir();
56
- const fp = path.join(dir, "v.env");
57
- fs.writeFileSync(fp, "export FOO=bar\n");
58
- expect(listKeys(fp).keys).toEqual(["FOO"]);
59
- });
60
- test("returns empty result for missing file", () => {
61
- const result = listKeys(path.join(tmpDir(), "missing.env"));
62
- expect(result).toEqual({ keys: [], comments: [] });
63
- });
64
- });
65
- // ── loadEnv (delegates to dotenv) ───────────────────────────────────────────
66
- describe("loadEnv", () => {
67
- test("returns parsed key/value pairs via dotenv", () => {
68
- const dir = tmpDir();
69
- const fp = path.join(dir, "v.env");
70
- fs.writeFileSync(fp, 'FOO=bar\nQUOTED="hello world"\nMULTI="line1\\nline2"\n');
71
- const env = loadEnv(fp);
72
- expect(env.FOO).toBe("bar");
73
- expect(env.QUOTED).toBe("hello world");
74
- expect(env.MULTI).toBe("line1\nline2");
75
- });
76
- test("returns empty object for missing file", () => {
77
- expect(loadEnv(path.join(tmpDir(), "missing.env"))).toEqual({});
78
- });
79
- });
80
- // ── setKey / unsetKey ───────────────────────────────────────────────────────
81
- describe("setKey", () => {
82
- test("creates the file and parent directory if missing", () => {
83
- const dir = tmpDir();
84
- const fp = path.join(dir, "vaults", "new.env");
85
- setKey(fp, "FOO", "bar");
86
- expect(fs.existsSync(fp)).toBe(true);
87
- expect(fs.readFileSync(fp, "utf8")).toContain("FOO=bar");
88
- });
89
- test("preserves comments and order when adding a new key", () => {
90
- const dir = tmpDir();
91
- const fp = path.join(dir, "v.env");
92
- fs.writeFileSync(fp, "# top\nFOO=one\n# middle\nBAR=two\n");
93
- setKey(fp, "BAZ", "three");
94
- const text = fs.readFileSync(fp, "utf8");
95
- expect(text).toContain("# top");
96
- expect(text).toContain("# middle");
97
- expect(text).toContain("FOO=one");
98
- expect(text).toContain("BAR=two");
99
- expect(text).toContain("BAZ=three");
100
- // New key appended after existing content (order preserved)
101
- expect(text.indexOf("BAR=two")).toBeLessThan(text.indexOf("BAZ=three"));
102
- });
103
- test("replaces existing key in place", () => {
104
- const dir = tmpDir();
105
- const fp = path.join(dir, "v.env");
106
- fs.writeFileSync(fp, "FOO=old\nBAR=keep\n");
107
- setKey(fp, "FOO", "new");
108
- const result = listKeys(fp);
109
- expect(result.keys).toEqual(["FOO", "BAR"]);
110
- expect(loadEnv(fp).FOO).toBe("new");
111
- expect(loadEnv(fp).BAR).toBe("keep");
112
- });
113
- test("round-trips values containing whitespace, quotes, and special characters", () => {
114
- const dir = tmpDir();
115
- const fp = path.join(dir, "v.env");
116
- setKey(fp, "FOO", "hello world");
117
- setKey(fp, "BAR", 'has"quote');
118
- setKey(fp, "BAZ", "trailing # not a comment");
119
- setKey(fp, "EQ", "a=b=c");
120
- setKey(fp, "BACK", "C:\\path\\to\\file");
121
- setKey(fp, "APOS", "it's fine");
122
- const env = loadEnv(fp);
123
- expect(env.FOO).toBe("hello world");
124
- expect(env.BAR).toBe('has"quote');
125
- expect(env.BAZ).toBe("trailing # not a comment");
126
- expect(env.EQ).toBe("a=b=c");
127
- expect(env.BACK).toBe("C:\\path\\to\\file");
128
- expect(env.APOS).toBe("it's fine");
129
- });
130
- test("rejects values containing newlines", () => {
131
- const dir = tmpDir();
132
- const fp = path.join(dir, "v.env");
133
- expect(() => setKey(fp, "FOO", "line1\nline2")).toThrow();
134
- });
135
- test("rejects values containing both single and double quotes", () => {
136
- const dir = tmpDir();
137
- const fp = path.join(dir, "v.env");
138
- expect(() => setKey(fp, "FOO", 'it\'s "complicated"')).toThrow();
139
- });
140
- test("rejects invalid key names", () => {
141
- const dir = tmpDir();
142
- const fp = path.join(dir, "v.env");
143
- expect(() => setKey(fp, "1BAD", "x")).toThrow();
144
- expect(() => setKey(fp, "WITH-DASH", "x")).toThrow();
145
- expect(() => setKey(fp, "WITH SPACE", "x")).toThrow();
146
- });
147
- test("file is written with mode 0600", () => {
148
- if (process.platform === "win32")
149
- return; // chmod is best-effort on win32
150
- const dir = tmpDir();
151
- const fp = path.join(dir, "v.env");
152
- setKey(fp, "FOO", "bar");
153
- const stat = fs.statSync(fp);
154
- expect(stat.mode & 0o777).toBe(0o600);
155
- });
156
- });
157
- describe("unsetKey", () => {
158
- test("removes a key and returns true", () => {
159
- const dir = tmpDir();
160
- const fp = path.join(dir, "v.env");
161
- fs.writeFileSync(fp, "FOO=one\nBAR=two\n");
162
- expect(unsetKey(fp, "FOO")).toBe(true);
163
- expect(listKeys(fp).keys).toEqual(["BAR"]);
164
- });
165
- test("returns false when the key is absent", () => {
166
- const dir = tmpDir();
167
- const fp = path.join(dir, "v.env");
168
- fs.writeFileSync(fp, "FOO=one\n");
169
- expect(unsetKey(fp, "NOPE")).toBe(false);
170
- });
171
- test("returns false when the file does not exist", () => {
172
- expect(unsetKey(path.join(tmpDir(), "missing.env"), "FOO")).toBe(false);
173
- });
174
- });
175
- // ── createVault ─────────────────────────────────────────────────────────────
176
- describe("createVault", () => {
177
- test("creates an empty file", () => {
178
- const dir = tmpDir();
179
- const fp = path.join(dir, "vaults", "prod.env");
180
- createVault(fp);
181
- expect(fs.existsSync(fp)).toBe(true);
182
- expect(fs.readFileSync(fp, "utf8")).toBe("");
183
- });
184
- test("does not overwrite an existing file", () => {
185
- const dir = tmpDir();
186
- const fp = path.join(dir, "v.env");
187
- fs.writeFileSync(fp, "FOO=existing\n");
188
- createVault(fp);
189
- expect(loadEnv(fp).FOO).toBe("existing");
190
- });
191
- });
192
- // ── injectIntoEnv ───────────────────────────────────────────────────────────
193
- describe("injectIntoEnv", () => {
194
- test("assigns values into the supplied target and returns the list of keys set", () => {
195
- const dir = tmpDir();
196
- const fp = path.join(dir, "v.env");
197
- fs.writeFileSync(fp, "ALPHA=one\nBETA=two\n");
198
- const target = { PRE_EXISTING: "kept" };
199
- const keys = injectIntoEnv(fp, target);
200
- expect(keys.sort()).toEqual(["ALPHA", "BETA"]);
201
- expect(target.ALPHA).toBe("one");
202
- expect(target.BETA).toBe("two");
203
- expect(target.PRE_EXISTING).toBe("kept");
204
- });
205
- test("returns empty list when the file is missing", () => {
206
- const target = {};
207
- expect(injectIntoEnv(path.join(tmpDir(), "missing.env"), target)).toEqual([]);
208
- expect(target).toEqual({});
209
- });
210
- });
211
- // ── quoteValue hardening (shell-metachar defence-in-depth) ──────────────────
212
- //
213
- // Even though `vault load` no longer `source`s the raw vault file (it
214
- // parses with dotenv and sources a safely-escaped temp file), the on-disk
215
- // vault format itself must be robust to direct `source` by any future
216
- // caller. These tests lock in that every non-trivial value is quoted.
217
- describe("setKey: shell-metachar hardening", () => {
218
- test("values containing $, backticks, or $(...) are quoted on disk", () => {
219
- const dir = tmpDir();
220
- const fp = path.join(dir, "v.env");
221
- setKey(fp, "DOLLAR", "abc$DEF");
222
- setKey(fp, "BACKTICK", "pre`whoami`post");
223
- setKey(fp, "CMDSUB", "pre$(id)post");
224
- const raw = fs.readFileSync(fp, "utf8");
225
- // None of these should appear as an unquoted assignment that a shell
226
- // would expand on `source`. Our impl single-quotes them.
227
- expect(raw).toMatch(/^DOLLAR='abc\$DEF'$/m);
228
- expect(raw).toMatch(/^BACKTICK='pre`whoami`post'$/m);
229
- expect(raw).toMatch(/^CMDSUB='pre\$\(id\)post'$/m);
230
- });
231
- test("values with shell-special chars ; & | * ? ( ) { } [ ] > < ~ ! are quoted", () => {
232
- const dir = tmpDir();
233
- const fp = path.join(dir, "v.env");
234
- for (const [k, v] of Object.entries({
235
- SEMI: "a;b",
236
- AMP: "a&b",
237
- PIPE: "a|b",
238
- GLOB: "abc*",
239
- QMARK: "abc?",
240
- PAREN: "a(b)c",
241
- BRACE: "a{b}c",
242
- BRACK: "a[b]c",
243
- REDIR: "a>b",
244
- REDIR2: "a<b",
245
- TILDE: "~/foo",
246
- BANG: "a!b",
247
- })) {
248
- setKey(fp, k, v);
249
- }
250
- const raw = fs.readFileSync(fp, "utf8");
251
- for (const line of raw.split("\n")) {
252
- if (!line.includes("="))
253
- continue;
254
- const [, val] = line.match(/^[A-Z_]+=(.*)$/) ?? [];
255
- if (!val)
256
- continue;
257
- // Any non-empty value must be quoted (either '...' or "...").
258
- expect(val[0] === "'" || val[0] === '"').toBe(true);
259
- }
260
- });
261
- test("round-trip preserves exact values through dotenv.parse", () => {
262
- const dir = tmpDir();
263
- const fp = path.join(dir, "v.env");
264
- const payloads = {
265
- DOLLAR: "abc$HOME",
266
- BACKTICK: "pre`whoami`",
267
- CMDSUB: "pre$(rm -rf /tmp/shouldnothappen)post",
268
- GLOB: "*.env",
269
- TILDE: "~/root",
270
- SEMI: "a;b",
271
- BANG: "echo!123",
272
- AMPERSAND: "x && y",
273
- NESTED: 'it has "double" quotes only',
274
- };
275
- for (const [k, v] of Object.entries(payloads))
276
- setKey(fp, k, v);
277
- const env = loadEnv(fp);
278
- for (const [k, v] of Object.entries(payloads)) {
279
- expect(env[k]).toBe(v);
280
- }
281
- });
282
- });
283
- // ── buildShellExportScript (vault load safety) ──────────────────────────────
284
- describe("buildShellExportScript", () => {
285
- test("emits export lines with `'\\''` escaping; no expansion-triggering syntax", () => {
286
- const dir = tmpDir();
287
- const fp = path.join(dir, "v.env");
288
- setKey(fp, "PLAIN", "hello");
289
- setKey(fp, "DOLLAR", "abc$HOME");
290
- setKey(fp, "APOS", "it's fine");
291
- const script = buildShellExportScript(fp);
292
- // Every line must be a single-quoted export assignment.
293
- for (const line of script.split("\n").filter(Boolean)) {
294
- expect(line).toMatch(/^export [A-Za-z_][A-Za-z0-9_]*='.*'$/);
295
- }
296
- expect(script).toContain("export PLAIN='hello'");
297
- expect(script).toContain("export DOLLAR='abc$HOME'");
298
- // Single quote inside value must be encoded as '\''
299
- expect(script).toContain("export APOS='it'\\''s fine'");
300
- });
301
- test("sourcing the emitted script populates env without executing payloads", () => {
302
- const dir = tmpDir();
303
- const fp = path.join(dir, "v.env");
304
- // The "value" is designed to execute `touch evidence` if it ever
305
- // reaches a shell without quoting; a safe implementation must keep it
306
- // literal.
307
- const evidence = path.join(dir, "evidence");
308
- setKey(fp, "EVIL", `$(touch ${evidence})`);
309
- setKey(fp, "OK", "ok-value");
310
- const script = buildShellExportScript(fp);
311
- const scriptPath = path.join(dir, "source-me.sh");
312
- fs.writeFileSync(scriptPath, script);
313
- const { spawnSync } = require("node:child_process");
314
- const result = spawnSync("bash", ["-c", `set -eu; . '${scriptPath}'; printf '%s\\n' "$EVIL" "$OK"`], {
315
- encoding: "utf8",
316
- });
317
- expect(result.status).toBe(0);
318
- const [evilOut, okOut] = (result.stdout ?? "").split("\n");
319
- // EVIL must come back as the literal string — not executed.
320
- expect(evilOut).toBe(`$(touch ${evidence})`);
321
- expect(okOut).toBe("ok-value");
322
- // The command substitution must NOT have run.
323
- expect(fs.existsSync(evidence)).toBe(false);
324
- });
325
- });
326
- // ── Indexer leakage safety (the critical security test) ─────────────────────
327
- const originalXdgConfig = process.env.XDG_CONFIG_HOME;
328
- const originalXdgCache = process.env.XDG_CACHE_HOME;
329
- const originalAkmStash = process.env.AKM_STASH_DIR;
330
- let testConfigDir = "";
331
- let testCacheDir = "";
332
- beforeEach(() => {
333
- testConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-vault-config-"));
334
- testCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-vault-cache-"));
335
- process.env.XDG_CONFIG_HOME = testConfigDir;
336
- process.env.XDG_CACHE_HOME = testCacheDir;
337
- const dbPath = getDbPath();
338
- for (const f of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
339
- try {
340
- fs.unlinkSync(f);
341
- }
342
- catch {
343
- /* ignore */
344
- }
345
- }
346
- });
347
- afterEach(() => {
348
- if (originalXdgConfig === undefined)
349
- delete process.env.XDG_CONFIG_HOME;
350
- else
351
- process.env.XDG_CONFIG_HOME = originalXdgConfig;
352
- if (originalXdgCache === undefined)
353
- delete process.env.XDG_CACHE_HOME;
354
- else
355
- process.env.XDG_CACHE_HOME = originalXdgCache;
356
- if (originalAkmStash === undefined)
357
- delete process.env.AKM_STASH_DIR;
358
- else
359
- process.env.AKM_STASH_DIR = originalAkmStash;
360
- if (testConfigDir)
361
- fs.rmSync(testConfigDir, { recursive: true, force: true });
362
- if (testCacheDir)
363
- fs.rmSync(testCacheDir, { recursive: true, force: true });
364
- });
365
- const SECRET_VALUE = "correct-horse-battery-staple-do-not-leak";
366
- describe("vault indexer safety", () => {
367
- test("vault values never appear in the FTS index, search_text, or entry_json", async () => {
368
- const stashDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-vault-stash-"));
369
- createdTmpDirs.push(stashDir);
370
- fs.mkdirSync(path.join(stashDir, "vaults"), { recursive: true });
371
- const vaultPath = path.join(stashDir, "vaults", "prod.env");
372
- fs.writeFileSync(vaultPath, [
373
- "# Production secrets",
374
- `SECRET_TOKEN=${SECRET_VALUE}`,
375
- "DB_PASSWORD=another-secret-pa55w0rd",
376
- "# Last rotated 2026-04-01",
377
- ].join("\n"));
378
- process.env.AKM_STASH_DIR = stashDir;
379
- const result = await akmIndex({ stashDir, full: true });
380
- expect(result.totalEntries).toBe(1);
381
- const db = openDatabase();
382
- try {
383
- const entries = getAllEntries(db);
384
- expect(entries.length).toBe(1);
385
- const vaultEntry = entries[0];
386
- // 1. The entry is classified as vault
387
- expect(vaultEntry.entry.type).toBe("vault");
388
- expect(vaultEntry.entry.name).toBe("prod");
389
- // 2. Keys are exposed via searchHints
390
- expect(vaultEntry.entry.searchHints).toContain("SECRET_TOKEN");
391
- expect(vaultEntry.entry.searchHints).toContain("DB_PASSWORD");
392
- // 3. Comments are surfaced in the description
393
- expect(vaultEntry.entry.description).toContain("Production secrets");
394
- // 4. CRITICAL: the secret value is nowhere in the persisted record
395
- const json = JSON.stringify(vaultEntry);
396
- expect(json).not.toContain(SECRET_VALUE);
397
- expect(json).not.toContain("another-secret-pa55w0rd");
398
- const rows = db.query("SELECT search_text, entry_json FROM entries WHERE entry_type = ?").all("vault");
399
- expect(rows.length).toBe(1);
400
- expect(rows[0].search_text ?? "").not.toContain(SECRET_VALUE);
401
- expect(rows[0].entry_json).not.toContain(SECRET_VALUE);
402
- const ftsHit = db
403
- .query("SELECT count(*) AS c FROM entries_fts WHERE entries_fts MATCH ?")
404
- .get("correct");
405
- expect(ftsHit.c).toBe(0);
406
- }
407
- finally {
408
- closeDatabase(db);
409
- }
410
- });
411
- test("vault entries are searchable by key name", async () => {
412
- const stashDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-vault-stash-"));
413
- createdTmpDirs.push(stashDir);
414
- fs.mkdirSync(path.join(stashDir, "vaults"), { recursive: true });
415
- fs.writeFileSync(path.join(stashDir, "vaults", "prod.env"), "STRIPE_API_KEY=sk_test_xxx\n");
416
- process.env.AKM_STASH_DIR = stashDir;
417
- await akmIndex({ stashDir, full: true });
418
- const db = openDatabase();
419
- try {
420
- const hit = db
421
- .query("SELECT count(*) AS c FROM entries_fts WHERE entries_fts MATCH ?")
422
- .get("STRIPE_API_KEY");
423
- expect(hit.c).toBe(1);
424
- }
425
- finally {
426
- closeDatabase(db);
427
- }
428
- });
429
- });