@getmikie/cli 1.1.123 → 1.1.125
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/bin/mikie.mjs +96 -11
- package/lib/launcher.mjs +48 -0
- package/lib/update.mjs +87 -10
- package/package.json +4 -4
- package/scripts/reconcile-install.mjs +17 -0
package/bin/mikie.mjs
CHANGED
|
@@ -16,8 +16,10 @@ import {
|
|
|
16
16
|
retireHistoricalWindowsRuntimes,
|
|
17
17
|
retireHistoricalRuntimes,
|
|
18
18
|
resolveDependencyPackageJson,
|
|
19
|
+
resolveInstalledPackages,
|
|
19
20
|
resolveNativeBinary,
|
|
20
21
|
selectPlatformPackage,
|
|
22
|
+
shouldCompleteSkippedInstall,
|
|
21
23
|
shouldReconcileInstalledClients,
|
|
22
24
|
translateArguments,
|
|
23
25
|
} from "../lib/launcher.mjs";
|
|
@@ -29,15 +31,32 @@ import {
|
|
|
29
31
|
detectUpdateTopology,
|
|
30
32
|
parseUpdateHandoff,
|
|
31
33
|
parseTrajectoryReconciliationReceipt,
|
|
34
|
+
pendingUpdateTransaction,
|
|
32
35
|
recoverPendingNpmUpdate,
|
|
33
36
|
runUpdateTransaction,
|
|
37
|
+
setAsideUpdateTransaction,
|
|
34
38
|
} from "../lib/update.mjs";
|
|
35
39
|
import {
|
|
40
|
+
completeSkippedInstall,
|
|
36
41
|
installedClientRuntimeDigests,
|
|
37
42
|
reconcileInstalledClients,
|
|
38
43
|
} from "../scripts/reconcile-install.mjs";
|
|
39
44
|
|
|
40
45
|
const require = createRequire(import.meta.url);
|
|
46
|
+
|
|
47
|
+
/** One next step for the update failures a customer can act on. */
|
|
48
|
+
function updateGuidance(message) {
|
|
49
|
+
if (/EBUSY|EPERM|resource busy|operation not permitted|locked/i.test(message)) {
|
|
50
|
+
return "; a running Mikie process holds a file: close Codex and Claude Code, then run `mikie update` again";
|
|
51
|
+
}
|
|
52
|
+
if (/registry did not resolve|registry/i.test(message)) {
|
|
53
|
+
return "; the npm registry could not be reached or returned no compatible version: check the connection and run `mikie update` again";
|
|
54
|
+
}
|
|
55
|
+
if (/another Mikie update transaction is active/.test(message)) {
|
|
56
|
+
return "; wait for the other `mikie update` to finish";
|
|
57
|
+
}
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
41
60
|
const packageMetadata = require("../package.json");
|
|
42
61
|
const arguments_ = process.argv.slice(2);
|
|
43
62
|
const singleFlag = arguments_.length === 1 ? arguments_[0].toLowerCase() : null;
|
|
@@ -93,7 +112,36 @@ All commands execute the compiled Mikie native engine.`);
|
|
|
93
112
|
const selected = selectPlatformPackage();
|
|
94
113
|
const platformPackageJson = require.resolve(`${selected.packageName}/package.json`);
|
|
95
114
|
const wrapperPackageJson = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
96
|
-
|
|
115
|
+
let binary;
|
|
116
|
+
try {
|
|
117
|
+
binary = await resolveNativeBinary({ version: packageMetadata.version });
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// A half-finished install (wrapper and native package at different
|
|
120
|
+
// versions, or a missing native package) cannot run its own updater.
|
|
121
|
+
// Reinstall the whole CLI from npm, the documented recovery.
|
|
122
|
+
const reason = error instanceof Error ? error.message : "the native package is unavailable";
|
|
123
|
+
if (topology !== UpdateTopology.NpmGlobal || !/native package|versions do not match/.test(reason)) {
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
console.error(`mikie: the installed CLI is incomplete (${reason}); reinstalling it from npm.`);
|
|
127
|
+
const registry = createRegistryAdapter({ commands });
|
|
128
|
+
const resolved = await registry.resolveExactCompatibleVersion({
|
|
129
|
+
packageName: "@getmikie/cli",
|
|
130
|
+
currentVersion: packageMetadata.version,
|
|
131
|
+
nodeVersion: process.versions.node,
|
|
132
|
+
});
|
|
133
|
+
const spec = `@getmikie/cli@${resolved.version}`;
|
|
134
|
+
const reinstall = await commands.run(
|
|
135
|
+
"npm",
|
|
136
|
+
["install", "--global", spec, "--allow-scripts=@getmikie/cli"],
|
|
137
|
+
{ shell: false, capture: true },
|
|
138
|
+
);
|
|
139
|
+
if (reinstall.code !== 0) {
|
|
140
|
+
throw new Error(`reinstalling the CLI failed; run: npm install --global ${spec} --allow-scripts=@getmikie/cli`);
|
|
141
|
+
}
|
|
142
|
+
console.log(`Reinstalled Mikie ${resolved.version}. Run \`mikie doctor\` to check client setup.`);
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
97
145
|
const globalPrefix = npmPrefix.stdout.trim();
|
|
98
146
|
const globalShims =
|
|
99
147
|
process.platform === "win32"
|
|
@@ -103,14 +151,29 @@ All commands execute the compiled Mikie native engine.`);
|
|
|
103
151
|
const transactionRoot = path.join(customerHome, ".mikie", "npm-update-v1");
|
|
104
152
|
const recoveryJournal = path.join(transactionRoot, "journal.json");
|
|
105
153
|
if (topology === UpdateTopology.NpmGlobal) {
|
|
154
|
+
let pending = true;
|
|
106
155
|
try {
|
|
107
156
|
await access(recoveryJournal);
|
|
108
|
-
await recoverPendingNpmUpdate(transactionRoot, {
|
|
109
|
-
commands,
|
|
110
|
-
supersedingInstallation: { version: packageMetadata.version, binary },
|
|
111
|
-
});
|
|
112
157
|
} catch (error) {
|
|
113
158
|
if (error?.code !== "ENOENT") throw error;
|
|
159
|
+
pending = false;
|
|
160
|
+
}
|
|
161
|
+
if (pending) {
|
|
162
|
+
try {
|
|
163
|
+
await recoverPendingNpmUpdate(transactionRoot, {
|
|
164
|
+
commands,
|
|
165
|
+
supersedingInstallation: { version: packageMetadata.version, binary },
|
|
166
|
+
});
|
|
167
|
+
} catch (error) {
|
|
168
|
+
// The installed packages were verified above. An earlier transaction
|
|
169
|
+
// that cannot be finished (superseded, tampered or from an older
|
|
170
|
+
// updater) is kept byte for byte but must not block this update.
|
|
171
|
+
const reason = error instanceof Error ? error.message : "recovery failed";
|
|
172
|
+
const aside = await setAsideUpdateTransaction(transactionRoot);
|
|
173
|
+
console.error(
|
|
174
|
+
`mikie: an earlier unfinished update could not be completed (${reason}); its files were kept at ${aside} and this update continues from ${packageMetadata.version}.`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
114
177
|
}
|
|
115
178
|
}
|
|
116
179
|
const handoffResult = await launchNativeCaptured(binary, ["update"], { topology });
|
|
@@ -148,10 +211,13 @@ All commands execute the compiled Mikie native engine.`);
|
|
|
148
211
|
: [path.join(stagingPrefix, "bin", "mikie")],
|
|
149
212
|
};
|
|
150
213
|
}
|
|
214
|
+
// The activated packages on disk: this process is still the
|
|
215
|
+
// previous wrapper, so its own version is not the one to verify.
|
|
216
|
+
const installed = await resolveInstalledPackages(wrapperPackageJson);
|
|
151
217
|
return {
|
|
152
218
|
wrapperPackageJson,
|
|
153
|
-
platformPackageJson:
|
|
154
|
-
binary:
|
|
219
|
+
platformPackageJson: installed.platformPackageJson,
|
|
220
|
+
binary: installed.binary,
|
|
155
221
|
shims: globalShims,
|
|
156
222
|
};
|
|
157
223
|
},
|
|
@@ -244,10 +310,7 @@ All commands execute the compiled Mikie native engine.`);
|
|
|
244
310
|
}
|
|
245
311
|
} catch (error) {
|
|
246
312
|
const message = error instanceof Error ? error.message : "Mikie update failed";
|
|
247
|
-
|
|
248
|
-
? "; keep the recovery files and contact support before retrying"
|
|
249
|
-
: "";
|
|
250
|
-
console.error(`mikie: ${message}${guidance}`);
|
|
313
|
+
console.error(`mikie: ${message}${updateGuidance(message)}`);
|
|
251
314
|
process.exitCode = 1;
|
|
252
315
|
}
|
|
253
316
|
} else {
|
|
@@ -261,6 +324,28 @@ All commands execute the compiled Mikie native engine.`);
|
|
|
261
324
|
if (clients.status === "deferred") {
|
|
262
325
|
console.error("mikie: client repair deferred; relay startup continues. Run `mikie doctor` to inspect client registration.");
|
|
263
326
|
}
|
|
327
|
+
} else if (shouldCompleteSkippedInstall(nativeArguments)) {
|
|
328
|
+
// An invalid install fails here, once, before any client repair.
|
|
329
|
+
await resolveNativeBinary();
|
|
330
|
+
try {
|
|
331
|
+
const clients = await completeSkippedInstall({ version: packageMetadata.version });
|
|
332
|
+
if (clients.status === "deferred") {
|
|
333
|
+
console.error("mikie: client repair deferred; run `mikie doctor` to inspect client registration.");
|
|
334
|
+
}
|
|
335
|
+
} catch (error) {
|
|
336
|
+
const reason = error instanceof Error ? error.message : "unavailable";
|
|
337
|
+
console.error(`mikie: install reconciliation is incomplete (${reason}); the command continues.`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (nativeArguments[0] === "doctor") {
|
|
341
|
+
const pendingUpdate = await pendingUpdateTransaction(
|
|
342
|
+
path.join(homedir(), ".mikie", "npm-update-v1"),
|
|
343
|
+
);
|
|
344
|
+
if (pendingUpdate) {
|
|
345
|
+
console.log(
|
|
346
|
+
`update transaction: pending (${pendingUpdate.phase}); run \`mikie update\` to finish it or set it aside`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
264
349
|
}
|
|
265
350
|
const binary = await prepareNativeLaunch(await resolveNativeBinary(), nativeArguments, {
|
|
266
351
|
version: packageMetadata.version,
|
package/lib/launcher.mjs
CHANGED
|
@@ -519,6 +519,27 @@ export function shouldReconcileInstalledClients(arguments_) {
|
|
|
519
519
|
return ["serve", "run-relay", "run-codex-hook", "run-claude-hook"].includes(arguments_[0]);
|
|
520
520
|
}
|
|
521
521
|
|
|
522
|
+
/**
|
|
523
|
+
* Return whether a customer command first finishes an install whose npm
|
|
524
|
+
* postinstall never ran (npm's allow-scripts policy skips it by default).
|
|
525
|
+
* Only commands a person types: hook and internal entrypoints (for example
|
|
526
|
+
* `efficiency-hook`, run on every prompt) and read-only doctor never do.
|
|
527
|
+
*/
|
|
528
|
+
const INSTALL_COMPLETING_COMMANDS = new Set([
|
|
529
|
+
"setup",
|
|
530
|
+
"login",
|
|
531
|
+
"connect",
|
|
532
|
+
"privacy",
|
|
533
|
+
"mode",
|
|
534
|
+
"savings",
|
|
535
|
+
"activity",
|
|
536
|
+
"control",
|
|
537
|
+
]);
|
|
538
|
+
|
|
539
|
+
export function shouldCompleteSkippedInstall(arguments_) {
|
|
540
|
+
return INSTALL_COMPLETING_COMMANDS.has(arguments_[0]);
|
|
541
|
+
}
|
|
542
|
+
|
|
522
543
|
async function materializeRuntime(
|
|
523
544
|
binary,
|
|
524
545
|
{ version, runtimeRoot, executableName, cleanupHistorical, preserveSha256 },
|
|
@@ -788,6 +809,33 @@ export function resolveDependencyPackageJson(wrapperPackageJson, dependencyPacka
|
|
|
788
809
|
return wrapperRequire.resolve(`${dependencyPackageName}/package.json`);
|
|
789
810
|
}
|
|
790
811
|
|
|
812
|
+
/**
|
|
813
|
+
* The wrapper and native package installed on disk now, checked against each
|
|
814
|
+
* other. After `mikie update` activates a new version, the running process is
|
|
815
|
+
* still the previous wrapper, so its own version must not be the reference.
|
|
816
|
+
*/
|
|
817
|
+
export async function resolveInstalledPackages(
|
|
818
|
+
wrapperPackageJson,
|
|
819
|
+
{ platform = process.platform, arch = process.arch, glibc } = {},
|
|
820
|
+
) {
|
|
821
|
+
const selected = selectPlatformPackage({ platform, arch, glibc });
|
|
822
|
+
let version;
|
|
823
|
+
try {
|
|
824
|
+
version = JSON.parse(await readFile(wrapperPackageJson, "utf8"))?.version;
|
|
825
|
+
} catch {
|
|
826
|
+
throw new Error("Mikie wrapper package metadata is invalid; reinstall the CLI");
|
|
827
|
+
}
|
|
828
|
+
const platformPackageJson = resolveDependencyPackageJson(wrapperPackageJson, selected.packageName);
|
|
829
|
+
const binary = await resolveNativeBinary({
|
|
830
|
+
platform,
|
|
831
|
+
arch,
|
|
832
|
+
glibc,
|
|
833
|
+
version,
|
|
834
|
+
resolvePackage: () => platformPackageJson,
|
|
835
|
+
});
|
|
836
|
+
return { wrapperPackageJson, platformPackageJson, binary, version };
|
|
837
|
+
}
|
|
838
|
+
|
|
791
839
|
export async function resolveNativeBinary({
|
|
792
840
|
platform = process.platform,
|
|
793
841
|
arch = process.arch,
|
package/lib/update.mjs
CHANGED
|
@@ -428,6 +428,55 @@ function validateHandoff(value, topology = value?.topology) {
|
|
|
428
428
|
return value;
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
/**
|
|
432
|
+
* Move an earlier update transaction that cannot be finished out of the way,
|
|
433
|
+
* byte for byte, so it cannot block every later `mikie update`. Refuses while
|
|
434
|
+
* another update process holds the transaction lock. Returns the new path.
|
|
435
|
+
*/
|
|
436
|
+
export async function setAsideUpdateTransaction(
|
|
437
|
+
transactionRoot,
|
|
438
|
+
{ now = new Date(), isProcessAlive = processIsAlive } = {},
|
|
439
|
+
) {
|
|
440
|
+
if (typeof transactionRoot !== "string" || !path.isAbsolute(transactionRoot)) {
|
|
441
|
+
throw new Error("unsafe Mikie update transaction root");
|
|
442
|
+
}
|
|
443
|
+
await assertNoLinkedAncestors(transactionRoot);
|
|
444
|
+
try {
|
|
445
|
+
const lock = parseJsonNoDuplicateKeys(await readFile(`${transactionRoot}.lock`));
|
|
446
|
+
if (isProcessAlive(lock?.process_id)) {
|
|
447
|
+
throw new Error("another Mikie update transaction is active");
|
|
448
|
+
}
|
|
449
|
+
} catch (error) {
|
|
450
|
+
if (error?.message === "another Mikie update transaction is active") throw error;
|
|
451
|
+
}
|
|
452
|
+
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
453
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
454
|
+
const target = `${transactionRoot}.set-aside-${stamp}${attempt ? `-${attempt}` : ""}`;
|
|
455
|
+
try {
|
|
456
|
+
await lstat(target);
|
|
457
|
+
continue;
|
|
458
|
+
} catch (error) {
|
|
459
|
+
if (error?.code !== "ENOENT") throw error;
|
|
460
|
+
}
|
|
461
|
+
await rename(transactionRoot, target);
|
|
462
|
+
return target;
|
|
463
|
+
}
|
|
464
|
+
throw new Error("no free name to set aside the Mikie update transaction");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** The phase of a pending update transaction, or null when there is none. */
|
|
468
|
+
export async function pendingUpdateTransaction(transactionRoot) {
|
|
469
|
+
try {
|
|
470
|
+
const journal = parseJsonNoDuplicateKeys(
|
|
471
|
+
await readFile(path.join(transactionRoot, "journal.json")),
|
|
472
|
+
);
|
|
473
|
+
return { phase: typeof journal?.phase === "string" ? journal.phase : "unknown" };
|
|
474
|
+
} catch (error) {
|
|
475
|
+
if (error?.code === "ENOENT") return null;
|
|
476
|
+
return { phase: "unreadable" };
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
431
480
|
export function parseUpdateHandoff(stdout, expectedTopology) {
|
|
432
481
|
if (typeof stdout !== "string" || stdout.trim().split(/\r?\n/).length !== 1) {
|
|
433
482
|
throw new Error("invalid Mikie update handoff");
|
|
@@ -2487,22 +2536,50 @@ export function createFilesystemAdapter({
|
|
|
2487
2536
|
return true;
|
|
2488
2537
|
};
|
|
2489
2538
|
|
|
2539
|
+
// The recorded layout, and the same wrapper with its platform package
|
|
2540
|
+
// nested under it: a later npm install can nest the platform package and
|
|
2541
|
+
// leave the journal's hoisted copy behind at the older version.
|
|
2542
|
+
const supersedingLayouts = () => {
|
|
2543
|
+
const platformRoot = path.dirname(currentLayout.platformPackageJson);
|
|
2544
|
+
const nestedRoot = path.join(
|
|
2545
|
+
path.dirname(currentLayout.wrapperPackageJson),
|
|
2546
|
+
"node_modules",
|
|
2547
|
+
path.basename(path.dirname(platformRoot)),
|
|
2548
|
+
path.basename(platformRoot),
|
|
2549
|
+
);
|
|
2550
|
+
const nested = {
|
|
2551
|
+
platformPackageJson: path.join(nestedRoot, "package.json"),
|
|
2552
|
+
binary: path.join(nestedRoot, path.relative(platformRoot, currentLayout.binary)),
|
|
2553
|
+
};
|
|
2554
|
+
return sameExecutablePath(nestedRoot, platformRoot) ? [currentLayout] : [currentLayout, nested];
|
|
2555
|
+
};
|
|
2556
|
+
|
|
2490
2557
|
const authenticateSupersedingInstallation = async () => {
|
|
2491
2558
|
if (!supersedingInstallation) return false;
|
|
2559
|
+
let wrapperVersion;
|
|
2492
2560
|
try {
|
|
2493
|
-
|
|
2494
|
-
readFile(currentLayout.wrapperPackageJson, "utf8"),
|
|
2495
|
-
readFile(currentLayout.platformPackageJson, "utf8"),
|
|
2496
|
-
executableDigest(currentLayout.binary),
|
|
2497
|
-
]);
|
|
2498
|
-
return (
|
|
2499
|
-
JSON.parse(wrapper)?.version === supersedingInstallation.version &&
|
|
2500
|
-
JSON.parse(platform)?.version === supersedingInstallation.version &&
|
|
2501
|
-
binarySha256 === supersedingInstallation.binarySha256
|
|
2502
|
-
);
|
|
2561
|
+
wrapperVersion = JSON.parse(await readFile(currentLayout.wrapperPackageJson, "utf8"))?.version;
|
|
2503
2562
|
} catch {
|
|
2504
2563
|
return false;
|
|
2505
2564
|
}
|
|
2565
|
+
if (wrapperVersion !== supersedingInstallation.version) return false;
|
|
2566
|
+
for (const layout of supersedingLayouts()) {
|
|
2567
|
+
try {
|
|
2568
|
+
const [platform, binarySha256] = await Promise.all([
|
|
2569
|
+
readFile(layout.platformPackageJson, "utf8"),
|
|
2570
|
+
executableDigest(layout.binary),
|
|
2571
|
+
]);
|
|
2572
|
+
if (
|
|
2573
|
+
JSON.parse(platform)?.version === supersedingInstallation.version &&
|
|
2574
|
+
binarySha256 === supersedingInstallation.binarySha256
|
|
2575
|
+
) {
|
|
2576
|
+
return true;
|
|
2577
|
+
}
|
|
2578
|
+
} catch {
|
|
2579
|
+
// Try the next layout.
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
return false;
|
|
2506
2583
|
};
|
|
2507
2584
|
|
|
2508
2585
|
const cleanupSupersededCommittedActivation = async (snapshot) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getmikie/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.125",
|
|
4
4
|
"description": "Native Mikie relay launcher for MCP clients",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"node": ">=22"
|
|
25
25
|
},
|
|
26
26
|
"optionalDependencies": {
|
|
27
|
-
"@getmikie/darwin-arm64": "1.1.
|
|
28
|
-
"@getmikie/linux-x64-gnu": "1.1.
|
|
29
|
-
"@getmikie/win32-x64": "1.1.
|
|
27
|
+
"@getmikie/darwin-arm64": "1.1.125",
|
|
28
|
+
"@getmikie/linux-x64-gnu": "1.1.125",
|
|
29
|
+
"@getmikie/win32-x64": "1.1.125"
|
|
30
30
|
},
|
|
31
31
|
"publishConfig": {
|
|
32
32
|
"access": "public",
|
|
@@ -594,6 +594,23 @@ export async function reconcileInstalledClients({
|
|
|
594
594
|
return { status: "reconciled", finalLauncher };
|
|
595
595
|
}
|
|
596
596
|
|
|
597
|
+
/**
|
|
598
|
+
* Finish an install whose postinstall was skipped: the same client and
|
|
599
|
+
* trajectory-store reconciliation, once per installed runtime. A current
|
|
600
|
+
* marker makes this a no-op; client repair never blocks the command.
|
|
601
|
+
*/
|
|
602
|
+
export async function completeSkippedInstall({
|
|
603
|
+
version,
|
|
604
|
+
reconcileClients = reconcileInstalledClients,
|
|
605
|
+
reconcileTrajectory = reconcileInstalledTrajectory,
|
|
606
|
+
} = {}) {
|
|
607
|
+
const clients = await reconcileClients({ version, allowDeferred: true });
|
|
608
|
+
if (clients.status === "reconciled") {
|
|
609
|
+
await reconcileTrajectory();
|
|
610
|
+
}
|
|
611
|
+
return clients;
|
|
612
|
+
}
|
|
613
|
+
|
|
597
614
|
const scriptIdentity = pathIdentity(fileURLToPath(import.meta.url), process.platform);
|
|
598
615
|
const invokedIdentity = pathIdentity(process.argv[1], process.platform);
|
|
599
616
|
if (scriptIdentity !== null && scriptIdentity === invokedIdentity) {
|