akm-cli 0.8.7 → 0.8.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +428 -0
- package/dist/assets/help/help-proposals.md +1 -2
- package/dist/assets/hints/cli-hints-full.md +34 -19
- package/dist/assets/hints/cli-hints-short.md +1 -1
- package/dist/assets/profiles/catchup.json +13 -0
- package/dist/assets/profiles/consolidate.json +13 -0
- package/dist/assets/profiles/frequent.json +13 -0
- package/dist/assets/tasks/core/backup.yml +4 -0
- package/dist/assets/tasks/core/extract.yml +4 -0
- package/dist/assets/tasks/core/improve.yml +4 -0
- package/dist/assets/tasks/core/index-refresh.yml +4 -0
- package/dist/assets/tasks/core/sync.yml +4 -0
- package/dist/assets/tasks/core/update-stashes.yml +4 -0
- package/dist/assets/tasks/core/version-check.yml +4 -0
- package/dist/assets/templates/html/default.html +78 -0
- package/dist/assets/templates/html/health.html +560 -0
- package/dist/assets/templates/html/vendor/echarts.min.js +45 -0
- package/dist/cli/config-migrate.js +6 -6
- package/dist/cli/config-validate.js +4 -4
- package/dist/cli/confirm.js +3 -3
- package/dist/cli/parse-args.js +1 -1
- package/dist/cli/shared.js +72 -19
- package/dist/cli-node.mjs +26 -0
- package/dist/cli.js +206 -3866
- package/dist/commands/{agent-dispatch.js → agent/agent-dispatch.js} +6 -6
- package/dist/commands/{agent-support.js → agent/agent-support.js} +2 -2
- package/dist/commands/agent/contribute-cli.js +200 -0
- package/dist/commands/completions.js +1 -1
- package/dist/commands/config-cli.js +230 -3
- package/dist/commands/db-cli.js +2 -2
- package/dist/commands/env/env-cli.js +529 -0
- package/dist/commands/env/env.js +410 -0
- package/dist/commands/env/secret-cli.js +259 -0
- package/dist/commands/{secret.js → env/secret.js} +6 -47
- package/dist/commands/events.js +4 -4
- package/dist/commands/feedback-cli.js +18 -34
- package/dist/commands/graph/graph-cli.js +132 -0
- package/dist/commands/{graph.js → graph/graph.js} +22 -16
- package/dist/commands/health/checks.js +279 -0
- package/dist/commands/health/html-report.js +448 -0
- package/dist/commands/health.js +189 -266
- package/dist/commands/{consolidate.js → improve/consolidate.js} +48 -36
- package/dist/commands/{distill-promotion-policy.js → improve/distill-promotion-policy.js} +3 -3
- package/dist/commands/{distill.js → improve/distill.js} +39 -18
- package/dist/commands/{eval-cases.js → improve/eval-cases.js} +1 -1
- package/dist/commands/{extract-cli.js → improve/extract-cli.js} +4 -4
- package/dist/commands/{extract-prompt.js → improve/extract-prompt.js} +2 -2
- package/dist/commands/{extract.js → improve/extract.js} +221 -26
- package/dist/commands/{improve-auto-accept.js → improve/improve-auto-accept.js} +30 -4
- package/dist/commands/{improve-cli.js → improve/improve-cli.js} +44 -22
- package/dist/commands/{improve-profiles.js → improve/improve-profiles.js} +13 -7
- package/dist/commands/{improve-result-file.js → improve/improve-result-file.js} +1 -1
- package/dist/commands/{improve.js → improve/improve.js} +672 -292
- package/dist/{core → commands/improve/memory}/memory-belief.js +2 -2
- package/dist/{core → commands/improve/memory}/memory-contradiction-detect.js +5 -5
- package/dist/{core → commands/improve/memory}/memory-improve.js +4 -4
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/{reflect.js → improve/reflect.js} +58 -28
- package/dist/commands/improve/session-asset.js +248 -0
- package/dist/commands/lint/agent-linter.js +1 -1
- package/dist/commands/lint/base-linter.js +55 -37
- package/dist/commands/lint/command-linter.js +1 -1
- package/dist/commands/lint/default-linter.js +1 -1
- package/dist/commands/lint/env-key-rules.js +1 -1
- package/dist/commands/lint/index.js +19 -25
- package/dist/commands/lint/knowledge-linter.js +1 -1
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +8 -8
- package/dist/commands/lint/skill-linter.js +1 -1
- package/dist/commands/lint/task-linter.js +1 -1
- package/dist/commands/lint/workflow-linter.js +1 -1
- package/dist/commands/lint.js +1 -1
- package/dist/commands/observability-cli.js +244 -0
- package/dist/commands/proposal/drain-policies.js +3 -3
- package/dist/commands/proposal/drain.js +87 -15
- package/dist/commands/proposal/proposal-cli.js +490 -0
- package/dist/commands/{proposal.js → proposal/proposal.js} +17 -6
- package/dist/commands/{propose.js → proposal/propose.js} +11 -11
- package/dist/{core → commands/proposal/validators}/proposal-quality-validators.js +8 -3
- package/dist/{core → commands/proposal/validators}/proposal-validators.js +5 -5
- package/dist/{core → commands/proposal/validators}/proposals.js +374 -345
- package/dist/commands/{curate.js → read/curate.js} +7 -7
- package/dist/commands/{knowledge.js → read/knowledge.js} +22 -9
- package/dist/commands/{registry-search.js → read/registry-search.js} +5 -5
- package/dist/commands/{remember-cli.js → read/remember-cli.js} +15 -7
- package/dist/commands/read/search-cli.js +207 -0
- package/dist/commands/{search.js → read/search.js} +22 -27
- package/dist/commands/{show.js → read/show.js} +31 -45
- package/dist/commands/registry-cli.js +8 -8
- package/dist/commands/remember.js +14 -10
- package/dist/commands/sources/add-cli.js +293 -0
- package/dist/commands/{history.js → sources/history.js} +27 -25
- package/dist/commands/{info.js → sources/info.js} +6 -6
- package/dist/commands/{init.js → sources/init.js} +6 -6
- package/dist/commands/{installed-stashes.js → sources/installed-stashes.js} +12 -12
- package/dist/commands/{migration-help.js → sources/migration-help.js} +3 -2
- package/dist/commands/{schema-repair.js → sources/schema-repair.js} +8 -8
- package/dist/commands/{self-update.js → sources/self-update.js} +10 -9
- package/dist/commands/{source-add.js → sources/source-add.js} +10 -10
- package/dist/commands/{source-clone.js → sources/source-clone.js} +7 -7
- package/dist/commands/{source-manage.js → sources/source-manage.js} +4 -4
- package/dist/commands/sources/sources-cli.js +305 -0
- package/dist/commands/sources/stash-cli.js +219 -0
- package/dist/commands/{stash-skeleton.js → sources/stash-skeleton.js} +2 -1
- package/dist/commands/tasks/default-tasks.js +173 -0
- package/dist/commands/tasks/tasks-cli.js +210 -0
- package/dist/commands/{tasks.js → tasks/tasks.js} +14 -14
- package/dist/commands/wiki-cli.js +307 -0
- package/dist/commands/workflow-cli.js +329 -0
- package/dist/core/action-contributors.js +1 -1
- package/dist/core/assert.js +40 -0
- package/dist/core/asset/asset-create.js +54 -0
- package/dist/core/{asset-ref.js → asset/asset-ref.js} +21 -4
- package/dist/core/{asset-registry.js → asset/asset-registry.js} +3 -3
- package/dist/core/{asset-spec.js → asset/asset-spec.js} +17 -31
- package/dist/core/{markdown.js → asset/markdown.js} +1 -1
- package/dist/core/{stash-meta.js → asset/stash-meta.js} +1 -1
- package/dist/core/best-effort.js +64 -0
- package/dist/core/common.js +32 -18
- package/dist/core/{config-io.js → config/config-io.js} +29 -19
- package/dist/core/{config-migration.js → config/config-migration.js} +11 -9
- package/dist/core/{config-schema.js → config/config-schema.js} +50 -7
- package/dist/core/config/config-types.js +16 -0
- package/dist/core/{config-walker.js → config/config-walker.js} +2 -2
- package/dist/core/{config.js → config/config.js} +10 -8
- package/dist/core/env-secret-ref.js +90 -0
- package/dist/core/errors.js +13 -3
- package/dist/core/events.js +27 -4
- package/dist/core/file-lock.js +1 -1
- package/dist/core/improve-types.js +48 -0
- package/dist/core/lesson-lint.js +2 -2
- package/dist/core/logs-db.js +304 -0
- package/dist/core/paths.js +2 -2
- package/dist/core/ripgrep/install.js +2 -2
- package/dist/core/ripgrep/resolve.js +2 -2
- package/dist/core/state-db.js +195 -60
- package/dist/core/text-truncation.js +148 -0
- package/dist/core/time.js +1 -1
- package/dist/core/write-source.js +98 -85
- package/dist/indexer/{db-backup.js → db/db-backup.js} +9 -24
- package/dist/indexer/{db.js → db/db.js} +128 -118
- package/dist/indexer/{graph-db.js → db/graph-db.js} +9 -4
- package/dist/indexer/{llm-cache.js → db/llm-cache.js} +15 -12
- package/dist/indexer/ensure-index.js +4 -4
- package/dist/indexer/{graph-boost.js → graph/graph-boost.js} +1 -1
- package/dist/indexer/{graph-extraction.js → graph/graph-extraction.js} +55 -13
- package/dist/indexer/indexer.js +37 -30
- package/dist/indexer/init.js +54 -0
- package/dist/indexer/manifest.js +10 -10
- package/dist/indexer/{memory-inference.js → passes/memory-inference.js} +141 -33
- package/dist/indexer/{metadata-contributors.js → passes/metadata-contributors.js} +10 -8
- package/dist/indexer/{metadata.js → passes/metadata.js} +15 -19
- package/dist/indexer/{staleness-detect.js → passes/staleness-detect.js} +53 -12
- package/dist/indexer/{db-search.js → search/db-search.js} +28 -16
- package/dist/indexer/{ranking-contributors.js → search/ranking-contributors.js} +1 -1
- package/dist/indexer/{ranking.js → search/ranking.js} +2 -2
- package/dist/indexer/{search-hit-enrichers.js → search/search-hit-enrichers.js} +3 -3
- package/dist/indexer/{search-source.js → search/search-source.js} +8 -8
- package/dist/indexer/{semantic-status.js → search/semantic-status.js} +3 -3
- package/dist/indexer/usage/unmigrated-vaults-guard.js +94 -0
- package/dist/indexer/{usage-events.js → usage/usage-events.js} +32 -0
- package/dist/indexer/{file-context.js → walk/file-context.js} +10 -15
- package/dist/indexer/{matchers.js → walk/matchers.js} +13 -9
- package/dist/indexer/{path-resolver.js → walk/path-resolver.js} +6 -6
- package/dist/indexer/{project-context.js → walk/project-context.js} +1 -1
- package/dist/indexer/{walker.js → walk/walker.js} +4 -3
- package/dist/integrations/agent/builder-shared.js +39 -0
- package/dist/integrations/agent/builders.js +14 -81
- package/dist/integrations/agent/config.js +6 -4
- package/dist/integrations/agent/detect.js +1 -1
- package/dist/integrations/agent/index.js +23 -8
- package/dist/integrations/agent/prompts.js +2 -3
- package/dist/integrations/agent/runner.js +22 -3
- package/dist/integrations/agent/spawn.js +9 -10
- package/dist/integrations/harnesses/claude/agent-builder.js +48 -0
- package/dist/integrations/harnesses/claude/config-import.js +70 -0
- package/dist/integrations/harnesses/claude/index.js +64 -0
- package/dist/integrations/{session-logs/providers/claude-code.js → harnesses/claude/session-log.js} +32 -5
- package/dist/integrations/harnesses/index.js +144 -0
- package/dist/integrations/harnesses/opencode/agent-builder.js +43 -0
- package/dist/integrations/harnesses/opencode/config-import.js +82 -0
- package/dist/integrations/harnesses/opencode/index.js +59 -0
- package/dist/integrations/{session-logs/providers/opencode.js → harnesses/opencode/session-log.js} +1 -1
- package/dist/integrations/harnesses/opencode-sdk/index.js +49 -0
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +234 -0
- package/dist/integrations/harnesses/types.js +43 -0
- package/dist/integrations/lockfile.js +7 -16
- package/dist/integrations/session-logs/index.js +82 -9
- package/dist/llm/call-ai.js +4 -4
- package/dist/llm/client.js +146 -6
- package/dist/llm/embedder.js +6 -6
- package/dist/llm/embedders/local.js +9 -22
- package/dist/llm/embedders/remote.js +2 -2
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +31 -12
- package/dist/llm/index-passes.js +1 -1
- package/dist/llm/memory-infer.js +12 -5
- package/dist/llm/metadata-enhance.js +2 -2
- package/dist/llm/usage-persist.js +77 -0
- package/dist/llm/usage-telemetry.js +103 -0
- package/dist/output/context.js +9 -46
- package/dist/output/html-render.js +73 -0
- package/dist/output/renderers.js +88 -58
- package/dist/output/shapes/curate.js +7 -3
- package/dist/output/shapes/distill.js +7 -3
- package/dist/output/shapes/env-list.js +18 -16
- package/dist/output/shapes/events.js +5 -4
- package/dist/output/shapes/helpers.js +19 -5
- package/dist/output/shapes/history.js +7 -3
- package/dist/output/shapes/passthrough.js +8 -11
- package/dist/output/shapes/{proposal-accept.js → proposal/accept.js} +7 -3
- package/dist/output/shapes/{proposal-diff.js → proposal/diff.js} +7 -3
- package/dist/output/shapes/{proposal-list.js → proposal/list.js} +7 -3
- package/dist/output/shapes/{proposal-producer.js → proposal/producer.js} +5 -4
- package/dist/output/shapes/{proposal-reject.js → proposal/reject.js} +7 -3
- package/dist/output/shapes/{proposal-show.js → proposal/show.js} +7 -3
- package/dist/output/shapes/registry-search.js +7 -3
- package/dist/output/shapes/registry.js +12 -0
- package/dist/output/shapes/search.js +7 -3
- package/dist/output/shapes/secret-list.js +18 -16
- package/dist/output/shapes/show.js +7 -3
- package/dist/output/shapes.js +55 -30
- package/dist/output/text/add.js +2 -3
- package/dist/output/text/clone.js +2 -3
- package/dist/output/text/config.js +2 -3
- package/dist/output/text/curate.js +4 -3
- package/dist/output/text/distill.js +2 -3
- package/dist/output/text/enable-disable.js +5 -4
- package/dist/output/text/env.js +13 -0
- package/dist/output/text/events.js +5 -4
- package/dist/output/text/feedback.js +4 -3
- package/dist/output/text/helpers.js +123 -40
- package/dist/output/text/history.js +2 -3
- package/dist/output/text/import.js +2 -3
- package/dist/output/text/index.js +2 -3
- package/dist/output/text/info.js +2 -3
- package/dist/output/text/init.js +2 -3
- package/dist/output/text/list.js +2 -3
- package/dist/output/text/proposal/producer.js +9 -0
- package/dist/output/text/proposal/proposal.js +13 -0
- package/dist/output/text/registry-commands.js +8 -7
- package/dist/output/text/registry.js +12 -0
- package/dist/output/text/remember.js +4 -3
- package/dist/output/text/remove.js +2 -3
- package/dist/output/text/save.js +2 -3
- package/dist/output/text/search.js +4 -3
- package/dist/output/text/show.js +4 -3
- package/dist/output/text/update.js +2 -3
- package/dist/output/text/upgrade.js +2 -3
- package/dist/output/text/wiki.js +12 -11
- package/dist/output/text/workflow.js +12 -10
- package/dist/output/text.js +66 -32
- package/dist/registry/build-index.js +11 -10
- package/dist/registry/factory.js +1 -1
- package/dist/registry/origin-resolve.js +1 -1
- package/dist/registry/providers/index.js +2 -2
- package/dist/registry/providers/skills-sh.js +91 -72
- package/dist/registry/providers/static-index.js +75 -52
- package/dist/registry/resolve.js +3 -3
- package/dist/runtime.js +242 -0
- package/dist/scripts/migrate-storage.js +1654 -683
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +254 -168
- package/dist/setup/detect.js +311 -9
- package/dist/setup/harness-config-import.js +6 -120
- package/dist/setup/setup.js +454 -43
- package/dist/sources/include.js +1 -1
- package/dist/sources/provider-factory.js +2 -2
- package/dist/sources/providers/filesystem.js +3 -3
- package/dist/sources/providers/git.js +9 -9
- package/dist/sources/providers/index.js +4 -4
- package/dist/sources/providers/npm.js +6 -6
- package/dist/sources/providers/provider-utils.js +13 -20
- package/dist/sources/providers/sync-from-ref.js +5 -5
- package/dist/sources/providers/tar-utils.js +2 -2
- package/dist/sources/providers/website.js +2 -2
- package/dist/sources/resolve.js +5 -5
- package/dist/sources/website-ingest.js +5 -5
- package/dist/storage/database.js +102 -0
- package/dist/storage/engines/sqlite-migrations.js +42 -0
- package/dist/storage/locations.js +25 -0
- package/dist/storage/repositories/index-db.js +43 -0
- package/dist/storage/repositories/workflow-runs-repository.js +141 -0
- package/dist/tasks/backends/cron.js +4 -4
- package/dist/tasks/backends/exec-utils.js +32 -0
- package/dist/tasks/backends/index.js +3 -3
- package/dist/tasks/backends/launchd.js +7 -14
- package/dist/tasks/backends/schtasks.js +7 -16
- package/dist/tasks/embedded.js +71 -0
- package/dist/tasks/parser.js +2 -2
- package/dist/tasks/resolveAkmBin.js +1 -1
- package/dist/tasks/runner.js +127 -31
- package/dist/tasks/schedule.js +1 -1
- package/dist/tasks/validator.js +7 -7
- package/dist/text-import-hook.mjs +51 -0
- package/dist/version.js +2 -1
- package/dist/wiki/wiki.js +7 -7
- package/dist/workflows/{authoring.js → authoring/authoring.js} +6 -6
- package/dist/workflows/{scope-key.js → authoring/scope-key.js} +1 -1
- package/dist/workflows/cli.js +1 -1
- package/dist/workflows/db.js +54 -32
- package/dist/workflows/parser.js +4 -4
- package/dist/workflows/renderer.js +5 -5
- package/dist/workflows/runtime/agent-identity.js +56 -0
- package/dist/workflows/runtime/checkin.js +57 -0
- package/dist/workflows/{runs.js → runtime/runs.js} +197 -101
- package/dist/workflows/validate-summary.js +82 -0
- package/docs/README.md +1 -1
- package/docs/data-and-telemetry.md +6 -6
- package/package.json +17 -8
- package/dist/commands/add-cli.js +0 -279
- package/dist/commands/env.js +0 -213
- package/dist/integrations/agent/sdk-runner.js +0 -126
- package/dist/output/shapes/vault-list.js +0 -19
- package/dist/output/text/proposal-producer.js +0 -8
- package/dist/output/text/proposal.js +0 -12
- package/dist/output/text/vault.js +0 -16
- /package/dist/core/{asset-serialize.js → asset/asset-serialize.js} +0 -0
- /package/dist/core/{frontmatter.js → asset/frontmatter.js} +0 -0
- /package/dist/core/{config-sources.js → config/config-sources.js} +0 -0
- /package/dist/indexer/{graph-dedup.js → graph/graph-dedup.js} +0 -0
- /package/dist/{core/config-types.js → indexer/passes/pass-context.js} +0 -0
- /package/dist/indexer/{search-fields.js → search/search-fields.js} +0 -0
- /package/dist/indexer/{index-context.js → walk/index-context.js} +0 -0
- /package/dist/workflows/{document-cache.js → runtime/document-cache.js} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
|
|
6
6
|
"keywords": [
|
|
@@ -50,13 +50,16 @@
|
|
|
50
50
|
"akm-migrate-storage": "dist/scripts/migrate-storage.js"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
|
-
"preinstall": "node -e \"var ua=process.env.npm_config_user_agent||'';if(process.versions.bun||ua.startsWith('bun/')||process.env.BUN_INSTALL){process.exit(0)}console.error('\\n ERROR: akm-cli
|
|
54
|
-
"build": "rm -rf dist && bun run tsc --project ./tsconfig.build.json && bun scripts/copy-assets.ts",
|
|
53
|
+
"preinstall": "node -e \"var ua=process.env.npm_config_user_agent||'';var major=parseInt((process.versions.node||'0').split('.')[0],10);if(process.versions.bun||ua.startsWith('bun/')||process.env.BUN_INSTALL||major>=20){process.exit(0)}console.error('\\n ERROR: akm-cli requires the Bun runtime (https://bun.sh), Node.js >= 20, or the prebuilt binary.\\n Install options:\\n 1. Bun: curl -fsSL https://bun.sh/install | bash && bun install -g akm-cli\\n 2. Binary: curl -fsSL https://github.com/itlackey/akm/releases/latest/download/install.sh | bash\\n');process.exit(1)\"",
|
|
54
|
+
"build": "rm -rf dist && bun run tsc --project ./tsconfig.build.json && bun scripts/copy-assets.ts && bun scripts/fix-esm-extensions.ts",
|
|
55
55
|
"check": "bun run lint && bunx tsc --noEmit && bun run test:unit && bun run test:integration",
|
|
56
|
+
"check:fast": "bun run lint && bunx tsc --noEmit && bun run test:unit",
|
|
56
57
|
"check:changed": "bun test tests/output-baseline.test.ts tests/integration/e2e.test.ts tests/stash-search.test.ts && bun run lint && bunx tsc --noEmit",
|
|
57
|
-
"test": "bun test --parallel
|
|
58
|
-
"test:unit": "bun test --parallel
|
|
59
|
-
"test:integration": "bun test --parallel
|
|
58
|
+
"test": "bun test --parallel=${TEST_PARALLEL:-12} --timeout=30000 ./tests --path-ignore-patterns=tests/integration",
|
|
59
|
+
"test:unit": "bun test --parallel=${TEST_PARALLEL:-12} --timeout=30000 ./tests --path-ignore-patterns=tests/integration",
|
|
60
|
+
"test:integration": "bun test --parallel=${TEST_PARALLEL:-12} --timeout=30000 ./tests/integration ./tests/commands ./tests/workflows",
|
|
61
|
+
"test:node-smoke": "bun scripts/node-smoke.ts",
|
|
62
|
+
"test:node-compat": "AKM_NODE_COMPAT_TESTS=1 bun test --timeout=120000 tests/integration/node-compat.test.ts",
|
|
60
63
|
"test:sharded": "bun test ./tests --shard=1/4 & bun test ./tests --shard=2/4 & bun test ./tests --shard=3/4 & bun test ./tests --shard=4/4 & wait",
|
|
61
64
|
"test:time": "bun scripts/test-timing-report.ts",
|
|
62
65
|
"lint:isolation": "bun scripts/lint-tests-isolation.ts",
|
|
@@ -64,7 +67,8 @@
|
|
|
64
67
|
"lint:devto-posts:fix": "bun scripts/lint-devto-posts.ts --fix",
|
|
65
68
|
"publish:devto": "npx -y @sinedied/devto-cli push \"docs/posts/**/*.md\" --token \"$DEVTO_TOKEN\" --repo \"$GITHUB_REPOSITORY\" --branch \"${GITHUB_REF_NAME:-main}\" --reconcile",
|
|
66
69
|
"release:check": "./tests/release-check.sh",
|
|
67
|
-
"lint": "bunx biome check src/ tests/ && bun scripts/lint-tests-isolation.ts && bun scripts/lint-license-headers.ts",
|
|
70
|
+
"lint": "bunx biome check src/ tests/ && bun scripts/lint-tests-isolation.ts && bun scripts/lint-license-headers.ts && bun scripts/lint-runtime-boundary.ts",
|
|
71
|
+
"lint:runtime-boundary": "bun scripts/lint-runtime-boundary.ts",
|
|
68
72
|
"lint:tests-isolation": "bun scripts/lint-tests-isolation.ts",
|
|
69
73
|
"lint:fix": "bunx biome check --write src/ tests/",
|
|
70
74
|
"format": "bunx biome format --write src/ tests/",
|
|
@@ -76,22 +80,27 @@
|
|
|
76
80
|
},
|
|
77
81
|
"devDependencies": {
|
|
78
82
|
"@biomejs/biome": "^2.4.14",
|
|
83
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
79
84
|
"@types/node": "^22.19.17",
|
|
85
|
+
"@types/semver": "^7.5.8",
|
|
80
86
|
"bun-types": "^1.3.13",
|
|
81
87
|
"typescript": "^5.9.3"
|
|
82
88
|
},
|
|
83
89
|
"optionalDependencies": {
|
|
84
90
|
"@huggingface/transformers": "^4.2.0",
|
|
91
|
+
"better-sqlite3": "^11.8.0",
|
|
85
92
|
"sqlite-vec": "^0.1.9"
|
|
86
93
|
},
|
|
87
94
|
"engines": {
|
|
88
|
-
"bun": ">=1.0.0"
|
|
95
|
+
"bun": ">=1.0.0",
|
|
96
|
+
"node": ">=20.0.0"
|
|
89
97
|
},
|
|
90
98
|
"dependencies": {
|
|
91
99
|
"@clack/prompts": "^1.3.0",
|
|
92
100
|
"@opencode-ai/sdk": "1.2.20",
|
|
93
101
|
"citty": "^0.2.2",
|
|
94
102
|
"dotenv": "^17.4.2",
|
|
103
|
+
"semver": "^7.6.0",
|
|
95
104
|
"yaml": "^2.8.4",
|
|
96
105
|
"zod": "^3.23.0",
|
|
97
106
|
"zod-to-json-schema": "^3.23.0"
|
package/dist/commands/add-cli.js
DELETED
|
@@ -1,279 +0,0 @@
|
|
|
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
|
-
import fs from "node:fs";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import * as p from "@clack/prompts";
|
|
7
|
-
import { defineCommand } from "citty";
|
|
8
|
-
import { output, runWithJsonErrors } from "../cli/shared";
|
|
9
|
-
import { UsageError } from "../core/errors";
|
|
10
|
-
import { appendEvent } from "../core/events";
|
|
11
|
-
import { warn } from "../core/warn";
|
|
12
|
-
import { getHyphenatedBoolean } from "../output/context";
|
|
13
|
-
import { akmRemove } from "./installed-stashes";
|
|
14
|
-
import { akmAdd } from "./source-add";
|
|
15
|
-
import { addStash } from "./source-manage";
|
|
16
|
-
// ── Shared website-options helper (also used by wikiRegisterCommand) ──────────
|
|
17
|
-
export function buildWebsiteOptions(args) {
|
|
18
|
-
const websiteOptions = {};
|
|
19
|
-
if (typeof args["max-pages"] === "string" && args["max-pages"].length > 0)
|
|
20
|
-
websiteOptions.maxPages = args["max-pages"];
|
|
21
|
-
if (typeof args["max-depth"] === "string" && args["max-depth"].length > 0)
|
|
22
|
-
websiteOptions.maxDepth = args["max-depth"];
|
|
23
|
-
return websiteOptions;
|
|
24
|
-
}
|
|
25
|
-
// ── HTTP safety check ─────────────────────────────────────────────────────────
|
|
26
|
-
export function shouldWarnOnPlainHttp(ref) {
|
|
27
|
-
if (!ref.startsWith("http://"))
|
|
28
|
-
return false;
|
|
29
|
-
try {
|
|
30
|
-
const hostname = new URL(ref).hostname.toLowerCase();
|
|
31
|
-
return (hostname !== "localhost" &&
|
|
32
|
-
hostname !== "127.0.0.1" &&
|
|
33
|
-
hostname !== "0.0.0.0" &&
|
|
34
|
-
hostname !== "::1" &&
|
|
35
|
-
hostname !== "[::1]" &&
|
|
36
|
-
!hostname.endsWith(".localhost"));
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
// ── Command definition ────────────────────────────────────────────────────────
|
|
43
|
-
export const addCommand = defineCommand({
|
|
44
|
-
meta: {
|
|
45
|
-
name: "add",
|
|
46
|
-
description: "Add a source (local directory, website, npm package, GitHub repo, git URL, or remote provider)",
|
|
47
|
-
},
|
|
48
|
-
args: {
|
|
49
|
-
ref: {
|
|
50
|
-
type: "positional",
|
|
51
|
-
description: "Path, URL, or registry ref (website URL, npm package, owner/repo, git URL, or local directory)",
|
|
52
|
-
required: true,
|
|
53
|
-
},
|
|
54
|
-
provider: { type: "string", description: "Provider type (e.g. website, npm). Required for URL sources." },
|
|
55
|
-
options: { type: "string", description: 'Provider options as JSON (e.g. \'{"apiKey":"key"}\').' },
|
|
56
|
-
name: { type: "string", description: "Human-friendly name for the source" },
|
|
57
|
-
writable: {
|
|
58
|
-
type: "boolean",
|
|
59
|
-
description: "Mark a git stash as writable so changes can be pushed back",
|
|
60
|
-
default: false,
|
|
61
|
-
},
|
|
62
|
-
type: {
|
|
63
|
-
type: "string",
|
|
64
|
-
description: "Override asset type for all files in this stash (currently supports: wiki)",
|
|
65
|
-
},
|
|
66
|
-
"max-pages": { type: "string", description: "Maximum pages to crawl for website sources (default: 50)" },
|
|
67
|
-
"max-depth": { type: "string", description: "Maximum crawl depth for website sources (default: 3)" },
|
|
68
|
-
"allow-insecure": {
|
|
69
|
-
type: "boolean",
|
|
70
|
-
description: "Allow a plain HTTP source URL and skip confirmation for dangerous vault keys (e.g. LD_PRELOAD, PATH). Use only after explicitly reviewing the stash.",
|
|
71
|
-
default: false,
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
async run({ args }) {
|
|
75
|
-
await runWithJsonErrors(async () => {
|
|
76
|
-
const ref = args.ref.trim();
|
|
77
|
-
const allowInsecure = getHyphenatedBoolean(args, "allow-insecure");
|
|
78
|
-
const allowDangerousKeys = allowInsecure;
|
|
79
|
-
// URL with --provider → stash source (remote or git provider)
|
|
80
|
-
if (args.provider) {
|
|
81
|
-
if (shouldWarnOnPlainHttp(ref)) {
|
|
82
|
-
if (!allowInsecure) {
|
|
83
|
-
throw new UsageError("Source URL uses plain HTTP (not HTTPS). An on-path attacker could substitute a malicious payload. " +
|
|
84
|
-
"Use https:// or pass --allow-insecure if you have explicitly accepted the risk.", "INVALID_FLAG_VALUE", "Re-run with `--allow-insecure` only after confirming the URL is trusted.");
|
|
85
|
-
}
|
|
86
|
-
warn("Warning: source URL uses plain HTTP (not HTTPS). --allow-insecure was set; an on-path attacker could substitute a malicious payload.");
|
|
87
|
-
}
|
|
88
|
-
let parsedOptions;
|
|
89
|
-
if (args.options) {
|
|
90
|
-
try {
|
|
91
|
-
const parsed = JSON.parse(args.options);
|
|
92
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
93
|
-
throw new UsageError("--options must be a JSON object");
|
|
94
|
-
}
|
|
95
|
-
parsedOptions = parsed;
|
|
96
|
-
}
|
|
97
|
-
catch (err) {
|
|
98
|
-
if (err instanceof UsageError)
|
|
99
|
-
throw err;
|
|
100
|
-
throw new UsageError("--options must be valid JSON");
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
const result = addStash({
|
|
104
|
-
target: ref,
|
|
105
|
-
name: args.name,
|
|
106
|
-
providerType: args.provider,
|
|
107
|
-
options: parsedOptions,
|
|
108
|
-
writable: args.writable,
|
|
109
|
-
});
|
|
110
|
-
appendEvent({
|
|
111
|
-
eventType: "add",
|
|
112
|
-
metadata: { target: ref, provider: args.provider, name: args.name ?? null, writable: args.writable === true },
|
|
113
|
-
});
|
|
114
|
-
output("add", result);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
if (shouldWarnOnPlainHttp(ref)) {
|
|
118
|
-
if (!allowInsecure) {
|
|
119
|
-
throw new UsageError("Source URL uses plain HTTP (not HTTPS). An on-path attacker could substitute a malicious payload. " +
|
|
120
|
-
"Use https:// or pass --allow-insecure if you have explicitly accepted the risk.", "INVALID_FLAG_VALUE", "Re-run with `--allow-insecure` only after confirming the URL is trusted.");
|
|
121
|
-
}
|
|
122
|
-
warn("Warning: source URL uses plain HTTP (not HTTPS). --allow-insecure was set; an on-path attacker could substitute a malicious payload.");
|
|
123
|
-
}
|
|
124
|
-
const websiteOptions = buildWebsiteOptions(args);
|
|
125
|
-
if (args.type === "wiki") {
|
|
126
|
-
const { registerWikiSource } = await import("./source-add");
|
|
127
|
-
const result = await registerWikiSource({
|
|
128
|
-
ref,
|
|
129
|
-
name: args.name,
|
|
130
|
-
options: Object.keys(websiteOptions).length > 0 ? websiteOptions : undefined,
|
|
131
|
-
writable: args.writable,
|
|
132
|
-
});
|
|
133
|
-
appendEvent({
|
|
134
|
-
eventType: "add",
|
|
135
|
-
metadata: { target: ref, type: "wiki", name: args.name ?? null, writable: args.writable === true },
|
|
136
|
-
});
|
|
137
|
-
output("add", result);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const result = await akmAdd({
|
|
141
|
-
ref,
|
|
142
|
-
name: args.name,
|
|
143
|
-
overrideType: args.type,
|
|
144
|
-
options: Object.keys(websiteOptions).length > 0 ? websiteOptions : undefined,
|
|
145
|
-
writable: args.writable,
|
|
146
|
-
});
|
|
147
|
-
appendEvent({
|
|
148
|
-
eventType: "add",
|
|
149
|
-
metadata: {
|
|
150
|
-
target: ref,
|
|
151
|
-
name: args.name ?? null,
|
|
152
|
-
overrideType: args.type ?? null,
|
|
153
|
-
writable: args.writable === true,
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
// ── Post-install vault key audit ────────────────────────────────────────
|
|
157
|
-
// Resolve the stash root from the install result and scan any vault files
|
|
158
|
-
// for dangerous env var keys. When findings are present the install is
|
|
159
|
-
// gated: TTY → interactive confirmation prompt; non-TTY without
|
|
160
|
-
// --allow-insecure → hard failure (exit 1). Pass
|
|
161
|
-
// --allow-insecure to skip the prompt non-interactively.
|
|
162
|
-
try {
|
|
163
|
-
const installedStashRoot = result.installed?.stashRoot ??
|
|
164
|
-
(result.sourceAdded && "stashRoot" in result.sourceAdded ? result.sourceAdded.stashRoot : undefined);
|
|
165
|
-
if (installedStashRoot) {
|
|
166
|
-
const { checkVaultForDangerousKeys } = await import("./lint/env-key-rules.js");
|
|
167
|
-
// Collect all dangerous-key findings across every env file (env/, and
|
|
168
|
-
// the deprecated vaults/) in the freshly-installed stash.
|
|
169
|
-
const allFindings = [];
|
|
170
|
-
for (const [subdir, prefix] of [
|
|
171
|
-
["env", "env"],
|
|
172
|
-
["vaults", "vault"],
|
|
173
|
-
]) {
|
|
174
|
-
const dir = path.join(installedStashRoot, subdir);
|
|
175
|
-
if (!fs.existsSync(dir))
|
|
176
|
-
continue;
|
|
177
|
-
const envFiles = fs.readdirSync(dir).filter((f) => f.endsWith(".env"));
|
|
178
|
-
for (const envFile of envFiles) {
|
|
179
|
-
const envPath = path.join(dir, envFile);
|
|
180
|
-
const baseName = path.basename(envFile, ".env");
|
|
181
|
-
const vaultRef = baseName === "" ? `${prefix}:default` : `${prefix}:${baseName}`;
|
|
182
|
-
const relPath = path.join(subdir, envFile);
|
|
183
|
-
const findings = checkVaultForDangerousKeys(envPath, relPath, vaultRef);
|
|
184
|
-
for (const finding of findings) {
|
|
185
|
-
// Extract the key name from the detail string for the summary line.
|
|
186
|
-
const keyMatch = finding.detail.match(/Env key `([^`]+)`/);
|
|
187
|
-
const keyName = keyMatch ? keyMatch[1] : finding.file;
|
|
188
|
-
allFindings.push({ vaultRef, keyName, relPath });
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
if (allFindings.length > 0) {
|
|
193
|
-
if (allowDangerousKeys) {
|
|
194
|
-
// Operator has explicitly accepted the risk — warn and continue.
|
|
195
|
-
for (const f of allFindings) {
|
|
196
|
-
warn(`[dangerous-vault-key] ${f.relPath}: key \`${f.keyName}\` in ${f.vaultRef} can hijack process execution via \`akm vault run\`. Proceeding because --allow-insecure was set.`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
else if (process.stdin.isTTY) {
|
|
200
|
-
// Interactive path: show findings and ask the user to confirm.
|
|
201
|
-
// Guard on stdin (not stdout) because p.confirm() reads from stdin;
|
|
202
|
-
// stdout may be a TTY while stdin is piped, which would cause a hang.
|
|
203
|
-
const stashLabel = ref;
|
|
204
|
-
const groupedByVault = new Map();
|
|
205
|
-
for (const f of allFindings) {
|
|
206
|
-
const existing = groupedByVault.get(f.vaultRef) ?? [];
|
|
207
|
-
existing.push(f.keyName);
|
|
208
|
-
groupedByVault.set(f.vaultRef, existing);
|
|
209
|
-
}
|
|
210
|
-
for (const [vaultRef, keys] of groupedByVault) {
|
|
211
|
-
warn(`[warn] Vault "${vaultRef}" in stash "${stashLabel}" contains potentially dangerous keys:`);
|
|
212
|
-
for (const key of keys) {
|
|
213
|
-
warn(` - ${key}: can hijack process execution via \`akm vault run\``);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
const confirmed = await p.confirm({
|
|
217
|
-
message: "Install anyway?",
|
|
218
|
-
initialValue: false,
|
|
219
|
-
});
|
|
220
|
-
if (p.isCancel(confirmed) || confirmed !== true) {
|
|
221
|
-
// Roll back the install before aborting.
|
|
222
|
-
// Use the canonical installed id (most reliably resolved by akmRemove) rather
|
|
223
|
-
// than the raw user-supplied ref which may not match after URL normalisation.
|
|
224
|
-
const rollbackTarget = result.installed?.id ?? result.sourceAdded?.stashRoot ?? ref;
|
|
225
|
-
let rollbackWarning;
|
|
226
|
-
try {
|
|
227
|
-
await akmRemove({ target: rollbackTarget });
|
|
228
|
-
}
|
|
229
|
-
catch (_rollbackErr) {
|
|
230
|
-
rollbackWarning =
|
|
231
|
-
`Rollback failed — stash may still be installed at ${installedStashRoot}. ` +
|
|
232
|
-
`Remove it manually with: akm remove ${rollbackTarget}`;
|
|
233
|
-
}
|
|
234
|
-
console.error(JSON.stringify({
|
|
235
|
-
ok: false,
|
|
236
|
-
error: "Install aborted: stash contains dangerous vault keys. Remove the keys or re-run with --allow-insecure to bypass.",
|
|
237
|
-
code: "DANGEROUS_VAULT_KEY",
|
|
238
|
-
...(rollbackWarning ? { rollbackWarning } : {}),
|
|
239
|
-
}, null, 2));
|
|
240
|
-
process.exit(1);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
// Non-interactive path without bypass flag: fail hard.
|
|
245
|
-
// Roll back the install before exiting.
|
|
246
|
-
// Use the canonical installed id (most reliably resolved by akmRemove) rather
|
|
247
|
-
// than the raw user-supplied ref which may not match after URL normalisation.
|
|
248
|
-
const rollbackTarget = result.installed?.id ?? result.sourceAdded?.stashRoot ?? ref;
|
|
249
|
-
let rollbackWarning;
|
|
250
|
-
try {
|
|
251
|
-
await akmRemove({ target: rollbackTarget });
|
|
252
|
-
}
|
|
253
|
-
catch (_rollbackErr) {
|
|
254
|
-
rollbackWarning =
|
|
255
|
-
`Rollback failed — stash may still be installed at ${installedStashRoot}. ` +
|
|
256
|
-
`Remove it manually with: akm remove ${rollbackTarget}`;
|
|
257
|
-
}
|
|
258
|
-
const keyList = allFindings.map((f) => ` - ${f.keyName} (${f.vaultRef})`).join("\n");
|
|
259
|
-
console.error(JSON.stringify({
|
|
260
|
-
ok: false,
|
|
261
|
-
error: `Install blocked: stash "${ref}" contains dangerous vault keys that can hijack process execution via \`akm vault run\`:\n${keyList}\nRe-run with --allow-insecure to bypass this check after reviewing the vault.`,
|
|
262
|
-
code: "DANGEROUS_VAULT_KEY",
|
|
263
|
-
...(rollbackWarning ? { rollbackWarning } : {}),
|
|
264
|
-
}, null, 2));
|
|
265
|
-
process.exit(1);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
catch (auditErr) {
|
|
271
|
-
// Only swallow errors that are NOT our intentional process.exit calls.
|
|
272
|
-
if (auditErr instanceof Error && auditErr.message === "process.exit called")
|
|
273
|
-
throw auditErr;
|
|
274
|
-
// Vault key audit is best-effort; never fail the install on unexpected audit errors.
|
|
275
|
-
}
|
|
276
|
-
output("add", result);
|
|
277
|
-
});
|
|
278
|
-
},
|
|
279
|
-
});
|
package/dist/commands/env.js
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
-
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
-
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
/**
|
|
5
|
-
* Environment asset type (`env`) — whole `.env` file storage.
|
|
6
|
-
*
|
|
7
|
-
* An `env` asset holds a GROUP of related CONFIGURATION for an app or service
|
|
8
|
-
* (URLs, feature flags, and any credentials it needs) in a single `.env` file,
|
|
9
|
-
* sourced/injected wholesale. Values may or may not be sensitive — akm protects
|
|
10
|
-
* them all the same. For a single sensitive value used on its own for
|
|
11
|
-
* authentication (a token, key, or cert), use the `secret` type instead.
|
|
12
|
-
*
|
|
13
|
-
* Unlike the deprecated `vault` type it replaces, akm does NOT manage individual
|
|
14
|
-
* KEY=value entries (no `set`/`unset`/quoting): you edit the `.env` file with
|
|
15
|
-
* your own editor, and akm loads it. The simplification removes the
|
|
16
|
-
* hand-rolled quoting/escaping surface; the safety guarantee moves to the READ
|
|
17
|
-
* path instead (see `buildShellExportScript` + `akm env export`).
|
|
18
|
-
*
|
|
19
|
-
* Invariant: env values must never be written to stdout, returned through the
|
|
20
|
-
* indexer, the `akm show` renderer, or any structured output channel. Key
|
|
21
|
-
* NAMES and start-of-line comments ARE surfaced by design (discoverability) —
|
|
22
|
-
* only values are secret. The supported value-load paths are:
|
|
23
|
-
*
|
|
24
|
-
* - `akm env run <ref> -- <command>` — values injected into the child
|
|
25
|
-
* process env (never via a shell), see `injectIntoEnv` / `loadEnv`. This is
|
|
26
|
-
* the primary path and the only one safe for AI agents (no values ever
|
|
27
|
-
* reach stdout). For an interactive shell, `akm env run <ref> -- $SHELL`.
|
|
28
|
-
* - `akm env export <ref> --out <file>` — write parse-then-reserialized safe
|
|
29
|
-
* `export KEY='value'` lines to a file (mode 0600) for `source`-ing. Values
|
|
30
|
-
* are re-emitted single-quoted so a raw `.env` containing `X=$(cmd)` cannot
|
|
31
|
-
* execute on load. `export` never prints values to stdout (would leak into
|
|
32
|
-
* an agent's context); `path` prints only the file path.
|
|
33
|
-
*
|
|
34
|
-
* Value parsing is delegated to the `dotenv` package — we deliberately do not
|
|
35
|
-
* implement our own quoting/escaping rules for security-sensitive content.
|
|
36
|
-
*/
|
|
37
|
-
import fs from "node:fs";
|
|
38
|
-
import path from "node:path";
|
|
39
|
-
import dotenv from "dotenv";
|
|
40
|
-
import { writeFileAtomic } from "../core/common";
|
|
41
|
-
/** Matches a KEY=value assignment line, capturing only the key. */
|
|
42
|
-
const ASSIGN_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
|
|
43
|
-
/** Scan lines and return KEY names in file order, without duplicates. */
|
|
44
|
-
function scanKeys(text) {
|
|
45
|
-
const keys = [];
|
|
46
|
-
const seen = new Set();
|
|
47
|
-
for (const line of text.split(/\r?\n/)) {
|
|
48
|
-
const m = line.match(ASSIGN_RE);
|
|
49
|
-
if (!m)
|
|
50
|
-
continue;
|
|
51
|
-
const key = m[1];
|
|
52
|
-
if (seen.has(key))
|
|
53
|
-
continue;
|
|
54
|
-
seen.add(key);
|
|
55
|
-
keys.push(key);
|
|
56
|
-
}
|
|
57
|
-
return keys;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Scan lines and return start-of-line `#` comments (with the leading `#` and
|
|
61
|
-
* any leading whitespace stripped). Inline/trailing `#` after an assignment is
|
|
62
|
-
* never extracted.
|
|
63
|
-
*/
|
|
64
|
-
function scanComments(text) {
|
|
65
|
-
const comments = [];
|
|
66
|
-
for (const line of text.split(/\r?\n/)) {
|
|
67
|
-
const trimmed = line.trimStart();
|
|
68
|
-
if (trimmed.startsWith("#")) {
|
|
69
|
-
comments.push(trimmed.slice(1).trimStart());
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return comments;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Read and return ONLY non-secret metadata (keys + start-of-line comments).
|
|
76
|
-
*
|
|
77
|
-
* The function reads the whole file into memory (same as any dotenv parser)
|
|
78
|
-
* but deliberately does not parse values — the LHS-only regex scanners above
|
|
79
|
-
* ensure no value content is retained or returned. The guarantee is that
|
|
80
|
-
* values never leave this function.
|
|
81
|
-
*/
|
|
82
|
-
export function listKeys(envPath) {
|
|
83
|
-
if (!fs.existsSync(envPath))
|
|
84
|
-
return { keys: [], comments: [] };
|
|
85
|
-
const text = fs.readFileSync(envPath, "utf8");
|
|
86
|
-
return { keys: scanKeys(text), comments: scanComments(text) };
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Return structured `entries` pairing each key with the nearest preceding
|
|
90
|
-
* comment line (if any). This is an easier-to-consume shape than the parallel
|
|
91
|
-
* `keys[]` + `comments[]` of `listKeys` (QA #35).
|
|
92
|
-
*
|
|
93
|
-
* Values are never included — the same privacy guarantee as `listKeys`.
|
|
94
|
-
*/
|
|
95
|
-
export function listEntries(envPath) {
|
|
96
|
-
if (!fs.existsSync(envPath))
|
|
97
|
-
return [];
|
|
98
|
-
const text = fs.readFileSync(envPath, "utf8");
|
|
99
|
-
const lines = text.split(/\r?\n/);
|
|
100
|
-
const seen = new Set();
|
|
101
|
-
const entries = [];
|
|
102
|
-
let pendingComment;
|
|
103
|
-
for (const line of lines) {
|
|
104
|
-
const trimmed = line.trimStart();
|
|
105
|
-
if (trimmed.startsWith("#")) {
|
|
106
|
-
// Capture the most recent comment before a key
|
|
107
|
-
pendingComment = trimmed.slice(1).trimStart() || undefined;
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
const m = line.match(ASSIGN_RE);
|
|
111
|
-
if (m) {
|
|
112
|
-
const key = m[1];
|
|
113
|
-
if (!seen.has(key)) {
|
|
114
|
-
seen.add(key);
|
|
115
|
-
const entry = { key };
|
|
116
|
-
if (pendingComment)
|
|
117
|
-
entry.comment = pendingComment;
|
|
118
|
-
entries.push(entry);
|
|
119
|
-
}
|
|
120
|
-
pendingComment = undefined;
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
// Any non-comment, non-assignment line (including blank lines)
|
|
124
|
-
// breaks "nearest preceding comment line" association.
|
|
125
|
-
pendingComment = undefined;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return entries;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Read all KEY=value pairs from an env file. Intended for programmatic callers
|
|
132
|
-
* that need to inject values into a process environment. Callers MUST NOT write
|
|
133
|
-
* the returned values to stdout or any logged output.
|
|
134
|
-
*
|
|
135
|
-
* Value parsing (quoting, escapes, multi-line, etc.) is delegated to dotenv.
|
|
136
|
-
*/
|
|
137
|
-
export function loadEnv(envPath) {
|
|
138
|
-
if (!fs.existsSync(envPath))
|
|
139
|
-
return {};
|
|
140
|
-
const buf = fs.readFileSync(envPath);
|
|
141
|
-
return dotenv.parse(buf);
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Load an env file and assign its values into `target` (defaults to
|
|
145
|
-
* `process.env`). Returns the list of keys that were set so the caller can
|
|
146
|
-
* log/observe without touching values.
|
|
147
|
-
*
|
|
148
|
-
* Existing keys in `target` are overwritten — callers who want to preserve
|
|
149
|
-
* pre-existing environment variables should filter before calling.
|
|
150
|
-
*/
|
|
151
|
-
export function injectIntoEnv(envPath, target = process.env) {
|
|
152
|
-
const env = loadEnv(envPath);
|
|
153
|
-
for (const [key, value] of Object.entries(env)) {
|
|
154
|
-
target[key] = value;
|
|
155
|
-
}
|
|
156
|
-
return Object.keys(env);
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Serialise an env file's values as a POSIX shell script of `export KEY='value'`
|
|
160
|
-
* lines, with single-quote escaping (`'\''`). Every line is an assignment of a
|
|
161
|
-
* literal string — there is no expansion, command substitution, or
|
|
162
|
-
* non-assignment content, so `eval`-ing the output is safe regardless of what
|
|
163
|
-
* the source file contains.
|
|
164
|
-
*
|
|
165
|
-
* This is the trust boundary for shell loading: a raw `.env` may contain
|
|
166
|
-
* `X=$(rm -rf ~)`, which would execute if `source`d directly, but dotenv parses
|
|
167
|
-
* it to the literal string `$(rm -rf ~)` and we re-emit it single-quoted. This
|
|
168
|
-
* backs `akm env export <ref> --out <file>` (file-only; never printed to stdout).
|
|
169
|
-
*/
|
|
170
|
-
export function buildShellExportScript(envPath) {
|
|
171
|
-
const env = loadEnv(envPath);
|
|
172
|
-
const lines = [];
|
|
173
|
-
for (const [key, value] of Object.entries(env)) {
|
|
174
|
-
// Defence in depth: dotenv already validates key shape, but reject any
|
|
175
|
-
// key we wouldn't be able to export safely.
|
|
176
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
177
|
-
continue;
|
|
178
|
-
const escaped = value.replace(/'/g, "'\\''");
|
|
179
|
-
lines.push(`export ${key}='${escaped}'`);
|
|
180
|
-
}
|
|
181
|
-
return lines.length > 0 ? `${lines.join("\n")}\n` : "";
|
|
182
|
-
}
|
|
183
|
-
/** Create an empty env file (does nothing if it already exists). */
|
|
184
|
-
export function createEnv(envPath) {
|
|
185
|
-
ensureParentDir(envPath);
|
|
186
|
-
if (fs.existsSync(envPath))
|
|
187
|
-
return;
|
|
188
|
-
writeFileAtomic(envPath, "", 0o600);
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Write (create or overwrite) an env file with the given text content,
|
|
192
|
-
* atomically at mode 0600. Used to ingest an existing `.env` file
|
|
193
|
-
* (`env create --from-file` / `--from-stdin`).
|
|
194
|
-
*/
|
|
195
|
-
export function writeEnv(envPath, content) {
|
|
196
|
-
ensureParentDir(envPath);
|
|
197
|
-
writeFileAtomic(envPath, content, 0o600);
|
|
198
|
-
}
|
|
199
|
-
/** Remove an env file (and its `.sensitive` marker, if present). Returns true if it existed. */
|
|
200
|
-
export function removeEnv(envPath) {
|
|
201
|
-
if (!fs.existsSync(envPath))
|
|
202
|
-
return false;
|
|
203
|
-
fs.rmSync(envPath);
|
|
204
|
-
const marker = `${envPath}.sensitive`;
|
|
205
|
-
if (fs.existsSync(marker))
|
|
206
|
-
fs.rmSync(marker);
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
function ensureParentDir(filePath) {
|
|
210
|
-
const dir = path.dirname(filePath);
|
|
211
|
-
if (!fs.existsSync(dir))
|
|
212
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
213
|
-
}
|