@getmikie/cli 1.1.85 → 1.1.87

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,12 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { createRequire } from "node:module";
4
+ import { homedir } from "node:os";
5
+ import { access } from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
4
8
 
5
9
  import {
10
+ launchNativeCaptured,
6
11
  launchNative,
7
12
  resolveNativeBinary,
13
+ selectPlatformPackage,
8
14
  translateArguments,
9
15
  } from "../lib/launcher.mjs";
16
+ import {
17
+ UpdateTopology,
18
+ createCommandAdapter,
19
+ createFilesystemAdapter,
20
+ createRegistryAdapter,
21
+ detectUpdateTopology,
22
+ externalRecoveryInvocation,
23
+ parseUpdateHandoff,
24
+ runUpdateTransaction,
25
+ } from "../lib/update.mjs";
10
26
 
11
27
  const require = createRequire(import.meta.url);
12
28
  const packageMetadata = require("../package.json");
@@ -23,8 +39,148 @@ Usage:
23
39
  mikie login
24
40
  mikie connect .
25
41
  mikie doctor
42
+ mikie update
26
43
 
27
44
  All commands execute the compiled Mikie native engine.`);
45
+ } else if (arguments_.length === 1 && arguments_[0].toLowerCase() === "update") {
46
+ try {
47
+ const commands = createCommandAdapter();
48
+ const npmRoot = await commands.run("npm", ["root", "--global"], {
49
+ shell: false,
50
+ capture: true,
51
+ });
52
+ const npmPrefix = await commands.run("npm", ["prefix", "--global"], {
53
+ shell: false,
54
+ capture: true,
55
+ });
56
+ if (
57
+ npmRoot.code !== 0 ||
58
+ npmRoot.stdout.trim().split(/\r?\n/).length !== 1 ||
59
+ !path.isAbsolute(npmRoot.stdout.trim()) ||
60
+ npmPrefix.code !== 0 ||
61
+ npmPrefix.stdout.trim().split(/\r?\n/).length !== 1 ||
62
+ !path.isAbsolute(npmPrefix.stdout.trim())
63
+ ) {
64
+ throw new Error("global npm installation authority is unavailable");
65
+ }
66
+ const topology = detectUpdateTopology({ globalNodeModules: npmRoot.stdout.trim() });
67
+ const customerHome = homedir();
68
+ const transactionRoot = path.join(customerHome, ".mikie", "npm-update-v1");
69
+ const recoveryJournal = path.join(transactionRoot, "journal.json");
70
+ if (topology === UpdateTopology.NpmGlobal) {
71
+ try {
72
+ await access(recoveryJournal);
73
+ const invocation = await externalRecoveryInvocation(transactionRoot);
74
+ const recovered = await commands.run(invocation.executable, invocation.args, {
75
+ shell: false,
76
+ capture: true,
77
+ });
78
+ if (recovered.code !== 0) {
79
+ throw new Error("pending Mikie update recovery failed");
80
+ }
81
+ } catch (error) {
82
+ if (error?.code !== "ENOENT") throw error;
83
+ }
84
+ }
85
+ const selected = selectPlatformPackage();
86
+ const platformPackageJson = require.resolve(`${selected.packageName}/package.json`);
87
+ const wrapperPackageJson = fileURLToPath(new URL("../package.json", import.meta.url));
88
+ const binary = await resolveNativeBinary();
89
+ const globalPrefix = npmPrefix.stdout.trim();
90
+ const globalShims =
91
+ process.platform === "win32"
92
+ ? ["mikie", "mikie.cmd", "mikie.ps1"].map((name) => path.join(globalPrefix, name))
93
+ : [path.join(globalPrefix, "bin", "mikie")];
94
+ const handoffResult = await launchNativeCaptured(binary, ["update"], { topology });
95
+ if (handoffResult.code !== 0) {
96
+ if (handoffResult.stderr) process.stderr.write(handoffResult.stderr);
97
+ throw new Error(`native update handoff exited with status ${handoffResult.code}`);
98
+ }
99
+ const handoff = parseUpdateHandoff(handoffResult.stdout, topology);
100
+ const registry = createRegistryAdapter({ commands });
101
+ const files = createFilesystemAdapter({
102
+ currentLayout: { wrapperPackageJson, platformPackageJson, binary, shims: globalShims },
103
+ locateCandidate: async ({ stagingPrefix } = {}) => {
104
+ if (stagingPrefix) {
105
+ const nodeModules =
106
+ process.platform === "win32"
107
+ ? path.join(stagingPrefix, "node_modules")
108
+ : path.join(stagingPrefix, "lib", "node_modules");
109
+ const stagedWrapper = path.join(nodeModules, "@getmikie", "cli", "package.json");
110
+ const stagedPlatform = path.join(
111
+ nodeModules,
112
+ ...selected.packageName.split("/"),
113
+ "package.json",
114
+ );
115
+ return {
116
+ wrapperPackageJson: stagedWrapper,
117
+ platformPackageJson: stagedPlatform,
118
+ binary: path.join(
119
+ path.dirname(stagedPlatform),
120
+ ...selected.binaryPath.split("/"),
121
+ ),
122
+ shims:
123
+ process.platform === "win32"
124
+ ? ["mikie", "mikie.cmd", "mikie.ps1"].map((name) =>
125
+ path.join(stagingPrefix, name),
126
+ )
127
+ : [path.join(stagingPrefix, "bin", "mikie")],
128
+ };
129
+ }
130
+ return {
131
+ wrapperPackageJson,
132
+ platformPackageJson: require.resolve(`${selected.packageName}/package.json`),
133
+ binary: await resolveNativeBinary(),
134
+ shims: globalShims,
135
+ };
136
+ },
137
+ providerPaths: {
138
+ registration: path.join(customerHome, ".claude.json"),
139
+ settings: path.join(customerHome, ".claude", "settings.json"),
140
+ ownership: path.join(customerHome, ".claude", "mikie-evidence-hook-ownership.json"),
141
+ },
142
+ transactionRoot,
143
+ settleProviderRepair: async (phase, snapshot) => {
144
+ const settlementBinary = snapshot?.preparedCandidate?.binary;
145
+ const finalLauncher = snapshot?.preparedCandidate?.finalLauncher;
146
+ if (!settlementBinary || !finalLauncher) {
147
+ throw new Error("prepared candidate recovery authority is unavailable");
148
+ }
149
+ const settlement = await commands.run(settlementBinary, handoff.repair_command, {
150
+ shell: false,
151
+ capture: true,
152
+ environment: {
153
+ MIKIE_DISTRIBUTION_MODE: "npm",
154
+ MIKIE_NPM_UPDATE_TOPOLOGY: UpdateTopology.NpmGlobal,
155
+ MIKIE_NPM_UPDATE_REPAIR_PHASE: phase,
156
+ MIKIE_UPDATE_REPAIR_CANDIDATE: settlementBinary,
157
+ MIKIE_UPDATE_REPAIR_LAUNCHER: finalLauncher,
158
+ },
159
+ });
160
+ if (settlement.code !== 0) {
161
+ throw new Error("candidate owned-adapter settlement failed");
162
+ }
163
+ },
164
+ });
165
+ const result = await runUpdateTransaction({
166
+ handoff,
167
+ currentVersion: packageMetadata.version,
168
+ platformPackage: selected.packageName,
169
+ registry,
170
+ commands,
171
+ files,
172
+ });
173
+ if (result.status === "updated") {
174
+ console.log(`Updated Mikie to ${result.version}.`);
175
+ for (const setting of result.foreignSettings) {
176
+ console.log(`Preserved foreign setting: ${setting}`);
177
+ }
178
+ }
179
+ } catch (error) {
180
+ const message = error instanceof Error ? error.message : "Mikie update failed";
181
+ console.error(`mikie: ${message}`);
182
+ process.exitCode = 1;
183
+ }
28
184
  } else {
29
185
  try {
30
186
  const binary = await resolveNativeBinary();
package/lib/launcher.mjs CHANGED
@@ -17,6 +17,8 @@ const SIGNAL_EXIT_CODES = new Map([
17
17
  ["SIGTERM", 143],
18
18
  ]);
19
19
 
20
+ const MAX_CAPTURED_OUTPUT_BYTES = 1024 * 1024;
21
+
20
22
  function currentRuntimeUsesGlibc() {
21
23
  try {
22
24
  const report = process.report?.getReport();
@@ -122,3 +124,89 @@ export function launchNative(
122
124
  });
123
125
  });
124
126
  }
127
+
128
+ export function launchNativeCaptured(
129
+ binary,
130
+ arguments_,
131
+ {
132
+ topology,
133
+ spawnProcess = spawn,
134
+ environment = process.env,
135
+ timeoutMs = 30_000,
136
+ scheduleTimeout = setTimeout,
137
+ cancelTimeout = clearTimeout,
138
+ } = {},
139
+ ) {
140
+ if (!["npm_global", "npm_ephemeral"].includes(topology)) {
141
+ throw new Error("Mikie npm update topology is invalid");
142
+ }
143
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
144
+ throw new Error("Mikie native update timeout is invalid");
145
+ }
146
+ return new Promise((resolve, reject) => {
147
+ let settled = false;
148
+ const stdout = [];
149
+ const stderr = [];
150
+ let capturedBytes = 0;
151
+ const child = spawnProcess(binary, arguments_, {
152
+ shell: false,
153
+ stdio: ["inherit", "pipe", "pipe"],
154
+ windowsHide: true,
155
+ env: {
156
+ ...environment,
157
+ MIKIE_DISTRIBUTION_MODE: "npm",
158
+ MIKIE_NPM_UPDATE_TOPOLOGY: topology,
159
+ },
160
+ });
161
+ const deadline = scheduleTimeout(() => {
162
+ if (settled) return;
163
+ settled = true;
164
+ try {
165
+ child.kill?.("SIGKILL");
166
+ } catch {
167
+ // The child may already have exited.
168
+ }
169
+ reject(new Error("Mikie native update command timed out"));
170
+ }, timeoutMs);
171
+ const finish = (callback) => {
172
+ if (settled) return false;
173
+ settled = true;
174
+ cancelTimeout(deadline);
175
+ callback();
176
+ return true;
177
+ };
178
+ const capture = (target) => (chunk) => {
179
+ if (settled) return;
180
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
181
+ capturedBytes += bytes.length;
182
+ if (capturedBytes > MAX_CAPTURED_OUTPUT_BYTES) {
183
+ finish(() => {
184
+ try {
185
+ child.kill?.("SIGKILL");
186
+ } catch {
187
+ // The child may already have exited.
188
+ }
189
+ reject(new Error("Mikie native update response exceeded the capture limit"));
190
+ });
191
+ return;
192
+ }
193
+ target.push(bytes);
194
+ };
195
+ child.stdout.on("data", capture(stdout));
196
+ child.stderr.on("data", capture(stderr));
197
+ child.once("error", (error) => {
198
+ finish(() =>
199
+ reject(new Error("Mikie native update command could not be started", { cause: error })),
200
+ );
201
+ });
202
+ child.once("close", (code, signal) => {
203
+ finish(() =>
204
+ resolve({
205
+ code: Number.isInteger(code) ? code : (SIGNAL_EXIT_CODES.get(signal) ?? 1),
206
+ stdout: Buffer.concat(stdout).toString("utf8"),
207
+ stderr: Buffer.concat(stderr).toString("utf8"),
208
+ }),
209
+ );
210
+ });
211
+ });
212
+ }