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
|
@@ -0,0 +1,529 @@
|
|
|
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 env` command family. Extracted verbatim from src/cli.ts (WS6) so the God
|
|
6
|
+
* Module shrinks; the `main.subCommands.env` key and every subcommand's
|
|
7
|
+
* args/output shape are byte-identical. The ref-resolution helpers
|
|
8
|
+
* (parseEnvRef / findEnvSource / makeEnvRef / resolveEnvPath + the env-path
|
|
9
|
+
* traversal guard) live in src/core/env-secret-ref.ts so env + secret share one
|
|
10
|
+
* copy.
|
|
11
|
+
*
|
|
12
|
+
* `akm env` manages whole `.env` files under each stash's env/ directory.
|
|
13
|
+
* Values are NEVER written to stdout or structured output — only key NAMES and
|
|
14
|
+
* start-of-line comments are surfaced. akm does not manage individual entries;
|
|
15
|
+
* you edit the `.env` file yourself and akm loads it. Replaced the deprecated
|
|
16
|
+
* `vault` type (removed in 0.9.0).
|
|
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 { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "../../core/asset/asset-create.js";
|
|
25
|
+
import { deriveCanonicalAssetName, resolveAssetPathFromName } from "../../core/asset/asset-spec.js";
|
|
26
|
+
import { isWithin, writeFileAtomic } from "../../core/common.js";
|
|
27
|
+
import { loadConfig } from "../../core/config/config.js";
|
|
28
|
+
import { findEnvSource, makeEnvRef, parseEnvRef, resolveEnvPath } from "../../core/env-secret-ref.js";
|
|
29
|
+
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
30
|
+
import { appendEvent } from "../../core/events.js";
|
|
31
|
+
import { isQuiet } from "../../core/warn.js";
|
|
32
|
+
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
33
|
+
import { getHyphenatedArg, parseFlagValue } from "../../output/context.js";
|
|
34
|
+
import { readStdin } from "../../runtime.js";
|
|
35
|
+
/**
|
|
36
|
+
* Walk each stash's env files and return one entry per `.env` file, using the
|
|
37
|
+
* env asset spec's canonical-name logic (e.g. `env/team/prod.env` →
|
|
38
|
+
* `env:team/prod`, `env/team/.env` → `env:team/default`).
|
|
39
|
+
*/
|
|
40
|
+
function listEnvsRecursive(listKeysFn) {
|
|
41
|
+
const result = [];
|
|
42
|
+
for (const source of resolveSourceEntries(undefined, loadConfig())) {
|
|
43
|
+
const root = path.join(source.path, "env");
|
|
44
|
+
if (!fs.existsSync(root))
|
|
45
|
+
continue;
|
|
46
|
+
const walk = (dir) => {
|
|
47
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
48
|
+
const full = path.join(dir, entry.name);
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
walk(full);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!entry.isFile())
|
|
54
|
+
continue;
|
|
55
|
+
if (entry.name !== ".env" && !entry.name.endsWith(".env"))
|
|
56
|
+
continue;
|
|
57
|
+
const canonical = deriveCanonicalAssetName("env", root, full);
|
|
58
|
+
if (!canonical)
|
|
59
|
+
continue;
|
|
60
|
+
// Skip sensitive envs: a sibling .sensitive marker file suppresses listing.
|
|
61
|
+
const markerPath = full.replace(/\.env$/, ".sensitive");
|
|
62
|
+
if (fs.existsSync(markerPath))
|
|
63
|
+
continue;
|
|
64
|
+
const { keys } = listKeysFn(full);
|
|
65
|
+
result.push({ ref: makeEnvRef(canonical, source), path: full, keys });
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
walk(root);
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
const envListCommand = defineCommand({
|
|
73
|
+
meta: { name: "list", description: "List all env files across all stashes with their key names (no values)" },
|
|
74
|
+
run() {
|
|
75
|
+
return runWithJsonErrors(async () => {
|
|
76
|
+
const { listKeys } = await import("./env.js");
|
|
77
|
+
output("env-list", { envs: listEnvsRecursive(listKeys) });
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const envCreateCommand = defineCommand({
|
|
82
|
+
meta: {
|
|
83
|
+
name: "create",
|
|
84
|
+
description: "Create an env file (empty by default; seed an existing `.env` with --from-file or --from-stdin). No-op if it already exists and no source is given.",
|
|
85
|
+
},
|
|
86
|
+
args: {
|
|
87
|
+
name: {
|
|
88
|
+
type: "positional",
|
|
89
|
+
description: "Env name (flat, e.g. prod → prod.env; use --path for a subdirectory)",
|
|
90
|
+
required: true,
|
|
91
|
+
},
|
|
92
|
+
path: {
|
|
93
|
+
type: "string",
|
|
94
|
+
description: "Relative subdirectory under env/ to place the env file in (e.g. 'staging'). The filename comes from the name.",
|
|
95
|
+
},
|
|
96
|
+
"from-file": { type: "string", description: "Seed the env file from an existing .env at this path" },
|
|
97
|
+
"from-stdin": { type: "boolean", description: "Seed the env file from stdin", default: false },
|
|
98
|
+
sensitive: {
|
|
99
|
+
type: "boolean",
|
|
100
|
+
description: "Exclude this env file from env list output and the search index",
|
|
101
|
+
default: false,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
run({ args }) {
|
|
105
|
+
return runWithJsonErrors(async () => {
|
|
106
|
+
const { createEnv, writeEnv } = await import("./env.js");
|
|
107
|
+
// `create` always targets env/, never the frozen vaults/ copy.
|
|
108
|
+
const parsed = parseEnvRef(args.name);
|
|
109
|
+
// `name` is flat; subdirectory placement is `--path`'s job.
|
|
110
|
+
assertFlatAssetName(parsed.name);
|
|
111
|
+
parsed.name = combineCreatePath(normalizeCreateSubPath(getStringArg(args, "path")), parsed.name);
|
|
112
|
+
const source = findEnvSource(parsed.origin);
|
|
113
|
+
const envRoot = path.join(source.path, "env");
|
|
114
|
+
const absPath = resolveAssetPathFromName("env", envRoot, parsed.name);
|
|
115
|
+
if (!isWithin(absPath, envRoot)) {
|
|
116
|
+
throw new UsageError(`Env name "${parsed.name}" escapes the env directory.`);
|
|
117
|
+
}
|
|
118
|
+
const fromFile = getHyphenatedArg(args, "from-file");
|
|
119
|
+
const fromStdin = getHyphenatedArg(args, "from-stdin") === true;
|
|
120
|
+
if (fromFile !== undefined && fromStdin) {
|
|
121
|
+
throw new UsageError("Pass only one of --from-file or --from-stdin.", "INVALID_FLAG_VALUE");
|
|
122
|
+
}
|
|
123
|
+
if (fromFile !== undefined || fromStdin) {
|
|
124
|
+
// Ingest path: never silently clobber an existing env file.
|
|
125
|
+
if (fs.existsSync(absPath)) {
|
|
126
|
+
throw new UsageError(`Env "${makeEnvRef(parsed.name, source)}" already exists. Remove it first (\`akm env remove\`) or edit the file directly.`, "RESOURCE_ALREADY_EXISTS");
|
|
127
|
+
}
|
|
128
|
+
let content;
|
|
129
|
+
if (fromFile !== undefined) {
|
|
130
|
+
if (!fs.existsSync(fromFile)) {
|
|
131
|
+
throw new NotFoundError(`Source file not found: ${fromFile}`, "FILE_NOT_FOUND");
|
|
132
|
+
}
|
|
133
|
+
content = fs.readFileSync(fromFile, "utf8");
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const MAX_ENV_BYTES = 1024 * 1024; // 1 MB
|
|
137
|
+
const buf = await readStdin(MAX_ENV_BYTES, () => new UsageError("Env file exceeds 1 MB limit.", "INVALID_FLAG_VALUE"));
|
|
138
|
+
content = buf.toString("utf8");
|
|
139
|
+
}
|
|
140
|
+
writeEnv(absPath, content);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
createEnv(absPath);
|
|
144
|
+
}
|
|
145
|
+
if (args.sensitive) {
|
|
146
|
+
const markerPath = absPath.replace(/\.env$/, ".sensitive");
|
|
147
|
+
if (!fs.existsSync(markerPath)) {
|
|
148
|
+
fs.writeFileSync(markerPath, "", { mode: 0o600 });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
output("env-create", { ref: makeEnvRef(parsed.name, source) });
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
const envPathCommand = defineCommand({
|
|
156
|
+
meta: {
|
|
157
|
+
name: "path",
|
|
158
|
+
description: "Print the absolute env file path (Docker `_FILE` convention / `--env-file`). To inject values, use `akm env run <ref> -- <cmd>` — do NOT `source` the raw file.",
|
|
159
|
+
},
|
|
160
|
+
args: {
|
|
161
|
+
ref: { type: "positional", description: "Env ref", required: true },
|
|
162
|
+
quiet: { type: "boolean", alias: "q", description: "Suppress the unsafe-source warning", default: false },
|
|
163
|
+
},
|
|
164
|
+
run({ args }) {
|
|
165
|
+
return runWithJsonErrors(async () => {
|
|
166
|
+
const { name, absPath, source } = resolveEnvPath(args.ref);
|
|
167
|
+
if (!fs.existsSync(absPath)) {
|
|
168
|
+
throw new NotFoundError(`Env not found: ${makeEnvRef(name, source)}`);
|
|
169
|
+
}
|
|
170
|
+
// The raw `.env` may contain `X=$(cmd)`, which executes if `source`d.
|
|
171
|
+
// Warning goes to stderr (never contaminates the path on stdout) and is
|
|
172
|
+
// suppressed with --quiet for the legitimate `_FILE` / `--env-file` use.
|
|
173
|
+
if (args.quiet !== true) {
|
|
174
|
+
process.stderr.write(`warning: this is the raw file path. Do NOT \`source\` it (shell substitutions in the file would execute).\n` +
|
|
175
|
+
` To inject values run: akm env run ${args.ref} -- <command>\n`);
|
|
176
|
+
}
|
|
177
|
+
process.stdout.write(`${absPath}\n`);
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
const envExportCommand = defineCommand({
|
|
182
|
+
meta: {
|
|
183
|
+
name: "export",
|
|
184
|
+
description: "Write safe `export KEY='value'` lines to a file (mode 0600) for `source`-ing — requires --out <path>. Values are re-serialised single-quoted so a raw `.env` cannot execute on load, and are NEVER printed to stdout. To use values directly, prefer `akm env run <ref> -- <command>`.",
|
|
185
|
+
},
|
|
186
|
+
args: {
|
|
187
|
+
ref: { type: "positional", description: "Env ref", required: true },
|
|
188
|
+
out: { type: "string", alias: "o", description: "Destination file (required). Written at mode 0600." },
|
|
189
|
+
},
|
|
190
|
+
run({ args }) {
|
|
191
|
+
return runWithJsonErrors(async () => {
|
|
192
|
+
const outPath = getHyphenatedArg(args, "out");
|
|
193
|
+
if (!outPath) {
|
|
194
|
+
throw new UsageError("`akm env export` writes to a file — pass --out <path>.\n" +
|
|
195
|
+
" To use values directly, run `akm env run <ref> -- <command>` (or `-- $SHELL` for an interactive\n" +
|
|
196
|
+
" session). export never prints values to stdout, to avoid leaking them into a captured context.", "MISSING_REQUIRED_ARGUMENT");
|
|
197
|
+
}
|
|
198
|
+
const { name, absPath, source } = resolveEnvPath(args.ref);
|
|
199
|
+
if (!fs.existsSync(absPath)) {
|
|
200
|
+
throw new NotFoundError(`Env not found: ${makeEnvRef(name, source)}`);
|
|
201
|
+
}
|
|
202
|
+
const { buildShellExportScript } = await import("./env.js");
|
|
203
|
+
const resolvedOut = path.resolve(outPath);
|
|
204
|
+
writeFileAtomic(resolvedOut, buildShellExportScript(absPath), 0o600);
|
|
205
|
+
output("env-export", { ref: makeEnvRef(name, source), out: resolvedOut });
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
/**
|
|
210
|
+
* Shared implementation for `env run`. Injects an entire env file's values into
|
|
211
|
+
* the child process env — never via a shell — after scanning the injected keys
|
|
212
|
+
* for process-hijacking variables.
|
|
213
|
+
*/
|
|
214
|
+
async function runEnvInjected(target, opts) {
|
|
215
|
+
const dashIndex = process.argv.indexOf("--");
|
|
216
|
+
if (dashIndex < 0 || dashIndex === process.argv.length - 1) {
|
|
217
|
+
throw new UsageError("Missing command. Usage: akm env run <ref> -- <command>");
|
|
218
|
+
}
|
|
219
|
+
const command = process.argv.slice(dashIndex + 1);
|
|
220
|
+
const { name, absPath, source } = resolveEnvPath(target);
|
|
221
|
+
if (!fs.existsSync(absPath)) {
|
|
222
|
+
// Help users who reach for the removed single-key `ref/KEY` form.
|
|
223
|
+
const slash = target.lastIndexOf("/");
|
|
224
|
+
if (slash > 0) {
|
|
225
|
+
const maybeKey = target.slice(slash + 1);
|
|
226
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(maybeKey)) {
|
|
227
|
+
let baseExists = false;
|
|
228
|
+
try {
|
|
229
|
+
baseExists = fs.existsSync(resolveEnvPath(target.slice(0, slash)).absPath);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
baseExists = false;
|
|
233
|
+
}
|
|
234
|
+
if (baseExists) {
|
|
235
|
+
throw new UsageError(`'akm env run' injects the whole file; the single-key '<ref>/${maybeKey}' form was removed.\n` +
|
|
236
|
+
` For one value use a secret: \`akm secret run secret:${maybeKey} ${maybeKey} -- <command>\`.`, "INVALID_FLAG_VALUE");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
throw new NotFoundError(`Env not found: ${makeEnvRef(name, source)}`);
|
|
241
|
+
}
|
|
242
|
+
const { loadEnv } = await import("./env.js");
|
|
243
|
+
const allValues = loadEnv(absPath);
|
|
244
|
+
// Value-safe key filtering (--only / --except operate on key NAMES only).
|
|
245
|
+
let envValues = allValues;
|
|
246
|
+
if (opts.only && opts.except) {
|
|
247
|
+
throw new UsageError("Pass only one of --only or --except.", "INVALID_FLAG_VALUE");
|
|
248
|
+
}
|
|
249
|
+
if (opts.only) {
|
|
250
|
+
const wanted = new Set(opts.only);
|
|
251
|
+
const missing = opts.only.filter((k) => !(k in allValues));
|
|
252
|
+
if (missing.length > 0) {
|
|
253
|
+
process.stderr.write(`warning: --only key(s) not present in ${makeEnvRef(name, source)}: ${missing.join(", ")}\n`);
|
|
254
|
+
}
|
|
255
|
+
envValues = Object.fromEntries(Object.entries(allValues).filter(([k]) => wanted.has(k)));
|
|
256
|
+
}
|
|
257
|
+
else if (opts.except) {
|
|
258
|
+
const excluded = new Set(opts.except);
|
|
259
|
+
envValues = Object.fromEntries(Object.entries(allValues).filter(([k]) => !excluded.has(k)));
|
|
260
|
+
}
|
|
261
|
+
// Substitute `${secret:NAME}` tokens in values with the value of the sibling
|
|
262
|
+
// secret asset in the SAME stash. The lookup is injected so commands/env.ts
|
|
263
|
+
// keeps its narrow dependency surface; we resolve each name against this env's
|
|
264
|
+
// own `source`. A missing secret is a hard error — inject NOTHING (no partial
|
|
265
|
+
// injection). Resolved values are never logged or printed.
|
|
266
|
+
const { resolveSecretTokens } = await import("./env.js");
|
|
267
|
+
const { readValue } = await import("./secret.js");
|
|
268
|
+
const secretsRoot = path.join(source.path, "secrets");
|
|
269
|
+
const resolveSecret = (secretName) => {
|
|
270
|
+
const secretPath = resolveAssetPathFromName("secret", secretsRoot, secretName);
|
|
271
|
+
// Defense-in-depth: ensure the resolved path stays inside the secrets dir.
|
|
272
|
+
if (!isWithin(secretPath, secretsRoot)) {
|
|
273
|
+
throw new UsageError(`Secret name "${secretName}" escapes the secrets directory.`);
|
|
274
|
+
}
|
|
275
|
+
if (!fs.existsSync(secretPath))
|
|
276
|
+
return undefined;
|
|
277
|
+
// Match `secret run`: read utf8, do not trim (stay consistent with that path).
|
|
278
|
+
return readValue(secretPath).toString("utf8");
|
|
279
|
+
};
|
|
280
|
+
const { values: substituted, missing } = resolveSecretTokens(envValues, resolveSecret);
|
|
281
|
+
if (missing.length > 0) {
|
|
282
|
+
const envRef = makeEnvRef(name, source);
|
|
283
|
+
throw new NotFoundError(`Env "${envRef}" references secret(s) not found in its stash: ${missing.map((n) => `secret:${n}`).join(", ")}. Nothing was injected.`, "FILE_NOT_FOUND", `Create the missing secret, e.g. \`akm secret set secret:${missing[0]}\`.`);
|
|
284
|
+
}
|
|
285
|
+
envValues = substituted;
|
|
286
|
+
const keys = Object.keys(envValues);
|
|
287
|
+
// Scan injected keys for known process-hijacking variables (LD_PRELOAD,
|
|
288
|
+
// PATH, ...). Block for third-party-sourced stashes (origin has a registryId);
|
|
289
|
+
// warn for the operator's own first-party stash, where they own the file.
|
|
290
|
+
const { isDangerousEnvKey } = await import("../lint/env-key-rules.js");
|
|
291
|
+
const dangerous = keys.filter(isDangerousEnvKey);
|
|
292
|
+
if (dangerous.length > 0) {
|
|
293
|
+
const detail = `Env "${makeEnvRef(name, source)}" injects process-hijacking variable(s): ${dangerous.join(", ")}.`;
|
|
294
|
+
if (source.registryId) {
|
|
295
|
+
throw new UsageError(`Refusing to inject env from a third-party stash. ${detail}\n` +
|
|
296
|
+
` Review the file, then copy the values into a first-party env if you trust them.`, "INVALID_FLAG_VALUE");
|
|
297
|
+
}
|
|
298
|
+
process.stderr.write(`warning: ${detail} Injecting anyway (first-party stash).\n`);
|
|
299
|
+
}
|
|
300
|
+
const mergedEnv = { ...process.env };
|
|
301
|
+
for (const [envKey, envValue] of Object.entries(envValues)) {
|
|
302
|
+
mergedEnv[envKey] = envValue;
|
|
303
|
+
}
|
|
304
|
+
// Audit trail: keys only, never values.
|
|
305
|
+
appendEvent({
|
|
306
|
+
eventType: "env_access",
|
|
307
|
+
ref: makeEnvRef(name, source),
|
|
308
|
+
metadata: { keys },
|
|
309
|
+
});
|
|
310
|
+
const result = spawnSync(command[0], command.slice(1), {
|
|
311
|
+
stdio: "inherit",
|
|
312
|
+
env: mergedEnv,
|
|
313
|
+
});
|
|
314
|
+
if (result.error) {
|
|
315
|
+
// Classify spawn failures (#483). Raw ErrnoException leaks a bare
|
|
316
|
+
// "spawn ENOENT" with no hint — wrap it so consumers get a usable
|
|
317
|
+
// code + hint in the standard JSON envelope.
|
|
318
|
+
const err = result.error;
|
|
319
|
+
if (err.code === "ENOENT") {
|
|
320
|
+
throw new NotFoundError(`Command not found: ${command[0]}`, "FILE_NOT_FOUND", `Install '${command[0]}' or add its directory to PATH before invoking 'akm env run'.`);
|
|
321
|
+
}
|
|
322
|
+
if (err.code === "EACCES") {
|
|
323
|
+
throw new ConfigError(`Command not executable: ${command[0]}`, "STASH_DIR_UNREADABLE", `Add execute permission ('chmod +x ${command[0]}') or invoke via an interpreter.`);
|
|
324
|
+
}
|
|
325
|
+
throw err;
|
|
326
|
+
}
|
|
327
|
+
process.exit(result.status ?? 0);
|
|
328
|
+
}
|
|
329
|
+
/** Parse a comma/space-separated key list flag into a trimmed, non-empty array. */
|
|
330
|
+
function parseKeyListFlag(raw) {
|
|
331
|
+
if (raw === undefined)
|
|
332
|
+
return undefined;
|
|
333
|
+
const keys = raw
|
|
334
|
+
.split(/[,\s]+/)
|
|
335
|
+
.map((k) => k.trim())
|
|
336
|
+
.filter(Boolean);
|
|
337
|
+
return keys.length > 0 ? keys : undefined;
|
|
338
|
+
}
|
|
339
|
+
const envRunCommand = defineCommand({
|
|
340
|
+
meta: {
|
|
341
|
+
name: "run",
|
|
342
|
+
description:
|
|
343
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal `${secret:NAME}` token syntax documented for users, not interpolation
|
|
344
|
+
"Run a command with the env file injected into its environment: `akm env run <ref> -- <command>`. Use `-- $SHELL` for an interactive session. Restrict which variables are injected with --only / --except. Values may embed `${secret:NAME}` tokens, replaced at run time with the sibling `secret:NAME` value from the same stash.",
|
|
345
|
+
},
|
|
346
|
+
args: {
|
|
347
|
+
target: { type: "positional", description: "Env ref", required: true },
|
|
348
|
+
only: {
|
|
349
|
+
type: "string",
|
|
350
|
+
description: "Inject ONLY these keys (comma-separated). Mutually exclusive with --except.",
|
|
351
|
+
},
|
|
352
|
+
except: { type: "string", description: "Inject all keys EXCEPT these (comma-separated)." },
|
|
353
|
+
},
|
|
354
|
+
run({ args }) {
|
|
355
|
+
return runWithJsonErrors(() => runEnvInjected(args.target, {
|
|
356
|
+
only: parseKeyListFlag(getHyphenatedArg(args, "only")),
|
|
357
|
+
except: parseKeyListFlag(getHyphenatedArg(args, "except")),
|
|
358
|
+
}));
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
const envRemoveCommand = defineCommand({
|
|
362
|
+
meta: { name: "remove", description: "Remove an env file (and its .sensitive marker, if any)" },
|
|
363
|
+
args: {
|
|
364
|
+
ref: { type: "positional", description: "Env ref", required: true },
|
|
365
|
+
yes: { type: "boolean", alias: "y", description: "Skip confirmation prompt", default: false },
|
|
366
|
+
},
|
|
367
|
+
run({ args }) {
|
|
368
|
+
return runWithJsonErrors(async () => {
|
|
369
|
+
const parsed = parseEnvRef(args.ref);
|
|
370
|
+
const source = findEnvSource(parsed.origin);
|
|
371
|
+
const envRoot = path.join(source.path, "env");
|
|
372
|
+
const absPath = resolveAssetPathFromName("env", envRoot, parsed.name);
|
|
373
|
+
if (!isWithin(absPath, envRoot)) {
|
|
374
|
+
throw new UsageError(`Env name "${parsed.name}" escapes the env directory.`);
|
|
375
|
+
}
|
|
376
|
+
const { confirmDestructive } = await import("../../cli/confirm.js");
|
|
377
|
+
const confirmed = await confirmDestructive(`Remove env "${args.ref}"? This cannot be undone.`, {
|
|
378
|
+
yes: args.yes === true,
|
|
379
|
+
});
|
|
380
|
+
if (!confirmed) {
|
|
381
|
+
process.stderr.write("Aborted.\n");
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (!fs.existsSync(absPath)) {
|
|
385
|
+
throw new NotFoundError(`Env not found: ${makeEnvRef(parsed.name, source)}`);
|
|
386
|
+
}
|
|
387
|
+
const { removeEnv } = await import("./env.js");
|
|
388
|
+
const removed = removeEnv(absPath);
|
|
389
|
+
output("env-remove", { ref: makeEnvRef(parsed.name, source), removed });
|
|
390
|
+
});
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
const envSetCommand = defineCommand({
|
|
394
|
+
meta: {
|
|
395
|
+
name: "set",
|
|
396
|
+
description: "Set (create or update) a single KEY in an env file: `akm env set <ref> <KEY>`. The value is read from stdin by default (never via argv); use --from-env <VAR> or --from-file <path>. Preserves existing comments and key order; the value is never printed. Creates the env file if it does not exist.",
|
|
397
|
+
},
|
|
398
|
+
args: {
|
|
399
|
+
ref: { type: "positional", description: "Env ref (e.g. env:prod or just prod)", required: true },
|
|
400
|
+
key: { type: "positional", description: "Key name to set (e.g. API_URL)", required: true },
|
|
401
|
+
"from-env": { type: "string", description: "Read the value from the named environment variable" },
|
|
402
|
+
"from-file": { type: "string", description: "Read the value from this file" },
|
|
403
|
+
},
|
|
404
|
+
run({ args }) {
|
|
405
|
+
return runWithJsonErrors(async () => {
|
|
406
|
+
const parsed = parseEnvRef(args.ref);
|
|
407
|
+
const source = findEnvSource(parsed.origin);
|
|
408
|
+
const envRoot = path.join(source.path, "env");
|
|
409
|
+
const absPath = resolveAssetPathFromName("env", envRoot, parsed.name);
|
|
410
|
+
if (!isWithin(absPath, envRoot)) {
|
|
411
|
+
throw new UsageError(`Env name "${parsed.name}" escapes the env directory.`);
|
|
412
|
+
}
|
|
413
|
+
const key = String(args.key);
|
|
414
|
+
const { ENV_KEY_RE, setEnvKey } = await import("./env.js");
|
|
415
|
+
if (!ENV_KEY_RE.test(key)) {
|
|
416
|
+
throw new UsageError(`Invalid env key "${key}". Keys match [A-Za-z_][A-Za-z0-9_]*.`, "INVALID_FLAG_VALUE");
|
|
417
|
+
}
|
|
418
|
+
const fromEnv = getHyphenatedArg(args, "from-env");
|
|
419
|
+
const fromFile = getHyphenatedArg(args, "from-file");
|
|
420
|
+
if (fromEnv !== undefined && fromFile !== undefined) {
|
|
421
|
+
throw new UsageError("Pass only one of --from-file or --from-env (or use stdin).", "INVALID_FLAG_VALUE");
|
|
422
|
+
}
|
|
423
|
+
const MAX_ENV_VALUE_BYTES = 1024 * 1024; // 1 MB
|
|
424
|
+
let value;
|
|
425
|
+
if (fromFile !== undefined) {
|
|
426
|
+
if (!fs.existsSync(fromFile)) {
|
|
427
|
+
throw new NotFoundError(`File not found: ${fromFile}`, "FILE_NOT_FOUND");
|
|
428
|
+
}
|
|
429
|
+
const buf = fs.readFileSync(fromFile);
|
|
430
|
+
if (buf.byteLength > MAX_ENV_VALUE_BYTES)
|
|
431
|
+
throw new UsageError("Value exceeds the 1 MB limit.");
|
|
432
|
+
value = buf.toString("utf8");
|
|
433
|
+
}
|
|
434
|
+
else if (fromEnv !== undefined) {
|
|
435
|
+
const v = process.env[fromEnv];
|
|
436
|
+
if (v === undefined) {
|
|
437
|
+
throw new UsageError(`Environment variable "${fromEnv}" is not set.`, "INVALID_FLAG_VALUE");
|
|
438
|
+
}
|
|
439
|
+
value = v;
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
const buf = await readStdin(MAX_ENV_VALUE_BYTES, () => new UsageError("Value exceeds the 1 MB limit."));
|
|
443
|
+
// Strip a single trailing newline so `echo "$VAL" | akm env set` is exact.
|
|
444
|
+
value = buf.toString("utf8").replace(/\n$/, "");
|
|
445
|
+
}
|
|
446
|
+
setEnvKey(absPath, key, value);
|
|
447
|
+
// Warn (never block) on process-hijacking key names, matching the env-run audit.
|
|
448
|
+
const { isDangerousEnvKey } = await import("../lint/env-key-rules.js");
|
|
449
|
+
if (isDangerousEnvKey(key) && !isQuiet()) {
|
|
450
|
+
process.stderr.write(`warning: "${key}" can influence process execution when this env is loaded via 'akm env run'.\n`);
|
|
451
|
+
}
|
|
452
|
+
output("env-set", { ref: makeEnvRef(parsed.name, source), key });
|
|
453
|
+
});
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
const envUnsetCommand = defineCommand({
|
|
457
|
+
meta: {
|
|
458
|
+
name: "unset",
|
|
459
|
+
description: "Remove one or more KEYs from an env file: `akm env unset <ref> <KEY...>`. Preserves other keys and comments. To remove the whole file, use `akm env remove`.",
|
|
460
|
+
},
|
|
461
|
+
args: {
|
|
462
|
+
ref: { type: "positional", description: "Env ref (e.g. env:prod or just prod)", required: true },
|
|
463
|
+
// `key` is read from the raw positionals (one or more) in run(); declared
|
|
464
|
+
// non-required so citty doesn't block before we emit a structured error.
|
|
465
|
+
key: { type: "positional", description: "Key name(s) to remove (one or more)", required: false },
|
|
466
|
+
},
|
|
467
|
+
run({ args }) {
|
|
468
|
+
return runWithJsonErrors(async () => {
|
|
469
|
+
const parsed = parseEnvRef(args.ref);
|
|
470
|
+
const source = findEnvSource(parsed.origin);
|
|
471
|
+
const envRoot = path.join(source.path, "env");
|
|
472
|
+
const absPath = resolveAssetPathFromName("env", envRoot, parsed.name);
|
|
473
|
+
if (!isWithin(absPath, envRoot)) {
|
|
474
|
+
throw new UsageError(`Env name "${parsed.name}" escapes the env directory.`);
|
|
475
|
+
}
|
|
476
|
+
if (!fs.existsSync(absPath)) {
|
|
477
|
+
throw new NotFoundError(`Env not found: ${makeEnvRef(parsed.name, source)}`);
|
|
478
|
+
}
|
|
479
|
+
// citty puts every positional in `args._` (incl. the ref at [0]); the keys
|
|
480
|
+
// are the remaining positionals. citty also mis-captures the space-separated
|
|
481
|
+
// value of a global flag (`--format json`) as a positional, so drop any
|
|
482
|
+
// token that is actually a global flag's value (cli.ts:1335 documents this).
|
|
483
|
+
const globalFlagValues = new Set(["--format", "--shape", "--detail", "--scope", "--filter", "--target"]
|
|
484
|
+
.map((flag) => parseFlagValue(process.argv, flag))
|
|
485
|
+
.filter((v) => typeof v === "string"));
|
|
486
|
+
const keys = (Array.isArray(args._) ? args._.map(String) : [])
|
|
487
|
+
.slice(1)
|
|
488
|
+
.filter((k) => !globalFlagValues.has(k));
|
|
489
|
+
if (keys.length === 0) {
|
|
490
|
+
throw new UsageError("Usage: akm env unset <ref> <KEY...> (one or more keys).", "MISSING_REQUIRED_ARGUMENT");
|
|
491
|
+
}
|
|
492
|
+
const { ENV_KEY_RE, unsetEnvKeys } = await import("./env.js");
|
|
493
|
+
const invalid = keys.filter((k) => !ENV_KEY_RE.test(k));
|
|
494
|
+
if (invalid.length > 0) {
|
|
495
|
+
throw new UsageError(`Invalid env key(s): ${invalid.join(", ")}.`, "INVALID_FLAG_VALUE");
|
|
496
|
+
}
|
|
497
|
+
const { removed, missing } = unsetEnvKeys(absPath, keys);
|
|
498
|
+
output("env-unset", { ref: makeEnvRef(parsed.name, source), removed, missing });
|
|
499
|
+
});
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
// Single source of truth: the routing set is derived from the subCommands keys
|
|
503
|
+
// (M10) so adding a subcommand can never silently desync from `hasSubcommand`.
|
|
504
|
+
const envSubCommands = {
|
|
505
|
+
list: envListCommand,
|
|
506
|
+
path: envPathCommand,
|
|
507
|
+
export: envExportCommand,
|
|
508
|
+
run: envRunCommand,
|
|
509
|
+
create: envCreateCommand,
|
|
510
|
+
set: envSetCommand,
|
|
511
|
+
unset: envUnsetCommand,
|
|
512
|
+
remove: envRemoveCommand,
|
|
513
|
+
};
|
|
514
|
+
const ENV_SUBCOMMAND_SET = new Set(Object.keys(envSubCommands));
|
|
515
|
+
export const envCommand = defineCommand({
|
|
516
|
+
meta: {
|
|
517
|
+
name: "env",
|
|
518
|
+
description: "Manage `.env` files — a group of related CONFIGURATION values for an app or service (URLs, flags, plus any credentials it needs), loaded together. Values may or may not be sensitive; akm protects them all the same (key names visible, values never in structured output). For a single sensitive value used on its own (an auth token, key, or cert), use `akm secret`.",
|
|
519
|
+
},
|
|
520
|
+
subCommands: envSubCommands,
|
|
521
|
+
run({ args }) {
|
|
522
|
+
return runWithJsonErrors(async () => {
|
|
523
|
+
if (hasSubcommand(args, ENV_SUBCOMMAND_SET))
|
|
524
|
+
return;
|
|
525
|
+
const { listKeys } = await import("./env.js");
|
|
526
|
+
output("env-list", { envs: listEnvsRecursive(listKeys) });
|
|
527
|
+
});
|
|
528
|
+
},
|
|
529
|
+
});
|