akm-cli 0.9.8 → 0.9.9
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 +57 -0
- 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
|
@@ -7,10 +7,11 @@ import fs from "node:fs";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { fetchWithRetry, IS_WINDOWS, ResponseTooLargeError, readBodyWithByteCap, readChunkWithDeadline, } from "../../core/common.js";
|
|
9
9
|
import { ConfigError } from "../../core/errors.js";
|
|
10
|
-
import { upgradeHistoricalStateDatabase } from "../../core/state-db.js";
|
|
11
10
|
import { warn } from "../../core/warn.js";
|
|
12
11
|
import { githubHeaders } from "../../integrations/github.js";
|
|
13
12
|
import { getDirname, mainPath, semverOrder } from "../../runtime.js";
|
|
13
|
+
import { resolveNpmGlobalRoot } from "../../tasks/resolve-akm-bin.js";
|
|
14
|
+
import { runMigrationTool } from "../migration-tool.js";
|
|
14
15
|
const REPO = "itlackey/akm";
|
|
15
16
|
const DEFAULT_PACKAGE_NAME = "akm-cli";
|
|
16
17
|
const NODE_MODULES_SEGMENT = "/node_modules/";
|
|
@@ -91,8 +92,37 @@ export function getInstallSignals() {
|
|
|
91
92
|
bunMain: mainPath,
|
|
92
93
|
importMetaDir: getDirname(import.meta.url),
|
|
93
94
|
hasAkmVersion: typeof AKM_VERSION !== "undefined",
|
|
95
|
+
npmGlobalRoot: resolveNpmGlobalRootForThisProcess(),
|
|
94
96
|
};
|
|
95
97
|
}
|
|
98
|
+
function resolveNpmGlobalRootForThisProcess() {
|
|
99
|
+
const nodePath = process.env.AKM_LAUNCHER_NODE?.trim() || (process.versions.bun ? undefined : process.execPath);
|
|
100
|
+
if (!nodePath)
|
|
101
|
+
return undefined;
|
|
102
|
+
try {
|
|
103
|
+
return resolveNpmGlobalRoot(nodePath, process.env);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function isUnderDirectory(dir, root) {
|
|
110
|
+
const real = (value) => {
|
|
111
|
+
try {
|
|
112
|
+
return fs.realpathSync(value);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return path.resolve(value);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const relative = path.relative(real(root), real(dir));
|
|
119
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
120
|
+
}
|
|
121
|
+
/** The package that depends on this akm: the directory holding the `node_modules` it lives in. */
|
|
122
|
+
function packageLocalRoot(importMetaDir) {
|
|
123
|
+
const index = normalizePathSeparators(importMetaDir).lastIndexOf(NODE_MODULES_SEGMENT);
|
|
124
|
+
return index < 0 ? importMetaDir : importMetaDir.slice(0, index);
|
|
125
|
+
}
|
|
96
126
|
// AKM_VERSION ambient type is declared in globals.d.ts
|
|
97
127
|
export function detectInstallMethod(signals) {
|
|
98
128
|
const s = signals ?? getInstallSignals();
|
|
@@ -104,6 +134,14 @@ export function detectInstallMethod(signals) {
|
|
|
104
134
|
if (PNPM_GLOBAL_INSTALL_PATTERN.test(normalizedImportMetaDir)) {
|
|
105
135
|
return "pnpm";
|
|
106
136
|
}
|
|
137
|
+
// A node_modules install outside the npm global root is a DEPENDENCY of
|
|
138
|
+
// some other package (an image's tools dir, a plugin's node_modules):
|
|
139
|
+
// it moves when that package does, and an `npm install -g` here would
|
|
140
|
+
// "succeed" while the parent kept executing its own copy. Only a proven
|
|
141
|
+
// global root can make that call; without one this stays "npm".
|
|
142
|
+
if (s.npmGlobalRoot && s.importMetaDir && !isUnderDirectory(s.importMetaDir, s.npmGlobalRoot)) {
|
|
143
|
+
return "package-local";
|
|
144
|
+
}
|
|
107
145
|
return "npm";
|
|
108
146
|
}
|
|
109
147
|
// Bun-compiled binaries: mainPath points to a virtual /$bunfs/
|
|
@@ -172,7 +210,23 @@ export async function performUpgrade(check, opts, dependencies) {
|
|
|
172
210
|
const { currentVersion, latestVersion, installMethod } = check;
|
|
173
211
|
const force = opts?.force === true;
|
|
174
212
|
const skipPostUpgrade = opts?.skipPostUpgrade === true;
|
|
175
|
-
|
|
213
|
+
const runTool = dependencies?.runMigrationTool ?? runMigrationTool;
|
|
214
|
+
// Every `akm upgrade` ends by running `akm-migrate apply`, install or no
|
|
215
|
+
// install: the migrator on disk after the install step is the one whose
|
|
216
|
+
// migrations the installed akm needs, and an image that ships akm has
|
|
217
|
+
// nothing to install and nobody to run a migration by hand (#895). The two
|
|
218
|
+
// no-install cases return here.
|
|
219
|
+
if (installMethod === "package-local") {
|
|
220
|
+
const parent = packageLocalRoot(getInstallSignals().importMetaDir ?? "");
|
|
221
|
+
return {
|
|
222
|
+
currentVersion,
|
|
223
|
+
newVersion: latestVersion,
|
|
224
|
+
upgraded: false,
|
|
225
|
+
installMethod,
|
|
226
|
+
message: `akm runs as a dependency of the package at ${parent}; upgrade that package to move akm.`,
|
|
227
|
+
migration: await runMigrationStep(runTool),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
176
230
|
if (!check.updateAvailable && !force) {
|
|
177
231
|
return {
|
|
178
232
|
currentVersion,
|
|
@@ -180,6 +234,7 @@ export async function performUpgrade(check, opts, dependencies) {
|
|
|
180
234
|
upgraded: false,
|
|
181
235
|
installMethod,
|
|
182
236
|
message: `akm v${currentVersion} is already the latest version`,
|
|
237
|
+
migration: await runMigrationStep(runTool),
|
|
183
238
|
};
|
|
184
239
|
}
|
|
185
240
|
const packageManagerCommand = getPackageManagerUpgradeCommand(installMethod);
|
|
@@ -190,7 +245,7 @@ export async function performUpgrade(check, opts, dependencies) {
|
|
|
190
245
|
latestVersion,
|
|
191
246
|
installMethod,
|
|
192
247
|
skipPostUpgrade,
|
|
193
|
-
|
|
248
|
+
runTool,
|
|
194
249
|
});
|
|
195
250
|
}
|
|
196
251
|
if (installMethod === "unknown") {
|
|
@@ -200,6 +255,7 @@ export async function performUpgrade(check, opts, dependencies) {
|
|
|
200
255
|
upgraded: false,
|
|
201
256
|
installMethod,
|
|
202
257
|
message: `Unable to detect install method. Upgrade manually from https://github.com/${REPO}/releases`,
|
|
258
|
+
migration: await runMigrationStep(runTool),
|
|
203
259
|
};
|
|
204
260
|
}
|
|
205
261
|
// Binary install
|
|
@@ -309,6 +365,8 @@ export async function performUpgrade(check, opts, dependencies) {
|
|
|
309
365
|
}
|
|
310
366
|
// The replacement completed; the temporary rollback copy is no longer needed.
|
|
311
367
|
removeFileBestEffort(backupPath);
|
|
368
|
+
// The new binary is at execPath now, so this re-execs the NEW migrator.
|
|
369
|
+
const migration = await runMigrationStep(runTool);
|
|
312
370
|
return {
|
|
313
371
|
currentVersion,
|
|
314
372
|
newVersion: latestVersion,
|
|
@@ -316,32 +374,46 @@ export async function performUpgrade(check, opts, dependencies) {
|
|
|
316
374
|
installMethod,
|
|
317
375
|
binaryPath: execPath,
|
|
318
376
|
checksumVerified,
|
|
319
|
-
|
|
377
|
+
migration,
|
|
378
|
+
postUpgrade: runPostUpgradeTasks(execPath, { skip: skipPostUpgrade }),
|
|
320
379
|
};
|
|
321
380
|
}
|
|
322
381
|
/**
|
|
323
|
-
*
|
|
382
|
+
* `akm-migrate apply`, spawned so it is whichever migrator is on disk NOW:
|
|
383
|
+
* after a successful install, the new one. Its JSON plan becomes the
|
|
384
|
+
* response's `migration`. A migrator that could not run or print a plan
|
|
385
|
+
* reports `failed` with its error text instead of throwing, so the install
|
|
386
|
+
* outcome the caller is about to report is never lost behind it.
|
|
324
387
|
*/
|
|
325
|
-
function
|
|
326
|
-
let
|
|
388
|
+
async function runMigrationStep(runTool) {
|
|
389
|
+
let result;
|
|
327
390
|
try {
|
|
328
|
-
|
|
391
|
+
result = await runTool(["apply"]);
|
|
329
392
|
}
|
|
330
393
|
catch (error) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
394
|
+
return { status: "failed", error: error instanceof Error ? error.message : String(error) };
|
|
395
|
+
}
|
|
396
|
+
const line = result.stdout.trim();
|
|
397
|
+
try {
|
|
398
|
+
const plan = JSON.parse(line);
|
|
399
|
+
if (plan.status === "current" || plan.status === "ready" || plan.status === "blocked") {
|
|
400
|
+
return plan;
|
|
401
|
+
}
|
|
338
402
|
}
|
|
339
|
-
|
|
403
|
+
catch {
|
|
404
|
+
// Not a plan; reported below with whatever the migrator did say.
|
|
405
|
+
}
|
|
406
|
+
return { status: "failed", error: result.stderr.trim() || line || `akm-migrate exited ${result.status}` };
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Rebuild the derived index after a successful upgrade.
|
|
410
|
+
*/
|
|
411
|
+
function runPostUpgradeTasks(akmBin, opts) {
|
|
340
412
|
if (opts.skip) {
|
|
341
413
|
return {
|
|
342
414
|
ok: true,
|
|
343
415
|
skipped: true,
|
|
344
|
-
message:
|
|
416
|
+
message: "Upgrade completed. Skipped the index rebuild. Run `akm index` manually to rebuild the index.",
|
|
345
417
|
};
|
|
346
418
|
}
|
|
347
419
|
try {
|
|
@@ -354,7 +426,7 @@ function runPostUpgradeTasks(akmBin, opts, upgradeState) {
|
|
|
354
426
|
return {
|
|
355
427
|
ok: false,
|
|
356
428
|
skipped: false,
|
|
357
|
-
message: `Upgrade completed
|
|
429
|
+
message: `Upgrade completed. The index rebuild could not start: ${result.error.message}. Run \`akm index\` manually.`,
|
|
358
430
|
};
|
|
359
431
|
}
|
|
360
432
|
if (result.status !== 0) {
|
|
@@ -363,14 +435,14 @@ function runPostUpgradeTasks(akmBin, opts, upgradeState) {
|
|
|
363
435
|
ok: false,
|
|
364
436
|
skipped: false,
|
|
365
437
|
exitCode: result.status,
|
|
366
|
-
message: `Upgrade completed
|
|
438
|
+
message: `Upgrade completed. Post-upgrade \`akm index\` failed (${detail}). Run \`akm index\` manually.`,
|
|
367
439
|
};
|
|
368
440
|
}
|
|
369
441
|
return {
|
|
370
442
|
ok: true,
|
|
371
443
|
skipped: false,
|
|
372
444
|
exitCode: 0,
|
|
373
|
-
message:
|
|
445
|
+
message: "Upgrade completed and the index was rebuilt against the new binary.",
|
|
374
446
|
};
|
|
375
447
|
}
|
|
376
448
|
catch (err) {
|
|
@@ -378,7 +450,7 @@ function runPostUpgradeTasks(akmBin, opts, upgradeState) {
|
|
|
378
450
|
return {
|
|
379
451
|
ok: false,
|
|
380
452
|
skipped: false,
|
|
381
|
-
message: `Upgrade completed
|
|
453
|
+
message: `Upgrade completed. The index rebuild failed: ${detail}. Run \`akm index\` manually.`,
|
|
382
454
|
};
|
|
383
455
|
}
|
|
384
456
|
}
|
|
@@ -387,8 +459,8 @@ function runPostUpgradeTasks(akmBin, opts, upgradeState) {
|
|
|
387
459
|
* version verification → post-upgrade tasks.
|
|
388
460
|
* Extracted whole so performUpgrade stays under its fn-size baseline.
|
|
389
461
|
*/
|
|
390
|
-
function runPackageManagerUpgrade(input) {
|
|
391
|
-
const { packageManagerCommand, currentVersion, latestVersion, installMethod, skipPostUpgrade,
|
|
462
|
+
async function runPackageManagerUpgrade(input) {
|
|
463
|
+
const { packageManagerCommand, currentVersion, latestVersion, installMethod, skipPostUpgrade, runTool } = input;
|
|
392
464
|
if (!latestVersion) {
|
|
393
465
|
throw new Error("Unable to determine latest version from GitHub releases. Check https://github.com/itlackey/akm/releases");
|
|
394
466
|
}
|
|
@@ -402,7 +474,12 @@ function runPackageManagerUpgrade(input) {
|
|
|
402
474
|
}
|
|
403
475
|
if (result.status !== 0) {
|
|
404
476
|
const details = (result.stderr ?? "").trim() || (result.stdout ?? "").trim() || `exit code ${result.status}`;
|
|
405
|
-
|
|
477
|
+
// The install could not change what runs, so the migrator on disk is
|
|
478
|
+
// still the right one: run it, then say so, or an operator reading the
|
|
479
|
+
// EACCES will assume the migration is stuck behind it (#895).
|
|
480
|
+
const migration = await runMigrationStep(runTool);
|
|
481
|
+
throw new Error(`Failed to upgrade akm via ${installMethod}: ${details}\nRun manually: ${packageManagerCommand.displayCommand}\n` +
|
|
482
|
+
`Pending migrations ran anyway (status: ${migration.status}).`);
|
|
406
483
|
}
|
|
407
484
|
// The package manager exiting 0 does not prove it delivered
|
|
408
485
|
// `latestVersion`: a lagging `@latest` dist-tag (partial publish,
|
|
@@ -420,6 +497,7 @@ function runPackageManagerUpgrade(input) {
|
|
|
420
497
|
`v${installedVersion} (expected v${latestVersion}). The ${installMethod} registry's @latest tag ` +
|
|
421
498
|
`may be lagging the GitHub release — try again shortly, or install the exact version: ` +
|
|
422
499
|
`${packageManagerCommand.displayCommand.replace(/@latest\b/, `@${latestVersion}`)}`,
|
|
500
|
+
migration: await runMigrationStep(runTool),
|
|
423
501
|
};
|
|
424
502
|
}
|
|
425
503
|
return {
|
|
@@ -430,7 +508,8 @@ function runPackageManagerUpgrade(input) {
|
|
|
430
508
|
message: installedVersion === latestVersion
|
|
431
509
|
? `akm upgraded via ${installMethod} (verified: akm --version reports v${installedVersion})`
|
|
432
510
|
: `akm upgraded via ${installMethod} (installed version could not be verified)`,
|
|
433
|
-
|
|
511
|
+
migration: await runMigrationStep(runTool),
|
|
512
|
+
postUpgrade: runPostUpgradeTasks("akm", { skip: skipPostUpgrade }),
|
|
434
513
|
};
|
|
435
514
|
}
|
|
436
515
|
/**
|
|
@@ -520,40 +599,3 @@ export function getPackageManagerUpgradeCommand(installMethod, packageName = get
|
|
|
520
599
|
}
|
|
521
600
|
return undefined;
|
|
522
601
|
}
|
|
523
|
-
/**
|
|
524
|
-
* Apply pending historical destructive state.db migrations WITHOUT installing a
|
|
525
|
-
* new akm — the body of `akm upgrade --state-only`.
|
|
526
|
-
*
|
|
527
|
-
* Migrations flagged `historical-destructive` are refused during an ordinary
|
|
528
|
-
* managed open: they need a verified sibling safety copy taken under the
|
|
529
|
-
* migration writer lock, and that is deliberate, so an unattended `akm index`
|
|
530
|
-
* can never quietly drop operator state.
|
|
531
|
-
*
|
|
532
|
-
* The bug this fixes is not the guard but its reachability (#895). The only
|
|
533
|
-
* code path that set `allowHistoricalDestructiveStateUpgrade` ran as a
|
|
534
|
-
* POST-INSTALL step of a real upgrade, so it sat behind an npm install. Where
|
|
535
|
-
* akm is installed globally by an image and the runtime user is unprivileged,
|
|
536
|
-
* that install fails EACCES and throws long before the migration is reached —
|
|
537
|
-
* leaving the documented remedy impossible to run and `akm index --full`
|
|
538
|
-
* permanently blocked. Nothing about the migration itself needs the network,
|
|
539
|
-
* root, or a new binary; it is local, offline, and already verified.
|
|
540
|
-
*
|
|
541
|
-
* The safety copy is NOT skipped here. This changes only who may ask for the
|
|
542
|
-
* migration, never what it does.
|
|
543
|
-
*/
|
|
544
|
-
export function upgradeStateOnly(currentVersion, dependencies) {
|
|
545
|
-
const upgradeState = dependencies?.upgradeHistoricalStateDatabase ?? upgradeHistoricalStateDatabase;
|
|
546
|
-
const result = upgradeState();
|
|
547
|
-
return {
|
|
548
|
-
currentVersion,
|
|
549
|
-
newVersion: currentVersion,
|
|
550
|
-
upgraded: false,
|
|
551
|
-
installMethod: detectInstallMethod(),
|
|
552
|
-
message: result.upgraded
|
|
553
|
-
? `Applied pending state.db migrations. Safety copy: ${result.safetyCopyPath}`
|
|
554
|
-
: "state.db is already current; no migration was needed",
|
|
555
|
-
stateUpgrade: result.safetyCopyPath
|
|
556
|
-
? { applied: result.upgraded, safetyCopyPath: result.safetyCopyPath }
|
|
557
|
-
: { applied: result.upgraded },
|
|
558
|
-
};
|
|
559
|
-
}
|
|
@@ -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.
|