akm-cli 0.9.0-beta.9 → 0.9.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +592 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/help/help-improve.md +9 -6
- package/dist/assets/hints/cli-hints-full.md +6 -5
- package/dist/assets/profiles/default.json +9 -4
- package/dist/assets/profiles/frequent.json +1 -1
- package/dist/assets/profiles/memory-focus.json +1 -1
- package/dist/assets/profiles/proactive-maintenance.json +25 -0
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/recombine-only.json +21 -0
- package/dist/assets/profiles/reflect-distill.json +30 -0
- package/dist/assets/profiles/synthesize.json +15 -0
- package/dist/assets/profiles/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +11 -3
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/assets/wiki/ingest-workflow-template.md +45 -16
- package/dist/assets/wiki/schema-template.md +4 -4
- package/dist/cli/clack.js +56 -0
- package/dist/cli/config-migrate.js +7 -1
- package/dist/cli/confirm.js +1 -1
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +25 -21
- package/dist/commands/agent/agent-dispatch.js +3 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +26 -7
- package/dist/commands/config-cli.js +26 -13
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +220 -227
- package/dist/commands/env/env.js +14 -67
- package/dist/commands/env/secret-cli.js +140 -138
- package/dist/commands/feedback-cli.js +156 -155
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +3 -3
- package/dist/commands/health/advisories.js +151 -0
- package/dist/commands/health/checks.js +103 -16
- package/dist/commands/health/html-report.js +447 -81
- package/dist/commands/health/improve-metrics.js +771 -0
- package/dist/commands/health/llm-usage.js +65 -0
- package/dist/commands/health/md-report.js +103 -0
- package/dist/commands/health/metrics.js +278 -0
- package/dist/commands/health/stash-exposure.js +46 -0
- package/dist/commands/health/surfaces.js +216 -0
- package/dist/commands/health/task-runs.js +135 -0
- package/dist/commands/health/types.js +26 -0
- package/dist/commands/health/windows.js +195 -0
- package/dist/commands/health.js +91 -1091
- package/dist/commands/improve/anti-collapse.js +170 -0
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/collapse-detector.js +421 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +64 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
- package/dist/commands/improve/consolidate.js +1295 -1277
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill/content-repair.js +202 -0
- package/dist/commands/improve/distill/promote-memory.js +229 -0
- package/dist/commands/improve/distill/quality-gate.js +236 -0
- package/dist/commands/improve/distill-guards.js +127 -0
- package/dist/commands/improve/distill-promotion-policy.js +826 -167
- package/dist/commands/improve/distill.js +228 -605
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +179 -59
- package/dist/commands/improve/extract-prompt.js +54 -3
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +409 -43
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/hot-probation.js +45 -0
- package/dist/commands/improve/improve-auto-accept.js +157 -10
- package/dist/commands/improve/improve-cli.js +115 -73
- package/dist/commands/improve/improve-profiles.js +28 -8
- package/dist/commands/improve/improve-result-file.js +15 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2764
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1100 -0
- package/dist/commands/improve/memory/memory-belief.js +14 -15
- package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
- package/dist/commands/improve/memory/memory-improve.js +27 -27
- package/dist/commands/improve/outcome-loop.js +270 -0
- package/dist/commands/improve/preparation.js +2002 -0
- package/dist/commands/improve/proactive-maintenance.js +37 -35
- package/dist/commands/improve/procedural.js +398 -0
- package/dist/commands/improve/recombine.js +818 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +206 -45
- package/dist/commands/improve/salience.js +455 -0
- package/dist/commands/improve/schema-similarity-gate.js +168 -0
- package/dist/commands/improve/shared.js +51 -0
- package/dist/commands/improve/triage.js +93 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +38 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/observability-cli.js +4 -4
- package/dist/commands/proposal/drain-policies.js +13 -4
- package/dist/commands/proposal/drain.js +45 -51
- package/dist/commands/proposal/legacy-import.js +115 -0
- package/dist/commands/proposal/proposal-cli.js +24 -34
- package/dist/commands/proposal/proposal.js +2 -1
- package/dist/commands/proposal/propose.js +8 -3
- package/dist/commands/proposal/repository.js +829 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +93 -895
- package/dist/commands/read/curate.js +410 -111
- package/dist/commands/read/knowledge.js +10 -3
- package/dist/commands/read/remember-cli.js +133 -138
- package/dist/commands/read/search-cli.js +15 -8
- package/dist/commands/read/search.js +22 -11
- package/dist/commands/read/show.js +106 -14
- package/dist/commands/registry-cli.js +76 -87
- package/dist/commands/remember.js +11 -12
- package/dist/commands/sources/add-cli.js +91 -95
- package/dist/commands/sources/history.js +1 -1
- package/dist/commands/sources/init.js +66 -18
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/schema-repair.js +44 -46
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/sources-cli.js +3 -3
- package/dist/commands/sources/stash-cli.js +19 -39
- package/dist/commands/sources/stash-skeleton.js +57 -8
- package/dist/commands/tasks/default-tasks.js +15 -2
- package/dist/commands/tasks/tasks-cli.js +20 -29
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +23 -38
- package/dist/commands/workflow-cli.js +15 -1
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +21 -4
- package/dist/core/asset/frontmatter.js +188 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +4 -23
- package/dist/core/concurrent.js +10 -1
- package/dist/core/config/config-io.js +10 -1
- package/dist/core/config/config-migration.js +18 -40
- package/dist/core/config/config-schema.js +382 -62
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +67 -22
- package/dist/core/deep-merge.js +38 -0
- package/dist/core/errors.js +1 -0
- package/dist/core/eval/rank-metrics.js +113 -0
- package/dist/core/events.js +4 -7
- package/dist/core/improve-types.js +47 -8
- package/dist/core/logs-db.js +14 -75
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +18 -18
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state/migrations.js +770 -0
- package/dist/core/state-db.js +132 -1126
- package/dist/core/structured.js +69 -0
- package/dist/core/time.js +53 -0
- package/dist/core/warn.js +21 -0
- package/dist/core/write-source.js +37 -0
- package/dist/indexer/db/db.js +259 -769
- package/dist/indexer/db/entry-mapper.js +41 -0
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/db/schema.js +516 -0
- package/dist/indexer/ensure-index.js +36 -92
- package/dist/indexer/feedback/utility-policy.js +75 -0
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +207 -4
- package/dist/indexer/index-writer-lock.js +18 -11
- package/dist/indexer/index-written-assets.js +105 -0
- package/dist/indexer/indexer.js +182 -204
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/metadata.js +20 -0
- package/dist/indexer/read-preflight.js +23 -0
- package/dist/indexer/search/db-search.js +89 -13
- package/dist/indexer/search/fts-query.js +51 -0
- package/dist/indexer/search/ranking-contributors.js +95 -9
- package/dist/indexer/search/ranking.js +79 -3
- package/dist/indexer/search/search-fields.js +6 -0
- package/dist/indexer/search/search-source.js +32 -21
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/indexer/walk/walker.js +21 -13
- package/dist/integrations/agent/builders.js +39 -13
- package/dist/integrations/agent/config.js +20 -59
- package/dist/integrations/agent/detect.js +9 -0
- package/dist/integrations/agent/index.js +3 -19
- package/dist/integrations/agent/model-aliases.js +7 -2
- package/dist/integrations/agent/profiles.js +7 -1
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/agent/runner.js +13 -9
- package/dist/integrations/agent/spawn.js +69 -67
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
- package/dist/integrations/harnesses/claude/index.js +2 -0
- package/dist/integrations/harnesses/claude/session-log.js +10 -0
- package/dist/integrations/harnesses/index.js +2 -3
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
- package/dist/integrations/harnesses/opencode/index.js +2 -0
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
- package/dist/integrations/harnesses/types.js +1 -0
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/call-ai.js +2 -2
- package/dist/llm/client.js +34 -11
- package/dist/llm/embedder.js +67 -4
- package/dist/llm/embedders/cache.js +3 -1
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +73 -3
- package/dist/llm/feature-gate.js +16 -15
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -127
- package/dist/llm/metadata-enhance.js +44 -31
- package/dist/llm/structured-call.js +49 -0
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/context.js +5 -5
- package/dist/output/renderers.js +85 -14
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +0 -3
- package/dist/output/shapes/passthrough.js +2 -1
- package/dist/output/text/helpers.js +29 -1
- package/dist/output/text/workflow.js +1 -0
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +27 -9
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +2661 -2369
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +883 -596
- package/dist/setup/detect.js +9 -0
- package/dist/setup/legacy-config.js +106 -0
- package/dist/setup/prompt.js +57 -0
- package/dist/setup/providers.js +14 -0
- package/dist/setup/registry-stash-loader.js +12 -0
- package/dist/setup/semantic-assets.js +124 -0
- package/dist/setup/setup.js +52 -1614
- package/dist/setup/steps/connection.js +734 -0
- package/dist/setup/steps/output.js +31 -0
- package/dist/setup/steps/platforms.js +124 -0
- package/dist/setup/steps/semantic.js +27 -0
- package/dist/setup/steps/sources.js +222 -0
- package/dist/setup/steps/stashdir.js +42 -0
- package/dist/setup/steps/tasks.js +152 -0
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +210 -0
- package/dist/sources/providers/git-provider.js +234 -0
- package/dist/sources/providers/git-stash.js +248 -0
- package/dist/sources/providers/git.js +10 -661
- package/dist/sources/providers/npm.js +2 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/website.js +9 -5
- package/dist/sources/website-ingest.js +187 -29
- package/dist/sources/wiki-fetchers/registry.js +53 -0
- package/dist/sources/wiki-fetchers/youtube.js +239 -0
- package/dist/storage/database.js +45 -10
- package/dist/storage/managed-db.js +82 -0
- package/dist/storage/repositories/canaries-repository.js +107 -0
- package/dist/storage/repositories/consolidation-repository.js +38 -0
- package/dist/storage/repositories/embeddings-repository.js +72 -0
- package/dist/storage/repositories/events-repository.js +187 -0
- package/dist/storage/repositories/extract-sessions-repository.js +96 -0
- package/dist/storage/repositories/improve-runs-repository.js +146 -0
- package/dist/storage/repositories/index-db.js +14 -8
- package/dist/storage/repositories/proposals-repository.js +220 -0
- package/dist/storage/repositories/recombine-repository.js +213 -0
- package/dist/storage/repositories/registry-cache.js +93 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
- package/dist/storage/repositories/task-history-repository.js +93 -0
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/index.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -1
- package/dist/tasks/backends/schtasks.js +1 -1
- package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
- package/dist/tasks/runner.js +15 -13
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +52 -11
- package/dist/workflows/cli.js +1 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +43 -118
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/README.md +69 -18
- package/docs/data-and-telemetry.md +5 -4
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +10 -10
- package/dist/assets/tasks/core/update-stashes.yml +0 -4
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
- package/dist/indexer/passes/staleness-detect.js +0 -488
package/dist/commands/env/env.js
CHANGED
|
@@ -18,10 +18,13 @@
|
|
|
18
18
|
* round-trip through `dotenv`; the shell-load safety guarantee still lives on
|
|
19
19
|
* the READ path (see `buildShellExportScript` + `akm env export`).
|
|
20
20
|
*
|
|
21
|
-
* Invariant: env
|
|
22
|
-
* indexer, the `akm show` renderer, or any
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* Invariant: nothing from an env file except key NAMES may be written to
|
|
22
|
+
* stdout, returned through the indexer, the `akm show` renderer, or any
|
|
23
|
+
* structured output channel. Key NAMES are surfaced for discoverability;
|
|
24
|
+
* comment text is NOT — real .env files routinely carry commented-out
|
|
25
|
+
* `KEY=value` lines and free-text notes containing live credentials, so
|
|
26
|
+
* comments are treated exactly like values. The supported value-load paths
|
|
27
|
+
* are:
|
|
25
28
|
*
|
|
26
29
|
* - `akm env run <ref> -- <command>` — values injected into the child
|
|
27
30
|
* process env (never via a shell), see `injectIntoEnv` / `loadEnv`. This is
|
|
@@ -71,75 +74,19 @@ function scanKeys(text) {
|
|
|
71
74
|
return keys;
|
|
72
75
|
}
|
|
73
76
|
/**
|
|
74
|
-
*
|
|
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).
|
|
77
|
+
* Read and return ONLY non-secret metadata: key names.
|
|
90
78
|
*
|
|
91
79
|
* The function reads the whole file into memory (same as any dotenv parser)
|
|
92
|
-
* but deliberately does not parse values — the LHS-only regex
|
|
93
|
-
*
|
|
94
|
-
*
|
|
80
|
+
* but deliberately does not parse values — the LHS-only regex scanner above
|
|
81
|
+
* ensures no value content is retained or returned. Comment text is never
|
|
82
|
+
* returned either: comments routinely contain commented-out `KEY=value`
|
|
83
|
+
* credentials and free-text secrets, so they never leave this function.
|
|
95
84
|
*/
|
|
96
85
|
export function listKeys(envPath) {
|
|
97
86
|
if (!fs.existsSync(envPath))
|
|
98
|
-
return { keys: []
|
|
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 [];
|
|
87
|
+
return { keys: [] };
|
|
112
88
|
const text = fs.readFileSync(envPath, "utf8");
|
|
113
|
-
|
|
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;
|
|
89
|
+
return { keys: scanKeys(text) };
|
|
143
90
|
}
|
|
144
91
|
/**
|
|
145
92
|
* Read all KEY=value pairs from an env file. Intended for programmatic callers
|
|
@@ -18,17 +18,25 @@
|
|
|
18
18
|
import { spawnSync } from "node:child_process";
|
|
19
19
|
import fs from "node:fs";
|
|
20
20
|
import path from "node:path";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { output, runWithJsonErrors } from "../../cli/shared.js";
|
|
21
|
+
import { getStringArg } from "../../cli/parse-args.js";
|
|
22
|
+
import { defineGroupCommand, defineJsonCommand, output } from "../../cli/shared.js";
|
|
24
23
|
import { deriveCanonicalAssetName } from "../../core/asset/asset-spec.js";
|
|
25
24
|
import { loadConfig } from "../../core/config/config.js";
|
|
26
25
|
import { makeSecretRef, resolveSecretPath } from "../../core/env-secret-ref.js";
|
|
27
26
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
28
27
|
import { appendEvent } from "../../core/events.js";
|
|
29
28
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
30
|
-
import { getHyphenatedArg } from "../../output/context.js";
|
|
31
29
|
import { readStdin } from "../../runtime.js";
|
|
30
|
+
import { buildChildEnv } from "./child-env.js";
|
|
31
|
+
function parseKeyListFlag(raw) {
|
|
32
|
+
if (raw === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
const keys = raw
|
|
35
|
+
.split(/[,\s]+/)
|
|
36
|
+
.map((k) => k.trim())
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
return keys.length > 0 ? keys : undefined;
|
|
39
|
+
}
|
|
32
40
|
/** Walk `secrets/` across all stashes, returning one entry per secret file. */
|
|
33
41
|
function listSecretsRecursive() {
|
|
34
42
|
const result = [];
|
|
@@ -60,18 +68,16 @@ function listSecretsRecursive() {
|
|
|
60
68
|
}
|
|
61
69
|
return result;
|
|
62
70
|
}
|
|
63
|
-
const secretListCommand =
|
|
71
|
+
const secretListCommand = defineJsonCommand({
|
|
64
72
|
meta: {
|
|
65
73
|
name: "list",
|
|
66
74
|
description: "List all secrets across all stashes by name (the file contents are never shown)",
|
|
67
75
|
},
|
|
68
|
-
run() {
|
|
69
|
-
|
|
70
|
-
output("secret-list", { secrets: listSecretsRecursive() });
|
|
71
|
-
});
|
|
76
|
+
async run() {
|
|
77
|
+
output("secret-list", { secrets: listSecretsRecursive() });
|
|
72
78
|
},
|
|
73
79
|
});
|
|
74
|
-
const secretSetCommand =
|
|
80
|
+
const secretSetCommand = defineJsonCommand({
|
|
75
81
|
meta: {
|
|
76
82
|
name: "set",
|
|
77
83
|
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.",
|
|
@@ -89,49 +95,47 @@ const secretSetCommand = defineCommand({
|
|
|
89
95
|
"from-file": { type: "string", description: "Read the value from this file (stored byte-exact)" },
|
|
90
96
|
"from-env": { type: "string", description: "Read the value from the named environment variable" },
|
|
91
97
|
},
|
|
92
|
-
run({ args }) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
async run({ args }) {
|
|
99
|
+
const { setSecret } = await import("./secret.js");
|
|
100
|
+
const { name, absPath, source } = resolveSecretPath(args.ref, { subPath: getStringArg(args, "path") });
|
|
101
|
+
const fromEnv = args["from-env"];
|
|
102
|
+
const fromFile = args["from-file"];
|
|
103
|
+
if (fromEnv !== undefined && fromFile !== undefined) {
|
|
104
|
+
throw new UsageError("Pass only one of --from-file or --from-env (or use stdin).", "INVALID_FLAG_VALUE");
|
|
105
|
+
}
|
|
106
|
+
const MAX_SECRET_BYTES = 5 * 1024 * 1024; // 5 MB
|
|
107
|
+
let value;
|
|
108
|
+
if (fromFile !== undefined) {
|
|
109
|
+
if (!fs.existsSync(fromFile)) {
|
|
110
|
+
throw new NotFoundError(`File not found: ${fromFile}`, "FILE_NOT_FOUND");
|
|
100
111
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
}
|
|
112
|
+
value = fs.readFileSync(fromFile);
|
|
113
|
+
if (value.byteLength > MAX_SECRET_BYTES) {
|
|
114
|
+
throw new UsageError("Secret exceeds the 5 MB limit.");
|
|
111
115
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
value = Buffer.from(envVal, "utf8");
|
|
116
|
+
}
|
|
117
|
+
else if (fromEnv !== undefined) {
|
|
118
|
+
const envVal = process.env[fromEnv];
|
|
119
|
+
if (envVal === undefined) {
|
|
120
|
+
throw new UsageError(`Environment variable "${fromEnv}" is not set.`, "INVALID_FLAG_VALUE");
|
|
118
121
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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");
|
|
122
|
+
value = Buffer.from(envVal, "utf8");
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
if (process.stdin.isTTY) {
|
|
126
|
+
process.stderr.write(`Enter value for secret "${name}" (Ctrl-D when done):\n`);
|
|
128
127
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
const stdinBuf = await readStdin(MAX_SECRET_BYTES, () => new UsageError("Secret exceeds the 5 MB limit."));
|
|
129
|
+
// Strip a single trailing newline so `echo "$TOKEN" | akm secret set`
|
|
130
|
+
// stores the token without the shell-added newline. Use --from-file for
|
|
131
|
+
// byte-exact storage of multi-line material (PEM keys, certs).
|
|
132
|
+
value = Buffer.from(stdinBuf.toString("utf8").replace(/\n$/, ""), "utf8");
|
|
133
|
+
}
|
|
134
|
+
setSecret(absPath, value);
|
|
135
|
+
output("secret-set", { ref: makeSecretRef(name, source) });
|
|
132
136
|
},
|
|
133
137
|
});
|
|
134
|
-
const secretPathCommand =
|
|
138
|
+
const secretPathCommand = defineJsonCommand({
|
|
135
139
|
meta: {
|
|
136
140
|
name: "path",
|
|
137
141
|
description: "Print the absolute secret file path for the Docker `_FILE` convention, e.g. `MY_SECRET_FILE=$(akm secret path secret:deploy-key)`.",
|
|
@@ -139,121 +143,119 @@ const secretPathCommand = defineCommand({
|
|
|
139
143
|
args: {
|
|
140
144
|
ref: { type: "positional", description: "Secret ref", required: true },
|
|
141
145
|
},
|
|
142
|
-
run({ args }) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
process.stdout.write(`${absPath}\n`);
|
|
149
|
-
});
|
|
146
|
+
async run({ args }) {
|
|
147
|
+
const { name, absPath, source } = resolveSecretPath(args.ref);
|
|
148
|
+
if (!fs.existsSync(absPath)) {
|
|
149
|
+
throw new NotFoundError(`Secret not found: ${makeSecretRef(name, source)}`);
|
|
150
|
+
}
|
|
151
|
+
process.stdout.write(`${absPath}\n`);
|
|
150
152
|
},
|
|
151
153
|
});
|
|
152
|
-
const secretRunCommand =
|
|
154
|
+
const secretRunCommand = defineJsonCommand({
|
|
153
155
|
meta: {
|
|
154
156
|
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.",
|
|
157
|
+
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. Pass --clean to start the child with a minimal inherited environment instead of the full parent environment.",
|
|
156
158
|
},
|
|
157
159
|
args: {
|
|
158
160
|
ref: { type: "positional", description: "Secret ref", required: true },
|
|
159
161
|
var: { type: "positional", description: "Environment variable name to inject the value into", required: true },
|
|
162
|
+
clean: {
|
|
163
|
+
type: "boolean",
|
|
164
|
+
description: "Start the child with a minimal inherited environment (PATH/HOME/locale/terminal basics) instead of the full parent environment.",
|
|
165
|
+
default: false,
|
|
166
|
+
},
|
|
167
|
+
inherit: {
|
|
168
|
+
type: "string",
|
|
169
|
+
description: "When used with --clean, also inherit these parent env vars (comma-separated). Ignored without --clean.",
|
|
170
|
+
},
|
|
160
171
|
},
|
|
161
|
-
run({ args }) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
172
|
+
async run({ args }) {
|
|
173
|
+
// Validate the target env var name FIRST (before the command split) so a
|
|
174
|
+
// dangerous/invalid name is rejected regardless of how the command is
|
|
175
|
+
// supplied — and so the failure does not depend on argv parsing.
|
|
176
|
+
const varName = args.var;
|
|
177
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(varName)) {
|
|
178
|
+
throw new UsageError(`"${varName}" is not a valid environment variable name.`, "INVALID_FLAG_VALUE");
|
|
179
|
+
}
|
|
180
|
+
const { isDangerousEnvKey } = await import("../lint/env-key-rules.js");
|
|
181
|
+
if (isDangerousEnvKey(varName)) {
|
|
182
|
+
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");
|
|
183
|
+
}
|
|
184
|
+
const dashIndex = process.argv.indexOf("--");
|
|
185
|
+
if (dashIndex < 0 || dashIndex === process.argv.length - 1) {
|
|
186
|
+
throw new UsageError("Missing command. Usage: akm secret run <ref> <VAR> -- <command>");
|
|
187
|
+
}
|
|
188
|
+
const command = process.argv.slice(dashIndex + 1);
|
|
189
|
+
const { name, absPath, source } = resolveSecretPath(args.ref);
|
|
190
|
+
if (!fs.existsSync(absPath)) {
|
|
191
|
+
throw new NotFoundError(`Secret not found: ${makeSecretRef(name, source)}`);
|
|
192
|
+
}
|
|
193
|
+
const { readValue } = await import("./secret.js");
|
|
194
|
+
const mergedEnv = buildChildEnv(process.env, {
|
|
195
|
+
clean: args.clean === true,
|
|
196
|
+
inherit: parseKeyListFlag(args.inherit) ?? [],
|
|
197
|
+
});
|
|
198
|
+
mergedEnv[varName] = readValue(absPath).toString("utf8");
|
|
199
|
+
// Audit trail: record access by ref + var name only — never the value.
|
|
200
|
+
appendEvent({
|
|
201
|
+
eventType: "secret_access",
|
|
202
|
+
ref: makeSecretRef(name, source),
|
|
203
|
+
metadata: { var: varName },
|
|
204
|
+
});
|
|
205
|
+
const result = spawnSync(command[0], command.slice(1), {
|
|
206
|
+
stdio: "inherit",
|
|
207
|
+
env: mergedEnv,
|
|
208
|
+
});
|
|
209
|
+
if (result.error) {
|
|
210
|
+
const err = result.error;
|
|
211
|
+
if (err.code === "ENOENT") {
|
|
212
|
+
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'.`);
|
|
182
213
|
}
|
|
183
|
-
|
|
184
|
-
|
|
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;
|
|
214
|
+
if (err.code === "EACCES") {
|
|
215
|
+
throw new ConfigError(`Command not executable: ${command[0]}`, "STASH_DIR_UNREADABLE", `Add execute permission ('chmod +x ${command[0]}') or invoke via an interpreter.`);
|
|
205
216
|
}
|
|
206
|
-
|
|
207
|
-
}
|
|
217
|
+
throw err;
|
|
218
|
+
}
|
|
219
|
+
process.exit(result.status ?? 0);
|
|
208
220
|
},
|
|
209
221
|
});
|
|
210
|
-
const secretRemoveCommand =
|
|
222
|
+
const secretRemoveCommand = defineJsonCommand({
|
|
211
223
|
meta: { name: "remove", description: "Remove a secret (and its .sensitive marker, if any)" },
|
|
212
224
|
args: {
|
|
213
225
|
ref: { type: "positional", description: "Secret ref", required: true },
|
|
214
226
|
yes: { type: "boolean", alias: "y", description: "Skip confirmation prompt", default: false },
|
|
215
227
|
},
|
|
216
|
-
run({ args }) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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 });
|
|
228
|
+
async run({ args }) {
|
|
229
|
+
const { name, absPath, source } = resolveSecretPath(args.ref);
|
|
230
|
+
const { confirmDestructive } = await import("../../cli/confirm.js");
|
|
231
|
+
const confirmed = await confirmDestructive(`Remove secret "${args.ref}"? This cannot be undone.`, {
|
|
232
|
+
yes: args.yes === true,
|
|
233
233
|
});
|
|
234
|
+
if (!confirmed) {
|
|
235
|
+
process.stderr.write("Aborted.\n");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const { removeSecret } = await import("./secret.js");
|
|
239
|
+
if (!fs.existsSync(absPath)) {
|
|
240
|
+
throw new NotFoundError(`Secret not found: ${makeSecretRef(name, source)}`);
|
|
241
|
+
}
|
|
242
|
+
const removed = removeSecret(absPath);
|
|
243
|
+
output("secret-remove", { ref: makeSecretRef(name, source), removed });
|
|
234
244
|
},
|
|
235
245
|
});
|
|
236
|
-
|
|
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({
|
|
246
|
+
export const secretCommand = defineGroupCommand({
|
|
247
247
|
meta: {
|
|
248
248
|
name: "secret",
|
|
249
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
250
|
},
|
|
251
|
-
subCommands:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
251
|
+
subCommands: {
|
|
252
|
+
list: secretListCommand,
|
|
253
|
+
path: secretPathCommand,
|
|
254
|
+
run: secretRunCommand,
|
|
255
|
+
set: secretSetCommand,
|
|
256
|
+
remove: secretRemoveCommand,
|
|
257
|
+
},
|
|
258
|
+
defaultRun() {
|
|
259
|
+
output("secret-list", { secrets: listSecretsRecursive() });
|
|
258
260
|
},
|
|
259
261
|
});
|