akm-cli 0.9.8-beta.3 → 0.9.9-beta.1
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 +186 -132
- package/dist/commands/health/checks.js +40 -0
- package/dist/commands/health.js +57 -31
- package/dist/commands/migrate-cli.js +29 -189
- package/dist/commands/sources/add-cli.js +7 -0
- package/dist/commands/sources/installed-stashes.js +36 -8
- package/dist/commands/sources/self-update.js +104 -62
- package/dist/commands/sources/source-add.js +6 -5
- package/dist/commands/sources/sources-cli.js +7 -18
- package/dist/commands/tasks/tasks-cli.js +4 -3
- package/dist/commands/tasks/tasks.js +13 -6
- package/dist/core/adapter/adapter-ids.js +35 -0
- package/dist/core/adapter/adapters/index.js +29 -0
- package/dist/core/adapter/detect-adapter.js +91 -3
- package/dist/core/config/config.js +1 -1
- package/dist/core/config/schema/sources-bundles.js +23 -0
- package/dist/core/extra-params.js +1 -1
- package/dist/core/state/migrations.js +2 -4
- package/dist/core/state-db.js +31 -8
- package/dist/indexer/indexer.js +64 -1
- package/dist/scripts/akm-migrate-node.js +86534 -20434
- package/dist/scripts/akm-migrate.js +86415 -20280
- package/dist/tasks/backends/cron.js +21 -6
- package/dist/tasks/resolve-akm-bin.js +1 -1
- package/docs/README.md +1 -0
- package/docs/integration/bundling-akm.md +276 -0
- package/docs/migration/v0.9.0-troubleshooting.md +10 -14
- package/docs/migration/v0.9.1-to-v0.9.2.md +12 -16
- package/docs/reference/cli.md +59 -31
- package/docs/reference/tasks.md +4 -10
- package/package.json +2 -1
- package/dist/commands/migrate/config-extra-params.js +0 -61
- package/dist/commands/migrate/dead-residue.js +0 -113
- package/dist/commands/migrate/stale-txn.js +0 -49
|
@@ -31,7 +31,7 @@ export async function akmAdd(input) {
|
|
|
31
31
|
try {
|
|
32
32
|
const parsed = parseRegistryRef(ref);
|
|
33
33
|
if (parsed.source === "local") {
|
|
34
|
-
return addLocalSource(ref, parsed.sourcePath, stashDir, input.name);
|
|
34
|
+
return addLocalSource(ref, parsed.sourcePath, stashDir, input.name, input.adapter);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
catch {
|
|
@@ -40,22 +40,23 @@ export async function akmAdd(input) {
|
|
|
40
40
|
return addRegistryStash(ref, stashDir, input.writable);
|
|
41
41
|
}
|
|
42
42
|
/** Add a local directory as a filesystem bundle. */
|
|
43
|
-
async function addLocalSource(ref, sourcePath, stashDir, explicitName) {
|
|
43
|
+
async function addLocalSource(ref, sourcePath, stashDir, explicitName, explicitAdapter) {
|
|
44
44
|
const stashRoot = detectStashRoot(sourcePath);
|
|
45
45
|
const resolvedPath = path.resolve(stashRoot);
|
|
46
|
+
const adapter = explicitAdapter ?? detectAdapterId(resolvedPath);
|
|
46
47
|
let bundleKey = explicitName ?? toReadableId(resolvedPath);
|
|
47
48
|
mutateConfig((config) => {
|
|
48
49
|
const existing = bundleKeyForPath(config, resolvedPath);
|
|
49
50
|
if (existing) {
|
|
50
51
|
bundleKey = existing;
|
|
51
52
|
const current = config.bundles?.[existing];
|
|
52
|
-
if (current?.components)
|
|
53
|
+
if (current?.components && explicitAdapter === undefined)
|
|
53
54
|
return config;
|
|
54
55
|
const bundles = { ...(config.bundles ?? {}) };
|
|
55
56
|
bundles[existing] = {
|
|
56
57
|
...current,
|
|
57
58
|
path: resolvedPath,
|
|
58
|
-
components: { main: { root: ".", adapter
|
|
59
|
+
components: { main: { root: ".", adapter } },
|
|
59
60
|
};
|
|
60
61
|
return { ...config, bundles };
|
|
61
62
|
}
|
|
@@ -63,7 +64,7 @@ async function addLocalSource(ref, sourcePath, stashDir, explicitName) {
|
|
|
63
64
|
bundleKey = nextBundleKey(bundles, explicitName, resolvedPath);
|
|
64
65
|
bundles[bundleKey] = {
|
|
65
66
|
path: resolvedPath,
|
|
66
|
-
components: { main: { root: ".", adapter
|
|
67
|
+
components: { main: { root: ".", adapter } },
|
|
67
68
|
};
|
|
68
69
|
return { ...config, bundles };
|
|
69
70
|
});
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { defineCommand } from "citty";
|
|
29
29
|
import { getParsedInvocation } from "../../cli/invocation.js";
|
|
30
|
-
import { defineJsonCommand, GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors } from "../../cli/shared.js";
|
|
30
|
+
import { defineJsonCommand, EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors } from "../../cli/shared.js";
|
|
31
31
|
import { loadConfig } from "../../core/config/config.js";
|
|
32
32
|
import { UsageError } from "../../core/errors.js";
|
|
33
33
|
import { appendEvent } from "../../core/events.js";
|
|
34
34
|
import { resolveWritableOverride, saveGitStash } from "../../sources/providers/git.js";
|
|
35
35
|
import { pkgVersion } from "../../version.js";
|
|
36
|
-
import { checkForUpdate, performUpgrade
|
|
36
|
+
import { checkForUpdate, performUpgrade } from "./self-update.js";
|
|
37
37
|
import { akmClone } from "./source-clone.js";
|
|
38
38
|
export const upgradeCommand = defineJsonCommand({
|
|
39
39
|
meta: { name: "upgrade", description: "Upgrade akm to the latest release" },
|
|
@@ -45,24 +45,8 @@ export const upgradeCommand = defineJsonCommand({
|
|
|
45
45
|
description: "Skip the post-upgrade index rebuild",
|
|
46
46
|
default: false,
|
|
47
47
|
},
|
|
48
|
-
"state-only": {
|
|
49
|
-
type: "boolean",
|
|
50
|
-
description: "Apply pending state.db migrations without installing a new akm",
|
|
51
|
-
default: false,
|
|
52
|
-
},
|
|
53
48
|
},
|
|
54
49
|
async run({ args }) {
|
|
55
|
-
// Applying a historical destructive state migration used to be reachable
|
|
56
|
-
// ONLY as a post-install step of a real upgrade, so an install akm cannot
|
|
57
|
-
// rewrite -- a global npm install owned by root, an image that ships the
|
|
58
|
-
// CLI -- had no route to it at all: the npm step fails EACCES and throws
|
|
59
|
-
// long before the migration runs (#895). The migration is a local,
|
|
60
|
-
// offline, already-verified operation; it does not need the network or a
|
|
61
|
-
// new binary, and coupling it to one was the bug.
|
|
62
|
-
if (args["state-only"]) {
|
|
63
|
-
output("upgrade", upgradeStateOnly(pkgVersion));
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
50
|
const check = await checkForUpdate(pkgVersion);
|
|
67
51
|
if (args.check) {
|
|
68
52
|
output("upgrade", check);
|
|
@@ -71,6 +55,11 @@ export const upgradeCommand = defineJsonCommand({
|
|
|
71
55
|
const skipPostUpgrade = args["skip-post-upgrade"];
|
|
72
56
|
const result = await performUpgrade(check, { force: args.force, skipPostUpgrade });
|
|
73
57
|
output("upgrade", result);
|
|
58
|
+
// The install may have succeeded, but an upgrade whose migration is
|
|
59
|
+
// blocked or could not run is not done: exit like `akm migrate apply` does.
|
|
60
|
+
if (result.migration?.status === "blocked" || result.migration?.status === "failed") {
|
|
61
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
62
|
+
}
|
|
74
63
|
},
|
|
75
64
|
});
|
|
76
65
|
// `sync` body, standalone so the git-commit/push logic stays in one place.
|
|
@@ -349,10 +349,11 @@ const tasksSyncCommand = defineJsonCommand({
|
|
|
349
349
|
const result = await akmTasksSync({}, args.bundle, { rebind });
|
|
350
350
|
output("task-sync", result);
|
|
351
351
|
// #867: sync degrades — sources that failed to parse/prepare are
|
|
352
|
-
// excluded from reconciliation and reported in `result.
|
|
352
|
+
// excluded from reconciliation and reported in `result.failures` rather
|
|
353
353
|
// than poisoning the whole sync, but their presence must still fail
|
|
354
|
-
// the command's exit code so the breakage stays visible.
|
|
355
|
-
|
|
354
|
+
// the command's exit code so the breakage stays visible. (#906: this key
|
|
355
|
+
// matches the `--dry-run` preview's `failures` field — no separate name.)
|
|
356
|
+
if (result.failures.length > 0)
|
|
356
357
|
process.exitCode = EXIT_CODES.GENERAL;
|
|
357
358
|
},
|
|
358
359
|
});
|
|
@@ -391,7 +391,7 @@ async function buildSchedulerSyncPlan(deps, bundleTarget, options) {
|
|
|
391
391
|
const expectedSignature = sched.expectedSignature?.bind(sched);
|
|
392
392
|
const needsRuntime = preflight.operations.some((operation) => operation.kind !== "remove" && operation.options?.binding === undefined);
|
|
393
393
|
const prepared = needsRuntime
|
|
394
|
-
? prepareSchedulerSyncRuntime(syncTarget ? { target: syncTarget } : undefined, deps, options.rebind === true, "reconcile native scheduler bindings", warnings)
|
|
394
|
+
? prepareSchedulerSyncRuntime(syncTarget ? { target: syncTarget } : undefined, deps, options.rebind === true, "reconcile native scheduler bindings", warnings, allEntries.map((entry) => entry.binding))
|
|
395
395
|
: undefined;
|
|
396
396
|
const plan = finalizeSchedulerSyncPlan({
|
|
397
397
|
...common,
|
|
@@ -416,7 +416,7 @@ export async function akmTasksSync(deps = {}, bundleTarget, options = {}) {
|
|
|
416
416
|
unchanged: [...plan.unchanged],
|
|
417
417
|
skipped: [],
|
|
418
418
|
backend: sched.name,
|
|
419
|
-
|
|
419
|
+
failures: plan.failures.map((failure) => ({ ...failure })),
|
|
420
420
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
421
421
|
};
|
|
422
422
|
}
|
|
@@ -841,16 +841,16 @@ async function prepareTaskAddSchedulerTransaction(input) {
|
|
|
841
841
|
publishOperationIndex: removals.length,
|
|
842
842
|
});
|
|
843
843
|
}
|
|
844
|
-
function prepareSchedulerSyncRuntime(base, deps, explicitRebind, operation, warnings) {
|
|
844
|
+
function prepareSchedulerSyncRuntime(base, deps, explicitRebind, operation, warnings, installedBindings = []) {
|
|
845
845
|
if (deps.backend && !deps.schedulerRuntime)
|
|
846
846
|
return base ? { options: base } : {};
|
|
847
847
|
if (deps.schedulerRuntime) {
|
|
848
848
|
const runtime = deps.schedulerRuntime();
|
|
849
|
-
warnIneligibleRebind(runtime, explicitRebind, warnings);
|
|
849
|
+
warnIneligibleRebind(runtime, explicitRebind, warnings, installedBindings);
|
|
850
850
|
return { options: { ...base, binding: runtime.binding, contextPath: runtime.contextPath } };
|
|
851
851
|
}
|
|
852
852
|
const invocation = resolveAndValidateSchedulerInvocation(explicitRebind, operation);
|
|
853
|
-
warnIneligibleRebind(invocation, explicitRebind, warnings);
|
|
853
|
+
warnIneligibleRebind(invocation, explicitRebind, warnings, installedBindings);
|
|
854
854
|
const descriptor = schedulerContextDescriptor();
|
|
855
855
|
const contextPath = schedulerContextPath(descriptor);
|
|
856
856
|
return {
|
|
@@ -870,9 +870,16 @@ function resolveAndValidateSchedulerInvocation(explicitRebind, operation) {
|
|
|
870
870
|
}
|
|
871
871
|
return { binding: invocation.argv, contextPath: "", eligible: invocation.eligible, kind: invocation.kind };
|
|
872
872
|
}
|
|
873
|
-
function warnIneligibleRebind(runtime, explicitRebind, warnings) {
|
|
873
|
+
function warnIneligibleRebind(runtime, explicitRebind, warnings, installedBindings) {
|
|
874
874
|
if (!explicitRebind || runtime.eligible !== false || warnings.length > 0)
|
|
875
875
|
return;
|
|
876
|
+
// #868 residue: a `--rebind` that binds every currently-installed
|
|
877
|
+
// entry to the SAME invocation it already carries changes nothing — this
|
|
878
|
+
// is the steady state of an image-baked install re-running `task sync
|
|
879
|
+
// --rebind` on a timer. Only warn when the rebind actually moves an entry
|
|
880
|
+
// to a different invocation.
|
|
881
|
+
if (installedBindings.length > 0 && installedBindings.every((bound) => sameArgv(bound, runtime.binding)))
|
|
882
|
+
return;
|
|
876
883
|
warnings.push(`--rebind bound scheduled tasks to an ineligible ${runtime.kind ?? "unknown"} invocation (${runtime.binding.join(" ")}); scheduled runs will invoke a mutable, unproven binary. Install akm via \`npm install --global akm-cli\` or a standalone release, then re-run \`akm task sync --rebind\`.`);
|
|
877
884
|
}
|
|
878
885
|
function groupInstalledBindings(entries, invocation) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Dependency-free adapter-id table (#909) — mirrors
|
|
6
|
+
* `src/integrations/harnesses/ids.ts`'s split from its own heavy barrel.
|
|
7
|
+
*
|
|
8
|
+
* `core/config/schema/sources-bundles.ts` needs one small, DATA-shaped fact
|
|
9
|
+
* about the adapter registry: the canonical ordered id list, to validate
|
|
10
|
+
* `components.*.adapter` and to reject a typo instead of silently falling
|
|
11
|
+
* back to `akm` (#909). Importing `./registry.ts` (`BUILTIN_ADAPTERS`) for
|
|
12
|
+
* that would pull in all 11 concrete adapters and, transitively, the indexer
|
|
13
|
+
* modules they delegate to (`indexer/passes/metadata`, `core/asset/*`, …) —
|
|
14
|
+
* weight a config-schema module has no reason to carry just to validate one
|
|
15
|
+
* enum. This table is the canonical, dependency-free MIRROR of the id list;
|
|
16
|
+
* `./adapters/index.ts`'s `BUILTIN_ADAPTERS` construction asserts its ids
|
|
17
|
+
* match this table (order included) at module-load time, so the two can
|
|
18
|
+
* never silently drift without a loud failure.
|
|
19
|
+
*/
|
|
20
|
+
/** Canonical, ordered list of valid adapter ids (matches `BUILTIN_ADAPTERS` order). */
|
|
21
|
+
export const ADAPTER_ID_TABLE = [
|
|
22
|
+
"website-snapshot",
|
|
23
|
+
"agent-skills",
|
|
24
|
+
"claude",
|
|
25
|
+
"opencode",
|
|
26
|
+
"dotenv",
|
|
27
|
+
"akm-workflow",
|
|
28
|
+
"akm-task",
|
|
29
|
+
"llm-wiki",
|
|
30
|
+
"akm",
|
|
31
|
+
"okf",
|
|
32
|
+
"generic-files",
|
|
33
|
+
];
|
|
34
|
+
/** The dependency-free counterpart of `./registry.ts`'s `getAdapters().map(a => a.id)`. */
|
|
35
|
+
export const VALID_ADAPTER_IDS = ADAPTER_ID_TABLE;
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Built-in `BundleAdapter` barrel + the static, frozen `BUILTIN_ADAPTERS` list
|
|
6
|
+
* (normative §12.6) — akm 0.9.0 chunk-2 (WI-A) + the format-family work item
|
|
7
|
+
* (#46).
|
|
8
|
+
*
|
|
9
|
+
* `BUILTIN_ADAPTERS` is the ordered built-in adapter set the registry
|
|
10
|
+
* (`../registry`) exposes via `getAdapters()` / `adapterForId()`. It is a
|
|
11
|
+
* plain frozen array populated at MODULE LOAD — there is no mutable
|
|
12
|
+
* registration step and no load-order dependency, so no production call site
|
|
13
|
+
* (`installations.ts#detectAdapterId`, `provider-utils.ts#detectStashRoot`)
|
|
14
|
+
* depends on anyone first calling a registration function (normative §12.6 —
|
|
15
|
+
* "static frozen `BUILTIN_ADAPTERS` map"). The earlier mutable
|
|
16
|
+
* `registerAdapter` singleton that this replaced is retired.
|
|
17
|
+
*/
|
|
18
|
+
import { ADAPTER_ID_TABLE } from "../adapter-ids.js";
|
|
4
19
|
import { agentSkillsAdapter } from "./agent-skills-adapter.js";
|
|
5
20
|
import { akmAdapter } from "./akm-adapter.js";
|
|
6
21
|
import { akmTaskAdapter } from "./akm-task-adapter.js";
|
|
@@ -69,3 +84,17 @@ export const BUILTIN_ADAPTERS = Object.freeze([
|
|
|
69
84
|
// Explicit-config fallback (never auto-selected) — last.
|
|
70
85
|
genericFilesAdapter,
|
|
71
86
|
]);
|
|
87
|
+
// Construction-time drift guard (#909): ../adapter-ids.ts's ADAPTER_ID_TABLE
|
|
88
|
+
// is config's dependency-free mirror of this list, kept out of core/config's
|
|
89
|
+
// import graph so config doesn't have to import this (heavier) barrel. Assert
|
|
90
|
+
// they match so the mirror can never silently drift — a new/reordered/renamed
|
|
91
|
+
// adapter that forgets to update adapter-ids.ts fails loudly here instead of
|
|
92
|
+
// quietly breaking config's `components.*.adapter` validation.
|
|
93
|
+
{
|
|
94
|
+
const actualIds = BUILTIN_ADAPTERS.map((a) => a.id);
|
|
95
|
+
const expectedIds = [...ADAPTER_ID_TABLE];
|
|
96
|
+
if (actualIds.length !== expectedIds.length || actualIds.some((id, i) => id !== expectedIds[i])) {
|
|
97
|
+
throw new Error(`adapter-ids.ts ADAPTER_ID_TABLE (${expectedIds.join(", ")}) does not match ` +
|
|
98
|
+
`BUILTIN_ADAPTERS (${actualIds.join(", ")}) — update adapter-ids.ts to match.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -1,13 +1,101 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
import
|
|
5
|
-
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { adapterForId, getAdapters } from "./registry.js";
|
|
7
|
+
/**
|
|
8
|
+
* Tool-dir-shaped adapters (#908): their install-time probe recognizes only a
|
|
9
|
+
* BOUNDED slice of the root — `claude`/`opencode`'s own `commands`/`agents`/
|
|
10
|
+
* `skills` tool dirs, or `agent-skills`' own `<name>/SKILL.md` packages. A
|
|
11
|
+
* bundle that ALSO carries ordinary akm content (`knowledge/`, `workflows/`,
|
|
12
|
+
* a stray `content/` folder of Markdown, …) alongside one of these layouts had
|
|
13
|
+
* that content SILENTLY dropped: the narrow adapter won the ordered probe and
|
|
14
|
+
* indexed only its own three-dir slice, with no warning that anything else
|
|
15
|
+
* was there (issue #908 — 73 documents disappeared from one real bundle).
|
|
16
|
+
*
|
|
17
|
+
* `okf` / `llm-wiki` / `dotenv` / `website-snapshot` / `akm-workflow` /
|
|
18
|
+
* `akm-task` are deliberately NOT in this set: each carries its own tight,
|
|
19
|
+
* disjoint marker (a root index document, `schema.md`+`pages/`, an env/secrets-only
|
|
20
|
+
* layout, `manifest.json`, a workflow/task-shaped top-level file) that is not
|
|
21
|
+
* at risk of firing merely because a FEW directory names happen to overlap
|
|
22
|
+
* with akm's own stash subdirs — narrowing the fix to the three families the
|
|
23
|
+
* issue is actually about keeps this from touching adapters it was never
|
|
24
|
+
* about.
|
|
25
|
+
*/
|
|
26
|
+
const SHADOWABLE_ADAPTER_IDS = new Set(["agent-skills", "claude", "opencode"]);
|
|
27
|
+
/**
|
|
28
|
+
* True when `root` — already claimed by `winnerId` (one of
|
|
29
|
+
* {@link SHADOWABLE_ADAPTER_IDS}) — ALSO carries a top-level directory the
|
|
30
|
+
* `akm` adapter's own probe recognizes as its workspace shape (spec §1.2) and
|
|
31
|
+
* that `winnerId` does not own (its `directoryList()`, or — for `agent-skills`,
|
|
32
|
+
* which owns no fixed directory names — a root-level `<name>/SKILL.md`
|
|
33
|
+
* package) but which holds at least one real file. Cheap by design: only
|
|
34
|
+
* shallow `readdirSync` calls, one level into each candidate top-level dir —
|
|
35
|
+
* no recursive walk, no file content read, no git spawn — so this never adds
|
|
36
|
+
* meaningful cost to the install-time probe it augments.
|
|
37
|
+
*/
|
|
38
|
+
function hasExtraAkmContent(root, winnerId) {
|
|
39
|
+
const akm = adapterForId("akm");
|
|
40
|
+
if (akm?.looksLikeRoot?.(root) !== true)
|
|
41
|
+
return false;
|
|
42
|
+
const winner = adapterForId(winnerId);
|
|
43
|
+
const stubComponent = { id: "detect", adapter: winnerId, root, writable: false };
|
|
44
|
+
const ownedDirs = new Set(winner?.directoryList?.(stubComponent) ?? []);
|
|
45
|
+
let entries;
|
|
46
|
+
try {
|
|
47
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (!entry.isDirectory() || entry.name.startsWith("."))
|
|
54
|
+
continue;
|
|
55
|
+
if (ownedDirs.has(entry.name))
|
|
56
|
+
continue;
|
|
57
|
+
if (winnerId === "agent-skills") {
|
|
58
|
+
// A root-level `<name>/SKILL.md` package IS agent-skills' own recognized
|
|
59
|
+
// surface even though it has no fixed directoryList — not "extra" content.
|
|
60
|
+
try {
|
|
61
|
+
if (fs.statSync(path.join(root, entry.name, "SKILL.md")).isFile())
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Not a skill package — fall through to the candidate-file check below.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
let children;
|
|
69
|
+
try {
|
|
70
|
+
children = fs.readdirSync(path.join(root, entry.name), { withFileTypes: true });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (children.some((child) => child.isFile() && !child.name.startsWith(".")))
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Select the first built-in adapter whose ordered root probe claims `root`.
|
|
82
|
+
*
|
|
83
|
+
* A mixed-layout bundle (#908) that both a tool-dir-shaped adapter AND the
|
|
84
|
+
* `akm` adapter would claim detects as `akm` — the superset, which still
|
|
85
|
+
* indexes the narrower layout's own files correctly (`agent-skills`'
|
|
86
|
+
* `<name>/SKILL.md` packages, `claude`/`opencode`'s `commands`/`agents`/
|
|
87
|
+
* `skills`) while also picking up whatever else is in the bundle. A bundle
|
|
88
|
+
* that carries ONLY the narrow layout (no extra content) is unaffected.
|
|
89
|
+
*/
|
|
6
90
|
export function detectAdapterId(root, fallback = "akm") {
|
|
7
91
|
for (const adapter of getAdapters()) {
|
|
8
92
|
try {
|
|
9
|
-
if (adapter.looksLikeRoot?.(root) === true)
|
|
93
|
+
if (adapter.looksLikeRoot?.(root) === true) {
|
|
94
|
+
if (SHADOWABLE_ADAPTER_IDS.has(adapter.id) && hasExtraAkmContent(root, adapter.id)) {
|
|
95
|
+
return "akm";
|
|
96
|
+
}
|
|
10
97
|
return adapter.id;
|
|
98
|
+
}
|
|
11
99
|
}
|
|
12
100
|
catch {
|
|
13
101
|
// An unreadable or racing probe does not claim the bundle.
|
|
@@ -151,7 +151,7 @@ export function parseAndValidateConfigText(text, sourcePath) {
|
|
|
151
151
|
// rewritten onto the first-class engine field they now shadow. This used
|
|
152
152
|
// to happen silently, in memory, on every load; that ran forever and never
|
|
153
153
|
// converged. The lift itself is now `akm migrate apply`'s job (see
|
|
154
|
-
//
|
|
154
|
+
// scripts/akm-migrate/migrate/config-extra-params.ts) and persists to disk, so a
|
|
155
155
|
// config that has not been migrated yet fails closed here instead of
|
|
156
156
|
// silently drifting from what's on disk.
|
|
157
157
|
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
@@ -7,9 +7,15 @@
|
|
|
7
7
|
* change.
|
|
8
8
|
*/
|
|
9
9
|
import { z } from "zod";
|
|
10
|
+
// Dependency-free mirror of the adapter registry's id list (#909) — see
|
|
11
|
+
// adapter-ids.ts's header for why config imports this leaf rather than
|
|
12
|
+
// `core/adapter/registry.ts` (which would pull in all 11 concrete adapters
|
|
13
|
+
// and, transitively, the indexer modules they delegate to).
|
|
14
|
+
import { VALID_ADAPTER_IDS } from "../../adapter/adapter-ids.js";
|
|
10
15
|
import { isBundleSlug } from "../../asset/asset-ref.js";
|
|
11
16
|
import { hasRegistryUrlCredentials, REGISTRY_CREDENTIALS_UNSUPPORTED } from "../../registry-url.js";
|
|
12
17
|
import { httpUrl, nonEmptyString, positiveInt } from "./primitives.js";
|
|
18
|
+
const VALID_ADAPTER_IDS_SET = new Set(VALID_ADAPTER_IDS);
|
|
13
19
|
// ── Sources / registries / installed ────────────────────────────────────────
|
|
14
20
|
const SourceConfigEntryOptionsSchema = z.record(z.unknown());
|
|
15
21
|
export const SourceConfigEntrySchema = z
|
|
@@ -165,6 +171,23 @@ export const BundleConfigEntrySchema = z
|
|
|
165
171
|
message: "writable: true is only supported on path and git bundle sources",
|
|
166
172
|
});
|
|
167
173
|
}
|
|
174
|
+
// #909: an unrecognized `components.*.adapter` used to silently fall back
|
|
175
|
+
// to `akm` at detect-time with no error and no disclosure — a typo
|
|
176
|
+
// (`agent_skills`, `akm-native`, …) then silently changed which files got
|
|
177
|
+
// indexed. Reject it here instead, the same treatment other enum-valued
|
|
178
|
+
// config fields get, listing the accepted values (derived from the
|
|
179
|
+
// adapter registry, never hardcoded — see adapter-ids.ts).
|
|
180
|
+
if (componentEntry !== undefined) {
|
|
181
|
+
const [componentName, componentValue] = componentEntry;
|
|
182
|
+
const adapterValue = componentValue.adapter;
|
|
183
|
+
if (adapterValue !== undefined && !VALID_ADAPTER_IDS_SET.has(adapterValue)) {
|
|
184
|
+
ctx.addIssue({
|
|
185
|
+
code: z.ZodIssueCode.custom,
|
|
186
|
+
path: ["components", componentName, "adapter"],
|
|
187
|
+
message: `unrecognized adapter "${adapterValue}"; expected one of: ${VALID_ADAPTER_IDS.join(", ")}`,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
168
191
|
});
|
|
169
192
|
/**
|
|
170
193
|
* `bundles` map. Keys are workspace bundle slugs (spec §11.1 / D-R5 charset).
|
|
@@ -125,7 +125,7 @@ export function formatExtraParamsIssue(label, issue) {
|
|
|
125
125
|
* (#852, following #815).
|
|
126
126
|
*
|
|
127
127
|
* Pure: never touches the filesystem. Two callers use this differently:
|
|
128
|
-
* `akm migrate apply` (
|
|
128
|
+
* `akm migrate apply` (scripts/akm-migrate/migrate/config-extra-params.ts) uses the
|
|
129
129
|
* returned `config` to persist the rewrite to disk, once; `parseAndValidateConfigText`
|
|
130
130
|
* (src/core/config/config.ts) calls this only to detect whether a lift is
|
|
131
131
|
* needed and discards `config` — an unmigrated config fails closed there
|
|
@@ -1187,8 +1187,7 @@ export function runMigrations(db, options) {
|
|
|
1187
1187
|
}
|
|
1188
1188
|
if (!options.allowHistoricalDestructiveStateUpgrade) {
|
|
1189
1189
|
throw new Error("Refusing to migrate an existing unversioned state.db during an ordinary managed open. " +
|
|
1190
|
-
"Run `akm upgrade
|
|
1191
|
-
"or `akm upgrade --state-only` where akm cannot reinstall itself (container/global install).");
|
|
1190
|
+
"Run `akm upgrade` (or `akm migrate apply`) to snapshot it before migration 001 and apply it deliberately.");
|
|
1192
1191
|
}
|
|
1193
1192
|
const ledger = assertMigrationLedger(lockedDb, STATE_MIGRATIONS);
|
|
1194
1193
|
if (ledger.migrationIds.length !== 0) {
|
|
@@ -1218,8 +1217,7 @@ export function runMigrations(db, options) {
|
|
|
1218
1217
|
assertMigrationLedger(lockedDb, STATE_MIGRATIONS);
|
|
1219
1218
|
if (!options?.allowHistoricalDestructiveStateUpgrade) {
|
|
1220
1219
|
throw new Error(`Refusing to apply historical destructive state migration ${migration.id} during an ordinary managed open. ` +
|
|
1221
|
-
"Run `akm upgrade
|
|
1222
|
-
"or `akm upgrade --state-only` where akm cannot reinstall itself (container/global install).");
|
|
1220
|
+
"Run `akm upgrade` (or `akm migrate apply`) to create a sibling state.db safety copy and apply it deliberately.");
|
|
1223
1221
|
}
|
|
1224
1222
|
if (!options.beforeHistoricalDestructiveMigration) {
|
|
1225
1223
|
throw new Error(`Historical destructive state migration ${migration.id} requires a verified safety-copy hook.`);
|
package/dist/core/state-db.js
CHANGED
|
@@ -407,8 +407,8 @@ export function openStateDatabase(dbPath, options) {
|
|
|
407
407
|
existingUnversionedDatabase = ledger.migrationIds.length === 0;
|
|
408
408
|
if (existingUnversionedDatabase && !options?.allowHistoricalDestructiveStateUpgrade) {
|
|
409
409
|
throw new Error("Refusing to migrate an existing unversioned state.db during an ordinary managed open. " +
|
|
410
|
-
"Run `akm upgrade
|
|
411
|
-
"
|
|
410
|
+
"Run `akm upgrade` (or `akm migrate apply`) to create a verified snapshot before migration 001 " +
|
|
411
|
+
"and apply it deliberately.");
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
414
|
finally {
|
|
@@ -499,13 +499,36 @@ export function openStateDatabase(dbPath, options) {
|
|
|
499
499
|
}
|
|
500
500
|
}
|
|
501
501
|
/**
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
502
|
+
* Read-only: the state migration IDs the running akm would apply to `dbPath`,
|
|
503
|
+
* in ledger order. Empty when the database is missing or current. Throws on a
|
|
504
|
+
* ledger this akm cannot own (newer, or not an exact ordered prefix) -- the
|
|
505
|
+
* same refusal a managed open makes.
|
|
505
506
|
*/
|
|
506
|
-
export function
|
|
507
|
+
export function listPendingStateMigrations(dbPath = getStateDbPath()) {
|
|
507
508
|
if (!fs.existsSync(dbPath))
|
|
508
|
-
return
|
|
509
|
+
return [];
|
|
510
|
+
const preflight = openDatabase(dbPath, { readonly: true });
|
|
511
|
+
try {
|
|
512
|
+
preflight.exec("PRAGMA busy_timeout = 30000");
|
|
513
|
+
const ledger = assertMigrationLedger(preflight, STATE_MIGRATIONS);
|
|
514
|
+
return STATE_MIGRATIONS.slice(ledger.migrationIds.length).map((migration) => migration.id);
|
|
515
|
+
}
|
|
516
|
+
finally {
|
|
517
|
+
preflight.close();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Apply every pending state migration, historical-destructive ones included:
|
|
522
|
+
* the one step `akm upgrade` and `akm migrate apply` share, and the only
|
|
523
|
+
* caller that may admit migration 018 (an ordinary managed open refuses it by
|
|
524
|
+
* design). Missing/current databases are no-ops. A pre-018 exact ledger, or an
|
|
525
|
+
* unversioned database, is snapshotted beside state.db and verified before the
|
|
526
|
+
* immutable released migration runs.
|
|
527
|
+
*/
|
|
528
|
+
export function upgradeHistoricalStateDatabase(dbPath = getStateDbPath()) {
|
|
529
|
+
const pending = listPendingStateMigrations(dbPath);
|
|
530
|
+
if (pending.length === 0)
|
|
531
|
+
return { upgraded: false, applied: [] };
|
|
509
532
|
let safetyCopyPath;
|
|
510
533
|
try {
|
|
511
534
|
const db = openStateDatabase(dbPath, {
|
|
@@ -521,7 +544,7 @@ export function upgradeHistoricalStateDatabase(dbPath = getStateDbPath()) {
|
|
|
521
544
|
const recovery = safetyCopyPath ? ` Verified safety copy: ${safetyCopyPath}.` : "";
|
|
522
545
|
throw new Error(`${detail}${recovery}`);
|
|
523
546
|
}
|
|
524
|
-
return safetyCopyPath ? { upgraded: true, safetyCopyPath } : { upgraded:
|
|
547
|
+
return safetyCopyPath ? { upgraded: true, applied: pending, safetyCopyPath } : { upgraded: true, applied: pending };
|
|
525
548
|
}
|
|
526
549
|
/**
|
|
527
550
|
* Run `fn` against state.db, owning the handle unless one is borrowed. The loan
|
package/dist/indexer/indexer.js
CHANGED
|
@@ -13,7 +13,7 @@ import { classifyPathAccess, describeInaccessiblePath } from "../core/path-acces
|
|
|
13
13
|
import { getDbPath } from "../core/paths.js";
|
|
14
14
|
import { SCRIPT_EXTENSIONS } from "../core/recognition-util.js";
|
|
15
15
|
import { withStateDb } from "../core/state-db.js";
|
|
16
|
-
import { isVerbose, warn, warnVerbose } from "../core/warn.js";
|
|
16
|
+
import { isVerbose, warn, warnOnce, warnVerbose } from "../core/warn.js";
|
|
17
17
|
import { disposeLoweredExecutionDispatchLease, } from "../integrations/agent/execution-lowering.js";
|
|
18
18
|
import { isLlmFeatureEnabled } from "../llm/feature-gate.js";
|
|
19
19
|
import { resolveIndexPassExecution } from "../llm/index-passes.js";
|
|
@@ -750,6 +750,67 @@ function sourceSnapshotRemovals(db, currentStashDir, bundleId, currentDirs, allI
|
|
|
750
750
|
reason: { kind: "not-in-source-snapshot" },
|
|
751
751
|
}));
|
|
752
752
|
}
|
|
753
|
+
/**
|
|
754
|
+
* Warn ONCE per process (#908) when the chosen adapter for a component
|
|
755
|
+
* entirely skips a top-level directory that holds files the `akm` adapter —
|
|
756
|
+
* the format-neutral superset — would have indexed. `detectAdapterId` now
|
|
757
|
+
* corrects this for AUTO-DETECTION (a mixed layout detects as `akm`); this
|
|
758
|
+
* covers the case detection cannot see, an EXPLICITLY configured narrow
|
|
759
|
+
* adapter (`components.<name>.adapter: "agent-skills"`, say) sitting next to
|
|
760
|
+
* ordinary akm content. One line for the whole process — not one per bundle,
|
|
761
|
+
* not one per directory — naming the count and the directories is enough to
|
|
762
|
+
* point an operator at the fix.
|
|
763
|
+
*/
|
|
764
|
+
function warnIfAdapterSkipsAkmContent(component, files, adapter) {
|
|
765
|
+
if (adapter.id === "akm")
|
|
766
|
+
return;
|
|
767
|
+
const akm = adapterForId("akm");
|
|
768
|
+
if (!akm)
|
|
769
|
+
return;
|
|
770
|
+
const byTopDir = new Map();
|
|
771
|
+
for (const file of files) {
|
|
772
|
+
const top = file.ancestorDirs[0];
|
|
773
|
+
if (!top)
|
|
774
|
+
continue; // a root-level file is not a "skipped directory" concern
|
|
775
|
+
const group = byTopDir.get(top);
|
|
776
|
+
if (group)
|
|
777
|
+
group.push(file);
|
|
778
|
+
else
|
|
779
|
+
byTopDir.set(top, [file]);
|
|
780
|
+
}
|
|
781
|
+
const akmComponent = { ...component, adapter: "akm" };
|
|
782
|
+
let skippedCount = 0;
|
|
783
|
+
const skippedDirs = [];
|
|
784
|
+
for (const [dir, dirFiles] of byTopDir) {
|
|
785
|
+
const chosenRecognizesAny = dirFiles.some((file) => {
|
|
786
|
+
try {
|
|
787
|
+
return adapter.recognize(component, file) !== null;
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
if (chosenRecognizesAny)
|
|
794
|
+
continue; // the chosen adapter owns this dir; nothing skipped
|
|
795
|
+
const akmCandidates = dirFiles.filter((file) => {
|
|
796
|
+
try {
|
|
797
|
+
return akm.recognize(akmComponent, file) !== null;
|
|
798
|
+
}
|
|
799
|
+
catch {
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
if (akmCandidates.length === 0)
|
|
804
|
+
continue; // akm would drop it too — not a shadowing case
|
|
805
|
+
skippedCount += akmCandidates.length;
|
|
806
|
+
skippedDirs.push(dir);
|
|
807
|
+
}
|
|
808
|
+
if (skippedCount === 0)
|
|
809
|
+
return;
|
|
810
|
+
skippedDirs.sort();
|
|
811
|
+
warnOnce("adapter-skip-akm-content", `${adapter.id} adapter skipped ${skippedCount} file${skippedCount === 1 ? "" : "s"} in ` +
|
|
812
|
+
`${skippedDirs.map((dir) => `${dir}/`).join(", ")} — set components.<name>.adapter to "akm" to index them`);
|
|
813
|
+
}
|
|
753
814
|
function buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMissingDirs) {
|
|
754
815
|
const componentBySource = buildComponentBySource(allSourceEntries);
|
|
755
816
|
const handoffDirs = new Set();
|
|
@@ -775,6 +836,8 @@ function buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMiss
|
|
|
775
836
|
});
|
|
776
837
|
const dirGroups = groupFileContextsByDir(walked.files);
|
|
777
838
|
const adapter = adapterForId(component.adapter);
|
|
839
|
+
if (adapter)
|
|
840
|
+
warnIfAdapterSkipsAkmContent(component, walked.files, adapter);
|
|
778
841
|
return {
|
|
779
842
|
currentStashDir,
|
|
780
843
|
component,
|