akm-cli 0.9.14 → 0.9.15-beta.2
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 +559 -0
- package/STABILITY.md +6 -3
- package/dist/akm +54 -1
- package/dist/akm-migrate +34 -1
- package/dist/assets/prompts/reflect-feedback-framing.md +1 -0
- package/dist/assets/prompts/reflect-llm-framed-contract.md +2 -0
- package/dist/assets/prompts/reflect-llm-schema-contract.md +2 -0
- package/dist/assets/tasks/core/improve.yml +1 -1
- package/dist/assets/tasks/core/index-refresh.yml +1 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +1 -1
- package/dist/cli/retired-commands.js +0 -1
- package/dist/cli/shared.js +9 -0
- package/dist/cli/unknown-flags.js +1 -0
- package/dist/cli.js +40 -3
- package/dist/commands/config-cli.js +85 -3
- package/dist/commands/env/env-cli.js +1 -42
- package/dist/commands/env/env.js +1 -1
- package/dist/commands/env/secret-cli.js +1 -2
- package/dist/commands/health/checks.js +357 -63
- package/dist/commands/health/engine-usage.js +45 -0
- package/dist/commands/health/improve-metrics.js +18 -0
- package/dist/commands/health/llm-usage.js +41 -1
- package/dist/commands/health/plugin-staleness.js +7 -3
- package/dist/commands/health/version-drift.js +93 -0
- package/dist/commands/health/windows.js +3 -1
- package/dist/commands/health.js +44 -9
- package/dist/commands/improve/consolidate/chunking.js +4 -2
- package/dist/commands/improve/improve-cli.js +99 -5
- package/dist/commands/improve/improve-report.js +154 -0
- package/dist/commands/improve/improve-result-file.js +45 -33
- package/dist/commands/improve/improve-strategies.js +133 -3
- package/dist/commands/improve/improve-usage-report.js +182 -0
- package/dist/commands/improve/improve.js +40 -3
- package/dist/commands/improve/locks.js +28 -78
- package/dist/commands/improve/planner.js +1 -0
- package/dist/commands/improve/preparation.js +9 -1
- package/dist/commands/improve/reflect.js +44 -4
- package/dist/commands/models-cli.js +50 -1
- package/dist/commands/proposal/repository.js +8 -3
- package/dist/commands/proposal/validators/proposal-quality-validators.js +41 -6
- package/dist/commands/proposal/validators/proposal-validators.js +24 -0
- package/dist/commands/read/search-cli.js +38 -2
- package/dist/commands/read/show.js +103 -4
- package/dist/commands/sources/info.js +5 -1
- package/dist/commands/sources/installed-stashes.js +58 -16
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/stash-cli.js +48 -0
- package/dist/commands/tasks/tasks-cli.js +49 -2
- package/dist/commands/workflow-cli.js +86 -12
- package/dist/core/asset/markdown-fragments.js +35 -0
- package/dist/core/config/config-schema.js +14 -0
- package/dist/core/config/config.js +302 -24
- package/dist/core/config/schema/embedding.js +41 -0
- package/dist/core/env-secret-ref.js +58 -5
- package/dist/core/errors.js +30 -0
- package/dist/core/file-lock.js +49 -15
- package/dist/core/improve-result.js +51 -0
- package/dist/core/loopback.js +17 -0
- package/dist/core/parent-watchdog.js +64 -0
- package/dist/core/paths.js +11 -0
- package/dist/core/run-lock.js +107 -0
- package/dist/core/sensitive-marker-path.js +19 -0
- package/dist/core/state-db.js +74 -14
- package/dist/indexer/index-rebuild-lock.js +73 -0
- package/dist/indexer/index-writer-lock.js +40 -1
- package/dist/indexer/index-written-assets.js +29 -1
- package/dist/indexer/indexer.js +93 -29
- package/dist/indexer/materialize-embeddings.js +564 -48
- package/dist/indexer/search/db-search.js +49 -2
- package/dist/indexer/search/search-source.js +23 -1
- package/dist/integrations/agent/engine-resolution.js +96 -6
- package/dist/integrations/agent/execution-definitions.js +6 -15
- package/dist/integrations/agent/execution-lowering.js +6 -1
- package/dist/integrations/agent/execution-preparation.js +1 -1
- package/dist/integrations/agent/model-map.js +123 -20
- package/dist/integrations/agent/prompts.js +40 -8
- package/dist/integrations/agent/runner-dispatch.js +9 -3
- package/dist/integrations/agent/runner.js +2 -0
- package/dist/llm/client.js +8 -3
- package/dist/llm/embedder.js +20 -8
- package/dist/llm/embedders/local.js +10 -2
- package/dist/llm/embedders/remote.js +497 -32
- package/dist/output/shapes/helpers.js +38 -2
- package/dist/output/shapes/models-list.js +16 -0
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/shapes.js +4 -0
- package/dist/output/text/command-format.js +29 -0
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/improve-report.js +27 -0
- package/dist/{commands/env/marker-path.js → output/text/models.js} +4 -3
- package/dist/output/text/show-format.js +4 -0
- package/dist/output/text.js +4 -0
- package/dist/scripts/akm-migrate-node.js +25146 -21759
- package/dist/scripts/akm-migrate.js +24271 -20885
- package/dist/storage/repositories/embedding-salvage-repository.js +184 -0
- package/dist/storage/repositories/improve-runs-repository.js +34 -0
- package/dist/storage/repositories/index-fts-repository.js +49 -6
- package/dist/storage/repositories/index-schema.js +16 -0
- package/dist/storage/repositories/index-vec-repository.js +30 -0
- package/dist/storage/repositories/workflow-runs-repository.js +55 -18
- package/dist/tasks/backends/cron.js +14 -7
- package/dist/tasks/run/run-native-task.js +23 -1
- package/dist/tasks/run/run-workflow-task.js +16 -0
- package/dist/workflows/exec/child-workflow.js +2 -2
- package/dist/workflows/exec/dispatch-redaction.js +21 -9
- package/dist/workflows/exec/run-workflow.js +6 -5
- package/dist/workflows/runtime/runs.js +33 -5
- package/docs/migration/release-notes/0.9.15.md +133 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/reference/cli.md +271 -30
- package/docs/reference/configuration.md +234 -21
- package/docs/reference/data-and-telemetry.md +8 -0
- package/docs/reference/tasks.md +16 -1
- package/docs/reference/workflow-schema.md +5 -1
- package/package.json +1 -1
- package/schemas/akm-config.json +47 -0
package/dist/akm
CHANGED
|
@@ -117,6 +117,10 @@ if (contextIndex !== -1) {
|
|
|
117
117
|
if (contextValid) {
|
|
118
118
|
process.env.AKM_LAUNCHER_NODE = process.execPath;
|
|
119
119
|
process.env.AKM_LAUNCHER_PATH = fileURLToPath(import.meta.url);
|
|
120
|
+
// #956: lets the child (and its lock payloads) name the launcher pid
|
|
121
|
+
// alongside its own, and lets the child's parent-death watchdog tell a
|
|
122
|
+
// real launcher-managed run apart from a direct `bun src/cli.ts` run.
|
|
123
|
+
process.env.AKM_LAUNCHER_PID = String(process.pid);
|
|
120
124
|
|
|
121
125
|
if (!process.versions.bun) {
|
|
122
126
|
const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
|
|
@@ -145,7 +149,56 @@ if (contextValid) {
|
|
|
145
149
|
const entry = useBun ? bunEntry : nodeEntry;
|
|
146
150
|
const runtime = useBun ? "Bun" : "Node.js";
|
|
147
151
|
const result = await new Promise((resolve) => {
|
|
148
|
-
const child = spawn(command, [entry, ...process.argv.slice(2)], {
|
|
152
|
+
const child = spawn(command, [entry, ...process.argv.slice(2)], {
|
|
153
|
+
stdio: "inherit",
|
|
154
|
+
env: process.env,
|
|
155
|
+
// #956: give the child its OWN process group on POSIX (`setsid()` —
|
|
156
|
+
// does not touch its inherited stdio; see `spawnsOwnProcessGroup` in
|
|
157
|
+
// src/core/subprocess.ts for the Windows caveat that rules this out
|
|
158
|
+
// there). Without this, a spawned child shares the launcher's
|
|
159
|
+
// process group by default, so a signal delivered to that GROUP (a
|
|
160
|
+
// terminal's Ctrl-C, or `kill -SIGINT -<pgid>`) reaches the child
|
|
161
|
+
// directly AND the launcher, which then also forwards it — a real
|
|
162
|
+
// second signal lands microseconds after the first. The child's
|
|
163
|
+
// `process.once(signal, ...)` handler has already unregistered
|
|
164
|
+
// itself for its first (direct) copy, so the second falls through
|
|
165
|
+
// to the runtime's default disposition (immediate termination) and
|
|
166
|
+
// the child's graceful shutdown (lock release, in-flight abort)
|
|
167
|
+
// never runs. Detaching the child's process group makes the
|
|
168
|
+
// launcher's forward below the SOLE delivery path to the child, so
|
|
169
|
+
// it is always exactly one signal.
|
|
170
|
+
detached: process.platform !== "win32",
|
|
171
|
+
});
|
|
172
|
+
// A `kill <launcher-pid>` (a scheduler timeout, a supervisor, an
|
|
173
|
+
// operator) used to end only this wrapper, orphaning the bun child —
|
|
174
|
+
// it kept running (and holding locks) for as long as its own work took.
|
|
175
|
+
// Forward the same signal so the child dies with its parent. Never
|
|
176
|
+
// forward once the child has already exited: forwarding to a
|
|
177
|
+
// dead/replaced pid would be at best a no-op and at worst a signal to
|
|
178
|
+
// an unrelated process that reused the pid.
|
|
179
|
+
let childExited = false;
|
|
180
|
+
child.once("exit", () => {
|
|
181
|
+
childExited = true;
|
|
182
|
+
});
|
|
183
|
+
const forwardSignal = (signal) => {
|
|
184
|
+
if (childExited) return;
|
|
185
|
+
try {
|
|
186
|
+
child.kill(signal);
|
|
187
|
+
} catch {
|
|
188
|
+
// Child exited in the race between the check above and here.
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
// `.once`, not `.on`: Node/Bun suppress a signal's default
|
|
192
|
+
// (process-terminating) disposition for as long as ANY listener stays
|
|
193
|
+
// registered for it. A persistent `.on` listener would still be
|
|
194
|
+
// registered when the `process.kill(process.pid, result.signal)`
|
|
195
|
+
// re-raise below runs at shutdown, swallowing it and leaving this
|
|
196
|
+
// launcher exiting 0 instead of reflecting the child's signal. `.once`
|
|
197
|
+
// consumes only the externally-delivered signal that triggers the
|
|
198
|
+
// forward, so the re-raise correctly falls through to the OS default.
|
|
199
|
+
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
200
|
+
process.once(signal, () => forwardSignal(signal));
|
|
201
|
+
}
|
|
149
202
|
child.once("error", (error) => resolve({ error }));
|
|
150
203
|
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
151
204
|
});
|
package/dist/akm-migrate
CHANGED
|
@@ -27,10 +27,43 @@ const bunEntry = fileURLToPath(new URL("./scripts/akm-migrate.js", import.meta.u
|
|
|
27
27
|
const nodeEntry = fileURLToPath(new URL("./scripts/akm-migrate-node.js", import.meta.url));
|
|
28
28
|
|
|
29
29
|
{
|
|
30
|
+
// #956: same shape as scripts/node-runtime/akm — the child names the
|
|
31
|
+
// launcher pid, and a `kill <launcher-pid>` forwards to the child instead
|
|
32
|
+
// of orphaning it.
|
|
33
|
+
const env = { ...process.env, AKM_LAUNCHER_PID: String(process.pid) };
|
|
30
34
|
const command = process.versions.bun ? process.execPath : useBun ? "bun" : process.execPath;
|
|
31
35
|
const entry = process.versions.bun || useBun ? bunEntry : nodeEntry;
|
|
32
36
|
const result = await new Promise((resolve) => {
|
|
33
|
-
const child = spawn(command, [entry, ...process.argv.slice(2)], {
|
|
37
|
+
const child = spawn(command, [entry, ...process.argv.slice(2)], {
|
|
38
|
+
stdio: "inherit",
|
|
39
|
+
env,
|
|
40
|
+
// #956: own process group on POSIX so the launcher's forward below is
|
|
41
|
+
// the SOLE delivery path to the child instead of a redundant second
|
|
42
|
+
// copy landing on top of a group-wide broadcast the child already got
|
|
43
|
+
// directly — see the matching comment in scripts/node-runtime/akm.
|
|
44
|
+
detached: process.platform !== "win32",
|
|
45
|
+
});
|
|
46
|
+
let childExited = false;
|
|
47
|
+
child.once("exit", () => {
|
|
48
|
+
childExited = true;
|
|
49
|
+
});
|
|
50
|
+
const forwardSignal = (signal) => {
|
|
51
|
+
if (childExited) return;
|
|
52
|
+
try {
|
|
53
|
+
child.kill(signal);
|
|
54
|
+
} catch {
|
|
55
|
+
// Child exited in the race between the check above and here.
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
// `.once`, not `.on`: see the matching comment in
|
|
59
|
+
// scripts/node-runtime/akm — a persistent `.on` listener would still be
|
|
60
|
+
// registered when the `process.kill(process.pid, result.signal)`
|
|
61
|
+
// re-raise below runs, suppressing the OS default disposition and
|
|
62
|
+
// leaving this launcher exiting 0 instead of reflecting the child's
|
|
63
|
+
// signal.
|
|
64
|
+
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
65
|
+
process.once(signal, () => forwardSignal(signal));
|
|
66
|
+
}
|
|
34
67
|
child.once("error", (error) => resolve({ error }));
|
|
35
68
|
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
36
69
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Feedback describes what a reader found missing or wrong. It is a signal to investigate, not a fact to insert. Do not add claims, numbers, dates, paths, ports, or incidents that are not already present in the asset content. If feedback asks for information the asset lacks, add a clearly marked `TODO: verify …` placeholder or leave the section unchanged.
|
|
@@ -9,3 +9,5 @@ AKM_REFLECT_CONTENT_END
|
|
|
9
9
|
The first begin marker and final end marker delimit the body; marker lines between them are literal content. Put the complete markdown body between those outer markers. Quotes, Markdown fences, and backslashes inside the body are literal content; do not JSON-escape them. Emit the body only, without YAML frontmatter, because AKM preserves and merges the source frontmatter itself.
|
|
10
10
|
|
|
11
11
|
The frontmatter patch must be a one-line JSON object with exactly `description` and `when_to_use`. Keep a field `null` when it should not change. Supply a non-empty string only when adding or correcting that field; AKM merges those values through its existing sanitizer.
|
|
12
|
+
|
|
13
|
+
Never include the truncation marker (the literal text `{{TRUNCATION_MARKER}}`) or any other text from outside the fenced asset content shown to you, anywhere in the body.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
Respond only through the provider's native JSON schema. {{FIELD_RULE}}
|
|
2
2
|
|
|
3
3
|
`content` must contain the complete improved markdown body only, without YAML frontmatter. `frontmatterPatch` must contain exactly `description` and `when_to_use`; set either field to `null` when it should not change, or to a non-empty string when adding or correcting it. AKM merges that narrow patch with the source frontmatter and preserves target identity itself. `confidence` is your honest self-rated quality confidence from 0 to 1. Do not add prose or Markdown fences around the JSON response.
|
|
4
|
+
|
|
5
|
+
Never include the truncation marker (the literal text `{{TRUNCATION_MARKER}}`) or any other text from outside the quoted asset content shown to you, anywhere in `content`.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
version: 4
|
|
2
|
-
run: akm improve --strategy catchup --skip-if-locked
|
|
2
|
+
run: akm improve --strategy catchup --skip-if-locked --require-engines
|
|
3
3
|
description: Manual recovery — consolidation + triage drain (run on demand via `akm task run akm-improve-catchup`)
|
|
4
4
|
# Manual-recovery task: ships disabled (the retired registerDefaultTasks
|
|
5
5
|
# marked it enableMode: "manual"). `akm task run` works while disabled;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
version: 4
|
|
2
|
-
run: akm improve --strategy reflect-distill --skip-if-locked
|
|
2
|
+
run: akm improve --strategy reflect-distill --skip-if-locked --require-engines
|
|
3
3
|
description: Frequent learning pass (hourly at :40) — reflect, distill, memory inference, judged triage
|
|
4
4
|
schedule: "40 * * * *"
|
|
@@ -65,7 +65,6 @@ const RETIRED_COMMAND_HINTS = {
|
|
|
65
65
|
"task enable": "`akm task enable` was removed in 0.9 — set `enabled: true` in the task YAML, then `akm task sync`.",
|
|
66
66
|
"task disable": "`akm task disable` was removed in 0.9 — set `enabled: false` in the task YAML, then `akm task sync`.",
|
|
67
67
|
"task init": "`akm task init` was removed in 0.9 — `akm setup` seeds the default schedules.",
|
|
68
|
-
"task list": "there is no `task list` — task files are indexed assets; use `akm search --type task`.",
|
|
69
68
|
"task show": "there is no `task show` — task files are indexed assets; use `akm show <ref>`.",
|
|
70
69
|
"task remove": "there is no `task remove` — delete the task YAML, then run `akm task sync` to unbind it.",
|
|
71
70
|
// `improve canary` is NOT here: `akm improve` is a leaf command (a
|
package/dist/cli/shared.js
CHANGED
|
@@ -31,6 +31,8 @@ export { parseAllFlagValues };
|
|
|
31
31
|
* 2 usage error
|
|
32
32
|
* 4 health warn (health command only)
|
|
33
33
|
* 70 internal / unclassified (sysexits EX_SOFTWARE — akm threw unexpectedly)
|
|
34
|
+
* 75 transient (sysexits EX_TEMPFAIL — retry shortly; another akm process
|
|
35
|
+
* holds a lock or is writing state.db right now, not a bad command line)
|
|
34
36
|
* 78 config error
|
|
35
37
|
*/
|
|
36
38
|
export const EXIT_CODES = {
|
|
@@ -41,6 +43,11 @@ export const EXIT_CODES = {
|
|
|
41
43
|
// sysexits.h EX_SOFTWARE. Distinct from GENERAL(1) so scripts can tell an
|
|
42
44
|
// expected "not found" outcome from akm itself throwing an unexpected error.
|
|
43
45
|
INTERNAL: 70,
|
|
46
|
+
// sysexits.h EX_TEMPFAIL (#948 addendum). Distinct from USAGE(2): a
|
|
47
|
+
// scheduler or cron wrapper classifies 2 as "fix the command line", but a
|
|
48
|
+
// TransientError means "try again in a few seconds" — a different retry
|
|
49
|
+
// contract callers can branch on.
|
|
50
|
+
TEMPFAIL: 75,
|
|
44
51
|
CONFIG: 78,
|
|
45
52
|
};
|
|
46
53
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -64,6 +71,8 @@ function classifyExitCode(error) {
|
|
|
64
71
|
return EXIT_CODES.CONFIG;
|
|
65
72
|
case "not-found":
|
|
66
73
|
return EXIT_CODES.GENERAL;
|
|
74
|
+
case "transient":
|
|
75
|
+
return EXIT_CODES.TEMPFAIL;
|
|
67
76
|
default:
|
|
68
77
|
return assertNever(error.kind, "classifyExitCode");
|
|
69
78
|
}
|
|
@@ -51,6 +51,7 @@ const SELF_DIAGNOSED_FLAGS = new Map(Object.entries({
|
|
|
51
51
|
"proposal drain": ["profile"], // retired, points at --strategy
|
|
52
52
|
search: ["source"], // renamed to --from
|
|
53
53
|
curate: ["source"], // renamed to --from
|
|
54
|
+
"task list": ["source"], // renamed to --from (alias of `search --type task`, #951)
|
|
54
55
|
remember: ["target"], // renamed to --bundle
|
|
55
56
|
clone: ["target"], // renamed to --bundle
|
|
56
57
|
improve: ["auto-accept", "target"], // retired in 0.9.0 / renamed to --bundle
|
package/dist/cli.js
CHANGED
|
@@ -87,6 +87,8 @@ import { taskCommand } from "./commands/tasks/tasks-cli.js";
|
|
|
87
87
|
import { workflowCommand } from "./commands/workflow-cli.js";
|
|
88
88
|
import { DEFAULT_CONFIG, loadConfig } from "./core/config/config.js";
|
|
89
89
|
import { UsageError } from "./core/errors.js";
|
|
90
|
+
import { launcherPidFromEnv } from "./core/file-lock.js";
|
|
91
|
+
import { startParentDeathWatchdog } from "./core/parent-watchdog.js";
|
|
90
92
|
import { getConfigPath } from "./core/paths.js";
|
|
91
93
|
import { DURATION_UNITS, parseDuration } from "./core/time.js";
|
|
92
94
|
import { plainize } from "./core/tty.js";
|
|
@@ -293,8 +295,8 @@ const healthCommand = defineCommand({
|
|
|
293
295
|
probe: {
|
|
294
296
|
type: "boolean",
|
|
295
297
|
default: true,
|
|
296
|
-
description: "Probe default-llm-engine / configured-engines reachability (on by default).",
|
|
297
|
-
negativeDescription: "Skip the reachability probes (for an offline or air-gapped host).",
|
|
298
|
+
description: "Probe default-llm-engine / configured-engines reachability and check for a newer akm release (on by default).",
|
|
299
|
+
negativeDescription: "Skip the reachability probes and the update check (for an offline or air-gapped host).",
|
|
298
300
|
},
|
|
299
301
|
},
|
|
300
302
|
async run({ args }) {
|
|
@@ -554,6 +556,7 @@ export const main = defineCommand({
|
|
|
554
556
|
" 2 usage error\n" +
|
|
555
557
|
" 4 health warn (akm health only)\n" +
|
|
556
558
|
" 70 internal / unclassified error\n" +
|
|
559
|
+
" 75 transient (retry shortly — another akm process holds a lock or is writing state.db)\n" +
|
|
557
560
|
" 78 config error",
|
|
558
561
|
},
|
|
559
562
|
args: {
|
|
@@ -1077,5 +1080,39 @@ async function runCli() {
|
|
|
1077
1080
|
// sets `AKM_STANDALONE_ENTRY=1` before importing this file. The test harness
|
|
1078
1081
|
// sets neither, so importing cli.ts under Bun stays inert as before.
|
|
1079
1082
|
if (import.meta.main || process.env.AKM_NODE_ENTRY === "1" || process.env.AKM_STANDALONE_ENTRY === "1") {
|
|
1080
|
-
|
|
1083
|
+
// #956: a launcher that dies WITHOUT delivering a signal
|
|
1084
|
+
// (SIGKILL, an OOM kill, a supervisor that force-removes the process)
|
|
1085
|
+
// still reparents this process to init with nothing to catch — the field
|
|
1086
|
+
// evidence was 40 orphaned `bun …/dist/cli.js` processes in one day, some
|
|
1087
|
+
// from a curate hook killing its own launcher on timeout. Active for
|
|
1088
|
+
// EVERY command (not only `index`), since a hook-invoked `curate`/`search`
|
|
1089
|
+
// child is orphaned the same way. Inert when there is no launcher
|
|
1090
|
+
// (AKM_LAUNCHER_PID unset — a direct `bun src/cli.ts` run, or any test
|
|
1091
|
+
// harness that imports this module without setting it): only a real
|
|
1092
|
+
// launcher-managed run has a parent worth watching.
|
|
1093
|
+
const parentWatchdog = launcherPidFromEnv()
|
|
1094
|
+
? startParentDeathWatchdog({
|
|
1095
|
+
initialPpid: process.ppid,
|
|
1096
|
+
onOrphaned: () => {
|
|
1097
|
+
// Self-delivers the same signal a real SIGTERM would be. `akm
|
|
1098
|
+
// index`'s own AbortController listens for it
|
|
1099
|
+
// (commands/sources/stash-cli.ts) and gets a graceful, in-process
|
|
1100
|
+
// shutdown. Every other command has no SIGTERM listener of its
|
|
1101
|
+
// own, so this self-signal terminates it directly via the
|
|
1102
|
+
// runtime's default disposition WITHOUT running `exit` handlers
|
|
1103
|
+
// (lock release included) — it stops the orphaned process, which
|
|
1104
|
+
// is the goal here, but any lock it held is left for the next
|
|
1105
|
+
// acquirer's dead-pid stale-reclaim (file-lock.ts) to clear, not
|
|
1106
|
+
// released in-process. No second, parallel abort path to keep in
|
|
1107
|
+
// sync either way.
|
|
1108
|
+
process.kill(process.pid, "SIGTERM");
|
|
1109
|
+
},
|
|
1110
|
+
})
|
|
1111
|
+
: undefined;
|
|
1112
|
+
try {
|
|
1113
|
+
await runCli();
|
|
1114
|
+
}
|
|
1115
|
+
finally {
|
|
1116
|
+
parentWatchdog?.stop();
|
|
1117
|
+
}
|
|
1081
1118
|
}
|
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
* settings use their canonical engine/strategy paths; retired aliases are not
|
|
13
13
|
* rewritten at this boundary.
|
|
14
14
|
*/
|
|
15
|
+
import { isDeepStrictEqual } from "node:util";
|
|
15
16
|
import { defineGroupCommand, defineJsonCommand, output } from "../cli/shared.js";
|
|
16
|
-
import { resolveStashDir } from "../core/common.js";
|
|
17
|
-
import { DEFAULT_CONFIG, loadConfig, mutateConfig } from "../core/config/config.js";
|
|
17
|
+
import { isRecord, resolveStashDir } from "../core/common.js";
|
|
18
|
+
import { DEFAULT_CONFIG, getConfigValueSource, loadConfig, mutateConfig, parseAndValidateConfigText, resolveConfigRefSource, } from "../core/config/config.js";
|
|
18
19
|
import { configGet, configSet, configUnset, unknownKeyHint } from "../core/config/config-walker.js";
|
|
20
|
+
import { UsageError } from "../core/errors.js";
|
|
19
21
|
import { getCacheDir, getConfigPath, getDbPath, getDefaultStashDir } from "../core/paths.js";
|
|
20
22
|
import { formatRegistryUrl } from "../core/registry-url.js";
|
|
21
23
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
@@ -53,6 +55,55 @@ function isRegistryUrlPath(path) {
|
|
|
53
55
|
return path[0] === "registries" && path.at(-1) === "url";
|
|
54
56
|
}
|
|
55
57
|
export { unknownKeyHint };
|
|
58
|
+
/**
|
|
59
|
+
* Flatten a (redacted) config object down to `{dottedPath: leafValue}`
|
|
60
|
+
* entries. A plain object descends; an array or scalar is a leaf — matching
|
|
61
|
+
* `deepMergeConfig`'s own "objects merge, arrays replace wholesale" rule, so
|
|
62
|
+
* an array difference reports as one row rather than per-index noise.
|
|
63
|
+
*/
|
|
64
|
+
function flattenConfigLeaves(value, prefix, out) {
|
|
65
|
+
if (isRecord(value) && Object.keys(value).length > 0) {
|
|
66
|
+
for (const [key, child] of Object.entries(value))
|
|
67
|
+
flattenConfigLeaves(child, [...prefix, key], out);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
out.set(prefix.join("."), value);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Diff two (already redacted) effective config objects into sorted
|
|
74
|
+
* `{path, local, other}` rows for every leaf that differs — including a leaf
|
|
75
|
+
* present on only one side (the other's value reads `undefined`).
|
|
76
|
+
*/
|
|
77
|
+
function diffConfigs(local, other) {
|
|
78
|
+
const localLeaves = new Map();
|
|
79
|
+
const otherLeaves = new Map();
|
|
80
|
+
flattenConfigLeaves(local, [], localLeaves);
|
|
81
|
+
flattenConfigLeaves(other, [], otherLeaves);
|
|
82
|
+
const paths = new Set([...localLeaves.keys(), ...otherLeaves.keys()]);
|
|
83
|
+
const rows = [];
|
|
84
|
+
for (const path of paths) {
|
|
85
|
+
const localValue = localLeaves.get(path);
|
|
86
|
+
const otherValue = otherLeaves.get(path);
|
|
87
|
+
if (!isDeepStrictEqual(localValue, otherValue))
|
|
88
|
+
rows.push({ path, local: localValue, other: otherValue });
|
|
89
|
+
}
|
|
90
|
+
rows.sort((a, b) => a.path.localeCompare(b.path));
|
|
91
|
+
return rows;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* `akm config diff <path|bundle//path>`: the current effective config
|
|
95
|
+
* (this instance's `extends` already applied) against another config file or
|
|
96
|
+
* bundle-relative file (loaded through the same loader, its own `extends`
|
|
97
|
+
* honoured), secrets redacted on both sides before comparison so a differing
|
|
98
|
+
* secret never round-trips into the diff.
|
|
99
|
+
*/
|
|
100
|
+
export function akmConfigDiff(ref) {
|
|
101
|
+
const current = loadConfig();
|
|
102
|
+
const { text, resolvedPath } = resolveConfigRefSource(ref, current, getConfigPath());
|
|
103
|
+
const other = parseAndValidateConfigText(text, resolvedPath);
|
|
104
|
+
const rows = diffConfigs(redactConfigValue(current), redactConfigValue(other));
|
|
105
|
+
return { rows };
|
|
106
|
+
}
|
|
56
107
|
// ── `akm config` command surface ────────────────────────────────────────────
|
|
57
108
|
// Extracted verbatim from src/cli.ts (WS6). The `main.subCommands.config` key
|
|
58
109
|
// and every config subcommand's args/output shape are byte-identical. Leaf
|
|
@@ -105,9 +156,16 @@ export const configCommand = defineGroupCommand({
|
|
|
105
156
|
meta: { name: "get", description: "Get a configuration value by key" },
|
|
106
157
|
args: {
|
|
107
158
|
key: { type: "positional", required: true, description: "Config key (for example: embedding, defaultBundle)" },
|
|
159
|
+
"show-source": {
|
|
160
|
+
type: "boolean",
|
|
161
|
+
description: 'Wrap the value as { value, source }, where source is "local", "extends:<ref>" (the nearest ' +
|
|
162
|
+
'extends chain member that sets it), or "default".',
|
|
163
|
+
default: false,
|
|
164
|
+
},
|
|
108
165
|
},
|
|
109
166
|
run({ args }) {
|
|
110
|
-
|
|
167
|
+
const value = getConfigValue(loadConfig(), args.key);
|
|
168
|
+
output("config", args["show-source"] ? { value, source: getConfigValueSource(args.key) } : value);
|
|
111
169
|
},
|
|
112
170
|
}),
|
|
113
171
|
set: defineJsonCommand({
|
|
@@ -152,6 +210,14 @@ export const configCommand = defineGroupCommand({
|
|
|
152
210
|
},
|
|
153
211
|
},
|
|
154
212
|
run({ args }) {
|
|
213
|
+
// A key only an `extends` base supplies has nothing local to remove:
|
|
214
|
+
// `unsetConfigValue` would silently no-op and the value comes right
|
|
215
|
+
// back on the next load. Refuse before mutating rather than pretend
|
|
216
|
+
// it worked.
|
|
217
|
+
const source = getConfigValueSource(args.key);
|
|
218
|
+
if (source.startsWith("extends:")) {
|
|
219
|
+
throw new UsageError(`Config key "${args.key}" is inherited from ${source} and cannot be unset here.`, "TARGET_NOT_UPDATABLE", `Override it locally with "akm config set ${args.key} <value>", or edit the base file directly.`);
|
|
220
|
+
}
|
|
155
221
|
const result = mutateConfig((current) => unsetConfigValue(current, args.key), { absentNoop: true });
|
|
156
222
|
const updated = result.config;
|
|
157
223
|
if (!args.silent) {
|
|
@@ -159,6 +225,22 @@ export const configCommand = defineGroupCommand({
|
|
|
159
225
|
}
|
|
160
226
|
},
|
|
161
227
|
}),
|
|
228
|
+
diff: defineJsonCommand({
|
|
229
|
+
meta: {
|
|
230
|
+
name: "diff",
|
|
231
|
+
description: "Show effective-config differences against another config file or bundle-relative file, secrets redacted",
|
|
232
|
+
},
|
|
233
|
+
args: {
|
|
234
|
+
ref: {
|
|
235
|
+
type: "positional",
|
|
236
|
+
required: true,
|
|
237
|
+
description: "Other config: a file path or bundle//path (relative to the bundle's content root)",
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
run({ args }) {
|
|
241
|
+
output("config-diff", akmConfigDiff(args.ref));
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
162
244
|
},
|
|
163
245
|
// The bare `akm config` invocation (and `akm config --list`) dumps the
|
|
164
246
|
// current config. defineGroupCommand short-circuits this body when a
|
|
@@ -22,52 +22,11 @@ import path from "node:path";
|
|
|
22
22
|
import { getParsedInvocation } from "../../cli/invocation.js";
|
|
23
23
|
import { getStringArg } from "../../cli/parse-args.js";
|
|
24
24
|
import { defineGroupCommand, defineJsonCommand, output } from "../../cli/shared.js";
|
|
25
|
-
import { deriveCanonicalAssetName } from "../../core/asset/asset-placement.js";
|
|
26
25
|
import { writeFileAtomic } from "../../core/common.js";
|
|
27
|
-
import {
|
|
28
|
-
import { makeEnvRef, resolveEnvPath, resolveEnvWriteTarget, withEnvSecretWrite } from "../../core/env-secret-ref.js";
|
|
26
|
+
import { listEnvsRecursive, makeEnvRef, resolveEnvPath, resolveEnvWriteTarget, sensitiveMarkerPath, withEnvSecretWrite, } from "../../core/env-secret-ref.js";
|
|
29
27
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
30
|
-
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
31
28
|
import { readStdin } from "../../runtime.js";
|
|
32
29
|
import { buildChildEnv } from "./child-env.js";
|
|
33
|
-
import { sensitiveMarkerPath } from "./marker-path.js";
|
|
34
|
-
/**
|
|
35
|
-
* Walk each stash's env files and return one entry per `.env` file, using the
|
|
36
|
-
* env asset spec's canonical-name logic (e.g. `env/team/prod.env` →
|
|
37
|
-
* `env/team/prod`, `env/team/.env` → `env/team/default`).
|
|
38
|
-
*/
|
|
39
|
-
function listEnvsRecursive(listKeysFn) {
|
|
40
|
-
const result = [];
|
|
41
|
-
for (const source of resolveSourceEntries(undefined, loadConfig())) {
|
|
42
|
-
const root = path.join(source.path, "env");
|
|
43
|
-
if (!fs.existsSync(root))
|
|
44
|
-
continue;
|
|
45
|
-
const walk = (dir) => {
|
|
46
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
47
|
-
const full = path.join(dir, entry.name);
|
|
48
|
-
if (entry.isDirectory()) {
|
|
49
|
-
walk(full);
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
if (!entry.isFile())
|
|
53
|
-
continue;
|
|
54
|
-
if (entry.name !== ".env" && !entry.name.endsWith(".env"))
|
|
55
|
-
continue;
|
|
56
|
-
const canonical = deriveCanonicalAssetName("env", root, full);
|
|
57
|
-
if (!canonical)
|
|
58
|
-
continue;
|
|
59
|
-
// Skip sensitive envs: a sibling .sensitive marker file suppresses listing.
|
|
60
|
-
const markerPath = sensitiveMarkerPath(full, "env");
|
|
61
|
-
if (fs.existsSync(markerPath))
|
|
62
|
-
continue;
|
|
63
|
-
const { keys } = listKeysFn(full);
|
|
64
|
-
result.push({ ref: makeEnvRef(canonical, source), path: full, keys });
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
walk(root);
|
|
68
|
-
}
|
|
69
|
-
return result;
|
|
70
|
-
}
|
|
71
30
|
const envListCommand = defineJsonCommand({
|
|
72
31
|
meta: { name: "list", description: "List all env files across all bundles with their key names (no values)" },
|
|
73
32
|
async run() {
|
package/dist/commands/env/env.js
CHANGED
|
@@ -49,7 +49,7 @@ import fs from "node:fs";
|
|
|
49
49
|
import path from "node:path";
|
|
50
50
|
import dotenv from "dotenv";
|
|
51
51
|
import { scanEnvKeyNames, writeFileAtomic } from "../../core/common.js";
|
|
52
|
-
import { sensitiveMarkerPath } from "
|
|
52
|
+
import { sensitiveMarkerPath } from "../../core/sensitive-marker-path.js";
|
|
53
53
|
export { scanEnvKeyNames };
|
|
54
54
|
/**
|
|
55
55
|
* Read and return ONLY non-secret metadata: key names.
|
|
@@ -35,14 +35,13 @@ import { defineGroupCommand, defineJsonCommand, output } from "../../cli/shared.
|
|
|
35
35
|
import { decideDangerousEnvInjection } from "../../core/activation-policy.js";
|
|
36
36
|
import { deriveCanonicalAssetName } from "../../core/asset/asset-placement.js";
|
|
37
37
|
import { loadConfig } from "../../core/config/config.js";
|
|
38
|
-
import { makeSecretRef, resolveSecretPath, resolveSecretWriteTarget, withEnvSecretWrite, } from "../../core/env-secret-ref.js";
|
|
38
|
+
import { makeSecretRef, resolveSecretPath, resolveSecretWriteTarget, sensitiveMarkerPath, withEnvSecretWrite, } from "../../core/env-secret-ref.js";
|
|
39
39
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
40
40
|
import { appendEvent } from "../../core/events.js";
|
|
41
41
|
import { warn } from "../../core/warn.js";
|
|
42
42
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
43
43
|
import { readStdin } from "../../runtime.js";
|
|
44
44
|
import { buildChildEnv } from "./child-env.js";
|
|
45
|
-
import { sensitiveMarkerPath } from "./marker-path.js";
|
|
46
45
|
function parseKeyListFlag(raw) {
|
|
47
46
|
if (raw === undefined)
|
|
48
47
|
return undefined;
|