@mutmutco/hub 4.4.9 → 4.4.11

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.
Files changed (2) hide show
  1. package/dist/index.cjs +949 -701
  2. package/package.json +1 -4
package/dist/index.cjs CHANGED
@@ -21,27 +21,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/index.ts
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
- ACCENT: () => ACCENT,
25
- FACE_PRODUCT: () => FACE_PRODUCT,
26
- MM_GREEN: () => MM_GREEN,
27
- RESET: () => RESET,
28
- accent: () => accent,
29
24
  acquireSharedDocLock: () => acquireSharedDocLock,
30
- attentionRows: () => attentionRows,
31
- autoupdateLine: () => autoupdateLine,
32
- brandLine: () => brandLine,
25
+ assertFaceConformance: () => assertFaceConformance,
33
26
  buildWatcherCommand: () => buildWatcherCommand,
34
27
  checkAtomicity: () => checkAtomicity,
35
- checklistLine: () => checklistLine,
36
28
  claudeArm: () => claudeArm,
37
29
  claudeHostEvidence: () => claudeHostEvidence,
38
30
  clearLaunchTarget: () => clearLaunchTarget,
39
31
  clearPending: () => clearPending,
32
+ createInstallerRun: () => createInstallerRun,
40
33
  enumerateSurfaces: () => enumerateSurfaces,
41
34
  failConsequence: () => failConsequence,
42
35
  fleetStamp: () => fleetStamp,
43
- formatReconcileReport: () => formatReconcileReport,
44
- heldOutput: () => heldOutput,
45
36
  hermesArm: () => hermesArm,
46
37
  hermesHome: () => hermesHome,
47
38
  hermesHomes: () => hermesHomes,
