@getmikie/cli 1.1.135 → 1.1.137

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
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { spawnSync } from "node:child_process";
3
4
  import { createRequire } from "node:module";
4
5
  import { homedir } from "node:os";
5
6
  import { access, mkdir, writeFile } from "node:fs/promises";
@@ -37,6 +38,7 @@ import {
37
38
  setAsideUpdateTransaction,
38
39
  } from "../lib/update.mjs";
39
40
  import {
41
+ clientReconciliationLine,
40
42
  completeSkippedInstall,
41
43
  installedClientRuntimeDigests,
42
44
  reconcileInstalledClients,
@@ -335,6 +337,18 @@ Account:
335
337
  } else if (result.status === "reconciled") {
336
338
  console.log(`Mikie is up to date (${result.version})`);
337
339
  }
340
+ if (result.status === "updated" || result.status === "reconciled") {
341
+ // Bring the clients already set up to the installed runtime now,
342
+ // through the installed package's own reconciliation (the files on
343
+ // disk are the new version's, unlike the modules this process loaded).
344
+ // Without it Codex and Claude Code stayed on the old runtime until
345
+ // some later command happened to finish the install.
346
+ spawnSync(
347
+ process.execPath,
348
+ [fileURLToPath(new URL("../scripts/reconcile-install.mjs", import.meta.url))],
349
+ { stdio: "inherit", windowsHide: true },
350
+ );
351
+ }
338
352
  } catch (error) {
339
353
  const message = error instanceof Error ? error.message : "Mikie update failed";
340
354
  for (const line of updateFailureLines(message, error?.recovery)) console.error(line);
@@ -358,6 +372,8 @@ Account:
358
372
  const clients = await completeSkippedInstall({ version: packageMetadata.version });
359
373
  if (clients.status === "deferred") {
360
374
  await reportDeferredOnce(packageMetadata.version);
375
+ } else if (clients.status === "reconciled_by_setup") {
376
+ console.error(clientReconciliationLine(clients));
361
377
  }
362
378
  } catch (error) {
363
379
  const reason = error instanceof Error ? error.message : "unavailable";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmikie/cli",
3
- "version": "1.1.135",
3
+ "version": "1.1.137",
4
4
  "description": "Native Mikie relay launcher for MCP clients",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -21,13 +21,13 @@
21
21
  "node": ">=22"
22
22
  },
23
23
  "optionalDependencies": {
24
- "@getmikie/darwin-arm64": "1.1.135",
25
- "@getmikie/linux-x64-gnu": "1.1.135",
26
- "@getmikie/win32-x64": "1.1.135"
24
+ "@getmikie/darwin-arm64": "1.1.137",
25
+ "@getmikie/linux-x64-gnu": "1.1.137",
26
+ "@getmikie/win32-x64": "1.1.137"
27
27
  },
28
28
  "publishConfig": {
29
29
  "access": "public",
30
30
  "registry": "https://registry.npmjs.org"
31
31
  },
32
- "mikieReleaseNote": "Mikie finds your real source in large repos, mikie usage is much faster, and updates no longer stall after an interrupted Claude Code setup."
32
+ "mikieReleaseNote": "Codex sessions now always use the project they were opened in, so Mikie returns the right code instead of an empty answer."
33
33
  }
@@ -1,7 +1,9 @@
1
1
  import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
+ import { spawn } from "node:child_process";
4
5
  import { createReadStream } from "node:fs";
