@getmikie/cli 1.1.124 → 1.1.126

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 CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  retireHistoricalWindowsRuntimes,
17
17
  retireHistoricalRuntimes,
18
18
  resolveDependencyPackageJson,
19
+ resolveInstalledPackages,
19
20
  resolveNativeBinary,
20
21
  selectPlatformPackage,
21
22
  shouldCompleteSkippedInstall,
@@ -30,8 +31,10 @@ import {
30
31
  detectUpdateTopology,
31
32
  parseUpdateHandoff,
32
33
  parseTrajectoryReconciliationReceipt,
34
+ pendingUpdateTransaction,
33
35
  recoverPendingNpmUpdate,
34
36
  runUpdateTransaction,
37
+ setAsideUpdateTransaction,
35
38
  } from "../lib/update.mjs";
36
39
  import {
37
40
  completeSkippedInstall,
@@ -40,6 +43,20 @@ import {
40
43
  } from "../scripts/reconcile-install.mjs";
41
44
 
42
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
+ }
43
60
  const packageMetadata = require("../package.json");
44
61
  const arguments_ = process.argv.slice(2);
45
62
  const singleFlag = arguments_.length === 1 ? arguments_[0].toLowerCase() : null;
@@ -95,7 +112,36 @@ All commands execute the compiled Mikie native engine.`);
95
112
  const selected = selectPlatformPackage();
96
113
  const platformPackageJson = require.resolve(`${selected.packageName}/package.json`);
97
114
  const wrapperPackageJson = fileURLToPath(new URL("../package.json", import.meta.url));
98
- const binary = await resolveNativeBinary({ version: packageMetadata.version });
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
+ }
99
145
  const globalPrefix = npmPrefix.stdout.trim();
100
146
  const globalShims =
101
147
  process.platform === "win32"
@@ -105,14 +151,29 @@ All commands execute the compiled Mikie native engine.`);
105
151
  const transactionRoot = path.join(customerHome, ".mikie", "npm-update-v1");
106
152
  const recoveryJournal = path.join(transactionRoot, "journal.json");
107
153
  if (topology === UpdateTopology.NpmGlobal) {
154
+ let pending = true;
108
155
  try {
109
156
  await access(recoveryJournal);
110
- await recoverPendingNpmUpdate(transactionRoot, {
111
- commands,
112
- supersedingInstallation: { version: packageMetadata.version, binary },
113
- });
114
157
  } catch (error) {
115
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
+ }
116
177
  }
117
178
  }
118
179
  const handoffResult = await launchNativeCaptured(binary, ["update"], { topology });
@@ -150,10 +211,13 @@ All commands execute the compiled Mikie native engine.`);
150
211
  : [path.join(stagingPrefix, "bin", "mikie")],
151
212
  };
152
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);
153
217
  return {
154
218
  wrapperPackageJson,
155
- platformPackageJson: require.resolve(`${selected.packageName}/package.json`),
156
- binary: await resolveNativeBinary({ version: packageMetadata.version }),
219
+ platformPackageJson: installed.platformPackageJson,
220
+ binary: installed.binary,
157
221
  shims: globalShims,
158
222
  };
159
223
  },
@@ -246,10 +310,7 @@ All commands execute the compiled Mikie native engine.`);
246
310
  }
247
311
  } catch (error) {
248
312
  const message = error instanceof Error ? error.message : "Mikie update failed";
249
- const guidance = message === "invalid Mikie update recovery authority"
250
- ? "; keep the recovery files and contact support before retrying"
251
- : "";
252
- console.error(`mikie: ${message}${guidance}`);
313
+ console.error(`mikie: ${message}${updateGuidance(message)}`);
253
314
  process.exitCode = 1;
254
315
  }
255
316
  } else {
@@ -276,6 +337,16 @@ All commands execute the compiled Mikie native engine.`);
276
337
  console.error(`mikie: install reconciliation is incomplete (${reason}); the command continues.`);
277
338
  }
278
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
+ }
349
+ }
279
350
  const binary = await prepareNativeLaunch(await resolveNativeBinary(), nativeArguments, {
280
351
  version: packageMetadata.version,
281
352
  });
package/lib/launcher.mjs CHANGED
@@ -809,6 +809,33 @@ export function resolveDependencyPackageJson(wrapperPackageJson, dependencyPacka
809
809
  return wrapperRequire.resolve(`${dependencyPackageName}/package.json`);
810
810
  }
811
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
+
812
839
  export async function resolveNativeBinary({
813
840
  platform = process.platform,
814
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
- const [wrapper, platform, binarySha256] = await Promise.all([
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.124",
3
+ "version": "1.1.126",
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.124",
28
- "@getmikie/linux-x64-gnu": "1.1.124",
29
- "@getmikie/win32-x64": "1.1.124"
27
+ "@getmikie/darwin-arm64": "1.1.126",
28
+ "@getmikie/linux-x64-gnu": "1.1.126",
29
+ "@getmikie/win32-x64": "1.1.126"
30
30
  },
31
31
  "publishConfig": {
32
32
  "access": "public",