@@ -68,17 +59,15 @@ __export(index_exports, {
68
59
  probeHermesLaunchStaging: () => probeHermesLaunchStaging,
69
60
  probeJervcodeInstallation: () => probeJervcodeInstallation,
70
61
  probeLaunchTargetVersion: () => probeLaunchTargetVersion,
71
- progressiveRow: () => progressiveRow,
72
62
  readLaunchTarget: () => readLaunchTarget,
73
63
  readPending: () => readPending,
74
- receiptText: () => receiptText,
75
64
  reconcile: () => reconcile,
76
65
  reconcileCursorUserHooks: () => reconcileCursorUserHooks,
77
66
  releaseSharedDocLock: () => releaseSharedDocLock,
78
67
  reportCounts: () => reportCounts,
79
- reportFooterLines: () => reportFooterLines,
80
68
  retiredSurfaces: () => retiredSurfaces,
81
69
  retryHint: () => retryHint,
70
+ runMaintenance: () => runMaintenance2,
82
71
  schedulePendingWatcher: () => schedulePendingWatcher,
83
72
  schedulerStatus: () => schedulerStatus,
84
73
  sharedDocLockPath: () => sharedDocLockPath,
@@ -89,40 +78,48 @@ __export(index_exports, {
89
78
  systemdServiceUnit: () => systemdServiceUnit,
90
79
  systemdTimerUnit: () => systemdTimerUnit,
91
80
  taskXml: () => taskXml,
92
- updateHeader: () => updateHeader,
93
- updaterFace: () => updaterFace,
94
81
  watcherLockPath: () => watcherLockPath,
95
82
  withSharedDocLock: () => withSharedDocLock,
96
- wrapWords: () => wrapWords2,
97
83
  writeLaunchTarget: () => writeLaunchTarget,
98
84
  writePending: () => writePending
99
85
  });
100
86
  module.exports = __toCommonJS(index_exports);
101
87
  var import_node_child_process9 = require("node:child_process");
102
- var import_node_fs18 = require("node:fs");
88
+ var import_node_fs22 = require("node:fs");
103
89
  var import_node_path13 = require("node:path");
104
90
 
105
- // node_modules/@mutmutco/installer-face/dist/index.js
91
+ // ../installer/face/src/face.ts
92
+ var import_node_fs = require("node:fs");
93
+
94
+ // ../installer/face/src/products.ts
106
95
  var PRODUCTS = Object.freeze({
107
96
  "mm-strategy": Object.freeze({
108
97
  name: "MM Strategy",
98
+ installWarm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
109
99
  accent: "38;2;249;115;22",
110
- warm: "Welcome. Let's set up MM Strategy \u2014 about a minute."
100
+ warm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
101
+ doctor: "mm-strategy doctor"
111
102
  }),
112
103
  "mmi-hub": Object.freeze({
113
104
  name: "mmi-hub",
105
+ installWarm: "Welcome. Setting up mmi-hub \u2014 about a minute.",
114
106
  accent: "38;2;125;211;252",
115
- warm: "Welcome back. Checking your surfaces\u2026"
107
+ warm: "Welcome back. Checking your surfaces\u2026",
108
+ doctor: "mmi doctor"
116
109
  }),
117
110
  "jerv-hub": Object.freeze({
118
111
  name: "jerv-hub",
112
+ installWarm: "Welcome. Setting up jerv-hub \u2014 about a minute.",
119
113
  accent: "38;2;248;113;113",
120
- warm: "Welcome back. Checking your surfaces\u2026"
114
+ warm: "Welcome back. Checking your surfaces\u2026",
115
+ doctor: "jerv doctor"
121
116
  }),
122
117
  jervcode: Object.freeze({
123
118
  name: "JervCode",
119
+ installWarm: "Welcome. Setting up JervCode \u2014 about a minute.",
124
120
  accent: "38;2;192;132;252",
125
- warm: "Welcome back. Keeping JervCode current\u2026"
121
+ warm: "Welcome back. Keeping JervCode current\u2026",
122
+ doctor: "jervcode doctor"
126
123
  })
127
124
  });
128
125
  function identityFor(product) {
@@ -132,6 +129,8 @@ function identityFor(product) {
132
129
  }
133
130
  return identity;
134
131
  }
132
+
133
+ // ../installer/face/src/face.ts
135
134
  var GLYPH = Object.freeze({
136
135
  diamond: "\u25C6",
137
136
  hollow: "\u25C7",
@@ -156,6 +155,9 @@ var ANSI = /\u001b\[[0-9;]*m/g;
156
155
  function visibleWidth(text) {
157
156
  return [...String(text).replace(ANSI, "")].length;
158
157
  }
158
+ function stripColor(text) {
159
+ return String(text).replace(ANSI, "");
160
+ }
159
161
  function faceWidth(columns) {
160
162
  const raw = Number(columns);
161
163
  return Math.max(40, Math.min(100, Number.isFinite(raw) && raw > 0 ? raw : 100));
@@ -184,35 +186,64 @@ function wrapWords(text, width) {
184
186
  if (line) lines.push(line);
185
187
  return lines.length ? lines : [""];
186
188
  }
187
- function createFace({ product, color = false, columns }) {
189
+ var PROGRESS_PROTOCOL = 1;
190
+ function readProgressFd(env) {
191
+ const fd = Number.parseInt(String(env.MM_PROGRESS_FD ?? ""), 10);
192
+ if (!Number.isInteger(fd) || fd <= 0) return null;
193
+ const protocol = Number.parseInt(String(env.MM_PROGRESS_PROTOCOL ?? PROGRESS_PROTOCOL), 10);
194
+ return protocol === PROGRESS_PROTOCOL ? fd : null;
195
+ }
196
+ function createFace({ product, color = false, columns, env = process.env, operation }) {
188
197
  const identity = identityFor(product);
189
198
  const width = faceWidth(columns);
190
199
  const paint = (sgr, text) => color ? `\x1B[${sgr}m${text}\x1B[0m` : String(text);
191
200
  const bar = () => paint(PALETTE.muted, GLYPH.bar);
192
201
  const indent = " ".repeat(TITLE_COLUMN - 1);
193
- const welcome = () => [
202
+ const continuedPhases = new Set((env.MM_FACE_CONTINUES ?? "").split(",").map((phase) => phase.trim()).filter(Boolean));
203
+ const continuesFace = continuedPhases.size > 0;
204
+ const nested = env.MM_OUTER_CONSOLE === "1";
205
+ const progressFd = readProgressFd(env);
206
+ const emitMilestone = (title, measure, kind) => {
207
+ if (progressFd === null) return false;
208
+ const record = { v: PROGRESS_PROTOCOL, step: stripColor(title), state: kind };
209
+ if (typeof measure === "number") record.ms = Math.max(0, Math.round(measure * 1e3));
210
+ try {
211
+ (0, import_node_fs.writeSync)(progressFd, `${JSON.stringify(record)}
212
+ `);
213
+ return true;
214
+ } catch {
215
+ return false;
216
+ }
217
+ };
218
+ const welcome = () => continuesFace || nested ? [] : [
194
219
  `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
195
220
  bar(),
196
- `${bar()} ${identity.warm}`,
221
+ `${bar()} ${operation === "install" ? identity.installWarm : identity.warm}`,
197
222
  bar()
198
223
  ];
199
- const step = (title, seconds = null, kind = "ok") => {
200
- const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.hollow) : paint(PALETTE.green, GLYPH.check);
201
- const time = seconds === null || seconds === void 0 ? "" : paint(PALETTE.muted, `${Math.max(0, Math.round(seconds))}s`);
224
+ const continues = (phase, kind = "ok") => {
225
+ const inherited = continuedPhases.delete(String(phase).trim());
226
+ return kind === "fail" ? false : inherited;
227
+ };
228
+ const step = (title, measure = null, kind = "ok") => {
229
+ if (emitMilestone(title, measure, kind)) return "";
230
+ const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
231
+ const measured = measure === null || measure === void 0 ? "" : paint(PALETTE.muted, typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : String(measure).trim());
232
+ const time = measured;
202
233
  const column = Math.min(44, Math.max(0, width - 8));
203
- const reserved = time ? 6 : 0;
234
+ const reserved = time ? Math.max(6, visibleWidth(time) + 2) : 0;
204
235
  const [first, ...rest] = wrapWords(title, width - TITLE_COLUMN - reserved);
205
236
  const head = `${bar()} ${glyph} ${first}`;
206
237
  const pad = Math.max(1, column - visibleWidth(head));
207
238
  return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
208
239
  };
209
- const relay = (text) => String(text).split("\n").map((line) => line.trim() === "" ? bar() : `${bar()}${indent}${line}`).join("\n");
210
- const receipt = (lines) => {
240
+ const relay = (text) => String(text).split("\n").flatMap((line) => line.trim() === "" ? [bar()] : (visibleWidth(line) <= width - TITLE_COLUMN ? [line] : wrapWords(line, width - TITLE_COLUMN)).map((part) => `${bar()}${indent}${part}`)).join("\n");
241
+ const receipt = (lines, { ready = true } = {}) => {
242
+ if (ready && nested) return [];
211
243
  const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
212
244
  const line = String(raw);
213
245
  if (visibleWidth(line) <= width - 6) return [line];
214
- const lead = /^\s*/u.exec(line)?.[0] ?? "";
215
- return wrapWords(line, width - 6 - lead.length).map((part) => `${lead}${part}`);
246
+ return wrapWords(line, width - 6);
216
247
  });
217
248
  const content = Math.min(width - 6, Math.max(...body.map(visibleWidth)));
218
249
  const rule = paint(identity.accent, GLYPH.rule.repeat(content + 4));
@@ -224,87 +255,713 @@ function createFace({ product, color = false, columns }) {
224
255
  frame(GLYPH.boxBottom, GLYPH.boxBottomEnd)
225
256
  ];
226
257
  };
227
- const signOff = () => `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
258
+ const outcome = (changed, options = {}) => {
259
+ const fact = String(changed ?? "").trim();
260
+ if (!fact) {
261
+ throw new Error(`installer face: the receipt must say what changed \u2014 pass the one measured fact this run produced, such as "Updated 2 of 7 surfaces to 4.4.10." (${identity.name})`);
262
+ }
263
+ return receipt([
264
+ `${GLYPH.check} ${identity.name} is ready.`,
265
+ fact,
266
+ `Check health any time: ${identity.doctor}`
267
+ ], options);
268
+ };
269
+ const signOff = () => nested ? "" : `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
228
270
  const refusal = (message3) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message3}`;
229
- return { identity, width, welcome, step, relay, receipt, signOff, refusal, paint };
271
+ return { identity, width, nested, emitsProgress: progressFd !== null, welcome, continues, step, relay, receipt, outcome, signOff, refusal, paint };
230
272
  }
273
+
274
+ // ../installer/face/src/shell.ts
231
275
  var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
232
- var SEPARATOR = "\xB7";
233
- var subjectOf = (row) => row.aspect ? `${row.surface} ${row.aspect}` : row.surface;
234
- var groupKey = (row) => `${subjectOf(row)}\0${row.state}\0${row.mark ?? ""}\0${row.note ?? ""}`;
235
- function renderRows(face, rows) {
236
- if (rows.length === 0) return [];
237
- const groups = /* @__PURE__ */ new Map();
238
- for (const row of rows) {
239
- const key = groupKey(row);
240
- const found = groups.get(key);
241
- if (found) {
242
- if (row.instance) found.instances.push(row.instance);
243
- } else {
244
- groups.set(key, { row, instances: row.instance ? [row.instance] : [] });
245
- }
246
- }
247
- const subjects = [...groups.values()].map(({ row, instances }) => instances.length > 1 ? subjectOf(row) : [subjectOf(row), instances[0]].filter(Boolean).join(` ${SEPARATOR} `));
248
- const column = Math.min(Math.max(...subjects.map(visibleWidth)), Math.max(12, face.width - 24));
249
- const lines = [];
250
- for (const [index, { row, instances }] of [...groups.values()].entries()) {
251
- const subject = subjects[index];
252
- const tail2 = instances.length > 1 ? `${row.state} ${SEPARATOR} ${instances.join(", ")}` : row.state;
253
- const body = `${clip(subject, column).padEnd(column)} ${row.mark ? `${markGlyph(face, row.mark)} ` : ""}${tail2}`;
254
- lines.push(...face.relay(body).split("\n"));
255
- if (row.note) {
256
- for (const part of wrapWords(row.note, Math.max(16, face.width - TITLE_COLUMN))) {
257
- lines.push(...face.relay(part).split("\n"));
258
- }
259
- }
276
+
277
+ // ../installer/face/src/spinner.ts
278
+ var import_node_fs2 = require("node:fs");
279
+ var import_node_worker_threads = require("node:worker_threads");
280
+ var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
281
+ var WORKER_SOURCE = `
282
+ const { parentPort, workerData } = require('node:worker_threads');
283
+ const { writeSync, appendFileSync } = require('node:fs');
284
+ const control = new Int32Array(workerData.control);
285
+ let frames = workerData.frames, frame = workerData.frame;
286
+ function draw() {
287
+ if (Atomics.compareExchange(control, 1, 0, 1) !== 0) return;
288
+ try {
289
+ if (Atomics.load(control, 0) || Atomics.load(control, 2) || !frames.length) return;
290
+ const text = frames[frame++ % frames.length];
291
+ writeSync(2, text);
292
+ if (workerData.transcriptPath) appendFileSync(workerData.transcriptPath, JSON.stringify({ channel: 'spinner', text }) + '\\n');
293
+ } finally {
294
+ Atomics.store(control, 1, 0);
295
+ Atomics.notify(control, 1);
260
296
  }
261
- return lines;
262
- }
263
- function markGlyph(face, kind) {
264
- const glyph = kind === "fail" ? GLYPH.cross : kind === "note" ? GLYPH.hollow : GLYPH.check;
265
- return face.paint(kind === "fail" ? face.identity.accent : kind === "note" ? "38;2;150;150;150" : "38;2;31;209;138", glyph);
266
- }
267
- function clip(text, width) {
268
- if (visibleWidth(text) <= width) return text;
269
- return `\u2026${[...text].slice(-(width - 1)).join("")}`;
270
297
  }
271
- var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
272
- function createSpinner(face, { animate, stream = process.stderr, intervalMs = 90 }) {
298
+ parentPort.on('message', (next) => {
299
+ if (Atomics.load(control, 2)) return;
300
+ frames = next.frames;
301
+ frame = next.frame;
302
+ Atomics.store(control, 0, 0);
303
+ });
304
+ setInterval(draw, workerData.intervalMs);
305
+ `;
306
+ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath }) {
307
+ animate = animate && !face.nested && !face.emitsProgress;
273
308
  let timer = null;
274
- let text = "";
309
+ let worker = null;
310
+ let control = null;
311
+ let frames = [];
275
312
  let frame = 0;
276
- const clear = () => {
277
- if (animate) stream.write("\r\x1B[2K");
313
+ const write = (text) => {
314
+ if (stream) stream.write(text);
315
+ else {
316
+ (0, import_node_fs2.writeSync)(2, text);
317
+ if (transcriptPath) (0, import_node_fs2.appendFileSync)(transcriptPath, `${JSON.stringify({ channel: "spinner", text })}
318
+ `);
319
+ }
320
+ };
321
+ const render = (text, measure) => {
322
+ const line = face.step(text, measure, "note").split("\n")[0];
323
+ frames = line ? FRAMES.map((glyph) => `\r\x1B[2K${line.replace(GLYPH.dot, glyph)}`) : [];
278
324
  };
279
325
  const draw = () => {
280
- if (!animate) return;
281
- const line = face.step(text, null, "note").split("\n")[0].replace(GLYPH.hollow, FRAMES[frame % FRAMES.length]);
282
- stream.write(`\r\x1B[2K${line}`);
283
- frame += 1;
326
+ if (animate && frames.length) write(frames[frame++ % frames.length]);
327
+ };
328
+ const pause = () => {
329
+ if (!control) return;
330
+ Atomics.store(control, 0, 1);
331
+ while (Atomics.load(control, 1)) Atomics.wait(control, 1, 1);
332
+ };
333
+ const halt = () => {
334
+ if (timer) clearInterval(timer);
335
+ timer = null;
336
+ if (control) Atomics.store(control, 2, 1);
337
+ pause();
338
+ if (worker) void worker.terminate();
339
+ worker = null;
340
+ control = null;
284
341
  };
285
342
  return {
286
- start(next) {
287
- text = next;
343
+ start(text, measure = null) {
344
+ if (!animate) return;
345
+ halt();
346
+ render(text, measure);
288
347
  frame = 0;
289
348
  draw();
290
- if (animate && !timer) timer = setInterval(draw, intervalMs).unref();
349
+ if (!frames.length) return;
350
+ if (stream) timer = setInterval(draw, intervalMs).unref();
351
+ else {
352
+ control = new Int32Array(new SharedArrayBuffer(12));
353
+ try {
354
+ worker = new import_node_worker_threads.Worker(WORKER_SOURCE, { eval: true, workerData: {
355
+ control: control.buffer,
356
+ frames,
357
+ frame,
358
+ intervalMs,
359
+ transcriptPath
360
+ } });
361
+ const active = worker;
362
+ worker.on("error", () => {
363
+ if (worker === active) halt();
364
+ });
365
+ worker.unref();
366
+ } catch {
367
+ halt();
368
+ }
369
+ }
291
370
  },
292
- say(next) {
293
- text = next;
371
+ say(text, measure = null) {
372
+ if (!animate) return;
373
+ pause();
374
+ render(text, measure);
294
375
  draw();
376
+ worker?.postMessage({ frames, frame });
295
377
  },
296
378
  stop() {
297
- if (timer) clearInterval(timer);
298
- timer = null;
299
- clear();
379
+ halt();
380
+ if (animate) write("\r\x1B[2K");
300
381
  }
301
382
  };
302
383
  }
303
384
  var SPINNER_FRAMES = Object.freeze([...FRAMES]);
385
+
386
+ // ../installer/face/src/conformance.ts
304
387
  var ALLOWED = new Set(Object.values(GLYPH));
388
+ var FORBIDDEN = /* @__PURE__ */ new Map([
389
+ ["\u2713", "U+2713 is not in the guidebook glyph set; the check is U+2714"],
390
+ ["\u2717", "U+2717 is not in the guidebook glyph set; the cross is U+2716"],
391
+ ["\u21BB", "U+21BB is not in the guidebook glyph set; a retry uses the dot U+25CF"],
392
+ ["\u250C", "U+250C is clack's intro corner; the welcome opens with the branded diamond"],
393
+ ["\u251C", "U+251C is clack's note frame; a receipt box pairs U+256D/U+256E over U+2570/U+256F"],
394
+ ["\u2514", "U+2514 is clack's outro corner; the sign-off carries the diamond"]
395
+ ]);
396
+ var EMOJI = /[\u{1f300}-\u{1faff}\u{2600}-\u{27bf}]/u;
397
+ function fail(rule, detail) {
398
+ throw new Error(`installer face: ${rule} \u2014 ${detail}`);
399
+ }
400
+ function assertFaceConformance(lines, options) {
401
+ const identity = identityFor(options.product);
402
+ const width = options.columns ?? 100;
403
+ const rendered = lines.flatMap((line) => String(line).split("\n"));
404
+ const plain = rendered.map(stripColor);
405
+ const head = plain[0] ?? "";
406
+ const signOffLine = `${GLYPH.diamond} ${identity.name} \xB7 Mutatis Mutandis`;
407
+ const opensWithWelcome = head.startsWith(GLYPH.diamond) && head !== signOffLine;
408
+ if (options.complete && options.tty !== false) {
409
+ const welcome = `${GLYPH.diamond} ${identity.name} \u2014 Mutatis Mutandis`;
410
+ const welcomes = plain.filter((line) => line === welcome).length;
411
+ const receipts = plain.filter((line) => line.startsWith(GLYPH.boxTop)).length;
412
+ const signOffs = plain.filter((line) => line === signOffLine).length;
413
+ if (welcomes !== (options.nested ? 0 : 1) || signOffs !== (options.nested ? 0 : 1) || !options.nested && receipts !== 1) {
414
+ fail("complete run", "expected one console owner, one welcome, one receipt and one sign-off");
415
+ }
416
+ for (const line of plain) {
417
+ if (line && ![GLYPH.bar, GLYPH.diamond, GLYPH.hollow, GLYPH.boxTop, GLYPH.boxBottom].some((glyph) => line.startsWith(glyph))) {
418
+ fail("complete run", `unframed output: ${JSON.stringify(line)}`);
419
+ }
420
+ }
421
+ }
422
+ if (rendered.length === 0) fail("R7 render before you ship", "no rendered lines were given to the guard");
423
+ const greetings = plain.filter((line) => ["\u2014", "-"].some((separator) => line.includes(`${identity.name} ${separator} Mutatis Mutandis`)));
424
+ if (greetings.length > 1) fail("R8 one welcome per run", "duplicate product greeting");
425
+ if (plain.filter((line) => line.startsWith(GLYPH.boxTop)).length > 1) {
426
+ fail("R8 one receipt per run", "duplicate receipt");
427
+ }
428
+ if (options.nested === true) {
429
+ if (opensWithWelcome) {
430
+ fail("R8 a nested run draws no welcome", `an outer console already greeted the user; this run opened with ${JSON.stringify(head)}`);
431
+ }
432
+ const signOffAt = plain.findIndex((line) => line === signOffLine);
433
+ if (signOffAt !== -1) {
434
+ fail("R8 a nested run draws no sign-off", `the outer console closes the run; line ${signOffAt + 1} signs off`);
435
+ }
436
+ const boxAt = plain.findIndex((line) => line.startsWith(GLYPH.boxTop));
437
+ if (boxAt !== -1) {
438
+ const headline = plain.slice(boxAt + 1).find((line) => line.startsWith(GLYPH.bar) && line.trim() !== GLYPH.bar) ?? "";
439
+ if (headline.includes(GLYPH.check)) {
440
+ fail("R8 a nested run draws no success receipt", `the outer console owns the outcome; this run printed ${JSON.stringify(headline.trim())}`);
441
+ }
442
+ }
443
+ }
444
+ if (options.tty === false) {
445
+ for (const line of plain) {
446
+ const glyph = [...line].find((char) => ALLOWED.has(char));
447
+ if (glyph) fail("non-TTY lane is plain", `a piped run emitted ${JSON.stringify(glyph)} in ${JSON.stringify(line)}`);
448
+ }
449
+ for (const line of rendered) {
450
+ if (line.includes("\x1B[")) fail("non-TTY lane is plain", `a piped run emitted colour in ${JSON.stringify(line)}`);
451
+ }
452
+ return;
453
+ }
454
+ for (const line of plain) {
455
+ if (line.startsWith(`${GLYPH.bar} ${GLYPH.hollow} `)) {
456
+ fail("note step uses U+25CF", `U+25C7 is the note-box opener in ${JSON.stringify(line)}`);
457
+ }
458
+ }
459
+ for (const line of plain) {
460
+ if (/[\u25c6\u25c7\u2714\u2716\u25cf] (?! )/u.test(line)) {
461
+ fail("R4 two spaces after a glyph", `a single space follows the glyph in ${JSON.stringify(line)}`);
462
+ }
463
+ }
464
+ for (const line of plain) {
465
+ for (const [glyph, why] of FORBIDDEN) {
466
+ if (line.includes(glyph)) fail("glyph set", `${why} (in ${JSON.stringify(line)})`);
467
+ }
468
+ const emoji = [...line].find((char) => EMOJI.test(char) && !ALLOWED.has(char));
469
+ if (emoji) fail("glyph set", `emoji ${JSON.stringify(emoji)} in ${JSON.stringify(line)}`);
470
+ }
471
+ const top = plain.findIndex((line) => line.startsWith(GLYPH.boxTop));
472
+ if (top !== -1) {
473
+ const bottom = plain.findIndex((line, index) => index > top && line.startsWith(GLYPH.boxBottom));
474
+ if (bottom === -1) fail("R5 receipt frame", "the receipt box opens and never closes");
475
+ if (!plain[top].endsWith(GLYPH.boxTopEnd)) fail("R5 receipt frame", `the top row does not pair its corners: ${JSON.stringify(plain[top])}`);
476
+ if (!plain[bottom].endsWith(GLYPH.boxBottomEnd)) fail("R5 receipt frame", `the bottom row does not pair its corners: ${JSON.stringify(plain[bottom])}`);
477
+ const widths = new Set(plain.slice(top, bottom + 1).map(visibleWidth));
478
+ if (widths.size !== 1) fail("R5 receipt frame", `the box is ragged: widths ${[...widths].join(", ")}`);
479
+ const rows = plain.slice(top + 1, bottom);
480
+ if (rows.length > 0 && rows[0].includes(GLYPH.check)) {
481
+ if (rows.length < 3) {
482
+ fail("R9 the receipt says what changed", `a success receipt has three lines \u2014 ready, what changed, the health command \u2014 and this one has ${rows.length}: ${JSON.stringify(rows.map((row) => row.trim()))}`);
483
+ }
484
+ const changed = rows[1].replace(GLYPH.bar, "").trim();
485
+ if (changed === "") fail("R9 the receipt says what changed", "the middle line of the success receipt is blank");
486
+ }
487
+ }
488
+ const inReceipt = (index) => top !== -1 && index >= top && index <= plain.findIndex((line, at) => at > top && line.startsWith(GLYPH.boxBottom));
489
+ const welcomeEnd = opensWithWelcome ? 4 : 0;
490
+ for (const [index, line] of plain.entries()) {
491
+ if (index < welcomeEnd || inReceipt(index)) continue;
492
+ if (!line.startsWith(GLYPH.bar) || line.trim() === GLYPH.bar) continue;
493
+ const after = [...line.slice(1)];
494
+ const at = after.findIndex((char) => char !== " ");
495
+ const edge = at + 2;
496
+ if (edge === 4 && ALLOWED.has(after[at])) continue;
497
+ if (edge !== TITLE_COLUMN + 1) {
498
+ fail("R1 one rail, one left edge", `text starts at column ${edge} without a glyph there; a line that carries no glyph belongs at the title column (${TITLE_COLUMN + 1}): ${JSON.stringify(line)}`);
499
+ }
500
+ }
501
+ for (const line of plain) {
502
+ if (/\(node:\d+\)|DeprecationWarning|ExperimentalWarning/u.test(line)) {
503
+ fail("R2 no foreign output in the face", `a Node warning reached the face: ${JSON.stringify(line)} \u2014 fix the spawn, or run the child with --no-deprecation`);
504
+ }
505
+ }
506
+ const receiptStart = top === -1 ? plain.length : top;
507
+ for (const line of plain.slice(0, receiptStart)) {
508
+ if (/[\u2714\u2716\u25cf\u25c7]/u.test(line) && /([A-Za-z]:\\|\/(?:home|Users|opt|usr)\/)/u.test(line)) {
509
+ fail("R3 paths live in the receipt", `a step line carries a path: ${JSON.stringify(line)}`);
510
+ }
511
+ }
512
+ for (const line of plain) {
513
+ if (visibleWidth(line) > width) {
514
+ fail("width cap", `a line is ${visibleWidth(line)} columns wide, cap ${width}: ${JSON.stringify(line)}`);
515
+ }
516
+ }
517
+ if (opensWithWelcome) {
518
+ const expected = `${GLYPH.diamond} ${identity.name} \u2014 Mutatis Mutandis`;
519
+ if (head !== expected) fail("welcome is the product table's", `expected ${JSON.stringify(expected)}, got ${JSON.stringify(head)}`);
520
+ const warm = plain.slice(1, 4).find((line) => line.includes(identity.warm) || line.includes(identity.installWarm));
521
+ if (!warm) fail("welcome is the product table's", `the warm line for ${options.product} is missing or reworded`);
522
+ }
523
+ const painted = rendered.join("\n");
524
+ if (painted.includes("\x1B[")) {
525
+ if (!painted.includes(identity.accent) && !painted.includes(PALETTE.muted)) {
526
+ fail("accent", `${options.product} rendered colour that is neither its accent nor a neutral token`);
527
+ }
528
+ }
529
+ }
530
+
531
+ // ../installer/face/src/run.ts
532
+ var import_node_fs4 = require("node:fs");
533
+
534
+ // ../installer/face/src/outcome.ts
535
+ var import_node_fs3 = require("node:fs");
536
+ var counts = ["total", "updated", "failed"];
537
+ var strings = ["version", "retry", "detail", "logPath"];
538
+ var flags = ["dryRun", "installed", "deferred", "operationFailed"];
539
+ function validateInstallerOutcome(value) {
540
+ const invalid = () => {
541
+ throw new Error("installer outcome: invalid child result");
542
+ };
543
+ if (!value || typeof value !== "object" || Array.isArray(value)) return invalid();
544
+ const facts = value;
545
+ const allowed = [...counts, ...strings, ...flags, "rollback", "logState"];
546
+ if (Object.keys(facts).some((key) => !allowed.includes(key))) return invalid();
547
+ for (const key of counts) if (!Number.isSafeInteger(facts[key]) || facts[key] < 0) return invalid();
548
+ if (facts.updated + facts.failed > facts.total) return invalid();
549
+ for (const key of strings) if (facts[key] !== void 0 && (typeof facts[key] !== "string" || facts[key].length > 4096)) return invalid();
550
+ for (const key of flags) if (facts[key] !== void 0 && typeof facts[key] !== "boolean") return invalid();
551
+ if (facts.rollback !== void 0 && !["completed", "partial", "not-needed", "unknown"].includes(facts.rollback)) return invalid();
552
+ if (facts.logState !== void 0 && !["unavailable", "omitted"].includes(facts.logState)) return invalid();
553
+ if (facts.logPath !== void 0 && (!facts.logPath.trim() || /[\x00-\x1f\x7f]/u.test(facts.logPath))) return invalid();
554
+ return { ...facts };
555
+ }
556
+ function writeInstallerOutcome(path, value) {
557
+ (0, import_node_fs3.writeFileSync)(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
558
+ }
559
+
560
+ // ../installer/face/src/run.ts
561
+ function validateInstallerProduct(value) {
562
+ const fail2 = (field) => {
563
+ throw new Error(`installer product: invalid ${field}`);
564
+ };
565
+ if (!value || typeof value !== "object" || Array.isArray(value)) return fail2("declaration");
566
+ const input = value;
567
+ const text = (value2, field) => typeof value2 === "string" && value2.trim() && !/[\r\n\x00-\x1f]/u.test(value2) ? value2 : fail2(field);
568
+ const key = text(input.product, "product");
569
+ const product = { mmi: "mmi-hub", jerv: "jerv-hub" }[key] ?? key;
570
+ const identity = identityFor(product);
571
+ const gate = text(input.gate, "gate");
572
+ let gateUrl;
573
+ try {
574
+ gateUrl = new URL(gate);
575
+ } catch {
576
+ return fail2("gate");
577
+ }
578
+ if (!["https:", "http:"].includes(gateUrl.protocol) || gateUrl.username || gateUrl.password) return fail2("gate");
579
+ const doctor = text(input.doctor, "doctor");
580
+ if (doctor !== identity.doctor) return fail2("doctor");
581
+ if (!Array.isArray(input.surfaces) || input.surfaces.length === 0) return fail2("surfaces");
582
+ const ids = /* @__PURE__ */ new Set();
583
+ const surfaces = input.surfaces.map((raw) => {
584
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail2("surface");
585
+ const source = raw;
586
+ const id = text(source.id, "surface.id");
587
+ if (ids.has(id)) return fail2("duplicate surface.id");
588
+ ids.add(id);
589
+ const surface = { id };
590
+ for (const field of ["npm", "bin", "kind", "activation"]) {
591
+ if (source[field] !== void 0) surface[field] = text(source[field], `surface.${field}`);
592
+ }
593
+ if (Boolean(surface.npm) !== Boolean(surface.bin)) return fail2("surface npm/bin pair");
594
+ if (surface.kind !== void 0 && !["agent-home", "payload"].includes(surface.kind)) return fail2("surface.kind");
595
+ if (!surface.npm && !surface.kind) return fail2("surface implementation");
596
+ return surface;
597
+ });
598
+ return { product, gate, doctor, surfaces };
599
+ }
600
+ var PHASES = {
601
+ preflight: ["Checking prerequisites", "Checked prerequisites"],
602
+ resolve: ["Resolving the release", "Resolved the release"],
603
+ download: ["Downloading the payload", "Downloaded the payload"],
604
+ "sign-in": ["Signing in", "Signed in"],
605
+ check: ["Checking surfaces", "Checked surfaces"],
606
+ arm: ["Scheduling updates", "Armed hourly updates"],
607
+ "verify-release": ["Checking the release version", "Verified the release version"],
608
+ verify: ["Verifying the payload", "Verified the payload"],
609
+ install: ["Installing the product", "Installed the product"],
610
+ activate: ["Activating surfaces", "Activated surfaces"],
611
+ doctor: ["Checking health", "Checked health"],
612
+ rollback: ["Restoring the previous version", "Restored the previous version"]
613
+ };
614
+ function createInstallerRun(value, options = {}) {
615
+ const declaration = validateInstallerProduct(value);
616
+ const env = options.env ?? process.env;
617
+ const tty = options.tty ?? Boolean(process.stdout.isTTY);
618
+ const face = createFace({
619
+ operation: options.operation,
620
+ product: declaration.product,
621
+ columns: options.columns,
622
+ env,
623
+ color: tty && options.color !== false && env.NO_COLOR === void 0 && env.TERM !== "dumb"
624
+ });
625
+ const errors = [];
626
+ const write = options.write ? (text, channel) => {
627
+ try {
628
+ options.write(text, channel);
629
+ } catch (error) {
630
+ errors.push(`installer output observer: ${error instanceof Error ? error.message : String(error)}`);
631
+ }
632
+ } : (text, channel) => {
633
+ (channel === "stdout" ? process.stdout : process.stderr).write(text);
634
+ };
635
+ const emit = (text, channel = "stdout", recorded = text) => {
636
+ if (!text) return;
637
+ write(text, channel);
638
+ if (env.MM_FACE_TRANSCRIPT) {
639
+ (0, import_node_fs4.appendFileSync)(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
640
+ `, "utf8");
641
+ }
642
+ };
643
+ const lines = (rows, channel = "stdout") => {
644
+ for (const row of rows.flatMap((row2) => row2.split("\n"))) if (row) emit(`${row}
645
+ `, channel);
646
+ };
647
+ const spinner = createSpinner(face, {
648
+ animate: tty && env.TERM !== "dumb" && (options.animate ?? Boolean(process.stderr.isTTY)),
649
+ ...options.write ? { stream: { write: (text) => {
650
+ emit(String(text), "spinner");
651
+ return true;
652
+ } } } : { transcriptPath: env.MM_FACE_TRANSCRIPT }
653
+ });
654
+ let started = false;
655
+ let finished = false;
656
+ const start = () => {
657
+ if (started || finished) return;
658
+ started = true;
659
+ if (options.quiet) return;
660
+ const welcome = face.welcome();
661
+ if (tty) lines(welcome);
662
+ else if (welcome.length) {
663
+ const warm = options.operation === "install" ? face.identity.installWarm : face.identity.warm;
664
+ lines([`${face.identity.name} - Mutatis Mutandis`, warm.replaceAll("\u2014", "-").replaceAll("\u2026", "...")]);
665
+ }
666
+ };
667
+ const durable = (title, measure, kind) => {
668
+ spinner.stop();
669
+ const rendered = face.step(title, measure, kind);
670
+ if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
671
+ };
672
+ const run = {
673
+ get errors() {
674
+ return [...errors];
675
+ },
676
+ start,
677
+ phase(id, facts = {}) {
678
+ if (finished) throw new Error("installer run already finished");
679
+ if (!Object.hasOwn(PHASES, id)) throw new Error("installer run: unknown phase");
680
+ start();
681
+ const state = facts.state ?? "ok";
682
+ const title = PHASES[id][state === "ok" ? 1 : 0];
683
+ if (state === "running") {
684
+ spinner.start(title, facts.measure ?? null);
685
+ return;
686
+ }
687
+ if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
688
+ if (facts.detail) run.relay(facts.detail);
689
+ },
690
+ surface(facts) {
691
+ if (finished) throw new Error("installer run already finished");
692
+ const surface = declaration.surfaces.find((surface2) => surface2.id === facts.id);
693
+ if (!surface) throw new Error("installer run: undeclared surface");
694
+ start();
695
+ const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} ${tty ? "\u2192" : "->"} ${facts.to}` : ` ${facts.to}` : "";
696
+ const status = { updated: options.dryRun ? "would update" : "updated", current: "already current", failed: "failed", skipped: "skipped", retry: "retrying", pending: "pending", kept: "kept" }[facts.state];
697
+ if (!status) throw new Error("installer run: unknown surface state");
698
+ const separator = tty ? "\xB7" : "-";
699
+ const activation = facts.state === "updated" && surface.activation ? ` ${separator} ${surface.activation}` : "";
700
+ const completed = ["updated", "current", "kept", "skipped"].includes(facts.state);
701
+ durable(`${facts.id}${versions} ${separator} ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
702
+ if (facts.detail) run.relay(facts.detail);
703
+ },
704
+ milestone({ step, state, ms }) {
705
+ start();
706
+ durable(step, ms === void 0 ? null : ms / 1e3, state);
707
+ },
708
+ signIn({ url, code }) {
709
+ start();
710
+ spinner.stop();
711
+ for (const text of [`Open ${url}`, `Enter code: ${code}`]) {
712
+ const rendered = `${tty ? face.relay(text) : text}
713
+ `;
714
+ emit(rendered, "stdout", rendered.replace(code, "[redacted]"));
715
+ }
716
+ },
717
+ // Only pass safe diagnostic text, never authentication output or credentials.
718
+ relay(text, channel = "stdout", record = true) {
719
+ spinner.stop();
720
+ const rendered = tty ? face.relay(text) : stripColor(text);
721
+ for (const row of rendered.split("\n")) if (row) {
722
+ emit(`${row}
723
+ `, channel, record ? `${row}
724
+ ` : `${tty ? face.relay("[external output omitted]") : "[external output omitted]"}
725
+ `);
726
+ }
727
+ },
728
+ cancel() {
729
+ if (finished) return;
730
+ if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(
731
+ env.MM_INSTALLER_OUTCOME_FILE,
732
+ { total: 0, updated: 0, failed: 0, deferred: true, detail: "Operation cancelled." }
733
+ );
734
+ start();
735
+ spinner.stop();
736
+ finished = true;
737
+ if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE) {
738
+ lines(tty ? face.receipt(["Operation cancelled."], { ready: false }) : ["Operation cancelled."]);
739
+ }
740
+ if (tty) lines([face.signOff()]);
741
+ },
742
+ finish(facts) {
743
+ if (finished) return;
744
+ validateInstallerOutcome(facts);
745
+ if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(env.MM_INSTALLER_OUTCOME_FILE, facts);
746
+ start();
747
+ spinner.stop();
748
+ finished = true;
749
+ if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
750
+ const changed = !facts.version ? "No release target is available." : facts.dryRun ? `Would update ${facts.updated} of ${facts.total} surfaces to ${facts.version}.` : facts.installed ? `Installed ${facts.version} across ${facts.total} surfaces.` : `Updated ${facts.updated} of ${facts.total} surfaces to ${facts.version}.`;
751
+ const ready = facts.failed === 0 && !facts.dryRun && !facts.deferred && !facts.operationFailed && Boolean(facts.version);
752
+ const body = [
753
+ `${ready ? GLYPH.check : facts.dryRun ? GLYPH.dot : GLYPH.cross} ${face.identity.name}${ready ? " is ready." : facts.dryRun ? " preview complete." : facts.deferred ? " update deferred." : " is not ready yet."}`,
754
+ changed,
755
+ ...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
756
+ ...facts.detail ? [facts.detail] : [],
757
+ ...facts.rollback ? [`Rollback: ${facts.rollback}`] : [],
758
+ ...facts.logPath ? [`Log: ${facts.logPath}`] : facts.logState ? [`Log: ${facts.logState}`] : [],
759
+ facts.retry && (facts.failed > 0 || facts.operationFailed) ? `Retry: ${facts.retry}` : `Check health any time: ${declaration.doctor}`
760
+ ];
761
+ if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE && (facts.failed > 0 || facts.operationFailed)) {
762
+ lines(tty ? face.receipt(body, { ready }) : body.map((row) => row.replace(/^[✔✖●] /u, "")));
763
+ }
764
+ if (tty) lines([face.signOff()]);
765
+ },
766
+ stop() {
767
+ spinner.stop();
768
+ }
769
+ };
770
+ return run;
771
+ }
772
+
773
+ // ../installer/face/src/maintenance.ts
774
+ async function runMaintenance(product, args, dependencies) {
775
+ const json = args.includes("--json");
776
+ const quiet = args.includes("--quiet");
777
+ const dryRun = args.includes("--dry-run");
778
+ const installed = args.includes("install");
779
+ const render = dependencies.render;
780
+ const env = render?.env ?? process.env;
781
+ const tty = render?.tty ?? Boolean(process.stdout.isTTY);
782
+ const run = json ? null : dependencies.run ?? createInstallerRun(product, {
783
+ ...render,
784
+ tty,
785
+ env,
786
+ color: tty && render?.color !== false && !args.includes("--no-color") && env.TERM !== "dumb",
787
+ columns: render?.columns ?? process.stdout.columns,
788
+ dryRun,
789
+ quiet,
790
+ operation: installed ? "install" : "update"
791
+ });
792
+ if (!quiet) run?.start();
793
+ let active;
794
+ let activeFacts;
795
+ let handedOff;
796
+ const shown = new Set((env.MM_FACE_CONTINUES ?? "").split(",").map((value) => value.trim()).filter(Boolean));
797
+ if (!quiet && !json) shown.add("welcome");
798
+ let phaseFailed = false;
799
+ let armFailed = false;
800
+ let knownFailure;
801
+ const phaseStarted = /* @__PURE__ */ new Map();
802
+ const phase = (event) => {
803
+ if (handedOff !== void 0) return;
804
+ if (event.state === "fail") phaseFailed = true;
805
+ const started = phaseStarted.get(event.id);
806
+ if (event.state === "running") {
807
+ if (started === void 0) phaseStarted.set(event.id, Date.now());
808
+ active = event.id;
809
+ } else {
810
+ phaseStarted.delete(event.id);
811
+ if (active === event.id) active = void 0;
812
+ }
813
+ if (!quiet && event.state === "running" && event.detail) run?.relay(event.detail);
814
+ const measured = event.measured;
815
+ const measure = measured?.kind === "percent" ? `${Math.max(0, Math.min(100, Math.round(measured.percent)))}%` : measured?.kind === "bytes" ? `${measured.done}/${measured.total} B` : measured?.kind === "count" ? `${measured.done}${measured.total === void 0 ? "" : `/${measured.total}`}` : event.measure;
816
+ const facts = {
817
+ state: event.state,
818
+ detail: event.detail,
819
+ measure,
820
+ ...event.state !== "running" && started !== void 0 ? { seconds: (Date.now() - started) / 1e3 } : {}
821
+ };
822
+ if (event.state === "running") activeFacts = facts;
823
+ if (!quiet) {
824
+ run?.phase(event.id, facts);
825
+ if (event.state === "ok" && run) shown.add(event.id);
826
+ }
827
+ };
828
+ const pending = [];
829
+ let streaming = false;
830
+ const emitArm = (arm) => {
831
+ if (quiet) return;
832
+ run?.surface({
833
+ id: arm.surface,
834
+ from: arm.from ?? void 0,
835
+ to: arm.to ?? void 0,
836
+ state: arm.verdict === "fail" ? "failed" : arm.verdict === "defer" ? "retry" : arm.verdict === "skip" ? "kept" : arm.from === arm.to && !arm.launchStaged ? "current" : "updated",
837
+ ...arm.durationMs === void 0 ? {} : { seconds: arm.durationMs / 1e3 },
838
+ detail: dependencies.diagnose?.(arm)
839
+ });
840
+ };
841
+ const writeJson = (summary, schedule) => {
842
+ process.stdout.write(JSON.stringify(dependencies.jsonResult ? dependencies.jsonResult(summary, schedule) : installed ? { convergence: summary, autoupdate: schedule } : summary, null, 2) + "\n");
843
+ };
844
+ try {
845
+ const summary = await dependencies.engine({
846
+ dryRun,
847
+ withCustody: async (operation) => {
848
+ if (handedOff !== void 0) throw new Error("installer terminal ownership already transferred");
849
+ run?.stop();
850
+ const result = await operation({ faceContinues: [...shown] });
851
+ if (result.kind === "reexec") {
852
+ if (!Number.isSafeInteger(result.code) || result.code < 0) throw new Error("invalid handoff exit code");
853
+ handedOff = result.code;
854
+ } else if (result.kind === "resume") {
855
+ if (!quiet && active) run?.phase(active, activeFacts);
856
+ } else throw new Error("invalid terminal custody result");
857
+ return result;
858
+ },
859
+ shippedTarget: installed ? dependencies.shippedTarget : void 0,
860
+ narrate: !quiet && !json && args.includes("--verbose") ? (text) => {
861
+ if (handedOff === void 0) run?.relay(text, "stderr", false);
862
+ } : void 0,
863
+ relay: (text, channel = "stdout", record = false) => {
864
+ if (handedOff === void 0) run?.relay(text, channel, record);
865
+ },
866
+ onTargetResolved: (target) => phase({ id: "verify-release", detail: target, state: "ok" }),
867
+ onPhase: () => {
868
+ if (!quiet && handedOff === void 0) run?.phase("check", { state: "running" });
869
+ },
870
+ onPhaseEvent: phase,
871
+ onArm: (arm) => {
872
+ if (handedOff !== void 0) return;
873
+ if (arm.verdict === "fail") armFailed = true;
874
+ pending.push(arm);
875
+ if (pending.length > 1) streaming = true;
876
+ if (streaming) for (const result of pending.splice(0)) emitArm(result);
877
+ }
878
+ });
879
+ if (handedOff !== void 0) return handedOff;
880
+ if (summary.reexec) {
881
+ if (!dependencies.handOff) {
882
+ knownFailure = "The updated version needs a handoff, but no handoff is configured.";
883
+ throw new Error(knownFailure);
884
+ }
885
+ run?.stop();
886
+ knownFailure = "The updated updater could not launch.";
887
+ return await dependencies.handOff(summary.reexec, args, { ...process.env, MM_FACE_CONTINUES: "welcome" });
888
+ }
889
+ const counts2 = dependencies.counts(summary);
890
+ validateInstallerOutcome({ total: counts2.total, updated: counts2.updated, failed: counts2.failed });
891
+ if (!Number.isSafeInteger(counts2.retry) || counts2.retry < 0 || counts2.retry > counts2.total) throw new Error("invalid retry count");
892
+ if (summary.cancelled && summary.exit === 0 && !phaseFailed && !armFailed && counts2.failed === 0 && counts2.updated === 0) {
893
+ if (json) writeJson(summary, null);
894
+ else run?.cancel();
895
+ return 0;
896
+ }
897
+ for (const arm of pending) emitArm(arm);
898
+ const waiting = summary.surfaces.filter((surface) => surface.present && surface.action === "arm-pending");
899
+ if (!quiet) for (const surface of waiting) run?.surface({ id: surface.id, state: "pending" });
900
+ let schedule = null;
901
+ if (installed && !dryRun && dependencies.schedule) {
902
+ phase({ id: "arm", state: "running" });
903
+ schedule = dependencies.schedule();
904
+ if (schedule) phase({ id: "arm", state: schedule.ok ? "ok" : "fail", detail: schedule.ok ? void 0 : schedule.detail });
905
+ }
906
+ const failed = phaseFailed || armFailed || counts2.failed > 0 || Boolean(schedule && !schedule.ok);
907
+ const exit = Math.max(summary.exit, failed ? 1 : 0);
908
+ if (json) {
909
+ writeJson({ ...summary, exit }, schedule);
910
+ } else {
911
+ run.finish({
912
+ version: summary.target ?? void 0,
913
+ total: counts2.total,
914
+ updated: counts2.updated,
915
+ failed: counts2.failed,
916
+ operationFailed: exit !== 0,
917
+ deferred: !failed && Boolean(counts2.retry || waiting.length || summary.deferred || summary.cancelled || !summary.target),
918
+ dryRun,
919
+ installed,
920
+ detail: schedule && !schedule.ok ? schedule.detail : summary.detail ?? (summary.cancelled ? "Operation cancelled." : void 0),
921
+ retry: summary.retry,
922
+ rollback: summary.rollback,
923
+ logPath: summary.logPath,
924
+ logState: summary.logState
925
+ });
926
+ }
927
+ return exit;
928
+ } catch (error) {
929
+ if (handedOff !== void 0) throw error;
930
+ let detail = knownFailure ?? (active === "sign-in" ? "Sign-in did not complete." : installed ? "Installation did not complete." : "Update did not complete.");
931
+ if (!knownFailure && dependencies.safeErrorDetail) {
932
+ try {
933
+ detail = dependencies.safeErrorDetail(error);
934
+ } catch {
935
+ }
936
+ }
937
+ if (active) phase({ id: active, state: "fail" });
938
+ run?.relay(detail, "stderr");
939
+ run?.finish({ total: 0, updated: 0, failed: 0, operationFailed: true, detail });
940
+ if (json) process.stdout.write(JSON.stringify({ error: detail }) + "\n");
941
+ return 1;
942
+ } finally {
943
+ run?.stop();
944
+ }
945
+ }
946
+
947
+ // ../installer.product.json
948
+ var installer_product_default = {
949
+ product: "mmi",
950
+ gate: "https://gate.mutmut.work/mmi",
951
+ doctor: "mmi doctor",
952
+ surfaces: [
953
+ { id: "updater", npm: "@mutmutco/hub", bin: "mmi-hub" },
954
+ { id: "cli", npm: "@mutmutco/cli", bin: "mmi-cli" },
955
+ { id: "claude", kind: "agent-home", activation: "next launch" },
956
+ { id: "codex", kind: "agent-home", activation: "next launch" },
957
+ { id: "cursor", kind: "agent-home", activation: "next launch" },
958
+ { id: "hermes", kind: "agent-home", activation: "next launch" },
959
+ { id: "jervcode", kind: "agent-home", activation: "next launch" }
960
+ ]
961
+ };
305
962
 
306
963
  // src/state.ts
307
- var import_node_fs = require("node:fs");
964
+ var import_node_fs5 = require("node:fs");
308
965
  var import_node_os = require("node:os");
309
966
  var import_node_path = require("node:path");
310
967
  function stateRoot(env = process.env) {
@@ -318,8 +975,8 @@ function statePaths(env = process.env) {
318
975
  }
319
976
  function ensureState(env = process.env) {
320
977
  const paths = statePaths(env);
321
- (0, import_node_fs.mkdirSync)(paths.staging, { recursive: true });
322
- (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(paths.gitCache), { recursive: true });
978
+ (0, import_node_fs5.mkdirSync)(paths.staging, { recursive: true });
979
+ (0, import_node_fs5.mkdirSync)((0, import_node_path.dirname)(paths.gitCache), { recursive: true });
323
980
  return paths;
324
981
  }
325
982
  var UpdaterBusy = class extends Error {
@@ -329,15 +986,15 @@ var LEASE_STALE_MS = 10 * 6e4;
329
986
  function tryAcquire(path) {
330
987
  const token = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
331
988
  try {
332
- const fd = (0, import_node_fs.openSync)(path, "wx");
333
- (0, import_node_fs.writeFileSync)(fd, JSON.stringify({ token, pid: process.pid, ts: Date.now() }) + "\n");
334
- (0, import_node_fs.closeSync)(fd);
989
+ const fd = (0, import_node_fs5.openSync)(path, "wx");
990
+ (0, import_node_fs5.writeFileSync)(fd, JSON.stringify({ token, pid: process.pid, ts: Date.now() }) + "\n");
991
+ (0, import_node_fs5.closeSync)(fd);
335
992
  return {
336
993
  token,
337
994
  release() {
338
995
  try {
339
996
  const current2 = readLease(path);
340
- if (current2 && current2.token === token) (0, import_node_fs.unlinkSync)(path);
997
+ if (current2 && current2.token === token) (0, import_node_fs5.unlinkSync)(path);
341
998
  } catch {
342
999
  }
343
1000
  }
@@ -348,7 +1005,7 @@ function tryAcquire(path) {
348
1005
  const current = readLease(path);
349
1006
  if (current && Date.now() - current.ts > LEASE_STALE_MS) {
350
1007
  try {
351
- (0, import_node_fs.renameSync)(path, `${path}.stale-${Date.now()}`);
1008
+ (0, import_node_fs5.renameSync)(path, `${path}.stale-${Date.now()}`);
352
1009
  return "stolen-stale";
353
1010
  } catch {
354
1011
  }
@@ -357,7 +1014,7 @@ function tryAcquire(path) {
357
1014
  }
358
1015
  function readLease(path) {
359
1016
  try {
360
- return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
1017
+ return JSON.parse((0, import_node_fs5.readFileSync)(path, "utf8"));
361
1018
  } catch {
362
1019
  return null;
363
1020
  }
@@ -382,10 +1039,10 @@ function withLease(path, fn, waitMs = LEASE_WAIT_MS) {
382
1039
  }
383
1040
  function appendJournal(path, event) {
384
1041
  try {
385
- (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
386
- const fd = (0, import_node_fs.openSync)(path, "a");
387
- (0, import_node_fs.writeFileSync)(fd, JSON.stringify(event) + "\n");
388
- (0, import_node_fs.closeSync)(fd);
1042
+ (0, import_node_fs5.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
1043
+ const fd = (0, import_node_fs5.openSync)(path, "a");
1044
+ (0, import_node_fs5.writeFileSync)(fd, JSON.stringify(event) + "\n");
1045
+ (0, import_node_fs5.closeSync)(fd);
389
1046
  return true;
390
1047
  } catch {
391
1048
  return false;
@@ -404,7 +1061,7 @@ function journalHighWater(path, surface, events) {
404
1061
  function parseJournal(path) {
405
1062
  const events = [];
406
1063
  try {
407
- for (const line of (0, import_node_fs.readFileSync)(path, "utf8").split("\n")) {
1064
+ for (const line of (0, import_node_fs5.readFileSync)(path, "utf8").split("\n")) {
408
1065
  if (!line.trim()) continue;
409
1066
  try {
410
1067
  events.push(JSON.parse(line));
@@ -418,7 +1075,7 @@ function parseJournal(path) {
418
1075
  function journalSurfaceRepeatedFail(path, surface, currentRun) {
419
1076
  let lastVerdict = null;
420
1077
  try {
421
- for (const line of (0, import_node_fs.readFileSync)(path, "utf8").split("\n")) {
1078
+ for (const line of (0, import_node_fs5.readFileSync)(path, "utf8").split("\n")) {
422
1079
  if (!line.trim()) continue;
423
1080
  try {
424
1081
  const event = JSON.parse(line);
@@ -461,7 +1118,7 @@ function compareSemver(a, b) {
461
1118
  return va.pre.join(".") < vb.pre.join(".") ? -1 : va.pre.join(".") > vb.pre.join(".") ? 1 : 0;
462
1119
  }
463
1120
  function fileExists(path) {
464
- return (0, import_node_fs.existsSync)(path);
1121
+ return (0, import_node_fs5.existsSync)(path);
465
1122
  }
466
1123
 
467
1124
  // src/reconcile.ts
@@ -469,7 +1126,7 @@ var import_node_perf_hooks2 = require("node:perf_hooks");
469
1126
 
470
1127
  // src/npm.ts
471
1128
  var import_node_child_process = require("node:child_process");
472
- var import_node_fs2 = require("node:fs");
1129
+ var import_node_fs6 = require("node:fs");
473
1130
  var import_node_path2 = require("node:path");
474
1131
  var cachedNpm = null;
475
1132
  function resolveNpm(env = process.env) {
@@ -480,7 +1137,7 @@ function resolveNpm(env = process.env) {
480
1137
  const first = where.stdout.split("\n").map((l) => l.trim()).find(Boolean);
481
1138
  if (where.status === 0 && first) {
482
1139
  const script = (0, import_node_path2.join)((0, import_node_path2.dirname)(first), "node_modules", "npm", "bin", "npm-cli.js");
483
- if ((0, import_node_fs2.existsSync)(script)) {
1140
+ if ((0, import_node_fs6.existsSync)(script)) {
484
1141
  cachedNpm = { kind: "node-script", script };
485
1142
  return cachedNpm;
486
1143
  }
@@ -489,13 +1146,13 @@ function resolveNpm(env = process.env) {
489
1146
  }
490
1147
  }
491
1148
  const bundled = (0, import_node_path2.join)((0, import_node_path2.dirname)(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
492
- if ((0, import_node_fs2.existsSync)(bundled)) {
1149
+ if ((0, import_node_fs6.existsSync)(bundled)) {
493
1150
  cachedNpm = { kind: "node-script", script: bundled };
494
1151
  return cachedNpm;
495
1152
  }
496
1153
  const located = (0, import_node_child_process.spawnSync)("/bin/sh", ["-lc", "command -v npm"], { encoding: "utf8", windowsHide: true });
497
1154
  const resolved = (located.stdout ?? "").trim().split("\n").map((line) => line.trim()).find(Boolean);
498
- if (located.status === 0 && resolved && (0, import_node_fs2.existsSync)(resolved)) {
1155
+ if (located.status === 0 && resolved && (0, import_node_fs6.existsSync)(resolved)) {
499
1156
  cachedNpm = { kind: "command", command: resolved };
500
1157
  return cachedNpm;
501
1158
  }
@@ -577,7 +1234,7 @@ function npmVersions(packageName, env = process.env) {
577
1234
 
578
1235
  // src/gate.ts
579
1236
  var import_node_child_process2 = require("node:child_process");
580
- var import_node_fs3 = require("node:fs");
1237
+ var import_node_fs7 = require("node:fs");
581
1238
  function parseBom(text) {
582
1239
  const bom = JSON.parse(text);
583
1240
  if (typeof bom?.version !== "string" || typeof bom?.sourceCommit !== "string" || !Array.isArray(bom?.artifacts)) {
@@ -599,7 +1256,7 @@ function ensureBareCache(cachePath, repoUrl) {
599
1256
  if (probe.status !== 0) {
600
1257
  throw new GateEnvError("git is not available \u2014 the release gate needs a git client");
601
1258
  }
602
- if (!(0, import_node_fs3.existsSync)(cachePath)) {
1259
+ if (!(0, import_node_fs7.existsSync)(cachePath)) {
603
1260
  const init = (0, import_node_child_process2.spawnSync)("git", ["init", "--bare", cachePath], { encoding: "utf8", windowsHide: true });
604
1261
  if (init.status !== 0) throw new GateEnvError(`cannot create bare cache at ${cachePath}: ${init.stderr}`);
605
1262
  }
@@ -706,24 +1363,24 @@ async function checkAtomicity(cachePath, tagCommit, bom, candidate, npm) {
706
1363
  }
707
1364
 
708
1365
  // src/surfaces.ts
709
- var import_node_fs8 = require("node:fs");
1366
+ var import_node_fs12 = require("node:fs");
710
1367
  var import_node_os3 = require("node:os");
711
1368
  var import_node_path7 = require("node:path");
712
1369
 
713
1370
  // src/arm-host-plugins.ts
714
1371
  var import_node_child_process5 = require("node:child_process");
715
- var import_node_fs7 = require("node:fs");
1372
+ var import_node_fs11 = require("node:fs");
716
1373
  var import_node_os2 = require("node:os");
717
1374
  var import_node_path6 = require("node:path");
718
1375
 
719
1376
  // src/arm-npm-global.ts
720
- var import_node_fs5 = require("node:fs");
1377
+ var import_node_fs9 = require("node:fs");
721
1378
  var import_node_child_process4 = require("node:child_process");
722
1379
  var import_node_path4 = require("node:path");
723
1380
 
724
1381
  // src/pending.ts
725
1382
  var import_node_child_process3 = require("node:child_process");
726
- var import_node_fs4 = require("node:fs");
1383
+ var import_node_fs8 = require("node:fs");
727
1384
  var import_node_path3 = require("node:path");
728
1385
  function hubDistPath(env) {
729
1386
  const root = globalRoot(env);
@@ -738,7 +1395,7 @@ function watcherLockPath(paths) {
738
1395
  }
739
1396
  function readPending(paths) {
740
1397
  try {
741
- const parsed = JSON.parse((0, import_node_fs4.readFileSync)(pendingPath(paths), "utf8"));
1398
+ const parsed = JSON.parse((0, import_node_fs8.readFileSync)(pendingPath(paths), "utf8"));
742
1399
  if (!Array.isArray(parsed)) return [];
743
1400
  return parsed.filter((row) => Boolean(row) && typeof row === "object" && typeof row.surface === "string" && typeof row.target === "string" && Array.isArray(row.images) && Array.isArray(row.dirs));
744
1401
  } catch {
@@ -746,12 +1403,12 @@ function readPending(paths) {
746
1403
  }
747
1404
  }
748
1405
  function writePending(paths, records) {
749
- (0, import_node_fs4.mkdirSync)(paths.root, { recursive: true });
1406
+ (0, import_node_fs8.mkdirSync)(paths.root, { recursive: true });
750
1407
  const path = pendingPath(paths);
751
1408
  const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
752
- (0, import_node_fs4.writeFileSync)(tmp, `${JSON.stringify(records, null, 2)}
1409
+ (0, import_node_fs8.writeFileSync)(tmp, `${JSON.stringify(records, null, 2)}
753
1410
  `, "utf8");
754
- (0, import_node_fs4.renameSync)(tmp, path);
1411
+ (0, import_node_fs8.renameSync)(tmp, path);
755
1412
  }
756
1413
  function clearPending(paths, surface) {
757
1414
  const records = readPending(paths);
@@ -820,7 +1477,7 @@ function schedulePendingWatcher(paths, env = process.env, seam = {}) {
820
1477
  const live = records.find((row) => row.watcherPid && pidAlive(row.watcherPid));
821
1478
  if (live) return { scheduled: false, reason: `watcher pid ${live.watcherPid} is already waiting` };
822
1479
  const hubDist = seam.hubDist ?? hubDistPath(env);
823
- if (!hubDist || !(0, import_node_fs4.existsSync)(hubDist)) return { scheduled: false, reason: "the installed hub bundle is unresolvable \u2014 the hourly tick remains the retry" };
1480
+ if (!hubDist || !(0, import_node_fs8.existsSync)(hubDist)) return { scheduled: false, reason: "the installed hub bundle is unresolvable \u2014 the hourly tick remains the retry" };
824
1481
  const command = buildWatcherCommand({
825
1482
  images: [...new Set(records.flatMap((row) => row.images))],
826
1483
  dirs: [...new Set(records.flatMap((row) => row.dirs))],
@@ -880,7 +1537,7 @@ function tail(output) {
880
1537
  function manifestVersion(packageRoot) {
881
1538
  if (!packageRoot) return null;
882
1539
  try {
883
- const version = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(packageRoot, "package.json"), "utf8")).version;
1540
+ const version = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path4.join)(packageRoot, "package.json"), "utf8")).version;
884
1541
  return typeof version === "string" ? version : null;
885
1542
  } catch {
886
1543
  return null;
@@ -891,8 +1548,8 @@ function stagePackage(options) {
891
1548
  const stageRoot = (0, import_node_path4.join)(paths.staging, surface, target);
892
1549
  const packageRoot = (0, import_node_path4.join)(stageRoot, "node_modules", ...packageName.split("/"));
893
1550
  if (!fileExists((0, import_node_path4.join)(packageRoot, ...witness.split("/")))) {
894
- (0, import_node_fs5.mkdirSync)(stageRoot, { recursive: true });
895
- (0, import_node_fs5.writeFileSync)((0, import_node_path4.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
1551
+ (0, import_node_fs9.mkdirSync)(stageRoot, { recursive: true });
1552
+ (0, import_node_fs9.writeFileSync)((0, import_node_path4.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
896
1553
  const install = runNpm(["install", "--prefix", stageRoot, "--no-audit", "--no-fund", "--no-package-lock", `${packageName}@${target}`], env);
897
1554
  if (install.status !== 0) {
898
1555
  return { defer: `staging install failed: ${tail(install.stderr || install.stdout)}` };
@@ -900,7 +1557,7 @@ function stagePackage(options) {
900
1557
  }
901
1558
  let stagedManifestVersion;
902
1559
  try {
903
- stagedManifestVersion = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(packageRoot, "package.json"), "utf8")).version;
1560
+ stagedManifestVersion = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path4.join)(packageRoot, "package.json"), "utf8")).version;
904
1561
  } catch {
905
1562
  }
906
1563
  if (stagedManifestVersion !== target) {
@@ -958,11 +1615,11 @@ function npmGlobalArm(options) {
958
1615
  }
959
1616
 
960
1617
  // src/launch-staging.ts
961
- var import_node_fs6 = require("node:fs");
1618
+ var import_node_fs10 = require("node:fs");
962
1619
  var import_node_path5 = require("node:path");
963
1620
  function readLaunchTarget(path) {
964
1621
  try {
965
- const parsed = JSON.parse((0, import_node_fs6.readFileSync)(path, "utf8"));
1622
+ const parsed = JSON.parse((0, import_node_fs10.readFileSync)(path, "utf8"));
966
1623
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
967
1624
  const row = parsed;
968
1625
  if (typeof row.surface !== "string" || typeof row.target !== "string" || typeof row.since !== "string" || typeof row.incoming !== "string") return null;
@@ -972,15 +1629,15 @@ function readLaunchTarget(path) {
972
1629
  }
973
1630
  }
974
1631
  function writeLaunchTarget(path, record) {
975
- (0, import_node_fs6.mkdirSync)((0, import_node_path5.join)(path, ".."), { recursive: true });
1632
+ (0, import_node_fs10.mkdirSync)((0, import_node_path5.join)(path, ".."), { recursive: true });
976
1633
  const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
977
- (0, import_node_fs6.writeFileSync)(tmp, `${JSON.stringify(record, null, 2)}
1634
+ (0, import_node_fs10.writeFileSync)(tmp, `${JSON.stringify(record, null, 2)}
978
1635
  `, "utf8");
979
- (0, import_node_fs6.renameSync)(tmp, path);
1636
+ (0, import_node_fs10.renameSync)(tmp, path);
980
1637
  }
981
1638
  function clearLaunchTarget(path) {
982
1639
  try {
983
- (0, import_node_fs6.rmSync)(path, { force: true });
1640
+ (0, import_node_fs10.rmSync)(path, { force: true });
984
1641
  } catch {
985
1642
  }
986
1643
  }
@@ -1001,7 +1658,7 @@ function noteLaunchStaged(paths, env, options, seam = {}) {
1001
1658
  }
1002
1659
  function probeLaunchTargetVersion(launchTargetPath, versionAt) {
1003
1660
  const record = readLaunchTarget(launchTargetPath);
1004
- if (!record || !(0, import_node_fs6.existsSync)(record.incoming)) return null;
1661
+ if (!record || !(0, import_node_fs10.existsSync)(record.incoming)) return null;
1005
1662
  const version = versionAt(record.incoming);
1006
1663
  return version === record.target ? record.target : null;
1007
1664
  }
@@ -1059,7 +1716,7 @@ function ensureClaudeMarketplace(env) {
1059
1716
  const path = claudeMarketplacesPath(env);
1060
1717
  let body;
1061
1718
  try {
1062
- body = JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8"));
1719
+ body = JSON.parse((0, import_node_fs11.readFileSync)(path, "utf8"));
1063
1720
  } catch (error) {
1064
1721
  if (error?.code === "ENOENT") return { ok: false, detail: `${path} does not exist \u2014 mutmutco marketplace is not registered`, unregistered: true };
1065
1722
  return { ok: false, detail: `cannot read ${path}` };
@@ -1087,7 +1744,7 @@ function claudePluginsRoot(env) {
1087
1744
  }
1088
1745
  function claudeMarkerVersion(root) {
1089
1746
  try {
1090
- const version = JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path6.join)(root, ...CLAUDE_MANIFEST.split("/")), "utf8")).version;
1747
+ const version = JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path6.join)(root, ...CLAUDE_MANIFEST.split("/")), "utf8")).version;
1091
1748
  return typeof version === "string" ? version : null;
1092
1749
  } catch {
1093
1750
  return null;
@@ -1184,14 +1841,14 @@ function claudeArm(options) {
1184
1841
  return result(from, target, "defer", `staged Claude manifest is ${claudeMarkerVersion(staged.packageRoot) ?? "unreadable"} or its tree is incomplete (expected ${target})`);
1185
1842
  }
1186
1843
  try {
1187
- (0, import_node_fs7.mkdirSync)((0, import_node_path6.join)(pluginsRoot, "staging"), { recursive: true });
1188
- (0, import_node_fs7.rmSync)(incomingRoot, { recursive: true, force: true });
1189
- (0, import_node_fs7.cpSync)(staged.packageRoot, incoming, { recursive: true });
1844
+ (0, import_node_fs11.mkdirSync)((0, import_node_path6.join)(pluginsRoot, "staging"), { recursive: true });
1845
+ (0, import_node_fs11.rmSync)(incomingRoot, { recursive: true, force: true });
1846
+ (0, import_node_fs11.cpSync)(staged.packageRoot, incoming, { recursive: true });
1190
1847
  } catch (error) {
1191
1848
  return result(from, target, "defer", `cannot place incoming Claude generation: ${error?.message ?? error}`);
1192
1849
  }
1193
1850
  if (claudeMarkerVersion(incoming) !== target || !claudeTreeHealthy(incoming)) {
1194
- (0, import_node_fs7.rmSync)(incomingRoot, { recursive: true, force: true });
1851
+ (0, import_node_fs11.rmSync)(incomingRoot, { recursive: true, force: true });
1195
1852
  return result(from, target, "defer", "incoming Claude generation did not reproduce the verified payload");
1196
1853
  }
1197
1854
  }
@@ -1223,10 +1880,10 @@ function claudeArm(options) {
1223
1880
  }
1224
1881
  const cacheTarget = (0, import_node_path6.join)(pluginsRoot, "cache", "mutmutco", "mmi", target);
1225
1882
  try {
1226
- if (!(0, import_node_fs7.existsSync)(cacheTarget) || claudeMarkerVersion(cacheTarget) !== target || !claudeTreeHealthy(cacheTarget)) {
1227
- (0, import_node_fs7.rmSync)(cacheTarget, { recursive: true, force: true });
1228
- (0, import_node_fs7.mkdirSync)((0, import_node_path6.join)(cacheTarget, ".."), { recursive: true });
1229
- (0, import_node_fs7.cpSync)(incoming, cacheTarget, { recursive: true });
1883
+ if (!(0, import_node_fs11.existsSync)(cacheTarget) || claudeMarkerVersion(cacheTarget) !== target || !claudeTreeHealthy(cacheTarget)) {
1884
+ (0, import_node_fs11.rmSync)(cacheTarget, { recursive: true, force: true });
1885
+ (0, import_node_fs11.mkdirSync)((0, import_node_path6.join)(cacheTarget, ".."), { recursive: true });
1886
+ (0, import_node_fs11.cpSync)(incoming, cacheTarget, { recursive: true });
1230
1887
  }
1231
1888
  } catch (error) {
1232
1889
  return result(from, target, "defer", `cannot populate Claude plugin cache: ${error?.message ?? error}`);
@@ -1254,7 +1911,7 @@ function claudeArm(options) {
1254
1911
  }
1255
1912
  clearLaunchTarget(launchTargetPath);
1256
1913
  try {
1257
- (0, import_node_fs7.rmSync)(incomingRoot, { recursive: true, force: true });
1914
+ (0, import_node_fs11.rmSync)(incomingRoot, { recursive: true, force: true });
1258
1915
  } catch {
1259
1916
  }
1260
1917
  return result(from, target, "ok", `catalog refreshed; converged and verified ${from ?? "absent"} -> ${target}`);
@@ -1367,21 +2024,21 @@ function enumerateSurfaces(env = process.env) {
1367
2024
  { id: "hermes", marker: `${hermesHostCommand(env)} --version` }
1368
2025
  ].map((probe) => ({
1369
2026
  ...probe,
1370
- present: probe.id === "hermes" ? hermesHostPresent(env) : (0, import_node_fs8.existsSync)(probe.marker)
2027
+ present: probe.id === "hermes" ? hermesHostPresent(env) : (0, import_node_fs12.existsSync)(probe.marker)
1371
2028
  }));
1372
2029
  }
1373
2030
 
1374
2031
  // src/arm-cursor.ts
1375
- var import_node_fs11 = require("node:fs");
2032
+ var import_node_fs15 = require("node:fs");
1376
2033
  var import_node_child_process6 = require("node:child_process");
1377
2034
  var import_node_os5 = require("node:os");
1378
2035
  var import_node_path8 = require("node:path");
1379
2036
 
1380
2037
  // src/compat.ts
1381
- var import_node_fs9 = require("node:fs");
2038
+ var import_node_fs13 = require("node:fs");
1382
2039
  function readManifestCompat(manifestPath) {
1383
2040
  try {
1384
- const parsed = JSON.parse((0, import_node_fs9.readFileSync)(manifestPath, "utf8"));
2041
+ const parsed = JSON.parse((0, import_node_fs13.readFileSync)(manifestPath, "utf8"));
1385
2042
  return typeof parsed.mmiCompat === "string" ? parsed.mmiCompat : void 0;
1386
2043
  } catch {
1387
2044
  return void 0;
@@ -1389,7 +2046,7 @@ function readManifestCompat(manifestPath) {
1389
2046
  }
1390
2047
 
1391
2048
  // src/host-doc-lock.ts
1392
- var import_node_fs10 = require("node:fs");
2049
+ var import_node_fs14 = require("node:fs");
1393
2050
  var import_node_os4 = require("node:os");
1394
2051
  var SHARED_DOC_LOCK_STALE_MS = 3e4;
1395
2052
  var SHARED_DOC_LOCK_ATTEMPTS = 50;
@@ -1403,7 +2060,7 @@ function lockContent(options) {
1403
2060
  }
1404
2061
  function readHolder(lockPath) {
1405
2062
  try {
1406
- const parsed = JSON.parse((0, import_node_fs10.readFileSync)(lockPath, "utf8"));
2063
+ const parsed = JSON.parse((0, import_node_fs14.readFileSync)(lockPath, "utf8"));
1407
2064
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
1408
2065
  return parsed;
1409
2066
  } catch {
@@ -1415,7 +2072,7 @@ function lockAgeMs(lockPath, now) {
1415
2072
  const declared = typeof holder?.acquiredAt === "string" ? Date.parse(holder.acquiredAt) : Number.NaN;
1416
2073
  if (!Number.isNaN(declared)) return Math.max(0, now - declared);
1417
2074
  try {
1418
- return Math.max(0, now - (0, import_node_fs10.statSync)(lockPath).mtimeMs);
2075
+ return Math.max(0, now - (0, import_node_fs14.statSync)(lockPath).mtimeMs);
1419
2076
  } catch {
1420
2077
  return 0;
1421
2078
  }
@@ -1438,9 +2095,9 @@ function acquireSharedDocLock(documentPath, options) {
1438
2095
  for (let attempt = 0; attempt < attempts; attempt += 1) {
1439
2096
  let fd;
1440
2097
  try {
1441
- fd = (0, import_node_fs10.openSync)(lockPath, "wx");
1442
- (0, import_node_fs10.writeSync)(fd, content);
1443
- (0, import_node_fs10.closeSync)(fd);
2098
+ fd = (0, import_node_fs14.openSync)(lockPath, "wx");
2099
+ (0, import_node_fs14.writeSync)(fd, content);
2100
+ (0, import_node_fs14.closeSync)(fd);
1444
2101
  return {
1445
2102
  documentPath,
1446
2103
  lockPath,
@@ -1453,11 +2110,11 @@ function acquireSharedDocLock(documentPath, options) {
1453
2110
  } catch (error) {
1454
2111
  if (fd !== void 0) {
1455
2112
  try {
1456
- (0, import_node_fs10.closeSync)(fd);
2113
+ (0, import_node_fs14.closeSync)(fd);
1457
2114
  } catch {
1458
2115
  }
1459
2116
  try {
1460
- (0, import_node_fs10.unlinkSync)(lockPath);
2117
+ (0, import_node_fs14.unlinkSync)(lockPath);
1461
2118
  } catch {
1462
2119
  }
1463
2120
  }
@@ -1466,12 +2123,12 @@ function acquireSharedDocLock(documentPath, options) {
1466
2123
  if (lockAgeMs(lockPath, Date.now()) >= staleAfterMs) {
1467
2124
  const aside = `${lockPath}.stale-${Date.now()}`;
1468
2125
  try {
1469
- (0, import_node_fs10.renameSync)(lockPath, aside);
2126
+ (0, import_node_fs14.renameSync)(lockPath, aside);
1470
2127
  } catch {
1471
2128
  continue;
1472
2129
  }
1473
2130
  try {
1474
- (0, import_node_fs10.rmSync)(aside, { force: true });
2131
+ (0, import_node_fs14.rmSync)(aside, { force: true });
1475
2132
  } catch {
1476
2133
  }
1477
2134
  continue;
@@ -1484,7 +2141,7 @@ function releaseSharedDocLock(handle) {
1484
2141
  try {
1485
2142
  const holder = readHolder(handle.lockPath);
1486
2143
  if (holder && holder.owner === handle.owner && Number(holder.pid) === handle.pid) {
1487
- (0, import_node_fs10.unlinkSync)(handle.lockPath);
2144
+ (0, import_node_fs14.unlinkSync)(handle.lockPath);
1488
2145
  }
1489
2146
  } catch {
1490
2147
  }
@@ -1509,11 +2166,11 @@ function cursorPluginsRoot() {
1509
2166
  return (0, import_node_path8.join)((0, import_node_os5.homedir)(), ".cursor", "plugins");
1510
2167
  }
1511
2168
  function treeHealthy(root) {
1512
- return TREE_FILES.every((rel) => (0, import_node_fs11.existsSync)((0, import_node_path8.join)(root, ...rel.split("/"))));
2169
+ return TREE_FILES.every((rel) => (0, import_node_fs15.existsSync)((0, import_node_path8.join)(root, ...rel.split("/"))));
1513
2170
  }
1514
2171
  function readJson(root, rel) {
1515
2172
  try {
1516
- return JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path8.join)(root, ...rel.split("/")), "utf8"));
2173
+ return JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path8.join)(root, ...rel.split("/")), "utf8"));
1517
2174
  } catch {
1518
2175
  return null;
1519
2176
  }
@@ -1537,7 +2194,7 @@ function message(error) {
1537
2194
  }
1538
2195
  function discard(path) {
1539
2196
  try {
1540
- (0, import_node_fs11.rmSync)(path, { recursive: true, force: true });
2197
+ (0, import_node_fs15.rmSync)(path, { recursive: true, force: true });
1541
2198
  } catch {
1542
2199
  }
1543
2200
  }
@@ -1591,10 +2248,10 @@ var CURSOR_HOOKS_LOCK_OWNER = "mmi-hub:cursor-hooks";
1591
2248
  function reconcileCursorUserHooks(pluginRoot, homeDir, lock = {}) {
1592
2249
  const path = (0, import_node_path8.join)(homeDir, ".cursor", USER_HOOKS_FILE);
1593
2250
  return withSharedDocLock(path, { owner: CURSOR_HOOKS_LOCK_OWNER, ...lock }, () => {
1594
- if (!(0, import_node_fs11.existsSync)(path)) return "current";
2251
+ if (!(0, import_node_fs15.existsSync)(path)) return "current";
1595
2252
  let existing;
1596
2253
  try {
1597
- existing = JSON.parse((0, import_node_fs11.readFileSync)(path, "utf8"));
2254
+ existing = JSON.parse((0, import_node_fs15.readFileSync)(path, "utf8"));
1598
2255
  } catch {
1599
2256
  return "user-conflict";
1600
2257
  }
@@ -1602,8 +2259,8 @@ function reconcileCursorUserHooks(pluginRoot, homeDir, lock = {}) {
1602
2259
  if (!stripped) return "user-conflict";
1603
2260
  if (JSON.stringify(stripped) === JSON.stringify(existing)) return "current";
1604
2261
  const tmp = `${path}.tmp-${process.pid}`;
1605
- (0, import_node_fs11.writeFileSync)(tmp, JSON.stringify(stripped, null, 2) + "\n", "utf8");
1606
- (0, import_node_fs11.renameSync)(tmp, path);
2262
+ (0, import_node_fs15.writeFileSync)(tmp, JSON.stringify(stripped, null, 2) + "\n", "utf8");
2263
+ (0, import_node_fs15.renameSync)(tmp, path);
1607
2264
  return "updated";
1608
2265
  });
1609
2266
  }
@@ -1627,15 +2284,15 @@ function writeRollbackPointer(root, entry) {
1627
2284
  const path = (0, import_node_path8.join)(root, "quarantine", "rollback.json");
1628
2285
  const tmp = `${path}.tmp-${process.pid}`;
1629
2286
  try {
1630
- (0, import_node_fs11.writeFileSync)(tmp, JSON.stringify({ surface: "cursor", ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry }, null, 2) + "\n", "utf8");
1631
- (0, import_node_fs11.renameSync)(tmp, path);
2287
+ (0, import_node_fs15.writeFileSync)(tmp, JSON.stringify({ surface: "cursor", ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry }, null, 2) + "\n", "utf8");
2288
+ (0, import_node_fs15.renameSync)(tmp, path);
1632
2289
  } catch {
1633
2290
  discard(tmp);
1634
2291
  }
1635
2292
  }
1636
2293
  function mtimeMs(path) {
1637
2294
  try {
1638
- return (0, import_node_fs11.statSync)(path).mtimeMs;
2295
+ return (0, import_node_fs15.statSync)(path).mtimeMs;
1639
2296
  } catch {
1640
2297
  return 0;
1641
2298
  }
@@ -1644,13 +2301,13 @@ function pruneQuarantine(root, keep) {
1644
2301
  const dir = (0, import_node_path8.join)(root, "quarantine");
1645
2302
  let generations;
1646
2303
  try {
1647
- generations = (0, import_node_fs11.readdirSync)(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("mmi-")).map((entry) => (0, import_node_path8.join)(dir, entry.name)).sort((a, b) => mtimeMs(b) - mtimeMs(a));
2304
+ generations = (0, import_node_fs15.readdirSync)(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("mmi-")).map((entry) => (0, import_node_path8.join)(dir, entry.name)).sort((a, b) => mtimeMs(b) - mtimeMs(a));
1648
2305
  } catch {
1649
2306
  return 0;
1650
2307
  }
1651
2308
  let stranded = 0;
1652
2309
  try {
1653
- for (const entry of (0, import_node_fs11.readdirSync)(dir, { withFileTypes: true })) {
2310
+ for (const entry of (0, import_node_fs15.readdirSync)(dir, { withFileTypes: true })) {
1654
2311
  if (!entry.isFile() || !/^rollback\.json\.tmp-\d+$/.test(entry.name)) continue;
1655
2312
  const path = (0, import_node_path8.join)(dir, entry.name);
1656
2313
  if (Date.now() - mtimeMs(path) < STRANDED_TMP_AGE_MS) continue;
@@ -1670,16 +2327,16 @@ function pruneQuarantine(root, keep) {
1670
2327
  }
1671
2328
  function restorePrevious(live, quarantined, broken) {
1672
2329
  try {
1673
- (0, import_node_fs11.renameSync)(live, broken);
2330
+ (0, import_node_fs15.renameSync)(live, broken);
1674
2331
  } catch {
1675
2332
  return "kept-broken";
1676
2333
  }
1677
2334
  try {
1678
- (0, import_node_fs11.renameSync)(quarantined, live);
2335
+ (0, import_node_fs15.renameSync)(quarantined, live);
1679
2336
  return "restored";
1680
2337
  } catch {
1681
2338
  try {
1682
- (0, import_node_fs11.renameSync)(broken, live);
2339
+ (0, import_node_fs15.renameSync)(broken, live);
1683
2340
  return "kept-broken";
1684
2341
  } catch {
1685
2342
  return "lost";
@@ -1714,7 +2371,7 @@ function cursorArm(options) {
1714
2371
  if (installed === target && treeHealthy(live)) {
1715
2372
  return result(installed, target, "ok", `already at target ${target}${userHooksDetail(live, (0, import_node_os5.homedir)())}`);
1716
2373
  }
1717
- if ((0, import_node_fs11.existsSync)(live) && !mmiOwned(live)) {
2374
+ if ((0, import_node_fs15.existsSync)(live) && !mmiOwned(live)) {
1718
2375
  return result(installed, target, "defer", `${live} carries no MMI ownership marker (plugin manifest name, or a ${PACKAGE} package.json) \u2014 refusing to displace an unmanaged directory (mmi-cli plugin heal adopts it)`);
1719
2376
  }
1720
2377
  if (dryRun) {
@@ -1732,12 +2389,12 @@ function cursorArm(options) {
1732
2389
  const priorLaunch = readLaunchTarget(launchTargetPath);
1733
2390
  let incoming = priorLaunch?.target === target && priorLaunch.incoming.startsWith((0, import_node_path8.join)(root, "staging")) && manifest(priorLaunch.incoming)?.version === target && treeHealthy(priorLaunch.incoming) ? priorLaunch.incoming : (0, import_node_path8.join)(root, "staging", `mmi-${safeSegment(target)}-${Date.now()}`);
1734
2391
  const reuseIncoming = incoming === priorLaunch?.incoming;
1735
- if (!reuseIncoming || !(0, import_node_fs11.existsSync)(incoming)) {
2392
+ if (!reuseIncoming || !(0, import_node_fs15.existsSync)(incoming)) {
1736
2393
  incoming = (0, import_node_path8.join)(root, "staging", `mmi-${safeSegment(target)}-${Date.now()}`);
1737
- for (const dir of ["local", "staging", "quarantine"]) (0, import_node_fs11.mkdirSync)((0, import_node_path8.join)(root, dir), { recursive: true });
2394
+ for (const dir of ["local", "staging", "quarantine"]) (0, import_node_fs15.mkdirSync)((0, import_node_path8.join)(root, dir), { recursive: true });
1738
2395
  try {
1739
2396
  discard(incoming);
1740
- (0, import_node_fs11.cpSync)(staged.packageRoot, incoming, { recursive: true });
2397
+ (0, import_node_fs15.cpSync)(staged.packageRoot, incoming, { recursive: true });
1741
2398
  } catch (error) {
1742
2399
  discard(incoming);
1743
2400
  return result(installed, target, "defer", `copy into ${root} failed: ${message(error)}`);
@@ -1761,7 +2418,7 @@ function cursorArm(options) {
1761
2418
  }
1762
2419
  const stamp = Date.now();
1763
2420
  const quarantined = (0, import_node_path8.join)(root, "quarantine", `mmi-${safeSegment(installed ?? "unknown")}-${stamp}`);
1764
- for (const dir of ["local", "staging", "quarantine"]) (0, import_node_fs11.mkdirSync)((0, import_node_path8.join)(root, dir), { recursive: true });
2421
+ for (const dir of ["local", "staging", "quarantine"]) (0, import_node_fs15.mkdirSync)((0, import_node_path8.join)(root, dir), { recursive: true });
1765
2422
  const started = cursorHostEvidence(env);
1766
2423
  if (started) {
1767
2424
  const pending = noteLaunchStaged(paths, env, {
@@ -1776,15 +2433,15 @@ function cursorArm(options) {
1776
2433
  }
1777
2434
  let displaced = false;
1778
2435
  try {
1779
- if ((0, import_node_fs11.existsSync)(live)) {
1780
- (0, import_node_fs11.renameSync)(live, quarantined);
2436
+ if ((0, import_node_fs15.existsSync)(live)) {
2437
+ (0, import_node_fs15.renameSync)(live, quarantined);
1781
2438
  displaced = true;
1782
2439
  }
1783
- (0, import_node_fs11.renameSync)(incoming, live);
2440
+ (0, import_node_fs15.renameSync)(incoming, live);
1784
2441
  } catch (error) {
1785
- if (displaced && !(0, import_node_fs11.existsSync)(live)) {
2442
+ if (displaced && !(0, import_node_fs15.existsSync)(live)) {
1786
2443
  try {
1787
- (0, import_node_fs11.renameSync)(quarantined, live);
2444
+ (0, import_node_fs15.renameSync)(quarantined, live);
1788
2445
  } catch {
1789
2446
  }
1790
2447
  }
@@ -1812,7 +2469,7 @@ function probeCursorLaunchStaging() {
1812
2469
  }
1813
2470
 
1814
2471
  // src/arm-jervcode.ts
1815
- var import_node_fs12 = require("node:fs");
2472
+ var import_node_fs16 = require("node:fs");
1816
2473
  var import_node_path9 = require("node:path");
1817
2474
  var PI_PACKAGE = "@mutmutco/pi-plugin";
1818
2475
  function hostEnv(env) {
@@ -1824,7 +2481,7 @@ function hostCommands(_env) {
1824
2481
  }
1825
2482
  function readPackageSpecs(env) {
1826
2483
  try {
1827
- const settings = JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path9.join)(jervcodeAgentDir(env), "settings.json"), "utf8"));
2484
+ const settings = JSON.parse((0, import_node_fs16.readFileSync)((0, import_node_path9.join)(jervcodeAgentDir(env), "settings.json"), "utf8"));
1828
2485
  return Array.isArray(settings.packages) ? settings.packages.filter((entry) => typeof entry === "string") : [];
1829
2486
  } catch {
1830
2487
  return null;
@@ -1832,7 +2489,7 @@ function readPackageSpecs(env) {
1832
2489
  }
1833
2490
  function installedVersion(env) {
1834
2491
  try {
1835
- const manifest2 = JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json"), "utf8"));
2492
+ const manifest2 = JSON.parse((0, import_node_fs16.readFileSync)((0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json"), "utf8"));
1836
2493
  return typeof manifest2.version === "string" ? manifest2.version : null;
1837
2494
  } catch {
1838
2495
  return null;
@@ -1952,7 +2609,7 @@ function jervcodeArm(options) {
1952
2609
  }
1953
2610
 
1954
2611
  // src/arm-hermes.ts
1955
- var import_node_fs13 = require("node:fs");
2612
+ var import_node_fs17 = require("node:fs");
1956
2613
  var import_node_child_process7 = require("node:child_process");
1957
2614
  var import_node_path10 = require("node:path");
1958
2615
  var PACKAGE2 = "@mutmutco/hermes-plugin";
@@ -1960,7 +2617,7 @@ var MANIFEST2 = "plugin.yaml";
1960
2617
  var TREE_MARKERS_FALLBACK = [MANIFEST2, "__init__.py", "skills"];
1961
2618
  function yamlVersion(root) {
1962
2619
  try {
1963
- const text = (0, import_node_fs13.readFileSync)((0, import_node_path10.join)(root, MANIFEST2), "utf8");
2620
+ const text = (0, import_node_fs17.readFileSync)((0, import_node_path10.join)(root, MANIFEST2), "utf8");
1964
2621
  try {
1965
2622
  const version = JSON.parse(text).version;
1966
2623
  if (typeof version === "string" && version.trim()) return version.trim();
@@ -1974,7 +2631,7 @@ function yamlVersion(root) {
1974
2631
  }
1975
2632
  function packageTreeMarkers(root) {
1976
2633
  try {
1977
- const files = JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path10.join)(root, "package.json"), "utf8")).files;
2634
+ const files = JSON.parse((0, import_node_fs17.readFileSync)((0, import_node_path10.join)(root, "package.json"), "utf8")).files;
1978
2635
  if (Array.isArray(files) && files.length && files.every((entry) => typeof entry === "string" && entry.trim())) {
1979
2636
  return files.map((entry) => entry.trim());
1980
2637
  }
@@ -1984,9 +2641,9 @@ function packageTreeMarkers(root) {
1984
2641
  }
1985
2642
  function markerPresent(root, rel) {
1986
2643
  const path = (0, import_node_path10.join)(root, ...rel.split("/"));
1987
- if (!(0, import_node_fs13.existsSync)(path)) return false;
2644
+ if (!(0, import_node_fs17.existsSync)(path)) return false;
1988
2645
  try {
1989
- const stat = (0, import_node_fs13.statSync)(path);
2646
+ const stat = (0, import_node_fs17.statSync)(path);
1990
2647
  return stat.isFile() || stat.isDirectory();
1991
2648
  } catch {
1992
2649
  return false;
@@ -2005,7 +2662,7 @@ function treeIncompleteDetail(root, label) {
2005
2662
  }
2006
2663
  function packageIdentity(root) {
2007
2664
  try {
2008
- return JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path10.join)(root, "package.json"), "utf8")).name === PACKAGE2;
2665
+ return JSON.parse((0, import_node_fs17.readFileSync)((0, import_node_path10.join)(root, "package.json"), "utf8")).name === PACKAGE2;
2009
2666
  } catch {
2010
2667
  return false;
2011
2668
  }
@@ -2020,7 +2677,7 @@ function mmiOwned2(root) {
2020
2677
  }
2021
2678
  function discard2(path) {
2022
2679
  try {
2023
- (0, import_node_fs13.rmSync)(path, { recursive: true, force: true });
2680
+ (0, import_node_fs17.rmSync)(path, { recursive: true, force: true });
2024
2681
  } catch {
2025
2682
  }
2026
2683
  }
@@ -2056,15 +2713,15 @@ function materializeHermesSkills(home) {
2056
2713
  const skillsRoot = (0, import_node_path10.join)(home, "skills");
2057
2714
  const targetRoot = (0, import_node_path10.join)(skillsRoot, "mmi");
2058
2715
  try {
2059
- if (!(0, import_node_fs13.existsSync)(pluginSkills)) return { ok: false, detail: `plugin skills tree missing at ${pluginSkills}` };
2060
- const names = (0, import_node_fs13.readdirSync)(pluginSkills, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_")).map((entry) => entry.name);
2716
+ if (!(0, import_node_fs17.existsSync)(pluginSkills)) return { ok: false, detail: `plugin skills tree missing at ${pluginSkills}` };
2717
+ const names = (0, import_node_fs17.readdirSync)(pluginSkills, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_")).map((entry) => entry.name);
2061
2718
  if (!names.length) return { ok: false, detail: `plugin skills tree is empty at ${pluginSkills}` };
2062
- (0, import_node_fs13.mkdirSync)(skillsRoot, { recursive: true });
2719
+ (0, import_node_fs17.mkdirSync)(skillsRoot, { recursive: true });
2063
2720
  const incoming = (0, import_node_path10.join)(skillsRoot, ".mmi-incoming");
2064
2721
  discard2(incoming);
2065
- (0, import_node_fs13.cpSync)(pluginSkills, incoming, { recursive: true });
2722
+ (0, import_node_fs17.cpSync)(pluginSkills, incoming, { recursive: true });
2066
2723
  discard2(targetRoot);
2067
- (0, import_node_fs13.renameSync)(incoming, targetRoot);
2724
+ (0, import_node_fs17.renameSync)(incoming, targetRoot);
2068
2725
  return { ok: true, detail: `provisioned ${names.length} skills to ${targetRoot}` };
2069
2726
  } catch (error) {
2070
2727
  return { ok: false, detail: message2(error) };
@@ -2086,7 +2743,7 @@ function hermesHomes(env = process.env) {
2086
2743
  const root = hermesHome(env);
2087
2744
  const homes = [root];
2088
2745
  try {
2089
- for (const entry of (0, import_node_fs13.readdirSync)((0, import_node_path10.join)(root, "profiles"), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
2746
+ for (const entry of (0, import_node_fs17.readdirSync)((0, import_node_path10.join)(root, "profiles"), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
2090
2747
  if (entry.isDirectory() && !entry.name.startsWith(".")) homes.push((0, import_node_path10.join)(root, "profiles", entry.name));
2091
2748
  }
2092
2749
  } catch {
@@ -2135,7 +2792,7 @@ function armHermesHome(home, env, options, primary) {
2135
2792
  detail,
2136
2793
  launchStaged: launchStaged || void 0
2137
2794
  });
2138
- if ((0, import_node_fs13.existsSync)(live) && !mmiOwned2(live)) {
2795
+ if ((0, import_node_fs17.existsSync)(live) && !mmiOwned2(live)) {
2139
2796
  return result(installed, options.target, "defer", `${live} carries no ${PACKAGE2} package identity \u2014 refusing to displace an unmanaged Hermes mmi plugin`);
2140
2797
  }
2141
2798
  if (installed && compareSemver(installed, options.target) > 0) return result(installed, installed, "skip", `installed ${installed} is above candidate ${options.target} (monotonic)`);
@@ -2167,10 +2824,10 @@ function armHermesHome(home, env, options, primary) {
2167
2824
  const reuseIncoming = priorLaunch?.target === options.target && priorLaunch.incoming === incoming && yamlVersion(incoming) === options.target && treeHealthy2(incoming);
2168
2825
  if (!reuseIncoming) {
2169
2826
  try {
2170
- (0, import_node_fs13.mkdirSync)(parent, { recursive: true });
2171
- (0, import_node_fs13.mkdirSync)((0, import_node_path10.join)(scratch, "quarantine"), { recursive: true });
2827
+ (0, import_node_fs17.mkdirSync)(parent, { recursive: true });
2828
+ (0, import_node_fs17.mkdirSync)((0, import_node_path10.join)(scratch, "quarantine"), { recursive: true });
2172
2829
  discard2(incomingRoot);
2173
- (0, import_node_fs13.cpSync)(staged.packageRoot, incoming, { recursive: true });
2830
+ (0, import_node_fs17.cpSync)(staged.packageRoot, incoming, { recursive: true });
2174
2831
  } catch (error) {
2175
2832
  return result(installed, options.target, "defer", `cannot place incoming Hermes generation: ${message2(error)}`);
2176
2833
  }
@@ -2185,7 +2842,7 @@ function armHermesHome(home, env, options, primary) {
2185
2842
  return result(installed, options.target, "defer", incomingTree);
2186
2843
  }
2187
2844
  }
2188
- const displaced = (0, import_node_fs13.existsSync)(live);
2845
+ const displaced = (0, import_node_fs17.existsSync)(live);
2189
2846
  const busy = displaced ? hermesHostEvidence(env, home, options.processListProvider) : null;
2190
2847
  if (busy) {
2191
2848
  const pending = noteLaunchStaged(options.paths, env, {
@@ -2200,11 +2857,11 @@ function armHermesHome(home, env, options, primary) {
2200
2857
  return result(installed, options.target, "ok", `staged ${options.target} for next launch while Hermes is running (${busy}) \u2014 ${pending}`, true);
2201
2858
  }
2202
2859
  try {
2203
- if (displaced) (0, import_node_fs13.renameSync)(live, quarantine);
2204
- (0, import_node_fs13.renameSync)(incoming, live);
2860
+ if (displaced) (0, import_node_fs17.renameSync)(live, quarantine);
2861
+ (0, import_node_fs17.renameSync)(incoming, live);
2205
2862
  } catch (error) {
2206
- if (displaced && !(0, import_node_fs13.existsSync)(live)) try {
2207
- (0, import_node_fs13.renameSync)(quarantine, live);
2863
+ if (displaced && !(0, import_node_fs17.existsSync)(live)) try {
2864
+ (0, import_node_fs17.renameSync)(quarantine, live);
2208
2865
  } catch {
2209
2866
  }
2210
2867
  discard2(incomingRoot);
@@ -2218,9 +2875,9 @@ function armHermesHome(home, env, options, primary) {
2218
2875
  const broken = (0, import_node_path10.join)(scratch, "rejected", `${options.target}-${Date.now()}`);
2219
2876
  const verifyDetail = liveVersion !== options.target ? `manifest is ${liveVersion ?? "unreadable"}, expected ${options.target}` : liveTree;
2220
2877
  try {
2221
- (0, import_node_fs13.mkdirSync)((0, import_node_path10.join)(scratch, "rejected"), { recursive: true });
2222
- (0, import_node_fs13.renameSync)(live, broken);
2223
- if (displaced) (0, import_node_fs13.renameSync)(quarantine, live);
2878
+ (0, import_node_fs17.mkdirSync)((0, import_node_path10.join)(scratch, "rejected"), { recursive: true });
2879
+ (0, import_node_fs17.renameSync)(live, broken);
2880
+ if (displaced) (0, import_node_fs17.renameSync)(quarantine, live);
2224
2881
  } catch (error) {
2225
2882
  return result(installed, liveVersion ?? "unknown", "fail", `post-switch Hermes verification failed and recovery failed: ${message2(error)}`);
2226
2883
  }
@@ -2239,7 +2896,7 @@ function probeHermesLaunchStaging(env = process.env) {
2239
2896
  }
2240
2897
 
2241
2898
  // src/reap.ts
2242
- var import_node_fs14 = require("node:fs");
2899
+ var import_node_fs18 = require("node:fs");
2243
2900
  var import_node_path11 = require("node:path");
2244
2901
  var import_node_perf_hooks = require("node:perf_hooks");
2245
2902
  function elapsedMs(startedAt) {
@@ -2249,7 +2906,7 @@ var KEEP_GENERATIONS2 = 2;
2249
2906
  var SCRATCH_AGE_MS = 24 * 60 * 6e4;
2250
2907
  function listDirs(path) {
2251
2908
  try {
2252
- return (0, import_node_fs14.readdirSync)(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
2909
+ return (0, import_node_fs18.readdirSync)(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
2253
2910
  } catch {
2254
2911
  return [];
2255
2912
  }
@@ -2270,7 +2927,7 @@ function pruneGenerations(root, category, dryRun) {
2270
2927
  continue;
2271
2928
  }
2272
2929
  try {
2273
- (0, import_node_fs14.rmSync)((0, import_node_path11.join)(set, version), { recursive: true, force: true });
2930
+ (0, import_node_fs18.rmSync)((0, import_node_path11.join)(set, version), { recursive: true, force: true });
2274
2931
  reaped += 1;
2275
2932
  } catch (error) {
2276
2933
  failures.push(`${version}: ${error?.code ?? error?.message ?? error}`);
@@ -2303,7 +2960,7 @@ function reapScratch(context, dryRun) {
2303
2960
  for (const [dir, patterns] of scanned) {
2304
2961
  let entries;
2305
2962
  try {
2306
- entries = (0, import_node_fs14.readdirSync)(dir, { withFileTypes: true });
2963
+ entries = (0, import_node_fs18.readdirSync)(dir, { withFileTypes: true });
2307
2964
  } catch {
2308
2965
  unreadable.push(dir);
2309
2966
  continue;
@@ -2314,7 +2971,7 @@ function reapScratch(context, dryRun) {
2314
2971
  const path = (0, import_node_path11.join)(dir, entry.name);
2315
2972
  let ageMs;
2316
2973
  try {
2317
- ageMs = now - (0, import_node_fs14.statSync)(path).mtimeMs;
2974
+ ageMs = now - (0, import_node_fs18.statSync)(path).mtimeMs;
2318
2975
  } catch {
2319
2976
  continue;
2320
2977
  }
@@ -2324,7 +2981,7 @@ function reapScratch(context, dryRun) {
2324
2981
  continue;
2325
2982
  }
2326
2983
  try {
2327
- (0, import_node_fs14.rmSync)(path, { force: true });
2984
+ (0, import_node_fs18.rmSync)(path, { force: true });
2328
2985
  reaped += 1;
2329
2986
  } catch (error) {
2330
2987
  failures.push(`${entry.name}: ${error?.code ?? error?.message ?? error}`);
@@ -2381,7 +3038,7 @@ async function reconcile(options) {
2381
3038
  const recordArm = (arm, durationMs) => {
2382
3039
  if (arm.verdict === "fail") arm.repeatFailure = journalSurfaceRepeatedFail(paths.journalPath, arm.surface, run);
2383
3040
  journal({ kind: "arm", surface: arm.surface, from: arm.from, to: arm.to, verdict: arm.verdict, detail: arm.detail, compat: arm.compat, durationMs });
2384
- options.onArm?.(arm);
3041
+ options.onArm?.({ ...arm, ...durationMs === void 0 ? {} : { durationMs } });
2385
3042
  if (arm.verdict === "ok" && !options.dryRun && !arm.launchStaged) clearPending(paths, arm.surface);
2386
3043
  if (arm.verdict === "defer") summary.exit = Math.max(summary.exit, 2);
2387
3044
  if (arm.verdict === "fail") summary.exit = Math.max(summary.exit, 3);
@@ -2546,7 +3203,7 @@ async function reconcile(options) {
2546
3203
 
2547
3204
  // src/scheduler.ts
2548
3205
  var import_node_child_process8 = require("node:child_process");
2549
- var import_node_fs15 = require("node:fs");
3206
+ var import_node_fs19 = require("node:fs");
2550
3207
  var import_node_os6 = require("node:os");
2551
3208
  var import_node_path12 = require("node:path");
2552
3209
  var TASK_NAME = "MMI Fleet Updater";
@@ -2706,7 +3363,7 @@ WantedBy=timers.target
2706
3363
  }
2707
3364
  function readFileOrNull(path) {
2708
3365
  try {
2709
- return (0, import_node_fs15.readFileSync)(path, "utf8");
3366
+ return (0, import_node_fs19.readFileSync)(path, "utf8");
2710
3367
  } catch {
2711
3368
  return null;
2712
3369
  }
@@ -2758,22 +3415,22 @@ function windowsSchedulerStatus(env) {
2758
3415
  const launcherPath = (0, import_node_path12.join)(statePaths(env).root, LAUNCHER_FILE);
2759
3416
  const info = taskRunInfo();
2760
3417
  const runInfo = info ?? EMPTY_RUN_INFO;
2761
- const fail = (detail) => ({ ok: false, supported: true, enabled: true, ...runInfo, detail });
3418
+ const fail2 = (detail) => ({ ok: false, supported: true, enabled: true, ...runInfo, detail });
2762
3419
  if (!query.stdout.includes(launcherPath)) {
2763
- return fail(`"${TASK_NAME}" action does not run the hidden launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
3420
+ return fail2(`"${TASK_NAME}" action does not run the hidden launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
2764
3421
  }
2765
3422
  const launcher = readFileOrNull(launcherPath);
2766
3423
  if (hubDist && !launcher?.includes(hubDist)) {
2767
- return fail(`"${TASK_NAME}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
3424
+ return fail2(`"${TASK_NAME}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
2768
3425
  }
2769
3426
  if (query.stdout.includes("<LogonTrigger>")) {
2770
- return fail(`"${TASK_NAME}" still carries the dormant logon-only trigger \u2014 run \`mmi-hub autoupdate on\``);
3427
+ return fail2(`"${TASK_NAME}" still carries the dormant logon-only trigger \u2014 run \`mmi-hub autoupdate on\``);
2771
3428
  }
2772
3429
  if (!query.stdout.includes("<TimeTrigger>") || !query.stdout.includes(`<Interval>${REPEAT_INTERVAL}</Interval>`)) {
2773
- return fail(`"${TASK_NAME}" has no ${REPEAT_INTERVAL}-repeating TimeTrigger \u2014 run \`mmi-hub autoupdate on\``);
3430
+ return fail2(`"${TASK_NAME}" has no ${REPEAT_INTERVAL}-repeating TimeTrigger \u2014 run \`mmi-hub autoupdate on\``);
2774
3431
  }
2775
3432
  if (info && neverRan(info) && !info.nextRunTime) {
2776
- return fail(`"${TASK_NAME}" has never run and has no next run time \u2014 run \`mmi-hub autoupdate on\``);
3433
+ return fail2(`"${TASK_NAME}" has never run and has no next run time \u2014 run \`mmi-hub autoupdate on\``);
2777
3434
  }
2778
3435
  const points = `points at ${hubDist ?? "the registered Hub dist"}`;
2779
3436
  const ran = !info ? "run history unreadable" : neverRan(info) ? "never run yet" : `last run ${info.lastRunTime} \u2192 exit ${info.lastTaskResult ?? "unknown"}`;
@@ -2787,20 +3444,20 @@ function darwinSchedulerStatus(env) {
2787
3444
  }
2788
3445
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2789
3446
  const launcherPath = (0, import_node_path12.join)(statePaths(env).root, POSIX_LAUNCHER_FILE);
2790
- const fail = (detail) => ({ ok: false, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail });
3447
+ const fail2 = (detail) => ({ ok: false, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail });
2791
3448
  if (!plist.includes(launcherPath)) {
2792
- return fail(`"${LAUNCHD_LABEL}" does not run the reconcile launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
3449
+ return fail2(`"${LAUNCHD_LABEL}" does not run the reconcile launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
2793
3450
  }
2794
3451
  const launcher = readFileOrNull(launcherPath);
2795
3452
  if (hubDist && !launcher?.includes(hubDist)) {
2796
- return fail(`"${LAUNCHD_LABEL}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
3453
+ return fail2(`"${LAUNCHD_LABEL}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
2797
3454
  }
2798
3455
  if (!plist.includes(`<integer>${STARTINTERVAL_SECONDS}</integer>`)) {
2799
- return fail(`"${LAUNCHD_LABEL}" has no ${STARTINTERVAL_SECONDS}s StartInterval \u2014 run \`mmi-hub autoupdate on\``);
3456
+ return fail2(`"${LAUNCHD_LABEL}" has no ${STARTINTERVAL_SECONDS}s StartInterval \u2014 run \`mmi-hub autoupdate on\``);
2800
3457
  }
2801
3458
  const loaded = launchctl(["print", `gui/${uid()}/${LAUNCHD_LABEL}`]);
2802
3459
  if (loaded.status !== 0) {
2803
- return fail(`"${LAUNCHD_LABEL}" is registered but not loaded \u2014 run \`mmi-hub autoupdate on\``);
3460
+ return fail2(`"${LAUNCHD_LABEL}" is registered but not loaded \u2014 run \`mmi-hub autoupdate on\``);
2804
3461
  }
2805
3462
  const points = `points at ${hubDist ?? "the registered Hub dist"}`;
2806
3463
  return { ok: true, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail: `"${LAUNCHD_LABEL}" registered ${POSIX_CADENCE}; ${points}` };
@@ -2815,20 +3472,20 @@ function linuxSchedulerStatus(env) {
2815
3472
  }
2816
3473
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2817
3474
  const launcherPath = (0, import_node_path12.join)(statePaths(env).root, POSIX_LAUNCHER_FILE);
2818
- const fail = (detail) => ({ ok: false, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail });
3475
+ const fail2 = (detail) => ({ ok: false, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail });
2819
3476
  if (!service.includes(launcherPath)) {
2820
- return fail(`"${SYSTEMD_SERVICE_NAME}" does not run the reconcile launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
3477
+ return fail2(`"${SYSTEMD_SERVICE_NAME}" does not run the reconcile launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
2821
3478
  }
2822
3479
  const launcher = readFileOrNull(launcherPath);
2823
3480
  if (hubDist && !launcher?.includes(hubDist)) {
2824
- return fail(`"${SYSTEMD_SERVICE_NAME}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
3481
+ return fail2(`"${SYSTEMD_SERVICE_NAME}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
2825
3482
  }
2826
3483
  if (!timer.includes(`OnUnitActiveSec=${STARTINTERVAL_SECONDS}`)) {
2827
- return fail(`"${SYSTEMD_TIMER_NAME}" has no ${STARTINTERVAL_SECONDS}s OnUnitActiveSec \u2014 run \`mmi-hub autoupdate on\``);
3484
+ return fail2(`"${SYSTEMD_TIMER_NAME}" has no ${STARTINTERVAL_SECONDS}s OnUnitActiveSec \u2014 run \`mmi-hub autoupdate on\``);
2828
3485
  }
2829
3486
  const enabled = systemctlUser(["is-enabled", SYSTEMD_TIMER_NAME]);
2830
3487
  if (enabled.status !== 0) {
2831
- return fail(`"${SYSTEMD_TIMER_NAME}" is registered but not enabled \u2014 run \`mmi-hub autoupdate on\``);
3488
+ return fail2(`"${SYSTEMD_TIMER_NAME}" is registered but not enabled \u2014 run \`mmi-hub autoupdate on\``);
2832
3489
  }
2833
3490
  const points = `points at ${hubDist ?? "the registered Hub dist"}`;
2834
3491
  return { ok: true, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail: `"${SYSTEMD_TIMER_NAME}" registered ${POSIX_CADENCE}, ${RUN_BOUND_SECONDS}s bound; ${points}` };
@@ -2854,9 +3511,9 @@ function installWindowsTask(env) {
2854
3511
  const userId = `${env.USERDOMAIN || env.COMPUTERNAME || ""}\\${env.USERNAME || ""}`.replace(/^\\/, "");
2855
3512
  if (!env.USERNAME) return { ok: false, detail: "cannot resolve the registering user (USERNAME unset) \u2014 the task principal must be explicit" };
2856
3513
  const launcherPath = (0, import_node_path12.join)(paths.root, LAUNCHER_FILE);
2857
- (0, import_node_fs15.writeFileSync)(launcherPath, launcherVbs(process.execPath, hubDist), "utf8");
3514
+ (0, import_node_fs19.writeFileSync)(launcherPath, launcherVbs(process.execPath, hubDist), "utf8");
2858
3515
  const xmlPath = (0, import_node_path12.join)(paths.root, "task.xml");
2859
- (0, import_node_fs15.writeFileSync)(xmlPath, "\uFEFF" + taskXml(launcherPath, paths.root, userId, localStartBoundary(/* @__PURE__ */ new Date())), "utf16le");
3516
+ (0, import_node_fs19.writeFileSync)(xmlPath, "\uFEFF" + taskXml(launcherPath, paths.root, userId, localStartBoundary(/* @__PURE__ */ new Date())), "utf16le");
2860
3517
  const create = schtasks(["/create", "/tn", TASK_NAME, "/xml", xmlPath, "/f"]);
2861
3518
  if (create.status !== 0) {
2862
3519
  const denied = /access is denied/i.test(create.stderr + create.stdout);
@@ -2875,8 +3532,8 @@ function refreshPosixLauncher(env) {
2875
3532
  const launcherPath = (0, import_node_path12.join)(paths.root, POSIX_LAUNCHER_FILE);
2876
3533
  const desired = launcherSh(process.execPath, hubDist);
2877
3534
  if (readFileOrNull(launcherPath) === desired) return null;
2878
- (0, import_node_fs15.writeFileSync)(launcherPath, desired, "utf8");
2879
- (0, import_node_fs15.chmodSync)(launcherPath, 493);
3535
+ (0, import_node_fs19.writeFileSync)(launcherPath, desired, "utf8");
3536
+ (0, import_node_fs19.chmodSync)(launcherPath, 493);
2880
3537
  return launcherPath;
2881
3538
  }
2882
3539
  function installLaunchd(env) {
@@ -2889,12 +3546,12 @@ function installLaunchd(env) {
2889
3546
  if (!hubDist) return { ok: false, detail: "cannot resolve the global @mutmutco/hub dist \u2014 run `npm install -g @mutmutco/hub` first" };
2890
3547
  const paths = ensureState(env);
2891
3548
  const launcherPath = (0, import_node_path12.join)(paths.root, POSIX_LAUNCHER_FILE);
2892
- (0, import_node_fs15.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
2893
- (0, import_node_fs15.chmodSync)(launcherPath, 493);
3549
+ (0, import_node_fs19.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
3550
+ (0, import_node_fs19.chmodSync)(launcherPath, 493);
2894
3551
  const agentsDir = (0, import_node_path12.join)((0, import_node_os6.homedir)(), "Library", "LaunchAgents");
2895
- (0, import_node_fs15.mkdirSync)(agentsDir, { recursive: true });
3552
+ (0, import_node_fs19.mkdirSync)(agentsDir, { recursive: true });
2896
3553
  const plistPath = (0, import_node_path12.join)(agentsDir, LAUNCHD_PLIST_NAME);
2897
- (0, import_node_fs15.writeFileSync)(plistPath, launchdPlist(launcherPath, paths.root), "utf8");
3554
+ (0, import_node_fs19.writeFileSync)(plistPath, launchdPlist(launcherPath, paths.root), "utf8");
2898
3555
  launchctl(["bootout", `gui/${uid()}/${LAUNCHD_LABEL}`]);
2899
3556
  const load = launchctl(["bootstrap", `gui/${uid()}`, plistPath]);
2900
3557
  if (load.status !== 0) {
@@ -2913,12 +3570,12 @@ function installSystemdTimer(env) {
2913
3570
  if (!hubDist) return { ok: false, detail: "cannot resolve the global @mutmutco/hub dist \u2014 run `npm install -g @mutmutco/hub` first" };
2914
3571
  const paths = ensureState(env);
2915
3572
  const launcherPath = (0, import_node_path12.join)(paths.root, POSIX_LAUNCHER_FILE);
2916
- (0, import_node_fs15.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
2917
- (0, import_node_fs15.chmodSync)(launcherPath, 493);
3573
+ (0, import_node_fs19.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
3574
+ (0, import_node_fs19.chmodSync)(launcherPath, 493);
2918
3575
  const unitDir = (0, import_node_path12.join)((0, import_node_os6.homedir)(), ".config", "systemd", "user");
2919
- (0, import_node_fs15.mkdirSync)(unitDir, { recursive: true });
2920
- (0, import_node_fs15.writeFileSync)((0, import_node_path12.join)(unitDir, SYSTEMD_SERVICE_NAME), systemdServiceUnit(launcherPath, paths.root), "utf8");
2921
- (0, import_node_fs15.writeFileSync)((0, import_node_path12.join)(unitDir, SYSTEMD_TIMER_NAME), systemdTimerUnit(), "utf8");
3576
+ (0, import_node_fs19.mkdirSync)(unitDir, { recursive: true });
3577
+ (0, import_node_fs19.writeFileSync)((0, import_node_path12.join)(unitDir, SYSTEMD_SERVICE_NAME), systemdServiceUnit(launcherPath, paths.root), "utf8");
3578
+ (0, import_node_fs19.writeFileSync)((0, import_node_path12.join)(unitDir, SYSTEMD_TIMER_NAME), systemdTimerUnit(), "utf8");
2922
3579
  const reload = systemctlUser(["daemon-reload"]);
2923
3580
  if (reload.status !== 0) {
2924
3581
  return { ok: false, detail: `systemctl --user daemon-reload failed: ${(reload.stderr || reload.stdout).trim() || "unknown error \u2014 is a user systemd instance running?"}` };
@@ -2958,11 +3615,11 @@ function uninstallWindowsTask(env) {
2958
3615
  function uninstallLaunchd(env) {
2959
3616
  const plistPath = (0, import_node_path12.join)((0, import_node_os6.homedir)(), "Library", "LaunchAgents", LAUNCHD_PLIST_NAME);
2960
3617
  const boot = launchctl(["bootout", `gui/${uid()}/${LAUNCHD_LABEL}`]);
2961
- if (boot.status !== 0 && (0, import_node_fs15.existsSync)(plistPath)) {
3618
+ if (boot.status !== 0 && (0, import_node_fs19.existsSync)(plistPath)) {
2962
3619
  return { ok: false, detail: `launchctl bootout failed: ${(boot.stderr || boot.stdout).trim() || "unknown error"}` };
2963
3620
  }
2964
3621
  try {
2965
- (0, import_node_fs15.rmSync)(plistPath, { force: true });
3622
+ (0, import_node_fs19.rmSync)(plistPath, { force: true });
2966
3623
  } catch {
2967
3624
  }
2968
3625
  dropLauncher(env, POSIX_LAUNCHER_FILE);
@@ -2972,12 +3629,12 @@ function uninstallSystemdTimer(env) {
2972
3629
  const unitDir = (0, import_node_path12.join)((0, import_node_os6.homedir)(), ".config", "systemd", "user");
2973
3630
  const timerPath = (0, import_node_path12.join)(unitDir, SYSTEMD_TIMER_NAME);
2974
3631
  const disable = systemctlUser(["disable", "--now", SYSTEMD_TIMER_NAME]);
2975
- if (disable.status !== 0 && (0, import_node_fs15.existsSync)(timerPath)) {
3632
+ if (disable.status !== 0 && (0, import_node_fs19.existsSync)(timerPath)) {
2976
3633
  return { ok: false, detail: `systemctl --user disable --now ${SYSTEMD_TIMER_NAME} failed: ${(disable.stderr || disable.stdout).trim() || "unknown error"}` };
2977
3634
  }
2978
3635
  try {
2979
- (0, import_node_fs15.rmSync)(timerPath, { force: true });
2980
- (0, import_node_fs15.rmSync)((0, import_node_path12.join)(unitDir, SYSTEMD_SERVICE_NAME), { force: true });
3636
+ (0, import_node_fs19.rmSync)(timerPath, { force: true });
3637
+ (0, import_node_fs19.rmSync)((0, import_node_path12.join)(unitDir, SYSTEMD_SERVICE_NAME), { force: true });
2981
3638
  } catch {
2982
3639
  }
2983
3640
  systemctlUser(["daemon-reload"]);
@@ -2992,16 +3649,16 @@ function uninstallTask(env = process.env) {
2992
3649
  }
2993
3650
  function dropLauncher(env, file) {
2994
3651
  try {
2995
- (0, import_node_fs15.rmSync)((0, import_node_path12.join)(statePaths(env).root, file), { force: true });
3652
+ (0, import_node_fs19.rmSync)((0, import_node_path12.join)(statePaths(env).root, file), { force: true });
2996
3653
  } catch {
2997
3654
  }
2998
3655
  }
2999
3656
 
3000
3657
  // src/status.ts
3001
- var import_node_fs16 = require("node:fs");
3658
+ var import_node_fs20 = require("node:fs");
3002
3659
  function readJournal(path) {
3003
3660
  try {
3004
- const events = (0, import_node_fs16.readFileSync)(path, "utf8").split("\n").filter((line) => line.trim()).map((line) => {
3661
+ const events = (0, import_node_fs20.readFileSync)(path, "utf8").split("\n").filter((line) => line.trim()).map((line) => {
3005
3662
  try {
3006
3663
  return JSON.parse(line);
3007
3664
  } catch {
@@ -3124,31 +3781,6 @@ function formatHubStatus(status) {
3124
3781
  }
3125
3782
 
3126
3783
  // src/report.ts
3127
- var NAME_WIDTH = 8;
3128
- var VERSION_WIDTH = 15;
3129
- var DEFAULT_WIDTH = 100;
3130
- var MIN_NOTE_SPACE = 8;
3131
- var ACTIVATION_NOTE = {
3132
- claude: "next session",
3133
- codex: "next session",
3134
- jervcode: "next launch",
3135
- cursor: "on reload",
3136
- hermes: "after restart",
3137
- updater: "next run"
3138
- };
3139
- var GREEN = "\x1B[32m";
3140
- var YELLOW = "\x1B[33m";
3141
- var RED = "\x1B[31m";
3142
- var RESET = "\x1B[0m";
3143
- var ACCENT = "\x1B[38;2;125;211;252m";
3144
- var MUTED = "\x1B[38;2;150;150;150m";
3145
- var MM_GREEN = "\x1B[38;2;31;209;138m";
3146
- function accent(text, color) {
3147
- return color ? ACCENT + text + RESET : text;
3148
- }
3149
- function brandLine(rest, color = false) {
3150
- return `${accent("mmi-hub", color)} \xB7 Mutatis Mutandis \u2014 ${rest}`;
3151
- }
3152
3784
  var RETRY_HINTS = [
3153
3785
  [/applies when the host exits|applies on a quiet tick/, "installs when the app closes"],
3154
3786
  [/in flight|changed while/, "rerun update \u2014 host race"],
@@ -3177,257 +3809,34 @@ function failConsequence(row) {
3177
3809
  }
3178
3810
  return matchFirst(FAIL_CONSEQUENCE, detail) ?? "update failed \u2014 previous version kept";
3179
3811
  }
3180
- function classify(arm) {
3181
- const kind = arm.verdict === "ok" ? arm.from !== null && arm.from === arm.to && !arm.launchStaged ? "current" : "updated" : arm.verdict === "skip" ? "keep" : arm.verdict === "defer" ? "retry" : "failed";
3182
- const note = kind === "updated" ? arm.launchStaged ? "next launch" : ACTIVATION_NOTE[arm.surface] ?? null : kind === "keep" ? "above target" : kind === "retry" ? retryHint(arm.detail) : null;
3183
- return { name: arm.surface === "updater" ? "hub" : arm.surface, from: arm.from, to: arm.to, kind, note, detail: arm.detail, repeatFailure: arm.repeatFailure === true };
3184
- }
3185
- function statusText(kind, dryRun) {
3186
- if (dryRun && kind === "updated") return "would update";
3187
- switch (kind) {
3188
- case "updated":
3189
- return "updated";
3190
- case "current":
3191
- return "current";
3192
- case "keep":
3193
- return "keep";
3194
- case "retry":
3195
- return "retry";
3196
- case "failed":
3197
- return "failed";
3198
- case "pending":
3199
- return "pending";
3200
- }
3201
- }
3202
- function glyphFor(kind) {
3203
- switch (kind) {
3204
- case "updated":
3205
- return "\u2713";
3206
- case "current":
3207
- return "\u2713";
3208
- case "keep":
3209
- return "\u2713";
3210
- case "retry":
3211
- return "\u21BB";
3212
- case "failed":
3213
- return "\u2717";
3214
- case "pending":
3215
- return "\xB7";
3216
- }
3217
- }
3218
- function colorFor(kind) {
3219
- return kind === "failed" ? RED : kind === "retry" ? YELLOW : GREEN;
3220
- }
3221
- var ARM_TITLE = {
3222
- updater: "Updating mmi-hub",
3223
- cli: "Installing mmi-cli",
3224
- claude: "Arming Claude Code",
3225
- codex: "Arming Codex",
3226
- cursor: "Arming Cursor",
3227
- hermes: "Arming Hermes",
3228
- jervcode: "Arming JervCode"
3229
- };
3230
- var FACE_PRODUCT = "mmi-hub";
3231
- function updaterFace(options = {}) {
3232
- return createFace({ product: FACE_PRODUCT, color: options.color ?? false, columns: options.width });
3233
- }
3234
- var STATE_WORD = {
3235
- updated: "updated",
3236
- current: "current",
3237
- keep: "kept",
3238
- retry: "will retry",
3239
- failed: "failed",
3240
- pending: "pending"
3241
- };
3242
- function stepKindFor(kind) {
3243
- return kind === "failed" ? "fail" : kind === "retry" ? "note" : "ok";
3244
- }
3245
- function checklistLine(arm, seconds, face, options = {}) {
3246
- const { kind } = classify(arm);
3247
- const state = options.dryRun && kind === "updated" ? "would update" : STATE_WORD[kind];
3248
- const title = `${ARM_TITLE[arm.surface] ?? `Arming ${arm.surface}`} \u2014 ${state}`;
3249
- const truthful = kind === "failed" || kind === "retry" ? null : seconds;
3250
- return face.step(title, truthful, stepKindFor(kind));
3251
- }
3252
- function receiptText(summary, autoupdate, options = {}) {
3253
- const dry = options.dryRun ?? false;
3254
- const counts = reportCounts(summary);
3255
- const health = "Check health any time: mmi-hub status";
3256
- if (!summary.target) {
3257
- return [`${summary.exit >= 1 ? "\u2716" : "\u25CF"} mmi-hub is not ready \u2014 no green release yet (hourly retry)`, "", health];
3258
- }
3259
- const ready = counts.failed === 0;
3260
- const glyph = counts.failed > 0 ? "\u2716" : counts.retry > 0 ? "\u25CF" : dry ? "\u25C7" : "\u2714";
3261
- const state = counts.failed > 0 ? `${counts.failed} of ${counts.total} surfaces failed` : counts.retry > 0 ? `${counts.retry} of ${counts.total} surfaces will retry` : counts.updated > 0 ? `${counts.updated}/${counts.total} surfaces ${dry ? "would update" : "updated"}` : `all ${counts.total} surfaces current`;
3262
- const tail2 = autoupdate ? ` \xB7 ${autoupdate.ok ? "auto-update on" : `auto-update NOT enabled \u2014 ${autoupdate.detail}`}` : ` \xB7 ${summary.target}${dry ? " (planned)" : " (verified)"}`;
3263
- return [`${glyph} mmi-hub is ${ready ? "ready" : "not ready"} \u2014 ${state}${tail2}`, "", health];
3264
- }
3265
- function autoupdateLine(state) {
3266
- return state.ok ? 'Auto-update on \u2014 hourly task "MMI Fleet Updater".' : `Auto-update NOT enabled \u2014 ${state.detail}`;
3267
- }
3268
- function attentionRows(summary) {
3269
- return collectRows(summary).filter((row) => row.kind === "failed" || row.kind === "retry" || row.kind === "pending").map((row) => ({
3270
- surface: row.name,
3271
- state: statusText(row.kind, false),
3272
- mark: row.kind === "failed" ? "fail" : "note",
3273
- note: row.kind === "failed" ? failConsequence(row) : row.kind === "retry" ? retryHint(row.detail) : "arm not landed"
3274
- }));
3275
- }
3276
- function wrapWords2(text, available) {
3277
- if (available < 1) return [text];
3278
- const lines = [];
3279
- let current = "";
3280
- for (const word of text.split(/\s+/).filter(Boolean)) {
3281
- const piece = word.length > available ? word.slice(0, Math.max(1, available - 1)) + "\u2026" : word;
3282
- if (!current) current = piece;
3283
- else if (current.length + 1 + piece.length <= available) current += " " + piece;
3284
- else {
3285
- lines.push(current);
3286
- current = piece;
3287
- }
3288
- }
3289
- if (current) lines.push(current);
3290
- return lines.length ? lines : [""];
3291
- }
3292
- function noteLines(prefix, note, width) {
3293
- const used = stripAnsi(prefix).length;
3294
- const available = width - used - 1;
3295
- const indent = available >= MIN_NOTE_SPACE ? " ".repeat(used + 1) : " ".repeat(Math.min(used + 1, Math.max(2, width - MIN_NOTE_SPACE)));
3296
- const room = available >= MIN_NOTE_SPACE ? available : width - indent.length;
3297
- const parts = wrapWords2(note, room);
3298
- const first = available >= MIN_NOTE_SPACE ? prefix + " " + parts[0] : prefix.replace(/\s+$/, "");
3299
- const lines = [first];
3300
- for (const part of parts.slice(available >= MIN_NOTE_SPACE ? 1 : 0)) lines.push(indent + part);
3301
- return lines;
3302
- }
3303
- function stripAnsi(text) {
3304
- return text.replace(/\x1b\[[0-9;]*m/g, "");
3305
- }
3306
- function collectRows(summary) {
3307
- const rows = [];
3308
- if (summary.updater) rows.push(classify(summary.updater));
3309
- if (summary.cli) rows.push(classify(summary.cli));
3310
- for (const arm of summary.plugins) rows.push(classify(arm));
3311
- for (const surface of summary.surfaces) {
3312
- if (surface.present && surface.action === "arm-pending") {
3313
- rows.push({ name: surface.id, from: null, to: summary.target ?? "?", kind: "pending", note: "arm not landed", detail: "", repeatFailure: false });
3314
- }
3315
- }
3316
- return rows;
3317
- }
3318
- function headline(detail, width) {
3319
- const first = (detail ?? "").split(/(?<=[.;:])\s/)[0]?.replace(/\s+/g, " ").trim() ?? "";
3320
- return wrapWords2(first || "blocked", Math.max(10, width - 4))[0];
3321
- }
3322
- function updateHeader(target, dryRun = false, options) {
3323
- return brandLine(
3324
- dryRun ? `update plan to ${target} (dry run)` : `updating to ${target} (verified)`,
3325
- Boolean(options?.color)
3326
- );
3327
- }
3328
- function renderRowLines(row, ctx) {
3329
- const paint = (kind, text) => ctx.color ? colorFor(kind) + text + RESET : text;
3330
- const versions = row.from !== null && row.from !== row.to ? `${row.from} \u2192 ${row.to}` : `${row.to}`;
3331
- const glyph = ctx.tty ? glyphFor(row.kind) + " " : "";
3332
- const status = statusText(row.kind, ctx.dryRun).padEnd(ctx.statusWidth);
3333
- const prefix = " " + row.name.padEnd(NAME_WIDTH) + " " + versions.padEnd(VERSION_WIDTH) + " " + paint(row.kind, glyph + status);
3334
- return row.note ? noteLines(prefix, row.note, ctx.width) : [prefix.replace(/\s+$/, "")];
3335
- }
3336
- function progressiveStatusWidth(dryRun) {
3337
- return Math.max((dryRun ? "would update" : "updated").length, 7);
3338
- }
3339
- function progressiveRow(arm, options = {}) {
3340
- const width = Math.max(40, options.width ?? DEFAULT_WIDTH);
3341
- const tty = options.tty ?? false;
3342
- const color = tty && (options.color ?? true);
3343
- const dryRun = options.dryRun ?? false;
3344
- return renderRowLines(classify(arm), { tty, color, dryRun, width, statusWidth: progressiveStatusWidth(dryRun) }).join("\n");
3345
- }
3346
3812
  function reportCounts(summary) {
3347
- const rows = collectRows(summary);
3348
- return {
3349
- updated: rows.filter((row) => row.kind === "updated").length,
3350
- current: rows.filter((row) => row.kind === "current" || row.kind === "keep").length,
3351
- retry: rows.filter((row) => row.kind === "retry").length,
3352
- failed: rows.filter((row) => row.kind === "failed").length,
3353
- total: rows.length
3813
+ const counts2 = {
3814
+ updated: 0,
3815
+ current: 0,
3816
+ retry: 0,
3817
+ failed: 0,
3818
+ total: summary.surfaces.filter((surface) => surface.present && surface.action === "arm-pending").length
3354
3819
  };
3355
- }
3356
- function reportFooterLines(summary, options = {}) {
3357
- const width = Math.max(40, options.width ?? DEFAULT_WIDTH);
3358
- const tty = options.tty ?? false;
3359
- const color = tty && (options.color ?? true);
3360
- const dryRun = options.dryRun ?? false;
3361
- const journalPath = options.journalPath ?? "~/.mmi/updater/journal.jsonl";
3362
- const paint = (kind, text) => color ? colorFor(kind) + text + RESET : text;
3363
- const rows = collectRows(summary);
3364
- const counts = reportCounts(summary);
3365
- const footer = dryRun ? `${counts.updated}/${counts.total} would update` : `${counts.updated}/${counts.total} updated` + (counts.retry ? ` \xB7 ${counts.retry} will retry` : "") + (counts.failed ? ` \xB7 ${counts.failed} failed` : "");
3366
- const out = ["", " " + footer];
3367
- if (tty && !dryRun && counts.failed === 0) {
3368
- out.push(" " + (color ? MUTED + "Suggested next: mmi-cli doctor" + RESET : "Suggested next: mmi-cli doctor"));
3369
- }
3370
- const self = summary.updater;
3371
- if (!dryRun && self && self.from && self.to && self.from !== self.to && self.verdict !== "fail") {
3372
- out.push("");
3373
- out.push(...noteLines(
3374
- " note: ",
3375
- `hub moved ${self.from} \u2192 ${self.to} while the surfaces above were converging, so they used ${self.from} logic \u2014 rerun \`mmi-hub update\` to bring them to ${self.to}`,
3376
- width
3377
- ));
3378
- }
3379
- if (counts.failed) {
3380
- out.push("");
3381
- for (const row of rows.filter((row2) => row2.kind === "failed")) {
3382
- const mark = tty ? paint("failed", "\u2717 ") : "";
3383
- out.push(...noteLines(" " + mark + row.name, failConsequence(row), width));
3384
- out.push(...noteLines(" fix: ", row.repeatFailure ? "doctor + update already tried and did not clear this \u2014 see log, do not repeat" : "mmi-cli doctor, then mmi-hub update", width));
3385
- out.push(...noteLines(" log: ", journalPath, width));
3386
- }
3387
- }
3388
- return out;
3389
- }
3390
- function formatReconcileReport(summary, options = {}) {
3391
- const width = Math.max(40, options.width ?? DEFAULT_WIDTH);
3392
- const tty = options.tty ?? false;
3393
- const color = tty && (options.color ?? true);
3394
- const dryRun = options.dryRun ?? false;
3395
- const out = [];
3396
- const paint = (kind, text) => color ? colorFor(kind) + text + RESET : text;
3397
- const rows = collectRows(summary);
3398
- const counts = reportCounts(summary);
3399
- if (!summary.target) {
3400
- if ((summary.exit >= 1 || summary.deferred) && summary.detail) {
3401
- out.push("MMI Hub \u2014 cannot update now");
3402
- out.push(...noteLines(" " + paint("failed", "blocked"), headline(summary.detail, width), width));
3403
- out.push(...noteLines(" fix:", retryHint(summary.detail), width));
3404
- } else {
3405
- out.push("MMI Hub \u2014 release still publishing, nothing changed (hourly retry)");
3406
- }
3407
- return out.join("\n") + "\n";
3408
- }
3409
- const allCurrent = rows.length > 0 && counts.updated === 0 && counts.retry === 0 && counts.failed === 0 && rows.every((row) => row.kind === "current" || row.kind === "keep");
3410
- if (allCurrent && !dryRun) {
3411
- out.push(`MMI Hub \u2014 MMI is up to date (${summary.target} \xB7 verified)`);
3412
- return out.join("\n") + "\n";
3820
+ for (const arm of [summary.updater, summary.cli, ...summary.plugins]) {
3821
+ if (!arm) continue;
3822
+ counts2.total++;
3823
+ if (arm.verdict === "fail") counts2.failed++;
3824
+ else if (arm.verdict === "defer") counts2.retry++;
3825
+ else if (arm.verdict === "skip" || arm.from !== null && arm.from === arm.to && !arm.launchStaged) counts2.current++;
3826
+ else counts2.updated++;
3413
3827
  }
3414
- out.push(updateHeader(summary.target, dryRun, { color }));
3415
- out.push("");
3416
- const statusWidth = Math.max(...rows.map((row) => statusText(row.kind, dryRun).length), 7);
3417
- for (const row of rows) out.push(...renderRowLines(row, { tty, color, dryRun, width, statusWidth }));
3418
- out.push(...reportFooterLines(summary, options));
3419
- return out.join("\n") + "\n";
3828
+ return counts2;
3420
3829
  }
3421
3830
 
3422
3831
  // src/stamp.ts
3423
- var import_node_fs17 = require("node:fs");
3832
+ var import_node_fs21 = require("node:fs");
3424
3833
  var ALWAYS_ARMED = ["cli", "updater"];
3425
3834
  var PLUGIN_ARMS = /* @__PURE__ */ new Set(["claude", "codex", "jervcode", "cursor", "hermes"]);
3426
3835
  function fleetStamp(env = process.env) {
3427
3836
  const paths = statePaths(env);
3428
3837
  let events = [];
3429
3838
  try {
3430
- events = (0, import_node_fs17.readFileSync)(paths.journalPath, "utf8").split("\n").filter((line) => line.trim()).map((line) => {
3839
+ events = (0, import_node_fs21.readFileSync)(paths.journalPath, "utf8").split("\n").filter((line) => line.trim()).map((line) => {
3431
3840
  try {
3432
3841
  return JSON.parse(line);
3433
3842
  } catch {
@@ -3477,7 +3886,7 @@ function fleetStamp(env = process.env) {
3477
3886
  // src/index.ts
3478
3887
  function ownVersion() {
3479
3888
  try {
3480
- return JSON.parse((0, import_node_fs18.readFileSync)((0, import_node_path13.join)(__dirname, "..", "package.json"), "utf8")).version;
3889
+ return JSON.parse((0, import_node_fs22.readFileSync)((0, import_node_path13.join)(__dirname, "..", "package.json"), "utf8")).version;
3481
3890
  } catch {
3482
3891
  return "0.0.0";
3483
3892
  }
@@ -3497,147 +3906,34 @@ var HELP = `mmi-hub ${ownVersion()} \u2014 install and maintain MMI tooling
3497
3906
  State remains under $MMI_UPDATER_HOME (default ~/.mmi/updater), and the Windows task name remains
3498
3907
  "MMI Fleet Updater", so existing installations continue in place.
3499
3908
  Status performs no npm install, host repair, scheduler write, state mkdir, or repository cleanup.`;
3500
- function humanOptions(dryRun) {
3501
- return {
3502
- tty: Boolean(process.stdout.isTTY),
3503
- color: !process.env.NO_COLOR,
3504
- width: process.stdout.columns ?? 100,
3505
- dryRun,
3506
- journalPath: statePaths(process.env).journalPath
3507
- };
3508
- }
3509
- function printReconcile(summary, json, dryRun = false) {
3510
- if (json) {
3511
- process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
3512
- return;
3513
- }
3514
- process.stdout.write(formatReconcileReport(summary, humanOptions(dryRun)));
3515
- }
3516
- function faceEnabled(args, json) {
3517
- return !json && !args.includes("--verbose") && !process.env.NO_COLOR && process.env.TERM !== "dumb" && Boolean(process.stdout.isTTY) && Boolean(process.stderr.isTTY);
3518
- }
3519
- function openFace(args, json, dryRun) {
3520
- if (!faceEnabled(args, json)) return null;
3521
- const face = updaterFace(humanOptions(dryRun));
3522
- if (!process.env.MMI_UPDATER_REEXEC) for (const line of face.welcome()) process.stdout.write(line + "\n");
3523
- return face;
3524
- }
3525
- function finishFace(face, summary, autoupdate, dryRun) {
3526
- const options = humanOptions(dryRun);
3527
- const write = (line) => {
3528
- process.stdout.write(line + "\n");
3529
- };
3530
- if (!summary.target) {
3531
- process.stdout.write(formatReconcileReport(summary, { ...options, tty: false }));
3532
- } else {
3533
- for (const line of renderRows(face, attentionRows(summary))) write(line);
3534
- }
3535
- const body = receiptText(summary, autoupdate, { dryRun });
3536
- const counts = reportCounts(summary);
3537
- if (counts.failed > 0) {
3538
- body.push("", "Fix: mmi-cli doctor, then mmi-hub update", `Log: ${options.journalPath}`);
3539
- }
3540
- const self = summary.updater;
3541
- if (!dryRun && self && self.from && self.to && self.from !== self.to && self.verdict !== "fail") {
3542
- body.push("", `Note: hub moved ${self.from} \u2192 ${self.to} while the surfaces above were converging, so they used ${self.from} logic \u2014 rerun \`mmi-hub update\` to bring them to ${self.to}`);
3543
- }
3544
- for (const line of face.receipt(body)) write(line);
3545
- write(face.signOff());
3546
- }
3547
- function handOff(reexec, args) {
3909
+ function handOff(reexec, args, env) {
3548
3910
  const child = (0, import_node_child_process9.spawnSync)(process.execPath, [reexec.dist, ...args], {
3549
3911
  stdio: "inherit",
3550
3912
  windowsHide: true,
3551
- env: { ...process.env, MMI_UPDATER_REEXEC: "1" }
3913
+ env: { ...env, MMI_UPDATER_REEXEC: "1" }
3552
3914
  });
3553
3915
  if (child.error || typeof child.status !== "number") {
3554
- process.stderr.write(`mmi-hub: could not run the updated updater at ${reexec.dist} \u2014 ${child.error?.message ?? "no exit status"}; rerun \`mmi-hub update\`
3555
- `);
3556
- return 1;
3916
+ throw new Error(`could not run the updated updater at ${reexec.dist} \u2014 ${child.error?.message ?? "no exit status"}; rerun mmi-hub update`);
3557
3917
  }
3558
3918
  return child.status;
3559
3919
  }
3560
- function heldOutput(write) {
3561
- const held = [];
3562
- let streaming = false;
3563
- return {
3564
- emit: (line) => {
3565
- if (streaming) write(line);
3566
- else held.push(line);
3567
- },
3568
- stream: () => {
3569
- if (streaming) return;
3570
- streaming = true;
3571
- for (const line of held) write(line);
3572
- held.length = 0;
3573
- }
3574
- };
3575
- }
3576
3920
  function shippedTargetFrom(args) {
3577
3921
  const index = args.indexOf("--shipped");
3578
3922
  if (index < 0) return void 0;
3579
3923
  const value = (args[index + 1] ?? "").trim();
3580
3924
  return value && !value.startsWith("--") ? value : void 0;
3581
3925
  }
3582
- async function runUpdate(args, json, face = null, shippedTarget) {
3583
- const dryRun = args.includes("--dry-run");
3584
- const verbose = args.includes("--verbose");
3585
- const narrate = (line) => {
3586
- process.stderr.write(`mmi-hub: ${line}
3587
- `);
3588
- };
3589
- const progressive = !json && !verbose && Boolean(process.stderr.isTTY);
3590
- if (!progressive) {
3591
- return reconcile({ dryRun, narrate: verbose ? narrate : void 0, shippedTarget }).then((summary2) => ({ summary: summary2, rendered: false }));
3592
- }
3593
- const reportOptions = humanOptions(dryRun);
3594
- const spinner = createSpinner(face ?? updaterFace(reportOptions), { animate: true, stream: process.stderr });
3595
- let spinning = false;
3596
- const phase = (line) => {
3597
- if (spinning) spinner.say(line);
3598
- else {
3599
- spinner.start(line);
3600
- spinning = true;
3601
- }
3602
- };
3603
- const persist = (line) => {
3604
- if (spinning) {
3605
- spinner.stop();
3606
- spinning = false;
3607
- }
3608
- process.stdout.write(line + "\n");
3609
- };
3610
- let headerEmitted = false;
3611
- let summary;
3612
- const { emit, stream } = heldOutput(persist);
3613
- let arms = 0;
3614
- let mark = Date.now();
3615
- try {
3616
- summary = await reconcile({
3617
- dryRun,
3618
- shippedTarget,
3619
- onPhase: (line) => phase(line),
3620
- onTargetResolved: face ? void 0 : (target) => {
3621
- emit(updateHeader(target, dryRun, reportOptions));
3622
- emit("");
3623
- headerEmitted = true;
3624
- },
3625
- onArm: (arm) => {
3626
- const seconds = (Date.now() - mark) / 1e3;
3627
- mark = Date.now();
3628
- if (arms++) stream();
3629
- emit(face ? checklistLine(arm, seconds, face, reportOptions) : progressiveRow(arm, reportOptions));
3630
- }
3631
- });
3632
- } finally {
3633
- spinner.stop();
3634
- }
3635
- if (summary.reexec) return { summary, rendered: true };
3636
- stream();
3637
- if (face) return { summary, rendered: true };
3638
- if (headerEmitted) process.stdout.write(reportFooterLines(summary, reportOptions).join("\n") + "\n");
3639
- else process.stdout.write(formatReconcileReport(summary, reportOptions));
3640
- return { summary, rendered: true };
3926
+ function runMaintenance2(args, dependencies = {}) {
3927
+ return runMaintenance(installer_product_default, args, {
3928
+ engine: dependencies.reconcile ?? reconcile,
3929
+ schedule: dependencies.schedule ?? installTask,
3930
+ handOff: dependencies.handOff ?? handOff,
3931
+ run: dependencies.run,
3932
+ shippedTarget: shippedTargetFrom(args),
3933
+ counts: reportCounts,
3934
+ diagnose: (arm) => arm.verdict === "fail" ? [arm.detail, failConsequence(arm), arm.repeatFailure ? "This failure persisted after repair; do not repeat the same remedy. Inspect the updater journal." : retryHint(arm.detail)].join("\n") : arm.verdict === "defer" ? `${arm.detail}
3935
+ ${retryHint(arm.detail)}` : void 0
3936
+ });
3641
3937
  }
3642
3938
  async function main(args = process.argv.slice(2)) {
3643
3939
  if (args.includes("--version")) {
@@ -3655,41 +3951,7 @@ async function main(args = process.argv.slice(2)) {
3655
3951
  process.stdout.write(json ? JSON.stringify(status, null, 2) + "\n" : formatHubStatus(status) + "\n");
3656
3952
  return status.failures.length ? 1 : 0;
3657
3953
  }
3658
- if (command === "update") {
3659
- const dryRun = args.includes("--dry-run");
3660
- const face = openFace(args, json, dryRun);
3661
- const { summary, rendered } = await runUpdate(args, json, face);
3662
- if (summary.reexec) return handOff(summary.reexec, args);
3663
- if (face) finishFace(face, summary, null, dryRun);
3664
- else if (!rendered) printReconcile(summary, json, dryRun);
3665
- return summary.exit;
3666
- }
3667
- if (command === "install") {
3668
- const dryRun = args.includes("--dry-run");
3669
- const face = openFace(args, json, dryRun);
3670
- const shippedTarget = shippedTargetFrom(args);
3671
- const { summary, rendered } = await runUpdate(args, json, face, shippedTarget);
3672
- if (summary.reexec) {
3673
- const handedOff = handOff(summary.reexec, ["update", ...args.filter((arg) => arg !== "install" && arg !== "--shipped" && arg !== shippedTarget)]);
3674
- const scheduled = installTask();
3675
- if (!json) {
3676
- process.stdout.write(autoupdateLine(scheduled) + "\n");
3677
- } else {
3678
- process.stdout.write(JSON.stringify({ convergence: summary, autoupdate: scheduled }, null, 2) + "\n");
3679
- }
3680
- return Math.max(handedOff, scheduled.ok ? 0 : 1);
3681
- }
3682
- const schedule = installTask();
3683
- if (json) {
3684
- process.stdout.write(JSON.stringify({ convergence: summary, autoupdate: schedule }, null, 2) + "\n");
3685
- } else if (face) {
3686
- finishFace(face, summary, schedule, dryRun);
3687
- } else {
3688
- if (!rendered) printReconcile(summary, false, dryRun);
3689
- process.stdout.write(autoupdateLine(schedule) + "\n");
3690
- }
3691
- return Math.max(summary.exit, schedule.ok ? 0 : 1);
3692
- }
3954
+ if (command === "update" || command === "install") return runMaintenance2(args);
3693
3955
  if (command === "autoupdate") {
3694
3956
  const commandIndex = args.indexOf(command);
3695
3957
  const mode = args[commandIndex + 1];
@@ -3714,27 +3976,18 @@ if (typeof require !== "undefined" && require.main === module) {
3714
3976
  }
3715
3977
  // Annotate the CommonJS export names for ESM import in node:
3716
3978
  0 && (module.exports = {
3717
- ACCENT,
3718
- FACE_PRODUCT,
3719
- MM_GREEN,
3720
- RESET,
3721
- accent,
3722
3979
  acquireSharedDocLock,
3723
- attentionRows,
3724
- autoupdateLine,
3725
- brandLine,
3980
+ assertFaceConformance,
3726
3981
  buildWatcherCommand,
3727
3982
  checkAtomicity,
3728
- checklistLine,
3729
3983
  claudeArm,
3730
3984
  claudeHostEvidence,
3731
3985
  clearLaunchTarget,
3732
3986
  clearPending,
3987
+ createInstallerRun,
3733
3988
  enumerateSurfaces,
3734
3989
  failConsequence,
3735
3990
  fleetStamp,
3736
- formatReconcileReport,
3737
- heldOutput,
3738
3991
  hermesArm,
3739
3992
  hermesHome,
3740
3993
  hermesHomes,
@@ -3761,17 +4014,15 @@ if (typeof require !== "undefined" && require.main === module) {
3761
4014
  probeHermesLaunchStaging,
3762
4015
  probeJervcodeInstallation,
3763
4016
  probeLaunchTargetVersion,
3764
- progressiveRow,
3765
4017
  readLaunchTarget,
3766
4018
  readPending,
3767
- receiptText,
3768
4019
  reconcile,
3769
4020
  reconcileCursorUserHooks,
3770
4021
  releaseSharedDocLock,
3771
4022
  reportCounts,
3772
- reportFooterLines,
3773
4023
  retiredSurfaces,
3774
4024
  retryHint,
4025
+ runMaintenance,
3775
4026
  schedulePendingWatcher,
3776
4027
  schedulerStatus,
3777
4028
  sharedDocLockPath,
@@ -3782,11 +4033,8 @@ if (typeof require !== "undefined" && require.main === module) {
3782
4033
  systemdServiceUnit,
3783
4034
  systemdTimerUnit,
3784
4035
  taskXml,
3785
- updateHeader,
3786
- updaterFace,
3787
4036
  watcherLockPath,
3788
4037
  withSharedDocLock,
3789
- wrapWords,
3790
4038
  writeLaunchTarget,
3791
4039
  writePending
3792
4040
  });