akm-cli 0.8.7 → 0.8.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +428 -0
- package/dist/assets/help/help-proposals.md +1 -2
- package/dist/assets/hints/cli-hints-full.md +34 -19
- package/dist/assets/hints/cli-hints-short.md +1 -1
- package/dist/assets/profiles/catchup.json +13 -0
- package/dist/assets/profiles/consolidate.json +13 -0
- package/dist/assets/profiles/frequent.json +13 -0
- package/dist/assets/tasks/core/backup.yml +4 -0
- package/dist/assets/tasks/core/extract.yml +4 -0
- package/dist/assets/tasks/core/improve.yml +4 -0
- package/dist/assets/tasks/core/index-refresh.yml +4 -0
- package/dist/assets/tasks/core/sync.yml +4 -0
- package/dist/assets/tasks/core/update-stashes.yml +4 -0
- package/dist/assets/tasks/core/version-check.yml +4 -0
- package/dist/assets/templates/html/default.html +78 -0
- package/dist/assets/templates/html/health.html +560 -0
- package/dist/assets/templates/html/vendor/echarts.min.js +45 -0
- package/dist/cli/config-migrate.js +6 -6
- package/dist/cli/config-validate.js +4 -4
- package/dist/cli/confirm.js +3 -3
- package/dist/cli/parse-args.js +1 -1
- package/dist/cli/shared.js +72 -19
- package/dist/cli-node.mjs +26 -0
- package/dist/cli.js +206 -3866
- package/dist/commands/{agent-dispatch.js → agent/agent-dispatch.js} +6 -6
- package/dist/commands/{agent-support.js → agent/agent-support.js} +2 -2
- package/dist/commands/agent/contribute-cli.js +200 -0
- package/dist/commands/completions.js +1 -1
- package/dist/commands/config-cli.js +230 -3
- package/dist/commands/db-cli.js +2 -2
- package/dist/commands/env/env-cli.js +529 -0
- package/dist/commands/env/env.js +410 -0
- package/dist/commands/env/secret-cli.js +259 -0
- package/dist/commands/{secret.js → env/secret.js} +6 -47
- package/dist/commands/events.js +4 -4
- package/dist/commands/feedback-cli.js +18 -34
- package/dist/commands/graph/graph-cli.js +132 -0
- package/dist/commands/{graph.js → graph/graph.js} +22 -16
- package/dist/commands/health/checks.js +279 -0
- package/dist/commands/health/html-report.js +448 -0
- package/dist/commands/health.js +189 -266
- package/dist/commands/{consolidate.js → improve/consolidate.js} +48 -36
- package/dist/commands/{distill-promotion-policy.js → improve/distill-promotion-policy.js} +3 -3
- package/dist/commands/{distill.js → improve/distill.js} +39 -18
- package/dist/commands/{eval-cases.js → improve/eval-cases.js} +1 -1
- package/dist/commands/{extract-cli.js → improve/extract-cli.js} +4 -4
- package/dist/commands/{extract-prompt.js → improve/extract-prompt.js} +2 -2
- package/dist/commands/{extract.js → improve/extract.js} +221 -26
- package/dist/commands/{improve-auto-accept.js → improve/improve-auto-accept.js} +30 -4
- package/dist/commands/{improve-cli.js → improve/improve-cli.js} +44 -22
- package/dist/commands/{improve-profiles.js → improve/improve-profiles.js} +13 -7
- package/dist/commands/{improve-result-file.js → improve/improve-result-file.js} +1 -1
- package/dist/commands/{improve.js → improve/improve.js} +672 -292
- package/dist/{core → commands/improve/memory}/memory-belief.js +2 -2
- package/dist/{core → commands/improve/memory}/memory-contradiction-detect.js +5 -5
- package/dist/{core → commands/improve/memory}/memory-improve.js +4 -4
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/{reflect.js → improve/reflect.js} +58 -28
- package/dist/commands/improve/session-asset.js +248 -0
- package/dist/commands/lint/agent-linter.js +1 -1
- package/dist/commands/lint/base-linter.js +55 -37
- package/dist/commands/lint/command-linter.js +1 -1
- package/dist/commands/lint/default-linter.js +1 -1
- package/dist/commands/lint/env-key-rules.js +1 -1
- package/dist/commands/lint/index.js +19 -25
- package/dist/commands/lint/knowledge-linter.js +1 -1
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +8 -8
- package/dist/commands/lint/skill-linter.js +1 -1
- package/dist/commands/lint/task-linter.js +1 -1
- package/dist/commands/lint/workflow-linter.js +1 -1
- package/dist/commands/lint.js +1 -1
- package/dist/commands/observability-cli.js +244 -0
- package/dist/commands/proposal/drain-policies.js +3 -3
- package/dist/commands/proposal/drain.js +87 -15
- package/dist/commands/proposal/proposal-cli.js +490 -0
- package/dist/commands/{proposal.js → proposal/proposal.js} +17 -6
- package/dist/commands/{propose.js → proposal/propose.js} +11 -11
- package/dist/{core → commands/proposal/validators}/proposal-quality-validators.js +8 -3
- package/dist/{core → commands/proposal/validators}/proposal-validators.js +5 -5
- package/dist/{core → commands/proposal/validators}/proposals.js +374 -345
- package/dist/commands/{curate.js → read/curate.js} +7 -7
- package/dist/commands/{knowledge.js → read/knowledge.js} +22 -9
- package/dist/commands/{registry-search.js → read/registry-search.js} +5 -5
- package/dist/commands/{remember-cli.js → read/remember-cli.js} +15 -7
- package/dist/commands/read/search-cli.js +207 -0
- package/dist/commands/{search.js → read/search.js} +22 -27
- package/dist/commands/{show.js → read/show.js} +31 -45
- package/dist/commands/registry-cli.js +8 -8
- package/dist/commands/remember.js +14 -10
- package/dist/commands/sources/add-cli.js +293 -0
- package/dist/commands/{history.js → sources/history.js} +27 -25
- package/dist/commands/{info.js → sources/info.js} +6 -6
- package/dist/commands/{init.js → sources/init.js} +6 -6
- package/dist/commands/{installed-stashes.js → sources/installed-stashes.js} +12 -12
- package/dist/commands/{migration-help.js → sources/migration-help.js} +3 -2
- package/dist/commands/{schema-repair.js → sources/schema-repair.js} +8 -8
- package/dist/commands/{self-update.js → sources/self-update.js} +10 -9
- package/dist/commands/{source-add.js → sources/source-add.js} +10 -10
- package/dist/commands/{source-clone.js → sources/source-clone.js} +7 -7
- package/dist/commands/{source-manage.js → sources/source-manage.js} +4 -4
- package/dist/commands/sources/sources-cli.js +305 -0
- package/dist/commands/sources/stash-cli.js +219 -0
- package/dist/commands/{stash-skeleton.js → sources/stash-skeleton.js} +2 -1
- package/dist/commands/tasks/default-tasks.js +173 -0
- package/dist/commands/tasks/tasks-cli.js +210 -0
- package/dist/commands/{tasks.js → tasks/tasks.js} +14 -14
- package/dist/commands/wiki-cli.js +307 -0
- package/dist/commands/workflow-cli.js +329 -0
- package/dist/core/action-contributors.js +1 -1
- package/dist/core/assert.js +40 -0
- package/dist/core/asset/asset-create.js +54 -0
- package/dist/core/{asset-ref.js → asset/asset-ref.js} +21 -4
- package/dist/core/{asset-registry.js → asset/asset-registry.js} +3 -3
- package/dist/core/{asset-spec.js → asset/asset-spec.js} +17 -31
- package/dist/core/{markdown.js → asset/markdown.js} +1 -1
- package/dist/core/{stash-meta.js → asset/stash-meta.js} +1 -1
- package/dist/core/best-effort.js +64 -0
- package/dist/core/common.js +32 -18
- package/dist/core/{config-io.js → config/config-io.js} +29 -19
- package/dist/core/{config-migration.js → config/config-migration.js} +11 -9
- package/dist/core/{config-schema.js → config/config-schema.js} +50 -7
- package/dist/core/config/config-types.js +16 -0
- package/dist/core/{config-walker.js → config/config-walker.js} +2 -2
- package/dist/core/{config.js → config/config.js} +10 -8
- package/dist/core/env-secret-ref.js +90 -0
- package/dist/core/errors.js +13 -3
- package/dist/core/events.js +27 -4
- package/dist/core/file-lock.js +1 -1
- package/dist/core/improve-types.js +48 -0
- package/dist/core/lesson-lint.js +2 -2
- package/dist/core/logs-db.js +304 -0
- package/dist/core/paths.js +2 -2
- package/dist/core/ripgrep/install.js +2 -2
- package/dist/core/ripgrep/resolve.js +2 -2
- package/dist/core/state-db.js +195 -60
- package/dist/core/text-truncation.js +148 -0
- package/dist/core/time.js +1 -1
- package/dist/core/write-source.js +98 -85
- package/dist/indexer/{db-backup.js → db/db-backup.js} +9 -24
- package/dist/indexer/{db.js → db/db.js} +128 -118
- package/dist/indexer/{graph-db.js → db/graph-db.js} +9 -4
- package/dist/indexer/{llm-cache.js → db/llm-cache.js} +15 -12
- package/dist/indexer/ensure-index.js +4 -4
- package/dist/indexer/{graph-boost.js → graph/graph-boost.js} +1 -1
- package/dist/indexer/{graph-extraction.js → graph/graph-extraction.js} +55 -13
- package/dist/indexer/indexer.js +37 -30
- package/dist/indexer/init.js +54 -0
- package/dist/indexer/manifest.js +10 -10
- package/dist/indexer/{memory-inference.js → passes/memory-inference.js} +141 -33
- package/dist/indexer/{metadata-contributors.js → passes/metadata-contributors.js} +10 -8
- package/dist/indexer/{metadata.js → passes/metadata.js} +15 -19
- package/dist/indexer/{staleness-detect.js → passes/staleness-detect.js} +53 -12
- package/dist/indexer/{db-search.js → search/db-search.js} +28 -16
- package/dist/indexer/{ranking-contributors.js → search/ranking-contributors.js} +1 -1
- package/dist/indexer/{ranking.js → search/ranking.js} +2 -2
- package/dist/indexer/{search-hit-enrichers.js → search/search-hit-enrichers.js} +3 -3
- package/dist/indexer/{search-source.js → search/search-source.js} +8 -8
- package/dist/indexer/{semantic-status.js → search/semantic-status.js} +3 -3
- package/dist/indexer/usage/unmigrated-vaults-guard.js +94 -0
- package/dist/indexer/{usage-events.js → usage/usage-events.js} +32 -0
- package/dist/indexer/{file-context.js → walk/file-context.js} +10 -15
- package/dist/indexer/{matchers.js → walk/matchers.js} +13 -9
- package/dist/indexer/{path-resolver.js → walk/path-resolver.js} +6 -6
- package/dist/indexer/{project-context.js → walk/project-context.js} +1 -1
- package/dist/indexer/{walker.js → walk/walker.js} +4 -3
- package/dist/integrations/agent/builder-shared.js +39 -0
- package/dist/integrations/agent/builders.js +14 -81
- package/dist/integrations/agent/config.js +6 -4
- package/dist/integrations/agent/detect.js +1 -1
- package/dist/integrations/agent/index.js +23 -8
- package/dist/integrations/agent/prompts.js +2 -3
- package/dist/integrations/agent/runner.js +22 -3
- package/dist/integrations/agent/spawn.js +9 -10
- package/dist/integrations/harnesses/claude/agent-builder.js +48 -0
- package/dist/integrations/harnesses/claude/config-import.js +70 -0
- package/dist/integrations/harnesses/claude/index.js +64 -0
- package/dist/integrations/{session-logs/providers/claude-code.js → harnesses/claude/session-log.js} +32 -5
- package/dist/integrations/harnesses/index.js +144 -0
- package/dist/integrations/harnesses/opencode/agent-builder.js +43 -0
- package/dist/integrations/harnesses/opencode/config-import.js +82 -0
- package/dist/integrations/harnesses/opencode/index.js +59 -0
- package/dist/integrations/{session-logs/providers/opencode.js → harnesses/opencode/session-log.js} +1 -1
- package/dist/integrations/harnesses/opencode-sdk/index.js +49 -0
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +234 -0
- package/dist/integrations/harnesses/types.js +43 -0
- package/dist/integrations/lockfile.js +7 -16
- package/dist/integrations/session-logs/index.js +82 -9
- package/dist/llm/call-ai.js +4 -4
- package/dist/llm/client.js +146 -6
- package/dist/llm/embedder.js +6 -6
- package/dist/llm/embedders/local.js +9 -22
- package/dist/llm/embedders/remote.js +2 -2
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +31 -12
- package/dist/llm/index-passes.js +1 -1
- package/dist/llm/memory-infer.js +12 -5
- package/dist/llm/metadata-enhance.js +2 -2
- package/dist/llm/usage-persist.js +77 -0
- package/dist/llm/usage-telemetry.js +103 -0
- package/dist/output/context.js +9 -46
- package/dist/output/html-render.js +73 -0
- package/dist/output/renderers.js +88 -58
- package/dist/output/shapes/curate.js +7 -3
- package/dist/output/shapes/distill.js +7 -3
- package/dist/output/shapes/env-list.js +18 -16
- package/dist/output/shapes/events.js +5 -4
- package/dist/output/shapes/helpers.js +19 -5
- package/dist/output/shapes/history.js +7 -3
- package/dist/output/shapes/passthrough.js +8 -11
- package/dist/output/shapes/{proposal-accept.js → proposal/accept.js} +7 -3
- package/dist/output/shapes/{proposal-diff.js → proposal/diff.js} +7 -3
- package/dist/output/shapes/{proposal-list.js → proposal/list.js} +7 -3
- package/dist/output/shapes/{proposal-producer.js → proposal/producer.js} +5 -4
- package/dist/output/shapes/{proposal-reject.js → proposal/reject.js} +7 -3
- package/dist/output/shapes/{proposal-show.js → proposal/show.js} +7 -3
- package/dist/output/shapes/registry-search.js +7 -3
- package/dist/output/shapes/registry.js +12 -0
- package/dist/output/shapes/search.js +7 -3
- package/dist/output/shapes/secret-list.js +18 -16
- package/dist/output/shapes/show.js +7 -3
- package/dist/output/shapes.js +55 -30
- package/dist/output/text/add.js +2 -3
- package/dist/output/text/clone.js +2 -3
- package/dist/output/text/config.js +2 -3
- package/dist/output/text/curate.js +4 -3
- package/dist/output/text/distill.js +2 -3
- package/dist/output/text/enable-disable.js +5 -4
- package/dist/output/text/env.js +13 -0
- package/dist/output/text/events.js +5 -4
- package/dist/output/text/feedback.js +4 -3
- package/dist/output/text/helpers.js +123 -40
- package/dist/output/text/history.js +2 -3
- package/dist/output/text/import.js +2 -3
- package/dist/output/text/index.js +2 -3
- package/dist/output/text/info.js +2 -3
- package/dist/output/text/init.js +2 -3
- package/dist/output/text/list.js +2 -3
- package/dist/output/text/proposal/producer.js +9 -0
- package/dist/output/text/proposal/proposal.js +13 -0
- package/dist/output/text/registry-commands.js +8 -7
- package/dist/output/text/registry.js +12 -0
- package/dist/output/text/remember.js +4 -3
- package/dist/output/text/remove.js +2 -3
- package/dist/output/text/save.js +2 -3
- package/dist/output/text/search.js +4 -3
- package/dist/output/text/show.js +4 -3
- package/dist/output/text/update.js +2 -3
- package/dist/output/text/upgrade.js +2 -3
- package/dist/output/text/wiki.js +12 -11
- package/dist/output/text/workflow.js +12 -10
- package/dist/output/text.js +66 -32
- package/dist/registry/build-index.js +11 -10
- package/dist/registry/factory.js +1 -1
- package/dist/registry/origin-resolve.js +1 -1
- package/dist/registry/providers/index.js +2 -2
- package/dist/registry/providers/skills-sh.js +91 -72
- package/dist/registry/providers/static-index.js +75 -52
- package/dist/registry/resolve.js +3 -3
- package/dist/runtime.js +242 -0
- package/dist/scripts/migrate-storage.js +1654 -683
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +254 -168
- package/dist/setup/detect.js +311 -9
- package/dist/setup/harness-config-import.js +6 -120
- package/dist/setup/setup.js +454 -43
- package/dist/sources/include.js +1 -1
- package/dist/sources/provider-factory.js +2 -2
- package/dist/sources/providers/filesystem.js +3 -3
- package/dist/sources/providers/git.js +9 -9
- package/dist/sources/providers/index.js +4 -4
- package/dist/sources/providers/npm.js +6 -6
- package/dist/sources/providers/provider-utils.js +13 -20
- package/dist/sources/providers/sync-from-ref.js +5 -5
- package/dist/sources/providers/tar-utils.js +2 -2
- package/dist/sources/providers/website.js +2 -2
- package/dist/sources/resolve.js +5 -5
- package/dist/sources/website-ingest.js +5 -5
- package/dist/storage/database.js +102 -0
- package/dist/storage/engines/sqlite-migrations.js +42 -0
- package/dist/storage/locations.js +25 -0
- package/dist/storage/repositories/index-db.js +43 -0
- package/dist/storage/repositories/workflow-runs-repository.js +141 -0
- package/dist/tasks/backends/cron.js +4 -4
- package/dist/tasks/backends/exec-utils.js +32 -0
- package/dist/tasks/backends/index.js +3 -3
- package/dist/tasks/backends/launchd.js +7 -14
- package/dist/tasks/backends/schtasks.js +7 -16
- package/dist/tasks/embedded.js +71 -0
- package/dist/tasks/parser.js +2 -2
- package/dist/tasks/resolveAkmBin.js +1 -1
- package/dist/tasks/runner.js +127 -31
- package/dist/tasks/schedule.js +1 -1
- package/dist/tasks/validator.js +7 -7
- package/dist/text-import-hook.mjs +51 -0
- package/dist/version.js +2 -1
- package/dist/wiki/wiki.js +7 -7
- package/dist/workflows/{authoring.js → authoring/authoring.js} +6 -6
- package/dist/workflows/{scope-key.js → authoring/scope-key.js} +1 -1
- package/dist/workflows/cli.js +1 -1
- package/dist/workflows/db.js +54 -32
- package/dist/workflows/parser.js +4 -4
- package/dist/workflows/renderer.js +5 -5
- package/dist/workflows/runtime/agent-identity.js +56 -0
- package/dist/workflows/runtime/checkin.js +57 -0
- package/dist/workflows/{runs.js → runtime/runs.js} +197 -101
- package/dist/workflows/validate-summary.js +82 -0
- package/docs/README.md +1 -1
- package/docs/data-and-telemetry.md +6 -6
- package/package.json +17 -8
- package/dist/commands/add-cli.js +0 -279
- package/dist/commands/env.js +0 -213
- package/dist/integrations/agent/sdk-runner.js +0 -126
- package/dist/output/shapes/vault-list.js +0 -19
- package/dist/output/text/proposal-producer.js +0 -8
- package/dist/output/text/proposal.js +0 -12
- package/dist/output/text/vault.js +0 -16
- /package/dist/core/{asset-serialize.js → asset/asset-serialize.js} +0 -0
- /package/dist/core/{frontmatter.js → asset/frontmatter.js} +0 -0
- /package/dist/core/{config-sources.js → config/config-sources.js} +0 -0
- /package/dist/indexer/{graph-dedup.js → graph/graph-dedup.js} +0 -0
- /package/dist/{core/config-types.js → indexer/passes/pass-context.js} +0 -0
- /package/dist/indexer/{search-fields.js → search/search-fields.js} +0 -0
- /package/dist/indexer/{index-context.js → walk/index-context.js} +0 -0
- /package/dist/workflows/{document-cache.js → runtime/document-cache.js} +0 -0
package/dist/cli.js
CHANGED
|
@@ -2,23 +2,28 @@
|
|
|
2
2
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
3
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
4
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
-
// Runtime guard: akm-cli 0.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// `
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
5
|
+
// Runtime guard: akm-cli 0.9 runs on Bun (primary) and Node.js >= 20 (#465,
|
|
6
|
+
// #560). The runtime boundary (src/runtime.ts, src/storage/database.ts) makes
|
|
7
|
+
// the Node path additive. Under Node the CLI must be launched via the
|
|
8
|
+
// `dist/cli-node.mjs` wrapper, which registers the text-import loader hook
|
|
9
|
+
// before this module graph loads; running `node dist/cli.js` directly still
|
|
10
|
+
// works for code paths that touch no embedded text asset, but the wrapper is
|
|
11
|
+
// the supported entry. The hard floor is Node 20: `@clack/core` (prompts) imports
|
|
12
|
+
// `node:util`'s `styleText` (added in Node 20.12) — Node 18 (EOL) throws at import.
|
|
13
|
+
{
|
|
14
|
+
const isBun = typeof globalThis.Bun !== "undefined";
|
|
15
|
+
if (!isBun) {
|
|
16
|
+
const major = Number.parseInt((process.versions.node ?? "0").split(".")[0], 10);
|
|
17
|
+
if (Number.isNaN(major) || major < 20) {
|
|
18
|
+
console.error("\n ERROR: akm-cli requires the Bun runtime (https://bun.sh) or Node.js >= 20.\n" +
|
|
19
|
+
` Detected Node.js ${process.versions.node ?? "unknown"}.\n` +
|
|
20
|
+
" Install options:\n" +
|
|
21
|
+
" 1. Bun: curl -fsSL https://bun.sh/install | bash && bun install -g akm-cli\n" +
|
|
22
|
+
" 2. Node: upgrade to Node.js 20 or newer (https://nodejs.org)\n" +
|
|
23
|
+
" 3. Binary: curl -fsSL https://github.com/itlackey/akm/releases/latest/download/install.sh | bash\n");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
22
27
|
}
|
|
23
28
|
// Global error handlers (#478) — route any async work outside the
|
|
24
29
|
// `runWithJsonErrors` envelope through the same JSON shape so users never see
|
|
@@ -49,84 +54,42 @@ process.on("uncaughtException", (err) => {
|
|
|
49
54
|
console.error(err.stack);
|
|
50
55
|
process.exit(1);
|
|
51
56
|
});
|
|
52
|
-
import { spawnSync } from "node:child_process";
|
|
53
57
|
import fs from "node:fs";
|
|
54
58
|
import path from "node:path";
|
|
55
|
-
import * as p from "@clack/prompts";
|
|
56
59
|
import { defineCommand, runMain } from "citty";
|
|
57
|
-
import {
|
|
58
|
-
import {
|
|
59
|
-
import {
|
|
60
|
-
import {
|
|
61
|
-
import {
|
|
62
|
-
import {
|
|
63
|
-
import {
|
|
64
|
-
import {
|
|
65
|
-
import {
|
|
66
|
-
import { extractCommand } from "./commands/extract-cli";
|
|
67
|
-
import {
|
|
68
|
-
import {
|
|
69
|
-
import {
|
|
70
|
-
import {
|
|
71
|
-
import {
|
|
72
|
-
import {
|
|
73
|
-
import {
|
|
74
|
-
import {
|
|
75
|
-
import {
|
|
76
|
-
import {
|
|
77
|
-
import {
|
|
78
|
-
import {
|
|
79
|
-
import {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return "user";
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
import { resolveImproveProfile } from "./commands/improve-profiles";
|
|
94
|
-
import { akmProposalAccept, akmProposalDiff, akmProposalList, akmProposalReject, akmProposalRevert, akmProposalShow, } from "./commands/proposal";
|
|
95
|
-
import { drainProposals } from "./commands/proposal/drain";
|
|
96
|
-
import { resolveDrainPolicy } from "./commands/proposal/drain-policies";
|
|
97
|
-
import { akmPropose } from "./commands/propose";
|
|
98
|
-
import { akmSearch, parseBeliefFilterMode, parseScopeFilterFlags, parseSearchSource } from "./commands/search";
|
|
99
|
-
import { checkForUpdate, performUpgrade } from "./commands/self-update";
|
|
100
|
-
import { akmShowUnified, normalizeShowArgv } from "./commands/show";
|
|
101
|
-
import { akmClone } from "./commands/source-clone";
|
|
102
|
-
import { akmTasksAdd, akmTasksDoctor, akmTasksHistory, akmTasksList, akmTasksRemove, akmTasksRun, akmTasksSetEnabled, akmTasksShow, akmTasksSync, parseTaskRef, } from "./commands/tasks";
|
|
103
|
-
import { parseAssetRef } from "./core/asset-ref";
|
|
104
|
-
import { deriveCanonicalAssetName, resolveAssetPathFromName } from "./core/asset-spec";
|
|
105
|
-
import { isHttpUrl, isWithin, resolveStashDir, writeFileAtomic } from "./core/common";
|
|
106
|
-
import { DEFAULT_CONFIG, loadConfig, loadUserConfig, resolveConfiguredSources, saveConfig } from "./core/config";
|
|
107
|
-
import { ConfigError, NotFoundError, UsageError } from "./core/errors";
|
|
108
|
-
import { appendEvent } from "./core/events";
|
|
109
|
-
import { getCacheDir, getConfigPath, getDbPath, getDefaultStashDir } from "./core/paths";
|
|
110
|
-
import { parseMetaRef } from "./core/stash-meta";
|
|
111
|
-
import { plainize } from "./core/tty";
|
|
112
|
-
import { clearLogFile, info, isQuiet, isVerbose, setLogFile, setQuiet, setVerbose, warn } from "./core/warn";
|
|
113
|
-
import { closeDatabase, openExistingDatabase } from "./indexer/db";
|
|
114
|
-
import { akmIndex } from "./indexer/indexer";
|
|
115
|
-
import { resolveSourceEntries } from "./indexer/search-source";
|
|
116
|
-
import { resolveTriageJudgmentRunner } from "./integrations/agent/runner";
|
|
117
|
-
import { EMBEDDED_HINTS, EMBEDDED_HINTS_FULL } from "./output/cli-hints";
|
|
118
|
-
import { getHyphenatedArg, getHyphenatedBoolean, getOutputMode, hasBooleanFlag, initOutputMode, parseDetailLevel, parseFlagValue, } from "./output/context";
|
|
119
|
-
import { formatEventLine } from "./output/text";
|
|
120
|
-
import { resolveSourcesForOrigin } from "./registry/origin-resolve";
|
|
121
|
-
import { resolveWritableOverride, saveGitStash } from "./sources/providers/git";
|
|
122
|
-
import { resolveAssetPath } from "./sources/resolve";
|
|
123
|
-
import { pkgVersion } from "./version";
|
|
124
|
-
import { createWorkflowAsset, formatWorkflowErrors, getWorkflowTemplate, validateWorkflowSource, } from "./workflows/authoring";
|
|
125
|
-
import { hasWorkflowSubcommand, parseWorkflowJsonObject, parseWorkflowStepState, WORKFLOW_STEP_STATES, } from "./workflows/cli";
|
|
126
|
-
import { completeWorkflowStep, getNextWorkflowStep, getWorkflowStatus, listWorkflowRuns, resumeWorkflowRun, startWorkflowRun, } from "./workflows/runs";
|
|
127
|
-
const SKILLS_SH_NAME = "skills.sh";
|
|
128
|
-
const SKILLS_SH_URL = "https://skills.sh";
|
|
129
|
-
const SKILLS_SH_PROVIDER = "skills-sh";
|
|
60
|
+
import { EXIT_CODES, emitJsonError, output, parseAllFlagValues, runWithJsonErrors } from "./cli/shared.js";
|
|
61
|
+
import { agentCommand, lintCommand, proposeCommand } from "./commands/agent/contribute-cli.js";
|
|
62
|
+
import { generateBashCompletions, installBashCompletions } from "./commands/completions.js";
|
|
63
|
+
import { configCommand } from "./commands/config-cli.js";
|
|
64
|
+
import { envCommand } from "./commands/env/env-cli.js";
|
|
65
|
+
import { secretCommand } from "./commands/env/secret-cli.js";
|
|
66
|
+
import { feedbackCommand } from "./commands/feedback-cli.js";
|
|
67
|
+
import { graphCommand } from "./commands/graph/graph-cli.js";
|
|
68
|
+
import { akmHealth, parseWindowSpec, renderRunsDetailMd, renderWindowCompareMd, } from "./commands/health.js";
|
|
69
|
+
import { extractCommand } from "./commands/improve/extract-cli.js";
|
|
70
|
+
import { improveCommand } from "./commands/improve/improve-cli.js";
|
|
71
|
+
import { hintsCommand, lessonsCommand, logCommand } from "./commands/observability-cli.js";
|
|
72
|
+
import { proposalCommand } from "./commands/proposal/proposal-cli.js";
|
|
73
|
+
import { rememberCommand } from "./commands/read/remember-cli.js";
|
|
74
|
+
import { curateCommand, searchCommand, showCommand } from "./commands/read/search-cli.js";
|
|
75
|
+
import { normalizeShowArgv } from "./commands/read/show.js";
|
|
76
|
+
import { registryCommand } from "./commands/registry-cli.js";
|
|
77
|
+
import { addCommand } from "./commands/sources/add-cli.js";
|
|
78
|
+
import { renderMigrationHelp } from "./commands/sources/migration-help.js";
|
|
79
|
+
import { cloneCommand, historyCommand, listCommand, removeCommand, syncCommand, updateCommand, upgradeCommand, } from "./commands/sources/sources-cli.js";
|
|
80
|
+
import { dbCommand, importKnowledgeCommand, indexCommand, infoCommand, initCommand, } from "./commands/sources/stash-cli.js";
|
|
81
|
+
import { tasksCommand } from "./commands/tasks/tasks-cli.js";
|
|
82
|
+
import { wikiCommand } from "./commands/wiki-cli.js";
|
|
83
|
+
import { workflowCommand } from "./commands/workflow-cli.js";
|
|
84
|
+
import { bestEffort } from "./core/best-effort.js";
|
|
85
|
+
import { loadConfig } from "./core/config/config.js";
|
|
86
|
+
import { UsageError } from "./core/errors.js";
|
|
87
|
+
import { getCacheDir, getConfigPath, getDbPath } from "./core/paths.js";
|
|
88
|
+
import { plainize } from "./core/tty.js";
|
|
89
|
+
import { info, isQuiet, setQuiet, setVerbose, warn } from "./core/warn.js";
|
|
90
|
+
import { getHyphenatedBoolean, getOutputMode, initOutputMode, parseFlagValue } from "./output/context.js";
|
|
91
|
+
import { deliverRendered, renderHtml, resolveTemplatePath } from "./output/html-render.js";
|
|
92
|
+
import { pkgVersion } from "./version.js";
|
|
130
93
|
function applyEarlyStderrFlags(argv) {
|
|
131
94
|
if (argv.includes("--quiet") || argv.includes("-q")) {
|
|
132
95
|
setQuiet(true);
|
|
@@ -231,10 +194,36 @@ const setupCommand = defineCommand({
|
|
|
231
194
|
default: false,
|
|
232
195
|
description: "Probe LLM/embedding endpoints after writing config to verify connectivity",
|
|
233
196
|
},
|
|
197
|
+
"detect-only": {
|
|
198
|
+
type: "boolean",
|
|
199
|
+
default: false,
|
|
200
|
+
description: "Run environment detection only and print the result (no prompts, no writes). Pair with --format json.",
|
|
201
|
+
},
|
|
202
|
+
"reset-recommended": {
|
|
203
|
+
type: "boolean",
|
|
204
|
+
default: false,
|
|
205
|
+
description: "Merge opinionated, detection-derived defaults into the existing config without removing custom keys.",
|
|
206
|
+
},
|
|
234
207
|
},
|
|
235
208
|
async run({ args }) {
|
|
236
209
|
await runWithJsonErrors(async () => {
|
|
237
210
|
const noInit = getHyphenatedBoolean(args, "no-init");
|
|
211
|
+
const detectOnly = getHyphenatedBoolean(args, "detect-only");
|
|
212
|
+
const resetRecommended = getHyphenatedBoolean(args, "reset-recommended");
|
|
213
|
+
if (detectOnly) {
|
|
214
|
+
// Detection only: no prompts, no writes.
|
|
215
|
+
const { runDetectOnly } = await import("./setup/setup.js");
|
|
216
|
+
const detection = await runDetectOnly();
|
|
217
|
+
output("setup", detection);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (resetRecommended) {
|
|
221
|
+
const { runResetRecommended } = await import("./setup/setup.js");
|
|
222
|
+
const result = await runResetRecommended({ dir: args.dir, noInit, probe: args.probe });
|
|
223
|
+
output("setup", result);
|
|
224
|
+
printSetupTtyHint(result);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
238
227
|
if (args.from && args.config) {
|
|
239
228
|
throw new UsageError("Pass either --from <file> or --config <json>, not both.", "INVALID_FLAG_VALUE");
|
|
240
229
|
}
|
|
@@ -243,32 +232,37 @@ const setupCommand = defineCommand({
|
|
|
243
232
|
// `~`, resolves relative paths against cwd, picks the YAML or JSON
|
|
244
233
|
// parser based on the file extension, and surfaces any
|
|
245
234
|
// read/parse/shape errors as ConfigError("INVALID_CONFIG_FILE").
|
|
246
|
-
|
|
235
|
+
// `runSetupFromConfig` is fully non-interactive; with `--yes` it also
|
|
236
|
+
// fills defaults for keys the file leaves missing.
|
|
237
|
+
const { loadSetupConfigFromFile, runSetupFromConfig } = await import("./setup/setup.js");
|
|
247
238
|
const loaded = await loadSetupConfigFromFile(args.from);
|
|
248
239
|
const result = await runSetupFromConfig({
|
|
249
240
|
configJson: loaded.configJson,
|
|
250
241
|
dir: args.dir,
|
|
251
242
|
noInit,
|
|
252
243
|
probe: args.probe,
|
|
244
|
+
applyDefaults: args.yes,
|
|
253
245
|
});
|
|
254
246
|
output("setup", result);
|
|
255
247
|
printSetupTtyHint(result);
|
|
256
248
|
}
|
|
257
249
|
else if (args.config) {
|
|
258
|
-
// Non-interactive config mode
|
|
259
|
-
|
|
250
|
+
// Non-interactive config mode. With `--yes`, defaults fill any keys
|
|
251
|
+
// the JSON blob leaves missing after the deep merge.
|
|
252
|
+
const { runSetupFromConfig } = await import("./setup/setup.js");
|
|
260
253
|
const result = await runSetupFromConfig({
|
|
261
254
|
configJson: args.config,
|
|
262
255
|
dir: args.dir,
|
|
263
256
|
noInit,
|
|
264
257
|
probe: args.probe,
|
|
258
|
+
applyDefaults: args.yes,
|
|
265
259
|
});
|
|
266
260
|
output("setup", result);
|
|
267
261
|
printSetupTtyHint(result);
|
|
268
262
|
}
|
|
269
263
|
else if (args.yes) {
|
|
270
264
|
// Defaults mode — no prompts
|
|
271
|
-
const { runSetupWithDefaults } = await import("./setup/setup");
|
|
265
|
+
const { runSetupWithDefaults } = await import("./setup/setup.js");
|
|
272
266
|
const result = await runSetupWithDefaults({
|
|
273
267
|
dir: args.dir,
|
|
274
268
|
noInit,
|
|
@@ -279,112 +273,12 @@ const setupCommand = defineCommand({
|
|
|
279
273
|
}
|
|
280
274
|
else {
|
|
281
275
|
// Interactive wizard
|
|
282
|
-
const { runSetupWizard } = await import("./setup/setup");
|
|
276
|
+
const { runSetupWizard } = await import("./setup/setup.js");
|
|
283
277
|
await runSetupWizard({ dir: args.dir, noInit });
|
|
284
278
|
}
|
|
285
279
|
});
|
|
286
280
|
},
|
|
287
281
|
});
|
|
288
|
-
const initCommand = defineCommand({
|
|
289
|
-
meta: {
|
|
290
|
-
name: "init",
|
|
291
|
-
description: "Initialize akm's working stash directory and persist stashDir in config",
|
|
292
|
-
},
|
|
293
|
-
args: {
|
|
294
|
-
dir: { type: "string", description: "Custom stash directory path (default: ~/akm)" },
|
|
295
|
-
},
|
|
296
|
-
async run({ args }) {
|
|
297
|
-
await runWithJsonErrors(async () => {
|
|
298
|
-
// Accept both historical spellings for backwards compatibility with
|
|
299
|
-
// older docs/scripts that used `--stashDir`.
|
|
300
|
-
const legacyDir = parseFlagValue(process.argv, "--stashDir") ?? parseFlagValue(process.argv, "--stash-dir");
|
|
301
|
-
const result = await akmInit({ dir: args.dir ?? legacyDir });
|
|
302
|
-
output("init", result);
|
|
303
|
-
});
|
|
304
|
-
},
|
|
305
|
-
});
|
|
306
|
-
const indexCommand = defineCommand({
|
|
307
|
-
meta: { name: "index", description: "Build search index (incremental by default; --full forces full reindex)" },
|
|
308
|
-
args: {
|
|
309
|
-
full: { type: "boolean", description: "Force full reindex", default: false },
|
|
310
|
-
clean: {
|
|
311
|
-
type: "boolean",
|
|
312
|
-
description: "After indexing, remove any entries whose source file no longer exists on disk.",
|
|
313
|
-
default: false,
|
|
314
|
-
},
|
|
315
|
-
"dry-run": {
|
|
316
|
-
type: "boolean",
|
|
317
|
-
description: "When combined with --clean, report stale entries without deleting them.",
|
|
318
|
-
default: false,
|
|
319
|
-
},
|
|
320
|
-
},
|
|
321
|
-
async run({ args }) {
|
|
322
|
-
await runWithJsonErrors(async () => {
|
|
323
|
-
if (getHyphenatedBoolean(args, "enrich") || parseFlagValue(process.argv, "--enrich") !== undefined) {
|
|
324
|
-
throw new UsageError("`akm index --enrich` has been removed. Plain `akm index` now performs metadata enrichment by default.");
|
|
325
|
-
}
|
|
326
|
-
if (getHyphenatedBoolean(args, "re-enrich") || parseFlagValue(process.argv, "--re-enrich") !== undefined) {
|
|
327
|
-
throw new UsageError("`akm index --re-enrich` has been removed. Re-enrichment of index-time LLM passes is not exposed in this slice.");
|
|
328
|
-
}
|
|
329
|
-
const outputMode = getOutputMode();
|
|
330
|
-
const controller = new AbortController();
|
|
331
|
-
const abort = () => controller.abort(new Error("index interrupted"));
|
|
332
|
-
process.once("SIGINT", abort);
|
|
333
|
-
process.once("SIGTERM", abort);
|
|
334
|
-
const indexLogFile = path.join(getCacheDir(), "logs", "index", `${new Date().toISOString().replace(/[:.]/g, "-")}.log`);
|
|
335
|
-
setLogFile(indexLogFile);
|
|
336
|
-
const verbose = isVerbose();
|
|
337
|
-
const spin = !verbose && outputMode.format === "text" ? p.spinner() : null;
|
|
338
|
-
if (spin) {
|
|
339
|
-
spin.start(`Building search index${args.full ? " (full rebuild)" : ""}...`);
|
|
340
|
-
}
|
|
341
|
-
let latestMessage = "";
|
|
342
|
-
try {
|
|
343
|
-
const result = await akmIndex({
|
|
344
|
-
full: args.full,
|
|
345
|
-
clean: args.clean,
|
|
346
|
-
dryRun: args["dry-run"],
|
|
347
|
-
onProgress: ({ phase, message, processed, total }) => {
|
|
348
|
-
latestMessage = message;
|
|
349
|
-
const progressPrefix = processed !== undefined && total !== undefined ? `[${processed}/${total}] ` : "";
|
|
350
|
-
if (verbose) {
|
|
351
|
-
info(`[index:${phase}] ${progressPrefix}${message}`);
|
|
352
|
-
}
|
|
353
|
-
else if (spin) {
|
|
354
|
-
spin.stop(`${progressPrefix}${message}`);
|
|
355
|
-
spin.start(`${progressPrefix}${message}`);
|
|
356
|
-
}
|
|
357
|
-
},
|
|
358
|
-
signal: controller.signal,
|
|
359
|
-
});
|
|
360
|
-
if (spin) {
|
|
361
|
-
spin.stop(`Indexed ${result.totalEntries} assets.`);
|
|
362
|
-
}
|
|
363
|
-
output("index", result);
|
|
364
|
-
}
|
|
365
|
-
catch (error) {
|
|
366
|
-
if (spin) {
|
|
367
|
-
spin.stop(latestMessage ? `Indexing failed after: ${latestMessage}` : "Indexing failed.");
|
|
368
|
-
}
|
|
369
|
-
throw error;
|
|
370
|
-
}
|
|
371
|
-
finally {
|
|
372
|
-
clearLogFile();
|
|
373
|
-
process.off("SIGINT", abort);
|
|
374
|
-
process.off("SIGTERM", abort);
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
},
|
|
378
|
-
});
|
|
379
|
-
const infoCommand = defineCommand({
|
|
380
|
-
meta: { name: "info", description: "Show system capabilities, configuration, and index stats" },
|
|
381
|
-
run() {
|
|
382
|
-
return runWithJsonErrors(() => {
|
|
383
|
-
const result = assembleInfo();
|
|
384
|
-
output("info", result);
|
|
385
|
-
});
|
|
386
|
-
},
|
|
387
|
-
});
|
|
388
282
|
const healthCommand = defineCommand({
|
|
389
283
|
meta: { name: "health", description: "Check akm runtime health, artifacts, and improve metrics" },
|
|
390
284
|
args: {
|
|
@@ -396,10 +290,6 @@ const healthCommand = defineCommand({
|
|
|
396
290
|
type: "string",
|
|
397
291
|
description: "Group rows by: run (one row per improve_runs entry). Omit for the default summary.",
|
|
398
292
|
},
|
|
399
|
-
detail: {
|
|
400
|
-
type: "string",
|
|
401
|
-
description: "DEPRECATED: use --group-by run instead of --detail per-run (removed 0.9.0).",
|
|
402
|
-
},
|
|
403
293
|
"window-compare": {
|
|
404
294
|
type: "string",
|
|
405
295
|
description: "Compare current window vs prior window of the same duration (e.g. 24h, 7d, 30m)",
|
|
@@ -408,35 +298,43 @@ const healthCommand = defineCommand({
|
|
|
408
298
|
type: "string",
|
|
409
299
|
description: "Explicit comparison window 'name=...,since=ISO,until=ISO' (repeatable, up to 4; mutually exclusive with --window-compare)",
|
|
410
300
|
},
|
|
301
|
+
compare: {
|
|
302
|
+
type: "string",
|
|
303
|
+
description: "Comparison window for the --format html report's trend deltas (default: 24h)",
|
|
304
|
+
},
|
|
411
305
|
},
|
|
412
306
|
async run({ args }) {
|
|
413
307
|
let resultStatus;
|
|
414
|
-
await runWithJsonErrors(() => {
|
|
308
|
+
await runWithJsonErrors(async () => {
|
|
415
309
|
// citty only surfaces the last value of a repeated flag, so read --windows
|
|
416
310
|
// directly from argv to support multi-window comparison.
|
|
417
311
|
const rawWindows = parseAllFlagValues("--windows");
|
|
418
312
|
const windows = rawWindows.length > 0 ? rawWindows.map((raw) => parseWindowSpec(raw)) : undefined;
|
|
419
|
-
const
|
|
420
|
-
const detailRaw = args.detail;
|
|
421
|
-
// Back-compat: `--detail per-run` → `--group-by run` (warns; removed 0.9.0).
|
|
422
|
-
let groupBy = groupByRaw;
|
|
423
|
-
if (detailRaw !== undefined) {
|
|
424
|
-
if (detailRaw === "per-run") {
|
|
425
|
-
// Read --quiet from argv (not the warn-module singleton) so the
|
|
426
|
-
// warning fires correctly even when the early-stderr flags were not
|
|
427
|
-
// applied (e.g. the in-process test harness), matching the WS2
|
|
428
|
-
// output-flag deprecations in src/output/context.ts.
|
|
429
|
-
const quietRequested = process.argv.includes("--quiet") || process.argv.includes("-q");
|
|
430
|
-
if (!quietRequested) {
|
|
431
|
-
process.stderr.write("warning: '--detail per-run' is deprecated for 'akm health'; use '--group-by run'. Removed in 0.9.0.\n");
|
|
432
|
-
}
|
|
433
|
-
groupBy = groupBy ?? "run";
|
|
434
|
-
}
|
|
435
|
-
else {
|
|
436
|
-
throw new UsageError(`Invalid value for --detail: ${detailRaw}. 'akm health' uses --group-by run (not --detail).`, "INVALID_DETAIL_VALUE");
|
|
437
|
-
}
|
|
438
|
-
}
|
|
313
|
+
const groupBy = args["group-by"];
|
|
439
314
|
const windowCompareRaw = args["window-compare"];
|
|
315
|
+
const mode = getOutputMode();
|
|
316
|
+
// `--format html` is health-specific: render the full HTML health
|
|
317
|
+
// report (charts, KPI cards, advisories) from the bespoke template.
|
|
318
|
+
// Mirrors the `md` intercept below. Two reads, exactly like the
|
|
319
|
+
// retired akm-health-report skill: the canonical per-run window plus a
|
|
320
|
+
// window-compare read for the trend deltas (defaults to 24h,
|
|
321
|
+
// overridable via --compare).
|
|
322
|
+
if (mode.format === "html") {
|
|
323
|
+
const compare = args.compare ?? windowCompareRaw ?? "24h";
|
|
324
|
+
const result = akmHealth({ since: args.since, groupBy: "run" });
|
|
325
|
+
resultStatus = result.status;
|
|
326
|
+
const deltas = akmHealth({ since: args.since, windowCompare: compare }).deltas;
|
|
327
|
+
const { buildHealthHtmlReplacements } = await import("./commands/health/html-report.js");
|
|
328
|
+
const { listPendingProposals } = await import("./commands/proposal/proposal.js");
|
|
329
|
+
const replacements = buildHealthHtmlReplacements(result, {
|
|
330
|
+
window: args.since ?? "24h",
|
|
331
|
+
compare,
|
|
332
|
+
proposals: listPendingProposals(),
|
|
333
|
+
deltas,
|
|
334
|
+
});
|
|
335
|
+
deliverRendered(renderHtml(resolveTemplatePath("health"), replacements), mode.outputPath);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
440
338
|
const result = akmHealth({
|
|
441
339
|
since: args.since,
|
|
442
340
|
groupBy: groupBy,
|
|
@@ -447,13 +345,12 @@ const healthCommand = defineCommand({
|
|
|
447
345
|
// `--format md` is health-specific: render a TSV-shaped per-run or
|
|
448
346
|
// window-compare table to stdout instead of going through the JSON
|
|
449
347
|
// envelope. Other modes fall through to the standard output() path.
|
|
450
|
-
const mode = getOutputMode();
|
|
451
348
|
if (mode.format === "md") {
|
|
452
349
|
if (result.windows && result.windows.length > 0) {
|
|
453
|
-
|
|
350
|
+
deliverRendered(renderWindowCompareMd(result.windows, result.deltas), mode.outputPath);
|
|
454
351
|
}
|
|
455
352
|
else if (result.runs) {
|
|
456
|
-
|
|
353
|
+
deliverRendered(renderRunsDetailMd(result.runs), mode.outputPath);
|
|
457
354
|
}
|
|
458
355
|
else {
|
|
459
356
|
output("health", result);
|
|
@@ -471,3624 +368,110 @@ const healthCommand = defineCommand({
|
|
|
471
368
|
}
|
|
472
369
|
},
|
|
473
370
|
});
|
|
474
|
-
const
|
|
475
|
-
meta: {
|
|
371
|
+
const helpCommand = defineCommand({
|
|
372
|
+
meta: {
|
|
373
|
+
name: "help",
|
|
374
|
+
description: "Print focused help topics such as migration guidance for a release",
|
|
375
|
+
},
|
|
476
376
|
subCommands: {
|
|
477
|
-
|
|
478
|
-
meta: {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
},
|
|
482
|
-
run({ args }) {
|
|
483
|
-
return runWithJsonErrors(() => {
|
|
484
|
-
output("graph-summary", akmGraphSummary({ source: args.source }));
|
|
485
|
-
});
|
|
486
|
-
},
|
|
487
|
-
}),
|
|
488
|
-
entities: defineCommand({
|
|
489
|
-
meta: { name: "entities", description: "List entities with per-file occurrence counts" },
|
|
490
|
-
args: {
|
|
491
|
-
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
492
|
-
limit: { type: "string", description: "Maximum entities to return" },
|
|
493
|
-
},
|
|
494
|
-
run({ args }) {
|
|
495
|
-
return runWithJsonErrors(() => {
|
|
496
|
-
output("graph-entities", akmGraphEntities({ source: args.source, limit: parsePositiveIntFlag(args.limit ?? undefined) }));
|
|
497
|
-
});
|
|
498
|
-
},
|
|
499
|
-
}),
|
|
500
|
-
relations: defineCommand({
|
|
501
|
-
meta: { name: "relations", description: "List relations with occurrence counts" },
|
|
502
|
-
args: {
|
|
503
|
-
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
504
|
-
limit: { type: "string", description: "Maximum relations to return" },
|
|
505
|
-
},
|
|
506
|
-
run({ args }) {
|
|
507
|
-
return runWithJsonErrors(() => {
|
|
508
|
-
output("graph-relations", akmGraphRelations({ source: args.source, limit: parsePositiveIntFlag(args.limit ?? undefined) }));
|
|
509
|
-
});
|
|
510
|
-
},
|
|
511
|
-
}),
|
|
512
|
-
related: defineCommand({
|
|
513
|
-
meta: { name: "related", description: "Show graph-related neighboring assets for a ref" },
|
|
514
|
-
args: {
|
|
515
|
-
ref: { type: "positional", description: "Asset ref", required: true },
|
|
516
|
-
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
517
|
-
limit: { type: "string", description: "Maximum related assets to return" },
|
|
518
|
-
},
|
|
519
|
-
async run({ args }) {
|
|
520
|
-
return runWithJsonErrors(async () => {
|
|
521
|
-
output("graph-related", await akmGraphRelated({
|
|
522
|
-
ref: args.ref ?? "",
|
|
523
|
-
source: args.source,
|
|
524
|
-
limit: parsePositiveIntFlag(args.limit ?? undefined),
|
|
525
|
-
}));
|
|
526
|
-
});
|
|
527
|
-
},
|
|
528
|
-
}),
|
|
529
|
-
entity: defineCommand({
|
|
530
|
-
meta: { name: "entity", description: "List assets that contain the given entity" },
|
|
531
|
-
args: {
|
|
532
|
-
name: { type: "positional", description: "Entity name", required: true },
|
|
533
|
-
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
534
|
-
limit: { type: "string", description: "Maximum matches to return" },
|
|
535
|
-
},
|
|
536
|
-
run({ args }) {
|
|
537
|
-
return runWithJsonErrors(() => {
|
|
538
|
-
output("graph-entity", akmGraphEntity({
|
|
539
|
-
name: args.name ?? "",
|
|
540
|
-
source: args.source,
|
|
541
|
-
limit: parsePositiveIntFlag(args.limit ?? undefined),
|
|
542
|
-
}));
|
|
543
|
-
});
|
|
544
|
-
},
|
|
545
|
-
}),
|
|
546
|
-
orphans: defineCommand({
|
|
547
|
-
meta: { name: "orphans", description: "List assets with no extracted graph entities" },
|
|
548
|
-
args: {
|
|
549
|
-
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
550
|
-
limit: { type: "string", description: "Maximum orphans to return" },
|
|
551
|
-
},
|
|
552
|
-
run({ args }) {
|
|
553
|
-
return runWithJsonErrors(() => {
|
|
554
|
-
output("graph-orphans", akmGraphOrphans({ source: args.source, limit: parsePositiveIntFlag(args.limit ?? undefined) }));
|
|
555
|
-
});
|
|
556
|
-
},
|
|
557
|
-
}),
|
|
558
|
-
export: defineCommand({
|
|
559
|
-
meta: { name: "export", description: "Export graph artifact as JSON or JSONL" },
|
|
560
|
-
args: {
|
|
561
|
-
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
562
|
-
out: { type: "string", description: "Output path" },
|
|
563
|
-
format: { type: "string", description: "Export format (json|jsonl)", default: "json" },
|
|
564
|
-
},
|
|
565
|
-
run({ args }) {
|
|
566
|
-
return runWithJsonErrors(() => {
|
|
567
|
-
output("graph-export", akmGraphExport({
|
|
568
|
-
source: args.source,
|
|
569
|
-
out: args.out ?? "",
|
|
570
|
-
format: args.format,
|
|
571
|
-
}));
|
|
572
|
-
});
|
|
377
|
+
migrate: defineCommand({
|
|
378
|
+
meta: {
|
|
379
|
+
name: "migrate",
|
|
380
|
+
description: "Print release notes and migration guidance for a version. Bundled notes live in docs/migration/release-notes/<version>.md; an unknown version lists what's available.",
|
|
573
381
|
},
|
|
574
|
-
}),
|
|
575
|
-
update: defineCommand({
|
|
576
|
-
meta: { name: "update", description: "Re-run graph extraction, optionally scoped to specific asset refs" },
|
|
577
382
|
args: {
|
|
578
|
-
|
|
383
|
+
// Optional in citty so run() is invoked even when omitted; we
|
|
384
|
+
// re-validate below to surface a structured UsageError (exit 2)
|
|
385
|
+
// instead of citty's default help-banner exit-0.
|
|
386
|
+
version: {
|
|
579
387
|
type: "positional",
|
|
580
|
-
description: "
|
|
388
|
+
description: "Version to review (for example 0.6.0, v0.6.0, 0.6.0-rc1, or latest)",
|
|
581
389
|
required: false,
|
|
582
|
-
default: "",
|
|
583
390
|
},
|
|
584
|
-
source: { type: "string", description: "Source name/path (default: primary stash source)" },
|
|
585
391
|
},
|
|
586
|
-
|
|
587
|
-
return runWithJsonErrors(
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
392
|
+
run({ args }) {
|
|
393
|
+
return runWithJsonErrors(() => {
|
|
394
|
+
const version = resolveHelpMigrateVersionArg(typeof args.version === "string" ? args.version : undefined);
|
|
395
|
+
if (!version?.trim()) {
|
|
396
|
+
throw new UsageError("Usage: akm help migrate <version>.", "MISSING_REQUIRED_ARGUMENT", "Pass a version like `0.6.0`, `v0.6.0`, `0.6.0-rc1`, or `latest`.");
|
|
397
|
+
}
|
|
398
|
+
process.stdout.write(renderMigrationHelp(version));
|
|
591
399
|
});
|
|
592
400
|
},
|
|
593
401
|
}),
|
|
594
402
|
},
|
|
595
|
-
run({ args }) {
|
|
596
|
-
return runWithJsonErrors(() => {
|
|
597
|
-
if (hasSubcommand(args, GRAPH_SUBCOMMAND_SET))
|
|
598
|
-
return;
|
|
599
|
-
output("graph-summary", akmGraphSummary());
|
|
600
|
-
});
|
|
601
|
-
},
|
|
602
403
|
});
|
|
603
|
-
|
|
604
|
-
// stop akm and run `scripts/migrations/restore-data-dir.sh <backup>`.
|
|
605
|
-
const DB_SUBCOMMAND_SET = new Set(["backups"]);
|
|
606
|
-
const dbCommand = defineCommand({
|
|
404
|
+
const completionsCommand = defineCommand({
|
|
607
405
|
meta: {
|
|
608
|
-
name: "
|
|
609
|
-
description: "
|
|
610
|
-
},
|
|
611
|
-
subCommands: {
|
|
612
|
-
backups: defineCommand({
|
|
613
|
-
meta: {
|
|
614
|
-
name: "backups",
|
|
615
|
-
description: "List pre-upgrade snapshots of the data directory (newest first). Backups are created automatically before destructive DB version upgrades unless AKM_DB_BACKUP=0.",
|
|
616
|
-
},
|
|
617
|
-
run() {
|
|
618
|
-
return runWithJsonErrors(() => {
|
|
619
|
-
output("db-backups", akmDbBackups());
|
|
620
|
-
});
|
|
621
|
-
},
|
|
622
|
-
}),
|
|
623
|
-
},
|
|
624
|
-
run({ args }) {
|
|
625
|
-
return runWithJsonErrors(() => {
|
|
626
|
-
if (hasSubcommand(args, DB_SUBCOMMAND_SET))
|
|
627
|
-
return;
|
|
628
|
-
// Default action: list backups.
|
|
629
|
-
output("db-backups", akmDbBackups());
|
|
630
|
-
});
|
|
406
|
+
name: "completions",
|
|
407
|
+
description: "Generate or install shell completion script",
|
|
631
408
|
},
|
|
632
|
-
});
|
|
633
|
-
const searchCommand = defineCommand({
|
|
634
|
-
meta: { name: "search", description: "Search the stash" },
|
|
635
409
|
args: {
|
|
636
|
-
|
|
637
|
-
type: {
|
|
638
|
-
type: "string",
|
|
639
|
-
description: "Asset type filter (skill, command, agent, knowledge, workflow, script, memory, vault, wiki, lesson, or any). Use workflow to find step-by-step task assets.",
|
|
640
|
-
},
|
|
641
|
-
limit: { type: "string", description: "Maximum number of results" },
|
|
642
|
-
source: { type: "string", description: "Search source (stash|registry|both)", default: "stash" },
|
|
643
|
-
filter: {
|
|
644
|
-
type: "string",
|
|
645
|
-
description: "Scope filter (repeatable): --filter user=<id> --filter agent=<id> --filter run=<id> --filter channel=<name>. Narrows results without changing ranking.",
|
|
646
|
-
},
|
|
647
|
-
"include-proposed": {
|
|
648
|
-
type: "boolean",
|
|
649
|
-
description: 'Include entries with quality:"proposed" in the result set. Excluded by default (v1 spec §4.2).',
|
|
650
|
-
default: false,
|
|
651
|
-
},
|
|
652
|
-
belief: {
|
|
653
|
-
type: "string",
|
|
654
|
-
description: "Memory belief filter: all|current|historical. current keeps active memory beliefs; historical keeps contradicted/superseded/archived memory beliefs.",
|
|
655
|
-
default: "all",
|
|
656
|
-
},
|
|
657
|
-
format: { type: "string", description: "Output format (json|jsonl|text|yaml)" },
|
|
658
|
-
detail: { type: "string", description: "Detail level (brief|normal|full)" },
|
|
659
|
-
"no-project-context": {
|
|
410
|
+
install: {
|
|
660
411
|
type: "boolean",
|
|
661
|
-
description: "
|
|
412
|
+
description: "Install completions to the appropriate directory",
|
|
662
413
|
default: false,
|
|
663
414
|
},
|
|
664
|
-
|
|
665
|
-
async run({ args }) {
|
|
666
|
-
await runWithJsonErrors(async () => {
|
|
667
|
-
const query = (args.query ?? "").trim();
|
|
668
|
-
if (!query) {
|
|
669
|
-
throw new UsageError('A search query is required. Usage: akm search "<query>" [--type <type>] [--limit <n>]', "MISSING_REQUIRED_ARGUMENT", 'Pass a query like `akm search "docker"` or `akm search "code review" --type skill`.');
|
|
670
|
-
}
|
|
671
|
-
const type = args.type;
|
|
672
|
-
const limit = parsePositiveIntFlag(args.limit ?? undefined);
|
|
673
|
-
const source = parseSearchSource(args.source);
|
|
674
|
-
// Repeatable; citty exposes only the last `--filter` value, so read all
|
|
675
|
-
// occurrences directly from argv (same pattern as `--tag`).
|
|
676
|
-
const filterTokens = parseAllFlagValues("--filter");
|
|
677
|
-
const filters = parseScopeFilterFlags(filterTokens, "--filter");
|
|
678
|
-
const includeProposed = args["include-proposed"] === true;
|
|
679
|
-
const belief = parseBeliefFilterMode(typeof args.belief === "string" ? args.belief : undefined);
|
|
680
|
-
const noProjectContext = getHyphenatedBoolean(args, "no-project-context");
|
|
681
|
-
// --no-project-context sets env so searchDatabase picks it up without
|
|
682
|
-
// threading the flag through the entire call stack.
|
|
683
|
-
if (noProjectContext)
|
|
684
|
-
process.env.AKM_DISABLE_PROJECT_CONTEXT = "1";
|
|
685
|
-
const result = await akmSearch({
|
|
686
|
-
query,
|
|
687
|
-
type,
|
|
688
|
-
limit,
|
|
689
|
-
source,
|
|
690
|
-
filters,
|
|
691
|
-
includeProposed,
|
|
692
|
-
belief,
|
|
693
|
-
eventSource: resolveEventSource(),
|
|
694
|
-
});
|
|
695
|
-
output("search", result);
|
|
696
|
-
});
|
|
697
|
-
},
|
|
698
|
-
});
|
|
699
|
-
const curateCommand = defineCommand({
|
|
700
|
-
meta: { name: "curate", description: "Curate the best matching assets for a task or prompt" },
|
|
701
|
-
args: {
|
|
702
|
-
// Optional in citty so run() is invoked when omitted; we re-validate
|
|
703
|
-
// below to surface a structured UsageError (exit 2) instead of citty's
|
|
704
|
-
// default help-banner exit-0.
|
|
705
|
-
query: { type: "positional", description: "Task or prompt to curate assets for", required: false },
|
|
706
|
-
type: {
|
|
415
|
+
shell: {
|
|
707
416
|
type: "string",
|
|
708
|
-
description: "
|
|
417
|
+
description: "Shell type (bash)",
|
|
418
|
+
default: "bash",
|
|
709
419
|
},
|
|
710
|
-
limit: { type: "string", description: "Maximum number of curated results", default: "4" },
|
|
711
|
-
source: { type: "string", description: "Search source (stash|registry|both)", default: "stash" },
|
|
712
|
-
// Output-contract flags. The active values are read from the process-level
|
|
713
|
-
// singleton (parsed from argv at startup); these declarations make them
|
|
714
|
-
// visible in `akm curate --help` and document the supported axes.
|
|
715
|
-
format: { type: "string", description: "Output format (json|jsonl|text|yaml)" },
|
|
716
|
-
detail: { type: "string", description: "Detail level (brief|normal|full)" },
|
|
717
|
-
shape: { type: "string", description: "Output projection (human|agent)" },
|
|
718
|
-
},
|
|
719
|
-
async run({ args }) {
|
|
720
|
-
await runWithJsonErrors(async () => {
|
|
721
|
-
if (!args.query || !String(args.query).trim()) {
|
|
722
|
-
throw new UsageError('A curate query is required. Usage: akm curate "<task or prompt>" [--type <type>] [--limit <n>]', "MISSING_REQUIRED_ARGUMENT", 'Describe the task you want assets for, e.g. `akm curate "deploy to prod"`.');
|
|
723
|
-
}
|
|
724
|
-
const type = args.type;
|
|
725
|
-
const limitParsed = parsePositiveIntFlag(args.limit ?? undefined);
|
|
726
|
-
const limit = limitParsed && limitParsed > 0 ? limitParsed : 4;
|
|
727
|
-
const source = parseSearchSource(args.source ?? "stash");
|
|
728
|
-
const curated = await akmCurate({ query: args.query, type, limit, source });
|
|
729
|
-
output("curate", curated);
|
|
730
|
-
});
|
|
731
420
|
},
|
|
732
|
-
})
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
421
|
+
run({ args }) {
|
|
422
|
+
if (args.shell !== "bash") {
|
|
423
|
+
throw new UsageError(`Unsupported shell: ${args.shell}. Only bash is supported.`);
|
|
424
|
+
}
|
|
425
|
+
const script = generateBashCompletions(main);
|
|
426
|
+
if (args.install) {
|
|
427
|
+
const dest = installBashCompletions(script);
|
|
428
|
+
info(`Completions installed to ${dest}`);
|
|
429
|
+
info(`Restart your shell or run: source ${dest}`);
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
process.stdout.write(script);
|
|
741
433
|
}
|
|
742
|
-
}
|
|
743
|
-
return kinds;
|
|
744
|
-
}
|
|
745
|
-
const listCommand = defineCommand({
|
|
746
|
-
meta: { name: "list", description: "List all sources (local directories, managed packages, remote providers)" },
|
|
747
|
-
args: {
|
|
748
|
-
kind: { type: "string", description: "Filter by source kind (local, managed, remote). Comma-separated." },
|
|
749
|
-
},
|
|
750
|
-
async run({ args }) {
|
|
751
|
-
await runWithJsonErrors(async () => {
|
|
752
|
-
const kind = parseKindFilter(args.kind);
|
|
753
|
-
const result = await akmListSources({ kind });
|
|
754
|
-
output("list", result);
|
|
755
|
-
});
|
|
756
|
-
},
|
|
757
|
-
});
|
|
758
|
-
const removeCommand = defineCommand({
|
|
759
|
-
meta: { name: "remove", description: "Remove a source by id, ref, path, URL, or name" },
|
|
760
|
-
args: {
|
|
761
|
-
target: { type: "positional", description: "Source to remove (id, ref, path, URL, or name)", required: true },
|
|
762
|
-
yes: { type: "boolean", alias: "y", description: "Skip confirmation prompt", default: false },
|
|
763
|
-
},
|
|
764
|
-
async run({ args }) {
|
|
765
|
-
await runWithJsonErrors(async () => {
|
|
766
|
-
const { confirmDestructive } = await import("./cli/confirm.js");
|
|
767
|
-
const confirmed = await confirmDestructive(`Remove source "${args.target}"? This cannot be undone.`, {
|
|
768
|
-
yes: args.yes === true,
|
|
769
|
-
});
|
|
770
|
-
if (!confirmed) {
|
|
771
|
-
process.stderr.write("Aborted.\n");
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
const result = await akmRemove({ target: args.target });
|
|
775
|
-
appendEvent({
|
|
776
|
-
eventType: "remove",
|
|
777
|
-
metadata: {
|
|
778
|
-
target: args.target,
|
|
779
|
-
ref: typeof result.removed?.ref === "string" ? result.removed.ref : null,
|
|
780
|
-
id: typeof result.removed?.id === "string" ? result.removed.id : null,
|
|
781
|
-
},
|
|
782
|
-
});
|
|
783
|
-
output("remove", result);
|
|
784
|
-
});
|
|
785
434
|
},
|
|
786
435
|
});
|
|
787
|
-
const
|
|
788
|
-
meta: {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
eventType: "update",
|
|
799
|
-
metadata: {
|
|
800
|
-
target: args.target ?? null,
|
|
801
|
-
all: args.all === true,
|
|
802
|
-
force: args.force === true,
|
|
803
|
-
processed: Array.isArray(result.processed)
|
|
804
|
-
? result.processed.length
|
|
805
|
-
: 0,
|
|
806
|
-
},
|
|
807
|
-
});
|
|
808
|
-
output("update", result);
|
|
809
|
-
});
|
|
436
|
+
export const main = defineCommand({
|
|
437
|
+
meta: {
|
|
438
|
+
name: "akm",
|
|
439
|
+
version: pkgVersion,
|
|
440
|
+
description: "Agent Knowledge Management — search, show, and manage assets from your stash.\n\n" +
|
|
441
|
+
"Exit codes:\n" +
|
|
442
|
+
" 0 success\n" +
|
|
443
|
+
" 1 general error / not found\n" +
|
|
444
|
+
" 2 usage error\n" +
|
|
445
|
+
" 4 health warn (akm health only)\n" +
|
|
446
|
+
" 78 config error",
|
|
810
447
|
},
|
|
811
|
-
});
|
|
812
|
-
const upgradeCommand = defineCommand({
|
|
813
|
-
meta: { name: "upgrade", description: "Upgrade akm to the latest release" },
|
|
814
448
|
args: {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
449
|
+
format: { type: "string", description: "Output format (json|jsonl|text|yaml|md|html)", default: "json" },
|
|
450
|
+
output: {
|
|
451
|
+
type: "string",
|
|
452
|
+
description: "Write rendered output to a file instead of stdout (all formats except jsonl)",
|
|
453
|
+
},
|
|
454
|
+
detail: {
|
|
455
|
+
type: "string",
|
|
456
|
+
description: "Detail level (verbosity): brief|normal|full. Default: brief.",
|
|
457
|
+
default: "brief",
|
|
458
|
+
},
|
|
459
|
+
shape: {
|
|
460
|
+
type: "string",
|
|
461
|
+
description: "Output projection: human|agent|summary. 'agent' trims to agent-essential fields; " +
|
|
462
|
+
"'summary' is only valid on 'akm show'. Default: human.",
|
|
463
|
+
},
|
|
464
|
+
quiet: {
|
|
818
465
|
type: "boolean",
|
|
819
|
-
|
|
466
|
+
alias: "q",
|
|
467
|
+
description: "Suppress non-essential stderr output (banners, spinners, progress info). " +
|
|
468
|
+
"Safety-critical output is never suppressed: errors, destructive-action confirmation prompts, " +
|
|
469
|
+
"and auto-migration banners always appear regardless of --quiet.",
|
|
820
470
|
default: false,
|
|
821
471
|
},
|
|
822
|
-
|
|
472
|
+
verbose: {
|
|
823
473
|
type: "boolean",
|
|
824
|
-
description: "
|
|
825
|
-
default: false,
|
|
826
|
-
},
|
|
827
|
-
},
|
|
828
|
-
async run({ args }) {
|
|
829
|
-
await runWithJsonErrors(async () => {
|
|
830
|
-
const check = await checkForUpdate(pkgVersion);
|
|
831
|
-
if (args.check) {
|
|
832
|
-
output("upgrade", check);
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
const skipChecksum = getHyphenatedBoolean(args, "skip-checksum");
|
|
836
|
-
const skipPostUpgrade = getHyphenatedBoolean(args, "skip-post-upgrade");
|
|
837
|
-
const result = await performUpgrade(check, { force: args.force, skipChecksum, skipPostUpgrade });
|
|
838
|
-
output("upgrade", result);
|
|
839
|
-
});
|
|
840
|
-
},
|
|
841
|
-
});
|
|
842
|
-
const showCommand = defineCommand({
|
|
843
|
-
meta: {
|
|
844
|
-
name: "show",
|
|
845
|
-
description: "Show a stash asset by ref (e.g. akm show knowledge:guide.md toc, akm show knowledge:guide.md section 'Auth')",
|
|
846
|
-
},
|
|
847
|
-
args: {
|
|
848
|
-
ref: {
|
|
849
|
-
type: "positional",
|
|
850
|
-
description: 'Asset ref ([origin//]type:name) optionally followed by a view mode. View modes: `toc` (table of contents), `section "Heading"` (extract one section), `lines <start> <end>` (line range), `frontmatter` (YAML metadata only), `full` (raw file). Example: `akm show knowledge:guide.md section "Auth"`.',
|
|
851
|
-
required: true,
|
|
852
|
-
},
|
|
853
|
-
format: { type: "string", description: "Output format (json|jsonl|text|yaml)" },
|
|
854
|
-
detail: { type: "string", description: "Detail level (brief|normal|full)" },
|
|
855
|
-
shape: { type: "string", description: "Output projection (human|agent|summary)" },
|
|
856
|
-
scope: {
|
|
857
|
-
type: "string",
|
|
858
|
-
description: "Scope filter (repeatable): --scope user=<id> --scope agent=<id> --scope run=<id> --scope channel=<name>. Narrows resolution to assets whose frontmatter scope matches.",
|
|
859
|
-
},
|
|
860
|
-
},
|
|
861
|
-
async run({ args }) {
|
|
862
|
-
await runWithJsonErrors(async () => {
|
|
863
|
-
const subcommand = Array.isArray(args._) ? args._[0] : undefined;
|
|
864
|
-
if (subcommand === "proposal") {
|
|
865
|
-
if (!isQuiet()) {
|
|
866
|
-
process.stderr.write("warning: 'akm show proposal <id>' is deprecated and will be removed in 0.9.0. Use 'akm proposal show <id>'.\n");
|
|
867
|
-
}
|
|
868
|
-
const proposalId = Array.isArray(args._) ? args._[1] : undefined;
|
|
869
|
-
if (typeof proposalId !== "string" || !proposalId.trim()) {
|
|
870
|
-
throw new UsageError("Usage: akm proposal show <id>", "MISSING_REQUIRED_ARGUMENT");
|
|
871
|
-
}
|
|
872
|
-
const result = akmProposalShow({ id: proposalId.trim() });
|
|
873
|
-
output("proposal-show", result);
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
// `[origin//]meta[:name]` targets the stash `.meta/` convention, which is
|
|
877
|
-
// not a typed asset ref — skip ref validation and let akmShowUnified
|
|
878
|
-
// direct-read it. (`parseAssetRef` would reject the non-type `meta`.)
|
|
879
|
-
if (!parseMetaRef(args.ref))
|
|
880
|
-
parseAssetRef(args.ref);
|
|
881
|
-
// The knowledge-view positional syntax (`akm show knowledge:foo section "Auth"`)
|
|
882
|
-
// is rewritten to `--akmView` / `--akmHeading` / `--akmStart` / `--akmEnd`
|
|
883
|
-
// by `normalizeShowArgv` before citty parses argv. We read those values
|
|
884
|
-
// directly via `parseFlagValue` so the flags don't surface as user-facing
|
|
885
|
-
// options in `akm show --help`.
|
|
886
|
-
const akmView = parseFlagValue(process.argv, "--akmView");
|
|
887
|
-
const akmHeading = parseFlagValue(process.argv, "--akmHeading");
|
|
888
|
-
const akmStart = parseFlagValue(process.argv, "--akmStart");
|
|
889
|
-
const akmEnd = parseFlagValue(process.argv, "--akmEnd");
|
|
890
|
-
let view;
|
|
891
|
-
if (akmView) {
|
|
892
|
-
switch (akmView) {
|
|
893
|
-
case "section":
|
|
894
|
-
view = { mode: "section", heading: akmHeading ?? "" };
|
|
895
|
-
break;
|
|
896
|
-
case "lines":
|
|
897
|
-
view = {
|
|
898
|
-
mode: "lines",
|
|
899
|
-
start: Number(akmStart ?? "1"),
|
|
900
|
-
end: akmEnd ? parseInt(akmEnd, 10) : Number.MAX_SAFE_INTEGER,
|
|
901
|
-
};
|
|
902
|
-
break;
|
|
903
|
-
case "toc":
|
|
904
|
-
case "frontmatter":
|
|
905
|
-
case "full":
|
|
906
|
-
view = { mode: akmView };
|
|
907
|
-
break;
|
|
908
|
-
default:
|
|
909
|
-
throw new UsageError(`Unknown view mode: ${akmView}. Expected one of: full|toc|frontmatter|section|lines`);
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
const cliShape = getOutputMode().shape;
|
|
913
|
-
const explicitDetail = parseFlagValue(process.argv, "--detail");
|
|
914
|
-
// `--shape summary` selects the compact metadata projection for show
|
|
915
|
-
// (the legacy `--detail summary` spelling still maps here via the
|
|
916
|
-
// back-compat path in resolveOutputMode). `--detail brief` forces the
|
|
917
|
-
// brief response regardless of shape.
|
|
918
|
-
const showDetail = explicitDetail === "brief" ? "brief" : cliShape === "summary" ? "summary" : undefined;
|
|
919
|
-
// `--scope` is repeatable — citty only exposes the last value, so read
|
|
920
|
-
// every occurrence directly from argv (same pattern as `--filter`).
|
|
921
|
-
const scopeTokens = parseAllFlagValues("--scope");
|
|
922
|
-
const scope = parseScopeFilterFlags(scopeTokens, "--scope");
|
|
923
|
-
const result = await akmShowUnified({
|
|
924
|
-
ref: args.ref,
|
|
925
|
-
view,
|
|
926
|
-
detail: showDetail,
|
|
927
|
-
scope,
|
|
928
|
-
eventSource: resolveEventSource(),
|
|
929
|
-
});
|
|
930
|
-
output("show", result);
|
|
931
|
-
});
|
|
932
|
-
},
|
|
933
|
-
});
|
|
934
|
-
const configCommand = defineCommand({
|
|
935
|
-
meta: { name: "config", description: "Show and manage configuration" },
|
|
936
|
-
args: {
|
|
937
|
-
list: { type: "boolean", description: "List current configuration", default: false },
|
|
938
|
-
},
|
|
939
|
-
subCommands: {
|
|
940
|
-
path: defineCommand({
|
|
941
|
-
meta: { name: "path", description: "Show paths to config, stash, cache, and index" },
|
|
942
|
-
args: {
|
|
943
|
-
all: { type: "boolean", description: "Show all paths (config, stash, cache, index)", default: false },
|
|
944
|
-
},
|
|
945
|
-
run({ args }) {
|
|
946
|
-
return runWithJsonErrors(() => {
|
|
947
|
-
const configPath = getConfigPath();
|
|
948
|
-
if (args.all) {
|
|
949
|
-
let stashDir;
|
|
950
|
-
try {
|
|
951
|
-
stashDir = resolveStashDir({ readOnly: true });
|
|
952
|
-
}
|
|
953
|
-
catch {
|
|
954
|
-
stashDir = `${getDefaultStashDir()} (not initialized)`;
|
|
955
|
-
}
|
|
956
|
-
const cacheDir = getCacheDir();
|
|
957
|
-
const result = {
|
|
958
|
-
config: configPath,
|
|
959
|
-
stash: stashDir,
|
|
960
|
-
cache: cacheDir,
|
|
961
|
-
index: getDbPath(),
|
|
962
|
-
};
|
|
963
|
-
output("config", result);
|
|
964
|
-
}
|
|
965
|
-
else {
|
|
966
|
-
console.log(configPath);
|
|
967
|
-
}
|
|
968
|
-
});
|
|
969
|
-
},
|
|
970
|
-
}),
|
|
971
|
-
list: defineCommand({
|
|
972
|
-
meta: { name: "list", description: "List current configuration" },
|
|
973
|
-
run() {
|
|
974
|
-
return runWithJsonErrors(() => {
|
|
975
|
-
output("config", listConfig(loadConfig()));
|
|
976
|
-
});
|
|
977
|
-
},
|
|
978
|
-
}),
|
|
979
|
-
show: defineCommand({
|
|
980
|
-
meta: { name: "show", description: "Alias for `akm config list` — list current configuration" },
|
|
981
|
-
run() {
|
|
982
|
-
return runWithJsonErrors(() => {
|
|
983
|
-
output("config", listConfig(loadConfig()));
|
|
984
|
-
});
|
|
985
|
-
},
|
|
986
|
-
}),
|
|
987
|
-
get: defineCommand({
|
|
988
|
-
meta: { name: "get", description: "Get a configuration value by key" },
|
|
989
|
-
args: {
|
|
990
|
-
key: { type: "positional", required: true, description: "Config key (for example: embedding, stashDir)" },
|
|
991
|
-
},
|
|
992
|
-
run({ args }) {
|
|
993
|
-
return runWithJsonErrors(() => {
|
|
994
|
-
output("config", getConfigValue(loadConfig(), args.key));
|
|
995
|
-
});
|
|
996
|
-
},
|
|
997
|
-
}),
|
|
998
|
-
set: defineCommand({
|
|
999
|
-
meta: { name: "set", description: "Set a configuration value by key" },
|
|
1000
|
-
args: {
|
|
1001
|
-
key: { type: "positional", required: true, description: "Config key (for example: embedding, llm)" },
|
|
1002
|
-
value: { type: "positional", required: true, description: "Config value" },
|
|
1003
|
-
// #463: stable machine-friendly entry point for plugins / hooks.
|
|
1004
|
-
// `--silent` suppresses the config dump on stdout so hook-driven
|
|
1005
|
-
// writes don't pollute their host's output stream.
|
|
1006
|
-
silent: {
|
|
1007
|
-
type: "boolean",
|
|
1008
|
-
description: "Suppress the post-write config dump on stdout. Use from hooks and CI scripts; the write still happens and errors still print.",
|
|
1009
|
-
default: false,
|
|
1010
|
-
},
|
|
1011
|
-
// #463: explicit layer flag for forward-compat. User layer is the only
|
|
1012
|
-
// settable layer today; the flag exists so plugin authors can encode
|
|
1013
|
-
// intent and the surface stays stable if project-layer writes return.
|
|
1014
|
-
layer: {
|
|
1015
|
-
type: "string",
|
|
1016
|
-
description: "Config layer to write to. Currently only `user` is supported.",
|
|
1017
|
-
default: "user",
|
|
1018
|
-
},
|
|
1019
|
-
},
|
|
1020
|
-
run({ args }) {
|
|
1021
|
-
return runWithJsonErrors(() => {
|
|
1022
|
-
if (args.layer && args.layer !== "user") {
|
|
1023
|
-
throw new UsageError(`Unsupported --layer "${args.layer}". Only "user" is settable in 0.8.0.`, "INVALID_FLAG_VALUE");
|
|
1024
|
-
}
|
|
1025
|
-
// Use loadConfig (not loadUserConfig) so the project-config
|
|
1026
|
-
// deprecation warning fires consistently with `akm config get`
|
|
1027
|
-
// (#457). Effective merged shape is identical post-0.8.0.
|
|
1028
|
-
const updated = setConfigValue(loadConfig(), args.key, args.value);
|
|
1029
|
-
saveConfig(updated);
|
|
1030
|
-
if (!args.silent) {
|
|
1031
|
-
output("config", listConfig(updated));
|
|
1032
|
-
}
|
|
1033
|
-
});
|
|
1034
|
-
},
|
|
1035
|
-
}),
|
|
1036
|
-
unset: defineCommand({
|
|
1037
|
-
meta: { name: "unset", description: "Unset an optional configuration key or whole embedding/llm section" },
|
|
1038
|
-
args: {
|
|
1039
|
-
key: { type: "positional", required: true, description: "Config key to unset" },
|
|
1040
|
-
silent: {
|
|
1041
|
-
type: "boolean",
|
|
1042
|
-
description: "Suppress the post-write config dump on stdout.",
|
|
1043
|
-
default: false,
|
|
1044
|
-
},
|
|
1045
|
-
layer: {
|
|
1046
|
-
type: "string",
|
|
1047
|
-
description: "Config layer to write to. Currently only `user` is supported.",
|
|
1048
|
-
default: "user",
|
|
1049
|
-
},
|
|
1050
|
-
},
|
|
1051
|
-
run({ args }) {
|
|
1052
|
-
return runWithJsonErrors(() => {
|
|
1053
|
-
if (args.layer && args.layer !== "user") {
|
|
1054
|
-
throw new UsageError(`Unsupported --layer "${args.layer}". Only "user" is settable in 0.8.0.`, "INVALID_FLAG_VALUE");
|
|
1055
|
-
}
|
|
1056
|
-
const updated = unsetConfigValue(loadConfig(), args.key);
|
|
1057
|
-
saveConfig(updated);
|
|
1058
|
-
if (!args.silent) {
|
|
1059
|
-
output("config", listConfig(updated));
|
|
1060
|
-
}
|
|
1061
|
-
});
|
|
1062
|
-
},
|
|
1063
|
-
}),
|
|
1064
|
-
validate: defineCommand({
|
|
1065
|
-
meta: {
|
|
1066
|
-
name: "validate",
|
|
1067
|
-
description: "Validate the on-disk config file against the schema. Exits non-zero on errors.",
|
|
1068
|
-
},
|
|
1069
|
-
async run() {
|
|
1070
|
-
return runWithJsonErrors(async () => {
|
|
1071
|
-
const { runConfigValidate } = await import("./cli/config-validate.js");
|
|
1072
|
-
await runConfigValidate();
|
|
1073
|
-
});
|
|
1074
|
-
},
|
|
1075
|
-
}),
|
|
1076
|
-
migrate: defineCommand({
|
|
1077
|
-
meta: {
|
|
1078
|
-
name: "migrate",
|
|
1079
|
-
description: "Migrate the config file to the current schema version. Use --dry-run to preview without writing.",
|
|
1080
|
-
},
|
|
1081
|
-
args: {
|
|
1082
|
-
"dry-run": { type: "boolean", description: "Preview the migration result without writing.", default: false },
|
|
1083
|
-
"print-diff": {
|
|
1084
|
-
type: "boolean",
|
|
1085
|
-
description: "Print a unified diff of old vs new config alongside the migration output.",
|
|
1086
|
-
default: false,
|
|
1087
|
-
},
|
|
1088
|
-
},
|
|
1089
|
-
async run({ args }) {
|
|
1090
|
-
return runWithJsonErrors(async () => {
|
|
1091
|
-
const { runConfigMigrate } = await import("./cli/config-migrate.js");
|
|
1092
|
-
await runConfigMigrate({ dryRun: Boolean(args["dry-run"]), printDiff: Boolean(args["print-diff"]) });
|
|
1093
|
-
});
|
|
1094
|
-
},
|
|
1095
|
-
}),
|
|
1096
|
-
enable: defineCommand({
|
|
1097
|
-
meta: { name: "enable", description: "Enable an optional component (skills.sh)" },
|
|
1098
|
-
args: {
|
|
1099
|
-
target: { type: "positional", description: "Component to enable (skills.sh)", required: true },
|
|
1100
|
-
},
|
|
1101
|
-
run({ args }) {
|
|
1102
|
-
return runWithJsonErrors(() => {
|
|
1103
|
-
const result = toggleComponent(args.target, true);
|
|
1104
|
-
output("enable", result);
|
|
1105
|
-
});
|
|
1106
|
-
},
|
|
1107
|
-
}),
|
|
1108
|
-
disable: defineCommand({
|
|
1109
|
-
meta: { name: "disable", description: "Disable an optional component (skills.sh)" },
|
|
1110
|
-
args: {
|
|
1111
|
-
target: { type: "positional", description: "Component to disable (skills.sh)", required: true },
|
|
1112
|
-
},
|
|
1113
|
-
run({ args }) {
|
|
1114
|
-
return runWithJsonErrors(() => {
|
|
1115
|
-
const result = toggleComponent(args.target, false);
|
|
1116
|
-
output("disable", result);
|
|
1117
|
-
});
|
|
1118
|
-
},
|
|
1119
|
-
}),
|
|
1120
|
-
},
|
|
1121
|
-
run({ args }) {
|
|
1122
|
-
return runWithJsonErrors(() => {
|
|
1123
|
-
if (hasSubcommand(args, CONFIG_SUBCOMMAND_SET))
|
|
1124
|
-
return;
|
|
1125
|
-
if (args.list) {
|
|
1126
|
-
output("config", listConfig(loadConfig()));
|
|
1127
|
-
return;
|
|
1128
|
-
}
|
|
1129
|
-
output("config", listConfig(loadConfig()));
|
|
1130
|
-
});
|
|
1131
|
-
},
|
|
1132
|
-
});
|
|
1133
|
-
// Shared `save`/`sync` body. `sync` is the canonical spelling in 0.8; `save`
|
|
1134
|
-
// remains a deprecated alias (removed 0.9.0). Both share this implementation so
|
|
1135
|
-
// the git-commit/push logic and the `--format`-as-name workaround stay in one place.
|
|
1136
|
-
async function runSyncBody(args, verb) {
|
|
1137
|
-
await runWithJsonErrors(async () => {
|
|
1138
|
-
// Fix: citty can consume `--format json` (space-separated) as the
|
|
1139
|
-
// positional `name` argument (e.g. `akm sync --format json` parses
|
|
1140
|
-
// name="json"). Detect the mis-parse by checking argv order — only
|
|
1141
|
-
// treat the positional as consumed by --format when --format appears
|
|
1142
|
-
// before any standalone occurrence of the same value in the sync
|
|
1143
|
-
// subcommand's argv slice. This preserves legitimate invocations
|
|
1144
|
-
// like `akm sync json --format json`.
|
|
1145
|
-
const parsedFormat = parseFlagValue(process.argv, "--format");
|
|
1146
|
-
const effectiveName = args.name !== undefined &&
|
|
1147
|
-
parsedFormat !== undefined &&
|
|
1148
|
-
args.name === parsedFormat &&
|
|
1149
|
-
wasFormatValueConsumedAsName(args.name, parsedFormat, verb)
|
|
1150
|
-
? undefined
|
|
1151
|
-
: args.name;
|
|
1152
|
-
let writable;
|
|
1153
|
-
if (effectiveName === undefined) {
|
|
1154
|
-
// Primary stash — honour the root-level writable flag from config.
|
|
1155
|
-
writable = resolveWritableOverride(loadConfig());
|
|
1156
|
-
}
|
|
1157
|
-
const result = saveGitStash(effectiveName, args.message, writable, { push: args.push !== false });
|
|
1158
|
-
appendEvent({
|
|
1159
|
-
eventType: "save",
|
|
1160
|
-
metadata: {
|
|
1161
|
-
name: effectiveName ?? null,
|
|
1162
|
-
message: args.message ?? null,
|
|
1163
|
-
ok: result.ok !== false,
|
|
1164
|
-
},
|
|
1165
|
-
});
|
|
1166
|
-
output("save", result);
|
|
1167
|
-
});
|
|
1168
|
-
}
|
|
1169
|
-
const syncCommand = defineCommand({
|
|
1170
|
-
meta: {
|
|
1171
|
-
name: "sync",
|
|
1172
|
-
description: "Sync changes in a git-backed stash: commits (and pushes when writable + remote is configured). No-op for non-git stashes.",
|
|
1173
|
-
},
|
|
1174
|
-
args: {
|
|
1175
|
-
name: {
|
|
1176
|
-
type: "positional",
|
|
1177
|
-
description: "Name of the git stash to sync (default: primary stash directory)",
|
|
1178
|
-
required: false,
|
|
1179
|
-
},
|
|
1180
|
-
message: {
|
|
1181
|
-
type: "string",
|
|
1182
|
-
alias: "m",
|
|
1183
|
-
description: "Commit message (default: timestamp)",
|
|
1184
|
-
},
|
|
1185
|
-
push: {
|
|
1186
|
-
type: "boolean",
|
|
1187
|
-
description: "Push after commit when writable + remote configured (use --no-push to commit only). Default: true.",
|
|
1188
|
-
default: true,
|
|
1189
|
-
},
|
|
1190
|
-
},
|
|
1191
|
-
async run({ args }) {
|
|
1192
|
-
await runSyncBody(args, "sync");
|
|
1193
|
-
},
|
|
1194
|
-
});
|
|
1195
|
-
// Deprecated alias (removed 0.9.0): `akm save` → `akm sync`.
|
|
1196
|
-
const saveCommand = defineCommand({
|
|
1197
|
-
meta: {
|
|
1198
|
-
name: "save",
|
|
1199
|
-
description: "DEPRECATED — use `akm sync`. Removed in 0.9.0.",
|
|
1200
|
-
},
|
|
1201
|
-
args: {
|
|
1202
|
-
name: {
|
|
1203
|
-
type: "positional",
|
|
1204
|
-
description: "Name of the git stash to save (default: primary stash directory)",
|
|
1205
|
-
required: false,
|
|
1206
|
-
},
|
|
1207
|
-
message: {
|
|
1208
|
-
type: "string",
|
|
1209
|
-
alias: "m",
|
|
1210
|
-
description: "Commit message (default: timestamp)",
|
|
1211
|
-
},
|
|
1212
|
-
push: {
|
|
1213
|
-
type: "boolean",
|
|
1214
|
-
description: "Push after commit when writable + remote configured (use --no-push to commit only). Default: true.",
|
|
1215
|
-
default: true,
|
|
1216
|
-
},
|
|
1217
|
-
},
|
|
1218
|
-
async run({ args }) {
|
|
1219
|
-
emitCommandDeprecation("save", "sync");
|
|
1220
|
-
await runSyncBody(args, "save");
|
|
1221
|
-
},
|
|
1222
|
-
});
|
|
1223
|
-
/**
|
|
1224
|
-
* Detect whether `--format <value>` was consumed by citty as the optional
|
|
1225
|
-
* `name` positional of `akm save`. Returns true only when `--format` appears
|
|
1226
|
-
* in the save subcommand's argv slice AND the candidate name does NOT
|
|
1227
|
-
* appear as a standalone positional elsewhere (before or after the flag).
|
|
1228
|
-
*
|
|
1229
|
-
* This keeps `akm sync json --format json` routing `json` as the stash name,
|
|
1230
|
-
* while `akm sync --format json` (no separate positional) is treated as a
|
|
1231
|
-
* primary-stash sync. `verb` is the subcommand token to anchor on (`sync` or
|
|
1232
|
-
* the deprecated `save`).
|
|
1233
|
-
*/
|
|
1234
|
-
function wasFormatValueConsumedAsName(name, formatValue, verb) {
|
|
1235
|
-
const argv = process.argv.slice(2);
|
|
1236
|
-
const verbIndex = argv.indexOf(verb);
|
|
1237
|
-
const tokens = verbIndex >= 0 ? argv.slice(verbIndex + 1) : argv;
|
|
1238
|
-
let formatIndex = -1;
|
|
1239
|
-
let formatConsumesNextToken = false;
|
|
1240
|
-
for (let i = 0; i < tokens.length; i += 1) {
|
|
1241
|
-
const token = tokens[i];
|
|
1242
|
-
if (token === "--format") {
|
|
1243
|
-
formatIndex = i;
|
|
1244
|
-
formatConsumesNextToken = true;
|
|
1245
|
-
break;
|
|
1246
|
-
}
|
|
1247
|
-
if (token === `--format=${formatValue}`) {
|
|
1248
|
-
formatIndex = i;
|
|
1249
|
-
break;
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
if (formatIndex === -1)
|
|
1253
|
-
return false;
|
|
1254
|
-
// If the name appears as a standalone token before --format, it's the
|
|
1255
|
-
// real positional and --format did not consume it.
|
|
1256
|
-
if (tokens.slice(0, formatIndex).includes(name))
|
|
1257
|
-
return false;
|
|
1258
|
-
// If --format has a space-separated value, skip past the value token
|
|
1259
|
-
// when scanning after the flag; otherwise start right after the flag.
|
|
1260
|
-
const firstTokenAfterFormat = formatIndex + (formatConsumesNextToken ? 2 : 1);
|
|
1261
|
-
if (tokens.slice(firstTokenAfterFormat).includes(name))
|
|
1262
|
-
return false;
|
|
1263
|
-
return true;
|
|
1264
|
-
}
|
|
1265
|
-
const cloneCommand = defineCommand({
|
|
1266
|
-
meta: {
|
|
1267
|
-
name: "clone",
|
|
1268
|
-
description: "Clone an asset from any source into the working stash or a custom destination",
|
|
1269
|
-
},
|
|
1270
|
-
args: {
|
|
1271
|
-
ref: { type: "positional", description: "Asset ref (e.g. npm:@scope/pkg//script:deploy.sh)", required: true },
|
|
1272
|
-
name: { type: "string", description: "New name for the cloned asset" },
|
|
1273
|
-
force: { type: "boolean", description: "Overwrite if asset already exists in working stash", default: false },
|
|
1274
|
-
dest: { type: "string", description: "Destination directory (default: working stash)" },
|
|
1275
|
-
},
|
|
1276
|
-
async run({ args }) {
|
|
1277
|
-
await runWithJsonErrors(async () => {
|
|
1278
|
-
const result = await akmClone({
|
|
1279
|
-
sourceRef: args.ref,
|
|
1280
|
-
newName: args.name,
|
|
1281
|
-
force: args.force,
|
|
1282
|
-
dest: args.dest,
|
|
1283
|
-
});
|
|
1284
|
-
output("clone", result);
|
|
1285
|
-
});
|
|
1286
|
-
},
|
|
1287
|
-
});
|
|
1288
|
-
const historyCommand = defineCommand({
|
|
1289
|
-
meta: {
|
|
1290
|
-
name: "history",
|
|
1291
|
-
description: "Show mutation/usage history for a single asset (--ref) or stash-wide.\n\n" +
|
|
1292
|
-
"Event sources:\n" +
|
|
1293
|
-
" usage_events (default): search, show, and feedback events from the local index.\n" +
|
|
1294
|
-
" state.db events (--include-proposals): proposal lifecycle events (promoted, rejected)\n" +
|
|
1295
|
-
" emitted by `akm accept` / `akm reject`.\n\n" +
|
|
1296
|
-
"Results from all active sources are merged and sorted chronologically.",
|
|
1297
|
-
},
|
|
1298
|
-
args: {
|
|
1299
|
-
ref: { type: "string", description: "Asset ref (type:name). Omit for stash-wide history." },
|
|
1300
|
-
since: { type: "string", description: "ISO timestamp or epoch ms — only events on/after this time" },
|
|
1301
|
-
generator: {
|
|
1302
|
-
type: "string",
|
|
1303
|
-
description: 'Filter by event generator: "user" (default) or "improve" (akm improve operations).',
|
|
1304
|
-
},
|
|
1305
|
-
source: {
|
|
1306
|
-
type: "string",
|
|
1307
|
-
description: "DEPRECATED — use --generator. Removed in 0.9.0.",
|
|
1308
|
-
},
|
|
1309
|
-
"include-proposals": {
|
|
1310
|
-
type: "boolean",
|
|
1311
|
-
description: "Also include proposal lifecycle events (promoted, rejected) from state.db events. " +
|
|
1312
|
-
"Default: false (usage_events only).",
|
|
1313
|
-
default: false,
|
|
1314
|
-
},
|
|
1315
|
-
"accept-rate-by-source": {
|
|
1316
|
-
type: "boolean",
|
|
1317
|
-
description: "Compute accept-rate-per-source metrics from the proposal store and include them in the output (F-4 / #385). " +
|
|
1318
|
-
"Useful for measuring which generators (reflect, distill, …) produce the most accepted proposals.",
|
|
1319
|
-
default: false,
|
|
1320
|
-
},
|
|
1321
|
-
format: { type: "string", description: "Output format (json|jsonl|text|yaml)" },
|
|
1322
|
-
},
|
|
1323
|
-
run({ args }) {
|
|
1324
|
-
return runWithJsonErrors(async () => {
|
|
1325
|
-
if (args.generator === undefined && args.source !== undefined) {
|
|
1326
|
-
emitFlagDeprecation("--source", "--generator", "history");
|
|
1327
|
-
}
|
|
1328
|
-
const generatorFlag = (args.generator ?? args.source);
|
|
1329
|
-
if (generatorFlag !== undefined && generatorFlag !== "user" && generatorFlag !== "improve") {
|
|
1330
|
-
// Name the flag the user actually typed so the diagnostic points at
|
|
1331
|
-
// their command line, not the canonical flag they may not have used.
|
|
1332
|
-
const usedFlag = args.generator !== undefined ? "--generator" : "--source";
|
|
1333
|
-
throw new UsageError(`Invalid ${usedFlag} value: "${generatorFlag}". Must be "user" or "improve".`, "INVALID_FLAG_VALUE");
|
|
1334
|
-
}
|
|
1335
|
-
const sources = resolveSourceEntries();
|
|
1336
|
-
const stashDir = sources[0]?.path;
|
|
1337
|
-
const result = await akmHistory({
|
|
1338
|
-
ref: args.ref,
|
|
1339
|
-
since: args.since,
|
|
1340
|
-
source: generatorFlag,
|
|
1341
|
-
includeProposals: args["include-proposals"],
|
|
1342
|
-
acceptRateBySource: args["accept-rate-by-source"],
|
|
1343
|
-
stashDir,
|
|
1344
|
-
});
|
|
1345
|
-
output("history", result);
|
|
1346
|
-
});
|
|
1347
|
-
},
|
|
1348
|
-
});
|
|
1349
|
-
const workflowStartCommand = defineCommand({
|
|
1350
|
-
meta: {
|
|
1351
|
-
name: "start",
|
|
1352
|
-
description: "Start a new workflow run in the current working scope",
|
|
1353
|
-
},
|
|
1354
|
-
args: {
|
|
1355
|
-
ref: { type: "positional", description: "Workflow ref (workflow:<name>)", required: true },
|
|
1356
|
-
params: { type: "string", description: "Workflow parameters as a JSON object" },
|
|
1357
|
-
force: {
|
|
1358
|
-
type: "boolean",
|
|
1359
|
-
description: "Allow a parallel run when an active run already exists in this scope (#485)",
|
|
1360
|
-
default: false,
|
|
1361
|
-
},
|
|
1362
|
-
},
|
|
1363
|
-
async run({ args }) {
|
|
1364
|
-
await runWithJsonErrors(async () => {
|
|
1365
|
-
const result = await startWorkflowRun(args.ref, parseWorkflowJsonObject(args.params, "--params"), {
|
|
1366
|
-
force: args.force === true,
|
|
1367
|
-
});
|
|
1368
|
-
output("workflow-start", result);
|
|
1369
|
-
});
|
|
1370
|
-
},
|
|
1371
|
-
});
|
|
1372
|
-
const workflowNextCommand = defineCommand({
|
|
1373
|
-
meta: {
|
|
1374
|
-
name: "next",
|
|
1375
|
-
description: "Show the next actionable workflow step in the current scope, auto-starting a run when passed a workflow ref",
|
|
1376
|
-
},
|
|
1377
|
-
args: {
|
|
1378
|
-
target: { type: "positional", description: "Workflow run id or workflow ref", required: true },
|
|
1379
|
-
params: { type: "string", description: "Workflow parameters as a JSON object (only for auto-started runs)" },
|
|
1380
|
-
},
|
|
1381
|
-
async run({ args }) {
|
|
1382
|
-
await runWithJsonErrors(async () => {
|
|
1383
|
-
// `--dry-run` is intentionally NOT a declared arg (so it stays out of
|
|
1384
|
-
// --help). The guard reads it straight from process.argv so existing
|
|
1385
|
-
// callers still get a clear, actionable error instead of a generic
|
|
1386
|
-
// "unknown flag" from citty.
|
|
1387
|
-
if (hasBooleanFlag(process.argv, "--dry-run")) {
|
|
1388
|
-
throw new UsageError("`akm workflow next` does not support --dry-run. Remove the flag to start or resume a run.", "INVALID_FLAG_VALUE");
|
|
1389
|
-
}
|
|
1390
|
-
const parsedParams = args.params ? parseWorkflowJsonObject(args.params, "--params") : undefined;
|
|
1391
|
-
// If the target looks like a UUID-style run id (no `:` and matches the
|
|
1392
|
-
// run-id shape), short-circuit with a structured WORKFLOW_NOT_FOUND
|
|
1393
|
-
// error before parseAssetRef gets to throw an unhelpful ref-parse error.
|
|
1394
|
-
if (looksLikeWorkflowRunId(args.target)) {
|
|
1395
|
-
const { hasWorkflowRun } = await import("./workflows/runs.js");
|
|
1396
|
-
if (!(await hasWorkflowRun(args.target))) {
|
|
1397
|
-
throw new NotFoundError(`Workflow run "${args.target}" not found.`, "WORKFLOW_NOT_FOUND", "Run `akm workflow list --active` to see runs.");
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
const result = await getNextWorkflowStep(args.target, parsedParams);
|
|
1401
|
-
output("workflow-next", result);
|
|
1402
|
-
});
|
|
1403
|
-
},
|
|
1404
|
-
});
|
|
1405
|
-
/**
|
|
1406
|
-
* Heuristic: a workflow run id is a UUID-shaped or hex-id-shaped string with
|
|
1407
|
-
* no `:` separator (refs always contain a colon: `workflow:<name>` or
|
|
1408
|
-
* `<origin>//workflow:<name>`). When this matches we can give a much better
|
|
1409
|
-
* error than parseAssetRef's "Invalid asset type" failure.
|
|
1410
|
-
*/
|
|
1411
|
-
function looksLikeWorkflowRunId(target) {
|
|
1412
|
-
if (target.includes(":"))
|
|
1413
|
-
return false;
|
|
1414
|
-
if (target.includes("/"))
|
|
1415
|
-
return false;
|
|
1416
|
-
// UUID v4-ish: 8-4-4-4-12 hex digits separated by dashes.
|
|
1417
|
-
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(target))
|
|
1418
|
-
return true;
|
|
1419
|
-
// Bare hex/alphanumeric run ids of >=8 chars (covers shortened ids).
|
|
1420
|
-
if (/^[0-9a-z][0-9a-z_-]{7,}$/i.test(target) && /[0-9]/.test(target))
|
|
1421
|
-
return true;
|
|
1422
|
-
return false;
|
|
1423
|
-
}
|
|
1424
|
-
const workflowCompleteCommand = defineCommand({
|
|
1425
|
-
meta: {
|
|
1426
|
-
name: "complete",
|
|
1427
|
-
description: "Update a workflow step state and persist notes/evidence",
|
|
1428
|
-
},
|
|
1429
|
-
args: {
|
|
1430
|
-
runId: { type: "positional", description: "Workflow run id", required: true },
|
|
1431
|
-
step: { type: "string", description: "Workflow step id", required: true },
|
|
1432
|
-
state: {
|
|
1433
|
-
type: "string",
|
|
1434
|
-
description: `Step state (default: completed). One of: ${WORKFLOW_STEP_STATES.join(", ")}.`,
|
|
1435
|
-
},
|
|
1436
|
-
notes: { type: "string", description: "Notes for the completed step" },
|
|
1437
|
-
evidence: { type: "string", description: "Evidence JSON object for the step" },
|
|
1438
|
-
},
|
|
1439
|
-
async run({ args }) {
|
|
1440
|
-
await runWithJsonErrors(async () => {
|
|
1441
|
-
const result = await completeWorkflowStep({
|
|
1442
|
-
runId: args.runId,
|
|
1443
|
-
stepId: args.step,
|
|
1444
|
-
status: parseWorkflowStepState(args.state),
|
|
1445
|
-
notes: args.notes,
|
|
1446
|
-
evidence: args.evidence ? parseWorkflowJsonObject(args.evidence, "--evidence") : undefined,
|
|
1447
|
-
});
|
|
1448
|
-
output("workflow-complete", result);
|
|
1449
|
-
});
|
|
1450
|
-
},
|
|
1451
|
-
});
|
|
1452
|
-
const workflowStatusCommand = defineCommand({
|
|
1453
|
-
meta: {
|
|
1454
|
-
name: "status",
|
|
1455
|
-
description: "Show full workflow run state for review or resume; workflow refs resolve within the current scope",
|
|
1456
|
-
},
|
|
1457
|
-
args: {
|
|
1458
|
-
target: { type: "positional", description: "Workflow run id or workflow ref (workflow:<name>)", required: true },
|
|
1459
|
-
},
|
|
1460
|
-
run({ args }) {
|
|
1461
|
-
return runWithJsonErrors(async () => {
|
|
1462
|
-
const target = args.target;
|
|
1463
|
-
// Check if target looks like a workflow ref
|
|
1464
|
-
const parsed = (() => {
|
|
1465
|
-
try {
|
|
1466
|
-
return parseAssetRef(target);
|
|
1467
|
-
}
|
|
1468
|
-
catch {
|
|
1469
|
-
return null;
|
|
1470
|
-
}
|
|
1471
|
-
})();
|
|
1472
|
-
if (parsed?.type === "workflow") {
|
|
1473
|
-
const ref = `${parsed.origin ? `${parsed.origin}//` : ""}workflow:${parsed.name}`;
|
|
1474
|
-
const { runs } = await listWorkflowRuns({ workflowRef: ref });
|
|
1475
|
-
if (runs.length === 0) {
|
|
1476
|
-
throw new NotFoundError(`No workflow runs found for ${ref}`, "WORKFLOW_NOT_FOUND");
|
|
1477
|
-
}
|
|
1478
|
-
const mostRecent = runs[0];
|
|
1479
|
-
if (!mostRecent)
|
|
1480
|
-
throw new NotFoundError(`No workflow runs found for ${ref}`, "WORKFLOW_NOT_FOUND");
|
|
1481
|
-
const result = await getWorkflowStatus(mostRecent.id);
|
|
1482
|
-
output("workflow-status", result);
|
|
1483
|
-
}
|
|
1484
|
-
else {
|
|
1485
|
-
const result = await getWorkflowStatus(target);
|
|
1486
|
-
output("workflow-status", result);
|
|
1487
|
-
}
|
|
1488
|
-
});
|
|
1489
|
-
},
|
|
1490
|
-
});
|
|
1491
|
-
const workflowListCommand = defineCommand({
|
|
1492
|
-
meta: {
|
|
1493
|
-
name: "list",
|
|
1494
|
-
description: "List workflow runs in the current working scope",
|
|
1495
|
-
},
|
|
1496
|
-
args: {
|
|
1497
|
-
ref: { type: "string", description: "Filter to one workflow ref" },
|
|
1498
|
-
active: { type: "boolean", description: "Only show active runs", default: false },
|
|
1499
|
-
},
|
|
1500
|
-
run({ args }) {
|
|
1501
|
-
return runWithJsonErrors(async () => {
|
|
1502
|
-
const result = await listWorkflowRuns({ workflowRef: args.ref, activeOnly: args.active });
|
|
1503
|
-
output("workflow-list", result);
|
|
1504
|
-
});
|
|
1505
|
-
},
|
|
1506
|
-
});
|
|
1507
|
-
const workflowCreateCommand = defineCommand({
|
|
1508
|
-
meta: {
|
|
1509
|
-
name: "create",
|
|
1510
|
-
description: "Create a workflow markdown document in the working stash",
|
|
1511
|
-
},
|
|
1512
|
-
args: {
|
|
1513
|
-
name: { type: "positional", description: "Workflow name", required: true },
|
|
1514
|
-
from: { type: "string", description: "Import and validate markdown from an existing file" },
|
|
1515
|
-
force: {
|
|
1516
|
-
type: "boolean",
|
|
1517
|
-
description: "Overwrite an existing workflow (requires --from or --reset)",
|
|
1518
|
-
default: false,
|
|
1519
|
-
},
|
|
1520
|
-
reset: {
|
|
1521
|
-
type: "boolean",
|
|
1522
|
-
description: "Explicitly replace an existing workflow with a fresh template (use with --force)",
|
|
1523
|
-
default: false,
|
|
1524
|
-
},
|
|
1525
|
-
},
|
|
1526
|
-
async run({ args }) {
|
|
1527
|
-
return runWithJsonErrors(async () => {
|
|
1528
|
-
const namePattern = /^[a-z0-9][a-z0-9._/-]*$/;
|
|
1529
|
-
if (!namePattern.test(args.name)) {
|
|
1530
|
-
throw new UsageError("Workflow name must start with a lowercase letter or digit and contain only lowercase letters, digits, hyphens, dots, underscores, and slashes.");
|
|
1531
|
-
}
|
|
1532
|
-
if (args.force && !args.from && !args.reset) {
|
|
1533
|
-
throw new UsageError("Refusing to overwrite with template: pass --from <file> to replace content, or --reset to explicitly replace with a fresh template.");
|
|
1534
|
-
}
|
|
1535
|
-
const result = createWorkflowAsset({
|
|
1536
|
-
name: args.name,
|
|
1537
|
-
from: args.from,
|
|
1538
|
-
force: args.force,
|
|
1539
|
-
});
|
|
1540
|
-
// Index the newly-written workflow so `akm workflow start` can resolve
|
|
1541
|
-
// a workflowEntryId without requiring an explicit `akm index` call
|
|
1542
|
-
// first. Uses the same incremental index path that `akm add` uses.
|
|
1543
|
-
await akmIndex({ stashDir: result.stashDir });
|
|
1544
|
-
output("workflow-create", { ok: true, ...result });
|
|
1545
|
-
});
|
|
1546
|
-
},
|
|
1547
|
-
});
|
|
1548
|
-
const workflowTemplateCommand = defineCommand({
|
|
1549
|
-
meta: {
|
|
1550
|
-
name: "template",
|
|
1551
|
-
description: "Print a valid workflow markdown template",
|
|
1552
|
-
},
|
|
1553
|
-
run() {
|
|
1554
|
-
process.stdout.write(getWorkflowTemplate());
|
|
1555
|
-
},
|
|
1556
|
-
});
|
|
1557
|
-
const workflowValidateCommand = defineCommand({
|
|
1558
|
-
meta: {
|
|
1559
|
-
name: "validate",
|
|
1560
|
-
description: "Validate a workflow markdown file or ref and print any errors",
|
|
1561
|
-
},
|
|
1562
|
-
args: {
|
|
1563
|
-
target: {
|
|
1564
|
-
type: "positional",
|
|
1565
|
-
description: "Workflow ref (workflow:<name>) or filesystem path to a workflow .md",
|
|
1566
|
-
required: true,
|
|
1567
|
-
},
|
|
1568
|
-
},
|
|
1569
|
-
async run({ args }) {
|
|
1570
|
-
return runWithJsonErrors(async () => {
|
|
1571
|
-
const filePath = await resolveWorkflowFilePath(args.target);
|
|
1572
|
-
const { parse } = validateWorkflowSource(filePath);
|
|
1573
|
-
if (parse.ok) {
|
|
1574
|
-
output("workflow-validate", {
|
|
1575
|
-
ok: true,
|
|
1576
|
-
path: filePath,
|
|
1577
|
-
title: parse.document.title,
|
|
1578
|
-
stepCount: parse.document.steps.length,
|
|
1579
|
-
});
|
|
1580
|
-
return;
|
|
1581
|
-
}
|
|
1582
|
-
throw new UsageError(formatWorkflowErrors(filePath, parse.errors));
|
|
1583
|
-
});
|
|
1584
|
-
},
|
|
1585
|
-
});
|
|
1586
|
-
async function resolveWorkflowFilePath(target) {
|
|
1587
|
-
if (!target.startsWith("workflow:"))
|
|
1588
|
-
return target;
|
|
1589
|
-
const parsed = parseAssetRef(target);
|
|
1590
|
-
if (parsed.type !== "workflow") {
|
|
1591
|
-
throw new UsageError(`Expected a workflow ref (workflow:<name>), got "${target}".`);
|
|
1592
|
-
}
|
|
1593
|
-
const config = loadConfig();
|
|
1594
|
-
const allSources = resolveSourceEntries(undefined, config);
|
|
1595
|
-
const searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
|
|
1596
|
-
for (const source of searchSources) {
|
|
1597
|
-
try {
|
|
1598
|
-
return await resolveAssetPath(source.path, "workflow", parsed.name);
|
|
1599
|
-
}
|
|
1600
|
-
catch {
|
|
1601
|
-
/* try next source */
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
throw new UsageError(`Workflow not found for ref: workflow:${parsed.name}`);
|
|
1605
|
-
}
|
|
1606
|
-
const workflowResumeCommand = defineCommand({
|
|
1607
|
-
meta: {
|
|
1608
|
-
name: "resume",
|
|
1609
|
-
description: "Resume a blocked or failed workflow run, flipping it back to active",
|
|
1610
|
-
},
|
|
1611
|
-
args: {
|
|
1612
|
-
runId: { type: "positional", description: "Workflow run id", required: true },
|
|
1613
|
-
},
|
|
1614
|
-
run({ args }) {
|
|
1615
|
-
return runWithJsonErrors(async () => {
|
|
1616
|
-
const result = await resumeWorkflowRun(args.runId);
|
|
1617
|
-
output("workflow-resume", result);
|
|
1618
|
-
});
|
|
1619
|
-
},
|
|
1620
|
-
});
|
|
1621
|
-
const workflowCommand = defineCommand({
|
|
1622
|
-
meta: {
|
|
1623
|
-
name: "workflow",
|
|
1624
|
-
description: "Author, inspect, and execute step-by-step workflow assets",
|
|
1625
|
-
},
|
|
1626
|
-
subCommands: {
|
|
1627
|
-
start: workflowStartCommand,
|
|
1628
|
-
next: workflowNextCommand,
|
|
1629
|
-
complete: workflowCompleteCommand,
|
|
1630
|
-
status: workflowStatusCommand,
|
|
1631
|
-
list: workflowListCommand,
|
|
1632
|
-
create: workflowCreateCommand,
|
|
1633
|
-
template: workflowTemplateCommand,
|
|
1634
|
-
resume: workflowResumeCommand,
|
|
1635
|
-
validate: workflowValidateCommand,
|
|
1636
|
-
},
|
|
1637
|
-
run({ args }) {
|
|
1638
|
-
return runWithJsonErrors(async () => {
|
|
1639
|
-
if (hasWorkflowSubcommand(args))
|
|
1640
|
-
return;
|
|
1641
|
-
output("workflow-list", await listWorkflowRuns({ activeOnly: true }));
|
|
1642
|
-
});
|
|
1643
|
-
},
|
|
1644
|
-
});
|
|
1645
|
-
const importKnowledgeCommand = defineCommand({
|
|
1646
|
-
meta: {
|
|
1647
|
-
name: "import",
|
|
1648
|
-
description: "Import a knowledge document or URL into the default stash",
|
|
1649
|
-
},
|
|
1650
|
-
args: {
|
|
1651
|
-
source: {
|
|
1652
|
-
type: "positional",
|
|
1653
|
-
description: 'Source file path, URL, or "-" to read from stdin',
|
|
1654
|
-
required: true,
|
|
1655
|
-
},
|
|
1656
|
-
name: {
|
|
1657
|
-
type: "string",
|
|
1658
|
-
description: "Knowledge name (defaults to the source filename or content slug)",
|
|
1659
|
-
},
|
|
1660
|
-
force: {
|
|
1661
|
-
type: "boolean",
|
|
1662
|
-
description: "Overwrite an existing knowledge document with the same name",
|
|
1663
|
-
default: false,
|
|
1664
|
-
},
|
|
1665
|
-
target: {
|
|
1666
|
-
type: "string",
|
|
1667
|
-
description: "Override the write destination. Accepts a source name from your config; falls back to defaultWriteTarget then the working stash.",
|
|
1668
|
-
},
|
|
1669
|
-
},
|
|
1670
|
-
async run({ args }) {
|
|
1671
|
-
return runWithJsonErrors(async () => {
|
|
1672
|
-
const { content, preferredName } = await readKnowledgeInput(args.source);
|
|
1673
|
-
const result = await writeMarkdownAsset({
|
|
1674
|
-
type: "knowledge",
|
|
1675
|
-
content,
|
|
1676
|
-
name: args.name ?? (isHttpUrl(args.source) ? preferredName : undefined),
|
|
1677
|
-
fallbackPrefix: "knowledge",
|
|
1678
|
-
preferredName,
|
|
1679
|
-
force: args.force,
|
|
1680
|
-
target: args.target,
|
|
1681
|
-
});
|
|
1682
|
-
appendEvent({
|
|
1683
|
-
eventType: "import",
|
|
1684
|
-
ref: result.ref,
|
|
1685
|
-
metadata: { source: args.source, path: result.path, force: args.force === true },
|
|
1686
|
-
});
|
|
1687
|
-
output("import", { ok: true, source: args.source, ...result });
|
|
1688
|
-
});
|
|
1689
|
-
},
|
|
1690
|
-
});
|
|
1691
|
-
const hintsCommand = defineCommand({
|
|
1692
|
-
meta: {
|
|
1693
|
-
name: "hints",
|
|
1694
|
-
description: "Print agent instructions on how to use akm, use --detail full for a complete guide",
|
|
1695
|
-
},
|
|
1696
|
-
args: {
|
|
1697
|
-
detail: {
|
|
1698
|
-
type: "string",
|
|
1699
|
-
description: "Hints detail level (brief|normal|full). `brief` prints the short guide; `normal`/`full` print the complete guide.",
|
|
1700
|
-
default: "normal",
|
|
1701
|
-
},
|
|
1702
|
-
},
|
|
1703
|
-
run({ args }) {
|
|
1704
|
-
return runWithJsonErrors(() => {
|
|
1705
|
-
// Let the global parser validate the value so an invalid `--detail`
|
|
1706
|
-
// returns the standard JSON error envelope (exit 2) rather than a raw
|
|
1707
|
-
// stack trace + exit 1. `brief` → short doc; `normal`/`full` → full doc.
|
|
1708
|
-
const detail = parseDetailLevel(args.detail) ?? "normal";
|
|
1709
|
-
process.stdout.write(loadHints(detail === "brief" ? "brief" : "full"));
|
|
1710
|
-
});
|
|
1711
|
-
},
|
|
1712
|
-
});
|
|
1713
|
-
const helpCommand = defineCommand({
|
|
1714
|
-
meta: {
|
|
1715
|
-
name: "help",
|
|
1716
|
-
description: "Print focused help topics such as migration guidance for a release",
|
|
1717
|
-
},
|
|
1718
|
-
subCommands: {
|
|
1719
|
-
migrate: defineCommand({
|
|
1720
|
-
meta: {
|
|
1721
|
-
name: "migrate",
|
|
1722
|
-
description: "Print release notes and migration guidance for a version. Bundled notes live in docs/migration/release-notes/<version>.md; an unknown version lists what's available.",
|
|
1723
|
-
},
|
|
1724
|
-
args: {
|
|
1725
|
-
// Optional in citty so run() is invoked even when omitted; we
|
|
1726
|
-
// re-validate below to surface a structured UsageError (exit 2)
|
|
1727
|
-
// instead of citty's default help-banner exit-0.
|
|
1728
|
-
version: {
|
|
1729
|
-
type: "positional",
|
|
1730
|
-
description: "Version to review (for example 0.6.0, v0.6.0, 0.6.0-rc1, or latest)",
|
|
1731
|
-
required: false,
|
|
1732
|
-
},
|
|
1733
|
-
},
|
|
1734
|
-
run({ args }) {
|
|
1735
|
-
return runWithJsonErrors(() => {
|
|
1736
|
-
const version = resolveHelpMigrateVersionArg(typeof args.version === "string" ? args.version : undefined);
|
|
1737
|
-
if (!version?.trim()) {
|
|
1738
|
-
throw new UsageError("Usage: akm help migrate <version>.", "MISSING_REQUIRED_ARGUMENT", "Pass a version like `0.6.0`, `v0.6.0`, `0.6.0-rc1`, or `latest`.");
|
|
1739
|
-
}
|
|
1740
|
-
process.stdout.write(renderMigrationHelp(version));
|
|
1741
|
-
});
|
|
1742
|
-
},
|
|
1743
|
-
}),
|
|
1744
|
-
},
|
|
1745
|
-
});
|
|
1746
|
-
const completionsCommand = defineCommand({
|
|
1747
|
-
meta: {
|
|
1748
|
-
name: "completions",
|
|
1749
|
-
description: "Generate or install shell completion script",
|
|
1750
|
-
},
|
|
1751
|
-
args: {
|
|
1752
|
-
install: {
|
|
1753
|
-
type: "boolean",
|
|
1754
|
-
description: "Install completions to the appropriate directory",
|
|
1755
|
-
default: false,
|
|
1756
|
-
},
|
|
1757
|
-
shell: {
|
|
1758
|
-
type: "string",
|
|
1759
|
-
description: "Shell type (bash)",
|
|
1760
|
-
default: "bash",
|
|
1761
|
-
},
|
|
1762
|
-
},
|
|
1763
|
-
run({ args }) {
|
|
1764
|
-
if (args.shell !== "bash") {
|
|
1765
|
-
throw new UsageError(`Unsupported shell: ${args.shell}. Only bash is supported.`);
|
|
1766
|
-
}
|
|
1767
|
-
const script = generateBashCompletions(main);
|
|
1768
|
-
if (args.install) {
|
|
1769
|
-
const dest = installBashCompletions(script);
|
|
1770
|
-
info(`Completions installed to ${dest}`);
|
|
1771
|
-
info(`Restart your shell or run: source ${dest}`);
|
|
1772
|
-
}
|
|
1773
|
-
else {
|
|
1774
|
-
process.stdout.write(script);
|
|
1775
|
-
}
|
|
1776
|
-
},
|
|
1777
|
-
});
|
|
1778
|
-
function normalizeToggleTarget(target) {
|
|
1779
|
-
const normalized = target.trim().toLowerCase();
|
|
1780
|
-
if (normalized === "skills.sh" || normalized === "skills-sh")
|
|
1781
|
-
return "skills.sh";
|
|
1782
|
-
throw new UsageError(`Unsupported target "${target}". Supported targets: skills.sh`);
|
|
1783
|
-
}
|
|
1784
|
-
function toggleSkillsShRegistry(enabled) {
|
|
1785
|
-
const config = loadUserConfig();
|
|
1786
|
-
const registries = (config.registries ?? DEFAULT_CONFIG.registries ?? []).map((registry) => ({ ...registry }));
|
|
1787
|
-
const idx = registries.findIndex((registry) => registry.provider === SKILLS_SH_PROVIDER || registry.name === SKILLS_SH_NAME || registry.url === SKILLS_SH_URL);
|
|
1788
|
-
if (idx >= 0) {
|
|
1789
|
-
const existing = registries[idx];
|
|
1790
|
-
const wasEnabled = existing.enabled !== false;
|
|
1791
|
-
existing.enabled = enabled;
|
|
1792
|
-
saveConfig({ ...config, registries });
|
|
1793
|
-
return { changed: wasEnabled !== enabled, component: SKILLS_SH_NAME, enabled };
|
|
1794
|
-
}
|
|
1795
|
-
if (!enabled) {
|
|
1796
|
-
// Materialize the skills.sh registry explicitly if absent.
|
|
1797
|
-
registries.push({ url: SKILLS_SH_URL, name: SKILLS_SH_NAME, provider: SKILLS_SH_PROVIDER, enabled: false });
|
|
1798
|
-
saveConfig({ ...config, registries });
|
|
1799
|
-
return { changed: true, component: SKILLS_SH_NAME, enabled: false };
|
|
1800
|
-
}
|
|
1801
|
-
registries.push({ url: SKILLS_SH_URL, name: SKILLS_SH_NAME, provider: SKILLS_SH_PROVIDER, enabled: true });
|
|
1802
|
-
saveConfig({ ...config, registries });
|
|
1803
|
-
return { changed: true, component: SKILLS_SH_NAME, enabled: true };
|
|
1804
|
-
}
|
|
1805
|
-
function toggleComponent(targetRaw, enabled) {
|
|
1806
|
-
const target = normalizeToggleTarget(targetRaw);
|
|
1807
|
-
if (target === "skills.sh")
|
|
1808
|
-
return toggleSkillsShRegistry(enabled);
|
|
1809
|
-
// normalizeToggleTarget throws for any unsupported target; this is unreachable.
|
|
1810
|
-
throw new UsageError(`Unsupported target "${targetRaw}". Supported targets: skills.sh`);
|
|
1811
|
-
}
|
|
1812
|
-
// Deprecated top-level aliases (removed 0.9.0) — delegate to `config enable|disable`.
|
|
1813
|
-
const enableCommand = defineCommand({
|
|
1814
|
-
meta: { name: "enable", description: "DEPRECATED — use `akm config enable`. Removed in 0.9.0." },
|
|
1815
|
-
args: {
|
|
1816
|
-
target: { type: "positional", description: "Component to enable (skills.sh)", required: true },
|
|
1817
|
-
},
|
|
1818
|
-
run({ args }) {
|
|
1819
|
-
return runWithJsonErrors(() => {
|
|
1820
|
-
emitCommandDeprecation("enable", "config enable");
|
|
1821
|
-
const result = toggleComponent(args.target, true);
|
|
1822
|
-
output("enable", result);
|
|
1823
|
-
});
|
|
1824
|
-
},
|
|
1825
|
-
});
|
|
1826
|
-
const disableCommand = defineCommand({
|
|
1827
|
-
meta: { name: "disable", description: "DEPRECATED — use `akm config disable`. Removed in 0.9.0." },
|
|
1828
|
-
args: {
|
|
1829
|
-
target: { type: "positional", description: "Component to disable (skills.sh)", required: true },
|
|
1830
|
-
},
|
|
1831
|
-
run({ args }) {
|
|
1832
|
-
return runWithJsonErrors(() => {
|
|
1833
|
-
emitCommandDeprecation("disable", "config disable");
|
|
1834
|
-
const result = toggleComponent(args.target, false);
|
|
1835
|
-
output("disable", result);
|
|
1836
|
-
});
|
|
1837
|
-
},
|
|
1838
|
-
});
|
|
1839
|
-
// ── env ───────────────────────────────────────────────────────────────────
|
|
1840
|
-
//
|
|
1841
|
-
// `akm env` manages whole `.env` files under each stash's env/ directory.
|
|
1842
|
-
// Values are NEVER written to stdout or structured output — only key NAMES and
|
|
1843
|
-
// start-of-line comments are surfaced. akm does not manage individual entries;
|
|
1844
|
-
// you edit the `.env` file yourself and akm loads it. Replaces the deprecated
|
|
1845
|
-
// `vault` type (see the shim further below; removed in 0.9.0).
|
|
1846
|
-
function parseEnvRef(ref) {
|
|
1847
|
-
return parseAssetRef(ref.includes(":") ? ref : `env:${ref}`);
|
|
1848
|
-
}
|
|
1849
|
-
function findEnvSource(origin) {
|
|
1850
|
-
const sources = resolveSourceEntries(undefined, loadConfig());
|
|
1851
|
-
if (sources.length === 0) {
|
|
1852
|
-
throw new UsageError("No stashes configured. Run `akm init` to create your working stash.");
|
|
1853
|
-
}
|
|
1854
|
-
if (!origin || origin === "local")
|
|
1855
|
-
return sources[0];
|
|
1856
|
-
const named = sources.find((source) => source.registryId === origin);
|
|
1857
|
-
if (!named) {
|
|
1858
|
-
throw new NotFoundError(`Source not found for origin: ${origin}`);
|
|
1859
|
-
}
|
|
1860
|
-
return named;
|
|
1861
|
-
}
|
|
1862
|
-
function makeEnvRef(name, source) {
|
|
1863
|
-
return source?.registryId ? `${source.registryId}//env:${name}` : `env:${name}`;
|
|
1864
|
-
}
|
|
1865
|
-
/**
|
|
1866
|
-
* Resolve an env ref to an absolute `.env` path. Accepts `env:`, `environment:`
|
|
1867
|
-
* (alias), and `vault:` (deprecated) refs as well as bare names. Prefers the
|
|
1868
|
-
* `env/` directory; falls back to the legacy `vaults/` directory when the env
|
|
1869
|
-
* file is absent there (handles an upgraded-but-not-yet-migrated stash). When
|
|
1870
|
-
* neither exists the env path is returned (so `create` writes under `env/`).
|
|
1871
|
-
*/
|
|
1872
|
-
function resolveEnvPath(ref) {
|
|
1873
|
-
const parsed = parseEnvRef(ref);
|
|
1874
|
-
if (parsed.type !== "env" && parsed.type !== "vault") {
|
|
1875
|
-
throw new UsageError(`Expected an env ref (env:<name>); got "${ref}".`);
|
|
1876
|
-
}
|
|
1877
|
-
const source = findEnvSource(parsed.origin);
|
|
1878
|
-
const envRoot = path.join(source.path, "env");
|
|
1879
|
-
const envPath = resolveAssetPathFromName("env", envRoot, parsed.name);
|
|
1880
|
-
// Defense-in-depth: ensure the resolved path stays inside the env directory.
|
|
1881
|
-
// validateName already rejects traversal patterns like "../../foo", but an
|
|
1882
|
-
// absolute-path override or symlink-based attack could still escape without
|
|
1883
|
-
// this second check.
|
|
1884
|
-
if (!isWithin(envPath, envRoot)) {
|
|
1885
|
-
throw new UsageError(`Env name "${parsed.name}" escapes the env directory.`);
|
|
1886
|
-
}
|
|
1887
|
-
const vaultRoot = path.join(source.path, "vaults");
|
|
1888
|
-
const vaultPath = resolveAssetPathFromName("vault", vaultRoot, parsed.name);
|
|
1889
|
-
if (!isWithin(vaultPath, vaultRoot)) {
|
|
1890
|
-
throw new UsageError(`Env name "${parsed.name}" escapes the env directory.`);
|
|
1891
|
-
}
|
|
1892
|
-
// Prefer env/; fall back to the frozen vaults/ copy only when the env file
|
|
1893
|
-
// is absent and the legacy vault file is present.
|
|
1894
|
-
if (!fs.existsSync(envPath) && fs.existsSync(vaultPath)) {
|
|
1895
|
-
return { name: parsed.name, absPath: vaultPath, source, parsedRef: parsed, dir: "vaults" };
|
|
1896
|
-
}
|
|
1897
|
-
return { name: parsed.name, absPath: envPath, source, parsedRef: parsed, dir: "env" };
|
|
1898
|
-
}
|
|
1899
|
-
/**
|
|
1900
|
-
* Walk each stash's env files and return one entry per `.env` file, using the
|
|
1901
|
-
* env asset spec's canonical-name logic (e.g. `env/team/prod.env` →
|
|
1902
|
-
* `env:team/prod`, `env/team/.env` → `env:team/default`). When a stash has not
|
|
1903
|
-
* yet migrated (no `env/` dir) the legacy `vaults/` dir is listed instead, so
|
|
1904
|
-
* `env list` stays continuous across the upgrade.
|
|
1905
|
-
*/
|
|
1906
|
-
function listEnvsRecursive(listKeysFn) {
|
|
1907
|
-
const result = [];
|
|
1908
|
-
for (const source of resolveSourceEntries(undefined, loadConfig())) {
|
|
1909
|
-
const envDir = path.join(source.path, "env");
|
|
1910
|
-
const legacyDir = path.join(source.path, "vaults");
|
|
1911
|
-
// Prefer env/; only fall back to the frozen vaults/ copy when env/ is absent.
|
|
1912
|
-
const scanType = fs.existsSync(envDir) ? "env" : "vault";
|
|
1913
|
-
const root = scanType === "env" ? envDir : legacyDir;
|
|
1914
|
-
if (!fs.existsSync(root))
|
|
1915
|
-
continue;
|
|
1916
|
-
const walk = (dir) => {
|
|
1917
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1918
|
-
const full = path.join(dir, entry.name);
|
|
1919
|
-
if (entry.isDirectory()) {
|
|
1920
|
-
walk(full);
|
|
1921
|
-
continue;
|
|
1922
|
-
}
|
|
1923
|
-
if (!entry.isFile())
|
|
1924
|
-
continue;
|
|
1925
|
-
if (entry.name !== ".env" && !entry.name.endsWith(".env"))
|
|
1926
|
-
continue;
|
|
1927
|
-
const canonical = deriveCanonicalAssetName(scanType, root, full);
|
|
1928
|
-
if (!canonical)
|
|
1929
|
-
continue;
|
|
1930
|
-
// Skip sensitive envs: a sibling .sensitive marker file suppresses listing.
|
|
1931
|
-
const markerPath = full.replace(/\.env$/, ".sensitive");
|
|
1932
|
-
if (fs.existsSync(markerPath))
|
|
1933
|
-
continue;
|
|
1934
|
-
const { keys } = listKeysFn(full);
|
|
1935
|
-
result.push({ ref: makeEnvRef(canonical, source), path: full, keys });
|
|
1936
|
-
}
|
|
1937
|
-
};
|
|
1938
|
-
walk(root);
|
|
1939
|
-
}
|
|
1940
|
-
return result;
|
|
1941
|
-
}
|
|
1942
|
-
const envListCommand = defineCommand({
|
|
1943
|
-
meta: { name: "list", description: "List all env files across all stashes with their key names (no values)" },
|
|
1944
|
-
run() {
|
|
1945
|
-
return runWithJsonErrors(async () => {
|
|
1946
|
-
const { listKeys } = await import("./commands/env.js");
|
|
1947
|
-
output("env-list", { envs: listEnvsRecursive(listKeys) });
|
|
1948
|
-
});
|
|
1949
|
-
},
|
|
1950
|
-
});
|
|
1951
|
-
const envCreateCommand = defineCommand({
|
|
1952
|
-
meta: {
|
|
1953
|
-
name: "create",
|
|
1954
|
-
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.",
|
|
1955
|
-
},
|
|
1956
|
-
args: {
|
|
1957
|
-
name: { type: "positional", description: "Env name (e.g. prod) — file becomes <name>.env", required: true },
|
|
1958
|
-
"from-file": { type: "string", description: "Seed the env file from an existing .env at this path" },
|
|
1959
|
-
"from-stdin": { type: "boolean", description: "Seed the env file from stdin", default: false },
|
|
1960
|
-
sensitive: {
|
|
1961
|
-
type: "boolean",
|
|
1962
|
-
description: "Exclude this env file from env list output and the search index",
|
|
1963
|
-
default: false,
|
|
1964
|
-
},
|
|
1965
|
-
},
|
|
1966
|
-
run({ args }) {
|
|
1967
|
-
return runWithJsonErrors(async () => {
|
|
1968
|
-
const { createEnv, writeEnv } = await import("./commands/env.js");
|
|
1969
|
-
// `create` always targets env/, never the frozen vaults/ copy.
|
|
1970
|
-
const parsed = parseEnvRef(args.name);
|
|
1971
|
-
const source = findEnvSource(parsed.origin);
|
|
1972
|
-
const envRoot = path.join(source.path, "env");
|
|
1973
|
-
const absPath = resolveAssetPathFromName("env", envRoot, parsed.name);
|
|
1974
|
-
if (!isWithin(absPath, envRoot)) {
|
|
1975
|
-
throw new UsageError(`Env name "${parsed.name}" escapes the env directory.`);
|
|
1976
|
-
}
|
|
1977
|
-
const fromFile = getHyphenatedArg(args, "from-file");
|
|
1978
|
-
const fromStdin = getHyphenatedArg(args, "from-stdin") === true;
|
|
1979
|
-
if (fromFile !== undefined && fromStdin) {
|
|
1980
|
-
throw new UsageError("Pass only one of --from-file or --from-stdin.", "INVALID_FLAG_VALUE");
|
|
1981
|
-
}
|
|
1982
|
-
if (fromFile !== undefined || fromStdin) {
|
|
1983
|
-
// Ingest path: never silently clobber an existing env file.
|
|
1984
|
-
if (fs.existsSync(absPath)) {
|
|
1985
|
-
throw new UsageError(`Env "${makeEnvRef(parsed.name, source)}" already exists. Remove it first (\`akm env remove\`) or edit the file directly.`, "RESOURCE_ALREADY_EXISTS");
|
|
1986
|
-
}
|
|
1987
|
-
let content;
|
|
1988
|
-
if (fromFile !== undefined) {
|
|
1989
|
-
if (!fs.existsSync(fromFile)) {
|
|
1990
|
-
throw new NotFoundError(`Source file not found: ${fromFile}`, "FILE_NOT_FOUND");
|
|
1991
|
-
}
|
|
1992
|
-
content = fs.readFileSync(fromFile, "utf8");
|
|
1993
|
-
}
|
|
1994
|
-
else {
|
|
1995
|
-
const MAX_ENV_BYTES = 1024 * 1024; // 1 MB
|
|
1996
|
-
let total = 0;
|
|
1997
|
-
const chunks = [];
|
|
1998
|
-
for await (const chunk of Bun.stdin.stream()) {
|
|
1999
|
-
total += chunk.byteLength;
|
|
2000
|
-
if (total > MAX_ENV_BYTES) {
|
|
2001
|
-
throw new UsageError("Env file exceeds 1 MB limit.", "INVALID_FLAG_VALUE");
|
|
2002
|
-
}
|
|
2003
|
-
chunks.push(chunk);
|
|
2004
|
-
}
|
|
2005
|
-
content = Buffer.concat(chunks).toString("utf8");
|
|
2006
|
-
}
|
|
2007
|
-
writeEnv(absPath, content);
|
|
2008
|
-
}
|
|
2009
|
-
else {
|
|
2010
|
-
createEnv(absPath);
|
|
2011
|
-
}
|
|
2012
|
-
if (args.sensitive) {
|
|
2013
|
-
const markerPath = absPath.replace(/\.env$/, ".sensitive");
|
|
2014
|
-
if (!fs.existsSync(markerPath)) {
|
|
2015
|
-
fs.writeFileSync(markerPath, "", { mode: 0o600 });
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
output("env-create", { ref: makeEnvRef(parsed.name, source) });
|
|
2019
|
-
});
|
|
2020
|
-
},
|
|
2021
|
-
});
|
|
2022
|
-
const envPathCommand = defineCommand({
|
|
2023
|
-
meta: {
|
|
2024
|
-
name: "path",
|
|
2025
|
-
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.",
|
|
2026
|
-
},
|
|
2027
|
-
args: {
|
|
2028
|
-
ref: { type: "positional", description: "Env ref", required: true },
|
|
2029
|
-
quiet: { type: "boolean", alias: "q", description: "Suppress the unsafe-source warning", default: false },
|
|
2030
|
-
},
|
|
2031
|
-
run({ args }) {
|
|
2032
|
-
return runWithJsonErrors(async () => {
|
|
2033
|
-
const { name, absPath, source } = resolveEnvPath(args.ref);
|
|
2034
|
-
if (!fs.existsSync(absPath)) {
|
|
2035
|
-
throw new NotFoundError(`Env not found: ${makeEnvRef(name, source)}`);
|
|
2036
|
-
}
|
|
2037
|
-
// The raw `.env` may contain `X=$(cmd)`, which executes if `source`d.
|
|
2038
|
-
// Warning goes to stderr (never contaminates the path on stdout) and is
|
|
2039
|
-
// suppressed with --quiet for the legitimate `_FILE` / `--env-file` use.
|
|
2040
|
-
if (args.quiet !== true) {
|
|
2041
|
-
process.stderr.write(`warning: this is the raw file path. Do NOT \`source\` it (shell substitutions in the file would execute).\n` +
|
|
2042
|
-
` To inject values run: akm env run ${args.ref} -- <command>\n`);
|
|
2043
|
-
}
|
|
2044
|
-
process.stdout.write(`${absPath}\n`);
|
|
2045
|
-
});
|
|
2046
|
-
},
|
|
2047
|
-
});
|
|
2048
|
-
const envExportCommand = defineCommand({
|
|
2049
|
-
meta: {
|
|
2050
|
-
name: "export",
|
|
2051
|
-
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>`.",
|
|
2052
|
-
},
|
|
2053
|
-
args: {
|
|
2054
|
-
ref: { type: "positional", description: "Env ref", required: true },
|
|
2055
|
-
out: { type: "string", alias: "o", description: "Destination file (required). Written at mode 0600." },
|
|
2056
|
-
},
|
|
2057
|
-
run({ args }) {
|
|
2058
|
-
return runWithJsonErrors(async () => {
|
|
2059
|
-
const outPath = getHyphenatedArg(args, "out");
|
|
2060
|
-
if (!outPath) {
|
|
2061
|
-
throw new UsageError("`akm env export` writes to a file — pass --out <path>.\n" +
|
|
2062
|
-
" To use values directly, run `akm env run <ref> -- <command>` (or `-- $SHELL` for an interactive\n" +
|
|
2063
|
-
" session). export never prints values to stdout, to avoid leaking them into a captured context.", "MISSING_REQUIRED_ARGUMENT");
|
|
2064
|
-
}
|
|
2065
|
-
const { name, absPath, source } = resolveEnvPath(args.ref);
|
|
2066
|
-
if (!fs.existsSync(absPath)) {
|
|
2067
|
-
throw new NotFoundError(`Env not found: ${makeEnvRef(name, source)}`);
|
|
2068
|
-
}
|
|
2069
|
-
const { buildShellExportScript } = await import("./commands/env.js");
|
|
2070
|
-
const resolvedOut = path.resolve(outPath);
|
|
2071
|
-
writeFileAtomic(resolvedOut, buildShellExportScript(absPath), 0o600);
|
|
2072
|
-
output("env-export", { ref: makeEnvRef(name, source), out: resolvedOut });
|
|
2073
|
-
});
|
|
2074
|
-
},
|
|
2075
|
-
});
|
|
2076
|
-
/**
|
|
2077
|
-
* Shared implementation for `env run` (and the deprecated `vault run` shim).
|
|
2078
|
-
* Injects an entire env file's values into the child process env — never via a
|
|
2079
|
-
* shell — after scanning the injected keys for process-hijacking variables.
|
|
2080
|
-
*/
|
|
2081
|
-
async function runEnvInjected(target, opts) {
|
|
2082
|
-
const dashIndex = process.argv.indexOf("--");
|
|
2083
|
-
if (dashIndex < 0 || dashIndex === process.argv.length - 1) {
|
|
2084
|
-
throw new UsageError("Missing command. Usage: akm env run <ref> -- <command>");
|
|
2085
|
-
}
|
|
2086
|
-
const command = process.argv.slice(dashIndex + 1);
|
|
2087
|
-
const { name, absPath, source } = resolveEnvPath(target);
|
|
2088
|
-
if (!fs.existsSync(absPath)) {
|
|
2089
|
-
// Help users who reach for the removed single-key `ref/KEY` form.
|
|
2090
|
-
const slash = target.lastIndexOf("/");
|
|
2091
|
-
if (slash > 0) {
|
|
2092
|
-
const maybeKey = target.slice(slash + 1);
|
|
2093
|
-
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(maybeKey)) {
|
|
2094
|
-
let baseExists = false;
|
|
2095
|
-
try {
|
|
2096
|
-
baseExists = fs.existsSync(resolveEnvPath(target.slice(0, slash)).absPath);
|
|
2097
|
-
}
|
|
2098
|
-
catch {
|
|
2099
|
-
baseExists = false;
|
|
2100
|
-
}
|
|
2101
|
-
if (baseExists) {
|
|
2102
|
-
throw new UsageError(`'akm env run' injects the whole file; the single-key '<ref>/${maybeKey}' form was removed.\n` +
|
|
2103
|
-
` For one value use a secret: \`akm secret run secret:${maybeKey} ${maybeKey} -- <command>\`.`, "INVALID_FLAG_VALUE");
|
|
2104
|
-
}
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
throw new NotFoundError(`Env not found: ${makeEnvRef(name, source)}`);
|
|
2108
|
-
}
|
|
2109
|
-
const { loadEnv } = await import("./commands/env.js");
|
|
2110
|
-
const allValues = loadEnv(absPath);
|
|
2111
|
-
// Value-safe key filtering (--only / --except operate on key NAMES only).
|
|
2112
|
-
let envValues = allValues;
|
|
2113
|
-
if (opts.only && opts.except) {
|
|
2114
|
-
throw new UsageError("Pass only one of --only or --except.", "INVALID_FLAG_VALUE");
|
|
2115
|
-
}
|
|
2116
|
-
if (opts.only) {
|
|
2117
|
-
const wanted = new Set(opts.only);
|
|
2118
|
-
const missing = opts.only.filter((k) => !(k in allValues));
|
|
2119
|
-
if (missing.length > 0) {
|
|
2120
|
-
process.stderr.write(`warning: --only key(s) not present in ${makeEnvRef(name, source)}: ${missing.join(", ")}\n`);
|
|
2121
|
-
}
|
|
2122
|
-
envValues = Object.fromEntries(Object.entries(allValues).filter(([k]) => wanted.has(k)));
|
|
2123
|
-
}
|
|
2124
|
-
else if (opts.except) {
|
|
2125
|
-
const excluded = new Set(opts.except);
|
|
2126
|
-
envValues = Object.fromEntries(Object.entries(allValues).filter(([k]) => !excluded.has(k)));
|
|
2127
|
-
}
|
|
2128
|
-
const keys = Object.keys(envValues);
|
|
2129
|
-
// Scan injected keys for known process-hijacking variables (LD_PRELOAD,
|
|
2130
|
-
// PATH, ...). Block for third-party-sourced stashes (origin has a registryId);
|
|
2131
|
-
// warn for the operator's own first-party stash, where they own the file.
|
|
2132
|
-
const { isDangerousEnvKey } = await import("./commands/lint/env-key-rules.js");
|
|
2133
|
-
const dangerous = keys.filter(isDangerousEnvKey);
|
|
2134
|
-
if (dangerous.length > 0) {
|
|
2135
|
-
const detail = `Env "${makeEnvRef(name, source)}" injects process-hijacking variable(s): ${dangerous.join(", ")}.`;
|
|
2136
|
-
if (source.registryId) {
|
|
2137
|
-
throw new UsageError(`Refusing to inject env from a third-party stash. ${detail}\n` +
|
|
2138
|
-
` Review the file, then copy the values into a first-party env if you trust them.`, "INVALID_FLAG_VALUE");
|
|
2139
|
-
}
|
|
2140
|
-
process.stderr.write(`warning: ${detail} Injecting anyway (first-party stash).\n`);
|
|
2141
|
-
}
|
|
2142
|
-
const mergedEnv = { ...process.env };
|
|
2143
|
-
for (const [envKey, envValue] of Object.entries(envValues)) {
|
|
2144
|
-
mergedEnv[envKey] = envValue;
|
|
2145
|
-
}
|
|
2146
|
-
// Audit trail: keys only, never values. A single `env_access` event carries a
|
|
2147
|
-
// `deprecatedAlias` marker when reached via the `vault run` shim, so log
|
|
2148
|
-
// consumers see one stable event type without a doubled physical record.
|
|
2149
|
-
appendEvent({
|
|
2150
|
-
eventType: "env_access",
|
|
2151
|
-
ref: makeEnvRef(name, source),
|
|
2152
|
-
metadata: opts.viaVault ? { keys, deprecatedAlias: "vault_access" } : { keys },
|
|
2153
|
-
});
|
|
2154
|
-
const result = spawnSync(command[0], command.slice(1), {
|
|
2155
|
-
stdio: "inherit",
|
|
2156
|
-
env: mergedEnv,
|
|
2157
|
-
});
|
|
2158
|
-
if (result.error) {
|
|
2159
|
-
// Classify spawn failures (#483). Raw ErrnoException leaks a bare
|
|
2160
|
-
// "spawn ENOENT" with no hint — wrap it so consumers get a usable
|
|
2161
|
-
// code + hint in the standard JSON envelope.
|
|
2162
|
-
const err = result.error;
|
|
2163
|
-
if (err.code === "ENOENT") {
|
|
2164
|
-
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'.`);
|
|
2165
|
-
}
|
|
2166
|
-
if (err.code === "EACCES") {
|
|
2167
|
-
throw new ConfigError(`Command not executable: ${command[0]}`, "STASH_DIR_UNREADABLE", `Add execute permission ('chmod +x ${command[0]}') or invoke via an interpreter.`);
|
|
2168
|
-
}
|
|
2169
|
-
throw err;
|
|
2170
|
-
}
|
|
2171
|
-
process.exit(result.status ?? 0);
|
|
2172
|
-
}
|
|
2173
|
-
/** Parse a comma/space-separated key list flag into a trimmed, non-empty array. */
|
|
2174
|
-
function parseKeyListFlag(raw) {
|
|
2175
|
-
if (raw === undefined)
|
|
2176
|
-
return undefined;
|
|
2177
|
-
const keys = raw
|
|
2178
|
-
.split(/[,\s]+/)
|
|
2179
|
-
.map((k) => k.trim())
|
|
2180
|
-
.filter(Boolean);
|
|
2181
|
-
return keys.length > 0 ? keys : undefined;
|
|
2182
|
-
}
|
|
2183
|
-
const envRunCommand = defineCommand({
|
|
2184
|
-
meta: {
|
|
2185
|
-
name: "run",
|
|
2186
|
-
description: "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.",
|
|
2187
|
-
},
|
|
2188
|
-
args: {
|
|
2189
|
-
target: { type: "positional", description: "Env ref", required: true },
|
|
2190
|
-
only: {
|
|
2191
|
-
type: "string",
|
|
2192
|
-
description: "Inject ONLY these keys (comma-separated). Mutually exclusive with --except.",
|
|
2193
|
-
},
|
|
2194
|
-
except: { type: "string", description: "Inject all keys EXCEPT these (comma-separated)." },
|
|
2195
|
-
},
|
|
2196
|
-
run({ args }) {
|
|
2197
|
-
return runWithJsonErrors(() => runEnvInjected(args.target, {
|
|
2198
|
-
viaVault: false,
|
|
2199
|
-
only: parseKeyListFlag(getHyphenatedArg(args, "only")),
|
|
2200
|
-
except: parseKeyListFlag(getHyphenatedArg(args, "except")),
|
|
2201
|
-
}));
|
|
2202
|
-
},
|
|
2203
|
-
});
|
|
2204
|
-
const envRemoveCommand = defineCommand({
|
|
2205
|
-
meta: { name: "remove", description: "Remove an env file (and its .sensitive marker, if any)" },
|
|
2206
|
-
args: {
|
|
2207
|
-
ref: { type: "positional", description: "Env ref", required: true },
|
|
2208
|
-
yes: { type: "boolean", alias: "y", description: "Skip confirmation prompt", default: false },
|
|
2209
|
-
},
|
|
2210
|
-
run({ args }) {
|
|
2211
|
-
return runWithJsonErrors(async () => {
|
|
2212
|
-
// Resolve against env/ specifically — never delete the frozen vaults/ copy.
|
|
2213
|
-
const parsed = parseEnvRef(args.ref);
|
|
2214
|
-
const source = findEnvSource(parsed.origin);
|
|
2215
|
-
const envRoot = path.join(source.path, "env");
|
|
2216
|
-
const absPath = resolveAssetPathFromName("env", envRoot, parsed.name);
|
|
2217
|
-
if (!isWithin(absPath, envRoot)) {
|
|
2218
|
-
throw new UsageError(`Env name "${parsed.name}" escapes the env directory.`);
|
|
2219
|
-
}
|
|
2220
|
-
const { confirmDestructive } = await import("./cli/confirm.js");
|
|
2221
|
-
const confirmed = await confirmDestructive(`Remove env "${args.ref}"? This cannot be undone.`, {
|
|
2222
|
-
yes: args.yes === true,
|
|
2223
|
-
});
|
|
2224
|
-
if (!confirmed) {
|
|
2225
|
-
process.stderr.write("Aborted.\n");
|
|
2226
|
-
return;
|
|
2227
|
-
}
|
|
2228
|
-
if (!fs.existsSync(absPath)) {
|
|
2229
|
-
throw new NotFoundError(`Env not found: ${makeEnvRef(parsed.name, source)}`);
|
|
2230
|
-
}
|
|
2231
|
-
const { removeEnv } = await import("./commands/env.js");
|
|
2232
|
-
const removed = removeEnv(absPath);
|
|
2233
|
-
output("env-remove", { ref: makeEnvRef(parsed.name, source), removed });
|
|
2234
|
-
});
|
|
2235
|
-
},
|
|
2236
|
-
});
|
|
2237
|
-
const envCommand = defineCommand({
|
|
2238
|
-
meta: {
|
|
2239
|
-
name: "env",
|
|
2240
|
-
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`.",
|
|
2241
|
-
},
|
|
2242
|
-
subCommands: {
|
|
2243
|
-
list: envListCommand,
|
|
2244
|
-
path: envPathCommand,
|
|
2245
|
-
export: envExportCommand,
|
|
2246
|
-
run: envRunCommand,
|
|
2247
|
-
create: envCreateCommand,
|
|
2248
|
-
remove: envRemoveCommand,
|
|
2249
|
-
},
|
|
2250
|
-
run({ args }) {
|
|
2251
|
-
return runWithJsonErrors(async () => {
|
|
2252
|
-
if (hasSubcommand(args, ENV_SUBCOMMAND_SET))
|
|
2253
|
-
return;
|
|
2254
|
-
const { listKeys } = await import("./commands/env.js");
|
|
2255
|
-
output("env-list", { envs: listEnvsRecursive(listKeys) });
|
|
2256
|
-
});
|
|
2257
|
-
},
|
|
2258
|
-
});
|
|
2259
|
-
// ── vault (DEPRECATED) ────────────────────────────────────────────────────────
|
|
2260
|
-
//
|
|
2261
|
-
// `akm vault` is deprecated in 0.8.0 and removed in 0.9.0. The verb now warns
|
|
2262
|
-
// to stderr and delegates to the `env` handlers. Entry management (`set` /
|
|
2263
|
-
// `unset`) and the single-key `run <ref>/KEY` form are hard-errors with a
|
|
2264
|
-
// signpost to `akm secret` — silent behaviour changes around secret material
|
|
2265
|
-
// are unacceptable.
|
|
2266
|
-
function emitVaultDeprecation(sub) {
|
|
2267
|
-
process.stderr.write(`warning: 'akm vault ${sub}' is deprecated and will be removed in 0.9.0. Use 'akm env ${sub}'.\n` +
|
|
2268
|
-
" For single-value injection use 'akm secret'.\n");
|
|
2269
|
-
}
|
|
2270
|
-
function emitFlagDeprecation(oldFlag, newFlag, cmd) {
|
|
2271
|
-
if (isQuiet())
|
|
2272
|
-
return;
|
|
2273
|
-
process.stderr.write(`warning: '${oldFlag}' is deprecated for 'akm ${cmd}'; use '${newFlag}'. Removed in 0.9.0.\n`);
|
|
2274
|
-
}
|
|
2275
|
-
/**
|
|
2276
|
-
* Emit a stderr deprecation warning for a renamed top-level command. The old
|
|
2277
|
-
* spelling keeps working in 0.8 (wrap-and-delegate) and is removed in 0.9.0.
|
|
2278
|
-
* Suppressed under --quiet; never written to stdout so JSON consumers are
|
|
2279
|
-
* unaffected.
|
|
2280
|
-
*/
|
|
2281
|
-
function emitCommandDeprecation(oldCmd, newCmd) {
|
|
2282
|
-
if (isQuiet())
|
|
2283
|
-
return;
|
|
2284
|
-
process.stderr.write(`warning: 'akm ${oldCmd}' is deprecated and will be removed in 0.9.0. Use 'akm ${newCmd}'.\n`);
|
|
2285
|
-
}
|
|
2286
|
-
const vaultSetCommand = defineCommand({
|
|
2287
|
-
meta: { name: "set", description: "DEPRECATED — removed. Edit the .env file directly, or use `akm secret set`." },
|
|
2288
|
-
args: {
|
|
2289
|
-
ref: { type: "positional", description: "(deprecated)", required: false },
|
|
2290
|
-
key: { type: "positional", description: "(deprecated)", required: false },
|
|
2291
|
-
},
|
|
2292
|
-
run() {
|
|
2293
|
-
return runWithJsonErrors(async () => {
|
|
2294
|
-
throw new UsageError("'akm vault set' was removed: akm no longer manages individual env entries.\n" +
|
|
2295
|
-
" Edit the .env file directly (then run with `akm env run <ref> -- <cmd>`),\n" +
|
|
2296
|
-
" or store a single value as a secret: `akm secret set secret:<name>`.", "INVALID_FLAG_VALUE");
|
|
2297
|
-
});
|
|
2298
|
-
},
|
|
2299
|
-
});
|
|
2300
|
-
const vaultUnsetCommand = defineCommand({
|
|
2301
|
-
meta: { name: "unset", description: "DEPRECATED — removed. Edit the .env file directly." },
|
|
2302
|
-
args: {
|
|
2303
|
-
ref: { type: "positional", description: "(deprecated)", required: false },
|
|
2304
|
-
key: { type: "positional", description: "(deprecated)", required: false },
|
|
2305
|
-
},
|
|
2306
|
-
run() {
|
|
2307
|
-
return runWithJsonErrors(async () => {
|
|
2308
|
-
throw new UsageError("'akm vault unset' was removed: akm no longer manages individual env entries.\n" +
|
|
2309
|
-
" Edit the .env file directly, or remove a secret with `akm secret remove secret:<name>`.", "INVALID_FLAG_VALUE");
|
|
2310
|
-
});
|
|
2311
|
-
},
|
|
2312
|
-
});
|
|
2313
|
-
const vaultListCommand = defineCommand({
|
|
2314
|
-
meta: { name: "list", description: "DEPRECATED — use `akm env list`." },
|
|
2315
|
-
run() {
|
|
2316
|
-
return runWithJsonErrors(async () => {
|
|
2317
|
-
emitVaultDeprecation("list");
|
|
2318
|
-
const { listKeys } = await import("./commands/env.js");
|
|
2319
|
-
output("env-list", { envs: listEnvsRecursive(listKeys) });
|
|
2320
|
-
});
|
|
2321
|
-
},
|
|
2322
|
-
});
|
|
2323
|
-
const vaultCreateCommand = defineCommand({
|
|
2324
|
-
meta: { name: "create", description: "DEPRECATED — use `akm env create`." },
|
|
2325
|
-
args: {
|
|
2326
|
-
name: { type: "positional", description: "Env name", required: true },
|
|
2327
|
-
sensitive: { type: "boolean", description: "Exclude from list output and the search index", default: false },
|
|
2328
|
-
},
|
|
2329
|
-
run({ args }) {
|
|
2330
|
-
return runWithJsonErrors(async () => {
|
|
2331
|
-
emitVaultDeprecation("create");
|
|
2332
|
-
const { createEnv } = await import("./commands/env.js");
|
|
2333
|
-
const parsed = parseEnvRef(args.name);
|
|
2334
|
-
const source = findEnvSource(parsed.origin);
|
|
2335
|
-
const envRoot = path.join(source.path, "env");
|
|
2336
|
-
const absPath = resolveAssetPathFromName("env", envRoot, parsed.name);
|
|
2337
|
-
if (!isWithin(absPath, envRoot)) {
|
|
2338
|
-
throw new UsageError(`Env name "${parsed.name}" escapes the env directory.`);
|
|
2339
|
-
}
|
|
2340
|
-
createEnv(absPath);
|
|
2341
|
-
if (args.sensitive) {
|
|
2342
|
-
const markerPath = absPath.replace(/\.env$/, ".sensitive");
|
|
2343
|
-
if (!fs.existsSync(markerPath))
|
|
2344
|
-
fs.writeFileSync(markerPath, "", { mode: 0o600 });
|
|
2345
|
-
}
|
|
2346
|
-
output("env-create", { ref: makeEnvRef(parsed.name, source) });
|
|
2347
|
-
});
|
|
2348
|
-
},
|
|
2349
|
-
});
|
|
2350
|
-
const vaultPathCommand = defineCommand({
|
|
2351
|
-
meta: { name: "path", description: "DEPRECATED — use `akm env path`." },
|
|
2352
|
-
args: {
|
|
2353
|
-
ref: { type: "positional", description: "Env ref", required: true },
|
|
2354
|
-
},
|
|
2355
|
-
run({ args }) {
|
|
2356
|
-
return runWithJsonErrors(async () => {
|
|
2357
|
-
emitVaultDeprecation("path");
|
|
2358
|
-
const { name, absPath, source } = resolveEnvPath(args.ref);
|
|
2359
|
-
if (!fs.existsSync(absPath)) {
|
|
2360
|
-
throw new NotFoundError(`Env not found: ${makeEnvRef(name, source)}`);
|
|
2361
|
-
}
|
|
2362
|
-
process.stderr.write(`warning: sourcing the raw file executes shell substitutions it contains. Use: akm env run ${args.ref} -- <command>\n`);
|
|
2363
|
-
process.stdout.write(`${absPath}\n`);
|
|
2364
|
-
});
|
|
2365
|
-
},
|
|
2366
|
-
});
|
|
2367
|
-
const vaultRunCommand = defineCommand({
|
|
2368
|
-
meta: { name: "run", description: "DEPRECATED — use `akm env run`. The single-key `<ref>/KEY` form was removed." },
|
|
2369
|
-
args: {
|
|
2370
|
-
target: { type: "positional", description: "Env ref", required: true },
|
|
2371
|
-
},
|
|
2372
|
-
run({ args }) {
|
|
2373
|
-
return runWithJsonErrors(async () => {
|
|
2374
|
-
emitVaultDeprecation("run");
|
|
2375
|
-
await runEnvInjected(args.target, { viaVault: true });
|
|
2376
|
-
});
|
|
2377
|
-
},
|
|
2378
|
-
});
|
|
2379
|
-
const vaultCommand = defineCommand({
|
|
2380
|
-
meta: {
|
|
2381
|
-
name: "vault",
|
|
2382
|
-
description: "DEPRECATED (use `akm env`) — removed in 0.9.0. Manages whole `.env` files; values never printed.",
|
|
2383
|
-
},
|
|
2384
|
-
subCommands: {
|
|
2385
|
-
list: vaultListCommand,
|
|
2386
|
-
path: vaultPathCommand,
|
|
2387
|
-
run: vaultRunCommand,
|
|
2388
|
-
create: vaultCreateCommand,
|
|
2389
|
-
set: vaultSetCommand,
|
|
2390
|
-
unset: vaultUnsetCommand,
|
|
2391
|
-
},
|
|
2392
|
-
run({ args }) {
|
|
2393
|
-
return runWithJsonErrors(async () => {
|
|
2394
|
-
if (hasSubcommand(args, VAULT_SUBCOMMAND_SET))
|
|
2395
|
-
return;
|
|
2396
|
-
emitVaultDeprecation("list");
|
|
2397
|
-
const { listKeys } = await import("./commands/env.js");
|
|
2398
|
-
output("env-list", { envs: listEnvsRecursive(listKeys) });
|
|
2399
|
-
});
|
|
2400
|
-
},
|
|
2401
|
-
});
|
|
2402
|
-
// ── secret ──────────────────────────────────────────────────────────────────
|
|
2403
|
-
//
|
|
2404
|
-
// `akm secret` manages whole-file secrets under each stash's secrets/ directory.
|
|
2405
|
-
// Unlike vaults (.env key/value), the ENTIRE file is the secret value. The bytes
|
|
2406
|
-
// are NEVER written to stdout or structured output. Values reach a command only
|
|
2407
|
-
// via `akm secret run` (injected into a child env var) or `akm secret path`
|
|
2408
|
-
// (the Docker /run/secrets + `_FILE` convention).
|
|
2409
|
-
function parseSecretRef(ref) {
|
|
2410
|
-
return parseAssetRef(ref.includes(":") ? ref : `secret:${ref}`);
|
|
2411
|
-
}
|
|
2412
|
-
function makeSecretRef(name, source) {
|
|
2413
|
-
return source?.registryId ? `${source.registryId}//secret:${name}` : `secret:${name}`;
|
|
2414
|
-
}
|
|
2415
|
-
function resolveSecretPath(ref) {
|
|
2416
|
-
const parsed = parseSecretRef(ref);
|
|
2417
|
-
if (parsed.type !== "secret") {
|
|
2418
|
-
throw new UsageError(`Expected a secret ref (secret:<name>); got "${ref}".`);
|
|
2419
|
-
}
|
|
2420
|
-
// Source resolution is identical for every asset type; reuse the env helper.
|
|
2421
|
-
const source = findEnvSource(parsed.origin);
|
|
2422
|
-
const typeRoot = path.join(source.path, "secrets");
|
|
2423
|
-
const absPath = resolveAssetPathFromName("secret", typeRoot, parsed.name);
|
|
2424
|
-
// Defense-in-depth: ensure the resolved path stays inside the secrets dir.
|
|
2425
|
-
if (!isWithin(absPath, typeRoot)) {
|
|
2426
|
-
throw new UsageError(`Secret name "${parsed.name}" escapes the secrets directory.`);
|
|
2427
|
-
}
|
|
2428
|
-
return { name: parsed.name, absPath, source };
|
|
2429
|
-
}
|
|
2430
|
-
/** Walk `secrets/` across all stashes, returning one entry per secret file. */
|
|
2431
|
-
function listSecretsRecursive() {
|
|
2432
|
-
const result = [];
|
|
2433
|
-
for (const source of resolveSourceEntries(undefined, loadConfig())) {
|
|
2434
|
-
const secretsDir = path.join(source.path, "secrets");
|
|
2435
|
-
if (!fs.existsSync(secretsDir))
|
|
2436
|
-
continue;
|
|
2437
|
-
const walk = (dir) => {
|
|
2438
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
2439
|
-
const full = path.join(dir, entry.name);
|
|
2440
|
-
if (entry.isDirectory()) {
|
|
2441
|
-
walk(full);
|
|
2442
|
-
continue;
|
|
2443
|
-
}
|
|
2444
|
-
if (!entry.isFile())
|
|
2445
|
-
continue;
|
|
2446
|
-
if (entry.name.endsWith(".lock") || entry.name.endsWith(".sensitive"))
|
|
2447
|
-
continue;
|
|
2448
|
-
// A sibling `<name>.sensitive` marker suppresses listing.
|
|
2449
|
-
if (fs.existsSync(`${full}.sensitive`))
|
|
2450
|
-
continue;
|
|
2451
|
-
const canonical = deriveCanonicalAssetName("secret", secretsDir, full);
|
|
2452
|
-
if (!canonical)
|
|
2453
|
-
continue;
|
|
2454
|
-
result.push({ ref: makeSecretRef(canonical, source), path: full });
|
|
2455
|
-
}
|
|
2456
|
-
};
|
|
2457
|
-
walk(secretsDir);
|
|
2458
|
-
}
|
|
2459
|
-
return result;
|
|
2460
|
-
}
|
|
2461
|
-
const secretListCommand = defineCommand({
|
|
2462
|
-
meta: {
|
|
2463
|
-
name: "list",
|
|
2464
|
-
description: "List all secrets across all stashes by name (the file contents are never shown)",
|
|
2465
|
-
},
|
|
2466
|
-
run() {
|
|
2467
|
-
return runWithJsonErrors(async () => {
|
|
2468
|
-
output("secret-list", { secrets: listSecretsRecursive() });
|
|
2469
|
-
});
|
|
2470
|
-
},
|
|
2471
|
-
});
|
|
2472
|
-
const secretSetCommand = defineCommand({
|
|
2473
|
-
meta: {
|
|
2474
|
-
name: "set",
|
|
2475
|
-
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.",
|
|
2476
|
-
},
|
|
2477
|
-
args: {
|
|
2478
|
-
ref: { type: "positional", description: "Secret ref (e.g. secret:deploy-key or just deploy-key)", required: true },
|
|
2479
|
-
"from-file": { type: "string", description: "Read the value from this file (stored byte-exact)" },
|
|
2480
|
-
"from-env": { type: "string", description: "Read the value from the named environment variable" },
|
|
2481
|
-
},
|
|
2482
|
-
run({ args }) {
|
|
2483
|
-
return runWithJsonErrors(async () => {
|
|
2484
|
-
const { setSecret } = await import("./commands/secret.js");
|
|
2485
|
-
const { name, absPath, source } = resolveSecretPath(args.ref);
|
|
2486
|
-
const fromEnv = getHyphenatedArg(args, "from-env");
|
|
2487
|
-
const fromFile = getHyphenatedArg(args, "from-file");
|
|
2488
|
-
if (fromEnv !== undefined && fromFile !== undefined) {
|
|
2489
|
-
throw new UsageError("Pass only one of --from-file or --from-env (or use stdin).", "INVALID_FLAG_VALUE");
|
|
2490
|
-
}
|
|
2491
|
-
const MAX_SECRET_BYTES = 5 * 1024 * 1024; // 5 MB
|
|
2492
|
-
let value;
|
|
2493
|
-
if (fromFile !== undefined) {
|
|
2494
|
-
if (!fs.existsSync(fromFile)) {
|
|
2495
|
-
throw new NotFoundError(`File not found: ${fromFile}`, "FILE_NOT_FOUND");
|
|
2496
|
-
}
|
|
2497
|
-
value = fs.readFileSync(fromFile);
|
|
2498
|
-
if (value.byteLength > MAX_SECRET_BYTES) {
|
|
2499
|
-
throw new UsageError("Secret exceeds the 5 MB limit.");
|
|
2500
|
-
}
|
|
2501
|
-
}
|
|
2502
|
-
else if (fromEnv !== undefined) {
|
|
2503
|
-
const envVal = process.env[fromEnv];
|
|
2504
|
-
if (envVal === undefined) {
|
|
2505
|
-
throw new UsageError(`Environment variable "${fromEnv}" is not set.`, "INVALID_FLAG_VALUE");
|
|
2506
|
-
}
|
|
2507
|
-
value = Buffer.from(envVal, "utf8");
|
|
2508
|
-
}
|
|
2509
|
-
else {
|
|
2510
|
-
if (process.stdin.isTTY) {
|
|
2511
|
-
process.stderr.write(`Enter value for secret "${name}" (Ctrl-D when done):\n`);
|
|
2512
|
-
}
|
|
2513
|
-
let totalBytes = 0;
|
|
2514
|
-
const chunks = [];
|
|
2515
|
-
for await (const chunk of Bun.stdin.stream()) {
|
|
2516
|
-
totalBytes += chunk.byteLength;
|
|
2517
|
-
if (totalBytes > MAX_SECRET_BYTES) {
|
|
2518
|
-
throw new UsageError("Secret exceeds the 5 MB limit.");
|
|
2519
|
-
}
|
|
2520
|
-
chunks.push(chunk);
|
|
2521
|
-
}
|
|
2522
|
-
// Strip a single trailing newline so `echo "$TOKEN" | akm secret set`
|
|
2523
|
-
// stores the token without the shell-added newline. Use --from-file for
|
|
2524
|
-
// byte-exact storage of multi-line material (PEM keys, certs).
|
|
2525
|
-
value = Buffer.from(Buffer.concat(chunks).toString("utf8").replace(/\n$/, ""), "utf8");
|
|
2526
|
-
}
|
|
2527
|
-
setSecret(absPath, value);
|
|
2528
|
-
output("secret-set", { ref: makeSecretRef(name, source) });
|
|
2529
|
-
});
|
|
2530
|
-
},
|
|
2531
|
-
});
|
|
2532
|
-
const secretPathCommand = defineCommand({
|
|
2533
|
-
meta: {
|
|
2534
|
-
name: "path",
|
|
2535
|
-
description: "Print the absolute secret file path for the Docker `_FILE` convention, e.g. `MY_SECRET_FILE=$(akm secret path secret:deploy-key)`.",
|
|
2536
|
-
},
|
|
2537
|
-
args: {
|
|
2538
|
-
ref: { type: "positional", description: "Secret ref", required: true },
|
|
2539
|
-
},
|
|
2540
|
-
run({ args }) {
|
|
2541
|
-
return runWithJsonErrors(async () => {
|
|
2542
|
-
const { name, absPath, source } = resolveSecretPath(args.ref);
|
|
2543
|
-
if (!fs.existsSync(absPath)) {
|
|
2544
|
-
throw new NotFoundError(`Secret not found: ${makeSecretRef(name, source)}`);
|
|
2545
|
-
}
|
|
2546
|
-
process.stdout.write(`${absPath}\n`);
|
|
2547
|
-
});
|
|
2548
|
-
},
|
|
2549
|
-
});
|
|
2550
|
-
const secretRunCommand = defineCommand({
|
|
2551
|
-
meta: {
|
|
2552
|
-
name: "run",
|
|
2553
|
-
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.",
|
|
2554
|
-
},
|
|
2555
|
-
args: {
|
|
2556
|
-
ref: { type: "positional", description: "Secret ref", required: true },
|
|
2557
|
-
var: { type: "positional", description: "Environment variable name to inject the value into", required: true },
|
|
2558
|
-
},
|
|
2559
|
-
run({ args }) {
|
|
2560
|
-
return runWithJsonErrors(async () => {
|
|
2561
|
-
// Validate the target env var name FIRST (before the command split) so a
|
|
2562
|
-
// dangerous/invalid name is rejected regardless of how the command is
|
|
2563
|
-
// supplied — and so the failure does not depend on argv parsing.
|
|
2564
|
-
const varName = args.var;
|
|
2565
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(varName)) {
|
|
2566
|
-
throw new UsageError(`"${varName}" is not a valid environment variable name.`, "INVALID_FLAG_VALUE");
|
|
2567
|
-
}
|
|
2568
|
-
const { isDangerousEnvKey } = await import("./commands/lint/env-key-rules.js");
|
|
2569
|
-
if (isDangerousEnvKey(varName)) {
|
|
2570
|
-
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");
|
|
2571
|
-
}
|
|
2572
|
-
const dashIndex = process.argv.indexOf("--");
|
|
2573
|
-
if (dashIndex < 0 || dashIndex === process.argv.length - 1) {
|
|
2574
|
-
throw new UsageError("Missing command. Usage: akm secret run <ref> <VAR> -- <command>");
|
|
2575
|
-
}
|
|
2576
|
-
const command = process.argv.slice(dashIndex + 1);
|
|
2577
|
-
const { name, absPath, source } = resolveSecretPath(args.ref);
|
|
2578
|
-
if (!fs.existsSync(absPath)) {
|
|
2579
|
-
throw new NotFoundError(`Secret not found: ${makeSecretRef(name, source)}`);
|
|
2580
|
-
}
|
|
2581
|
-
const { readValue } = await import("./commands/secret.js");
|
|
2582
|
-
const mergedEnv = { ...process.env };
|
|
2583
|
-
mergedEnv[varName] = readValue(absPath).toString("utf8");
|
|
2584
|
-
// Audit trail: record access by ref + var name only — never the value.
|
|
2585
|
-
appendEvent({
|
|
2586
|
-
eventType: "secret_access",
|
|
2587
|
-
ref: makeSecretRef(name, source),
|
|
2588
|
-
metadata: { var: varName },
|
|
2589
|
-
});
|
|
2590
|
-
const result = spawnSync(command[0], command.slice(1), {
|
|
2591
|
-
stdio: "inherit",
|
|
2592
|
-
env: mergedEnv,
|
|
2593
|
-
});
|
|
2594
|
-
if (result.error) {
|
|
2595
|
-
const err = result.error;
|
|
2596
|
-
if (err.code === "ENOENT") {
|
|
2597
|
-
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'.`);
|
|
2598
|
-
}
|
|
2599
|
-
if (err.code === "EACCES") {
|
|
2600
|
-
throw new ConfigError(`Command not executable: ${command[0]}`, "STASH_DIR_UNREADABLE", `Add execute permission ('chmod +x ${command[0]}') or invoke via an interpreter.`);
|
|
2601
|
-
}
|
|
2602
|
-
throw err;
|
|
2603
|
-
}
|
|
2604
|
-
process.exit(result.status ?? 0);
|
|
2605
|
-
});
|
|
2606
|
-
},
|
|
2607
|
-
});
|
|
2608
|
-
const secretRemoveCommand = defineCommand({
|
|
2609
|
-
meta: { name: "remove", description: "Remove a secret (and its .sensitive marker, if any)" },
|
|
2610
|
-
args: {
|
|
2611
|
-
ref: { type: "positional", description: "Secret ref", required: true },
|
|
2612
|
-
yes: { type: "boolean", alias: "y", description: "Skip confirmation prompt", default: false },
|
|
2613
|
-
},
|
|
2614
|
-
run({ args }) {
|
|
2615
|
-
return runWithJsonErrors(async () => {
|
|
2616
|
-
const { name, absPath, source } = resolveSecretPath(args.ref);
|
|
2617
|
-
const { confirmDestructive } = await import("./cli/confirm.js");
|
|
2618
|
-
const confirmed = await confirmDestructive(`Remove secret "${args.ref}"? This cannot be undone.`, {
|
|
2619
|
-
yes: args.yes === true,
|
|
2620
|
-
});
|
|
2621
|
-
if (!confirmed) {
|
|
2622
|
-
process.stderr.write("Aborted.\n");
|
|
2623
|
-
return;
|
|
2624
|
-
}
|
|
2625
|
-
const { removeSecret } = await import("./commands/secret.js");
|
|
2626
|
-
if (!fs.existsSync(absPath)) {
|
|
2627
|
-
throw new NotFoundError(`Secret not found: ${makeSecretRef(name, source)}`);
|
|
2628
|
-
}
|
|
2629
|
-
const removed = removeSecret(absPath);
|
|
2630
|
-
output("secret-remove", { ref: makeSecretRef(name, source), removed });
|
|
2631
|
-
});
|
|
2632
|
-
},
|
|
2633
|
-
});
|
|
2634
|
-
const secretCommand = defineCommand({
|
|
2635
|
-
meta: {
|
|
2636
|
-
name: "secret",
|
|
2637
|
-
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`.",
|
|
2638
|
-
},
|
|
2639
|
-
subCommands: {
|
|
2640
|
-
list: secretListCommand,
|
|
2641
|
-
path: secretPathCommand,
|
|
2642
|
-
run: secretRunCommand,
|
|
2643
|
-
set: secretSetCommand,
|
|
2644
|
-
remove: secretRemoveCommand,
|
|
2645
|
-
},
|
|
2646
|
-
run({ args }) {
|
|
2647
|
-
return runWithJsonErrors(async () => {
|
|
2648
|
-
if (hasSubcommand(args, SECRET_SUBCOMMAND_SET))
|
|
2649
|
-
return;
|
|
2650
|
-
output("secret-list", { secrets: listSecretsRecursive() });
|
|
2651
|
-
});
|
|
2652
|
-
},
|
|
2653
|
-
});
|
|
2654
|
-
// ── Wiki subcommands ─────────────────────────────────────────────────────────
|
|
2655
|
-
const wikiCreateCommand = defineCommand({
|
|
2656
|
-
meta: { name: "create", description: "Scaffold a new wiki under <stashDir>/wikis/<name>/" },
|
|
2657
|
-
args: {
|
|
2658
|
-
name: { type: "positional", description: "Wiki name (lowercase, digits, hyphens)", required: true },
|
|
2659
|
-
},
|
|
2660
|
-
run({ args }) {
|
|
2661
|
-
return runWithJsonErrors(async () => {
|
|
2662
|
-
const { createWiki } = await import("./wiki/wiki.js");
|
|
2663
|
-
const stashDir = resolveStashDir();
|
|
2664
|
-
const result = createWiki(stashDir, args.name);
|
|
2665
|
-
output("wiki-create", result);
|
|
2666
|
-
});
|
|
2667
|
-
},
|
|
2668
|
-
});
|
|
2669
|
-
const wikiRegisterCommand = defineCommand({
|
|
2670
|
-
meta: {
|
|
2671
|
-
name: "register",
|
|
2672
|
-
description: "Register an existing directory or repo as a first-class wiki without copying or mutating it; refreshes source and wiki search state immediately",
|
|
2673
|
-
},
|
|
2674
|
-
args: {
|
|
2675
|
-
name: { type: "positional", description: "Wiki name (lowercase, digits, hyphens)", required: true },
|
|
2676
|
-
ref: { type: "positional", description: "Path or repo ref for the external wiki source", required: true },
|
|
2677
|
-
writable: {
|
|
2678
|
-
type: "boolean",
|
|
2679
|
-
description: "Mark a git-backed source as writable so changes can be pushed back",
|
|
2680
|
-
default: false,
|
|
2681
|
-
},
|
|
2682
|
-
"max-pages": { type: "string", description: "Maximum pages to crawl for website sources (default: 50)" },
|
|
2683
|
-
"max-depth": { type: "string", description: "Maximum crawl depth for website sources (default: 3)" },
|
|
2684
|
-
},
|
|
2685
|
-
run({ args }) {
|
|
2686
|
-
return runWithJsonErrors(async () => {
|
|
2687
|
-
const { registerWikiSource } = await import("./commands/source-add");
|
|
2688
|
-
const result = await registerWikiSource({
|
|
2689
|
-
ref: args.ref.trim(),
|
|
2690
|
-
name: args.name,
|
|
2691
|
-
options: Object.keys(buildWebsiteOptions(args)).length > 0 ? buildWebsiteOptions(args) : undefined,
|
|
2692
|
-
writable: args.writable,
|
|
2693
|
-
});
|
|
2694
|
-
output("wiki-register", result);
|
|
2695
|
-
});
|
|
2696
|
-
},
|
|
2697
|
-
});
|
|
2698
|
-
const wikiListCommand = defineCommand({
|
|
2699
|
-
meta: { name: "list", description: "List wikis with page/raw counts and last-modified timestamps" },
|
|
2700
|
-
run() {
|
|
2701
|
-
return runWithJsonErrors(async () => {
|
|
2702
|
-
const { listWikis } = await import("./wiki/wiki.js");
|
|
2703
|
-
const stashDir = resolveStashDir();
|
|
2704
|
-
const wikis = listWikis(stashDir);
|
|
2705
|
-
output("wiki-list", { wikis });
|
|
2706
|
-
});
|
|
2707
|
-
},
|
|
2708
|
-
});
|
|
2709
|
-
const wikiShowCommand = defineCommand({
|
|
2710
|
-
meta: { name: "show", description: "Show a wiki's path, description, counts, and last 3 log entries" },
|
|
2711
|
-
args: {
|
|
2712
|
-
name: { type: "positional", description: "Wiki name", required: true },
|
|
2713
|
-
},
|
|
2714
|
-
run({ args }) {
|
|
2715
|
-
return runWithJsonErrors(async () => {
|
|
2716
|
-
const { showWiki } = await import("./wiki/wiki.js");
|
|
2717
|
-
const stashDir = resolveStashDir();
|
|
2718
|
-
const result = showWiki(stashDir, args.name);
|
|
2719
|
-
output("wiki-show", result);
|
|
2720
|
-
});
|
|
2721
|
-
},
|
|
2722
|
-
});
|
|
2723
|
-
const wikiRemoveCommand = defineCommand({
|
|
2724
|
-
meta: {
|
|
2725
|
-
name: "remove",
|
|
2726
|
-
description: "Remove a wiki and refresh the index. Preserves raw/ by default; pass --with-sources to also delete raw/",
|
|
2727
|
-
},
|
|
2728
|
-
args: {
|
|
2729
|
-
name: { type: "positional", description: "Wiki name", required: true },
|
|
2730
|
-
yes: {
|
|
2731
|
-
type: "boolean",
|
|
2732
|
-
alias: "y",
|
|
2733
|
-
description: "Skip confirmation prompt (required in non-interactive shells)",
|
|
2734
|
-
default: false,
|
|
2735
|
-
},
|
|
2736
|
-
force: {
|
|
2737
|
-
type: "boolean",
|
|
2738
|
-
description: "DEPRECATED — use -y/--yes. Removed in 0.9.0.",
|
|
2739
|
-
default: false,
|
|
2740
|
-
},
|
|
2741
|
-
"with-sources": {
|
|
2742
|
-
type: "boolean",
|
|
2743
|
-
description: "Also delete the raw/ directory (immutable ingested sources)",
|
|
2744
|
-
default: false,
|
|
2745
|
-
},
|
|
2746
|
-
},
|
|
2747
|
-
run({ args }) {
|
|
2748
|
-
return runWithJsonErrors(async () => {
|
|
2749
|
-
if (args.yes !== true && args.force === true) {
|
|
2750
|
-
emitFlagDeprecation("--force", "-y/--yes", "wiki remove");
|
|
2751
|
-
}
|
|
2752
|
-
const { confirmDestructive } = await import("./cli/confirm.js");
|
|
2753
|
-
const confirmed = await confirmDestructive(`Remove wiki "${args.name}"? This cannot be undone.`, {
|
|
2754
|
-
yes: args.yes === true || args.force === true,
|
|
2755
|
-
});
|
|
2756
|
-
if (!confirmed) {
|
|
2757
|
-
process.stderr.write("Aborted.\n");
|
|
2758
|
-
return;
|
|
2759
|
-
}
|
|
2760
|
-
const withSources = getHyphenatedBoolean(args, "with-sources");
|
|
2761
|
-
const { removeWiki } = await import("./wiki/wiki.js");
|
|
2762
|
-
const { akmIndex } = await import("./indexer/indexer");
|
|
2763
|
-
const stashDir = resolveStashDir();
|
|
2764
|
-
const result = removeWiki(stashDir, args.name, { withSources });
|
|
2765
|
-
await akmIndex({ stashDir });
|
|
2766
|
-
output("wiki-remove", result);
|
|
2767
|
-
});
|
|
2768
|
-
},
|
|
2769
|
-
});
|
|
2770
|
-
const wikiPagesCommand = defineCommand({
|
|
2771
|
-
meta: {
|
|
2772
|
-
name: "pages",
|
|
2773
|
-
description: "List wiki pages (ref + frontmatter description), excluding schema/index/log/raw",
|
|
2774
|
-
},
|
|
2775
|
-
args: {
|
|
2776
|
-
name: { type: "positional", description: "Wiki name", required: true },
|
|
2777
|
-
},
|
|
2778
|
-
run({ args }) {
|
|
2779
|
-
return runWithJsonErrors(async () => {
|
|
2780
|
-
const { listPages } = await import("./wiki/wiki.js");
|
|
2781
|
-
const stashDir = resolveStashDir();
|
|
2782
|
-
const pages = listPages(stashDir, args.name);
|
|
2783
|
-
output("wiki-pages", { wiki: args.name, pages });
|
|
2784
|
-
});
|
|
2785
|
-
},
|
|
2786
|
-
});
|
|
2787
|
-
const wikiSearchCommand = defineCommand({
|
|
2788
|
-
meta: {
|
|
2789
|
-
name: "search",
|
|
2790
|
-
description: "Search wiki pages within a single wiki (scoped wrapper over `akm search --type wiki`; excludes raw/schema/index/log and returns canonical wiki refs)",
|
|
2791
|
-
},
|
|
2792
|
-
args: {
|
|
2793
|
-
name: { type: "positional", description: "Wiki name", required: true },
|
|
2794
|
-
query: { type: "positional", description: "Search query", required: true },
|
|
2795
|
-
limit: { type: "string", description: "Max hits (default 20)", required: false },
|
|
2796
|
-
},
|
|
2797
|
-
run({ args }) {
|
|
2798
|
-
return runWithJsonErrors(async () => {
|
|
2799
|
-
const { resolveWikiSource, searchInWiki } = await import("./wiki/wiki.js");
|
|
2800
|
-
const stashDir = resolveStashDir();
|
|
2801
|
-
resolveWikiSource(stashDir, args.name);
|
|
2802
|
-
const parsedLimit = args.limit ? Number(args.limit) : undefined;
|
|
2803
|
-
const limit = typeof parsedLimit === "number" && Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : undefined;
|
|
2804
|
-
const response = await searchInWiki({ stashDir, wikiName: args.name, query: args.query, limit });
|
|
2805
|
-
output("search", response);
|
|
2806
|
-
});
|
|
2807
|
-
},
|
|
2808
|
-
});
|
|
2809
|
-
const wikiStashCommand = defineCommand({
|
|
2810
|
-
meta: {
|
|
2811
|
-
name: "stash",
|
|
2812
|
-
description: "Copy a source into wikis/<name>/raw/<slug>.md with frontmatter. Source may be a file path, URL, or '-' for stdin.",
|
|
2813
|
-
},
|
|
2814
|
-
args: {
|
|
2815
|
-
name: { type: "positional", description: "Wiki name", required: true },
|
|
2816
|
-
source: { type: "positional", description: "Source file path, URL, or '-' to read from stdin", required: true },
|
|
2817
|
-
as: { type: "string", description: "Preferred slug base (defaults to source filename or first-line slug)" },
|
|
2818
|
-
target: {
|
|
2819
|
-
type: "string",
|
|
2820
|
-
description: "Name of a writable stash source to write into instead of the default stash. Must match a configured source name (run `akm list` to see sources).",
|
|
2821
|
-
},
|
|
2822
|
-
},
|
|
2823
|
-
run({ args }) {
|
|
2824
|
-
return runWithJsonErrors(async () => {
|
|
2825
|
-
const { stashRaw } = await import("./wiki/wiki.js");
|
|
2826
|
-
const { content, preferredName } = await (async () => {
|
|
2827
|
-
if (!isHttpUrl(args.source))
|
|
2828
|
-
return readKnowledgeInput(args.source);
|
|
2829
|
-
const { fetchWebsiteMarkdownSnapshot } = await import("./sources/website-ingest");
|
|
2830
|
-
const snapshot = await fetchWebsiteMarkdownSnapshot(args.source);
|
|
2831
|
-
return { content: snapshot.content, preferredName: args.as ?? snapshot.preferredName };
|
|
2832
|
-
})();
|
|
2833
|
-
let stashDir;
|
|
2834
|
-
if (args.target) {
|
|
2835
|
-
// Resolve the named source to its filesystem path.
|
|
2836
|
-
const cfg = loadConfig();
|
|
2837
|
-
const sources = resolveConfiguredSources(cfg);
|
|
2838
|
-
const match = sources.find((s) => s.name === args.target);
|
|
2839
|
-
if (!match) {
|
|
2840
|
-
throw new UsageError(`--target must reference a configured source name. No source named "${args.target}" found. Run \`akm list\` to see available sources.`, "INVALID_FLAG_VALUE");
|
|
2841
|
-
}
|
|
2842
|
-
const spec = match.source;
|
|
2843
|
-
if (spec.type !== "filesystem" && spec.type !== "local") {
|
|
2844
|
-
throw new ConfigError(`Source "${args.target}" is not a filesystem source and cannot be used as a wiki stash target.`, "INVALID_CONFIG_FILE", `Use a source with type "filesystem" or "local", or omit --target to use the default stash.`);
|
|
2845
|
-
}
|
|
2846
|
-
stashDir = spec.path;
|
|
2847
|
-
}
|
|
2848
|
-
else {
|
|
2849
|
-
stashDir = resolveStashDir();
|
|
2850
|
-
}
|
|
2851
|
-
const result = stashRaw({
|
|
2852
|
-
stashDir,
|
|
2853
|
-
wikiName: args.name,
|
|
2854
|
-
content,
|
|
2855
|
-
preferredName: args.as ?? preferredName,
|
|
2856
|
-
explicitSlug: args.as !== undefined,
|
|
2857
|
-
});
|
|
2858
|
-
output("wiki-stash", { ok: true, wiki: args.name, source: args.source, ...result });
|
|
2859
|
-
});
|
|
2860
|
-
},
|
|
2861
|
-
});
|
|
2862
|
-
const wikiLintCommand = defineCommand({
|
|
2863
|
-
meta: {
|
|
2864
|
-
name: "lint",
|
|
2865
|
-
description: "Structural lint for a wiki: orphans, broken xrefs, missing descriptions, uncited raws, stale index",
|
|
2866
|
-
},
|
|
2867
|
-
args: {
|
|
2868
|
-
name: { type: "positional", description: "Wiki name", required: true },
|
|
2869
|
-
},
|
|
2870
|
-
async run({ args }) {
|
|
2871
|
-
let findingCount = 0;
|
|
2872
|
-
await runWithJsonErrors(async () => {
|
|
2873
|
-
const { lintWiki } = await import("./wiki/wiki.js");
|
|
2874
|
-
const stashDir = resolveStashDir();
|
|
2875
|
-
const report = lintWiki(stashDir, args.name);
|
|
2876
|
-
output("wiki-lint", report);
|
|
2877
|
-
findingCount = report.findings.length;
|
|
2878
|
-
});
|
|
2879
|
-
if (findingCount > 0)
|
|
2880
|
-
process.exit(1); // EXIT_GENERAL
|
|
2881
|
-
},
|
|
2882
|
-
});
|
|
2883
|
-
const wikiIngestCommand = defineCommand({
|
|
2884
|
-
meta: {
|
|
2885
|
-
name: "ingest",
|
|
2886
|
-
description: "Dispatch an agent to execute the ingest workflow for this wiki. Uses --profile or config.defaults.agent.",
|
|
2887
|
-
},
|
|
2888
|
-
args: {
|
|
2889
|
-
name: { type: "positional", description: "Wiki name", required: true },
|
|
2890
|
-
profile: {
|
|
2891
|
-
type: "string",
|
|
2892
|
-
description: "Agent profile to use (default: config.defaults.agent).",
|
|
2893
|
-
},
|
|
2894
|
-
model: {
|
|
2895
|
-
type: "string",
|
|
2896
|
-
description: "Model override — accepts aliases (opus, sonnet, haiku) or exact platform model IDs.",
|
|
2897
|
-
},
|
|
2898
|
-
"timeout-ms": { type: "string", description: "Override the agent CLI timeout in milliseconds." },
|
|
2899
|
-
},
|
|
2900
|
-
run({ args }) {
|
|
2901
|
-
return runWithJsonErrors(async () => {
|
|
2902
|
-
const { buildIngestWorkflow } = await import("./wiki/wiki.js");
|
|
2903
|
-
const stashDir = resolveStashDir();
|
|
2904
|
-
const built = buildIngestWorkflow(stashDir, args.name);
|
|
2905
|
-
const config = loadConfig();
|
|
2906
|
-
const profileName = getStringArg(args, "profile") ?? config.defaults?.agent;
|
|
2907
|
-
if (!profileName) {
|
|
2908
|
-
throw new UsageError("akm wiki ingest requires an agent profile. Pass --profile <name> or set defaults.agent in config.", "MISSING_REQUIRED_ARGUMENT", "Available profiles are listed under profiles.agent in your config. Run `akm config get profiles.agent` to inspect.");
|
|
2909
|
-
}
|
|
2910
|
-
const timeoutMs = parsePositiveIntFlag(getHyphenatedArg(args, "timeout-ms"), "--timeout-ms");
|
|
2911
|
-
const model = getStringArg(args, "model");
|
|
2912
|
-
const { getDefaultLlmConfig } = await import("./core/config.js");
|
|
2913
|
-
const dispatchResult = await akmAgentDispatch({
|
|
2914
|
-
profileName,
|
|
2915
|
-
agentConfig: config,
|
|
2916
|
-
llmConfig: getDefaultLlmConfig(config),
|
|
2917
|
-
prompt: built.workflow,
|
|
2918
|
-
dispatch: {
|
|
2919
|
-
prompt: built.workflow,
|
|
2920
|
-
...(model !== undefined ? { model } : {}),
|
|
2921
|
-
},
|
|
2922
|
-
...(timeoutMs !== undefined && Number.isFinite(timeoutMs) ? { timeoutMs } : {}),
|
|
2923
|
-
});
|
|
2924
|
-
output("wiki-ingest", {
|
|
2925
|
-
wiki: built.wiki,
|
|
2926
|
-
path: built.path,
|
|
2927
|
-
schemaPath: built.schemaPath,
|
|
2928
|
-
dispatched: true,
|
|
2929
|
-
profile: profileName,
|
|
2930
|
-
agentResult: dispatchResult,
|
|
2931
|
-
});
|
|
2932
|
-
});
|
|
2933
|
-
},
|
|
2934
|
-
});
|
|
2935
|
-
const wikiCommand = defineCommand({
|
|
2936
|
-
meta: {
|
|
2937
|
-
name: "wiki",
|
|
2938
|
-
description: "Manage multiple markdown wikis (Karpathy-style). akm surfaces (lifecycle, raw/, lint, index); the agent writes pages.",
|
|
2939
|
-
},
|
|
2940
|
-
subCommands: {
|
|
2941
|
-
create: wikiCreateCommand,
|
|
2942
|
-
register: wikiRegisterCommand,
|
|
2943
|
-
list: wikiListCommand,
|
|
2944
|
-
show: wikiShowCommand,
|
|
2945
|
-
remove: wikiRemoveCommand,
|
|
2946
|
-
pages: wikiPagesCommand,
|
|
2947
|
-
search: wikiSearchCommand,
|
|
2948
|
-
stash: wikiStashCommand,
|
|
2949
|
-
lint: wikiLintCommand,
|
|
2950
|
-
ingest: wikiIngestCommand,
|
|
2951
|
-
},
|
|
2952
|
-
run({ args }) {
|
|
2953
|
-
return runWithJsonErrors(async () => {
|
|
2954
|
-
if (hasSubcommand(args, WIKI_SUBCOMMAND_SET))
|
|
2955
|
-
return;
|
|
2956
|
-
// Default action: list wikis
|
|
2957
|
-
const { listWikis } = await import("./wiki/wiki.js");
|
|
2958
|
-
output("wiki-list", { wikis: listWikis(resolveStashDir()) });
|
|
2959
|
-
});
|
|
2960
|
-
},
|
|
2961
|
-
});
|
|
2962
|
-
// ── `akm events` ────────────────────────────────────────────────────────────
|
|
2963
|
-
// Append-only events stream surface (#204). `list` reads state.db events
|
|
2964
|
-
// with optional --since/--type/--ref filters; `tail` follows the table via
|
|
2965
|
-
// a polling loop and prints each event as a single JSONL line.
|
|
2966
|
-
const eventsListCommand = defineCommand({
|
|
2967
|
-
meta: { name: "list", description: "List events from the append-only state.db events stream" },
|
|
2968
|
-
args: {
|
|
2969
|
-
since: {
|
|
2970
|
-
type: "string",
|
|
2971
|
-
description: "ISO timestamp / epoch ms, OR `@offset:<id>` for a durable row-id cursor (resume across processes)",
|
|
2972
|
-
},
|
|
2973
|
-
type: { type: "string", description: "Filter by event type (add, remove, remember, feedback, ...)" },
|
|
2974
|
-
ref: { type: "string", description: "Filter by asset ref (type:name)" },
|
|
2975
|
-
"exclude-tags": {
|
|
2976
|
-
type: "string",
|
|
2977
|
-
description: "Exclude events matching these tags (repeatable)",
|
|
2978
|
-
},
|
|
2979
|
-
"include-tags": {
|
|
2980
|
-
type: "string",
|
|
2981
|
-
description: "Only include events with ALL these tags (repeatable)",
|
|
2982
|
-
},
|
|
2983
|
-
},
|
|
2984
|
-
run({ args }) {
|
|
2985
|
-
return runWithJsonErrors(() => {
|
|
2986
|
-
const excludeTags = parseAllFlagValues("--exclude-tags");
|
|
2987
|
-
const includeTags = parseAllFlagValues("--include-tags");
|
|
2988
|
-
const result = akmEventsList({
|
|
2989
|
-
since: args.since,
|
|
2990
|
-
type: args.type,
|
|
2991
|
-
ref: args.ref,
|
|
2992
|
-
...(excludeTags.length > 0 ? { excludeTags } : {}),
|
|
2993
|
-
...(includeTags.length > 0 ? { includeTags } : {}),
|
|
2994
|
-
});
|
|
2995
|
-
output("events-list", result);
|
|
2996
|
-
});
|
|
2997
|
-
},
|
|
2998
|
-
});
|
|
2999
|
-
const eventsTailCommand = defineCommand({
|
|
3000
|
-
meta: { name: "tail", description: "Follow the append-only state.db events stream (polling)" },
|
|
3001
|
-
args: {
|
|
3002
|
-
since: {
|
|
3003
|
-
type: "string",
|
|
3004
|
-
description: "ISO timestamp / epoch ms, OR `@offset:<id>` for a durable row-id cursor (resume across processes)",
|
|
3005
|
-
},
|
|
3006
|
-
type: { type: "string", description: "Filter by event type" },
|
|
3007
|
-
ref: { type: "string", description: "Filter by asset ref (type:name)" },
|
|
3008
|
-
"interval-ms": { type: "string", description: "Polling interval in ms (default: 75)" },
|
|
3009
|
-
"max-duration-ms": { type: "string", description: "Stop after this many ms (default: never)" },
|
|
3010
|
-
"max-events": { type: "string", description: "Stop after observing this many events" },
|
|
3011
|
-
"exclude-tags": {
|
|
3012
|
-
type: "string",
|
|
3013
|
-
description: "Exclude events matching these tags (repeatable)",
|
|
3014
|
-
},
|
|
3015
|
-
"include-tags": {
|
|
3016
|
-
type: "string",
|
|
3017
|
-
description: "Only include events with ALL these tags (repeatable)",
|
|
3018
|
-
},
|
|
3019
|
-
},
|
|
3020
|
-
async run({ args }) {
|
|
3021
|
-
await runWithJsonErrors(async () => {
|
|
3022
|
-
const intervalMs = parsePositiveIntFlag(getHyphenatedArg(args, "interval-ms"), "--interval-ms");
|
|
3023
|
-
const maxDurationMs = parsePositiveIntFlag(getHyphenatedArg(args, "max-duration-ms"), "--max-duration-ms");
|
|
3024
|
-
const maxEvents = parsePositiveIntFlag(getHyphenatedArg(args, "max-events"), "--max-events");
|
|
3025
|
-
const mode = getOutputMode();
|
|
3026
|
-
// In streaming text mode we want each event to print as soon as it
|
|
3027
|
-
// arrives. The polling loop emits via `onEvent`; the final result is
|
|
3028
|
-
// also rendered through the standard output() pipeline so JSON
|
|
3029
|
-
// consumers always get the canonical envelope.
|
|
3030
|
-
const stream = mode.format === "text" || mode.format === "jsonl";
|
|
3031
|
-
const excludeTags = parseAllFlagValues("--exclude-tags");
|
|
3032
|
-
const includeTags = parseAllFlagValues("--include-tags");
|
|
3033
|
-
const result = await akmEventsTail({
|
|
3034
|
-
since: args.since,
|
|
3035
|
-
type: args.type,
|
|
3036
|
-
ref: args.ref,
|
|
3037
|
-
intervalMs,
|
|
3038
|
-
maxDurationMs,
|
|
3039
|
-
maxEvents,
|
|
3040
|
-
...(excludeTags.length > 0 ? { excludeTags } : {}),
|
|
3041
|
-
...(includeTags.length > 0 ? { includeTags } : {}),
|
|
3042
|
-
onEvent: stream
|
|
3043
|
-
? (event) => {
|
|
3044
|
-
if (mode.format === "jsonl") {
|
|
3045
|
-
console.log(JSON.stringify(event));
|
|
3046
|
-
}
|
|
3047
|
-
else {
|
|
3048
|
-
console.log(formatEventLine(event));
|
|
3049
|
-
}
|
|
3050
|
-
}
|
|
3051
|
-
: undefined,
|
|
3052
|
-
});
|
|
3053
|
-
// Emit the canonical envelope last (JSON/YAML modes rely on this;
|
|
3054
|
-
// streaming modes already printed each event but we still emit a
|
|
3055
|
-
// trailer so callers can persist the resumable cursor).
|
|
3056
|
-
if (!stream) {
|
|
3057
|
-
output("events-tail", result);
|
|
3058
|
-
}
|
|
3059
|
-
else if (mode.format === "jsonl") {
|
|
3060
|
-
// Final discriminated trailer row so jsonl consumers can resume.
|
|
3061
|
-
const trailer = {
|
|
3062
|
-
_kind: "trailer",
|
|
3063
|
-
schemaVersion: 1,
|
|
3064
|
-
nextOffset: result.nextOffset,
|
|
3065
|
-
totalCount: result.totalCount,
|
|
3066
|
-
reason: result.reason,
|
|
3067
|
-
};
|
|
3068
|
-
console.log(JSON.stringify(trailer));
|
|
3069
|
-
}
|
|
3070
|
-
else {
|
|
3071
|
-
// text mode: keep stdout pristine for line-oriented parsers and
|
|
3072
|
-
// emit the trailer on stderr.
|
|
3073
|
-
process.stderr.write(`[events-tail] reason=${result.reason} nextOffset=${result.nextOffset} total=${result.totalCount}\n`);
|
|
3074
|
-
}
|
|
3075
|
-
});
|
|
3076
|
-
},
|
|
3077
|
-
});
|
|
3078
|
-
const eventsCommand = defineCommand({
|
|
3079
|
-
meta: {
|
|
3080
|
-
name: "events",
|
|
3081
|
-
alias: "log",
|
|
3082
|
-
description: "Read or follow the append-only state.db events stream (mutations, feedback, indexing)",
|
|
3083
|
-
},
|
|
3084
|
-
subCommands: {
|
|
3085
|
-
list: eventsListCommand,
|
|
3086
|
-
tail: eventsTailCommand,
|
|
3087
|
-
},
|
|
3088
|
-
});
|
|
3089
|
-
// ── lessons subcommands (Phase 7A / Advantage D4c) ──────────────────────────
|
|
3090
|
-
const lessonsCoverageCommand = defineCommand({
|
|
3091
|
-
meta: {
|
|
3092
|
-
name: "coverage",
|
|
3093
|
-
description: "Report tags that exist on indexed assets but are NOT yet covered by any lesson.\n\n" +
|
|
3094
|
-
"Useful for spotting topics where the stash has skills/commands/scripts but no\n" +
|
|
3095
|
-
"crystallized lesson — a signal that the team has tacit knowledge worth distilling.\n\n" +
|
|
3096
|
-
"Default output is JSON: { uncoveredTags: string[], lessonTagCount: number, totalTagCount: number }.\n" +
|
|
3097
|
-
"Pass --format text for a plain-text bulleted list.",
|
|
3098
|
-
},
|
|
3099
|
-
args: {},
|
|
3100
|
-
run() {
|
|
3101
|
-
return runWithJsonErrors(() => {
|
|
3102
|
-
const db = openExistingDatabase();
|
|
3103
|
-
try {
|
|
3104
|
-
const allTagSet = collectTagSetFromEntries(db, undefined);
|
|
3105
|
-
const lessonTagSet = collectTagSetFromEntries(db, "lesson");
|
|
3106
|
-
const uncovered = [];
|
|
3107
|
-
for (const tag of allTagSet) {
|
|
3108
|
-
if (!lessonTagSet.has(tag))
|
|
3109
|
-
uncovered.push(tag);
|
|
3110
|
-
}
|
|
3111
|
-
uncovered.sort((a, b) => a.localeCompare(b));
|
|
3112
|
-
output("lessons-coverage", {
|
|
3113
|
-
ok: true,
|
|
3114
|
-
uncoveredTags: uncovered,
|
|
3115
|
-
lessonTagCount: lessonTagSet.size,
|
|
3116
|
-
totalTagCount: allTagSet.size,
|
|
3117
|
-
});
|
|
3118
|
-
}
|
|
3119
|
-
finally {
|
|
3120
|
-
closeDatabase(db);
|
|
3121
|
-
}
|
|
3122
|
-
});
|
|
3123
|
-
},
|
|
3124
|
-
});
|
|
3125
|
-
/**
|
|
3126
|
-
* Walk indexed entries and collect a deduplicated set of tags. When
|
|
3127
|
-
* `entryType` is provided, only entries of that type contribute tags.
|
|
3128
|
-
*
|
|
3129
|
-
* Pure read; never mutates the DB. Used by `akm lessons coverage` (Phase 7A)
|
|
3130
|
-
* to compute the diff between all-asset tags and lesson tags.
|
|
3131
|
-
*/
|
|
3132
|
-
function collectTagSetFromEntries(db, entryType) {
|
|
3133
|
-
const tags = new Set();
|
|
3134
|
-
const stmt = entryType
|
|
3135
|
-
? db.prepare("SELECT entry_json FROM entries WHERE entry_type = ?")
|
|
3136
|
-
: db.prepare("SELECT entry_json FROM entries");
|
|
3137
|
-
const rows = (entryType ? stmt.all(entryType) : stmt.all());
|
|
3138
|
-
for (const row of rows) {
|
|
3139
|
-
let parsed;
|
|
3140
|
-
try {
|
|
3141
|
-
parsed = JSON.parse(row.entry_json);
|
|
3142
|
-
}
|
|
3143
|
-
catch {
|
|
3144
|
-
continue;
|
|
3145
|
-
}
|
|
3146
|
-
if (!Array.isArray(parsed.tags))
|
|
3147
|
-
continue;
|
|
3148
|
-
for (const tag of parsed.tags) {
|
|
3149
|
-
if (typeof tag === "string" && tag.trim().length > 0) {
|
|
3150
|
-
tags.add(tag.trim().toLowerCase());
|
|
3151
|
-
}
|
|
3152
|
-
}
|
|
3153
|
-
}
|
|
3154
|
-
return tags;
|
|
3155
|
-
}
|
|
3156
|
-
const lessonsCommand = defineCommand({
|
|
3157
|
-
meta: {
|
|
3158
|
-
name: "lessons",
|
|
3159
|
-
alias: "lesson",
|
|
3160
|
-
description: "Lesson-asset tooling: tag-coverage gaps, strength queries.",
|
|
3161
|
-
},
|
|
3162
|
-
subCommands: {
|
|
3163
|
-
coverage: lessonsCoverageCommand,
|
|
3164
|
-
},
|
|
3165
|
-
});
|
|
3166
|
-
// ── proposal substrate (#225) ────────────────────────────────────────────────
|
|
3167
|
-
const proposalListCommand = defineCommand({
|
|
3168
|
-
meta: { name: "list", description: "List proposal queue entries" },
|
|
3169
|
-
args: {
|
|
3170
|
-
status: {
|
|
3171
|
-
type: "string",
|
|
3172
|
-
description: "Filter by status (pending|accepted|rejected|reverted)",
|
|
3173
|
-
},
|
|
3174
|
-
ref: { type: "string", description: "Filter by asset ref (type:name)" },
|
|
3175
|
-
type: { type: "string", description: "Filter by asset type" },
|
|
3176
|
-
},
|
|
3177
|
-
run({ args }) {
|
|
3178
|
-
return runWithJsonErrors(() => {
|
|
3179
|
-
const status = parseProposalStatus(args.status);
|
|
3180
|
-
const result = akmProposalList({
|
|
3181
|
-
status,
|
|
3182
|
-
ref: args.ref,
|
|
3183
|
-
type: args.type,
|
|
3184
|
-
includeArchive: status === "accepted" || status === "rejected" || status === "reverted",
|
|
3185
|
-
});
|
|
3186
|
-
output("proposal-list", result);
|
|
3187
|
-
});
|
|
3188
|
-
},
|
|
3189
|
-
});
|
|
3190
|
-
const proposalAcceptCommand = defineCommand({
|
|
3191
|
-
meta: { name: "accept", description: "Accept a proposal and promote it into the stash" },
|
|
3192
|
-
args: {
|
|
3193
|
-
id: {
|
|
3194
|
-
type: "positional",
|
|
3195
|
-
description: "Proposal id (uuid / prefix) or asset ref (e.g. skill:akm-dream). Optional when --generator is provided.",
|
|
3196
|
-
required: false,
|
|
3197
|
-
},
|
|
3198
|
-
target: { type: "string", description: "Override the write target by source name" },
|
|
3199
|
-
// F-6 / #393: Batch accept by generator, diff size, or age.
|
|
3200
|
-
generator: {
|
|
3201
|
-
type: "string",
|
|
3202
|
-
description: "F-6: Bulk-accept all pending proposals from this generator (e.g. reflect, distill). Requires no positional id.",
|
|
3203
|
-
},
|
|
3204
|
-
source: {
|
|
3205
|
-
type: "string",
|
|
3206
|
-
description: "DEPRECATED — use --generator. Removed in 0.9.0.",
|
|
3207
|
-
},
|
|
3208
|
-
"max-diff-lines": {
|
|
3209
|
-
type: "string",
|
|
3210
|
-
description: "F-6: When bulk-accepting, only accept proposals whose content is <= this many lines. Skips larger proposals.",
|
|
3211
|
-
},
|
|
3212
|
-
"older-than": {
|
|
3213
|
-
type: "string",
|
|
3214
|
-
description: "F-6: When bulk-accepting, only accept proposals created more than this many days ago (e.g. '7' for 7 days).",
|
|
3215
|
-
},
|
|
3216
|
-
"dry-run": {
|
|
3217
|
-
type: "boolean",
|
|
3218
|
-
description: "F-6: List proposals that would be bulk-accepted without accepting them.",
|
|
3219
|
-
default: false,
|
|
3220
|
-
},
|
|
3221
|
-
yes: {
|
|
3222
|
-
type: "boolean",
|
|
3223
|
-
alias: "y",
|
|
3224
|
-
description: "Skip confirmation prompt (required in non-interactive mode for bulk accept)",
|
|
3225
|
-
default: false,
|
|
3226
|
-
},
|
|
3227
|
-
},
|
|
3228
|
-
async run({ args }) {
|
|
3229
|
-
await runWithJsonErrors(async () => {
|
|
3230
|
-
if (args.generator === undefined && args.source !== undefined) {
|
|
3231
|
-
emitFlagDeprecation("--source", "--generator", "proposal accept");
|
|
3232
|
-
}
|
|
3233
|
-
const generator = (args.generator ?? args.source);
|
|
3234
|
-
// F-6 / #393: Bulk-accept when --generator is provided without a positional id.
|
|
3235
|
-
if (generator && !args.id) {
|
|
3236
|
-
const { confirmDestructive } = await import("./cli/confirm.js");
|
|
3237
|
-
const confirmed = await confirmDestructive(`Bulk-accept all matching proposals from generator "${generator}"? This cannot be undone.`, { yes: args.yes === true || args["dry-run"] === true });
|
|
3238
|
-
if (!confirmed) {
|
|
3239
|
-
process.stderr.write("Aborted.\n");
|
|
3240
|
-
return;
|
|
3241
|
-
}
|
|
3242
|
-
const { listProposals } = await import("./core/proposals");
|
|
3243
|
-
const stashDir = resolveStashDir();
|
|
3244
|
-
const rawMaxDiff = args["max-diff-lines"] ? Number.parseInt(String(args["max-diff-lines"]), 10) : undefined;
|
|
3245
|
-
if (rawMaxDiff !== undefined && (Number.isNaN(rawMaxDiff) || rawMaxDiff < 0)) {
|
|
3246
|
-
throw new UsageError("--max-diff-lines must be a non-negative integer", "INVALID_FLAG_VALUE");
|
|
3247
|
-
}
|
|
3248
|
-
const rawOlderThan = args["older-than"] ? Number.parseInt(String(args["older-than"]), 10) : undefined;
|
|
3249
|
-
if (rawOlderThan !== undefined && (Number.isNaN(rawOlderThan) || rawOlderThan < 0)) {
|
|
3250
|
-
throw new UsageError("--older-than must be a non-negative integer (days)", "INVALID_FLAG_VALUE");
|
|
3251
|
-
}
|
|
3252
|
-
const maxDiffLines = rawMaxDiff;
|
|
3253
|
-
const olderThanMs = rawOlderThan !== undefined ? rawOlderThan * 86_400_000 : undefined;
|
|
3254
|
-
const pending = listProposals(stashDir, { status: "pending" }).filter((p) => {
|
|
3255
|
-
if (p.source !== generator)
|
|
3256
|
-
return false;
|
|
3257
|
-
if (maxDiffLines !== undefined) {
|
|
3258
|
-
const lines = (p.payload.content ?? "").split("\n").length;
|
|
3259
|
-
if (lines > maxDiffLines)
|
|
3260
|
-
return false;
|
|
3261
|
-
}
|
|
3262
|
-
if (olderThanMs !== undefined) {
|
|
3263
|
-
const age = Date.now() - new Date(p.createdAt).getTime();
|
|
3264
|
-
if (age < olderThanMs)
|
|
3265
|
-
return false;
|
|
3266
|
-
}
|
|
3267
|
-
return true;
|
|
3268
|
-
});
|
|
3269
|
-
const results = [];
|
|
3270
|
-
for (const proposal of pending) {
|
|
3271
|
-
if (args["dry-run"]) {
|
|
3272
|
-
results.push({ id: proposal.id, ref: proposal.ref, source: proposal.source, dryRun: true });
|
|
3273
|
-
}
|
|
3274
|
-
else {
|
|
3275
|
-
const result = await akmProposalAccept({ id: proposal.id, target: args.target });
|
|
3276
|
-
results.push(result);
|
|
3277
|
-
}
|
|
3278
|
-
}
|
|
3279
|
-
output("proposal-accept-batch", { accepted: results.length, results, dryRun: args["dry-run"] });
|
|
3280
|
-
return;
|
|
3281
|
-
}
|
|
3282
|
-
if (!args.id) {
|
|
3283
|
-
throw new UsageError("Usage: akm proposal accept <id> OR akm proposal accept --generator <generator>", "MISSING_REQUIRED_ARGUMENT");
|
|
3284
|
-
}
|
|
3285
|
-
const result = await akmProposalAccept({ id: args.id, target: args.target });
|
|
3286
|
-
output("proposal-accept", result);
|
|
3287
|
-
});
|
|
3288
|
-
},
|
|
3289
|
-
});
|
|
3290
|
-
const proposalRejectCommand = defineCommand({
|
|
3291
|
-
meta: { name: "reject", description: "Reject a proposal and record the reason" },
|
|
3292
|
-
args: {
|
|
3293
|
-
id: {
|
|
3294
|
-
type: "positional",
|
|
3295
|
-
description: "Proposal id (uuid / prefix) or asset ref (e.g. skill:akm-dream). Optional when --generator is provided.",
|
|
3296
|
-
required: false,
|
|
3297
|
-
},
|
|
3298
|
-
reason: { type: "string", description: "Reason for rejection (required)" },
|
|
3299
|
-
// F-6 / #393: Batch reject by generator, diff size, or age.
|
|
3300
|
-
generator: {
|
|
3301
|
-
type: "string",
|
|
3302
|
-
description: "F-6: Bulk-reject all pending proposals from this generator (e.g. reflect, distill). Requires no positional id.",
|
|
3303
|
-
},
|
|
3304
|
-
source: {
|
|
3305
|
-
type: "string",
|
|
3306
|
-
description: "DEPRECATED — use --generator. Removed in 0.9.0.",
|
|
3307
|
-
},
|
|
3308
|
-
"max-diff-lines": {
|
|
3309
|
-
type: "string",
|
|
3310
|
-
description: "F-6: When bulk-rejecting, only reject proposals whose content is <= this many lines. Skips larger proposals.",
|
|
3311
|
-
},
|
|
3312
|
-
"older-than": {
|
|
3313
|
-
type: "string",
|
|
3314
|
-
description: "F-6: When bulk-rejecting, only reject proposals created more than this many days ago (e.g. '7' for 7 days).",
|
|
3315
|
-
},
|
|
3316
|
-
"dry-run": {
|
|
3317
|
-
type: "boolean",
|
|
3318
|
-
description: "F-6: List proposals that would be bulk-rejected without rejecting them.",
|
|
3319
|
-
default: false,
|
|
3320
|
-
},
|
|
3321
|
-
yes: {
|
|
3322
|
-
type: "boolean",
|
|
3323
|
-
alias: "y",
|
|
3324
|
-
description: "Skip confirmation prompt (required in non-interactive mode)",
|
|
3325
|
-
default: false,
|
|
3326
|
-
},
|
|
3327
|
-
},
|
|
3328
|
-
run({ args }) {
|
|
3329
|
-
return runWithJsonErrors(async () => {
|
|
3330
|
-
if (args.generator === undefined && args.source !== undefined) {
|
|
3331
|
-
emitFlagDeprecation("--source", "--generator", "proposal reject");
|
|
3332
|
-
}
|
|
3333
|
-
const generator = (args.generator ?? args.source);
|
|
3334
|
-
if (!args.reason || !String(args.reason).trim()) {
|
|
3335
|
-
throw new UsageError("Usage: akm proposal reject <id> --reason '<reason>' OR akm proposal reject --generator <generator> --reason '<reason>'", "MISSING_REQUIRED_ARGUMENT");
|
|
3336
|
-
}
|
|
3337
|
-
// F-6 / #393: Bulk-reject when --generator is provided without a positional id.
|
|
3338
|
-
if (generator && !args.id) {
|
|
3339
|
-
const { confirmDestructive } = await import("./cli/confirm.js");
|
|
3340
|
-
const confirmed = await confirmDestructive(`Bulk-reject all matching proposals from generator "${generator}"? This cannot be undone.`, { yes: args.yes === true || args["dry-run"] === true });
|
|
3341
|
-
if (!confirmed) {
|
|
3342
|
-
process.stderr.write("Aborted.\n");
|
|
3343
|
-
return;
|
|
3344
|
-
}
|
|
3345
|
-
const { listProposals } = await import("./core/proposals");
|
|
3346
|
-
const stashDir = resolveStashDir();
|
|
3347
|
-
const rawMaxDiff = args["max-diff-lines"] ? Number.parseInt(String(args["max-diff-lines"]), 10) : undefined;
|
|
3348
|
-
if (rawMaxDiff !== undefined && (Number.isNaN(rawMaxDiff) || rawMaxDiff < 0)) {
|
|
3349
|
-
throw new UsageError("--max-diff-lines must be a non-negative integer", "INVALID_FLAG_VALUE");
|
|
3350
|
-
}
|
|
3351
|
-
const rawOlderThan = args["older-than"] ? Number.parseInt(String(args["older-than"]), 10) : undefined;
|
|
3352
|
-
if (rawOlderThan !== undefined && (Number.isNaN(rawOlderThan) || rawOlderThan < 0)) {
|
|
3353
|
-
throw new UsageError("--older-than must be a non-negative integer (days)", "INVALID_FLAG_VALUE");
|
|
3354
|
-
}
|
|
3355
|
-
const maxDiffLines = rawMaxDiff;
|
|
3356
|
-
const olderThanMs = rawOlderThan !== undefined ? rawOlderThan * 86_400_000 : undefined;
|
|
3357
|
-
const pending = listProposals(stashDir, { status: "pending" }).filter((p) => {
|
|
3358
|
-
if (p.source !== generator)
|
|
3359
|
-
return false;
|
|
3360
|
-
if (maxDiffLines !== undefined) {
|
|
3361
|
-
const lines = (p.payload.content ?? "").split("\n").length;
|
|
3362
|
-
if (lines > maxDiffLines)
|
|
3363
|
-
return false;
|
|
3364
|
-
}
|
|
3365
|
-
if (olderThanMs !== undefined) {
|
|
3366
|
-
const age = Date.now() - new Date(p.createdAt).getTime();
|
|
3367
|
-
if (age < olderThanMs)
|
|
3368
|
-
return false;
|
|
3369
|
-
}
|
|
3370
|
-
return true;
|
|
3371
|
-
});
|
|
3372
|
-
const results = [];
|
|
3373
|
-
for (const proposal of pending) {
|
|
3374
|
-
if (args["dry-run"]) {
|
|
3375
|
-
results.push({ id: proposal.id, ref: proposal.ref, source: proposal.source, dryRun: true });
|
|
3376
|
-
}
|
|
3377
|
-
else {
|
|
3378
|
-
const result = akmProposalReject({ id: proposal.id, reason: String(args.reason) });
|
|
3379
|
-
results.push(result);
|
|
3380
|
-
}
|
|
3381
|
-
}
|
|
3382
|
-
output("proposal-reject-batch", { rejected: results.length, results, dryRun: args["dry-run"] });
|
|
3383
|
-
return;
|
|
3384
|
-
}
|
|
3385
|
-
if (!args.id) {
|
|
3386
|
-
throw new UsageError("Usage: akm proposal reject <id> --reason '<reason>' OR akm proposal reject --generator <generator> --reason '<reason>'", "MISSING_REQUIRED_ARGUMENT");
|
|
3387
|
-
}
|
|
3388
|
-
const { confirmDestructive } = await import("./cli/confirm.js");
|
|
3389
|
-
const confirmed = await confirmDestructive(`Reject proposal "${args.id}"? This cannot be undone.`, {
|
|
3390
|
-
yes: args.yes === true,
|
|
3391
|
-
});
|
|
3392
|
-
if (!confirmed) {
|
|
3393
|
-
process.stderr.write("Aborted.\n");
|
|
3394
|
-
return;
|
|
3395
|
-
}
|
|
3396
|
-
const result = akmProposalReject({ id: args.id, reason: String(args.reason) });
|
|
3397
|
-
output("proposal-reject", result);
|
|
3398
|
-
});
|
|
3399
|
-
},
|
|
3400
|
-
});
|
|
3401
|
-
const proposalDiffCommand = defineCommand({
|
|
3402
|
-
meta: { name: "diff", description: "Show the diff for a proposal (accepts full UUID, UUID prefix, or asset ref)" },
|
|
3403
|
-
args: {
|
|
3404
|
-
id: {
|
|
3405
|
-
type: "positional",
|
|
3406
|
-
description: "Proposal id (uuid / prefix) or asset ref (e.g. skill:akm-dream)",
|
|
3407
|
-
required: true,
|
|
3408
|
-
},
|
|
3409
|
-
target: { type: "string", description: "Override the write target by source name" },
|
|
3410
|
-
},
|
|
3411
|
-
run({ args }) {
|
|
3412
|
-
return runWithJsonErrors(() => {
|
|
3413
|
-
const result = akmProposalDiff({ id: args.id, target: args.target });
|
|
3414
|
-
output("proposal-diff", result);
|
|
3415
|
-
});
|
|
3416
|
-
},
|
|
3417
|
-
});
|
|
3418
|
-
// Phase 6C (Advantage D6c): revert an accepted proposal.
|
|
3419
|
-
//
|
|
3420
|
-
// Exit codes (mapped by `runWithJsonErrors` from the typed errors thrown by
|
|
3421
|
-
// `akmProposalRevert` / `revertProposal`):
|
|
3422
|
-
// 0 — success; prior content restored.
|
|
3423
|
-
// 1 — generic error (also used by `UsageError("INVALID_FLAG_VALUE")` and
|
|
3424
|
-
// `UsageError("MISSING_REQUIRED_ARGUMENT")` when the proposal is not
|
|
3425
|
-
// accepted, or no backup is available).
|
|
3426
|
-
// 1 — `NotFoundError("FILE_NOT_FOUND")` when the proposal id does not resolve.
|
|
3427
|
-
const proposalRevertCommand = defineCommand({
|
|
3428
|
-
meta: {
|
|
3429
|
-
name: "revert",
|
|
3430
|
-
description: "Revert an accepted proposal: restore the prior asset content from the backup captured at promotion time. " +
|
|
3431
|
-
"Errors if the proposal is not accepted or has no backup (new-asset proposals leave no backup). " +
|
|
3432
|
-
"Accepts the full proposal UUID or the asset ref. UUID prefixes are not supported for archived proposals — use the full UUID.",
|
|
3433
|
-
},
|
|
3434
|
-
args: {
|
|
3435
|
-
id: {
|
|
3436
|
-
type: "positional",
|
|
3437
|
-
description: "Proposal id (full uuid) or asset ref (e.g. skill:akm-dream). UUID prefixes are not supported for archived proposals — use the full UUID.",
|
|
3438
|
-
required: true,
|
|
3439
|
-
},
|
|
3440
|
-
target: { type: "string", description: "Override the write target by source name" },
|
|
3441
|
-
},
|
|
3442
|
-
async run({ args }) {
|
|
3443
|
-
await runWithJsonErrors(async () => {
|
|
3444
|
-
const result = await akmProposalRevert({
|
|
3445
|
-
id: args.id,
|
|
3446
|
-
target: args.target,
|
|
3447
|
-
});
|
|
3448
|
-
output("proposal-revert", result);
|
|
3449
|
-
});
|
|
3450
|
-
},
|
|
3451
|
-
});
|
|
3452
|
-
// `proposal show` (#225): show a single proposal with its validation findings.
|
|
3453
|
-
// `akmProposalShow` already backs `akm show proposal <id>` (now deprecated); this
|
|
3454
|
-
// is the canonical noun-group entry point.
|
|
3455
|
-
const proposalShowCommand = defineCommand({
|
|
3456
|
-
meta: { name: "show", description: "Show a single proposal and its validation findings" },
|
|
3457
|
-
args: {
|
|
3458
|
-
id: {
|
|
3459
|
-
type: "positional",
|
|
3460
|
-
description: "Proposal id (uuid / prefix) or asset ref (e.g. skill:akm-dream)",
|
|
3461
|
-
required: true,
|
|
3462
|
-
},
|
|
3463
|
-
},
|
|
3464
|
-
run({ args }) {
|
|
3465
|
-
return runWithJsonErrors(() => {
|
|
3466
|
-
const result = akmProposalShow({ id: args.id });
|
|
3467
|
-
output("proposal-show", result);
|
|
3468
|
-
});
|
|
3469
|
-
},
|
|
3470
|
-
});
|
|
3471
|
-
const proposalDrainCommand = defineCommand({
|
|
3472
|
-
meta: {
|
|
3473
|
-
name: "drain",
|
|
3474
|
-
description: "Drain the standing pending proposal backlog using a deterministic triage policy",
|
|
3475
|
-
},
|
|
3476
|
-
args: {
|
|
3477
|
-
policy: {
|
|
3478
|
-
type: "string",
|
|
3479
|
-
description: "Built-in preset (personal-stash|conservative|manual) or path to a policy file",
|
|
3480
|
-
},
|
|
3481
|
-
"dry-run": {
|
|
3482
|
-
type: "boolean",
|
|
3483
|
-
description: "List what would be accepted/rejected/deferred without writing.",
|
|
3484
|
-
default: false,
|
|
3485
|
-
},
|
|
3486
|
-
yes: {
|
|
3487
|
-
type: "boolean",
|
|
3488
|
-
alias: "y",
|
|
3489
|
-
description: "Skip confirmation prompt (required in non-interactive mode for promotion).",
|
|
3490
|
-
default: false,
|
|
3491
|
-
},
|
|
3492
|
-
"max-accepts": {
|
|
3493
|
-
type: "string",
|
|
3494
|
-
description: "Hard per-run accept ceiling. Accepts beyond this are reported as skippedByCap.",
|
|
3495
|
-
},
|
|
3496
|
-
"max-diff-lines": {
|
|
3497
|
-
type: "string",
|
|
3498
|
-
description: "Defer (never promote) accepts whose proposed content exceeds this many lines.",
|
|
3499
|
-
},
|
|
3500
|
-
"older-than": {
|
|
3501
|
-
type: "string",
|
|
3502
|
-
description: "Only consider proposals created more than this many days ago.",
|
|
3503
|
-
},
|
|
3504
|
-
promote: {
|
|
3505
|
-
type: "boolean",
|
|
3506
|
-
description: "Promote (accept) matching proposals. Default is queue mode (stage only, no writes to assets).",
|
|
3507
|
-
default: false,
|
|
3508
|
-
},
|
|
3509
|
-
judgment: {
|
|
3510
|
-
type: "boolean",
|
|
3511
|
-
description: "Opt into the judgment tier (llm by default; agent/sdk per config) for deferred items. No-op with a logged triage_deferred summary when no runner is configured.",
|
|
3512
|
-
default: false,
|
|
3513
|
-
},
|
|
3514
|
-
profile: {
|
|
3515
|
-
type: "string",
|
|
3516
|
-
description: "Read the triage block (policy, applyMode, ceilings, judgment) from this improve profile.",
|
|
3517
|
-
},
|
|
3518
|
-
},
|
|
3519
|
-
async run({ args }) {
|
|
3520
|
-
await runWithJsonErrors(async () => {
|
|
3521
|
-
const stashDir = resolveStashDir();
|
|
3522
|
-
const cfg = loadConfig();
|
|
3523
|
-
// Phase 2: read the triage block from the named improve profile. CLI flags
|
|
3524
|
-
// always override config; config supplies defaults for any flag omitted.
|
|
3525
|
-
const triageConfig = args.profile !== undefined ? resolveImproveProfile(args.profile, cfg).processes?.triage : undefined;
|
|
3526
|
-
const policy = resolveDrainPolicy(args.policy ?? triageConfig?.policy);
|
|
3527
|
-
const dryRun = args["dry-run"] === true;
|
|
3528
|
-
const applyMode = args.promote === true ? "promote" : (triageConfig?.applyMode ?? "queue");
|
|
3529
|
-
const maxAccepts = parsePositiveIntFlag(args["max-accepts"], "--max-accepts") ??
|
|
3530
|
-
triageConfig?.maxAcceptsPerRun ??
|
|
3531
|
-
25;
|
|
3532
|
-
const maxDiffLines = parsePositiveIntFlag(args["max-diff-lines"], "--max-diff-lines") ??
|
|
3533
|
-
triageConfig?.maxDiffLines;
|
|
3534
|
-
const rawOlderThan = parsePositiveIntFlag(args["older-than"], "--older-than");
|
|
3535
|
-
const olderThanMs = rawOlderThan !== undefined ? rawOlderThan * 86_400_000 : undefined;
|
|
3536
|
-
// Promotion in promote mode is destructive (commits to git, no batch revert).
|
|
3537
|
-
if (applyMode === "promote" && !dryRun) {
|
|
3538
|
-
const { confirmDestructive } = await import("./cli/confirm.js");
|
|
3539
|
-
const confirmed = await confirmDestructive(`Drain and promote matching pending proposals under policy "${policy.name}"? Promotions commit to git and cannot be batch-reverted.`, { yes: args.yes === true });
|
|
3540
|
-
if (!confirmed) {
|
|
3541
|
-
process.stderr.write("Aborted.\n");
|
|
3542
|
-
return;
|
|
3543
|
-
}
|
|
3544
|
-
}
|
|
3545
|
-
// `--older-than` is applied here as a pre-filter on excludeIds: ids that
|
|
3546
|
-
// are too fresh are excluded so the engine never touches them. This reads
|
|
3547
|
-
// the pending set once here; drainProposals reads the pending set again
|
|
3548
|
-
// internally, so a future engine-level olderThan option could remove this
|
|
3549
|
-
// second read (engine API owned by another agent — not changed here).
|
|
3550
|
-
let excludeIds;
|
|
3551
|
-
if (olderThanMs !== undefined) {
|
|
3552
|
-
const { listProposals } = await import("./core/proposals");
|
|
3553
|
-
const now = Date.now();
|
|
3554
|
-
excludeIds = new Set(listProposals(stashDir, { status: "pending" })
|
|
3555
|
-
// Fail SAFE: exclude a proposal when its age cannot be computed
|
|
3556
|
-
// (NaN createdAt) OR it is too fresh. An unparseable createdAt must
|
|
3557
|
-
// never be treated as old enough to drain/promote.
|
|
3558
|
-
.filter((proposal) => {
|
|
3559
|
-
const age = now - new Date(proposal.createdAt).getTime();
|
|
3560
|
-
return Number.isNaN(age) || age < olderThanMs;
|
|
3561
|
-
})
|
|
3562
|
-
.map((proposal) => proposal.id));
|
|
3563
|
-
}
|
|
3564
|
-
// Phase 3: resolve the judgment runner when --judgment is set. Default
|
|
3565
|
-
// mode is llm; falls back to defaults.llm when the triage block sets
|
|
3566
|
-
// neither mode nor profile (mirrors resolveValidationRunner). null when
|
|
3567
|
-
// nothing is configured → the engine leaves deferred items unresolved and
|
|
3568
|
-
// emits triage_deferred.
|
|
3569
|
-
const judgment = args.judgment === true ? resolveTriageJudgmentRunner(triageConfig?.judgment, cfg) : null;
|
|
3570
|
-
const result = await drainProposals({
|
|
3571
|
-
stashDir,
|
|
3572
|
-
policy,
|
|
3573
|
-
applyMode,
|
|
3574
|
-
maxAccepts,
|
|
3575
|
-
dryRun,
|
|
3576
|
-
...(maxDiffLines !== undefined ? { maxDiffLines } : {}),
|
|
3577
|
-
...(excludeIds ? { excludeIds } : {}),
|
|
3578
|
-
judgment,
|
|
3579
|
-
});
|
|
3580
|
-
output("proposal-drain", {
|
|
3581
|
-
schemaVersion: 1,
|
|
3582
|
-
ok: true,
|
|
3583
|
-
policy: policy.name,
|
|
3584
|
-
applyMode,
|
|
3585
|
-
dryRun,
|
|
3586
|
-
promoted: result.promoted,
|
|
3587
|
-
rejected: result.rejected,
|
|
3588
|
-
deferred: result.deferred,
|
|
3589
|
-
skippedByCap: result.skippedByCap,
|
|
3590
|
-
});
|
|
3591
|
-
});
|
|
3592
|
-
},
|
|
3593
|
-
});
|
|
3594
|
-
// ── proposal noun group (#225 / 0.8 CLI stabilization) ────────────────────────
|
|
3595
|
-
//
|
|
3596
|
-
// `akm proposal <verb>` is the canonical grammar in 0.8. The flat verbs
|
|
3597
|
-
// (`proposals`/`accept`/`reject`/`diff`/`revert`) remain as deprecated aliases
|
|
3598
|
-
// that warn to stderr and delegate to the same command bodies; they are removed
|
|
3599
|
-
// in 0.9.0. Bare `akm proposal` behaves as `proposal list` (mirrors `akm env`).
|
|
3600
|
-
const PROPOSAL_SUBCOMMAND_SET = new Set(["list", "show", "diff", "accept", "reject", "revert", "drain"]);
|
|
3601
|
-
function emitProposalVerbDeprecation(oldVerb, canonical) {
|
|
3602
|
-
if (isQuiet())
|
|
3603
|
-
return;
|
|
3604
|
-
process.stderr.write(`warning: 'akm ${oldVerb}' is deprecated and will be removed in 0.9.0. Use 'akm ${canonical}'.\n`);
|
|
3605
|
-
}
|
|
3606
|
-
const proposalCommand = defineCommand({
|
|
3607
|
-
meta: { name: "proposal", description: "Manage the proposal queue: list, show, diff, accept, reject, revert" },
|
|
3608
|
-
args: {
|
|
3609
|
-
status: {
|
|
3610
|
-
type: "string",
|
|
3611
|
-
description: "Filter by status (pending|accepted|rejected|reverted)",
|
|
3612
|
-
},
|
|
3613
|
-
ref: { type: "string", description: "Filter by asset ref (type:name)" },
|
|
3614
|
-
type: { type: "string", description: "Filter by asset type" },
|
|
3615
|
-
},
|
|
3616
|
-
subCommands: {
|
|
3617
|
-
list: proposalListCommand,
|
|
3618
|
-
show: proposalShowCommand,
|
|
3619
|
-
diff: proposalDiffCommand,
|
|
3620
|
-
accept: proposalAcceptCommand,
|
|
3621
|
-
reject: proposalRejectCommand,
|
|
3622
|
-
revert: proposalRevertCommand,
|
|
3623
|
-
drain: proposalDrainCommand,
|
|
3624
|
-
},
|
|
3625
|
-
run({ args }) {
|
|
3626
|
-
return runWithJsonErrors(() => {
|
|
3627
|
-
// citty runs the group body even after a subcommand; short-circuit so the
|
|
3628
|
-
// default-to-list body only fires for bare `akm proposal [--status …]`.
|
|
3629
|
-
if (hasSubcommand(args, PROPOSAL_SUBCOMMAND_SET))
|
|
3630
|
-
return;
|
|
3631
|
-
const status = parseProposalStatus(args.status);
|
|
3632
|
-
const result = akmProposalList({
|
|
3633
|
-
status,
|
|
3634
|
-
ref: args.ref,
|
|
3635
|
-
type: args.type,
|
|
3636
|
-
includeArchive: status === "accepted" || status === "rejected" || status === "reverted",
|
|
3637
|
-
});
|
|
3638
|
-
output("proposal-list", result);
|
|
3639
|
-
});
|
|
3640
|
-
},
|
|
3641
|
-
});
|
|
3642
|
-
// Deprecated flat-verb aliases (removed 0.9.0). Each wraps the canonical command
|
|
3643
|
-
// body so bulk/guard logic is not duplicated.
|
|
3644
|
-
const proposalsCommand = defineCommand({
|
|
3645
|
-
meta: { name: "proposals", description: "DEPRECATED — use `akm proposal list`. Removed in 0.9.0." },
|
|
3646
|
-
args: proposalListCommand.args,
|
|
3647
|
-
run(ctx) {
|
|
3648
|
-
emitProposalVerbDeprecation("proposals", "proposal list");
|
|
3649
|
-
return proposalListCommand.run?.(ctx);
|
|
3650
|
-
},
|
|
3651
|
-
});
|
|
3652
|
-
const acceptCommand = defineCommand({
|
|
3653
|
-
meta: { name: "accept", description: "DEPRECATED — use `akm proposal accept`. Removed in 0.9.0." },
|
|
3654
|
-
args: proposalAcceptCommand.args,
|
|
3655
|
-
run(ctx) {
|
|
3656
|
-
emitProposalVerbDeprecation("accept", "proposal accept");
|
|
3657
|
-
return proposalAcceptCommand.run?.(ctx);
|
|
3658
|
-
},
|
|
3659
|
-
});
|
|
3660
|
-
const rejectCommand = defineCommand({
|
|
3661
|
-
meta: { name: "reject", description: "DEPRECATED — use `akm proposal reject`. Removed in 0.9.0." },
|
|
3662
|
-
args: proposalRejectCommand.args,
|
|
3663
|
-
run(ctx) {
|
|
3664
|
-
emitProposalVerbDeprecation("reject", "proposal reject");
|
|
3665
|
-
return proposalRejectCommand.run?.(ctx);
|
|
3666
|
-
},
|
|
3667
|
-
});
|
|
3668
|
-
const diffCommand = defineCommand({
|
|
3669
|
-
meta: { name: "diff", description: "DEPRECATED — use `akm proposal diff`. Removed in 0.9.0." },
|
|
3670
|
-
args: proposalDiffCommand.args,
|
|
3671
|
-
run(ctx) {
|
|
3672
|
-
emitProposalVerbDeprecation("diff", "proposal diff");
|
|
3673
|
-
return proposalDiffCommand.run?.(ctx);
|
|
3674
|
-
},
|
|
3675
|
-
});
|
|
3676
|
-
const revertCommand = defineCommand({
|
|
3677
|
-
meta: { name: "revert", description: "DEPRECATED — use `akm proposal revert`. Removed in 0.9.0." },
|
|
3678
|
-
args: proposalRevertCommand.args,
|
|
3679
|
-
run(ctx) {
|
|
3680
|
-
emitProposalVerbDeprecation("revert", "proposal revert");
|
|
3681
|
-
return proposalRevertCommand.run?.(ctx);
|
|
3682
|
-
},
|
|
3683
|
-
});
|
|
3684
|
-
// ── distill (#228) ──────────────────────────────────────────────────────────
|
|
3685
|
-
function parseProposalStatus(raw) {
|
|
3686
|
-
if (raw === undefined)
|
|
3687
|
-
return undefined;
|
|
3688
|
-
const trimmed = raw.trim();
|
|
3689
|
-
if (!trimmed)
|
|
3690
|
-
return undefined;
|
|
3691
|
-
if (trimmed === "pending" || trimmed === "accepted" || trimmed === "rejected" || trimmed === "reverted") {
|
|
3692
|
-
return trimmed;
|
|
3693
|
-
}
|
|
3694
|
-
throw new UsageError(`Invalid --status value: "${raw}". Expected one of: pending, accepted, rejected, reverted.`, "INVALID_FLAG_VALUE");
|
|
3695
|
-
}
|
|
3696
|
-
const agentCommand = defineCommand({
|
|
3697
|
-
meta: {
|
|
3698
|
-
name: "agent",
|
|
3699
|
-
description: "Dispatch an agent CLI (opencode, claude, …) with an optional agent asset that provides the system prompt, model, and tool policy. Use <agent-ref> to embody a stash agent, --model to override the model, and --prompt/--command/--workflow to provide the task.",
|
|
3700
|
-
},
|
|
3701
|
-
args: {
|
|
3702
|
-
profile: {
|
|
3703
|
-
type: "positional",
|
|
3704
|
-
description: "Agent profile / platform to use (opencode, claude, …)",
|
|
3705
|
-
required: false,
|
|
3706
|
-
},
|
|
3707
|
-
"agent-ref": {
|
|
3708
|
-
type: "positional",
|
|
3709
|
-
description: "Optional agent asset ref (e.g. agent:code-reviewer). Loads system prompt, model, and tool policy from the stash asset.",
|
|
3710
|
-
required: false,
|
|
3711
|
-
},
|
|
3712
|
-
prompt: { type: "string", description: "Task prompt to pass to the agent" },
|
|
3713
|
-
command: { type: "string", description: "Load prompt from a command: asset" },
|
|
3714
|
-
workflow: { type: "string", description: "Load prompt from a workflow: asset" },
|
|
3715
|
-
model: {
|
|
3716
|
-
type: "string",
|
|
3717
|
-
description: "Model override — accepts aliases (opus, sonnet, haiku) or exact platform model IDs. Overrides the model specified in the agent asset.",
|
|
3718
|
-
},
|
|
3719
|
-
"timeout-ms": { type: "string", description: "Override the agent CLI timeout in milliseconds" },
|
|
3720
|
-
},
|
|
3721
|
-
async run({ args }) {
|
|
3722
|
-
await runWithJsonErrors(async () => {
|
|
3723
|
-
if (!args.profile) {
|
|
3724
|
-
throw new UsageError("Usage: akm agent <profile> [<agent-ref>] [--prompt <text>] [--model <model>]", "MISSING_REQUIRED_ARGUMENT", "Provide the agent profile name. Available profiles are listed in profiles.agent.");
|
|
3725
|
-
}
|
|
3726
|
-
const timeoutMs = parsePositiveIntFlag(getHyphenatedArg(args, "timeout-ms"), "--timeout-ms");
|
|
3727
|
-
const config = loadConfig();
|
|
3728
|
-
const { getDefaultLlmConfig } = await import("./core/config.js");
|
|
3729
|
-
// After 0.8.0 the agent block IS the loaded AkmConfig.
|
|
3730
|
-
const agentConfig = config;
|
|
3731
|
-
// Resolve agent asset ref → extract system prompt, model, and tool policy.
|
|
3732
|
-
const agentRef = getStringArg(args, "agent-ref");
|
|
3733
|
-
let systemPrompt;
|
|
3734
|
-
let assetModel;
|
|
3735
|
-
let assetTools;
|
|
3736
|
-
if (agentRef) {
|
|
3737
|
-
const { akmShowUnified } = await import("./commands/show.js");
|
|
3738
|
-
const asset = await akmShowUnified({ ref: agentRef, detail: "full" });
|
|
3739
|
-
systemPrompt = typeof asset.content === "string" ? asset.content : undefined;
|
|
3740
|
-
assetModel = typeof asset.modelHint === "string" ? asset.modelHint : undefined;
|
|
3741
|
-
assetTools = asset.toolPolicy;
|
|
3742
|
-
}
|
|
3743
|
-
// --model flag wins over the asset's modelHint.
|
|
3744
|
-
const model = getStringArg(args, "model") ?? assetModel;
|
|
3745
|
-
const promptText = getStringArg(args, "prompt");
|
|
3746
|
-
const commandRef = getStringArg(args, "command");
|
|
3747
|
-
const workflowRef = getStringArg(args, "workflow");
|
|
3748
|
-
// Only build a dispatch request when there is something to dispatch — a
|
|
3749
|
-
// prompt, an agent asset, or a model override. When none of these are
|
|
3750
|
-
// present the agent is launched interactively (no injected prompt, no
|
|
3751
|
-
// platform-specific flags beyond the profile's base args).
|
|
3752
|
-
const hasDispatchContent = !!(promptText ?? commandRef ?? workflowRef ?? systemPrompt ?? model ?? assetTools);
|
|
3753
|
-
const result = await akmAgentDispatch({
|
|
3754
|
-
profileName: String(args.profile),
|
|
3755
|
-
prompt: promptText,
|
|
3756
|
-
commandRef,
|
|
3757
|
-
workflowRef,
|
|
3758
|
-
agentConfig,
|
|
3759
|
-
llmConfig: getDefaultLlmConfig(config),
|
|
3760
|
-
...(hasDispatchContent
|
|
3761
|
-
? {
|
|
3762
|
-
dispatch: {
|
|
3763
|
-
prompt: promptText ?? "",
|
|
3764
|
-
systemPrompt,
|
|
3765
|
-
model,
|
|
3766
|
-
tools: assetTools,
|
|
3767
|
-
},
|
|
3768
|
-
}
|
|
3769
|
-
: {}),
|
|
3770
|
-
...(timeoutMs !== undefined && Number.isFinite(timeoutMs) ? { timeoutMs } : {}),
|
|
3771
|
-
});
|
|
3772
|
-
output("agent-result", result);
|
|
3773
|
-
if (!result.ok) {
|
|
3774
|
-
process.exit(EXIT_GENERAL);
|
|
3775
|
-
}
|
|
3776
|
-
});
|
|
3777
|
-
},
|
|
3778
|
-
});
|
|
3779
|
-
const lintCommand = defineCommand({
|
|
3780
|
-
meta: {
|
|
3781
|
-
name: "lint",
|
|
3782
|
-
description: "Scan stash .md files for structural issues (unquoted colons, missing updated field, orphaned stubs, placeholder stubs, missing name/type, stale paths). Use --fix to auto-fix Tier 1 issues. Exits 0 on success regardless of findings; use --fail-on-flagged for CI fail-on-finding behavior.",
|
|
3783
|
-
},
|
|
3784
|
-
args: {
|
|
3785
|
-
fix: { type: "boolean", description: "Apply auto-fixes in place", default: false },
|
|
3786
|
-
dir: { type: "string", description: "Override stash root directory (default: from config)" },
|
|
3787
|
-
"fail-on-flagged": {
|
|
3788
|
-
type: "boolean",
|
|
3789
|
-
description: "Exit non-zero when summary.flagged > 0 (CI-friendly). Default: exit 0 regardless of findings.",
|
|
3790
|
-
default: false,
|
|
3791
|
-
},
|
|
3792
|
-
},
|
|
3793
|
-
async run({ args }) {
|
|
3794
|
-
await runWithJsonErrors(async () => {
|
|
3795
|
-
const result = akmLint({
|
|
3796
|
-
fix: args.fix ?? false,
|
|
3797
|
-
dir: getStringArg(args, "dir"),
|
|
3798
|
-
});
|
|
3799
|
-
output("lint", result);
|
|
3800
|
-
if (args["fail-on-flagged"] && result.summary.flagged > 0)
|
|
3801
|
-
process.exit(EXIT_GENERAL);
|
|
3802
|
-
});
|
|
3803
|
-
},
|
|
3804
|
-
});
|
|
3805
|
-
const proposeCommand = defineCommand({
|
|
3806
|
-
meta: {
|
|
3807
|
-
name: "propose",
|
|
3808
|
-
description: "Ask the configured agent CLI to author a brand-new asset and queue it as a proposal",
|
|
3809
|
-
},
|
|
3810
|
-
args: {
|
|
3811
|
-
// Optional in citty so run() is invoked when omitted; we re-validate
|
|
3812
|
-
// below to surface a structured UsageError (exit 2) instead of citty's
|
|
3813
|
-
// default help-banner exit-0.
|
|
3814
|
-
type: { type: "positional", description: "Asset type (skill, command, knowledge, lesson, ...)", required: false },
|
|
3815
|
-
name: { type: "positional", description: "Asset name (slug or path under the type dir)", required: false },
|
|
3816
|
-
task: { type: "string", description: "Task description for the agent (what should the asset do?)" },
|
|
3817
|
-
file: { type: "string", description: "Read the task or prompt text from a UTF-8 file" },
|
|
3818
|
-
profile: { type: "string", description: "Override the agent profile (defaults to agent.default)" },
|
|
3819
|
-
"timeout-ms": { type: "string", description: "Override the agent CLI timeout in milliseconds" },
|
|
3820
|
-
},
|
|
3821
|
-
async run({ args }) {
|
|
3822
|
-
await runWithJsonErrors(async () => {
|
|
3823
|
-
// citty silently shows help and exits 0 when required positionals are
|
|
3824
|
-
// omitted. Re-validate explicitly so the exit code is 2 (USAGE) and a
|
|
3825
|
-
// structured JSON error reaches scripted callers.
|
|
3826
|
-
const taskFromFlag = typeof args.task === "string" ? args.task : undefined;
|
|
3827
|
-
const fileFromFlag = typeof args.file === "string" ? args.file : undefined;
|
|
3828
|
-
if (!args.type || !args.name || (!taskFromFlag && !fileFromFlag)) {
|
|
3829
|
-
throw new UsageError("Usage: akm propose <type> <name> (--task '<task>' | --file <path>).", "MISSING_REQUIRED_ARGUMENT", "Provide the asset type, name, and exactly one of --task or --file.");
|
|
3830
|
-
}
|
|
3831
|
-
if (taskFromFlag && fileFromFlag) {
|
|
3832
|
-
throw new UsageError("Pass exactly one of --task or --file.", "INVALID_FLAG_VALUE");
|
|
3833
|
-
}
|
|
3834
|
-
const taskText = fileFromFlag ? fs.readFileSync(path.resolve(fileFromFlag), "utf8") : (taskFromFlag ?? "");
|
|
3835
|
-
const timeoutMs = parsePositiveIntFlag(getHyphenatedArg(args, "timeout-ms"), "--timeout-ms");
|
|
3836
|
-
const result = await akmPropose({
|
|
3837
|
-
type: String(args.type),
|
|
3838
|
-
name: String(args.name),
|
|
3839
|
-
task: taskText,
|
|
3840
|
-
profile: getStringArg(args, "profile"),
|
|
3841
|
-
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
3842
|
-
});
|
|
3843
|
-
output("propose", result);
|
|
3844
|
-
if (result.ok === false) {
|
|
3845
|
-
process.exit(EXIT_GENERAL);
|
|
3846
|
-
}
|
|
3847
|
-
});
|
|
3848
|
-
},
|
|
3849
|
-
});
|
|
3850
|
-
const TASKS_SUBCOMMAND_SET = new Set([
|
|
3851
|
-
"add",
|
|
3852
|
-
"list",
|
|
3853
|
-
"show",
|
|
3854
|
-
"remove",
|
|
3855
|
-
"enable",
|
|
3856
|
-
"disable",
|
|
3857
|
-
"run",
|
|
3858
|
-
"history",
|
|
3859
|
-
"sync",
|
|
3860
|
-
"doctor",
|
|
3861
|
-
]);
|
|
3862
|
-
const GRAPH_SUBCOMMAND_SET = new Set([
|
|
3863
|
-
"summary",
|
|
3864
|
-
"entities",
|
|
3865
|
-
"entity",
|
|
3866
|
-
"relations",
|
|
3867
|
-
"related",
|
|
3868
|
-
"orphans",
|
|
3869
|
-
"export",
|
|
3870
|
-
"update",
|
|
3871
|
-
]);
|
|
3872
|
-
const tasksAddCommand = defineCommand({
|
|
3873
|
-
meta: { name: "add", description: "Register a new scheduled task and install it in the OS scheduler" },
|
|
3874
|
-
args: {
|
|
3875
|
-
id: { type: "positional", description: "Task id (used as filename and scheduler entry)", required: true },
|
|
3876
|
-
schedule: { type: "string", description: 'Cron-style schedule, e.g. "0 9 * * *" or "@daily"', required: true },
|
|
3877
|
-
workflow: { type: "string", description: "Workflow ref to invoke (e.g. workflow:my-flow)" },
|
|
3878
|
-
prompt: {
|
|
3879
|
-
type: "string",
|
|
3880
|
-
description: "Prompt for the configured agent harness — inline text, an asset ref like agent:foo, or ./path.md",
|
|
3881
|
-
},
|
|
3882
|
-
command: {
|
|
3883
|
-
type: "string",
|
|
3884
|
-
description: 'Shell command to run on the schedule (no AI agent), e.g. "akm improve --auto-accept safe". Split on whitespace; quote the whole flag value.',
|
|
3885
|
-
},
|
|
3886
|
-
profile: { type: "string", description: "Agent profile to use for prompt targets (default: defaults.agent)" },
|
|
3887
|
-
params: { type: "string", description: "Workflow params as a JSON object" },
|
|
3888
|
-
name: { type: "string", description: "Human-readable name for the task" },
|
|
3889
|
-
"when-to-use": { type: "string", description: "Guidance on when this task runs or should be used" },
|
|
3890
|
-
description: { type: "string", description: "Human-readable description" },
|
|
3891
|
-
tags: { type: "string", description: "Comma-separated tags" },
|
|
3892
|
-
disabled: { type: "boolean", description: "Register but leave disabled in the OS scheduler", default: false },
|
|
3893
|
-
force: { type: "boolean", description: "Overwrite an existing task with the same id", default: false },
|
|
3894
|
-
},
|
|
3895
|
-
async run({ args }) {
|
|
3896
|
-
await runWithJsonErrors(async () => {
|
|
3897
|
-
const result = await akmTasksAdd({
|
|
3898
|
-
id: args.id,
|
|
3899
|
-
schedule: args.schedule,
|
|
3900
|
-
workflow: args.workflow,
|
|
3901
|
-
prompt: args.prompt,
|
|
3902
|
-
command: args.command,
|
|
3903
|
-
profile: args.profile,
|
|
3904
|
-
params: args.params,
|
|
3905
|
-
name: args.name,
|
|
3906
|
-
when_to_use: getHyphenatedArg(args, "when-to-use"),
|
|
3907
|
-
description: args.description,
|
|
3908
|
-
tags: args.tags
|
|
3909
|
-
? args.tags
|
|
3910
|
-
.split(/[\s,]+/)
|
|
3911
|
-
.map((s) => s.trim())
|
|
3912
|
-
.filter(Boolean)
|
|
3913
|
-
: undefined,
|
|
3914
|
-
disabled: args.disabled === true,
|
|
3915
|
-
force: args.force === true,
|
|
3916
|
-
});
|
|
3917
|
-
output("tasks-add", result);
|
|
3918
|
-
});
|
|
3919
|
-
},
|
|
3920
|
-
});
|
|
3921
|
-
const tasksListCommand = defineCommand({
|
|
3922
|
-
meta: { name: "list", description: "List scheduled tasks in the stash" },
|
|
3923
|
-
async run() {
|
|
3924
|
-
await runWithJsonErrors(async () => {
|
|
3925
|
-
const result = await akmTasksList();
|
|
3926
|
-
output("tasks-list", result);
|
|
3927
|
-
});
|
|
3928
|
-
},
|
|
3929
|
-
});
|
|
3930
|
-
const tasksShowCommand = defineCommand({
|
|
3931
|
-
meta: { name: "show", description: "Show a parsed task definition" },
|
|
3932
|
-
args: { id: { type: "positional", description: "Task id or task:<id>", required: true } },
|
|
3933
|
-
async run({ args }) {
|
|
3934
|
-
await runWithJsonErrors(async () => {
|
|
3935
|
-
const { id } = parseTaskRef(args.id);
|
|
3936
|
-
const result = await akmTasksShow(id);
|
|
3937
|
-
output("tasks-show", result);
|
|
3938
|
-
});
|
|
3939
|
-
},
|
|
3940
|
-
});
|
|
3941
|
-
const tasksRemoveCommand = defineCommand({
|
|
3942
|
-
meta: { name: "remove", description: "Delete a task file and uninstall it from the OS scheduler" },
|
|
3943
|
-
args: { id: { type: "positional", description: "Task id", required: true } },
|
|
3944
|
-
async run({ args }) {
|
|
3945
|
-
await runWithJsonErrors(async () => {
|
|
3946
|
-
const { id } = parseTaskRef(args.id);
|
|
3947
|
-
const result = await akmTasksRemove(id);
|
|
3948
|
-
output("tasks-remove", result);
|
|
3949
|
-
});
|
|
3950
|
-
},
|
|
3951
|
-
});
|
|
3952
|
-
function makeTasksToggleCommand(enabled) {
|
|
3953
|
-
const verb = enabled ? "enable" : "disable";
|
|
3954
|
-
const description = enabled
|
|
3955
|
-
? "Enable a previously-disabled task"
|
|
3956
|
-
: "Disable a task in the OS scheduler without removing the file";
|
|
3957
|
-
return defineCommand({
|
|
3958
|
-
meta: { name: verb, description },
|
|
3959
|
-
args: { id: { type: "positional", description: "Task id", required: true } },
|
|
3960
|
-
async run({ args }) {
|
|
3961
|
-
await runWithJsonErrors(async () => {
|
|
3962
|
-
const { id } = parseTaskRef(args.id);
|
|
3963
|
-
const result = await akmTasksSetEnabled(id, enabled);
|
|
3964
|
-
output(`tasks-${verb}`, result);
|
|
3965
|
-
});
|
|
3966
|
-
},
|
|
3967
|
-
});
|
|
3968
|
-
}
|
|
3969
|
-
const tasksEnableCommand = makeTasksToggleCommand(true);
|
|
3970
|
-
const tasksDisableCommand = makeTasksToggleCommand(false);
|
|
3971
|
-
const tasksRunCommand = defineCommand({
|
|
3972
|
-
meta: {
|
|
3973
|
-
name: "run",
|
|
3974
|
-
description: "Execute a task now (this is what cron / launchd / schtasks invoke at the scheduled time)",
|
|
3975
|
-
},
|
|
3976
|
-
args: { id: { type: "positional", description: "Task id", required: true } },
|
|
3977
|
-
async run({ args }) {
|
|
3978
|
-
await runWithJsonErrors(async () => {
|
|
3979
|
-
const { id } = parseTaskRef(args.id);
|
|
3980
|
-
const envelope = await akmTasksRun(id);
|
|
3981
|
-
output("tasks-run", envelope);
|
|
3982
|
-
if (envelope.exitCode !== 0)
|
|
3983
|
-
process.exit(envelope.exitCode);
|
|
3984
|
-
});
|
|
3985
|
-
},
|
|
3986
|
-
});
|
|
3987
|
-
const tasksHistoryCommand = defineCommand({
|
|
3988
|
-
meta: { name: "history", description: "Show recent task run history" },
|
|
3989
|
-
args: {
|
|
3990
|
-
id: { type: "string", description: "Filter to one task id" },
|
|
3991
|
-
limit: { type: "string", description: "Maximum rows to return (default 50)" },
|
|
3992
|
-
},
|
|
3993
|
-
async run({ args }) {
|
|
3994
|
-
await runWithJsonErrors(async () => {
|
|
3995
|
-
const limit = parsePositiveIntFlag(args.limit ?? undefined);
|
|
3996
|
-
const result = await akmTasksHistory({ id: args.id, limit });
|
|
3997
|
-
output("tasks-history", result);
|
|
3998
|
-
});
|
|
3999
|
-
},
|
|
4000
|
-
});
|
|
4001
|
-
const tasksSyncCommand = defineCommand({
|
|
4002
|
-
meta: {
|
|
4003
|
-
name: "sync",
|
|
4004
|
-
description: "Reconcile the on-disk task files with the OS scheduler",
|
|
4005
|
-
},
|
|
4006
|
-
async run() {
|
|
4007
|
-
await runWithJsonErrors(async () => {
|
|
4008
|
-
const result = await akmTasksSync();
|
|
4009
|
-
output("tasks-sync", result);
|
|
4010
|
-
});
|
|
4011
|
-
},
|
|
4012
|
-
});
|
|
4013
|
-
const tasksDoctorCommand = defineCommand({
|
|
4014
|
-
meta: {
|
|
4015
|
-
name: "doctor",
|
|
4016
|
-
description: "Report the active scheduler backend, akm bin path, log dir, and supported schedule subset",
|
|
4017
|
-
},
|
|
4018
|
-
async run() {
|
|
4019
|
-
await runWithJsonErrors(async () => {
|
|
4020
|
-
const result = await akmTasksDoctor();
|
|
4021
|
-
output("tasks-doctor", result);
|
|
4022
|
-
});
|
|
4023
|
-
},
|
|
4024
|
-
});
|
|
4025
|
-
const tasksCommand = defineCommand({
|
|
4026
|
-
meta: {
|
|
4027
|
-
name: "tasks",
|
|
4028
|
-
alias: "task",
|
|
4029
|
-
description: "Schedule workflows or prompts via the OS-native scheduler (cron / launchd / schtasks)",
|
|
4030
|
-
},
|
|
4031
|
-
subCommands: {
|
|
4032
|
-
add: tasksAddCommand,
|
|
4033
|
-
list: tasksListCommand,
|
|
4034
|
-
show: tasksShowCommand,
|
|
4035
|
-
remove: tasksRemoveCommand,
|
|
4036
|
-
enable: tasksEnableCommand,
|
|
4037
|
-
disable: tasksDisableCommand,
|
|
4038
|
-
run: tasksRunCommand,
|
|
4039
|
-
history: tasksHistoryCommand,
|
|
4040
|
-
sync: tasksSyncCommand,
|
|
4041
|
-
doctor: tasksDoctorCommand,
|
|
4042
|
-
},
|
|
4043
|
-
run({ args }) {
|
|
4044
|
-
return runWithJsonErrors(async () => {
|
|
4045
|
-
if (hasSubcommand(args, TASKS_SUBCOMMAND_SET))
|
|
4046
|
-
return;
|
|
4047
|
-
const result = await akmTasksList();
|
|
4048
|
-
output("tasks-list", result);
|
|
4049
|
-
});
|
|
4050
|
-
},
|
|
4051
|
-
});
|
|
4052
|
-
export const main = defineCommand({
|
|
4053
|
-
meta: {
|
|
4054
|
-
name: "akm",
|
|
4055
|
-
version: pkgVersion,
|
|
4056
|
-
description: "Agent Knowledge Management — search, show, and manage assets from your stash.\n\n" +
|
|
4057
|
-
"Exit codes:\n" +
|
|
4058
|
-
" 0 success\n" +
|
|
4059
|
-
" 1 general error / not found\n" +
|
|
4060
|
-
" 2 usage error\n" +
|
|
4061
|
-
" 4 health warn (akm health only)\n" +
|
|
4062
|
-
" 78 config error",
|
|
4063
|
-
},
|
|
4064
|
-
args: {
|
|
4065
|
-
format: { type: "string", description: "Output format (json|jsonl|text|yaml)", default: "json" },
|
|
4066
|
-
detail: {
|
|
4067
|
-
type: "string",
|
|
4068
|
-
description: "Detail level (verbosity): brief|normal|full. Default: brief.",
|
|
4069
|
-
default: "brief",
|
|
4070
|
-
},
|
|
4071
|
-
shape: {
|
|
4072
|
-
type: "string",
|
|
4073
|
-
description: "Output projection: human|agent|summary. 'agent' trims to agent-essential fields; " +
|
|
4074
|
-
"'summary' is only valid on 'akm show'. Default: human.",
|
|
4075
|
-
},
|
|
4076
|
-
"for-agent": {
|
|
4077
|
-
type: "boolean",
|
|
4078
|
-
description: "DEPRECATED alias for '--shape agent' (removed 0.9.0).",
|
|
4079
|
-
default: false,
|
|
4080
|
-
},
|
|
4081
|
-
quiet: {
|
|
4082
|
-
type: "boolean",
|
|
4083
|
-
alias: "q",
|
|
4084
|
-
description: "Suppress non-essential stderr output (banners, spinners, progress info). " +
|
|
4085
|
-
"Safety-critical output is never suppressed: errors, destructive-action confirmation prompts, " +
|
|
4086
|
-
"and auto-migration banners always appear regardless of --quiet.",
|
|
4087
|
-
default: false,
|
|
4088
|
-
},
|
|
4089
|
-
verbose: {
|
|
4090
|
-
type: "boolean",
|
|
4091
|
-
description: "Print per-spec diagnostics to stderr (also honours AKM_VERBOSE env var)",
|
|
474
|
+
description: "Print per-spec diagnostics to stderr (also honours AKM_VERBOSE env var)",
|
|
4092
475
|
default: false,
|
|
4093
476
|
},
|
|
4094
477
|
},
|
|
@@ -4112,16 +495,12 @@ export const main = defineCommand({
|
|
|
4112
495
|
remember: rememberCommand,
|
|
4113
496
|
import: importKnowledgeCommand,
|
|
4114
497
|
sync: syncCommand,
|
|
4115
|
-
// Deprecated alias (removed 0.9.0) — delegates to `sync`.
|
|
4116
|
-
save: saveCommand,
|
|
4117
498
|
clone: cloneCommand,
|
|
4118
499
|
registry: registryCommand,
|
|
4119
500
|
config: configCommand,
|
|
4120
|
-
enable: enableCommand,
|
|
4121
|
-
disable: disableCommand,
|
|
4122
501
|
feedback: feedbackCommand,
|
|
4123
502
|
history: historyCommand,
|
|
4124
|
-
|
|
503
|
+
log: logCommand,
|
|
4125
504
|
lessons: lessonsCommand,
|
|
4126
505
|
agent: agentCommand,
|
|
4127
506
|
lint: lintCommand,
|
|
@@ -4129,38 +508,15 @@ export const main = defineCommand({
|
|
|
4129
508
|
extract: extractCommand,
|
|
4130
509
|
propose: proposeCommand,
|
|
4131
510
|
proposal: proposalCommand,
|
|
4132
|
-
// Deprecated flat verbs (removed 0.9.0) — delegate to `proposal <verb>`.
|
|
4133
|
-
proposals: proposalsCommand,
|
|
4134
|
-
accept: acceptCommand,
|
|
4135
|
-
reject: rejectCommand,
|
|
4136
|
-
diff: diffCommand,
|
|
4137
|
-
revert: revertCommand,
|
|
4138
511
|
help: helpCommand,
|
|
4139
512
|
hints: hintsCommand,
|
|
4140
513
|
completions: completionsCommand,
|
|
4141
514
|
env: envCommand,
|
|
4142
|
-
vault: vaultCommand,
|
|
4143
515
|
secret: secretCommand,
|
|
4144
516
|
wiki: wikiCommand,
|
|
4145
517
|
tasks: tasksCommand,
|
|
4146
518
|
},
|
|
4147
519
|
});
|
|
4148
|
-
const CONFIG_SUBCOMMAND_SET = new Set(["path", "list", "show", "get", "set", "unset", "enable", "disable"]);
|
|
4149
|
-
const ENV_SUBCOMMAND_SET = new Set(["list", "path", "export", "run", "create", "remove"]);
|
|
4150
|
-
const VAULT_SUBCOMMAND_SET = new Set(["list", "path", "run", "create", "set", "unset"]);
|
|
4151
|
-
const SECRET_SUBCOMMAND_SET = new Set(["list", "path", "run", "set", "remove"]);
|
|
4152
|
-
const WIKI_SUBCOMMAND_SET = new Set([
|
|
4153
|
-
"create",
|
|
4154
|
-
"register",
|
|
4155
|
-
"list",
|
|
4156
|
-
"show",
|
|
4157
|
-
"remove",
|
|
4158
|
-
"pages",
|
|
4159
|
-
"search",
|
|
4160
|
-
"stash",
|
|
4161
|
-
"lint",
|
|
4162
|
-
"ingest",
|
|
4163
|
-
]);
|
|
4164
520
|
// ── Exit codes ──────────────────────────────────────────────────────────────
|
|
4165
521
|
// Canonical table lives in `src/cli/shared.ts` (EXIT_CODES). These aliases keep
|
|
4166
522
|
// the local call sites terse. EXIT_HEALTH_WARN (4) is the `akm health` "warn"
|
|
@@ -4173,7 +529,13 @@ const EXIT_HEALTH_WARN = EXIT_CODES.HEALTH_WARN;
|
|
|
4173
529
|
// `import.meta.main` is false and we skip all startup side effects (argv
|
|
4174
530
|
// mutation, output-mode init, index cleanup, banner, runMain) so importers
|
|
4175
531
|
// can drive the `main` command themselves without the process exiting.
|
|
4176
|
-
|
|
532
|
+
//
|
|
533
|
+
// Node path: this module carries a `#!/usr/bin/env bun` shebang and is launched
|
|
534
|
+
// under Node via the `dist/cli-node.mjs` wrapper, which `import()`s this file
|
|
535
|
+
// (so `import.meta.main` is false here even though the CLI is the real entry).
|
|
536
|
+
// The wrapper sets `AKM_NODE_ENTRY=1` to opt into the startup block. The test
|
|
537
|
+
// harness never sets it, so importing cli.ts under Bun stays inert as before.
|
|
538
|
+
if (import.meta.main || process.env.AKM_NODE_ENTRY === "1") {
|
|
4177
539
|
// citty reads process.argv directly and does not accept a custom argv array,
|
|
4178
540
|
// so we must replace process.argv with the normalized version before runMain.
|
|
4179
541
|
process.argv = normalizeShowArgv(process.argv);
|
|
@@ -4202,7 +564,7 @@ if (import.meta.main) {
|
|
|
4202
564
|
// 0.8.0 moved the index to $XDG_DATA_HOME/akm/index.db (getDataDir()).
|
|
4203
565
|
// If the old file exists at $XDG_CACHE_HOME/akm/index.db, remove it so the
|
|
4204
566
|
// user isn't confused by a phantom DB. Best-effort; never fatal.
|
|
4205
|
-
|
|
567
|
+
bestEffort(() => {
|
|
4206
568
|
const oldIndexPath = path.join(getCacheDir(), "index.db");
|
|
4207
569
|
if (fs.existsSync(oldIndexPath)) {
|
|
4208
570
|
fs.rmSync(oldIndexPath, { force: true });
|
|
@@ -4210,10 +572,7 @@ if (import.meta.main) {
|
|
|
4210
572
|
fs.rmSync(`${oldIndexPath}-wal`, { force: true });
|
|
4211
573
|
warn(`Cleaned up stale 0.7.x index from ${oldIndexPath}. Canonical path is now ${getDbPath()}.`);
|
|
4212
574
|
}
|
|
4213
|
-
}
|
|
4214
|
-
catch {
|
|
4215
|
-
// Non-fatal; one-time warning only.
|
|
4216
|
-
}
|
|
575
|
+
}, "stale 0.7.x index cleanup is non-fatal");
|
|
4217
576
|
// First-time-user breadcrumb: when run with no subcommand AND no config
|
|
4218
577
|
// exists yet AND stderr is a TTY, print a friendly pointer to `akm setup`
|
|
4219
578
|
// above citty's auto-generated usage block. Triggers only when stdin/stderr
|
|
@@ -4240,22 +599,3 @@ if (import.meta.main) {
|
|
|
4240
599
|
})();
|
|
4241
600
|
runMain(main);
|
|
4242
601
|
}
|
|
4243
|
-
// ── Hints (embedded AGENTS.md) ──────────────────────────────────────────────
|
|
4244
|
-
function loadHints(detail = "normal") {
|
|
4245
|
-
// `brief` → the short AGENTS.md guide; `normal`/`full` → the complete guide.
|
|
4246
|
-
const wantFull = detail !== "brief";
|
|
4247
|
-
const filename = wantFull ? "AGENTS.full.md" : "AGENTS.md";
|
|
4248
|
-
const fallback = wantFull ? EMBEDDED_HINTS_FULL : EMBEDDED_HINTS;
|
|
4249
|
-
// Try reading from the docs/ directory (works in dev and when installed via npm)
|
|
4250
|
-
try {
|
|
4251
|
-
const docsPath = path.resolve(import.meta.dir ?? __dirname, `../docs/agents/${filename}`);
|
|
4252
|
-
if (fs.existsSync(docsPath)) {
|
|
4253
|
-
return fs.readFileSync(docsPath, "utf8");
|
|
4254
|
-
}
|
|
4255
|
-
}
|
|
4256
|
-
catch {
|
|
4257
|
-
// fall through
|
|
4258
|
-
}
|
|
4259
|
-
// Fallback for compiled binary — inline content
|
|
4260
|
-
return fallback;
|
|
4261
|
-
}
|