6
+ import { homedir } from "node:os";
5
7
  import { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
6
8
 
7
9
  import {
@@ -449,6 +451,84 @@ export async function installedClientRuntimeDigests({
449
451
  return retainedRuntimeDigests(defaultClientMarkerRoot(environment, platform), platform);
450
452
  }
451
453
 
454
+ const SETUP_TIMEOUT_MS = 180_000;
455
+
456
+ /**
457
+ * The clients this person already set up Mikie in: Codex when its config
458
+ * has a `[mcp_servers.mikie]` table, Claude Code when `~/.claude.json` has a
459
+ * `mikie` MCP server. Clients never set up are never touched.
460
+ */
461
+ export async function configuredClients({ environment = process.env } = {}) {
462
+ const home = environment.USERPROFILE || environment.HOME || homedir();
463
+ const clients = [];
464
+ try {
465
+ const codexHome = environment.CODEX_HOME || path.join(home, ".codex");
466
+ const codex = await readFile(path.join(codexHome, "config.toml"), "utf8");
467
+ if (/^\[mcp_servers\.mikie\]\s*$/m.test(codex)) clients.push("codex");
468
+ } catch {
469
+ // No Codex config: Codex was never set up.
470
+ }
471
+ try {
472
+ const claude = JSON.parse(await readFile(path.join(home, ".claude.json"), "utf8"));
473
+ if (claude?.mcpServers?.mikie) clients.push("claude");
474
+ } catch {
475
+ // No Claude config: Claude Code was never set up.
476
+ }
477
+ return clients;
478
+ }
479
+
480
+ /**
481
+ * Run `<launcher> setup <client>`, answering its confirmation with yes, the
482
+ * way the `mikie` command starts the runtime (as an npm install).
483
+ */
484
+ export function runAnsweredSetup(
485
+ launcher,
486
+ client,
487
+ { spawnProcess = spawn, environment = process.env } = {},
488
+ ) {
489
+ return new Promise((resolve) => {
490
+ let settled = false;
491
+ const finish = (ok) => {
492
+ if (!settled) {
493
+ settled = true;
494
+ clearTimeout(deadline);
495
+ resolve(ok);
496
+ }
497
+ };
498
+ const child = spawnProcess(launcher, ["setup", client], {
499
+ shell: false,
500
+ stdio: ["pipe", "ignore", "ignore"],
501
+ windowsHide: true,
502
+ env: { ...environment, MIKIE_DISTRIBUTION_MODE: "npm" },
503
+ });
504
+ const deadline = setTimeout(() => {
505
+ child.kill?.();
506
+ finish(false);
507
+ }, SETUP_TIMEOUT_MS);
508
+ child.on("error", () => finish(false));
509
+ child.on("close", (code) => finish(code === 0));
510
+ child.stdin?.end("y\n");
511
+ });
512
+ }
513
+
514
+ /**
515
+ * Bring every already-configured client to `launcher` through the same
516
+ * setup a person runs. The update's in-place repair is strict and refuses
517
+ * on states it cannot prove; left alone that kept clients on an old
518
+ * runtime release after release while the update reported success.
519
+ */
520
+ export async function setupConfiguredClients({
521
+ launcher,
522
+ environment = process.env,
523
+ runSetup = runAnsweredSetup,
524
+ } = {}) {
525
+ const results = [];
526
+ for (const client of await configuredClients({ environment })) {
527
+ results.push({ client, ok: await runSetup(launcher, client, { environment }) });
528
+ }
529
+ return results;
530
+ }
531
+
452
532
  /** Reconcile exact-owned client launchers once for this installed package. */
453
533
  export async function reconcileInstalledClients({
454
534
  environment = process.env,
@@ -464,6 +544,7 @@ export async function reconcileInstalledClients({
464
544
  }),
465
545
  commands = createCommandAdapter({ environment, platform }),
466
546
  allowDeferred = false,
547
+ setupClients = setupConfiguredClients,
467
548
  } = {}) {
468
549
  if (isTransactionStagedInstall(environment, platform)) {
469
550
  return { status: "staged_candidate" };
@@ -572,6 +653,15 @@ export async function reconcileInstalledClients({
572
653
  parseClientSettlement(committed, settlementPhase);
573
654
  if (preparedReceipt.action === "refused") {
574
655
  if (allowDeferred) {
656
+ const setup = await setupClients({ launcher: finalLauncher, environment });
657
+ if (setup.length > 0 && setup.every((result) => result.ok)) {
658
+ await writeClientMarker(scopedMarker, authority);
659
+ return {
660
+ status: "reconciled_by_setup",
661
+ clients: setup.map((result) => result.client),
662
+ finalLauncher,
663
+ };
664
+ }
575
665
  return { status: "deferred", providerState: preparedReceipt.before };
576
666
  }
577
667
  throw new Error(
@@ -605,12 +695,24 @@ export async function completeSkippedInstall({
605
695
  reconcileTrajectory = reconcileInstalledTrajectory,
606
696
  } = {}) {
607
697
  const clients = await reconcileClients({ version, allowDeferred: true });
608
- if (clients.status === "reconciled") {
698
+ if (clients.status === "reconciled" || clients.status === "reconciled_by_setup") {
609
699
  await reconcileTrajectory();
610
700
  }
611
701
  return clients;
612
702
  }
613
703
 
704
+ /** One line for the person: what an install did to their clients' setup. */
705
+ export function clientReconciliationLine(clients) {
706
+ if (clients?.status === "reconciled_by_setup") {
707
+ const names = clients.clients.map((client) => (client === "claude" ? "Claude Code" : "Codex"));
708
+ return `Mikie updated its setup in ${names.join(" and ")}. Restart ${names.length > 1 ? "them" : "it"} to use the new version.`;
709
+ }
710
+ if (clients?.status === "deferred") {
711
+ return "Mikie could not update its Codex and Claude Code setup. Run `mikie setup codex` or `mikie setup claude`.";
712
+ }
713
+ return null;
714
+ }
715
+
614
716
  const scriptIdentity = pathIdentity(fileURLToPath(import.meta.url), process.platform);
615
717
  const invokedIdentity = pathIdentity(process.argv[1], process.platform);
616
718
  if (scriptIdentity !== null && scriptIdentity === invokedIdentity) {
@@ -623,9 +725,8 @@ if (scriptIdentity !== null && scriptIdentity === invokedIdentity) {
623
725
  version: packageMetadata.version,
624
726
  allowDeferred: true,
625
727
  });
626
- if (clients.status === "deferred") {
627
- console.warn("Mikie will finish updating its Codex and Claude Code setup next time. If Mikie stops working there, run `mikie setup codex` or `mikie setup claude`.");
628
- }
728
+ const line = clientReconciliationLine(clients);
729
+ if (line) console.warn(line);
629
730
  } catch (error) {
630
731
  console.error(`Mikie install reconciliation failed: ${error.message}`);
631
732
  process.exitCode = 1;