@getmikie/cli 1.1.103 → 1.1.104
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 +39 -1
- package/lib/launcher.mjs +460 -23
- package/lib/update.mjs +217 -22
- package/package.json +5 -5
- package/scripts/reconcile-install.mjs +477 -1
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { createReadStream } from "node:fs";
|
|
5
|
+
import { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
6
|
|
|
4
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
materializedRuntimePath,
|
|
9
|
+
prepareNativeLaunch,
|
|
10
|
+
resolveNativeBinary,
|
|
11
|
+
retireHistoricalRuntimes,
|
|
12
|
+
} from "../lib/launcher.mjs";
|
|
5
13
|
import {
|
|
6
14
|
createCommandAdapter,
|
|
7
15
|
parseTrajectoryReconciliationReceipt,
|
|
8
16
|
} from "../lib/update.mjs";
|
|
9
17
|
|
|
10
18
|
const RECONCILIATION_ARGUMENTS = ["reconcile-trajectory-store-v2", "--json"];
|
|
19
|
+
const CLIENT_REPAIR_ARGUMENTS = ["repair-claude-hooks-v1", "--json"];
|
|
20
|
+
const CLIENT_RECONCILIATION_SCHEMA = "mikie.npm-client-reconciliation.v2";
|
|
21
|
+
const CLIENT_REPAIR_SCHEMA = "mikie.claude-hook-repair.v1";
|
|
22
|
+
const CLIENT_SETTLEMENT_SCHEMA = "mikie.claude-hook-repair-settlement.v1";
|
|
11
23
|
const TRANSACTION_TOKEN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
24
|
+
const PUBLIC_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/;
|
|
25
|
+
const CLIENT_MARKER_NAME = /^[0-9a-f]{64}\.json$/;
|
|
26
|
+
const CLIENT_MARKER_TEMP_NAME =
|
|
27
|
+
/^([0-9a-f]{64}\.json)\.([1-9][0-9]{0,9})\.([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.tmp$/;
|
|
28
|
+
const MAX_CLIENT_MARKERS = 256;
|
|
29
|
+
const MAX_CLIENT_MARKER_TEMPS = 32;
|
|
30
|
+
const MAX_RETAINED_CLIENT_RUNTIMES = 8;
|
|
31
|
+
|
|
32
|
+
async function sha256File(filePath) {
|
|
33
|
+
const digest = createHash("sha256");
|
|
34
|
+
for await (const chunk of createReadStream(filePath)) digest.update(chunk);
|
|
35
|
+
return digest.digest("hex");
|
|
36
|
+
}
|
|
12
37
|
|
|
13
38
|
function pathIdentity(value, platform) {
|
|
14
39
|
const pathApi = platform === "win32" ? path.win32 : path.posix;
|
|
@@ -47,11 +72,462 @@ export async function reconcileInstalledTrajectory({
|
|
|
47
72
|
return parseTrajectoryReconciliationReceipt(result.stdout, { allowNotConfigured: true });
|
|
48
73
|
}
|
|
49
74
|
|
|
75
|
+
function hasExactKeys(value, keys) {
|
|
76
|
+
return (
|
|
77
|
+
value !== null &&
|
|
78
|
+
typeof value === "object" &&
|
|
79
|
+
!Array.isArray(value) &&
|
|
80
|
+
Object.keys(value).sort().join(",") === [...keys].sort().join(",")
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseClientRepair(stdout, code) {
|
|
85
|
+
let receipt;
|
|
86
|
+
try {
|
|
87
|
+
receipt = JSON.parse(stdout.trim());
|
|
88
|
+
} catch {
|
|
89
|
+
throw new Error("native client reconciliation returned invalid JSON");
|
|
90
|
+
}
|
|
91
|
+
const exact = hasExactKeys(receipt, [
|
|
92
|
+
"schema_version",
|
|
93
|
+
"before",
|
|
94
|
+
"after",
|
|
95
|
+
"action",
|
|
96
|
+
"restart_required",
|
|
97
|
+
]);
|
|
98
|
+
const current =
|
|
99
|
+
exact &&
|
|
100
|
+
receipt.schema_version === CLIENT_REPAIR_SCHEMA &&
|
|
101
|
+
receipt.before === "current" &&
|
|
102
|
+
receipt.after === "current" &&
|
|
103
|
+
receipt.action === "already_current" &&
|
|
104
|
+
receipt.restart_required === false;
|
|
105
|
+
const migrated =
|
|
106
|
+
exact &&
|
|
107
|
+
receipt.schema_version === CLIENT_REPAIR_SCHEMA &&
|
|
108
|
+
receipt.before === "outdated_owned" &&
|
|
109
|
+
receipt.after === "current" &&
|
|
110
|
+
receipt.action === "migrated" &&
|
|
111
|
+
receipt.restart_required === true;
|
|
112
|
+
const absent =
|
|
113
|
+
exact &&
|
|
114
|
+
receipt.schema_version === CLIENT_REPAIR_SCHEMA &&
|
|
115
|
+
receipt.before === "absent" &&
|
|
116
|
+
receipt.after === "absent" &&
|
|
117
|
+
receipt.action === "not_configured" &&
|
|
118
|
+
receipt.restart_required === false;
|
|
119
|
+
const refused =
|
|
120
|
+
exact &&
|
|
121
|
+
receipt.schema_version === CLIENT_REPAIR_SCHEMA &&
|
|
122
|
+
["foreign_conflict", "unavailable"].includes(receipt.before) &&
|
|
123
|
+
receipt.after === receipt.before &&
|
|
124
|
+
receipt.action === "refused" &&
|
|
125
|
+
receipt.restart_required === false;
|
|
126
|
+
if ((code === 0 && !current && !migrated && !absent) || (code !== 0 && !refused)) {
|
|
127
|
+
throw new Error("native client reconciliation returned an invalid receipt");
|
|
128
|
+
}
|
|
129
|
+
return receipt;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseClientSettlement(stdout, code, phase) {
|
|
133
|
+
let receipt;
|
|
134
|
+
try {
|
|
135
|
+
receipt = JSON.parse(stdout.trim());
|
|
136
|
+
} catch {
|
|
137
|
+
throw new Error("native client reconciliation settlement returned invalid JSON");
|
|
138
|
+
}
|
|
139
|
+
if (
|
|
140
|
+
code !== 0 ||
|
|
141
|
+
!hasExactKeys(receipt, ["schema_version", "phase", "status"]) ||
|
|
142
|
+
receipt.schema_version !== CLIENT_SETTLEMENT_SCHEMA ||
|
|
143
|
+
receipt.phase !== phase ||
|
|
144
|
+
receipt.status !== "settled"
|
|
145
|
+
) {
|
|
146
|
+
throw new Error("native client reconciliation settlement failed");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function reconciliationAuthority({ environment, platform, version, runtimeSha256 }) {
|
|
151
|
+
const profileRoot = environment.USERPROFILE ?? environment.HOME;
|
|
152
|
+
if (pathIdentity(profileRoot, platform) === null) {
|
|
153
|
+
throw new Error("installed client reconciliation profile authority is unavailable");
|
|
154
|
+
}
|
|
155
|
+
const pathApi = platform === "win32" ? path.win32 : path.posix;
|
|
156
|
+
const profileIdentity = pathIdentity(profileRoot, platform);
|
|
157
|
+
const codexIdentity = pathIdentity(
|
|
158
|
+
environment.CODEX_HOME ?? pathApi.join(profileRoot, ".codex"),
|
|
159
|
+
platform,
|
|
160
|
+
);
|
|
161
|
+
if (codexIdentity === null || !/^[0-9a-f]{64}$/.test(runtimeSha256)) {
|
|
162
|
+
throw new Error("installed client reconciliation profile authority is unavailable");
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
schema_version: CLIENT_RECONCILIATION_SCHEMA,
|
|
166
|
+
version,
|
|
167
|
+
platform,
|
|
168
|
+
runtime_sha256: runtimeSha256,
|
|
169
|
+
profile_root: profileIdentity,
|
|
170
|
+
codex_root: codexIdentity,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function defaultClientMarkerRoot(environment, platform) {
|
|
175
|
+
const pathApi = platform === "win32" ? path.win32 : path.posix;
|
|
176
|
+
const root =
|
|
177
|
+
platform === "win32"
|
|
178
|
+
? environment.LOCALAPPDATA && pathApi.join(environment.LOCALAPPDATA, "Mikie")
|
|
179
|
+
: environment.XDG_STATE_HOME
|
|
180
|
+
? pathApi.join(environment.XDG_STATE_HOME, "mikie")
|
|
181
|
+
: environment.HOME && pathApi.join(environment.HOME, ".local", "state", "mikie");
|
|
182
|
+
if (pathIdentity(root, platform) === null) {
|
|
183
|
+
throw new Error("installed client reconciliation state authority is unavailable");
|
|
184
|
+
}
|
|
185
|
+
return pathApi.join(root, "client-reconciliation-v2");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function defaultClientMarker(environment, platform, authority) {
|
|
189
|
+
const pathApi = platform === "win32" ? path.win32 : path.posix;
|
|
190
|
+
return pathApi.join(
|
|
191
|
+
defaultClientMarkerRoot(environment, platform),
|
|
192
|
+
clientMarkerFileName(authority),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function clientMarkerFileName(authority) {
|
|
197
|
+
const scope = JSON.stringify({
|
|
198
|
+
platform: authority.platform,
|
|
199
|
+
profile_root: authority.profile_root,
|
|
200
|
+
codex_root: authority.codex_root,
|
|
201
|
+
});
|
|
202
|
+
return `${createHash("sha256").update(scope).digest("hex")}.json`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function encodeClientMarker(authority) {
|
|
206
|
+
return `${JSON.stringify(authority)}\n`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function parseClientMarker(bytes) {
|
|
210
|
+
let marker;
|
|
211
|
+
try {
|
|
212
|
+
marker = JSON.parse(bytes.toString("utf8"));
|
|
213
|
+
} catch {
|
|
214
|
+
throw new Error("installed client reconciliation marker is invalid");
|
|
215
|
+
}
|
|
216
|
+
if (
|
|
217
|
+
!hasExactKeys(marker, [
|
|
218
|
+
"schema_version",
|
|
219
|
+
"version",
|
|
220
|
+
"platform",
|
|
221
|
+
"runtime_sha256",
|
|
222
|
+
"profile_root",
|
|
223
|
+
"codex_root",
|
|
224
|
+
]) ||
|
|
225
|
+
marker.schema_version !== CLIENT_RECONCILIATION_SCHEMA ||
|
|
226
|
+
!PUBLIC_VERSION.test(marker.version ?? "") ||
|
|
227
|
+
!["win32", "darwin", "linux"].includes(marker.platform) ||
|
|
228
|
+
!/^[0-9a-f]{64}$/.test(marker.runtime_sha256 ?? "") ||
|
|
229
|
+
pathIdentity(marker.profile_root, marker.platform) !== marker.profile_root ||
|
|
230
|
+
pathIdentity(marker.codex_root, marker.platform) !== marker.codex_root ||
|
|
231
|
+
!bytes.equals(Buffer.from(encodeClientMarker(marker)))
|
|
232
|
+
) {
|
|
233
|
+
throw new Error("installed client reconciliation marker is invalid");
|
|
234
|
+
}
|
|
235
|
+
return marker;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function readStableClientMarker(markerPath) {
|
|
239
|
+
const before = await lstat(markerPath);
|
|
240
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > 2048) {
|
|
241
|
+
throw new Error("installed client reconciliation marker is invalid");
|
|
242
|
+
}
|
|
243
|
+
const bytes = await readFile(markerPath);
|
|
244
|
+
const after = await lstat(markerPath);
|
|
245
|
+
if (
|
|
246
|
+
before.dev !== after.dev ||
|
|
247
|
+
before.ino !== after.ino ||
|
|
248
|
+
before.size !== after.size ||
|
|
249
|
+
before.mtimeMs !== after.mtimeMs
|
|
250
|
+
) {
|
|
251
|
+
throw new Error("installed client reconciliation marker changed during inspection");
|
|
252
|
+
}
|
|
253
|
+
return parseClientMarker(bytes);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function readClientMarker(markerPath, authority) {
|
|
257
|
+
try {
|
|
258
|
+
const marker = await readStableClientMarker(markerPath);
|
|
259
|
+
return Object.keys(authority).every((key) => marker[key] === authority[key]);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (error?.code === "ENOENT") return false;
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function writeClientMarker(markerPath, authority) {
|
|
267
|
+
await mkdir(path.dirname(markerPath), { recursive: true });
|
|
268
|
+
const temporary = `${markerPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
269
|
+
const body = encodeClientMarker(authority);
|
|
270
|
+
try {
|
|
271
|
+
await writeFile(temporary, body, { flag: "wx", mode: 0o600 });
|
|
272
|
+
await rename(temporary, markerPath);
|
|
273
|
+
} finally {
|
|
274
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function processIsAlive(pid) {
|
|
279
|
+
if (pid === process.pid) return true;
|
|
280
|
+
try {
|
|
281
|
+
process.kill(pid, 0);
|
|
282
|
+
return true;
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (error?.code === "ESRCH") return false;
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function scavengeCrashedClientMarkerTemp(markerRoot, entry, match, platform) {
|
|
290
|
+
const temporary = path.join(markerRoot, entry.name);
|
|
291
|
+
const before = await lstat(temporary);
|
|
292
|
+
const ownerPrivate =
|
|
293
|
+
platform === "win32" ||
|
|
294
|
+
(((before.mode & 0o077) === 0) &&
|
|
295
|
+
(typeof process.getuid !== "function" || before.uid === process.getuid()));
|
|
296
|
+
if (
|
|
297
|
+
!before.isFile() ||
|
|
298
|
+
before.isSymbolicLink() ||
|
|
299
|
+
before.size > 2048 ||
|
|
300
|
+
!ownerPrivate
|
|
301
|
+
) {
|
|
302
|
+
throw new Error("installed client reconciliation marker inventory is invalid");
|
|
303
|
+
}
|
|
304
|
+
const pid = Number(match[2]);
|
|
305
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || processIsAlive(pid)) {
|
|
306
|
+
throw new Error("installed client reconciliation marker write is in progress");
|
|
307
|
+
}
|
|
308
|
+
const after = await lstat(temporary);
|
|
309
|
+
if (
|
|
310
|
+
before.dev !== after.dev ||
|
|
311
|
+
before.ino !== after.ino ||
|
|
312
|
+
before.size !== after.size ||
|
|
313
|
+
before.mtimeMs !== after.mtimeMs
|
|
314
|
+
) {
|
|
315
|
+
throw new Error("installed client reconciliation marker temp changed during inspection");
|
|
316
|
+
}
|
|
317
|
+
await rm(temporary);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function retainedRuntimeDigests(
|
|
321
|
+
markerRoot,
|
|
322
|
+
platform,
|
|
323
|
+
{ replacementName, replacementSha256 } = {},
|
|
324
|
+
) {
|
|
325
|
+
if (
|
|
326
|
+
(replacementName === undefined) !== (replacementSha256 === undefined) ||
|
|
327
|
+
(replacementName !== undefined &&
|
|
328
|
+
(!CLIENT_MARKER_NAME.test(replacementName) ||
|
|
329
|
+
!/^[0-9a-f]{64}$/.test(replacementSha256)))
|
|
330
|
+
) {
|
|
331
|
+
throw new Error("installed client reconciliation runtime authority is invalid");
|
|
332
|
+
}
|
|
333
|
+
let entries;
|
|
334
|
+
try {
|
|
335
|
+
const metadata = await lstat(markerRoot);
|
|
336
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
337
|
+
throw new Error("installed client reconciliation marker inventory is invalid");
|
|
338
|
+
}
|
|
339
|
+
entries = await readdir(markerRoot, { withFileTypes: true });
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (error?.code === "ENOENT") {
|
|
342
|
+
entries = [];
|
|
343
|
+
} else {
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (entries.length > MAX_CLIENT_MARKERS + MAX_CLIENT_MARKER_TEMPS) {
|
|
348
|
+
throw new Error("installed client reconciliation marker inventory is invalid");
|
|
349
|
+
}
|
|
350
|
+
const markerEntries = [];
|
|
351
|
+
const temporaryEntries = [];
|
|
352
|
+
for (const entry of entries) {
|
|
353
|
+
const temporary = CLIENT_MARKER_TEMP_NAME.exec(entry.name);
|
|
354
|
+
if (entry.isFile() && !entry.isSymbolicLink() && CLIENT_MARKER_NAME.test(entry.name)) {
|
|
355
|
+
markerEntries.push(entry);
|
|
356
|
+
} else if (entry.isFile() && !entry.isSymbolicLink() && temporary !== null) {
|
|
357
|
+
temporaryEntries.push([entry, temporary]);
|
|
358
|
+
} else {
|
|
359
|
+
throw new Error("installed client reconciliation marker inventory is invalid");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (
|
|
363
|
+
markerEntries.length > MAX_CLIENT_MARKERS ||
|
|
364
|
+
temporaryEntries.length > MAX_CLIENT_MARKER_TEMPS
|
|
365
|
+
) {
|
|
366
|
+
throw new Error("installed client reconciliation marker inventory is invalid");
|
|
367
|
+
}
|
|
368
|
+
for (const [entry, match] of temporaryEntries) {
|
|
369
|
+
await scavengeCrashedClientMarkerTemp(markerRoot, entry, match, platform);
|
|
370
|
+
}
|
|
371
|
+
const retained = new Set();
|
|
372
|
+
let replaced = false;
|
|
373
|
+
for (const entry of markerEntries) {
|
|
374
|
+
const marker = await readStableClientMarker(path.join(markerRoot, entry.name));
|
|
375
|
+
if (marker.platform !== platform || clientMarkerFileName(marker) !== entry.name) {
|
|
376
|
+
throw new Error("installed client reconciliation marker inventory is invalid");
|
|
377
|
+
}
|
|
378
|
+
if (entry.name === replacementName) {
|
|
379
|
+
retained.add(replacementSha256);
|
|
380
|
+
replaced = true;
|
|
381
|
+
} else {
|
|
382
|
+
retained.add(marker.runtime_sha256);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (replacementName !== undefined && !replaced) {
|
|
386
|
+
retained.add(replacementSha256);
|
|
387
|
+
}
|
|
388
|
+
if (retained.size > MAX_RETAINED_CLIENT_RUNTIMES) {
|
|
389
|
+
throw new Error("installed client reconciliation runtime authority exceeds its bound");
|
|
390
|
+
}
|
|
391
|
+
return [...retained];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Read the runtime digests still referenced by every authenticated profile marker. */
|
|
395
|
+
export async function installedClientRuntimeDigests({
|
|
396
|
+
environment = process.env,
|
|
397
|
+
platform = process.platform,
|
|
398
|
+
} = {}) {
|
|
399
|
+
return retainedRuntimeDigests(defaultClientMarkerRoot(environment, platform), platform);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Reconcile exact-owned client launchers once for this installed package. */
|
|
403
|
+
export async function reconcileInstalledClients({
|
|
404
|
+
environment = process.env,
|
|
405
|
+
platform = process.platform,
|
|
406
|
+
version,
|
|
407
|
+
markerPath,
|
|
408
|
+
resolveBinary = resolveNativeBinary,
|
|
409
|
+
prepareLauncher = (binary) =>
|
|
410
|
+
prepareNativeLaunch(binary, ["setup", "codex"], { platform, version, environment }),
|
|
411
|
+
retireLauncher = (launcher, runtimeSha256, preserveSha256) =>
|
|
412
|
+
retireHistoricalRuntimes(launcher, {
|
|
413
|
+
expectedSha256: runtimeSha256,
|
|
414
|
+
preserveSha256,
|
|
415
|
+
}),
|
|
416
|
+
commands = createCommandAdapter({ environment, platform }),
|
|
417
|
+
} = {}) {
|
|
418
|
+
if (isTransactionStagedInstall(environment, platform)) {
|
|
419
|
+
return { status: "staged_candidate" };
|
|
420
|
+
}
|
|
421
|
+
if (!PUBLIC_VERSION.test(version ?? "")) {
|
|
422
|
+
throw new Error("installed client reconciliation authority is invalid");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const binary = await resolveBinary();
|
|
426
|
+
const initialRuntimeSha256 = await sha256File(binary);
|
|
427
|
+
const authority = reconciliationAuthority({
|
|
428
|
+
environment,
|
|
429
|
+
platform,
|
|
430
|
+
version,
|
|
431
|
+
runtimeSha256: initialRuntimeSha256,
|
|
432
|
+
});
|
|
433
|
+
const explicitMarker = markerPath !== undefined;
|
|
434
|
+
const scopedMarker = markerPath ?? defaultClientMarker(environment, platform, authority);
|
|
435
|
+
if (!path.isAbsolute(scopedMarker)) {
|
|
436
|
+
throw new Error("installed client reconciliation authority is invalid");
|
|
437
|
+
}
|
|
438
|
+
const markerRoot = path.dirname(scopedMarker);
|
|
439
|
+
if (!explicitMarker) {
|
|
440
|
+
await retainedRuntimeDigests(markerRoot, platform, {
|
|
441
|
+
replacementName: path.basename(scopedMarker),
|
|
442
|
+
replacementSha256: authority.runtime_sha256,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
if (await readClientMarker(scopedMarker, authority)) {
|
|
446
|
+
const expectedLauncher = materializedRuntimePath({
|
|
447
|
+
version,
|
|
448
|
+
sha256: authority.runtime_sha256,
|
|
449
|
+
platform,
|
|
450
|
+
environment,
|
|
451
|
+
});
|
|
452
|
+
let launcher = expectedLauncher;
|
|
453
|
+
try {
|
|
454
|
+
if ((await sha256File(expectedLauncher)) !== authority.runtime_sha256) {
|
|
455
|
+
throw new Error("cached runtime does not match its reconciliation marker");
|
|
456
|
+
}
|
|
457
|
+
} catch (error) {
|
|
458
|
+
if (error?.code !== "ENOENT") throw error;
|
|
459
|
+
if ((await sha256File(binary)) !== initialRuntimeSha256) {
|
|
460
|
+
throw new Error("installed Mikie runtime changed during client reconciliation");
|
|
461
|
+
}
|
|
462
|
+
launcher = await prepareLauncher(binary);
|
|
463
|
+
if (
|
|
464
|
+
pathIdentity(launcher, platform) !== pathIdentity(expectedLauncher, platform) ||
|
|
465
|
+
(await sha256File(binary)) !== initialRuntimeSha256 ||
|
|
466
|
+
(await sha256File(launcher)) !== initialRuntimeSha256
|
|
467
|
+
) {
|
|
468
|
+
throw new Error("materialized Mikie runtime does not match its reconciliation marker");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (!(await readClientMarker(scopedMarker, authority))) {
|
|
472
|
+
throw new Error("installed client reconciliation marker changed during runtime recovery");
|
|
473
|
+
}
|
|
474
|
+
const preserved = explicitMarker
|
|
475
|
+
? [authority.runtime_sha256]
|
|
476
|
+
: await retainedRuntimeDigests(markerRoot, platform);
|
|
477
|
+
await retireLauncher(launcher, authority.runtime_sha256, preserved);
|
|
478
|
+
return { status: "current" };
|
|
479
|
+
}
|
|
480
|
+
const finalLauncher = await prepareLauncher(binary);
|
|
481
|
+
if (![binary, finalLauncher].every((value) => typeof value === "string" && path.isAbsolute(value))) {
|
|
482
|
+
throw new Error("installed client reconciliation authority is invalid");
|
|
483
|
+
}
|
|
484
|
+
const baseEnvironment = {
|
|
485
|
+
MIKIE_DISTRIBUTION_MODE: "npm",
|
|
486
|
+
MIKIE_NPM_UPDATE_TOPOLOGY: "npm_global",
|
|
487
|
+
MIKIE_UPDATE_REPAIR_CANDIDATE: binary,
|
|
488
|
+
MIKIE_UPDATE_REPAIR_LAUNCHER: finalLauncher,
|
|
489
|
+
};
|
|
490
|
+
const prepared = await commands.run(binary, CLIENT_REPAIR_ARGUMENTS, {
|
|
491
|
+
shell: false,
|
|
492
|
+
capture: true,
|
|
493
|
+
environment: { ...baseEnvironment, MIKIE_NPM_UPDATE_REPAIR_PHASE: "prepare" },
|
|
494
|
+
});
|
|
495
|
+
const preparedReceipt = parseClientRepair(prepared.stdout, prepared.code);
|
|
496
|
+
const settlementPhase = preparedReceipt.action === "refused" ? "rollback" : "commit";
|
|
497
|
+
const committed = await commands.run(binary, CLIENT_REPAIR_ARGUMENTS, {
|
|
498
|
+
shell: false,
|
|
499
|
+
capture: true,
|
|
500
|
+
environment: { ...baseEnvironment, MIKIE_NPM_UPDATE_REPAIR_PHASE: settlementPhase },
|
|
501
|
+
});
|
|
502
|
+
parseClientSettlement(committed.stdout, committed.code, settlementPhase);
|
|
503
|
+
if (preparedReceipt.action === "refused") {
|
|
504
|
+
throw new Error(
|
|
505
|
+
"Mikie could not authenticate every configured client; close active clients and run `mikie update` before reinstalling",
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
if ((await sha256File(binary)) !== initialRuntimeSha256) {
|
|
509
|
+
throw new Error("installed Mikie runtime changed during client reconciliation");
|
|
510
|
+
}
|
|
511
|
+
if ((await sha256File(finalLauncher)) !== initialRuntimeSha256) {
|
|
512
|
+
throw new Error("materialized Mikie runtime does not match the installed package");
|
|
513
|
+
}
|
|
514
|
+
await writeClientMarker(scopedMarker, authority);
|
|
515
|
+
const preserved = explicitMarker
|
|
516
|
+
? [authority.runtime_sha256]
|
|
517
|
+
: await retainedRuntimeDigests(markerRoot, platform);
|
|
518
|
+
await retireLauncher(finalLauncher, authority.runtime_sha256, preserved);
|
|
519
|
+
return { status: "reconciled", finalLauncher };
|
|
520
|
+
}
|
|
521
|
+
|
|
50
522
|
const scriptIdentity = pathIdentity(fileURLToPath(import.meta.url), process.platform);
|
|
51
523
|
const invokedIdentity = pathIdentity(process.argv[1], process.platform);
|
|
52
524
|
if (scriptIdentity !== null && scriptIdentity === invokedIdentity) {
|
|
53
525
|
try {
|
|
54
526
|
await reconcileInstalledTrajectory();
|
|
527
|
+
const packageMetadata = JSON.parse(
|
|
528
|
+
await readFile(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"),
|
|
529
|
+
);
|
|
530
|
+
await reconcileInstalledClients({ version: packageMetadata.version });
|
|
55
531
|
} catch (error) {
|
|
56
532
|
console.error(`Mikie install reconciliation failed: ${error.message}`);
|
|
57
533
|
process.exitCode = 1;
|