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