@mutmutco/hub 4.4.10 → 4.4.12

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 +1034 -699
  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 get you ready.",
109
99
  accent: "38;2;249;115;22",
110
- warm: "Welcome. Let's set up MM Strategy \u2014 about a minute."
100
+ warm: "Welcome back. Checking for updates...",
101
+ doctor: "mm-strategy doctor"
111
102
  }),
112
103
  "mmi-hub": Object.freeze({
113
104
  name: "mmi-hub",
105
+ installWarm: "Welcome. Let's get you ready.",
114
106
  accent: "38;2;125;211;252",
115
- warm: "Welcome back. Checking your surfaces\u2026"
107
+ warm: "Welcome back. Checking for updates...",
108
+ doctor: "mmi doctor"
116
109
  }),
117
110
  "jerv-hub": Object.freeze({
118
111
  name: "jerv-hub",
112
+ installWarm: "Welcome. Let's get you ready.",
119
113
  accent: "38;2;248;113;113",
120
- warm: "Welcome back. Checking your surfaces\u2026"
114
+ warm: "Welcome back. Checking for updates...",
115
+ doctor: "jerv doctor"
121
116
  }),
122
117
  jervcode: Object.freeze({
123
118
  name: "JervCode",
119
+ installWarm: "Welcome. Let's get you ready.",
124
120
  accent: "38;2;192;132;252",
125
- warm: "Welcome back. Keeping JervCode current\u2026"
121
+ warm: "Welcome back. Checking for updates...",
122
+ doctor: "jervcode doctor"
126
123
  })
127
124
  });
128
125
  function identityFor(product) {
@@ -132,7 +129,10 @@ function identityFor(product) {
132
129
  }
133
130
  return identity;
134
131
  }
132
+
133
+ // ../installer/face/src/face.ts
135
134
  var GLYPH = Object.freeze({
135
+ clock: "\u23F0",
136
136
  diamond: "\u25C6",
137
137
  hollow: "\u25C7",
138
138
  bar: "\u2502",
@@ -156,6 +156,9 @@ var ANSI = /\u001b\[[0-9;]*m/g;
156
156
  function visibleWidth(text) {
157
157
  return [...String(text).replace(ANSI, "")].length;
158
158
  }
159
+ function stripColor(text) {
160
+ return String(text).replace(ANSI, "");
161
+ }
159
162
  function faceWidth(columns) {
160
163
  const raw = Number(columns);
161
164
  return Math.max(40, Math.min(100, Number.isFinite(raw) && raw > 0 ? raw : 100));
@@ -184,7 +187,14 @@ function wrapWords(text, width) {
184
187
  if (line) lines.push(line);
185
188
  return lines.length ? lines : [""];
186
189
  }
187
- function createFace({ product, color = false, columns, env = process.env }) {
190
+ var PROGRESS_PROTOCOL = 1;
191
+ function readProgressFd(env) {
192
+ const fd = Number.parseInt(String(env.MM_PROGRESS_FD ?? ""), 10);
193
+ if (!Number.isInteger(fd) || fd <= 0) return null;
194
+ const protocol = Number.parseInt(String(env.MM_PROGRESS_PROTOCOL ?? PROGRESS_PROTOCOL), 10);
195
+ return protocol === PROGRESS_PROTOCOL ? fd : null;
196
+ }
197
+ function createFace({ product, color = false, columns, env = process.env, operation }) {
188
198
  const identity = identityFor(product);
189
199
  const width = faceWidth(columns);
190
200
  const paint = (sgr, text) => color ? `\x1B[${sgr}m${text}\x1B[0m` : String(text);
@@ -192,28 +202,49 @@ function createFace({ product, color = false, columns, env = process.env }) {
192
202
  const indent = " ".repeat(TITLE_COLUMN - 1);
193
203
  const continuedPhases = new Set((env.MM_FACE_CONTINUES ?? "").split(",").map((phase) => phase.trim()).filter(Boolean));
194
204
  const continuesFace = continuedPhases.size > 0;
195
- const welcome = () => continuesFace ? [] : [
205
+ const nested = env.MM_OUTER_CONSOLE === "1";
206
+ const progressFd = readProgressFd(env);
207
+ const progressFile = env.MM_PROGRESS_PROTOCOL === "1" ? env.MM_PROGRESS_FILE : void 0;
208
+ const emitMilestone = (title, measure, kind) => {
209
+ if (progressFd === null && !progressFile) return false;
210
+ const record = { v: PROGRESS_PROTOCOL, step: stripColor(title), state: kind };
211
+ if (typeof measure === "number") record.ms = Math.max(0, Math.round(measure * 1e3));
212
+ if (kind === "running" && typeof measure === "string") record.measure = measure;
213
+ try {
214
+ if (progressFile) (0, import_node_fs.appendFileSync)(progressFile, `${JSON.stringify(record)}
215
+ `);
216
+ else (0, import_node_fs.writeSync)(progressFd, `${JSON.stringify(record)}
217
+ `);
218
+ return true;
219
+ } catch {
220
+ return false;
221
+ }
222
+ };
223
+ const welcome = () => continuesFace || nested ? [] : [
196
224
  `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
197
225
  bar(),
198
- `${bar()} ${identity.warm}`,
226
+ `${bar()} ${operation === "install" ? identity.installWarm : identity.warm}`,
199
227
  bar()
200
228
  ];
201
229
  const continues = (phase, kind = "ok") => {
202
230
  const inherited = continuedPhases.delete(String(phase).trim());
203
231
  return kind === "fail" ? false : inherited;
204
232
  };
205
- const step = (title, seconds = null, kind = "ok") => {
206
- const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
207
- const time = seconds === null || seconds === void 0 ? "" : paint(PALETTE.muted, `${Math.max(0, Math.round(seconds))}s`);
233
+ const step = (title, measure = null, kind = "ok") => {
234
+ if (emitMilestone(title, measure, kind)) return "";
235
+ const glyph = title === "Armed hourly updates" && kind === "ok" ? GLYPH.clock : kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
236
+ const measured = measure === null || measure === void 0 ? "" : paint(PALETTE.muted, typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : String(measure).trim());
237
+ const time = measured;
208
238
  const column = Math.min(44, Math.max(0, width - 8));
209
- const reserved = time ? 6 : 0;
239
+ const reserved = time ? Math.max(6, visibleWidth(time) + 2) : 0;
210
240
  const [first, ...rest] = wrapWords(title, width - TITLE_COLUMN - reserved);
211
241
  const head = `${bar()} ${glyph} ${first}`;
212
242
  const pad = Math.max(1, column - visibleWidth(head));
213
243
  return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
214
244
  };
215
- const relay = (text) => String(text).split("\n").map((line) => line.trim() === "" ? bar() : `${bar()}${indent}${line}`).join("\n");
216
- const receipt = (lines) => {
245
+ const relay = (text) => String(text).replace(/\r?\n$/, "").split(/\r?\n/).flatMap((line) => line.trim() === "" ? [bar()] : (visibleWidth(line) <= width - TITLE_COLUMN ? [line] : wrapWords(line, width - TITLE_COLUMN)).map((part) => `${bar()}${indent}${part}`)).join("\n");
246
+ const receipt = (lines, { ready = true } = {}) => {
247
+ if (ready && nested) return [];
217
248
  const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
218
249
  const line = String(raw);
219
250
  if (visibleWidth(line) <= width - 6) return [line];
@@ -229,87 +260,800 @@ function createFace({ product, color = false, columns, env = process.env }) {
229
260
  frame(GLYPH.boxBottom, GLYPH.boxBottomEnd)
230
261
  ];
231
262
  };
232
- const signOff = () => `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
263
+ const outcome = (changed, options = {}) => {
264
+ const fact = String(changed ?? "").trim();
265
+ if (!fact) {
266
+ 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})`);
267
+ }
268
+ return receipt([
269
+ `${GLYPH.check} ${identity.name} is ready.`,
270
+ fact,
271
+ `Check health any time: ${identity.doctor}`
272
+ ], options);
273
+ };
274
+ const signOff = () => nested ? "" : `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
233
275
  const refusal = (message3) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message3}`;
234
- return { identity, width, welcome, continues, step, relay, receipt, signOff, refusal, paint };
276
+ return { identity, width, nested, emitsProgress: progressFd !== null || Boolean(progressFile), welcome, continues, step, progress: (title, measure = null) => emitMilestone(title, measure, "running"), relay, receipt, outcome, signOff, refusal, paint };
235
277
  }
278
+
279
+ // ../installer/face/src/shell.ts
236
280
  var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
237
- var SEPARATOR = "\xB7";
238
- var subjectOf = (row) => row.aspect ? `${row.surface} ${row.aspect}` : row.surface;
239
- var groupKey = (row) => `${subjectOf(row)}\0${row.state}\0${row.mark ?? ""}\0${row.note ?? ""}`;
240
- function renderRows(face, rows) {
241
- if (rows.length === 0) return [];
242
- const groups = /* @__PURE__ */ new Map();
243
- for (const row of rows) {
244
- const key = groupKey(row);
245
- const found = groups.get(key);
246
- if (found) {
247
- if (row.instance) found.instances.push(row.instance);
248
- } else {
249
- groups.set(key, { row, instances: row.instance ? [row.instance] : [] });
250
- }
251
- }
252
- const subjects = [...groups.values()].map(({ row, instances }) => instances.length > 1 ? subjectOf(row) : [subjectOf(row), instances[0]].filter(Boolean).join(` ${SEPARATOR} `));
253
- const column = Math.min(Math.max(...subjects.map(visibleWidth)), Math.max(12, face.width - 24));
254
- const lines = [];
255
- for (const [index, { row, instances }] of [...groups.values()].entries()) {
256
- const subject = subjects[index];
257
- const tail2 = instances.length > 1 ? `${row.state} ${SEPARATOR} ${instances.join(", ")}` : row.state;
258
- const body = `${clip(subject, column).padEnd(column)} ${row.mark ? `${markGlyph(face, row.mark)} ` : ""}${tail2}`;
259
- lines.push(...face.relay(body).split("\n"));
260
- if (row.note) {
261
- for (const part of wrapWords(row.note, Math.max(16, face.width - TITLE_COLUMN))) {
262
- lines.push(...face.relay(part).split("\n"));
263
- }
264
- }
281
+
282
+ // ../installer/face/src/spinner.ts
283
+ var import_node_fs2 = require("node:fs");
284
+ var import_node_worker_threads = require("node:worker_threads");
285
+ var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
286
+ var WORKER_SOURCE = `
287
+ const { parentPort, workerData } = require('node:worker_threads');
288
+ const { writeSync, appendFileSync } = require('node:fs');
289
+ const control = new Int32Array(workerData.control);
290
+ let frames = workerData.frames, frame = workerData.frame;
291
+ function draw() {
292
+ if (Atomics.compareExchange(control, 1, 0, 1) !== 0) return;
293
+ try {
294
+ if (Atomics.load(control, 0) || Atomics.load(control, 2) || !frames.length) return;
295
+ const text = frames[frame++ % frames.length].replace('__elapsed__', Math.floor((Date.now() - workerData.started) / 1000) + 's');
296
+ writeSync(2, text);
297
+ if (workerData.transcriptPath) appendFileSync(workerData.transcriptPath, JSON.stringify({ channel: 'spinner', text }) + '\\n');
298
+ } finally {
299
+ Atomics.store(control, 1, 0);
300
+ Atomics.notify(control, 1);
265
301
  }
266
- return lines;
267
- }
268
- function markGlyph(face, kind) {
269
- const glyph = kind === "fail" ? GLYPH.cross : kind === "note" ? GLYPH.hollow : GLYPH.check;
270
- return face.paint(kind === "fail" ? face.identity.accent : kind === "note" ? "38;2;150;150;150" : "38;2;31;209;138", glyph);
271
- }
272
- function clip(text, width) {
273
- if (visibleWidth(text) <= width) return text;
274
- return `\u2026${[...text].slice(-(width - 1)).join("")}`;
275
302
  }
276
- var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
277
- function createSpinner(face, { animate, stream = process.stderr, intervalMs = 90 }) {
303
+ parentPort.on('message', (next) => {
304
+ if (Atomics.load(control, 2)) return;
305
+ frames = next.frames;
306
+ frame = next.frame;
307
+ });
308
+ setInterval(draw, workerData.intervalMs);
309
+ `;
310
+ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath }) {
311
+ animate = animate && !face.nested && !face.emitsProgress;
278
312
  let timer = null;
279
- let text = "";
313
+ let worker = null;
314
+ let control = null;
315
+ let frames = [];
280
316
  let frame = 0;
281
- const clear = () => {
282
- if (animate) stream.write("\r\x1B[2K");
317
+ let started = 0;
318
+ let suspended = false;
319
+ const write = (text) => {
320
+ if (stream) stream.write(text);
321
+ else {
322
+ (0, import_node_fs2.writeSync)(2, text);
323
+ if (transcriptPath) (0, import_node_fs2.appendFileSync)(transcriptPath, `${JSON.stringify({ channel: "spinner", text })}
324
+ `);
325
+ }
326
+ };
327
+ const render = (text, measure) => {
328
+ const line = face.step(text, measure ?? "__elapsed__", "note").split("\n")[0];
329
+ frames = line ? FRAMES.map((glyph) => `\r\x1B[2K${line.replace(GLYPH.dot, glyph)}`) : [];
283
330
  };
284
331
  const draw = () => {
285
- if (!animate) return;
286
- const line = face.step(text, null, "note").split("\n")[0].replace(GLYPH.dot, FRAMES[frame % FRAMES.length]);
287
- stream.write(`\r\x1B[2K${line}`);
288
- frame += 1;
332
+ if (animate && !suspended && frames.length) write(frames[frame++ % frames.length].replace("__elapsed__", `${Math.floor((Date.now() - started) / 1e3)}s`));
333
+ };
334
+ const pause = () => {
335
+ if (!control) return;
336
+ Atomics.store(control, 0, 1);
337
+ while (Atomics.load(control, 1)) Atomics.wait(control, 1, 1);
338
+ };
339
+ const halt = () => {
340
+ if (timer) clearInterval(timer);
341
+ timer = null;
342
+ if (control) Atomics.store(control, 2, 1);
343
+ pause();
344
+ if (worker) void worker.terminate();
345
+ worker = null;
346
+ control = null;
289
347
  };
290
348
  return {
291
- start(next) {
292
- text = next;
349
+ start(text, measure = null) {
350
+ if (!animate) return;
351
+ halt();
352
+ suspended = false;
353
+ started = Date.now();
354
+ render(text, measure);
293
355
  frame = 0;
294
356
  draw();
295
- if (animate && !timer) timer = setInterval(draw, intervalMs).unref();
357
+ if (!frames.length) return;
358
+ if (stream) timer = setInterval(draw, intervalMs).unref();
359
+ else {
360
+ control = new Int32Array(new SharedArrayBuffer(12));
361
+ try {
362
+ worker = new import_node_worker_threads.Worker(WORKER_SOURCE, { eval: true, workerData: {
363
+ control: control.buffer,
364
+ frames,
365
+ frame,
366
+ intervalMs,
367
+ transcriptPath,
368
+ started
369
+ } });
370
+ const active = worker;
371
+ worker.on("error", () => {
372
+ if (worker === active) halt();
373
+ });
374
+ worker.unref();
375
+ } catch {
376
+ halt();
377
+ }
378
+ }
296
379
  },
297
- say(next) {
298
- text = next;
380
+ say(text, measure = null) {
381
+ if (!animate) return;
382
+ pause();
383
+ render(text, measure);
299
384
  draw();
385
+ worker?.postMessage({ frames, frame });
386
+ if (control && !suspended) Atomics.store(control, 0, 0);
387
+ },
388
+ pause() {
389
+ if (suspended) return;
390
+ suspended = true;
391
+ pause();
392
+ if (animate && frames.length) write("\r\x1B[2K");
393
+ },
394
+ resume() {
395
+ suspended = false;
396
+ if (control) Atomics.store(control, 0, 0);
397
+ if (timer || worker) draw();
300
398
  },
301
399
  stop() {
302
- if (timer) clearInterval(timer);
303
- timer = null;
304
- clear();
400
+ halt();
401
+ frames = [];
402
+ if (animate) write("\r\x1B[2K");
305
403
  }
306
404
  };
307
405
  }
308
406
  var SPINNER_FRAMES = Object.freeze([...FRAMES]);
407
+
408
+ // ../installer/face/src/conformance.ts
309
409
  var ALLOWED = new Set(Object.values(GLYPH));
410
+ var FORBIDDEN = /* @__PURE__ */ new Map([
411
+ ["\u2713", "U+2713 is not in the guidebook glyph set; the check is U+2714"],
412
+ ["\u2717", "U+2717 is not in the guidebook glyph set; the cross is U+2716"],
413
+ ["\u21BB", "U+21BB is not in the guidebook glyph set; a retry uses the dot U+25CF"],
414
+ ["\u250C", "U+250C is clack's intro corner; the welcome opens with the branded diamond"],
415
+ ["\u251C", "U+251C is clack's note frame; a receipt box pairs U+256D/U+256E over U+2570/U+256F"],
416
+ ["\u2514", "U+2514 is clack's outro corner; the sign-off carries the diamond"]
417
+ ]);
418
+ var EMOJI = /[\u{1f300}-\u{1faff}\u{2600}-\u{27bf}]/u;
419
+ function fail(rule, detail) {
420
+ throw new Error(`installer face: ${rule} \u2014 ${detail}`);
421
+ }
422
+ function assertFaceConformance(lines, options) {
423
+ const identity = identityFor(options.product);
424
+ const width = options.columns ?? 100;
425
+ const rendered = lines.flatMap((line) => String(line).split("\n"));
426
+ const plain = rendered.map(stripColor);
427
+ const head = plain[0] ?? "";
428
+ const signOffLine = `${GLYPH.diamond} ${identity.name} \xB7 Mutatis Mutandis`;
429
+ const opensWithWelcome = head.startsWith(GLYPH.diamond) && head !== signOffLine;
430
+ if (options.complete && options.tty !== false) {
431
+ const welcome = `${GLYPH.diamond} ${identity.name} \u2014 Mutatis Mutandis`;
432
+ const welcomes = plain.filter((line) => line === welcome).length;
433
+ const receipts = plain.filter((line) => line.startsWith(GLYPH.boxTop)).length;
434
+ const signOffs = plain.filter((line) => line === signOffLine).length;
435
+ if (welcomes !== (options.nested ? 0 : 1) || signOffs !== (options.nested ? 0 : 1) || !options.nested && receipts !== 1) {
436
+ fail("complete run", "expected one console owner, one welcome, one receipt and one sign-off");
437
+ }
438
+ for (const line of plain) {
439
+ if (line && ![GLYPH.bar, GLYPH.diamond, GLYPH.hollow, GLYPH.boxTop, GLYPH.boxBottom].some((glyph) => line.startsWith(glyph))) {
440
+ fail("complete run", `unframed output: ${JSON.stringify(line)}`);
441
+ }
442
+ }
443
+ }
444
+ if (rendered.length === 0) fail("R7 render before you ship", "no rendered lines were given to the guard");
445
+ const greetings = plain.filter((line) => ["\u2014", "-"].some((separator) => line.includes(`${identity.name} ${separator} Mutatis Mutandis`)));
446
+ if (greetings.length > 1) fail("R8 one welcome per run", "duplicate product greeting");
447
+ if (plain.filter((line) => line.startsWith(GLYPH.boxTop)).length > 1) {
448
+ fail("R8 one receipt per run", "duplicate receipt");
449
+ }
450
+ if (options.nested === true) {
451
+ if (opensWithWelcome) {
452
+ fail("R8 a nested run draws no welcome", `an outer console already greeted the user; this run opened with ${JSON.stringify(head)}`);
453
+ }
454
+ const signOffAt = plain.findIndex((line) => line === signOffLine);
455
+ if (signOffAt !== -1) {
456
+ fail("R8 a nested run draws no sign-off", `the outer console closes the run; line ${signOffAt + 1} signs off`);
457
+ }
458
+ const boxAt = plain.findIndex((line) => line.startsWith(GLYPH.boxTop));
459
+ if (boxAt !== -1) {
460
+ const headline = plain.slice(boxAt + 1).find((line) => line.startsWith(GLYPH.bar) && line.trim() !== GLYPH.bar) ?? "";
461
+ if (headline.includes(GLYPH.check)) {
462
+ fail("R8 a nested run draws no success receipt", `the outer console owns the outcome; this run printed ${JSON.stringify(headline.trim())}`);
463
+ }
464
+ }
465
+ }
466
+ if (options.tty === false) {
467
+ for (const line of plain) {
468
+ const glyph = [...line].find((char) => ALLOWED.has(char));
469
+ if (glyph) fail("non-TTY lane is plain", `a piped run emitted ${JSON.stringify(glyph)} in ${JSON.stringify(line)}`);
470
+ }
471
+ for (const line of rendered) {
472
+ if (line.includes("\x1B[")) fail("non-TTY lane is plain", `a piped run emitted colour in ${JSON.stringify(line)}`);
473
+ }
474
+ return;
475
+ }
476
+ for (const line of plain) {
477
+ if (line.startsWith(`${GLYPH.bar} ${GLYPH.hollow} `)) {
478
+ fail("note step uses U+25CF", `U+25C7 is the note-box opener in ${JSON.stringify(line)}`);
479
+ }
480
+ }
481
+ for (const line of plain) {
482
+ if (/[\u25c6\u25c7\u2714\u2716\u25cf] (?! )/u.test(line)) {
483
+ fail("R4 two spaces after a glyph", `a single space follows the glyph in ${JSON.stringify(line)}`);
484
+ }
485
+ }
486
+ for (const line of plain) {
487
+ for (const [glyph, why] of FORBIDDEN) {
488
+ if (line.includes(glyph)) fail("glyph set", `${why} (in ${JSON.stringify(line)})`);
489
+ }
490
+ const emoji = [...line].find((char) => EMOJI.test(char) && !ALLOWED.has(char));
491
+ if (emoji) fail("glyph set", `emoji ${JSON.stringify(emoji)} in ${JSON.stringify(line)}`);
492
+ }
493
+ const top = plain.findIndex((line) => line.startsWith(GLYPH.boxTop));
494
+ if (top !== -1) {
495
+ const bottom = plain.findIndex((line, index) => index > top && line.startsWith(GLYPH.boxBottom));
496
+ if (bottom === -1) fail("R5 receipt frame", "the receipt box opens and never closes");
497
+ if (!plain[top].endsWith(GLYPH.boxTopEnd)) fail("R5 receipt frame", `the top row does not pair its corners: ${JSON.stringify(plain[top])}`);
498
+ if (!plain[bottom].endsWith(GLYPH.boxBottomEnd)) fail("R5 receipt frame", `the bottom row does not pair its corners: ${JSON.stringify(plain[bottom])}`);
499
+ const widths = new Set(plain.slice(top, bottom + 1).map(visibleWidth));
500
+ if (widths.size !== 1) fail("R5 receipt frame", `the box is ragged: widths ${[...widths].join(", ")}`);
501
+ const rows = plain.slice(top + 1, bottom);
502
+ if (rows.length > 0 && rows[0].includes(GLYPH.check)) {
503
+ if (rows.length < 3) {
504
+ 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()))}`);
505
+ }
506
+ const changed = rows[1].replace(GLYPH.bar, "").trim();
507
+ if (changed === "") fail("R9 the receipt says what changed", "the middle line of the success receipt is blank");
508
+ }
509
+ }
510
+ const inReceipt = (index) => top !== -1 && index >= top && index <= plain.findIndex((line, at) => at > top && line.startsWith(GLYPH.boxBottom));
511
+ const welcomeEnd = opensWithWelcome ? 4 : 0;
512
+ for (const [index, line] of plain.entries()) {
513
+ if (index < welcomeEnd || inReceipt(index)) continue;
514
+ if (!line.startsWith(GLYPH.bar) || line.trim() === GLYPH.bar) continue;
515
+ const after = [...line.slice(1)];
516
+ const at = after.findIndex((char) => char !== " ");
517
+ const edge = at + 2;
518
+ if (edge === 4 && ALLOWED.has(after[at])) continue;
519
+ if (edge !== TITLE_COLUMN + 1) {
520
+ 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)}`);
521
+ }
522
+ }
523
+ for (const line of plain) {
524
+ if (/\(node:\d+\)|DeprecationWarning|ExperimentalWarning/u.test(line)) {
525
+ 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`);
526
+ }
527
+ }
528
+ const receiptStart = top === -1 ? plain.length : top;
529
+ for (const line of plain.slice(0, receiptStart)) {
530
+ if (/[\u2714\u2716\u25cf\u25c7]/u.test(line) && /([A-Za-z]:\\|\/(?:home|Users|opt|usr)\/)/u.test(line)) {
531
+ fail("R3 paths live in the receipt", `a step line carries a path: ${JSON.stringify(line)}`);
532
+ }
533
+ }
534
+ for (const line of plain) {
535
+ if (visibleWidth(line) > width) {
536
+ fail("width cap", `a line is ${visibleWidth(line)} columns wide, cap ${width}: ${JSON.stringify(line)}`);
537
+ }
538
+ }
539
+ if (opensWithWelcome) {
540
+ const expected = `${GLYPH.diamond} ${identity.name} \u2014 Mutatis Mutandis`;
541
+ if (head !== expected) fail("welcome is the product table's", `expected ${JSON.stringify(expected)}, got ${JSON.stringify(head)}`);
542
+ const warm = plain.slice(1, 4).find((line) => line.includes(identity.warm) || line.includes(identity.installWarm));
543
+ if (!warm) fail("welcome is the product table's", `the warm line for ${options.product} is missing or reworded`);
544
+ }
545
+ const painted = rendered.join("\n");
546
+ if (painted.includes("\x1B[")) {
547
+ if (!painted.includes(identity.accent) && !painted.includes(PALETTE.muted)) {
548
+ fail("accent", `${options.product} rendered colour that is neither its accent nor a neutral token`);
549
+ }
550
+ }
551
+ }
552
+
553
+ // ../installer/face/src/run.ts
554
+ var import_node_fs4 = require("node:fs");
555
+
556
+ // ../installer/face/src/outcome.ts
557
+ var import_node_fs3 = require("node:fs");
558
+ var counts = ["total", "updated", "failed"];
559
+ var strings = ["version", "retry", "detail", "logPath"];
560
+ var flags = ["dryRun", "installed", "deferred", "operationFailed"];
561
+ function validateInstallerOutcome(value) {
562
+ const invalid = () => {
563
+ throw new Error("installer outcome: invalid child result");
564
+ };
565
+ if (!value || typeof value !== "object" || Array.isArray(value)) return invalid();
566
+ const facts = value;
567
+ const allowed = [...counts, ...strings, ...flags, "rollback", "logState"];
568
+ if (Object.keys(facts).some((key) => !allowed.includes(key))) return invalid();
569
+ for (const key of counts) if (!Number.isSafeInteger(facts[key]) || facts[key] < 0) return invalid();
570
+ if (facts.updated + facts.failed > facts.total) return invalid();
571
+ for (const key of strings) if (facts[key] !== void 0 && (typeof facts[key] !== "string" || facts[key].length > 4096)) return invalid();
572
+ for (const key of flags) if (facts[key] !== void 0 && typeof facts[key] !== "boolean") return invalid();
573
+ if (facts.rollback !== void 0 && !["completed", "partial", "not-needed", "unknown"].includes(facts.rollback)) return invalid();
574
+ if (facts.logState !== void 0 && !["unavailable", "omitted"].includes(facts.logState)) return invalid();
575
+ if (facts.logPath !== void 0 && (!facts.logPath.trim() || /[\x00-\x1f\x7f]/u.test(facts.logPath))) return invalid();
576
+ return { ...facts };
577
+ }
578
+ function writeInstallerOutcome(path, value) {
579
+ (0, import_node_fs3.writeFileSync)(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
580
+ }
581
+
582
+ // ../installer/face/src/run.ts
583
+ function validateInstallerProduct(value) {
584
+ const fail2 = (field) => {
585
+ throw new Error(`installer product: invalid ${field}`);
586
+ };
587
+ if (!value || typeof value !== "object" || Array.isArray(value)) return fail2("declaration");
588
+ const input = value;
589
+ const text = (value2, field) => typeof value2 === "string" && value2.trim() && !/[\r\n\x00-\x1f]/u.test(value2) ? value2 : fail2(field);
590
+ const key = text(input.product, "product");
591
+ const product = { mmi: "mmi-hub", jerv: "jerv-hub" }[key] ?? key;
592
+ const identity = identityFor(product);
593
+ const gate = text(input.gate, "gate");
594
+ let gateUrl;
595
+ try {
596
+ gateUrl = new URL(gate);
597
+ } catch {
598
+ return fail2("gate");
599
+ }
600
+ if (!["https:", "http:"].includes(gateUrl.protocol) || gateUrl.username || gateUrl.password) return fail2("gate");
601
+ const doctor = text(input.doctor, "doctor");
602
+ if (doctor !== identity.doctor) return fail2("doctor");
603
+ if (!Array.isArray(input.surfaces) || input.surfaces.length === 0) return fail2("surfaces");
604
+ const ids = /* @__PURE__ */ new Set();
605
+ const surfaces = input.surfaces.map((raw) => {
606
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail2("surface");
607
+ const source = raw;
608
+ const id = text(source.id, "surface.id");
609
+ if (ids.has(id)) return fail2("duplicate surface.id");
610
+ ids.add(id);
611
+ const surface = { id };
612
+ for (const field of ["npm", "bin", "kind", "activation"]) {
613
+ if (source[field] !== void 0) surface[field] = text(source[field], `surface.${field}`);
614
+ }
615
+ if (Boolean(surface.npm) !== Boolean(surface.bin)) return fail2("surface npm/bin pair");
616
+ if (surface.kind !== void 0 && !["agent-home", "payload"].includes(surface.kind)) return fail2("surface.kind");
617
+ if (!surface.npm && !surface.kind) return fail2("surface implementation");
618
+ return surface;
619
+ });
620
+ return { product, gate, doctor, surfaces };
621
+ }
622
+ var PHASES = {
623
+ preflight: ["Checking prerequisites", "Checked prerequisites"],
624
+ resolve: ["Resolving the release", "Resolved the release"],
625
+ download: ["Downloading the payload", "Downloaded the payload"],
626
+ "sign-in": ["Signing in", "Signed in"],
627
+ check: ["Checking surfaces", "Checked surfaces"],
628
+ arm: ["Scheduling updates", "Armed hourly updates"],
629
+ "verify-release": ["Checking the release version", "Verified the release version"],
630
+ verify: ["Verifying the payload", "Verified the payload"],
631
+ install: ["Installing the product", "Installed the product"],
632
+ configure: ["Configuring the product", "Configured the product"],
633
+ activate: ["Activating surfaces", "Activated surfaces"],
634
+ doctor: ["Checking health", "Checked health"],
635
+ rollback: ["Restoring the previous version", "Restored the previous version"]
636
+ };
637
+ function createInstallerRun(value, options = {}) {
638
+ const declaration = validateInstallerProduct(value);
639
+ const env = options.env ?? process.env;
640
+ const tty = options.tty ?? Boolean(process.stdout.isTTY);
641
+ const face = createFace({
642
+ operation: options.operation,
643
+ product: declaration.product,
644
+ columns: options.columns,
645
+ env,
646
+ color: tty && options.color !== false && env.NO_COLOR === void 0 && env.TERM !== "dumb"
647
+ });
648
+ const errors = [];
649
+ const write = options.write ? (text, channel) => {
650
+ try {
651
+ options.write(text, channel);
652
+ } catch (error) {
653
+ errors.push(`installer output observer: ${error instanceof Error ? error.message : String(error)}`);
654
+ }
655
+ } : (text, channel) => {
656
+ (channel === "stdout" ? process.stdout : process.stderr).write(text);
657
+ };
658
+ const emit = (text, channel = "stdout", recorded = text) => {
659
+ if (!text) return;
660
+ write(text, channel);
661
+ if (env.MM_FACE_TRANSCRIPT) {
662
+ (0, import_node_fs4.appendFileSync)(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
663
+ `, "utf8");
664
+ }
665
+ };
666
+ const lines = (rows, channel = "stdout") => {
667
+ for (const row of rows.flatMap((row2) => row2.split("\n"))) if (row) emit(`${row}
668
+ `, channel);
669
+ };
670
+ const spinner = createSpinner(face, {
671
+ animate: tty && env.TERM !== "dumb" && (options.animate ?? Boolean(process.stderr.isTTY)),
672
+ ...options.write ? { stream: { write: (text) => {
673
+ emit(String(text), "spinner");
674
+ return true;
675
+ } } } : { transcriptPath: env.MM_FACE_TRANSCRIPT }
676
+ });
677
+ let activeTitle;
678
+ let chunkOpen = false;
679
+ let chunkChannel = "stdout";
680
+ let chunkColumn = 0;
681
+ const trailingCR = /* @__PURE__ */ new Set();
682
+ let deferredActivity;
683
+ const closeChunk = () => {
684
+ if (chunkOpen) emit("\n", chunkChannel);
685
+ chunkOpen = false;
686
+ chunkColumn = 0;
687
+ };
688
+ let started = false;
689
+ let finished = false;
690
+ const start = () => {
691
+ if (started || finished) return;
692
+ started = true;
693
+ if (options.quiet) return;
694
+ const welcome = face.welcome();
695
+ if (tty) lines(welcome);
696
+ else if (welcome.length) {
697
+ const warm = options.operation === "install" ? face.identity.installWarm : face.identity.warm;
698
+ lines([`${face.identity.name} - Mutatis Mutandis`, warm.replaceAll("\u2014", "-").replaceAll("\u2026", "...")]);
699
+ }
700
+ };
701
+ const durable = (title, measure, kind) => {
702
+ closeChunk();
703
+ activeTitle = void 0;
704
+ deferredActivity = void 0;
705
+ spinner.stop();
706
+ const rendered = face.step(title, measure, kind);
707
+ if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
708
+ };
709
+ const run = {
710
+ get errors() {
711
+ return [...errors];
712
+ },
713
+ start,
714
+ phase(id, facts = {}) {
715
+ if (finished) throw new Error("installer run already finished");
716
+ if (!Object.hasOwn(PHASES, id)) throw new Error("installer run: unknown phase");
717
+ start();
718
+ const state = facts.state ?? "ok";
719
+ const title = PHASES[id][state === "ok" ? 1 : 0];
720
+ if (state === "running") {
721
+ if (face.progress(title, facts.measure ?? null)) return;
722
+ if (activeTitle === title) spinner.say(title, facts.measure ?? null);
723
+ else {
724
+ activeTitle = title;
725
+ spinner.start(title, facts.measure ?? null);
726
+ }
727
+ return;
728
+ }
729
+ if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
730
+ if (facts.detail) run.relay(facts.detail);
731
+ },
732
+ surface(facts) {
733
+ if (finished) throw new Error("installer run already finished");
734
+ const surface = declaration.surfaces.find((surface2) => surface2.id === facts.id);
735
+ if (!surface) throw new Error("installer run: undeclared surface");
736
+ start();
737
+ const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} ${tty ? "\u2192" : "->"} ${facts.to}` : ` ${facts.to}` : "";
738
+ const status = { updated: options.dryRun ? "would update" : "updated", current: "already current", failed: "failed", skipped: "skipped", retry: "retrying", pending: "pending", kept: "kept" }[facts.state];
739
+ if (!status) throw new Error("installer run: unknown surface state");
740
+ const separator = tty ? "\xB7" : "-";
741
+ const activation = facts.state === "updated" && surface.activation ? ` ${separator} ${surface.activation}` : "";
742
+ const completed = ["updated", "current", "kept", "skipped"].includes(facts.state);
743
+ durable(`${facts.id}${versions} ${separator} ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
744
+ if (facts.detail) run.relay(facts.detail);
745
+ },
746
+ milestone({ step, state, ms, measure }) {
747
+ start();
748
+ if (state === "running") {
749
+ if (chunkOpen) {
750
+ deferredActivity = { title: step, measure: measure ?? null };
751
+ return;
752
+ }
753
+ if (activeTitle === step) spinner.say(step, measure ?? null);
754
+ else {
755
+ activeTitle = step;
756
+ spinner.start(step, measure ?? null);
757
+ }
758
+ return;
759
+ }
760
+ durable(step, ms === void 0 ? null : ms / 1e3, state);
761
+ },
762
+ signIn({ url, code }) {
763
+ start();
764
+ spinner.stop();
765
+ for (const text of [`Open ${url}`, `Enter code: ${code}`]) {
766
+ const rendered = `${tty ? face.relay(text) : text}
767
+ `;
768
+ emit(rendered, "stdout", rendered.replace(code, "[redacted]"));
769
+ }
770
+ },
771
+ // Only pass safe diagnostic text, never authentication output or credentials.
772
+ relay(text, channel = "stdout", record = true) {
773
+ closeChunk();
774
+ spinner.pause();
775
+ const rendered = tty ? face.relay(text) : stripColor(text);
776
+ for (const row of rendered.split("\n")) if (row) {
777
+ emit(`${row}
778
+ `, channel, record ? `${row}
779
+ ` : `${tty ? face.relay("[external output omitted]") : "[external output omitted]"}
780
+ `);
781
+ }
782
+ spinner.resume();
783
+ },
784
+ relayChunk(text, channel = "stdout") {
785
+ spinner.pause();
786
+ if (chunkOpen && chunkChannel !== channel) closeChunk();
787
+ chunkChannel = channel;
788
+ if (trailingCR.delete(channel) && text.startsWith("\n")) text = text.slice(1);
789
+ if (text.endsWith("\r")) trailingCR.add(channel);
790
+ const parts = text.replace(/\r\n|\r/g, "\n").split("\n");
791
+ for (let i = 0; i < parts.length; i++) {
792
+ const characters = [...stripColor(parts[i])];
793
+ while (characters.length) {
794
+ if (tty && chunkColumn >= face.width - 6) closeChunk();
795
+ const part = characters.splice(0, tty ? face.width - 6 - chunkColumn : characters.length).join("");
796
+ emit(tty && !chunkOpen ? face.relay(part) : part, channel, chunkOpen ? "" : tty ? face.relay("[external output omitted]") : "[external output omitted]");
797
+ chunkColumn += [...part].length;
798
+ chunkOpen = true;
799
+ }
800
+ if (i < parts.length - 1) {
801
+ emit("\n", channel);
802
+ chunkOpen = false;
803
+ chunkColumn = 0;
804
+ }
805
+ }
806
+ if (!chunkOpen) {
807
+ if (deferredActivity) {
808
+ activeTitle = deferredActivity.title;
809
+ spinner.start(deferredActivity.title, deferredActivity.measure);
810
+ deferredActivity = void 0;
811
+ } else spinner.resume();
812
+ }
813
+ },
814
+ cancel() {
815
+ if (finished) return;
816
+ if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(
817
+ env.MM_INSTALLER_OUTCOME_FILE,
818
+ { total: 0, updated: 0, failed: 0, deferred: true, detail: "Operation cancelled." }
819
+ );
820
+ start();
821
+ closeChunk();
822
+ activeTitle = void 0;
823
+ spinner.stop();
824
+ finished = true;
825
+ if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE) {
826
+ lines(tty ? face.receipt(["Operation cancelled."], { ready: false }) : ["Operation cancelled."]);
827
+ }
828
+ if (tty) lines([face.signOff()]);
829
+ },
830
+ finish(facts) {
831
+ if (finished) return;
832
+ validateInstallerOutcome(facts);
833
+ if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(env.MM_INSTALLER_OUTCOME_FILE, facts);
834
+ start();
835
+ closeChunk();
836
+ activeTitle = void 0;
837
+ spinner.stop();
838
+ finished = true;
839
+ if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
840
+ 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}.`;
841
+ const ready = facts.failed === 0 && !facts.dryRun && !facts.deferred && !facts.operationFailed && Boolean(facts.version);
842
+ const body = [
843
+ `${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."}`,
844
+ changed,
845
+ ...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
846
+ ...facts.detail ? [facts.detail] : [],
847
+ ...facts.rollback ? [`Rollback: ${facts.rollback}`] : [],
848
+ ...facts.logPath ? [`Log: ${facts.logPath}`] : facts.logState ? [`Log: ${facts.logState}`] : [],
849
+ facts.retry && (facts.failed > 0 || facts.operationFailed) ? `Retry: ${facts.retry}` : `Check health any time: ${declaration.doctor}`
850
+ ];
851
+ if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE && (facts.failed > 0 || facts.operationFailed)) {
852
+ lines(tty ? face.receipt(body, { ready }) : body.map((row) => row.replace(/^[✔✖●] /u, "")));
853
+ }
854
+ if (tty) lines([face.signOff()]);
855
+ },
856
+ stop() {
857
+ closeChunk();
858
+ activeTitle = void 0;
859
+ spinner.stop();
860
+ }
861
+ };
862
+ return run;
863
+ }
864
+
865
+ // ../installer/face/src/maintenance.ts
866
+ async function runMaintenance(product, args, dependencies) {
867
+ const json = args.includes("--json");
868
+ const quiet = args.includes("--quiet");
869
+ const dryRun = args.includes("--dry-run");
870
+ const installed = args.includes("install");
871
+ const render = dependencies.render;
872
+ const env = render?.env ?? process.env;
873
+ const tty = render?.tty ?? Boolean(process.stdout.isTTY);
874
+ const run = json ? null : dependencies.run ?? createInstallerRun(product, {
875
+ ...render,
876
+ tty,
877
+ env,
878
+ color: tty && render?.color !== false && !args.includes("--no-color") && env.TERM !== "dumb",
879
+ columns: render?.columns ?? process.stdout.columns,
880
+ dryRun,
881
+ quiet,
882
+ operation: installed ? "install" : "update"
883
+ });
884
+ if (!quiet) run?.start();
885
+ let active;
886
+ let activeFacts;
887
+ let handedOff;
888
+ const shown = new Set((env.MM_FACE_CONTINUES ?? "").split(",").map((value) => value.trim()).filter(Boolean));
889
+ if (!quiet && !json) shown.add("welcome");
890
+ let phaseFailed = false;
891
+ let armFailed = false;
892
+ let knownFailure;
893
+ const phaseStarted = /* @__PURE__ */ new Map();
894
+ const phase = (event) => {
895
+ if (handedOff !== void 0) return;
896
+ if (event.state === "fail") phaseFailed = true;
897
+ const started = phaseStarted.get(event.id);
898
+ if (event.state === "running") {
899
+ if (started === void 0) phaseStarted.set(event.id, Date.now());
900
+ active = event.id;
901
+ } else {
902
+ phaseStarted.delete(event.id);
903
+ if (active === event.id) active = void 0;
904
+ }
905
+ if (!quiet && event.state === "running" && event.detail) run?.relay(event.detail);
906
+ const measured = event.measured;
907
+ 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;
908
+ const facts = {
909
+ state: event.state,
910
+ detail: event.detail,
911
+ measure,
912
+ ...event.state !== "running" && started !== void 0 ? { seconds: (Date.now() - started) / 1e3 } : {}
913
+ };
914
+ if (event.state === "running") activeFacts = facts;
915
+ if (!quiet) {
916
+ run?.phase(event.id, facts);
917
+ if (event.state === "ok" && run) shown.add(event.id);
918
+ }
919
+ };
920
+ const pending = [];
921
+ let streaming = false;
922
+ const emitArm = (arm) => {
923
+ if (quiet) return;
924
+ run?.surface({
925
+ id: arm.surface,
926
+ from: arm.from ?? void 0,
927
+ to: arm.to ?? void 0,
928
+ state: arm.verdict === "fail" ? "failed" : arm.verdict === "defer" ? "retry" : arm.verdict === "skip" ? "kept" : arm.from === arm.to && !arm.launchStaged ? "current" : "updated",
929
+ ...arm.durationMs === void 0 ? {} : { seconds: arm.durationMs / 1e3 },
930
+ detail: dependencies.diagnose?.(arm)
931
+ });
932
+ };
933
+ const writeJson = (summary, schedule) => {
934
+ process.stdout.write(JSON.stringify(dependencies.jsonResult ? dependencies.jsonResult(summary, schedule) : installed ? { convergence: summary, autoupdate: schedule } : summary, null, 2) + "\n");
935
+ };
936
+ try {
937
+ const summary = await dependencies.engine({
938
+ dryRun,
939
+ withCustody: async (operation) => {
940
+ if (handedOff !== void 0) throw new Error("installer terminal ownership already transferred");
941
+ run?.stop();
942
+ const result = await operation({ faceContinues: [...shown] });
943
+ if (result.kind === "reexec") {
944
+ if (!Number.isSafeInteger(result.code) || result.code < 0) throw new Error("invalid handoff exit code");
945
+ handedOff = result.code;
946
+ } else if (result.kind === "resume") {
947
+ if (!quiet && active) run?.phase(active, activeFacts);
948
+ } else throw new Error("invalid terminal custody result");
949
+ return result;
950
+ },
951
+ shippedTarget: installed ? dependencies.shippedTarget : void 0,
952
+ narrate: !quiet && !json && args.includes("--verbose") ? (text) => {
953
+ if (handedOff === void 0) run?.relay(text, "stderr", false);
954
+ } : void 0,
955
+ relay: (text, channel = "stdout", record = false) => {
956
+ if (handedOff === void 0) run?.relay(text, channel, record);
957
+ },
958
+ onTargetResolved: (target) => phase({ id: "verify-release", detail: target, state: "ok" }),
959
+ onPhase: () => {
960
+ if (!quiet && handedOff === void 0) run?.phase("check", { state: "running" });
961
+ },
962
+ onPhaseEvent: phase,
963
+ onArm: (arm) => {
964
+ if (handedOff !== void 0) return;
965
+ if (arm.verdict === "fail") armFailed = true;
966
+ pending.push(arm);
967
+ if (pending.length > 1) streaming = true;
968
+ if (streaming) for (const result of pending.splice(0)) emitArm(result);
969
+ }
970
+ });
971
+ if (handedOff !== void 0) return handedOff;
972
+ if (summary.reexec) {
973
+ if (!dependencies.handOff) {
974
+ knownFailure = "The updated version needs a handoff, but no handoff is configured.";
975
+ throw new Error(knownFailure);
976
+ }
977
+ run?.stop();
978
+ knownFailure = "The updated updater could not launch.";
979
+ return await dependencies.handOff(summary.reexec, args, { ...process.env, MM_FACE_CONTINUES: "welcome" });
980
+ }
981
+ const counts2 = dependencies.counts(summary);
982
+ validateInstallerOutcome({ total: counts2.total, updated: counts2.updated, failed: counts2.failed });
983
+ if (!Number.isSafeInteger(counts2.retry) || counts2.retry < 0 || counts2.retry > counts2.total) throw new Error("invalid retry count");
984
+ if (summary.cancelled && summary.exit === 0 && !phaseFailed && !armFailed && counts2.failed === 0 && counts2.updated === 0) {
985
+ if (json) writeJson(summary, null);
986
+ else run?.cancel();
987
+ return 0;
988
+ }
989
+ for (const arm of pending) emitArm(arm);
990
+ const waiting = summary.surfaces.filter((surface) => surface.present && surface.action === "arm-pending");
991
+ if (!quiet) for (const surface of waiting) run?.surface({ id: surface.id, state: "pending" });
992
+ let schedule = null;
993
+ if (installed && !dryRun && dependencies.schedule) {
994
+ phase({ id: "arm", state: "running" });
995
+ schedule = dependencies.schedule();
996
+ if (schedule) phase({ id: "arm", state: schedule.ok ? "ok" : "fail", detail: schedule.ok ? void 0 : schedule.detail });
997
+ }
998
+ const failed = phaseFailed || armFailed || counts2.failed > 0 || Boolean(schedule && !schedule.ok);
999
+ const exit = Math.max(summary.exit, failed ? 1 : 0);
1000
+ if (json) {
1001
+ writeJson({ ...summary, exit }, schedule);
1002
+ } else {
1003
+ run.finish({
1004
+ version: summary.target ?? void 0,
1005
+ total: counts2.total,
1006
+ updated: counts2.updated,
1007
+ failed: counts2.failed,
1008
+ operationFailed: exit !== 0,
1009
+ deferred: !failed && Boolean(counts2.retry || waiting.length || summary.deferred || summary.cancelled || !summary.target),
1010
+ dryRun,
1011
+ installed,
1012
+ detail: schedule && !schedule.ok ? schedule.detail : summary.detail ?? (summary.cancelled ? "Operation cancelled." : void 0),
1013
+ retry: summary.retry,
1014
+ rollback: summary.rollback,
1015
+ logPath: summary.logPath,
1016
+ logState: summary.logState
1017
+ });
1018
+ }
1019
+ return exit;
1020
+ } catch (error) {
1021
+ if (handedOff !== void 0) throw error;
1022
+ let detail = knownFailure ?? (active === "sign-in" ? "Sign-in did not complete." : installed ? "Installation did not complete." : "Update did not complete.");
1023
+ if (!knownFailure && dependencies.safeErrorDetail) {
1024
+ try {
1025
+ detail = dependencies.safeErrorDetail(error);
1026
+ } catch {
1027
+ }
1028
+ }
1029
+ if (active) phase({ id: active, state: "fail" });
1030
+ run?.relay(detail, "stderr");
1031
+ run?.finish({ total: 0, updated: 0, failed: 0, operationFailed: true, detail });
1032
+ if (json) process.stdout.write(JSON.stringify({ error: detail }) + "\n");
1033
+ return 1;
1034
+ } finally {
1035
+ run?.stop();
1036
+ }
1037
+ }
1038
+
1039
+ // ../installer.product.json
1040
+ var installer_product_default = {
1041
+ product: "mmi",
1042
+ gate: "https://gate.mutmut.work/mmi",
1043
+ doctor: "mmi doctor",
1044
+ surfaces: [
1045
+ { id: "updater", npm: "@mutmutco/hub", bin: "mmi-hub" },
1046
+ { id: "cli", npm: "@mutmutco/cli", bin: "mmi-cli" },
1047
+ { id: "claude", kind: "agent-home", activation: "next launch" },
1048
+ { id: "codex", kind: "agent-home", activation: "next launch" },
1049
+ { id: "cursor", kind: "agent-home", activation: "next launch" },
1050
+ { id: "hermes", kind: "agent-home", activation: "next launch" },
1051
+ { id: "jervcode", kind: "agent-home", activation: "next launch" }
1052
+ ]
1053
+ };
310
1054
 
311
1055
  // src/state.ts
312
- var import_node_fs = require("node:fs");
1056
+ var import_node_fs5 = require("node:fs");
313
1057
  var import_node_os = require("node:os");
314
1058
  var import_node_path = require("node:path");
315
1059
  function stateRoot(env = process.env) {
@@ -323,8 +1067,8 @@ function statePaths(env = process.env) {
323
1067
  }
324
1068
  function ensureState(env = process.env) {
325
1069
  const paths = statePaths(env);
326
- (0, import_node_fs.mkdirSync)(paths.staging, { recursive: true });
327
- (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(paths.gitCache), { recursive: true });
1070
+ (0, import_node_fs5.mkdirSync)(paths.staging, { recursive: true });
1071
+ (0, import_node_fs5.mkdirSync)((0, import_node_path.dirname)(paths.gitCache), { recursive: true });
328
1072
  return paths;
329
1073
  }
330
1074
  var UpdaterBusy = class extends Error {
@@ -334,15 +1078,15 @@ var LEASE_STALE_MS = 10 * 6e4;
334
1078
  function tryAcquire(path) {
335
1079
  const token = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
336
1080
  try {
337
- const fd = (0, import_node_fs.openSync)(path, "wx");
338
- (0, import_node_fs.writeFileSync)(fd, JSON.stringify({ token, pid: process.pid, ts: Date.now() }) + "\n");
339
- (0, import_node_fs.closeSync)(fd);
1081
+ const fd = (0, import_node_fs5.openSync)(path, "wx");
1082
+ (0, import_node_fs5.writeFileSync)(fd, JSON.stringify({ token, pid: process.pid, ts: Date.now() }) + "\n");
1083
+ (0, import_node_fs5.closeSync)(fd);
340
1084
  return {
341
1085
  token,
342
1086
  release() {
343
1087
  try {
344
1088
  const current2 = readLease(path);
345
- if (current2 && current2.token === token) (0, import_node_fs.unlinkSync)(path);
1089
+ if (current2 && current2.token === token) (0, import_node_fs5.unlinkSync)(path);
346
1090
  } catch {
347
1091
  }
348
1092
  }
@@ -353,7 +1097,7 @@ function tryAcquire(path) {
353
1097
  const current = readLease(path);
354
1098
  if (current && Date.now() - current.ts > LEASE_STALE_MS) {
355
1099
  try {
356
- (0, import_node_fs.renameSync)(path, `${path}.stale-${Date.now()}`);
1100
+ (0, import_node_fs5.renameSync)(path, `${path}.stale-${Date.now()}`);
357
1101
  return "stolen-stale";
358
1102
  } catch {
359
1103
  }
@@ -362,7 +1106,7 @@ function tryAcquire(path) {
362
1106
  }
363
1107
  function readLease(path) {
364
1108
  try {
365
- return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
1109
+ return JSON.parse((0, import_node_fs5.readFileSync)(path, "utf8"));
366
1110
  } catch {
367
1111
  return null;
368
1112
  }
@@ -387,10 +1131,10 @@ function withLease(path, fn, waitMs = LEASE_WAIT_MS) {
387
1131
  }
388
1132
  function appendJournal(path, event) {
389
1133
  try {
390
- (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
391
- const fd = (0, import_node_fs.openSync)(path, "a");
392
- (0, import_node_fs.writeFileSync)(fd, JSON.stringify(event) + "\n");
393
- (0, import_node_fs.closeSync)(fd);
1134
+ (0, import_node_fs5.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
1135
+ const fd = (0, import_node_fs5.openSync)(path, "a");
1136
+ (0, import_node_fs5.writeFileSync)(fd, JSON.stringify(event) + "\n");
1137
+ (0, import_node_fs5.closeSync)(fd);
394
1138
  return true;
395
1139
  } catch {
396
1140
  return false;
@@ -409,7 +1153,7 @@ function journalHighWater(path, surface, events) {
409
1153
  function parseJournal(path) {
410
1154
  const events = [];
411
1155
  try {
412
- for (const line of (0, import_node_fs.readFileSync)(path, "utf8").split("\n")) {
1156
+ for (const line of (0, import_node_fs5.readFileSync)(path, "utf8").split("\n")) {
413
1157
  if (!line.trim()) continue;
414
1158
  try {
415
1159
  events.push(JSON.parse(line));
@@ -423,7 +1167,7 @@ function parseJournal(path) {
423
1167
  function journalSurfaceRepeatedFail(path, surface, currentRun) {
424
1168
  let lastVerdict = null;
425
1169
  try {
426
- for (const line of (0, import_node_fs.readFileSync)(path, "utf8").split("\n")) {
1170
+ for (const line of (0, import_node_fs5.readFileSync)(path, "utf8").split("\n")) {
427
1171
  if (!line.trim()) continue;
428
1172
  try {
429
1173
  const event = JSON.parse(line);
@@ -466,7 +1210,7 @@ function compareSemver(a, b) {
466
1210
  return va.pre.join(".") < vb.pre.join(".") ? -1 : va.pre.join(".") > vb.pre.join(".") ? 1 : 0;
467
1211
  }
468
1212
  function fileExists(path) {
469
- return (0, import_node_fs.existsSync)(path);
1213
+ return (0, import_node_fs5.existsSync)(path);
470
1214
  }
471
1215
 
472
1216
  // src/reconcile.ts
@@ -474,7 +1218,7 @@ var import_node_perf_hooks2 = require("node:perf_hooks");
474
1218
 
475
1219
  // src/npm.ts
476
1220
  var import_node_child_process = require("node:child_process");
477
- var import_node_fs2 = require("node:fs");
1221
+ var import_node_fs6 = require("node:fs");
478
1222
  var import_node_path2 = require("node:path");
479
1223
  var cachedNpm = null;
480
1224
  function resolveNpm(env = process.env) {
@@ -485,7 +1229,7 @@ function resolveNpm(env = process.env) {
485
1229
  const first = where.stdout.split("\n").map((l) => l.trim()).find(Boolean);
486
1230
  if (where.status === 0 && first) {
487
1231
  const script = (0, import_node_path2.join)((0, import_node_path2.dirname)(first), "node_modules", "npm", "bin", "npm-cli.js");
488
- if ((0, import_node_fs2.existsSync)(script)) {
1232
+ if ((0, import_node_fs6.existsSync)(script)) {
489
1233
  cachedNpm = { kind: "node-script", script };
490
1234
  return cachedNpm;
491
1235
  }
@@ -494,13 +1238,13 @@ function resolveNpm(env = process.env) {
494
1238
  }
495
1239
  }
496
1240
  const bundled = (0, import_node_path2.join)((0, import_node_path2.dirname)(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
497
- if ((0, import_node_fs2.existsSync)(bundled)) {
1241
+ if ((0, import_node_fs6.existsSync)(bundled)) {
498
1242
  cachedNpm = { kind: "node-script", script: bundled };
499
1243
  return cachedNpm;
500
1244
  }
501
1245
  const located = (0, import_node_child_process.spawnSync)("/bin/sh", ["-lc", "command -v npm"], { encoding: "utf8", windowsHide: true });
502
1246
  const resolved = (located.stdout ?? "").trim().split("\n").map((line) => line.trim()).find(Boolean);
503
- if (located.status === 0 && resolved && (0, import_node_fs2.existsSync)(resolved)) {
1247
+ if (located.status === 0 && resolved && (0, import_node_fs6.existsSync)(resolved)) {
504
1248
  cachedNpm = { kind: "command", command: resolved };
505
1249
  return cachedNpm;
506
1250
  }
@@ -582,7 +1326,7 @@ function npmVersions(packageName, env = process.env) {
582
1326
 
583
1327
  // src/gate.ts
584
1328
  var import_node_child_process2 = require("node:child_process");
585
- var import_node_fs3 = require("node:fs");
1329
+ var import_node_fs7 = require("node:fs");
586
1330
  function parseBom(text) {
587
1331
  const bom = JSON.parse(text);
588
1332
  if (typeof bom?.version !== "string" || typeof bom?.sourceCommit !== "string" || !Array.isArray(bom?.artifacts)) {
@@ -604,7 +1348,7 @@ function ensureBareCache(cachePath, repoUrl) {
604
1348
  if (probe.status !== 0) {
605
1349
  throw new GateEnvError("git is not available \u2014 the release gate needs a git client");
606
1350
  }
607
- if (!(0, import_node_fs3.existsSync)(cachePath)) {
1351
+ if (!(0, import_node_fs7.existsSync)(cachePath)) {
608
1352
  const init = (0, import_node_child_process2.spawnSync)("git", ["init", "--bare", cachePath], { encoding: "utf8", windowsHide: true });
609
1353
  if (init.status !== 0) throw new GateEnvError(`cannot create bare cache at ${cachePath}: ${init.stderr}`);
610
1354
  }
@@ -711,24 +1455,24 @@ async function checkAtomicity(cachePath, tagCommit, bom, candidate, npm) {
711
1455
  }
712
1456
 
713
1457
  // src/surfaces.ts
714
- var import_node_fs8 = require("node:fs");
1458
+ var import_node_fs12 = require("node:fs");
715
1459
  var import_node_os3 = require("node:os");
716
1460
  var import_node_path7 = require("node:path");
717
1461
 
718
1462
  // src/arm-host-plugins.ts
719
1463
  var import_node_child_process5 = require("node:child_process");
720
- var import_node_fs7 = require("node:fs");
1464
+ var import_node_fs11 = require("node:fs");
721
1465
  var import_node_os2 = require("node:os");
722
1466
  var import_node_path6 = require("node:path");
723
1467
 
724
1468
  // src/arm-npm-global.ts
725
- var import_node_fs5 = require("node:fs");
1469
+ var import_node_fs9 = require("node:fs");
726
1470
  var import_node_child_process4 = require("node:child_process");
727
1471
  var import_node_path4 = require("node:path");
728
1472
 
729
1473
  // src/pending.ts
730
1474
  var import_node_child_process3 = require("node:child_process");
731
- var import_node_fs4 = require("node:fs");
1475
+ var import_node_fs8 = require("node:fs");
732
1476
  var import_node_path3 = require("node:path");
733
1477
  function hubDistPath(env) {
734
1478
  const root = globalRoot(env);
@@ -743,7 +1487,7 @@ function watcherLockPath(paths) {
743
1487
  }
744
1488
  function readPending(paths) {
745
1489
  try {
746
- const parsed = JSON.parse((0, import_node_fs4.readFileSync)(pendingPath(paths), "utf8"));
1490
+ const parsed = JSON.parse((0, import_node_fs8.readFileSync)(pendingPath(paths), "utf8"));
747
1491
  if (!Array.isArray(parsed)) return [];
748
1492
  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));
749
1493
  } catch {
@@ -751,12 +1495,12 @@ function readPending(paths) {
751
1495
  }
752
1496
  }
753
1497
  function writePending(paths, records) {
754
- (0, import_node_fs4.mkdirSync)(paths.root, { recursive: true });
1498
+ (0, import_node_fs8.mkdirSync)(paths.root, { recursive: true });
755
1499
  const path = pendingPath(paths);
756
1500
  const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
757
- (0, import_node_fs4.writeFileSync)(tmp, `${JSON.stringify(records, null, 2)}
1501
+ (0, import_node_fs8.writeFileSync)(tmp, `${JSON.stringify(records, null, 2)}
758
1502
  `, "utf8");
759
- (0, import_node_fs4.renameSync)(tmp, path);
1503
+ (0, import_node_fs8.renameSync)(tmp, path);
760
1504
  }
761
1505
  function clearPending(paths, surface) {
762
1506
  const records = readPending(paths);
@@ -825,7 +1569,7 @@ function schedulePendingWatcher(paths, env = process.env, seam = {}) {
825
1569
  const live = records.find((row) => row.watcherPid && pidAlive(row.watcherPid));
826
1570
  if (live) return { scheduled: false, reason: `watcher pid ${live.watcherPid} is already waiting` };
827
1571
  const hubDist = seam.hubDist ?? hubDistPath(env);
828
- 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" };
1572
+ 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" };
829
1573
  const command = buildWatcherCommand({
830
1574
  images: [...new Set(records.flatMap((row) => row.images))],
831
1575
  dirs: [...new Set(records.flatMap((row) => row.dirs))],
@@ -885,7 +1629,7 @@ function tail(output) {
885
1629
  function manifestVersion(packageRoot) {
886
1630
  if (!packageRoot) return null;
887
1631
  try {
888
- const version = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(packageRoot, "package.json"), "utf8")).version;
1632
+ const version = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path4.join)(packageRoot, "package.json"), "utf8")).version;
889
1633
  return typeof version === "string" ? version : null;
890
1634
  } catch {
891
1635
  return null;
@@ -896,8 +1640,8 @@ function stagePackage(options) {
896
1640
  const stageRoot = (0, import_node_path4.join)(paths.staging, surface, target);
897
1641
  const packageRoot = (0, import_node_path4.join)(stageRoot, "node_modules", ...packageName.split("/"));
898
1642
  if (!fileExists((0, import_node_path4.join)(packageRoot, ...witness.split("/")))) {
899
- (0, import_node_fs5.mkdirSync)(stageRoot, { recursive: true });
900
- (0, import_node_fs5.writeFileSync)((0, import_node_path4.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
1643
+ (0, import_node_fs9.mkdirSync)(stageRoot, { recursive: true });
1644
+ (0, import_node_fs9.writeFileSync)((0, import_node_path4.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
901
1645
  const install = runNpm(["install", "--prefix", stageRoot, "--no-audit", "--no-fund", "--no-package-lock", `${packageName}@${target}`], env);
902
1646
  if (install.status !== 0) {
903
1647
  return { defer: `staging install failed: ${tail(install.stderr || install.stdout)}` };
@@ -905,7 +1649,7 @@ function stagePackage(options) {
905
1649
  }
906
1650
  let stagedManifestVersion;
907
1651
  try {
908
- stagedManifestVersion = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(packageRoot, "package.json"), "utf8")).version;
1652
+ stagedManifestVersion = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path4.join)(packageRoot, "package.json"), "utf8")).version;
909
1653
  } catch {
910
1654
  }
911
1655
  if (stagedManifestVersion !== target) {
@@ -963,11 +1707,11 @@ function npmGlobalArm(options) {
963
1707
  }
964
1708
 
965
1709
  // src/launch-staging.ts
966
- var import_node_fs6 = require("node:fs");
1710
+ var import_node_fs10 = require("node:fs");
967
1711
  var import_node_path5 = require("node:path");
968
1712
  function readLaunchTarget(path) {
969
1713
  try {
970
- const parsed = JSON.parse((0, import_node_fs6.readFileSync)(path, "utf8"));
1714
+ const parsed = JSON.parse((0, import_node_fs10.readFileSync)(path, "utf8"));
971
1715
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
972
1716
  const row = parsed;
973
1717
  if (typeof row.surface !== "string" || typeof row.target !== "string" || typeof row.since !== "string" || typeof row.incoming !== "string") return null;
@@ -977,15 +1721,15 @@ function readLaunchTarget(path) {
977
1721
  }
978
1722
  }
979
1723
  function writeLaunchTarget(path, record) {
980
- (0, import_node_fs6.mkdirSync)((0, import_node_path5.join)(path, ".."), { recursive: true });
1724
+ (0, import_node_fs10.mkdirSync)((0, import_node_path5.join)(path, ".."), { recursive: true });
981
1725
  const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
982
- (0, import_node_fs6.writeFileSync)(tmp, `${JSON.stringify(record, null, 2)}
1726
+ (0, import_node_fs10.writeFileSync)(tmp, `${JSON.stringify(record, null, 2)}
983
1727
  `, "utf8");
984
- (0, import_node_fs6.renameSync)(tmp, path);
1728
+ (0, import_node_fs10.renameSync)(tmp, path);
985
1729
  }
986
1730
  function clearLaunchTarget(path) {
987
1731
  try {
988
- (0, import_node_fs6.rmSync)(path, { force: true });
1732
+ (0, import_node_fs10.rmSync)(path, { force: true });
989
1733
  } catch {
990
1734
  }
991
1735
  }
@@ -1006,7 +1750,7 @@ function noteLaunchStaged(paths, env, options, seam = {}) {
1006
1750
  }
1007
1751
  function probeLaunchTargetVersion(launchTargetPath, versionAt) {
1008
1752
  const record = readLaunchTarget(launchTargetPath);
1009
- if (!record || !(0, import_node_fs6.existsSync)(record.incoming)) return null;
1753
+ if (!record || !(0, import_node_fs10.existsSync)(record.incoming)) return null;
1010
1754
  const version = versionAt(record.incoming);
1011
1755
  return version === record.target ? record.target : null;
1012
1756
  }
@@ -1064,7 +1808,7 @@ function ensureClaudeMarketplace(env) {
1064
1808
  const path = claudeMarketplacesPath(env);
1065
1809
  let body;
1066
1810
  try {
1067
- body = JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8"));
1811
+ body = JSON.parse((0, import_node_fs11.readFileSync)(path, "utf8"));
1068
1812
  } catch (error) {
1069
1813
  if (error?.code === "ENOENT") return { ok: false, detail: `${path} does not exist \u2014 mutmutco marketplace is not registered`, unregistered: true };
1070
1814
  return { ok: false, detail: `cannot read ${path}` };
@@ -1092,7 +1836,7 @@ function claudePluginsRoot(env) {
1092
1836
  }
1093
1837
  function claudeMarkerVersion(root) {
1094
1838
  try {
1095
- const version = JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path6.join)(root, ...CLAUDE_MANIFEST.split("/")), "utf8")).version;
1839
+ const version = JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path6.join)(root, ...CLAUDE_MANIFEST.split("/")), "utf8")).version;
1096
1840
  return typeof version === "string" ? version : null;
1097
1841
  } catch {
1098
1842
  return null;
@@ -1189,14 +1933,14 @@ function claudeArm(options) {
1189
1933
  return result(from, target, "defer", `staged Claude manifest is ${claudeMarkerVersion(staged.packageRoot) ?? "unreadable"} or its tree is incomplete (expected ${target})`);
1190
1934
  }
1191
1935
  try {
1192
- (0, import_node_fs7.mkdirSync)((0, import_node_path6.join)(pluginsRoot, "staging"), { recursive: true });
1193
- (0, import_node_fs7.rmSync)(incomingRoot, { recursive: true, force: true });
1194
- (0, import_node_fs7.cpSync)(staged.packageRoot, incoming, { recursive: true });
1936
+ (0, import_node_fs11.mkdirSync)((0, import_node_path6.join)(pluginsRoot, "staging"), { recursive: true });
1937
+ (0, import_node_fs11.rmSync)(incomingRoot, { recursive: true, force: true });
1938
+ (0, import_node_fs11.cpSync)(staged.packageRoot, incoming, { recursive: true });
1195
1939
  } catch (error) {
1196
1940
  return result(from, target, "defer", `cannot place incoming Claude generation: ${error?.message ?? error}`);
1197
1941
  }
1198
1942
  if (claudeMarkerVersion(incoming) !== target || !claudeTreeHealthy(incoming)) {
1199
- (0, import_node_fs7.rmSync)(incomingRoot, { recursive: true, force: true });
1943
+ (0, import_node_fs11.rmSync)(incomingRoot, { recursive: true, force: true });
1200
1944
  return result(from, target, "defer", "incoming Claude generation did not reproduce the verified payload");
1201
1945
  }
1202
1946
  }
@@ -1228,10 +1972,10 @@ function claudeArm(options) {
1228
1972
  }
1229
1973
  const cacheTarget = (0, import_node_path6.join)(pluginsRoot, "cache", "mutmutco", "mmi", target);
1230
1974
  try {
1231
- if (!(0, import_node_fs7.existsSync)(cacheTarget) || claudeMarkerVersion(cacheTarget) !== target || !claudeTreeHealthy(cacheTarget)) {
1232
- (0, import_node_fs7.rmSync)(cacheTarget, { recursive: true, force: true });
1233
- (0, import_node_fs7.mkdirSync)((0, import_node_path6.join)(cacheTarget, ".."), { recursive: true });
1234
- (0, import_node_fs7.cpSync)(incoming, cacheTarget, { recursive: true });
1975
+ if (!(0, import_node_fs11.existsSync)(cacheTarget) || claudeMarkerVersion(cacheTarget) !== target || !claudeTreeHealthy(cacheTarget)) {
1976
+ (0, import_node_fs11.rmSync)(cacheTarget, { recursive: true, force: true });
1977
+ (0, import_node_fs11.mkdirSync)((0, import_node_path6.join)(cacheTarget, ".."), { recursive: true });
1978
+ (0, import_node_fs11.cpSync)(incoming, cacheTarget, { recursive: true });
1235
1979
  }
1236
1980
  } catch (error) {
1237
1981
  return result(from, target, "defer", `cannot populate Claude plugin cache: ${error?.message ?? error}`);
@@ -1259,7 +2003,7 @@ function claudeArm(options) {
1259
2003
  }
1260
2004
  clearLaunchTarget(launchTargetPath);
1261
2005
  try {
1262
- (0, import_node_fs7.rmSync)(incomingRoot, { recursive: true, force: true });
2006
+ (0, import_node_fs11.rmSync)(incomingRoot, { recursive: true, force: true });
1263
2007
  } catch {
1264
2008
  }
1265
2009
  return result(from, target, "ok", `catalog refreshed; converged and verified ${from ?? "absent"} -> ${target}`);
@@ -1372,21 +2116,21 @@ function enumerateSurfaces(env = process.env) {
1372
2116
  { id: "hermes", marker: `${hermesHostCommand(env)} --version` }
1373
2117
  ].map((probe) => ({
1374
2118
  ...probe,
1375
- present: probe.id === "hermes" ? hermesHostPresent(env) : (0, import_node_fs8.existsSync)(probe.marker)
2119
+ present: probe.id === "hermes" ? hermesHostPresent(env) : (0, import_node_fs12.existsSync)(probe.marker)
1376
2120
  }));
1377
2121
  }
1378
2122
 
1379
2123
  // src/arm-cursor.ts
1380
- var import_node_fs11 = require("node:fs");
2124
+ var import_node_fs15 = require("node:fs");
1381
2125
  var import_node_child_process6 = require("node:child_process");
1382
2126
  var import_node_os5 = require("node:os");
1383
2127
  var import_node_path8 = require("node:path");
1384
2128
 
1385
2129
  // src/compat.ts
1386
- var import_node_fs9 = require("node:fs");
2130
+ var import_node_fs13 = require("node:fs");
1387
2131
  function readManifestCompat(manifestPath) {
1388
2132
  try {
1389
- const parsed = JSON.parse((0, import_node_fs9.readFileSync)(manifestPath, "utf8"));
2133
+ const parsed = JSON.parse((0, import_node_fs13.readFileSync)(manifestPath, "utf8"));
1390
2134
  return typeof parsed.mmiCompat === "string" ? parsed.mmiCompat : void 0;
1391
2135
  } catch {
1392
2136
  return void 0;
@@ -1394,7 +2138,7 @@ function readManifestCompat(manifestPath) {
1394
2138
  }
1395
2139
 
1396
2140
  // src/host-doc-lock.ts
1397
- var import_node_fs10 = require("node:fs");
2141
+ var import_node_fs14 = require("node:fs");
1398
2142
  var import_node_os4 = require("node:os");
1399
2143
  var SHARED_DOC_LOCK_STALE_MS = 3e4;
1400
2144
  var SHARED_DOC_LOCK_ATTEMPTS = 50;
@@ -1408,7 +2152,7 @@ function lockContent(options) {
1408
2152
  }
1409
2153
  function readHolder(lockPath) {
1410
2154
  try {
1411
- const parsed = JSON.parse((0, import_node_fs10.readFileSync)(lockPath, "utf8"));
2155
+ const parsed = JSON.parse((0, import_node_fs14.readFileSync)(lockPath, "utf8"));
1412
2156
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
1413
2157
  return parsed;
1414
2158
  } catch {
@@ -1420,7 +2164,7 @@ function lockAgeMs(lockPath, now) {
1420
2164
  const declared = typeof holder?.acquiredAt === "string" ? Date.parse(holder.acquiredAt) : Number.NaN;
1421
2165
  if (!Number.isNaN(declared)) return Math.max(0, now - declared);
1422
2166
  try {
1423
- return Math.max(0, now - (0, import_node_fs10.statSync)(lockPath).mtimeMs);
2167
+ return Math.max(0, now - (0, import_node_fs14.statSync)(lockPath).mtimeMs);
1424
2168
  } catch {
1425
2169
  return 0;
1426
2170
  }
@@ -1443,9 +2187,9 @@ function acquireSharedDocLock(documentPath, options) {
1443
2187
  for (let attempt = 0; attempt < attempts; attempt += 1) {
1444
2188
  let fd;
1445
2189
  try {
1446
- fd = (0, import_node_fs10.openSync)(lockPath, "wx");
1447
- (0, import_node_fs10.writeSync)(fd, content);
1448
- (0, import_node_fs10.closeSync)(fd);
2190
+ fd = (0, import_node_fs14.openSync)(lockPath, "wx");
2191
+ (0, import_node_fs14.writeSync)(fd, content);
2192
+ (0, import_node_fs14.closeSync)(fd);
1449
2193
  return {
1450
2194
  documentPath,
1451
2195
  lockPath,
@@ -1458,11 +2202,11 @@ function acquireSharedDocLock(documentPath, options) {
1458
2202
  } catch (error) {
1459
2203
  if (fd !== void 0) {
1460
2204
  try {
1461
- (0, import_node_fs10.closeSync)(fd);
2205
+ (0, import_node_fs14.closeSync)(fd);
1462
2206
  } catch {
1463
2207
  }
1464
2208
  try {
1465
- (0, import_node_fs10.unlinkSync)(lockPath);
2209
+ (0, import_node_fs14.unlinkSync)(lockPath);
1466
2210
  } catch {
1467
2211
  }
1468
2212
  }
@@ -1471,12 +2215,12 @@ function acquireSharedDocLock(documentPath, options) {
1471
2215
  if (lockAgeMs(lockPath, Date.now()) >= staleAfterMs) {
1472
2216
  const aside = `${lockPath}.stale-${Date.now()}`;
1473
2217
  try {
1474
- (0, import_node_fs10.renameSync)(lockPath, aside);
2218
+ (0, import_node_fs14.renameSync)(lockPath, aside);
1475
2219
  } catch {
1476
2220
  continue;
1477
2221
  }
1478
2222
  try {
1479
- (0, import_node_fs10.rmSync)(aside, { force: true });
2223
+ (0, import_node_fs14.rmSync)(aside, { force: true });
1480
2224
  } catch {
1481
2225
  }
1482
2226
  continue;
@@ -1489,7 +2233,7 @@ function releaseSharedDocLock(handle) {
1489
2233
  try {
1490
2234
  const holder = readHolder(handle.lockPath);
1491
2235
  if (holder && holder.owner === handle.owner && Number(holder.pid) === handle.pid) {
1492
- (0, import_node_fs10.unlinkSync)(handle.lockPath);
2236
+ (0, import_node_fs14.unlinkSync)(handle.lockPath);
1493
2237
  }
1494
2238
  } catch {
1495
2239
  }
@@ -1514,11 +2258,11 @@ function cursorPluginsRoot() {
1514
2258
  return (0, import_node_path8.join)((0, import_node_os5.homedir)(), ".cursor", "plugins");
1515
2259
  }
1516
2260
  function treeHealthy(root) {
1517
- return TREE_FILES.every((rel) => (0, import_node_fs11.existsSync)((0, import_node_path8.join)(root, ...rel.split("/"))));
2261
+ return TREE_FILES.every((rel) => (0, import_node_fs15.existsSync)((0, import_node_path8.join)(root, ...rel.split("/"))));
1518
2262
  }
1519
2263
  function readJson(root, rel) {
1520
2264
  try {
1521
- return JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path8.join)(root, ...rel.split("/")), "utf8"));
2265
+ return JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path8.join)(root, ...rel.split("/")), "utf8"));
1522
2266
  } catch {
1523
2267
  return null;
1524
2268
  }
@@ -1542,7 +2286,7 @@ function message(error) {
1542
2286
  }
1543
2287
  function discard(path) {
1544
2288
  try {
1545
- (0, import_node_fs11.rmSync)(path, { recursive: true, force: true });
2289
+ (0, import_node_fs15.rmSync)(path, { recursive: true, force: true });
1546
2290
  } catch {
1547
2291
  }
1548
2292
  }
@@ -1596,10 +2340,10 @@ var CURSOR_HOOKS_LOCK_OWNER = "mmi-hub:cursor-hooks";
1596
2340
  function reconcileCursorUserHooks(pluginRoot, homeDir, lock = {}) {
1597
2341
  const path = (0, import_node_path8.join)(homeDir, ".cursor", USER_HOOKS_FILE);
1598
2342
  return withSharedDocLock(path, { owner: CURSOR_HOOKS_LOCK_OWNER, ...lock }, () => {
1599
- if (!(0, import_node_fs11.existsSync)(path)) return "current";
2343
+ if (!(0, import_node_fs15.existsSync)(path)) return "current";
1600
2344
  let existing;
1601
2345
  try {
1602
- existing = JSON.parse((0, import_node_fs11.readFileSync)(path, "utf8"));
2346
+ existing = JSON.parse((0, import_node_fs15.readFileSync)(path, "utf8"));
1603
2347
  } catch {
1604
2348
  return "user-conflict";
1605
2349
  }
@@ -1607,8 +2351,8 @@ function reconcileCursorUserHooks(pluginRoot, homeDir, lock = {}) {
1607
2351
  if (!stripped) return "user-conflict";
1608
2352
  if (JSON.stringify(stripped) === JSON.stringify(existing)) return "current";
1609
2353
  const tmp = `${path}.tmp-${process.pid}`;
1610
- (0, import_node_fs11.writeFileSync)(tmp, JSON.stringify(stripped, null, 2) + "\n", "utf8");
1611
- (0, import_node_fs11.renameSync)(tmp, path);
2354
+ (0, import_node_fs15.writeFileSync)(tmp, JSON.stringify(stripped, null, 2) + "\n", "utf8");
2355
+ (0, import_node_fs15.renameSync)(tmp, path);
1612
2356
  return "updated";
1613
2357
  });
1614
2358
  }
@@ -1632,15 +2376,15 @@ function writeRollbackPointer(root, entry) {
1632
2376
  const path = (0, import_node_path8.join)(root, "quarantine", "rollback.json");
1633
2377
  const tmp = `${path}.tmp-${process.pid}`;
1634
2378
  try {
1635
- (0, import_node_fs11.writeFileSync)(tmp, JSON.stringify({ surface: "cursor", ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry }, null, 2) + "\n", "utf8");
1636
- (0, import_node_fs11.renameSync)(tmp, path);
2379
+ (0, import_node_fs15.writeFileSync)(tmp, JSON.stringify({ surface: "cursor", ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry }, null, 2) + "\n", "utf8");
2380
+ (0, import_node_fs15.renameSync)(tmp, path);
1637
2381
  } catch {
1638
2382
  discard(tmp);
1639
2383
  }
1640
2384
  }
1641
2385
  function mtimeMs(path) {
1642
2386
  try {
1643
- return (0, import_node_fs11.statSync)(path).mtimeMs;
2387
+ return (0, import_node_fs15.statSync)(path).mtimeMs;
1644
2388
  } catch {
1645
2389
  return 0;
1646
2390
  }
@@ -1649,13 +2393,13 @@ function pruneQuarantine(root, keep) {
1649
2393
  const dir = (0, import_node_path8.join)(root, "quarantine");
1650
2394
  let generations;
1651
2395
  try {
1652
- 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));
2396
+ 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));
1653
2397
  } catch {
1654
2398
  return 0;
1655
2399
  }
1656
2400
  let stranded = 0;
1657
2401
  try {
1658
- for (const entry of (0, import_node_fs11.readdirSync)(dir, { withFileTypes: true })) {
2402
+ for (const entry of (0, import_node_fs15.readdirSync)(dir, { withFileTypes: true })) {
1659
2403
  if (!entry.isFile() || !/^rollback\.json\.tmp-\d+$/.test(entry.name)) continue;
1660
2404
  const path = (0, import_node_path8.join)(dir, entry.name);
1661
2405
  if (Date.now() - mtimeMs(path) < STRANDED_TMP_AGE_MS) continue;
@@ -1675,16 +2419,16 @@ function pruneQuarantine(root, keep) {
1675
2419
  }
1676
2420
  function restorePrevious(live, quarantined, broken) {
1677
2421
  try {
1678
- (0, import_node_fs11.renameSync)(live, broken);
2422
+ (0, import_node_fs15.renameSync)(live, broken);
1679
2423
  } catch {
1680
2424
  return "kept-broken";
1681
2425
  }
1682
2426
  try {
1683
- (0, import_node_fs11.renameSync)(quarantined, live);
2427
+ (0, import_node_fs15.renameSync)(quarantined, live);
1684
2428
  return "restored";
1685
2429
  } catch {
1686
2430
  try {
1687
- (0, import_node_fs11.renameSync)(broken, live);
2431
+ (0, import_node_fs15.renameSync)(broken, live);
1688
2432
  return "kept-broken";
1689
2433
  } catch {
1690
2434
  return "lost";
@@ -1719,7 +2463,7 @@ function cursorArm(options) {
1719
2463
  if (installed === target && treeHealthy(live)) {
1720
2464
  return result(installed, target, "ok", `already at target ${target}${userHooksDetail(live, (0, import_node_os5.homedir)())}`);
1721
2465
  }
1722
- if ((0, import_node_fs11.existsSync)(live) && !mmiOwned(live)) {
2466
+ if ((0, import_node_fs15.existsSync)(live) && !mmiOwned(live)) {
1723
2467
  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)`);
1724
2468
  }
1725
2469
  if (dryRun) {
@@ -1737,12 +2481,12 @@ function cursorArm(options) {
1737
2481
  const priorLaunch = readLaunchTarget(launchTargetPath);
1738
2482
  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()}`);
1739
2483
  const reuseIncoming = incoming === priorLaunch?.incoming;
1740
- if (!reuseIncoming || !(0, import_node_fs11.existsSync)(incoming)) {
2484
+ if (!reuseIncoming || !(0, import_node_fs15.existsSync)(incoming)) {
1741
2485
  incoming = (0, import_node_path8.join)(root, "staging", `mmi-${safeSegment(target)}-${Date.now()}`);
1742
- for (const dir of ["local", "staging", "quarantine"]) (0, import_node_fs11.mkdirSync)((0, import_node_path8.join)(root, dir), { recursive: true });
2486
+ for (const dir of ["local", "staging", "quarantine"]) (0, import_node_fs15.mkdirSync)((0, import_node_path8.join)(root, dir), { recursive: true });
1743
2487
  try {
1744
2488
  discard(incoming);
1745
- (0, import_node_fs11.cpSync)(staged.packageRoot, incoming, { recursive: true });
2489
+ (0, import_node_fs15.cpSync)(staged.packageRoot, incoming, { recursive: true });
1746
2490
  } catch (error) {
1747
2491
  discard(incoming);
1748
2492
  return result(installed, target, "defer", `copy into ${root} failed: ${message(error)}`);
@@ -1766,7 +2510,7 @@ function cursorArm(options) {
1766
2510
  }
1767
2511
  const stamp = Date.now();
1768
2512
  const quarantined = (0, import_node_path8.join)(root, "quarantine", `mmi-${safeSegment(installed ?? "unknown")}-${stamp}`);
1769
- for (const dir of ["local", "staging", "quarantine"]) (0, import_node_fs11.mkdirSync)((0, import_node_path8.join)(root, dir), { recursive: true });
2513
+ for (const dir of ["local", "staging", "quarantine"]) (0, import_node_fs15.mkdirSync)((0, import_node_path8.join)(root, dir), { recursive: true });
1770
2514
  const started = cursorHostEvidence(env);
1771
2515
  if (started) {
1772
2516
  const pending = noteLaunchStaged(paths, env, {
@@ -1781,15 +2525,15 @@ function cursorArm(options) {
1781
2525
  }
1782
2526
  let displaced = false;
1783
2527
  try {
1784
- if ((0, import_node_fs11.existsSync)(live)) {
1785
- (0, import_node_fs11.renameSync)(live, quarantined);
2528
+ if ((0, import_node_fs15.existsSync)(live)) {
2529
+ (0, import_node_fs15.renameSync)(live, quarantined);
1786
2530
  displaced = true;
1787
2531
  }
1788
- (0, import_node_fs11.renameSync)(incoming, live);
2532
+ (0, import_node_fs15.renameSync)(incoming, live);
1789
2533
  } catch (error) {
1790
- if (displaced && !(0, import_node_fs11.existsSync)(live)) {
2534
+ if (displaced && !(0, import_node_fs15.existsSync)(live)) {
1791
2535
  try {
1792
- (0, import_node_fs11.renameSync)(quarantined, live);
2536
+ (0, import_node_fs15.renameSync)(quarantined, live);
1793
2537
  } catch {
1794
2538
  }
1795
2539
  }
@@ -1817,7 +2561,7 @@ function probeCursorLaunchStaging() {
1817
2561
  }
1818
2562
 
1819
2563
  // src/arm-jervcode.ts
1820
- var import_node_fs12 = require("node:fs");
2564
+ var import_node_fs16 = require("node:fs");
1821
2565
  var import_node_path9 = require("node:path");
1822
2566
  var PI_PACKAGE = "@mutmutco/pi-plugin";
1823
2567
  function hostEnv(env) {
@@ -1829,7 +2573,7 @@ function hostCommands(_env) {
1829
2573
  }
1830
2574
  function readPackageSpecs(env) {
1831
2575
  try {
1832
- const settings = JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path9.join)(jervcodeAgentDir(env), "settings.json"), "utf8"));
2576
+ const settings = JSON.parse((0, import_node_fs16.readFileSync)((0, import_node_path9.join)(jervcodeAgentDir(env), "settings.json"), "utf8"));
1833
2577
  return Array.isArray(settings.packages) ? settings.packages.filter((entry) => typeof entry === "string") : [];
1834
2578
  } catch {
1835
2579
  return null;
@@ -1837,7 +2581,7 @@ function readPackageSpecs(env) {
1837
2581
  }
1838
2582
  function installedVersion(env) {
1839
2583
  try {
1840
- 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"));
2584
+ 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"));
1841
2585
  return typeof manifest2.version === "string" ? manifest2.version : null;
1842
2586
  } catch {
1843
2587
  return null;
@@ -1957,7 +2701,7 @@ function jervcodeArm(options) {
1957
2701
  }
1958
2702
 
1959
2703
  // src/arm-hermes.ts
1960
- var import_node_fs13 = require("node:fs");
2704
+ var import_node_fs17 = require("node:fs");
1961
2705
  var import_node_child_process7 = require("node:child_process");
1962
2706
  var import_node_path10 = require("node:path");
1963
2707
  var PACKAGE2 = "@mutmutco/hermes-plugin";
@@ -1965,7 +2709,7 @@ var MANIFEST2 = "plugin.yaml";
1965
2709
  var TREE_MARKERS_FALLBACK = [MANIFEST2, "__init__.py", "skills"];
1966
2710
  function yamlVersion(root) {
1967
2711
  try {
1968
- const text = (0, import_node_fs13.readFileSync)((0, import_node_path10.join)(root, MANIFEST2), "utf8");
2712
+ const text = (0, import_node_fs17.readFileSync)((0, import_node_path10.join)(root, MANIFEST2), "utf8");
1969
2713
  try {
1970
2714
  const version = JSON.parse(text).version;
1971
2715
  if (typeof version === "string" && version.trim()) return version.trim();
@@ -1979,7 +2723,7 @@ function yamlVersion(root) {
1979
2723
  }
1980
2724
  function packageTreeMarkers(root) {
1981
2725
  try {
1982
- const files = JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path10.join)(root, "package.json"), "utf8")).files;
2726
+ const files = JSON.parse((0, import_node_fs17.readFileSync)((0, import_node_path10.join)(root, "package.json"), "utf8")).files;
1983
2727
  if (Array.isArray(files) && files.length && files.every((entry) => typeof entry === "string" && entry.trim())) {
1984
2728
  return files.map((entry) => entry.trim());
1985
2729
  }
@@ -1989,9 +2733,9 @@ function packageTreeMarkers(root) {
1989
2733
  }
1990
2734
  function markerPresent(root, rel) {
1991
2735
  const path = (0, import_node_path10.join)(root, ...rel.split("/"));
1992
- if (!(0, import_node_fs13.existsSync)(path)) return false;
2736
+ if (!(0, import_node_fs17.existsSync)(path)) return false;
1993
2737
  try {
1994
- const stat = (0, import_node_fs13.statSync)(path);
2738
+ const stat = (0, import_node_fs17.statSync)(path);
1995
2739
  return stat.isFile() || stat.isDirectory();
1996
2740
  } catch {
1997
2741
  return false;
@@ -2010,7 +2754,7 @@ function treeIncompleteDetail(root, label) {
2010
2754
  }
2011
2755
  function packageIdentity(root) {
2012
2756
  try {
2013
- return JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path10.join)(root, "package.json"), "utf8")).name === PACKAGE2;
2757
+ return JSON.parse((0, import_node_fs17.readFileSync)((0, import_node_path10.join)(root, "package.json"), "utf8")).name === PACKAGE2;
2014
2758
  } catch {
2015
2759
  return false;
2016
2760
  }
@@ -2025,7 +2769,7 @@ function mmiOwned2(root) {
2025
2769
  }
2026
2770
  function discard2(path) {
2027
2771
  try {
2028
- (0, import_node_fs13.rmSync)(path, { recursive: true, force: true });
2772
+ (0, import_node_fs17.rmSync)(path, { recursive: true, force: true });
2029
2773
  } catch {
2030
2774
  }
2031
2775
  }
@@ -2061,15 +2805,15 @@ function materializeHermesSkills(home) {
2061
2805
  const skillsRoot = (0, import_node_path10.join)(home, "skills");
2062
2806
  const targetRoot = (0, import_node_path10.join)(skillsRoot, "mmi");
2063
2807
  try {
2064
- if (!(0, import_node_fs13.existsSync)(pluginSkills)) return { ok: false, detail: `plugin skills tree missing at ${pluginSkills}` };
2065
- const names = (0, import_node_fs13.readdirSync)(pluginSkills, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_")).map((entry) => entry.name);
2808
+ if (!(0, import_node_fs17.existsSync)(pluginSkills)) return { ok: false, detail: `plugin skills tree missing at ${pluginSkills}` };
2809
+ const names = (0, import_node_fs17.readdirSync)(pluginSkills, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_")).map((entry) => entry.name);
2066
2810
  if (!names.length) return { ok: false, detail: `plugin skills tree is empty at ${pluginSkills}` };
2067
- (0, import_node_fs13.mkdirSync)(skillsRoot, { recursive: true });
2811
+ (0, import_node_fs17.mkdirSync)(skillsRoot, { recursive: true });
2068
2812
  const incoming = (0, import_node_path10.join)(skillsRoot, ".mmi-incoming");
2069
2813
  discard2(incoming);
2070
- (0, import_node_fs13.cpSync)(pluginSkills, incoming, { recursive: true });
2814
+ (0, import_node_fs17.cpSync)(pluginSkills, incoming, { recursive: true });
2071
2815
  discard2(targetRoot);
2072
- (0, import_node_fs13.renameSync)(incoming, targetRoot);
2816
+ (0, import_node_fs17.renameSync)(incoming, targetRoot);
2073
2817
  return { ok: true, detail: `provisioned ${names.length} skills to ${targetRoot}` };
2074
2818
  } catch (error) {
2075
2819
  return { ok: false, detail: message2(error) };
@@ -2091,7 +2835,7 @@ function hermesHomes(env = process.env) {
2091
2835
  const root = hermesHome(env);
2092
2836
  const homes = [root];
2093
2837
  try {
2094
- 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))) {
2838
+ 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))) {
2095
2839
  if (entry.isDirectory() && !entry.name.startsWith(".")) homes.push((0, import_node_path10.join)(root, "profiles", entry.name));
2096
2840
  }
2097
2841
  } catch {
@@ -2140,7 +2884,7 @@ function armHermesHome(home, env, options, primary) {
2140
2884
  detail,
2141
2885
  launchStaged: launchStaged || void 0
2142
2886
  });
2143
- if ((0, import_node_fs13.existsSync)(live) && !mmiOwned2(live)) {
2887
+ if ((0, import_node_fs17.existsSync)(live) && !mmiOwned2(live)) {
2144
2888
  return result(installed, options.target, "defer", `${live} carries no ${PACKAGE2} package identity \u2014 refusing to displace an unmanaged Hermes mmi plugin`);
2145
2889
  }
2146
2890
  if (installed && compareSemver(installed, options.target) > 0) return result(installed, installed, "skip", `installed ${installed} is above candidate ${options.target} (monotonic)`);
@@ -2172,10 +2916,10 @@ function armHermesHome(home, env, options, primary) {
2172
2916
  const reuseIncoming = priorLaunch?.target === options.target && priorLaunch.incoming === incoming && yamlVersion(incoming) === options.target && treeHealthy2(incoming);
2173
2917
  if (!reuseIncoming) {
2174
2918
  try {
2175
- (0, import_node_fs13.mkdirSync)(parent, { recursive: true });
2176
- (0, import_node_fs13.mkdirSync)((0, import_node_path10.join)(scratch, "quarantine"), { recursive: true });
2919
+ (0, import_node_fs17.mkdirSync)(parent, { recursive: true });
2920
+ (0, import_node_fs17.mkdirSync)((0, import_node_path10.join)(scratch, "quarantine"), { recursive: true });
2177
2921
  discard2(incomingRoot);
2178
- (0, import_node_fs13.cpSync)(staged.packageRoot, incoming, { recursive: true });
2922
+ (0, import_node_fs17.cpSync)(staged.packageRoot, incoming, { recursive: true });
2179
2923
  } catch (error) {
2180
2924
  return result(installed, options.target, "defer", `cannot place incoming Hermes generation: ${message2(error)}`);
2181
2925
  }
@@ -2190,7 +2934,7 @@ function armHermesHome(home, env, options, primary) {
2190
2934
  return result(installed, options.target, "defer", incomingTree);
2191
2935
  }
2192
2936
  }
2193
- const displaced = (0, import_node_fs13.existsSync)(live);
2937
+ const displaced = (0, import_node_fs17.existsSync)(live);
2194
2938
  const busy = displaced ? hermesHostEvidence(env, home, options.processListProvider) : null;
2195
2939
  if (busy) {
2196
2940
  const pending = noteLaunchStaged(options.paths, env, {
@@ -2205,11 +2949,11 @@ function armHermesHome(home, env, options, primary) {
2205
2949
  return result(installed, options.target, "ok", `staged ${options.target} for next launch while Hermes is running (${busy}) \u2014 ${pending}`, true);
2206
2950
  }
2207
2951
  try {
2208
- if (displaced) (0, import_node_fs13.renameSync)(live, quarantine);
2209
- (0, import_node_fs13.renameSync)(incoming, live);
2952
+ if (displaced) (0, import_node_fs17.renameSync)(live, quarantine);
2953
+ (0, import_node_fs17.renameSync)(incoming, live);
2210
2954
  } catch (error) {
2211
- if (displaced && !(0, import_node_fs13.existsSync)(live)) try {
2212
- (0, import_node_fs13.renameSync)(quarantine, live);
2955
+ if (displaced && !(0, import_node_fs17.existsSync)(live)) try {
2956
+ (0, import_node_fs17.renameSync)(quarantine, live);
2213
2957
  } catch {
2214
2958
  }
2215
2959
  discard2(incomingRoot);
@@ -2223,9 +2967,9 @@ function armHermesHome(home, env, options, primary) {
2223
2967
  const broken = (0, import_node_path10.join)(scratch, "rejected", `${options.target}-${Date.now()}`);
2224
2968
  const verifyDetail = liveVersion !== options.target ? `manifest is ${liveVersion ?? "unreadable"}, expected ${options.target}` : liveTree;
2225
2969
  try {
2226
- (0, import_node_fs13.mkdirSync)((0, import_node_path10.join)(scratch, "rejected"), { recursive: true });
2227
- (0, import_node_fs13.renameSync)(live, broken);
2228
- if (displaced) (0, import_node_fs13.renameSync)(quarantine, live);
2970
+ (0, import_node_fs17.mkdirSync)((0, import_node_path10.join)(scratch, "rejected"), { recursive: true });
2971
+ (0, import_node_fs17.renameSync)(live, broken);
2972
+ if (displaced) (0, import_node_fs17.renameSync)(quarantine, live);
2229
2973
  } catch (error) {
2230
2974
  return result(installed, liveVersion ?? "unknown", "fail", `post-switch Hermes verification failed and recovery failed: ${message2(error)}`);
2231
2975
  }
@@ -2244,7 +2988,7 @@ function probeHermesLaunchStaging(env = process.env) {
2244
2988
  }
2245
2989
 
2246
2990
  // src/reap.ts
2247
- var import_node_fs14 = require("node:fs");
2991
+ var import_node_fs18 = require("node:fs");
2248
2992
  var import_node_path11 = require("node:path");
2249
2993
  var import_node_perf_hooks = require("node:perf_hooks");
2250
2994
  function elapsedMs(startedAt) {
@@ -2254,7 +2998,7 @@ var KEEP_GENERATIONS2 = 2;
2254
2998
  var SCRATCH_AGE_MS = 24 * 60 * 6e4;
2255
2999
  function listDirs(path) {
2256
3000
  try {
2257
- return (0, import_node_fs14.readdirSync)(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
3001
+ return (0, import_node_fs18.readdirSync)(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
2258
3002
  } catch {
2259
3003
  return [];
2260
3004
  }
@@ -2275,7 +3019,7 @@ function pruneGenerations(root, category, dryRun) {
2275
3019
  continue;
2276
3020
  }
2277
3021
  try {
2278
- (0, import_node_fs14.rmSync)((0, import_node_path11.join)(set, version), { recursive: true, force: true });
3022
+ (0, import_node_fs18.rmSync)((0, import_node_path11.join)(set, version), { recursive: true, force: true });
2279
3023
  reaped += 1;
2280
3024
  } catch (error) {
2281
3025
  failures.push(`${version}: ${error?.code ?? error?.message ?? error}`);
@@ -2308,7 +3052,7 @@ function reapScratch(context, dryRun) {
2308
3052
  for (const [dir, patterns] of scanned) {
2309
3053
  let entries;
2310
3054
  try {
2311
- entries = (0, import_node_fs14.readdirSync)(dir, { withFileTypes: true });
3055
+ entries = (0, import_node_fs18.readdirSync)(dir, { withFileTypes: true });
2312
3056
  } catch {
2313
3057
  unreadable.push(dir);
2314
3058
  continue;
@@ -2319,7 +3063,7 @@ function reapScratch(context, dryRun) {
2319
3063
  const path = (0, import_node_path11.join)(dir, entry.name);
2320
3064
  let ageMs;
2321
3065
  try {
2322
- ageMs = now - (0, import_node_fs14.statSync)(path).mtimeMs;
3066
+ ageMs = now - (0, import_node_fs18.statSync)(path).mtimeMs;
2323
3067
  } catch {
2324
3068
  continue;
2325
3069
  }
@@ -2329,7 +3073,7 @@ function reapScratch(context, dryRun) {
2329
3073
  continue;
2330
3074
  }
2331
3075
  try {
2332
- (0, import_node_fs14.rmSync)(path, { force: true });
3076
+ (0, import_node_fs18.rmSync)(path, { force: true });
2333
3077
  reaped += 1;
2334
3078
  } catch (error) {
2335
3079
  failures.push(`${entry.name}: ${error?.code ?? error?.message ?? error}`);
@@ -2386,7 +3130,7 @@ async function reconcile(options) {
2386
3130
  const recordArm = (arm, durationMs) => {
2387
3131
  if (arm.verdict === "fail") arm.repeatFailure = journalSurfaceRepeatedFail(paths.journalPath, arm.surface, run);
2388
3132
  journal({ kind: "arm", surface: arm.surface, from: arm.from, to: arm.to, verdict: arm.verdict, detail: arm.detail, compat: arm.compat, durationMs });
2389
- options.onArm?.(arm);
3133
+ options.onArm?.({ ...arm, ...durationMs === void 0 ? {} : { durationMs } });
2390
3134
  if (arm.verdict === "ok" && !options.dryRun && !arm.launchStaged) clearPending(paths, arm.surface);
2391
3135
  if (arm.verdict === "defer") summary.exit = Math.max(summary.exit, 2);
2392
3136
  if (arm.verdict === "fail") summary.exit = Math.max(summary.exit, 3);
@@ -2551,7 +3295,7 @@ async function reconcile(options) {
2551
3295
 
2552
3296
  // src/scheduler.ts
2553
3297
  var import_node_child_process8 = require("node:child_process");
2554
- var import_node_fs15 = require("node:fs");
3298
+ var import_node_fs19 = require("node:fs");
2555
3299
  var import_node_os6 = require("node:os");
2556
3300
  var import_node_path12 = require("node:path");
2557
3301
  var TASK_NAME = "MMI Fleet Updater";
@@ -2711,7 +3455,7 @@ WantedBy=timers.target
2711
3455
  }
2712
3456
  function readFileOrNull(path) {
2713
3457
  try {
2714
- return (0, import_node_fs15.readFileSync)(path, "utf8");
3458
+ return (0, import_node_fs19.readFileSync)(path, "utf8");
2715
3459
  } catch {
2716
3460
  return null;
2717
3461
  }
@@ -2763,22 +3507,22 @@ function windowsSchedulerStatus(env) {
2763
3507
  const launcherPath = (0, import_node_path12.join)(statePaths(env).root, LAUNCHER_FILE);
2764
3508
  const info = taskRunInfo();
2765
3509
  const runInfo = info ?? EMPTY_RUN_INFO;
2766
- const fail = (detail) => ({ ok: false, supported: true, enabled: true, ...runInfo, detail });
3510
+ const fail2 = (detail) => ({ ok: false, supported: true, enabled: true, ...runInfo, detail });
2767
3511
  if (!query.stdout.includes(launcherPath)) {
2768
- return fail(`"${TASK_NAME}" action does not run the hidden launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
3512
+ return fail2(`"${TASK_NAME}" action does not run the hidden launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
2769
3513
  }
2770
3514
  const launcher = readFileOrNull(launcherPath);
2771
3515
  if (hubDist && !launcher?.includes(hubDist)) {
2772
- return fail(`"${TASK_NAME}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
3516
+ return fail2(`"${TASK_NAME}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
2773
3517
  }
2774
3518
  if (query.stdout.includes("<LogonTrigger>")) {
2775
- return fail(`"${TASK_NAME}" still carries the dormant logon-only trigger \u2014 run \`mmi-hub autoupdate on\``);
3519
+ return fail2(`"${TASK_NAME}" still carries the dormant logon-only trigger \u2014 run \`mmi-hub autoupdate on\``);
2776
3520
  }
2777
3521
  if (!query.stdout.includes("<TimeTrigger>") || !query.stdout.includes(`<Interval>${REPEAT_INTERVAL}</Interval>`)) {
2778
- return fail(`"${TASK_NAME}" has no ${REPEAT_INTERVAL}-repeating TimeTrigger \u2014 run \`mmi-hub autoupdate on\``);
3522
+ return fail2(`"${TASK_NAME}" has no ${REPEAT_INTERVAL}-repeating TimeTrigger \u2014 run \`mmi-hub autoupdate on\``);
2779
3523
  }
2780
3524
  if (info && neverRan(info) && !info.nextRunTime) {
2781
- return fail(`"${TASK_NAME}" has never run and has no next run time \u2014 run \`mmi-hub autoupdate on\``);
3525
+ return fail2(`"${TASK_NAME}" has never run and has no next run time \u2014 run \`mmi-hub autoupdate on\``);
2782
3526
  }
2783
3527
  const points = `points at ${hubDist ?? "the registered Hub dist"}`;
2784
3528
  const ran = !info ? "run history unreadable" : neverRan(info) ? "never run yet" : `last run ${info.lastRunTime} \u2192 exit ${info.lastTaskResult ?? "unknown"}`;
@@ -2792,20 +3536,20 @@ function darwinSchedulerStatus(env) {
2792
3536
  }
2793
3537
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2794
3538
  const launcherPath = (0, import_node_path12.join)(statePaths(env).root, POSIX_LAUNCHER_FILE);
2795
- const fail = (detail) => ({ ok: false, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail });
3539
+ const fail2 = (detail) => ({ ok: false, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail });
2796
3540
  if (!plist.includes(launcherPath)) {
2797
- return fail(`"${LAUNCHD_LABEL}" does not run the reconcile launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
3541
+ return fail2(`"${LAUNCHD_LABEL}" does not run the reconcile launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
2798
3542
  }
2799
3543
  const launcher = readFileOrNull(launcherPath);
2800
3544
  if (hubDist && !launcher?.includes(hubDist)) {
2801
- return fail(`"${LAUNCHD_LABEL}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
3545
+ return fail2(`"${LAUNCHD_LABEL}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
2802
3546
  }
2803
3547
  if (!plist.includes(`<integer>${STARTINTERVAL_SECONDS}</integer>`)) {
2804
- return fail(`"${LAUNCHD_LABEL}" has no ${STARTINTERVAL_SECONDS}s StartInterval \u2014 run \`mmi-hub autoupdate on\``);
3548
+ return fail2(`"${LAUNCHD_LABEL}" has no ${STARTINTERVAL_SECONDS}s StartInterval \u2014 run \`mmi-hub autoupdate on\``);
2805
3549
  }
2806
3550
  const loaded = launchctl(["print", `gui/${uid()}/${LAUNCHD_LABEL}`]);
2807
3551
  if (loaded.status !== 0) {
2808
- return fail(`"${LAUNCHD_LABEL}" is registered but not loaded \u2014 run \`mmi-hub autoupdate on\``);
3552
+ return fail2(`"${LAUNCHD_LABEL}" is registered but not loaded \u2014 run \`mmi-hub autoupdate on\``);
2809
3553
  }
2810
3554
  const points = `points at ${hubDist ?? "the registered Hub dist"}`;
2811
3555
  return { ok: true, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail: `"${LAUNCHD_LABEL}" registered ${POSIX_CADENCE}; ${points}` };
@@ -2820,20 +3564,20 @@ function linuxSchedulerStatus(env) {
2820
3564
  }
2821
3565
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2822
3566
  const launcherPath = (0, import_node_path12.join)(statePaths(env).root, POSIX_LAUNCHER_FILE);
2823
- const fail = (detail) => ({ ok: false, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail });
3567
+ const fail2 = (detail) => ({ ok: false, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail });
2824
3568
  if (!service.includes(launcherPath)) {
2825
- return fail(`"${SYSTEMD_SERVICE_NAME}" does not run the reconcile launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
3569
+ return fail2(`"${SYSTEMD_SERVICE_NAME}" does not run the reconcile launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
2826
3570
  }
2827
3571
  const launcher = readFileOrNull(launcherPath);
2828
3572
  if (hubDist && !launcher?.includes(hubDist)) {
2829
- return fail(`"${SYSTEMD_SERVICE_NAME}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
3573
+ return fail2(`"${SYSTEMD_SERVICE_NAME}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
2830
3574
  }
2831
3575
  if (!timer.includes(`OnUnitActiveSec=${STARTINTERVAL_SECONDS}`)) {
2832
- return fail(`"${SYSTEMD_TIMER_NAME}" has no ${STARTINTERVAL_SECONDS}s OnUnitActiveSec \u2014 run \`mmi-hub autoupdate on\``);
3576
+ return fail2(`"${SYSTEMD_TIMER_NAME}" has no ${STARTINTERVAL_SECONDS}s OnUnitActiveSec \u2014 run \`mmi-hub autoupdate on\``);
2833
3577
  }
2834
3578
  const enabled = systemctlUser(["is-enabled", SYSTEMD_TIMER_NAME]);
2835
3579
  if (enabled.status !== 0) {
2836
- return fail(`"${SYSTEMD_TIMER_NAME}" is registered but not enabled \u2014 run \`mmi-hub autoupdate on\``);
3580
+ return fail2(`"${SYSTEMD_TIMER_NAME}" is registered but not enabled \u2014 run \`mmi-hub autoupdate on\``);
2837
3581
  }
2838
3582
  const points = `points at ${hubDist ?? "the registered Hub dist"}`;
2839
3583
  return { ok: true, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail: `"${SYSTEMD_TIMER_NAME}" registered ${POSIX_CADENCE}, ${RUN_BOUND_SECONDS}s bound; ${points}` };
@@ -2859,9 +3603,9 @@ function installWindowsTask(env) {
2859
3603
  const userId = `${env.USERDOMAIN || env.COMPUTERNAME || ""}\\${env.USERNAME || ""}`.replace(/^\\/, "");
2860
3604
  if (!env.USERNAME) return { ok: false, detail: "cannot resolve the registering user (USERNAME unset) \u2014 the task principal must be explicit" };
2861
3605
  const launcherPath = (0, import_node_path12.join)(paths.root, LAUNCHER_FILE);
2862
- (0, import_node_fs15.writeFileSync)(launcherPath, launcherVbs(process.execPath, hubDist), "utf8");
3606
+ (0, import_node_fs19.writeFileSync)(launcherPath, launcherVbs(process.execPath, hubDist), "utf8");
2863
3607
  const xmlPath = (0, import_node_path12.join)(paths.root, "task.xml");
2864
- (0, import_node_fs15.writeFileSync)(xmlPath, "\uFEFF" + taskXml(launcherPath, paths.root, userId, localStartBoundary(/* @__PURE__ */ new Date())), "utf16le");
3608
+ (0, import_node_fs19.writeFileSync)(xmlPath, "\uFEFF" + taskXml(launcherPath, paths.root, userId, localStartBoundary(/* @__PURE__ */ new Date())), "utf16le");
2865
3609
  const create = schtasks(["/create", "/tn", TASK_NAME, "/xml", xmlPath, "/f"]);
2866
3610
  if (create.status !== 0) {
2867
3611
  const denied = /access is denied/i.test(create.stderr + create.stdout);
@@ -2880,8 +3624,8 @@ function refreshPosixLauncher(env) {
2880
3624
  const launcherPath = (0, import_node_path12.join)(paths.root, POSIX_LAUNCHER_FILE);
2881
3625
  const desired = launcherSh(process.execPath, hubDist);
2882
3626
  if (readFileOrNull(launcherPath) === desired) return null;
2883
- (0, import_node_fs15.writeFileSync)(launcherPath, desired, "utf8");
2884
- (0, import_node_fs15.chmodSync)(launcherPath, 493);
3627
+ (0, import_node_fs19.writeFileSync)(launcherPath, desired, "utf8");
3628
+ (0, import_node_fs19.chmodSync)(launcherPath, 493);
2885
3629
  return launcherPath;
2886
3630
  }
2887
3631
  function installLaunchd(env) {
@@ -2894,12 +3638,12 @@ function installLaunchd(env) {
2894
3638
  if (!hubDist) return { ok: false, detail: "cannot resolve the global @mutmutco/hub dist \u2014 run `npm install -g @mutmutco/hub` first" };
2895
3639
  const paths = ensureState(env);
2896
3640
  const launcherPath = (0, import_node_path12.join)(paths.root, POSIX_LAUNCHER_FILE);
2897
- (0, import_node_fs15.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
2898
- (0, import_node_fs15.chmodSync)(launcherPath, 493);
3641
+ (0, import_node_fs19.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
3642
+ (0, import_node_fs19.chmodSync)(launcherPath, 493);
2899
3643
  const agentsDir = (0, import_node_path12.join)((0, import_node_os6.homedir)(), "Library", "LaunchAgents");
2900
- (0, import_node_fs15.mkdirSync)(agentsDir, { recursive: true });
3644
+ (0, import_node_fs19.mkdirSync)(agentsDir, { recursive: true });
2901
3645
  const plistPath = (0, import_node_path12.join)(agentsDir, LAUNCHD_PLIST_NAME);
2902
- (0, import_node_fs15.writeFileSync)(plistPath, launchdPlist(launcherPath, paths.root), "utf8");
3646
+ (0, import_node_fs19.writeFileSync)(plistPath, launchdPlist(launcherPath, paths.root), "utf8");
2903
3647
  launchctl(["bootout", `gui/${uid()}/${LAUNCHD_LABEL}`]);
2904
3648
  const load = launchctl(["bootstrap", `gui/${uid()}`, plistPath]);
2905
3649
  if (load.status !== 0) {
@@ -2918,12 +3662,12 @@ function installSystemdTimer(env) {
2918
3662
  if (!hubDist) return { ok: false, detail: "cannot resolve the global @mutmutco/hub dist \u2014 run `npm install -g @mutmutco/hub` first" };
2919
3663
  const paths = ensureState(env);
2920
3664
  const launcherPath = (0, import_node_path12.join)(paths.root, POSIX_LAUNCHER_FILE);
2921
- (0, import_node_fs15.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
2922
- (0, import_node_fs15.chmodSync)(launcherPath, 493);
3665
+ (0, import_node_fs19.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
3666
+ (0, import_node_fs19.chmodSync)(launcherPath, 493);
2923
3667
  const unitDir = (0, import_node_path12.join)((0, import_node_os6.homedir)(), ".config", "systemd", "user");
2924
- (0, import_node_fs15.mkdirSync)(unitDir, { recursive: true });
2925
- (0, import_node_fs15.writeFileSync)((0, import_node_path12.join)(unitDir, SYSTEMD_SERVICE_NAME), systemdServiceUnit(launcherPath, paths.root), "utf8");
2926
- (0, import_node_fs15.writeFileSync)((0, import_node_path12.join)(unitDir, SYSTEMD_TIMER_NAME), systemdTimerUnit(), "utf8");
3668
+ (0, import_node_fs19.mkdirSync)(unitDir, { recursive: true });
3669
+ (0, import_node_fs19.writeFileSync)((0, import_node_path12.join)(unitDir, SYSTEMD_SERVICE_NAME), systemdServiceUnit(launcherPath, paths.root), "utf8");
3670
+ (0, import_node_fs19.writeFileSync)((0, import_node_path12.join)(unitDir, SYSTEMD_TIMER_NAME), systemdTimerUnit(), "utf8");
2927
3671
  const reload = systemctlUser(["daemon-reload"]);
2928
3672
  if (reload.status !== 0) {
2929
3673
  return { ok: false, detail: `systemctl --user daemon-reload failed: ${(reload.stderr || reload.stdout).trim() || "unknown error \u2014 is a user systemd instance running?"}` };
@@ -2963,11 +3707,11 @@ function uninstallWindowsTask(env) {
2963
3707
  function uninstallLaunchd(env) {
2964
3708
  const plistPath = (0, import_node_path12.join)((0, import_node_os6.homedir)(), "Library", "LaunchAgents", LAUNCHD_PLIST_NAME);
2965
3709
  const boot = launchctl(["bootout", `gui/${uid()}/${LAUNCHD_LABEL}`]);
2966
- if (boot.status !== 0 && (0, import_node_fs15.existsSync)(plistPath)) {
3710
+ if (boot.status !== 0 && (0, import_node_fs19.existsSync)(plistPath)) {
2967
3711
  return { ok: false, detail: `launchctl bootout failed: ${(boot.stderr || boot.stdout).trim() || "unknown error"}` };
2968
3712
  }
2969
3713
  try {
2970
- (0, import_node_fs15.rmSync)(plistPath, { force: true });
3714
+ (0, import_node_fs19.rmSync)(plistPath, { force: true });
2971
3715
  } catch {
2972
3716
  }
2973
3717
  dropLauncher(env, POSIX_LAUNCHER_FILE);
@@ -2977,12 +3721,12 @@ function uninstallSystemdTimer(env) {
2977
3721
  const unitDir = (0, import_node_path12.join)((0, import_node_os6.homedir)(), ".config", "systemd", "user");
2978
3722
  const timerPath = (0, import_node_path12.join)(unitDir, SYSTEMD_TIMER_NAME);
2979
3723
  const disable = systemctlUser(["disable", "--now", SYSTEMD_TIMER_NAME]);
2980
- if (disable.status !== 0 && (0, import_node_fs15.existsSync)(timerPath)) {
3724
+ if (disable.status !== 0 && (0, import_node_fs19.existsSync)(timerPath)) {
2981
3725
  return { ok: false, detail: `systemctl --user disable --now ${SYSTEMD_TIMER_NAME} failed: ${(disable.stderr || disable.stdout).trim() || "unknown error"}` };
2982
3726
  }
2983
3727
  try {
2984
- (0, import_node_fs15.rmSync)(timerPath, { force: true });
2985
- (0, import_node_fs15.rmSync)((0, import_node_path12.join)(unitDir, SYSTEMD_SERVICE_NAME), { force: true });
3728
+ (0, import_node_fs19.rmSync)(timerPath, { force: true });
3729
+ (0, import_node_fs19.rmSync)((0, import_node_path12.join)(unitDir, SYSTEMD_SERVICE_NAME), { force: true });
2986
3730
  } catch {
2987
3731
  }
2988
3732
  systemctlUser(["daemon-reload"]);
@@ -2997,16 +3741,16 @@ function uninstallTask(env = process.env) {
2997
3741
  }
2998
3742
  function dropLauncher(env, file) {
2999
3743
  try {
3000
- (0, import_node_fs15.rmSync)((0, import_node_path12.join)(statePaths(env).root, file), { force: true });
3744
+ (0, import_node_fs19.rmSync)((0, import_node_path12.join)(statePaths(env).root, file), { force: true });
3001
3745
  } catch {
3002
3746
  }
3003
3747
  }
3004
3748
 
3005
3749
  // src/status.ts
3006
- var import_node_fs16 = require("node:fs");
3750
+ var import_node_fs20 = require("node:fs");
3007
3751
  function readJournal(path) {
3008
3752
  try {
3009
- const events = (0, import_node_fs16.readFileSync)(path, "utf8").split("\n").filter((line) => line.trim()).map((line) => {
3753
+ const events = (0, import_node_fs20.readFileSync)(path, "utf8").split("\n").filter((line) => line.trim()).map((line) => {
3010
3754
  try {
3011
3755
  return JSON.parse(line);
3012
3756
  } catch {
@@ -3129,31 +3873,6 @@ function formatHubStatus(status) {
3129
3873
  }
3130
3874
 
3131
3875
  // src/report.ts
3132
- var NAME_WIDTH = 8;
3133
- var VERSION_WIDTH = 15;
3134
- var DEFAULT_WIDTH = 100;
3135
- var MIN_NOTE_SPACE = 8;
3136
- var ACTIVATION_NOTE = {
3137
- claude: "next session",
3138
- codex: "next session",
3139
- jervcode: "next launch",
3140
- cursor: "on reload",
3141
- hermes: "after restart",
3142
- updater: "next run"
3143
- };
3144
- var GREEN = "\x1B[32m";
3145
- var YELLOW = "\x1B[33m";
3146
- var RED = "\x1B[31m";
3147
- var RESET = "\x1B[0m";
3148
- var ACCENT = "\x1B[38;2;125;211;252m";
3149
- var MUTED = "\x1B[38;2;150;150;150m";
3150
- var MM_GREEN = "\x1B[38;2;31;209;138m";
3151
- function accent(text, color) {
3152
- return color ? ACCENT + text + RESET : text;
3153
- }
3154
- function brandLine(rest, color = false) {
3155
- return `${accent("mmi-hub", color)} \xB7 Mutatis Mutandis \u2014 ${rest}`;
3156
- }
3157
3876
  var RETRY_HINTS = [
3158
3877
  [/applies when the host exits|applies on a quiet tick/, "installs when the app closes"],
3159
3878
  [/in flight|changed while/, "rerun update \u2014 host race"],
@@ -3182,257 +3901,34 @@ function failConsequence(row) {
3182
3901
  }
3183
3902
  return matchFirst(FAIL_CONSEQUENCE, detail) ?? "update failed \u2014 previous version kept";
3184
3903
  }
3185
- function classify(arm) {
3186
- 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";
3187
- const note = kind === "updated" ? arm.launchStaged ? "next launch" : ACTIVATION_NOTE[arm.surface] ?? null : kind === "keep" ? "above target" : kind === "retry" ? retryHint(arm.detail) : null;
3188
- return { name: arm.surface === "updater" ? "hub" : arm.surface, from: arm.from, to: arm.to, kind, note, detail: arm.detail, repeatFailure: arm.repeatFailure === true };
3189
- }
3190
- function statusText(kind, dryRun) {
3191
- if (dryRun && kind === "updated") return "would update";
3192
- switch (kind) {
3193
- case "updated":
3194
- return "updated";
3195
- case "current":
3196
- return "current";
3197
- case "keep":
3198
- return "keep";
3199
- case "retry":
3200
- return "retry";
3201
- case "failed":
3202
- return "failed";
3203
- case "pending":
3204
- return "pending";
3205
- }
3206
- }
3207
- function glyphFor(kind) {
3208
- switch (kind) {
3209
- case "updated":
3210
- return "\u2713";
3211
- case "current":
3212
- return "\u2713";
3213
- case "keep":
3214
- return "\u2713";
3215
- case "retry":
3216
- return "\u21BB";
3217
- case "failed":
3218
- return "\u2717";
3219
- case "pending":
3220
- return "\xB7";
3221
- }
3222
- }
3223
- function colorFor(kind) {
3224
- return kind === "failed" ? RED : kind === "retry" ? YELLOW : GREEN;
3225
- }
3226
- var ARM_TITLE = {
3227
- updater: "Updating mmi-hub",
3228
- cli: "Installing mmi-cli",
3229
- claude: "Arming Claude Code",
3230
- codex: "Arming Codex",
3231
- cursor: "Arming Cursor",
3232
- hermes: "Arming Hermes",
3233
- jervcode: "Arming JervCode"
3234
- };
3235
- var FACE_PRODUCT = "mmi-hub";
3236
- function updaterFace(options = {}) {
3237
- return createFace({ product: FACE_PRODUCT, color: options.color ?? false, columns: options.width });
3238
- }
3239
- var STATE_WORD = {
3240
- updated: "updated",
3241
- current: "current",
3242
- keep: "kept",
3243
- retry: "will retry",
3244
- failed: "failed",
3245
- pending: "pending"
3246
- };
3247
- function stepKindFor(kind) {
3248
- return kind === "failed" ? "fail" : kind === "retry" ? "note" : "ok";
3249
- }
3250
- function checklistLine(arm, seconds, face, options = {}) {
3251
- const { kind } = classify(arm);
3252
- const state = options.dryRun && kind === "updated" ? "would update" : STATE_WORD[kind];
3253
- const title = `${ARM_TITLE[arm.surface] ?? `Arming ${arm.surface}`} \u2014 ${state}`;
3254
- const truthful = kind === "failed" || kind === "retry" ? null : seconds;
3255
- return face.step(title, truthful, stepKindFor(kind));
3256
- }
3257
- function receiptText(summary, autoupdate, options = {}) {
3258
- const dry = options.dryRun ?? false;
3259
- const counts = reportCounts(summary);
3260
- const health = "Check health any time: mmi-hub status";
3261
- if (!summary.target) {
3262
- return [`${summary.exit >= 1 ? "\u2716" : "\u25CF"} mmi-hub is not ready \u2014 no green release yet (hourly retry)`, "", health];
3263
- }
3264
- const ready = counts.failed === 0;
3265
- const glyph = counts.failed > 0 ? "\u2716" : counts.retry > 0 ? "\u25CF" : dry ? "\u25CF" : "\u2714";
3266
- 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`;
3267
- const tail2 = autoupdate ? ` \xB7 ${autoupdate.ok ? "auto-update on" : `auto-update NOT enabled \u2014 ${autoupdate.detail}`}` : ` \xB7 ${summary.target}${dry ? " (planned)" : " (verified)"}`;
3268
- return [`${glyph} mmi-hub is ${ready ? "ready" : "not ready"} \u2014 ${state}${tail2}`, "", health];
3269
- }
3270
- function autoupdateLine(state) {
3271
- return state.ok ? 'Auto-update on \u2014 hourly task "MMI Fleet Updater".' : `Auto-update NOT enabled \u2014 ${state.detail}`;
3272
- }
3273
- function attentionRows(summary) {
3274
- return collectRows(summary).filter((row) => row.kind === "failed" || row.kind === "retry" || row.kind === "pending").map((row) => ({
3275
- surface: row.name,
3276
- state: statusText(row.kind, false),
3277
- mark: row.kind === "failed" ? "fail" : "note",
3278
- note: row.kind === "failed" ? failConsequence(row) : row.kind === "retry" ? retryHint(row.detail) : "arm not landed"
3279
- }));
3280
- }
3281
- function wrapWords2(text, available) {
3282
- if (available < 1) return [text];
3283
- const lines = [];
3284
- let current = "";
3285
- for (const word of text.split(/\s+/).filter(Boolean)) {
3286
- const piece = word.length > available ? word.slice(0, Math.max(1, available - 1)) + "\u2026" : word;
3287
- if (!current) current = piece;
3288
- else if (current.length + 1 + piece.length <= available) current += " " + piece;
3289
- else {
3290
- lines.push(current);
3291
- current = piece;
3292
- }
3293
- }
3294
- if (current) lines.push(current);
3295
- return lines.length ? lines : [""];
3296
- }
3297
- function noteLines(prefix, note, width) {
3298
- const used = stripAnsi(prefix).length;
3299
- const available = width - used - 1;
3300
- const indent = available >= MIN_NOTE_SPACE ? " ".repeat(used + 1) : " ".repeat(Math.min(used + 1, Math.max(2, width - MIN_NOTE_SPACE)));
3301
- const room = available >= MIN_NOTE_SPACE ? available : width - indent.length;
3302
- const parts = wrapWords2(note, room);
3303
- const first = available >= MIN_NOTE_SPACE ? prefix + " " + parts[0] : prefix.replace(/\s+$/, "");
3304
- const lines = [first];
3305
- for (const part of parts.slice(available >= MIN_NOTE_SPACE ? 1 : 0)) lines.push(indent + part);
3306
- return lines;
3307
- }
3308
- function stripAnsi(text) {
3309
- return text.replace(/\x1b\[[0-9;]*m/g, "");
3310
- }
3311
- function collectRows(summary) {
3312
- const rows = [];
3313
- if (summary.updater) rows.push(classify(summary.updater));
3314
- if (summary.cli) rows.push(classify(summary.cli));
3315
- for (const arm of summary.plugins) rows.push(classify(arm));
3316
- for (const surface of summary.surfaces) {
3317
- if (surface.present && surface.action === "arm-pending") {
3318
- rows.push({ name: surface.id, from: null, to: summary.target ?? "?", kind: "pending", note: "arm not landed", detail: "", repeatFailure: false });
3319
- }
3320
- }
3321
- return rows;
3322
- }
3323
- function headline(detail, width) {
3324
- const first = (detail ?? "").split(/(?<=[.;:])\s/)[0]?.replace(/\s+/g, " ").trim() ?? "";
3325
- return wrapWords2(first || "blocked", Math.max(10, width - 4))[0];
3326
- }
3327
- function updateHeader(target, dryRun = false, options) {
3328
- return brandLine(
3329
- dryRun ? `update plan to ${target} (dry run)` : `updating to ${target} (verified)`,
3330
- Boolean(options?.color)
3331
- );
3332
- }
3333
- function renderRowLines(row, ctx) {
3334
- const paint = (kind, text) => ctx.color ? colorFor(kind) + text + RESET : text;
3335
- const versions = row.from !== null && row.from !== row.to ? `${row.from} \u2192 ${row.to}` : `${row.to}`;
3336
- const glyph = ctx.tty ? glyphFor(row.kind) + " " : "";
3337
- const status = statusText(row.kind, ctx.dryRun).padEnd(ctx.statusWidth);
3338
- const prefix = " " + row.name.padEnd(NAME_WIDTH) + " " + versions.padEnd(VERSION_WIDTH) + " " + paint(row.kind, glyph + status);
3339
- return row.note ? noteLines(prefix, row.note, ctx.width) : [prefix.replace(/\s+$/, "")];
3340
- }
3341
- function progressiveStatusWidth(dryRun) {
3342
- return Math.max((dryRun ? "would update" : "updated").length, 7);
3343
- }
3344
- function progressiveRow(arm, options = {}) {
3345
- const width = Math.max(40, options.width ?? DEFAULT_WIDTH);
3346
- const tty = options.tty ?? false;
3347
- const color = tty && (options.color ?? true);
3348
- const dryRun = options.dryRun ?? false;
3349
- return renderRowLines(classify(arm), { tty, color, dryRun, width, statusWidth: progressiveStatusWidth(dryRun) }).join("\n");
3350
- }
3351
3904
  function reportCounts(summary) {
3352
- const rows = collectRows(summary);
3353
- return {
3354
- updated: rows.filter((row) => row.kind === "updated").length,
3355
- current: rows.filter((row) => row.kind === "current" || row.kind === "keep").length,
3356
- retry: rows.filter((row) => row.kind === "retry").length,
3357
- failed: rows.filter((row) => row.kind === "failed").length,
3358
- total: rows.length
3905
+ const counts2 = {
3906
+ updated: 0,
3907
+ current: 0,
3908
+ retry: 0,
3909
+ failed: 0,
3910
+ total: summary.surfaces.filter((surface) => surface.present && surface.action === "arm-pending").length
3359
3911
  };
3360
- }
3361
- function reportFooterLines(summary, options = {}) {
3362
- const width = Math.max(40, options.width ?? DEFAULT_WIDTH);
3363
- const tty = options.tty ?? false;
3364
- const color = tty && (options.color ?? true);
3365
- const dryRun = options.dryRun ?? false;
3366
- const journalPath = options.journalPath ?? "~/.mmi/updater/journal.jsonl";
3367
- const paint = (kind, text) => color ? colorFor(kind) + text + RESET : text;
3368
- const rows = collectRows(summary);
3369
- const counts = reportCounts(summary);
3370
- 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` : "");
3371
- const out = ["", " " + footer];
3372
- if (tty && !dryRun && counts.failed === 0) {
3373
- out.push(" " + (color ? MUTED + "Suggested next: mmi-cli doctor" + RESET : "Suggested next: mmi-cli doctor"));
3374
- }
3375
- const self = summary.updater;
3376
- if (!dryRun && self && self.from && self.to && self.from !== self.to && self.verdict !== "fail") {
3377
- out.push("");
3378
- out.push(...noteLines(
3379
- " note: ",
3380
- `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}`,
3381
- width
3382
- ));
3383
- }
3384
- if (counts.failed) {
3385
- out.push("");
3386
- for (const row of rows.filter((row2) => row2.kind === "failed")) {
3387
- const mark = tty ? paint("failed", "\u2717 ") : "";
3388
- out.push(...noteLines(" " + mark + row.name, failConsequence(row), width));
3389
- 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));
3390
- out.push(...noteLines(" log: ", journalPath, width));
3391
- }
3392
- }
3393
- return out;
3394
- }
3395
- function formatReconcileReport(summary, options = {}) {
3396
- const width = Math.max(40, options.width ?? DEFAULT_WIDTH);
3397
- const tty = options.tty ?? false;
3398
- const color = tty && (options.color ?? true);
3399
- const dryRun = options.dryRun ?? false;
3400
- const out = [];
3401
- const paint = (kind, text) => color ? colorFor(kind) + text + RESET : text;
3402
- const rows = collectRows(summary);
3403
- const counts = reportCounts(summary);
3404
- if (!summary.target) {
3405
- if ((summary.exit >= 1 || summary.deferred) && summary.detail) {
3406
- out.push("MMI Hub \u2014 cannot update now");
3407
- out.push(...noteLines(" " + paint("failed", "blocked"), headline(summary.detail, width), width));
3408
- out.push(...noteLines(" fix:", retryHint(summary.detail), width));
3409
- } else {
3410
- out.push("MMI Hub \u2014 release still publishing, nothing changed (hourly retry)");
3411
- }
3412
- return out.join("\n") + "\n";
3413
- }
3414
- const allCurrent = rows.length > 0 && counts.updated === 0 && counts.retry === 0 && counts.failed === 0 && rows.every((row) => row.kind === "current" || row.kind === "keep");
3415
- if (allCurrent && !dryRun) {
3416
- out.push(`MMI Hub \u2014 MMI is up to date (${summary.target} \xB7 verified)`);
3417
- return out.join("\n") + "\n";
3912
+ for (const arm of [summary.updater, summary.cli, ...summary.plugins]) {
3913
+ if (!arm) continue;
3914
+ counts2.total++;
3915
+ if (arm.verdict === "fail") counts2.failed++;
3916
+ else if (arm.verdict === "defer") counts2.retry++;
3917
+ else if (arm.verdict === "skip" || arm.from !== null && arm.from === arm.to && !arm.launchStaged) counts2.current++;
3918
+ else counts2.updated++;
3418
3919
  }
3419
- out.push(updateHeader(summary.target, dryRun, { color }));
3420
- out.push("");
3421
- const statusWidth = Math.max(...rows.map((row) => statusText(row.kind, dryRun).length), 7);
3422
- for (const row of rows) out.push(...renderRowLines(row, { tty, color, dryRun, width, statusWidth }));
3423
- out.push(...reportFooterLines(summary, options));
3424
- return out.join("\n") + "\n";
3920
+ return counts2;
3425
3921
  }
3426
3922
 
3427
3923
  // src/stamp.ts
3428
- var import_node_fs17 = require("node:fs");
3924
+ var import_node_fs21 = require("node:fs");
3429
3925
  var ALWAYS_ARMED = ["cli", "updater"];
3430
3926
  var PLUGIN_ARMS = /* @__PURE__ */ new Set(["claude", "codex", "jervcode", "cursor", "hermes"]);
3431
3927
  function fleetStamp(env = process.env) {
3432
3928
  const paths = statePaths(env);
3433
3929
  let events = [];
3434
3930
  try {
3435
- events = (0, import_node_fs17.readFileSync)(paths.journalPath, "utf8").split("\n").filter((line) => line.trim()).map((line) => {
3931
+ events = (0, import_node_fs21.readFileSync)(paths.journalPath, "utf8").split("\n").filter((line) => line.trim()).map((line) => {
3436
3932
  try {
3437
3933
  return JSON.parse(line);
3438
3934
  } catch {
@@ -3482,7 +3978,7 @@ function fleetStamp(env = process.env) {
3482
3978
  // src/index.ts
3483
3979
  function ownVersion() {
3484
3980
  try {
3485
- return JSON.parse((0, import_node_fs18.readFileSync)((0, import_node_path13.join)(__dirname, "..", "package.json"), "utf8")).version;
3981
+ return JSON.parse((0, import_node_fs22.readFileSync)((0, import_node_path13.join)(__dirname, "..", "package.json"), "utf8")).version;
3486
3982
  } catch {
3487
3983
  return "0.0.0";
3488
3984
  }
@@ -3502,147 +3998,34 @@ var HELP = `mmi-hub ${ownVersion()} \u2014 install and maintain MMI tooling
3502
3998
  State remains under $MMI_UPDATER_HOME (default ~/.mmi/updater), and the Windows task name remains
3503
3999
  "MMI Fleet Updater", so existing installations continue in place.
3504
4000
  Status performs no npm install, host repair, scheduler write, state mkdir, or repository cleanup.`;
3505
- function humanOptions(dryRun) {
3506
- return {
3507
- tty: Boolean(process.stdout.isTTY),
3508
- color: !process.env.NO_COLOR,
3509
- width: process.stdout.columns ?? 100,
3510
- dryRun,
3511
- journalPath: statePaths(process.env).journalPath
3512
- };
3513
- }
3514
- function printReconcile(summary, json, dryRun = false) {
3515
- if (json) {
3516
- process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
3517
- return;
3518
- }
3519
- process.stdout.write(formatReconcileReport(summary, humanOptions(dryRun)));
3520
- }
3521
- function faceEnabled(args, json) {
3522
- return !json && !args.includes("--verbose") && !process.env.NO_COLOR && process.env.TERM !== "dumb" && Boolean(process.stdout.isTTY) && Boolean(process.stderr.isTTY);
3523
- }
3524
- function openFace(args, json, dryRun) {
3525
- if (!faceEnabled(args, json)) return null;
3526
- const face = updaterFace(humanOptions(dryRun));
3527
- if (!process.env.MMI_UPDATER_REEXEC) for (const line of face.welcome()) process.stdout.write(line + "\n");
3528
- return face;
3529
- }
3530
- function finishFace(face, summary, autoupdate, dryRun) {
3531
- const options = humanOptions(dryRun);
3532
- const write = (line) => {
3533
- process.stdout.write(line + "\n");
3534
- };
3535
- if (!summary.target) {
3536
- process.stdout.write(formatReconcileReport(summary, { ...options, tty: false }));
3537
- } else {
3538
- for (const line of renderRows(face, attentionRows(summary))) write(line);
3539
- }
3540
- const body = receiptText(summary, autoupdate, { dryRun });
3541
- const counts = reportCounts(summary);
3542
- if (counts.failed > 0) {
3543
- body.push("", "Fix: mmi-cli doctor, then mmi-hub update", `Log: ${options.journalPath}`);
3544
- }
3545
- const self = summary.updater;
3546
- if (!dryRun && self && self.from && self.to && self.from !== self.to && self.verdict !== "fail") {
3547
- 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}`);
3548
- }
3549
- for (const line of face.receipt(body)) write(line);
3550
- write(face.signOff());
3551
- }
3552
- function handOff(reexec, args) {
4001
+ function handOff(reexec, args, env) {
3553
4002
  const child = (0, import_node_child_process9.spawnSync)(process.execPath, [reexec.dist, ...args], {
3554
4003
  stdio: "inherit",
3555
4004
  windowsHide: true,
3556
- env: { ...process.env, MMI_UPDATER_REEXEC: "1" }
4005
+ env: { ...env, MMI_UPDATER_REEXEC: "1" }
3557
4006
  });
3558
4007
  if (child.error || typeof child.status !== "number") {
3559
- 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\`
3560
- `);
3561
- return 1;
4008
+ throw new Error(`could not run the updated updater at ${reexec.dist} \u2014 ${child.error?.message ?? "no exit status"}; rerun mmi-hub update`);
3562
4009
  }
3563
4010
  return child.status;
3564
4011
  }
3565
- function heldOutput(write) {
3566
- const held = [];
3567
- let streaming = false;
3568
- return {
3569
- emit: (line) => {
3570
- if (streaming) write(line);
3571
- else held.push(line);
3572
- },
3573
- stream: () => {
3574
- if (streaming) return;
3575
- streaming = true;
3576
- for (const line of held) write(line);
3577
- held.length = 0;
3578
- }
3579
- };
3580
- }
3581
4012
  function shippedTargetFrom(args) {
3582
4013
  const index = args.indexOf("--shipped");
3583
4014
  if (index < 0) return void 0;
3584
4015
  const value = (args[index + 1] ?? "").trim();
3585
4016
  return value && !value.startsWith("--") ? value : void 0;
3586
4017
  }
3587
- async function runUpdate(args, json, face = null, shippedTarget) {
3588
- const dryRun = args.includes("--dry-run");
3589
- const verbose = args.includes("--verbose");
3590
- const narrate = (line) => {
3591
- process.stderr.write(`mmi-hub: ${line}
3592
- `);
3593
- };
3594
- const progressive = !json && !verbose && Boolean(process.stderr.isTTY);
3595
- if (!progressive) {
3596
- return reconcile({ dryRun, narrate: verbose ? narrate : void 0, shippedTarget }).then((summary2) => ({ summary: summary2, rendered: false }));
3597
- }
3598
- const reportOptions = humanOptions(dryRun);
3599
- const spinner = createSpinner(face ?? updaterFace(reportOptions), { animate: true, stream: process.stderr });
3600
- let spinning = false;
3601
- const phase = (line) => {
3602
- if (spinning) spinner.say(line);
3603
- else {
3604
- spinner.start(line);
3605
- spinning = true;
3606
- }
3607
- };
3608
- const persist = (line) => {
3609
- if (spinning) {
3610
- spinner.stop();
3611
- spinning = false;
3612
- }
3613
- process.stdout.write(line + "\n");
3614
- };
3615
- let headerEmitted = false;
3616
- let summary;
3617
- const { emit, stream } = heldOutput(persist);
3618
- let arms = 0;
3619
- let mark = Date.now();
3620
- try {
3621
- summary = await reconcile({
3622
- dryRun,
3623
- shippedTarget,
3624
- onPhase: (line) => phase(line),
3625
- onTargetResolved: face ? void 0 : (target) => {
3626
- emit(updateHeader(target, dryRun, reportOptions));
3627
- emit("");
3628
- headerEmitted = true;
3629
- },
3630
- onArm: (arm) => {
3631
- const seconds = (Date.now() - mark) / 1e3;
3632
- mark = Date.now();
3633
- if (arms++) stream();
3634
- emit(face ? checklistLine(arm, seconds, face, reportOptions) : progressiveRow(arm, reportOptions));
3635
- }
3636
- });
3637
- } finally {
3638
- spinner.stop();
3639
- }
3640
- if (summary.reexec) return { summary, rendered: true };
3641
- stream();
3642
- if (face) return { summary, rendered: true };
3643
- if (headerEmitted) process.stdout.write(reportFooterLines(summary, reportOptions).join("\n") + "\n");
3644
- else process.stdout.write(formatReconcileReport(summary, reportOptions));
3645
- return { summary, rendered: true };
4018
+ function runMaintenance2(args, dependencies = {}) {
4019
+ return runMaintenance(installer_product_default, args, {
4020
+ engine: dependencies.reconcile ?? reconcile,
4021
+ schedule: dependencies.schedule ?? installTask,
4022
+ handOff: dependencies.handOff ?? handOff,
4023
+ run: dependencies.run,
4024
+ shippedTarget: shippedTargetFrom(args),
4025
+ counts: reportCounts,
4026
+ 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}
4027
+ ${retryHint(arm.detail)}` : void 0
4028
+ });
3646
4029
  }
3647
4030
  async function main(args = process.argv.slice(2)) {
3648
4031
  if (args.includes("--version")) {
@@ -3660,41 +4043,7 @@ async function main(args = process.argv.slice(2)) {
3660
4043
  process.stdout.write(json ? JSON.stringify(status, null, 2) + "\n" : formatHubStatus(status) + "\n");
3661
4044
  return status.failures.length ? 1 : 0;
3662
4045
  }
3663
- if (command === "update") {
3664
- const dryRun = args.includes("--dry-run");
3665
- const face = openFace(args, json, dryRun);
3666
- const { summary, rendered } = await runUpdate(args, json, face);
3667
- if (summary.reexec) return handOff(summary.reexec, args);
3668
- if (face) finishFace(face, summary, null, dryRun);
3669
- else if (!rendered) printReconcile(summary, json, dryRun);
3670
- return summary.exit;
3671
- }
3672
- if (command === "install") {
3673
- const dryRun = args.includes("--dry-run");
3674
- const face = openFace(args, json, dryRun);
3675
- const shippedTarget = shippedTargetFrom(args);
3676
- const { summary, rendered } = await runUpdate(args, json, face, shippedTarget);
3677
- if (summary.reexec) {
3678
- const handedOff = handOff(summary.reexec, ["update", ...args.filter((arg) => arg !== "install" && arg !== "--shipped" && arg !== shippedTarget)]);
3679
- const scheduled = installTask();
3680
- if (!json) {
3681
- process.stdout.write(autoupdateLine(scheduled) + "\n");
3682
- } else {
3683
- process.stdout.write(JSON.stringify({ convergence: summary, autoupdate: scheduled }, null, 2) + "\n");
3684
- }
3685
- return Math.max(handedOff, scheduled.ok ? 0 : 1);
3686
- }
3687
- const schedule = installTask();
3688
- if (json) {
3689
- process.stdout.write(JSON.stringify({ convergence: summary, autoupdate: schedule }, null, 2) + "\n");
3690
- } else if (face) {
3691
- finishFace(face, summary, schedule, dryRun);
3692
- } else {
3693
- if (!rendered) printReconcile(summary, false, dryRun);
3694
- process.stdout.write(autoupdateLine(schedule) + "\n");
3695
- }
3696
- return Math.max(summary.exit, schedule.ok ? 0 : 1);
3697
- }
4046
+ if (command === "update" || command === "install") return runMaintenance2(args);
3698
4047
  if (command === "autoupdate") {
3699
4048
  const commandIndex = args.indexOf(command);
3700
4049
  const mode = args[commandIndex + 1];
@@ -3719,27 +4068,18 @@ if (typeof require !== "undefined" && require.main === module) {
3719
4068
  }
3720
4069
  // Annotate the CommonJS export names for ESM import in node:
3721
4070
  0 && (module.exports = {
3722
- ACCENT,
3723
- FACE_PRODUCT,
3724
- MM_GREEN,
3725
- RESET,
3726
- accent,
3727
4071
  acquireSharedDocLock,
3728
- attentionRows,
3729
- autoupdateLine,
3730
- brandLine,
4072
+ assertFaceConformance,
3731
4073
  buildWatcherCommand,
3732
4074
  checkAtomicity,
3733
- checklistLine,
3734
4075
  claudeArm,
3735
4076
  claudeHostEvidence,
3736
4077
  clearLaunchTarget,
3737
4078
  clearPending,
4079
+ createInstallerRun,
3738
4080
  enumerateSurfaces,
3739
4081
  failConsequence,
3740
4082
  fleetStamp,
3741
- formatReconcileReport,
3742
- heldOutput,
3743
4083
  hermesArm,
3744
4084
  hermesHome,
3745
4085
  hermesHomes,
@@ -3766,17 +4106,15 @@ if (typeof require !== "undefined" && require.main === module) {
3766
4106
  probeHermesLaunchStaging,
3767
4107
  probeJervcodeInstallation,
3768
4108
  probeLaunchTargetVersion,
3769
- progressiveRow,
3770
4109
  readLaunchTarget,
3771
4110
  readPending,
3772
- receiptText,
3773
4111
  reconcile,
3774
4112
  reconcileCursorUserHooks,
3775
4113
  releaseSharedDocLock,
3776
4114
  reportCounts,
3777
- reportFooterLines,
3778
4115
  retiredSurfaces,
3779
4116
  retryHint,
4117
+ runMaintenance,
3780
4118
  schedulePendingWatcher,
3781
4119
  schedulerStatus,
3782
4120
  sharedDocLockPath,
@@ -3787,11 +4125,8 @@ if (typeof require !== "undefined" && require.main === module) {
3787
4125
  systemdServiceUnit,
3788
4126
  systemdTimerUnit,
3789
4127
  taskXml,
3790
- updateHeader,
3791
- updaterFace,
3792
4128
  watcherLockPath,
3793
4129
  withSharedDocLock,
3794
- wrapWords,
3795
4130
  writeLaunchTarget,
3796
4131
  writePending
3797
4132
  });