@mutmutco/hub 4.4.10 → 4.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.cjs +941 -698
  2. package/package.json +1 -4
package/dist/index.cjs CHANGED
@@ -21,27 +21,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/index.ts
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
- ACCENT: () => ACCENT,
25
- FACE_PRODUCT: () => FACE_PRODUCT,
26
- MM_GREEN: () => MM_GREEN,
27
- RESET: () => RESET,
28
- accent: () => accent,
29
24
  acquireSharedDocLock: () => acquireSharedDocLock,
30
- attentionRows: () => attentionRows,
31
- autoupdateLine: () => autoupdateLine,
32
- brandLine: () => brandLine,
25
+ assertFaceConformance: () => assertFaceConformance,
33
26
  buildWatcherCommand: () => buildWatcherCommand,
34
27
  checkAtomicity: () => checkAtomicity,
35
- checklistLine: () => checklistLine,
36
28
  claudeArm: () => claudeArm,
37
29
  claudeHostEvidence: () => claudeHostEvidence,
38
30
  clearLaunchTarget: () => clearLaunchTarget,
39
31
  clearPending: () => clearPending,
32
+ createInstallerRun: () => createInstallerRun,
40
33
  enumerateSurfaces: () => enumerateSurfaces,
41
34
  failConsequence: () => failConsequence,
42
35
  fleetStamp: () => fleetStamp,
43
- formatReconcileReport: () => formatReconcileReport,
44
- heldOutput: () => heldOutput,
45
36
  hermesArm: () => hermesArm,
46
37
  hermesHome: () => hermesHome,
47
38
  hermesHomes: () => hermesHomes,
@@ -68,17 +59,15 @@ __export(index_exports, {
68
59
  probeHermesLaunchStaging: () => probeHermesLaunchStaging,
69
60
  probeJervcodeInstallation: () => probeJervcodeInstallation,
70
61
  probeLaunchTargetVersion: () => probeLaunchTargetVersion,
71
- progressiveRow: () => progressiveRow,
72
62
  readLaunchTarget: () => readLaunchTarget,
73
63
  readPending: () => readPending,
74
- receiptText: () => receiptText,
75
64
  reconcile: () => reconcile,
76
65
  reconcileCursorUserHooks: () => reconcileCursorUserHooks,
77
66
  releaseSharedDocLock: () => releaseSharedDocLock,
78
67
  reportCounts: () => reportCounts,
79
- reportFooterLines: () => reportFooterLines,
80
68
  retiredSurfaces: () => retiredSurfaces,
81
69
  retryHint: () => retryHint,
70
+ runMaintenance: () => runMaintenance2,
82
71
  schedulePendingWatcher: () => schedulePendingWatcher,
83
72
  schedulerStatus: () => schedulerStatus,
84
73
  sharedDocLockPath: () => sharedDocLockPath,
@@ -89,40 +78,48 @@ __export(index_exports, {
89
78
  systemdServiceUnit: () => systemdServiceUnit,
90
79
  systemdTimerUnit: () => systemdTimerUnit,
91
80
  taskXml: () => taskXml,
92
- updateHeader: () => updateHeader,
93
- updaterFace: () => updaterFace,
94
81
  watcherLockPath: () => watcherLockPath,
95
82
  withSharedDocLock: () => withSharedDocLock,
96
- wrapWords: () => wrapWords2,
97
83
  writeLaunchTarget: () => writeLaunchTarget,
98
84
  writePending: () => writePending
99
85
  });
100
86
  module.exports = __toCommonJS(index_exports);
101
87
  var import_node_child_process9 = require("node:child_process");
102
- var import_node_fs18 = require("node:fs");
88
+ var import_node_fs22 = require("node:fs");
103
89
  var import_node_path13 = require("node:path");
104
90
 
105
- // node_modules/@mutmutco/installer-face/dist/index.js
91
+ // ../installer/face/src/face.ts
92
+ var import_node_fs = require("node:fs");
93
+
94
+ // ../installer/face/src/products.ts
106
95
  var PRODUCTS = Object.freeze({
107
96
  "mm-strategy": Object.freeze({
108
97
  name: "MM Strategy",
98
+ installWarm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
109
99
  accent: "38;2;249;115;22",
110
- warm: "Welcome. Let's set up MM Strategy \u2014 about a minute."
100
+ warm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
101
+ doctor: "mm-strategy doctor"
111
102
  }),
112
103
  "mmi-hub": Object.freeze({
113
104
  name: "mmi-hub",
105
+ installWarm: "Welcome. Setting up mmi-hub \u2014 about a minute.",
114
106
  accent: "38;2;125;211;252",
115
- warm: "Welcome back. Checking your surfaces\u2026"
107
+ warm: "Welcome back. Checking your surfaces\u2026",
108
+ doctor: "mmi doctor"
116
109
  }),
117
110
  "jerv-hub": Object.freeze({
118
111
  name: "jerv-hub",
112
+ installWarm: "Welcome. Setting up jerv-hub \u2014 about a minute.",
119
113
  accent: "38;2;248;113;113",
120
- warm: "Welcome back. Checking your surfaces\u2026"
114
+ warm: "Welcome back. Checking your surfaces\u2026",
115
+ doctor: "jerv doctor"
121
116
  }),
122
117
  jervcode: Object.freeze({
123
118
  name: "JervCode",
119
+ installWarm: "Welcome. Setting up JervCode \u2014 about a minute.",
124
120
  accent: "38;2;192;132;252",
125
- warm: "Welcome back. Keeping JervCode current\u2026"
121
+ warm: "Welcome back. Keeping JervCode current\u2026",
122
+ doctor: "jervcode doctor"
126
123
  })
127
124
  });
128
125
  function identityFor(product) {
@@ -132,6 +129,8 @@ function identityFor(product) {
132
129
  }
133
130
  return identity;
134
131
  }
132
+
133
+ // ../installer/face/src/face.ts
135
134
  var GLYPH = Object.freeze({
136
135
  diamond: "\u25C6",
137
136
  hollow: "\u25C7",
@@ -156,6 +155,9 @@ var ANSI = /\u001b\[[0-9;]*m/g;
156
155
  function visibleWidth(text) {
157
156
  return [...String(text).replace(ANSI, "")].length;
158
157
  }
158
+ function stripColor(text) {
159
+ return String(text).replace(ANSI, "");
160
+ }
159
161
  function faceWidth(columns) {
160
162
  const raw = Number(columns);
161
163
  return Math.max(40, Math.min(100, Number.isFinite(raw) && raw > 0 ? raw : 100));
@@ -184,7 +186,14 @@ function wrapWords(text, width) {
184
186
  if (line) lines.push(line);
185
187
  return lines.length ? lines : [""];
186
188
  }
187
- function createFace({ product, color = false, columns, env = process.env }) {
189
+ var PROGRESS_PROTOCOL = 1;
190
+ function readProgressFd(env) {
191
+ const fd = Number.parseInt(String(env.MM_PROGRESS_FD ?? ""), 10);
192
+ if (!Number.isInteger(fd) || fd <= 0) return null;
193
+ const protocol = Number.parseInt(String(env.MM_PROGRESS_PROTOCOL ?? PROGRESS_PROTOCOL), 10);
194
+ return protocol === PROGRESS_PROTOCOL ? fd : null;
195
+ }
196
+ function createFace({ product, color = false, columns, env = process.env, operation }) {
188
197
  const identity = identityFor(product);
189
198
  const width = faceWidth(columns);
190
199
  const paint = (sgr, text) => color ? `\x1B[${sgr}m${text}\x1B[0m` : String(text);
@@ -192,28 +201,45 @@ function createFace({ product, color = false, columns, env = process.env }) {
192
201
  const indent = " ".repeat(TITLE_COLUMN - 1);
193
202
  const continuedPhases = new Set((env.MM_FACE_CONTINUES ?? "").split(",").map((phase) => phase.trim()).filter(Boolean));
194
203
  const continuesFace = continuedPhases.size > 0;
195
- const welcome = () => continuesFace ? [] : [
204
+ const nested = env.MM_OUTER_CONSOLE === "1";
205
+ const progressFd = readProgressFd(env);
206
+ const emitMilestone = (title, measure, kind) => {
207
+ if (progressFd === null) return false;
208
+ const record = { v: PROGRESS_PROTOCOL, step: stripColor(title), state: kind };
209
+ if (typeof measure === "number") record.ms = Math.max(0, Math.round(measure * 1e3));
210
+ try {
211
+ (0, import_node_fs.writeSync)(progressFd, `${JSON.stringify(record)}
212
+ `);
213
+ return true;
214
+ } catch {
215
+ return false;
216
+ }
217
+ };
218
+ const welcome = () => continuesFace || nested ? [] : [
196
219
  `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
197
220
  bar(),
198
- `${bar()} ${identity.warm}`,
221
+ `${bar()} ${operation === "install" ? identity.installWarm : identity.warm}`,
199
222
  bar()
200
223
  ];
201
224
  const continues = (phase, kind = "ok") => {
202
225
  const inherited = continuedPhases.delete(String(phase).trim());
203
226
  return kind === "fail" ? false : inherited;
204
227
  };
205
- const step = (title, seconds = null, kind = "ok") => {
228
+ const step = (title, measure = null, kind = "ok") => {
229
+ if (emitMilestone(title, measure, kind)) return "";
206
230
  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`);
231
+ const measured = measure === null || measure === void 0 ? "" : paint(PALETTE.muted, typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : String(measure).trim());
232
+ const time = measured;
208
233
  const column = Math.min(44, Math.max(0, width - 8));
209
- const reserved = time ? 6 : 0;
234
+ const reserved = time ? Math.max(6, visibleWidth(time) + 2) : 0;
210
235
  const [first, ...rest] = wrapWords(title, width - TITLE_COLUMN - reserved);
211
236
  const head = `${bar()} ${glyph} ${first}`;
212
237
  const pad = Math.max(1, column - visibleWidth(head));
213
238
  return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
214
239
  };
215
- const relay = (text) => String(text).split("\n").map((line) => line.trim() === "" ? bar() : `${bar()}${indent}${line}`).join("\n");
216
- const receipt = (lines) => {
240
+ const relay = (text) => String(text).split("\n").flatMap((line) => line.trim() === "" ? [bar()] : (visibleWidth(line) <= width - TITLE_COLUMN ? [line] : wrapWords(line, width - TITLE_COLUMN)).map((part) => `${bar()}${indent}${part}`)).join("\n");
241
+ const receipt = (lines, { ready = true } = {}) => {
242
+ if (ready && nested) return [];
217
243
  const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
218
244
  const line = String(raw);
219
245
  if (visibleWidth(line) <= width - 6) return [line];
@@ -229,87 +255,713 @@ function createFace({ product, color = false, columns, env = process.env }) {
229
255
  frame(GLYPH.boxBottom, GLYPH.boxBottomEnd)
230
256
  ];
231
257
  };
232
- const signOff = () => `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
258
+ const outcome = (changed, options = {}) => {
259
+ const fact = String(changed ?? "").trim();
260
+ if (!fact) {
261
+ throw new Error(`installer face: the receipt must say what changed \u2014 pass the one measured fact this run produced, such as "Updated 2 of 7 surfaces to 4.4.10." (${identity.name})`);
262
+ }
263
+ return receipt([
264
+ `${GLYPH.check} ${identity.name} is ready.`,
265
+ fact,
266
+ `Check health any time: ${identity.doctor}`
267
+ ], options);
268
+ };
269
+ const signOff = () => nested ? "" : `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
233
270
  const refusal = (message3) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message3}`;
234
- return { identity, width, welcome, continues, step, relay, receipt, signOff, refusal, paint };
271
+ return { identity, width, nested, emitsProgress: progressFd !== null, welcome, continues, step, relay, receipt, outcome, signOff, refusal, paint };
235
272
  }
273
+
274
+ // ../installer/face/src/shell.ts
236
275
  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
- }
276
+
277
+ // ../installer/face/src/spinner.ts
278
+ var import_node_fs2 = require("node:fs");
279
+ var import_node_worker_threads = require("node:worker_threads");
280
+ var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
281
+ var WORKER_SOURCE = `
282
+ const { parentPort, workerData } = require('node:worker_threads');
283
+ const { writeSync, appendFileSync } = require('node:fs');
284
+ const control = new Int32Array(workerData.control);
285
+ let frames = workerData.frames, frame = workerData.frame;
286
+ function draw() {
287
+ if (Atomics.compareExchange(control, 1, 0, 1) !== 0) return;
288
+ try {
289
+ if (Atomics.load(control, 0) || Atomics.load(control, 2) || !frames.length) return;
290
+ const text = frames[frame++ % frames.length];
291
+ writeSync(2, text);
292
+ if (workerData.transcriptPath) appendFileSync(workerData.transcriptPath, JSON.stringify({ channel: 'spinner', text }) + '\\n');
293
+ } finally {
294
+ Atomics.store(control, 1, 0);
295
+ Atomics.notify(control, 1);
265
296
  }
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
297
  }
272
- function clip(text, width) {
273
- if (visibleWidth(text) <= width) return text;
274
- return `\u2026${[...text].slice(-(width - 1)).join("")}`;
275
- }
276
- var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
277
- function createSpinner(face, { animate, stream = process.stderr, intervalMs = 90 }) {
298
+ parentPort.on('message', (next) => {
299
+ if (Atomics.load(control, 2)) return;
300
+ frames = next.frames;
301
+ frame = next.frame;
302
+ Atomics.store(control, 0, 0);
303
+ });
304
+ setInterval(draw, workerData.intervalMs);
305
+ `;
306
+ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath }) {
307
+ animate = animate && !face.nested && !face.emitsProgress;
278
308
  let timer = null;
279
- let text = "";
309
+ let worker = null;
310
+ let control = null;
311
+ let frames = [];
280
312
  let frame = 0;
281
- const clear = () => {
282
- if (animate) stream.write("\r\x1B[2K");
313
+ const write = (text) => {
314
+ if (stream) stream.write(text);
315
+ else {
316
+ (0, import_node_fs2.writeSync)(2, text);
317
+ if (transcriptPath) (0, import_node_fs2.appendFileSync)(transcriptPath, `${JSON.stringify({ channel: "spinner", text })}
318
+ `);
319
+ }
320
+ };
321
+ const render = (text, measure) => {
322
+ const line = face.step(text, measure, "note").split("\n")[0];
323
+ frames = line ? FRAMES.map((glyph) => `\r\x1B[2K${line.replace(GLYPH.dot, glyph)}`) : [];
283
324
  };
284
325
  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;
326
+ if (animate && frames.length) write(frames[frame++ % frames.length]);
327
+ };
328
+ const pause = () => {
329
+ if (!control) return;
330
+ Atomics.store(control, 0, 1);
331
+ while (Atomics.load(control, 1)) Atomics.wait(control, 1, 1);
332
+ };
333
+ const halt = () => {
334
+ if (timer) clearInterval(timer);
335
+ timer = null;
336
+ if (control) Atomics.store(control, 2, 1);
337
+ pause();
338
+ if (worker) void worker.terminate();
339
+ worker = null;
340
+ control = null;
289
341
  };
290
342
  return {
291
- start(next) {
292
- text = next;
343
+ start(text, measure = null) {
344
+ if (!animate) return;
345
+ halt();
346
+ render(text, measure);
293
347
  frame = 0;
294
348
  draw();
295
- if (animate && !timer) timer = setInterval(draw, intervalMs).unref();
349
+ if (!frames.length) return;
350
+ if (stream) timer = setInterval(draw, intervalMs).unref();
351
+ else {
352
+ control = new Int32Array(new SharedArrayBuffer(12));
353
+ try {
354
+ worker = new import_node_worker_threads.Worker(WORKER_SOURCE, { eval: true, workerData: {
355
+ control: control.buffer,
356
+ frames,
357
+ frame,
358
+ intervalMs,
359
+ transcriptPath
360
+ } });
361
+ const active = worker;
362
+ worker.on("error", () => {
363
+ if (worker === active) halt();
364
+ });
365
+ worker.unref();
366
+ } catch {
367
+ halt();
368
+ }
369
+ }
296
370
  },
297
- say(next) {
298
- text = next;
371
+ say(text, measure = null) {
372
+ if (!animate) return;
373
+ pause();
374
+ render(text, measure);
299
375
  draw();
376
+ worker?.postMessage({ frames, frame });
300
377
  },
301
378
  stop() {
302
- if (timer) clearInterval(timer);
303
- timer = null;
304
- clear();
379
+ halt();
380
+ if (animate) write("\r\x1B[2K");
305
381
  }
306
382
  };
307
383
  }
308
384
  var SPINNER_FRAMES = Object.freeze([...FRAMES]);
385
+
386
+ // ../installer/face/src/conformance.ts
309
387
  var ALLOWED = new Set(Object.values(GLYPH));
388
+ var FORBIDDEN = /* @__PURE__ */ new Map([
389
+ ["\u2713", "U+2713 is not in the guidebook glyph set; the check is U+2714"],
390
+ ["\u2717", "U+2717 is not in the guidebook glyph set; the cross is U+2716"],
391
+ ["\u21BB", "U+21BB is not in the guidebook glyph set; a retry uses the dot U+25CF"],
392
+ ["\u250C", "U+250C is clack's intro corner; the welcome opens with the branded diamond"],
393
+ ["\u251C", "U+251C is clack's note frame; a receipt box pairs U+256D/U+256E over U+2570/U+256F"],
394
+ ["\u2514", "U+2514 is clack's outro corner; the sign-off carries the diamond"]
395
+ ]);
396
+ var EMOJI = /[\u{1f300}-\u{1faff}\u{2600}-\u{27bf}]/u;
397
+ function fail(rule, detail) {
398
+ throw new Error(`installer face: ${rule} \u2014 ${detail}`);
399
+ }
400
+ function assertFaceConformance(lines, options) {
401
+ const identity = identityFor(options.product);
402
+ const width = options.columns ?? 100;
403
+ const rendered = lines.flatMap((line) => String(line).split("\n"));
404
+ const plain = rendered.map(stripColor);
405
+ const head = plain[0] ?? "";
406
+ const signOffLine = `${GLYPH.diamond} ${identity.name} \xB7 Mutatis Mutandis`;
407
+ const opensWithWelcome = head.startsWith(GLYPH.diamond) && head !== signOffLine;
408
+ if (options.complete && options.tty !== false) {
409
+ const welcome = `${GLYPH.diamond} ${identity.name} \u2014 Mutatis Mutandis`;
410
+ const welcomes = plain.filter((line) => line === welcome).length;
411
+ const receipts = plain.filter((line) => line.startsWith(GLYPH.boxTop)).length;
412
+ const signOffs = plain.filter((line) => line === signOffLine).length;
413
+ if (welcomes !== (options.nested ? 0 : 1) || signOffs !== (options.nested ? 0 : 1) || !options.nested && receipts !== 1) {
414
+ fail("complete run", "expected one console owner, one welcome, one receipt and one sign-off");
415
+ }
416
+ for (const line of plain) {
417
+ if (line && ![GLYPH.bar, GLYPH.diamond, GLYPH.hollow, GLYPH.boxTop, GLYPH.boxBottom].some((glyph) => line.startsWith(glyph))) {
418
+ fail("complete run", `unframed output: ${JSON.stringify(line)}`);
419
+ }
420
+ }
421
+ }
422
+ if (rendered.length === 0) fail("R7 render before you ship", "no rendered lines were given to the guard");
423
+ const greetings = plain.filter((line) => ["\u2014", "-"].some((separator) => line.includes(`${identity.name} ${separator} Mutatis Mutandis`)));
424
+ if (greetings.length > 1) fail("R8 one welcome per run", "duplicate product greeting");
425
+ if (plain.filter((line) => line.startsWith(GLYPH.boxTop)).length > 1) {
426
+ fail("R8 one receipt per run", "duplicate receipt");
427
+ }
428
+ if (options.nested === true) {
429
+ if (opensWithWelcome) {
430
+ fail("R8 a nested run draws no welcome", `an outer console already greeted the user; this run opened with ${JSON.stringify(head)}`);
431
+ }
432
+ const signOffAt = plain.findIndex((line) => line === signOffLine);
433
+ if (signOffAt !== -1) {
434
+ fail("R8 a nested run draws no sign-off", `the outer console closes the run; line ${signOffAt + 1} signs off`);
435
+ }
436
+ const boxAt = plain.findIndex((line) => line.startsWith(GLYPH.boxTop));
437
+ if (boxAt !== -1) {
438
+ const headline = plain.slice(boxAt + 1).find((line) => line.startsWith(GLYPH.bar) && line.trim() !== GLYPH.bar) ?? "";
439
+ if (headline.includes(GLYPH.check)) {
440
+ fail("R8 a nested run draws no success receipt", `the outer console owns the outcome; this run printed ${JSON.stringify(headline.trim())}`);
441
+ }
442
+ }
443
+ }
444
+ if (options.tty === false) {
445
+ for (const line of plain) {
446
+ const glyph = [...line].find((char) => ALLOWED.has(char));
447
+ if (glyph) fail("non-TTY lane is plain", `a piped run emitted ${JSON.stringify(glyph)} in ${JSON.stringify(line)}`);
448
+ }
449
+ for (const line of rendered) {
450
+ if (line.includes("\x1B[")) fail("non-TTY lane is plain", `a piped run emitted colour in ${JSON.stringify(line)}`);
451
+ }
452
+ return;
453
+ }
454
+ for (const line of plain) {
455
+ if (line.startsWith(`${GLYPH.bar} ${GLYPH.hollow} `)) {
456
+ fail("note step uses U+25CF", `U+25C7 is the note-box opener in ${JSON.stringify(line)}`);
457
+ }
458
+ }
459
+ for (const line of plain) {
460
+ if (/[\u25c6\u25c7\u2714\u2716\u25cf] (?! )/u.test(line)) {
461
+ fail("R4 two spaces after a glyph", `a single space follows the glyph in ${JSON.stringify(line)}`);
462
+ }
463
+ }
464
+ for (const line of plain) {
465
+ for (const [glyph, why] of FORBIDDEN) {
466
+ if (line.includes(glyph)) fail("glyph set", `${why} (in ${JSON.stringify(line)})`);
467
+ }
468
+ const emoji = [...line].find((char) => EMOJI.test(char) && !ALLOWED.has(char));
469
+ if (emoji) fail("glyph set", `emoji ${JSON.stringify(emoji)} in ${JSON.stringify(line)}`);
470
+ }
471
+ const top = plain.findIndex((line) => line.startsWith(GLYPH.boxTop));
472
+ if (top !== -1) {
473
+ const bottom = plain.findIndex((line, index) => index > top && line.startsWith(GLYPH.boxBottom));
474
+ if (bottom === -1) fail("R5 receipt frame", "the receipt box opens and never closes");
475
+ if (!plain[top].endsWith(GLYPH.boxTopEnd)) fail("R5 receipt frame", `the top row does not pair its corners: ${JSON.stringify(plain[top])}`);
476
+ if (!plain[bottom].endsWith(GLYPH.boxBottomEnd)) fail("R5 receipt frame", `the bottom row does not pair its corners: ${JSON.stringify(plain[bottom])}`);
477
+ const widths = new Set(plain.slice(top, bottom + 1).map(visibleWidth));
478
+ if (widths.size !== 1) fail("R5 receipt frame", `the box is ragged: widths ${[...widths].join(", ")}`);
479
+ const rows = plain.slice(top + 1, bottom);
480
+ if (rows.length > 0 && rows[0].includes(GLYPH.check)) {
481
+ if (rows.length < 3) {
482
+ fail("R9 the receipt says what changed", `a success receipt has three lines \u2014 ready, what changed, the health command \u2014 and this one has ${rows.length}: ${JSON.stringify(rows.map((row) => row.trim()))}`);
483
+ }
484
+ const changed = rows[1].replace(GLYPH.bar, "").trim();
485
+ if (changed === "") fail("R9 the receipt says what changed", "the middle line of the success receipt is blank");
486
+ }
487
+ }
488
+ const inReceipt = (index) => top !== -1 && index >= top && index <= plain.findIndex((line, at) => at > top && line.startsWith(GLYPH.boxBottom));
489
+ const welcomeEnd = opensWithWelcome ? 4 : 0;
490
+ for (const [index, line] of plain.entries()) {
491
+ if (index < welcomeEnd || inReceipt(index)) continue;
492
+ if (!line.startsWith(GLYPH.bar) || line.trim() === GLYPH.bar) continue;
493
+ const after = [...line.slice(1)];
494
+ const at = after.findIndex((char) => char !== " ");
495
+ const edge = at + 2;
496
+ if (edge === 4 && ALLOWED.has(after[at])) continue;
497
+ if (edge !== TITLE_COLUMN + 1) {
498
+ fail("R1 one rail, one left edge", `text starts at column ${edge} without a glyph there; a line that carries no glyph belongs at the title column (${TITLE_COLUMN + 1}): ${JSON.stringify(line)}`);
499
+ }
500
+ }
501
+ for (const line of plain) {
502
+ if (/\(node:\d+\)|DeprecationWarning|ExperimentalWarning/u.test(line)) {
503
+ fail("R2 no foreign output in the face", `a Node warning reached the face: ${JSON.stringify(line)} \u2014 fix the spawn, or run the child with --no-deprecation`);
504
+ }
505
+ }
506
+ const receiptStart = top === -1 ? plain.length : top;
507
+ for (const line of plain.slice(0, receiptStart)) {
508
+ if (/[\u2714\u2716\u25cf\u25c7]/u.test(line) && /([A-Za-z]:\\|\/(?:home|Users|opt|usr)\/)/u.test(line)) {
509
+ fail("R3 paths live in the receipt", `a step line carries a path: ${JSON.stringify(line)}`);
510
+ }
511
+ }
512
+ for (const line of plain) {
513
+ if (visibleWidth(line) > width) {
514
+ fail("width cap", `a line is ${visibleWidth(line)} columns wide, cap ${width}: ${JSON.stringify(line)}`);
515
+ }
516
+ }
517
+ if (opensWithWelcome) {
518
+ const expected = `${GLYPH.diamond} ${identity.name} \u2014 Mutatis Mutandis`;
519
+ if (head !== expected) fail("welcome is the product table's", `expected ${JSON.stringify(expected)}, got ${JSON.stringify(head)}`);
520
+ const warm = plain.slice(1, 4).find((line) => line.includes(identity.warm) || line.includes(identity.installWarm));
521
+ if (!warm) fail("welcome is the product table's", `the warm line for ${options.product} is missing or reworded`);
522
+ }
523
+ const painted = rendered.join("\n");
524
+ if (painted.includes("\x1B[")) {
525
+ if (!painted.includes(identity.accent) && !painted.includes(PALETTE.muted)) {
526
+ fail("accent", `${options.product} rendered colour that is neither its accent nor a neutral token`);
527
+ }
528
+ }
529
+ }
530
+
531
+ // ../installer/face/src/run.ts
532
+ var import_node_fs4 = require("node:fs");
533
+
534
+ // ../installer/face/src/outcome.ts
535
+ var import_node_fs3 = require("node:fs");
536
+ var counts = ["total", "updated", "failed"];
537
+ var strings = ["version", "retry", "detail", "logPath"];
538
+ var flags = ["dryRun", "installed", "deferred", "operationFailed"];
539
+ function validateInstallerOutcome(value) {
540
+ const invalid = () => {
541
+ throw new Error("installer outcome: invalid child result");
542
+ };
543
+ if (!value || typeof value !== "object" || Array.isArray(value)) return invalid();
544
+ const facts = value;
545
+ const allowed = [...counts, ...strings, ...flags, "rollback", "logState"];
546
+ if (Object.keys(facts).some((key) => !allowed.includes(key))) return invalid();
547
+ for (const key of counts) if (!Number.isSafeInteger(facts[key]) || facts[key] < 0) return invalid();
548
+ if (facts.updated + facts.failed > facts.total) return invalid();
549
+ for (const key of strings) if (facts[key] !== void 0 && (typeof facts[key] !== "string" || facts[key].length > 4096)) return invalid();
550
+ for (const key of flags) if (facts[key] !== void 0 && typeof facts[key] !== "boolean") return invalid();
551
+ if (facts.rollback !== void 0 && !["completed", "partial", "not-needed", "unknown"].includes(facts.rollback)) return invalid();
552
+ if (facts.logState !== void 0 && !["unavailable", "omitted"].includes(facts.logState)) return invalid();
553
+ if (facts.logPath !== void 0 && (!facts.logPath.trim() || /[\x00-\x1f\x7f]/u.test(facts.logPath))) return invalid();
554
+ return { ...facts };
555
+ }
556
+ function writeInstallerOutcome(path, value) {
557
+ (0, import_node_fs3.writeFileSync)(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
558
+ }
559
+
560
+ // ../installer/face/src/run.ts
561
+ function validateInstallerProduct(value) {
562
+ const fail2 = (field) => {
563
+ throw new Error(`installer product: invalid ${field}`);
564
+ };
565
+ if (!value || typeof value !== "object" || Array.isArray(value)) return fail2("declaration");
566
+ const input = value;
567
+ const text = (value2, field) => typeof value2 === "string" && value2.trim() && !/[\r\n\x00-\x1f]/u.test(value2) ? value2 : fail2(field);
568
+ const key = text(input.product, "product");
569
+ const product = { mmi: "mmi-hub", jerv: "jerv-hub" }[key] ?? key;
570
+ const identity = identityFor(product);
571
+ const gate = text(input.gate, "gate");
572
+ let gateUrl;
573
+ try {
574
+ gateUrl = new URL(gate);
575
+ } catch {
576
+ return fail2("gate");
577
+ }
578
+ if (!["https:", "http:"].includes(gateUrl.protocol) || gateUrl.username || gateUrl.password) return fail2("gate");
579
+ const doctor = text(input.doctor, "doctor");
580
+ if (doctor !== identity.doctor) return fail2("doctor");
581
+ if (!Array.isArray(input.surfaces) || input.surfaces.length === 0) return fail2("surfaces");
582
+ const ids = /* @__PURE__ */ new Set();
583
+ const surfaces = input.surfaces.map((raw) => {
584
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail2("surface");
585
+ const source = raw;
586
+ const id = text(source.id, "surface.id");
587
+ if (ids.has(id)) return fail2("duplicate surface.id");
588
+ ids.add(id);
589
+ const surface = { id };
590
+ for (const field of ["npm", "bin", "kind", "activation"]) {
591
+ if (source[field] !== void 0) surface[field] = text(source[field], `surface.${field}`);
592
+ }
593
+ if (Boolean(surface.npm) !== Boolean(surface.bin)) return fail2("surface npm/bin pair");
594
+ if (surface.kind !== void 0 && !["agent-home", "payload"].includes(surface.kind)) return fail2("surface.kind");
595
+ if (!surface.npm && !surface.kind) return fail2("surface implementation");
596
+ return surface;
597
+ });
598
+ return { product, gate, doctor, surfaces };
599
+ }
600
+ var PHASES = {
601
+ preflight: ["Checking prerequisites", "Checked prerequisites"],
602
+ resolve: ["Resolving the release", "Resolved the release"],
603
+ download: ["Downloading the payload", "Downloaded the payload"],
604
+ "sign-in": ["Signing in", "Signed in"],
605
+ check: ["Checking surfaces", "Checked surfaces"],
606
+ arm: ["Scheduling updates", "Armed hourly updates"],
607
+ "verify-release": ["Checking the release version", "Verified the release version"],
608
+ verify: ["Verifying the payload", "Verified the payload"],
609
+ install: ["Installing the product", "Installed the product"],
610
+ activate: ["Activating surfaces", "Activated surfaces"],
611
+ doctor: ["Checking health", "Checked health"],
612
+ rollback: ["Restoring the previous version", "Restored the previous version"]
613
+ };
614
+ function createInstallerRun(value, options = {}) {
615
+ const declaration = validateInstallerProduct(value);
616
+ const env = options.env ?? process.env;
617
+ const tty = options.tty ?? Boolean(process.stdout.isTTY);
618
+ const face = createFace({
619
+ operation: options.operation,
620
+ product: declaration.product,
621
+ columns: options.columns,
622
+ env,
623
+ color: tty && options.color !== false && env.NO_COLOR === void 0 && env.TERM !== "dumb"
624
+ });
625
+ const errors = [];
626
+ const write = options.write ? (text, channel) => {
627
+ try {
628
+ options.write(text, channel);
629
+ } catch (error) {
630
+ errors.push(`installer output observer: ${error instanceof Error ? error.message : String(error)}`);
631
+ }
632
+ } : (text, channel) => {
633
+ (channel === "stdout" ? process.stdout : process.stderr).write(text);
634
+ };
635
+ const emit = (text, channel = "stdout", recorded = text) => {
636
+ if (!text) return;
637
+ write(text, channel);
638
+ if (env.MM_FACE_TRANSCRIPT) {
639
+ (0, import_node_fs4.appendFileSync)(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
640
+ `, "utf8");
641
+ }
642
+ };
643
+ const lines = (rows, channel = "stdout") => {
644
+ for (const row of rows.flatMap((row2) => row2.split("\n"))) if (row) emit(`${row}
645
+ `, channel);
646
+ };
647
+ const spinner = createSpinner(face, {
648
+ animate: tty && env.TERM !== "dumb" && (options.animate ?? Boolean(process.stderr.isTTY)),
649
+ ...options.write ? { stream: { write: (text) => {
650
+ emit(String(text), "spinner");
651
+ return true;
652
+ } } } : { transcriptPath: env.MM_FACE_TRANSCRIPT }
653
+ });
654
+ let started = false;
655
+ let finished = false;
656
+ const start = () => {
657
+ if (started || finished) return;
658
+ started = true;
659
+ if (options.quiet) return;
660
+ const welcome = face.welcome();
661
+ if (tty) lines(welcome);
662
+ else if (welcome.length) {
663
+ const warm = options.operation === "install" ? face.identity.installWarm : face.identity.warm;
664
+ lines([`${face.identity.name} - Mutatis Mutandis`, warm.replaceAll("\u2014", "-").replaceAll("\u2026", "...")]);
665
+ }
666
+ };
667
+ const durable = (title, measure, kind) => {
668
+ spinner.stop();
669
+ const rendered = face.step(title, measure, kind);
670
+ if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
671
+ };
672
+ const run = {
673
+ get errors() {
674
+ return [...errors];
675
+ },
676
+ start,
677
+ phase(id, facts = {}) {
678
+ if (finished) throw new Error("installer run already finished");
679
+ if (!Object.hasOwn(PHASES, id)) throw new Error("installer run: unknown phase");
680
+ start();
681
+ const state = facts.state ?? "ok";
682
+ const title = PHASES[id][state === "ok" ? 1 : 0];
683
+ if (state === "running") {
684
+ spinner.start(title, facts.measure ?? null);
685
+ return;
686
+ }
687
+ if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
688
+ if (facts.detail) run.relay(facts.detail);
689
+ },
690
+ surface(facts) {
691
+ if (finished) throw new Error("installer run already finished");
692
+ const surface = declaration.surfaces.find((surface2) => surface2.id === facts.id);
693
+ if (!surface) throw new Error("installer run: undeclared surface");
694
+ start();
695
+ const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} ${tty ? "\u2192" : "->"} ${facts.to}` : ` ${facts.to}` : "";
696
+ const status = { updated: options.dryRun ? "would update" : "updated", current: "already current", failed: "failed", skipped: "skipped", retry: "retrying", pending: "pending", kept: "kept" }[facts.state];
697
+ if (!status) throw new Error("installer run: unknown surface state");
698
+ const separator = tty ? "\xB7" : "-";
699
+ const activation = facts.state === "updated" && surface.activation ? ` ${separator} ${surface.activation}` : "";
700
+ const completed = ["updated", "current", "kept", "skipped"].includes(facts.state);
701
+ durable(`${facts.id}${versions} ${separator} ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
702
+ if (facts.detail) run.relay(facts.detail);
703
+ },
704
+ milestone({ step, state, ms }) {
705
+ start();
706
+ durable(step, ms === void 0 ? null : ms / 1e3, state);
707
+ },
708
+ signIn({ url, code }) {
709
+ start();
710
+ spinner.stop();
711
+ for (const text of [`Open ${url}`, `Enter code: ${code}`]) {
712
+ const rendered = `${tty ? face.relay(text) : text}
713
+ `;
714
+ emit(rendered, "stdout", rendered.replace(code, "[redacted]"));
715
+ }
716
+ },
717
+ // Only pass safe diagnostic text, never authentication output or credentials.
718
+ relay(text, channel = "stdout", record = true) {
719
+ spinner.stop();
720
+ const rendered = tty ? face.relay(text) : stripColor(text);
721
+ for (const row of rendered.split("\n")) if (row) {
722
+ emit(`${row}
723
+ `, channel, record ? `${row}
724
+ ` : `${tty ? face.relay("[external output omitted]") : "[external output omitted]"}
725
+ `);
726
+ }
727
+ },
728
+ cancel() {
729
+ if (finished) return;
730
+ if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(
731
+ env.MM_INSTALLER_OUTCOME_FILE,
732
+ { total: 0, updated: 0, failed: 0, deferred: true, detail: "Operation cancelled." }
733
+ );
734
+ start();
735
+ spinner.stop();
736
+ finished = true;
737
+ if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE) {
738
+ lines(tty ? face.receipt(["Operation cancelled."], { ready: false }) : ["Operation cancelled."]);
739
+ }
740
+ if (tty) lines([face.signOff()]);
741
+ },
742
+ finish(facts) {
743
+ if (finished) return;
744
+ validateInstallerOutcome(facts);
745
+ if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(env.MM_INSTALLER_OUTCOME_FILE, facts);
746
+ start();
747
+ spinner.stop();
748
+ finished = true;
749
+ if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
750
+ const changed = !facts.version ? "No release target is available." : facts.dryRun ? `Would update ${facts.updated} of ${facts.total} surfaces to ${facts.version}.` : facts.installed ? `Installed ${facts.version} across ${facts.total} surfaces.` : `Updated ${facts.updated} of ${facts.total} surfaces to ${facts.version}.`;
751
+ const ready = facts.failed === 0 && !facts.dryRun && !facts.deferred && !facts.operationFailed && Boolean(facts.version);
752
+ const body = [
753
+ `${ready ? GLYPH.check : facts.dryRun ? GLYPH.dot : GLYPH.cross} ${face.identity.name}${ready ? " is ready." : facts.dryRun ? " preview complete." : facts.deferred ? " update deferred." : " is not ready yet."}`,
754
+ changed,
755
+ ...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
756
+ ...facts.detail ? [facts.detail] : [],
757
+ ...facts.rollback ? [`Rollback: ${facts.rollback}`] : [],
758
+ ...facts.logPath ? [`Log: ${facts.logPath}`] : facts.logState ? [`Log: ${facts.logState}`] : [],
759
+ facts.retry && (facts.failed > 0 || facts.operationFailed) ? `Retry: ${facts.retry}` : `Check health any time: ${declaration.doctor}`
760
+ ];
761
+ if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE && (facts.failed > 0 || facts.operationFailed)) {
762
+ lines(tty ? face.receipt(body, { ready }) : body.map((row) => row.replace(/^[✔✖●] /u, "")));
763
+ }
764
+ if (tty) lines([face.signOff()]);
765
+ },
766
+ stop() {
767
+ spinner.stop();
768
+ }
769
+ };
770
+ return run;
771
+ }
772
+
773
+ // ../installer/face/src/maintenance.ts
774
+ async function runMaintenance(product, args, dependencies) {
775
+ const json = args.includes("--json");
776
+ const quiet = args.includes("--quiet");
777
+ const dryRun = args.includes("--dry-run");
778
+ const installed = args.includes("install");
779
+ const render = dependencies.render;
780
+ const env = render?.env ?? process.env;
781
+ const tty = render?.tty ?? Boolean(process.stdout.isTTY);
782
+ const run = json ? null : dependencies.run ?? createInstallerRun(product, {
783
+ ...render,
784
+ tty,
785
+ env,
786
+ color: tty && render?.color !== false && !args.includes("--no-color") && env.TERM !== "dumb",
787
+ columns: render?.columns ?? process.stdout.columns,
788
+ dryRun,
789
+ quiet,
790
+ operation: installed ? "install" : "update"
791
+ });
792
+ if (!quiet) run?.start();
793
+ let active;
794
+ let activeFacts;
795
+ let handedOff;
796
+ const shown = new Set((env.MM_FACE_CONTINUES ?? "").split(",").map((value) => value.trim()).filter(Boolean));
797
+ if (!quiet && !json) shown.add("welcome");
798
+ let phaseFailed = false;
799
+ let armFailed = false;
800
+ let knownFailure;
801
+ const phaseStarted = /* @__PURE__ */ new Map();
802
+ const phase = (event) => {
803
+ if (handedOff !== void 0) return;
804
+ if (event.state === "fail") phaseFailed = true;
805
+ const started = phaseStarted.get(event.id);
806
+ if (event.state === "running") {
807
+ if (started === void 0) phaseStarted.set(event.id, Date.now());
808
+ active = event.id;
809
+ } else {
810
+ phaseStarted.delete(event.id);
811
+ if (active === event.id) active = void 0;
812
+ }
813
+ if (!quiet && event.state === "running" && event.detail) run?.relay(event.detail);
814
+ const measured = event.measured;
815
+ const measure = measured?.kind === "percent" ? `${Math.max(0, Math.min(100, Math.round(measured.percent)))}%` : measured?.kind === "bytes" ? `${measured.done}/${measured.total} B` : measured?.kind === "count" ? `${measured.done}${measured.total === void 0 ? "" : `/${measured.total}`}` : event.measure;
816
+ const facts = {
817
+ state: event.state,
818
+ detail: event.detail,
819
+ measure,
820
+ ...event.state !== "running" && started !== void 0 ? { seconds: (Date.now() - started) / 1e3 } : {}
821
+ };
822
+ if (event.state === "running") activeFacts = facts;
823
+ if (!quiet) {
824
+ run?.phase(event.id, facts);
825
+ if (event.state === "ok" && run) shown.add(event.id);
826
+ }
827
+ };
828
+ const pending = [];
829
+ let streaming = false;
830
+ const emitArm = (arm) => {
831
+ if (quiet) return;
832
+ run?.surface({
833
+ id: arm.surface,
834
+ from: arm.from ?? void 0,
835
+ to: arm.to ?? void 0,
836
+ state: arm.verdict === "fail" ? "failed" : arm.verdict === "defer" ? "retry" : arm.verdict === "skip" ? "kept" : arm.from === arm.to && !arm.launchStaged ? "current" : "updated",
837
+ ...arm.durationMs === void 0 ? {} : { seconds: arm.durationMs / 1e3 },
838
+ detail: dependencies.diagnose?.(arm)
839
+ });
840
+ };
841
+ const writeJson = (summary, schedule) => {
842
+ process.stdout.write(JSON.stringify(dependencies.jsonResult ? dependencies.jsonResult(summary, schedule) : installed ? { convergence: summary, autoupdate: schedule } : summary, null, 2) + "\n");
843
+ };
844
+ try {
845
+ const summary = await dependencies.engine({
846
+ dryRun,
847
+ withCustody: async (operation) => {
848
+ if (handedOff !== void 0) throw new Error("installer terminal ownership already transferred");
849
+ run?.stop();
850
+ const result = await operation({ faceContinues: [...shown] });
851
+ if (result.kind === "reexec") {
852
+ if (!Number.isSafeInteger(result.code) || result.code < 0) throw new Error("invalid handoff exit code");
853
+ handedOff = result.code;
854
+ } else if (result.kind === "resume") {
855
+ if (!quiet && active) run?.phase(active, activeFacts);
856
+ } else throw new Error("invalid terminal custody result");
857
+ return result;
858
+ },
859
+ shippedTarget: installed ? dependencies.shippedTarget : void 0,
860
+ narrate: !quiet && !json && args.includes("--verbose") ? (text) => {
861
+ if (handedOff === void 0) run?.relay(text, "stderr", false);
862
+ } : void 0,
863
+ relay: (text, channel = "stdout", record = false) => {
864
+ if (handedOff === void 0) run?.relay(text, channel, record);
865
+ },
866
+ onTargetResolved: (target) => phase({ id: "verify-release", detail: target, state: "ok" }),
867
+ onPhase: () => {
868
+ if (!quiet && handedOff === void 0) run?.phase("check", { state: "running" });
869
+ },
870
+ onPhaseEvent: phase,
871
+ onArm: (arm) => {
872
+ if (handedOff !== void 0) return;
873
+ if (arm.verdict === "fail") armFailed = true;
874
+ pending.push(arm);
875
+ if (pending.length > 1) streaming = true;
876
+ if (streaming) for (const result of pending.splice(0)) emitArm(result);
877
+ }
878
+ });
879
+ if (handedOff !== void 0) return handedOff;
880
+ if (summary.reexec) {
881
+ if (!dependencies.handOff) {
882
+ knownFailure = "The updated version needs a handoff, but no handoff is configured.";
883
+ throw new Error(knownFailure);
884
+ }
885
+ run?.stop();
886
+ knownFailure = "The updated updater could not launch.";
887
+ return await dependencies.handOff(summary.reexec, args, { ...process.env, MM_FACE_CONTINUES: "welcome" });
888
+ }
889
+ const counts2 = dependencies.counts(summary);
890
+ validateInstallerOutcome({ total: counts2.total, updated: counts2.updated, failed: counts2.failed });
891
+ if (!Number.isSafeInteger(counts2.retry) || counts2.retry < 0 || counts2.retry > counts2.total) throw new Error("invalid retry count");
892
+ if (summary.cancelled && summary.exit === 0 && !phaseFailed && !armFailed && counts2.failed === 0 && counts2.updated === 0) {
893
+ if (json) writeJson(summary, null);
894
+ else run?.cancel();
895
+ return 0;
896
+ }
897
+ for (const arm of pending) emitArm(arm);
898
+ const waiting = summary.surfaces.filter((surface) => surface.present && surface.action === "arm-pending");
899
+ if (!quiet) for (const surface of waiting) run?.surface({ id: surface.id, state: "pending" });
900
+ let schedule = null;
901
+ if (installed && !dryRun && dependencies.schedule) {
902
+ phase({ id: "arm", state: "running" });
903
+ schedule = dependencies.schedule();
904
+ if (schedule) phase({ id: "arm", state: schedule.ok ? "ok" : "fail", detail: schedule.ok ? void 0 : schedule.detail });
905
+ }
906
+ const failed = phaseFailed || armFailed || counts2.failed > 0 || Boolean(schedule && !schedule.ok);
907
+ const exit = Math.max(summary.exit, failed ? 1 : 0);
908
+ if (json) {
909
+ writeJson({ ...summary, exit }, schedule);
910
+ } else {
911
+ run.finish({
912
+ version: summary.target ?? void 0,
913
+ total: counts2.total,
914
+ updated: counts2.updated,
915
+ failed: counts2.failed,
916
+ operationFailed: exit !== 0,
917
+ deferred: !failed && Boolean(counts2.retry || waiting.length || summary.deferred || summary.cancelled || !summary.target),
918
+ dryRun,
919
+ installed,
920
+ detail: schedule && !schedule.ok ? schedule.detail : summary.detail ?? (summary.cancelled ? "Operation cancelled." : void 0),
921
+ retry: summary.retry,
922
+ rollback: summary.rollback,
923
+ logPath: summary.logPath,
924
+ logState: summary.logState
925
+ });
926
+ }
927
+ return exit;
928
+ } catch (error) {
929
+ if (handedOff !== void 0) throw error;
930
+ let detail = knownFailure ?? (active === "sign-in" ? "Sign-in did not complete." : installed ? "Installation did not complete." : "Update did not complete.");
931
+ if (!knownFailure && dependencies.safeErrorDetail) {
932
+ try {
933
+ detail = dependencies.safeErrorDetail(error);
934
+ } catch {
935
+ }
936
+ }
937
+ if (active) phase({ id: active, state: "fail" });
938
+ run?.relay(detail, "stderr");
939
+ run?.finish({ total: 0, updated: 0, failed: 0, operationFailed: true, detail });
940
+ if (json) process.stdout.write(JSON.stringify({ error: detail }) + "\n");
941
+ return 1;
942
+ } finally {
943
+ run?.stop();
944
+ }
945
+ }
946
+
947
+ // ../installer.product.json
948
+ var installer_product_default = {
949
+ product: "mmi",
950
+ gate: "https://gate.mutmut.work/mmi",
951
+ doctor: "mmi doctor",
952
+ surfaces: [
953
+ { id: "updater", npm: "@mutmutco/hub", bin: "mmi-hub" },
954
+ { id: "cli", npm: "@mutmutco/cli", bin: "mmi-cli" },
955
+ { id: "claude", kind: "agent-home", activation: "next launch" },
956
+ { id: "codex", kind: "agent-home", activation: "next launch" },
957
+ { id: "cursor", kind: "agent-home", activation: "next launch" },
958
+ { id: "hermes", kind: "agent-home", activation: "next launch" },
959
+ { id: "jervcode", kind: "agent-home", activation: "next launch" }
960
+ ]
961
+ };
310
962
 
311
963
  // src/state.ts
312
- var import_node_fs = require("node:fs");
964
+ var import_node_fs5 = require("node:fs");
313
965
  var import_node_os = require("node:os");
314
966
  var import_node_path = require("node:path");
315
967
  function stateRoot(env = process.env) {
@@ -323,8 +975,8 @@ function statePaths(env = process.env) {
323
975
  }
324
976
  function ensureState(env = process.env) {
325
977
  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 });
978
+ (0, import_node_fs5.mkdirSync)(paths.staging, { recursive: true });
979
+ (0, import_node_fs5.mkdirSync)((0, import_node_path.dirname)(paths.gitCache), { recursive: true });
328
980
  return paths;
329
981
  }
330
982
  var UpdaterBusy = class extends Error {
@@ -334,15 +986,15 @@ var LEASE_STALE_MS = 10 * 6e4;
334
986
  function tryAcquire(path) {
335
987
  const token = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
336
988
  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);
989
+ const fd = (0, import_node_fs5.openSync)(path, "wx");
990
+ (0, import_node_fs5.writeFileSync)(fd, JSON.stringify({ token, pid: process.pid, ts: Date.now() }) + "\n");
991
+ (0, import_node_fs5.closeSync)(fd);
340
992
  return {
341
993
  token,
342
994
  release() {
343
995
  try {
344
996
  const current2 = readLease(path);
345
- if (current2 && current2.token === token) (0, import_node_fs.unlinkSync)(path);
997
+ if (current2 && current2.token === token) (0, import_node_fs5.unlinkSync)(path);
346
998
  } catch {
347
999
  }
348
1000
  }
@@ -353,7 +1005,7 @@ function tryAcquire(path) {
353
1005
  const current = readLease(path);
354
1006
  if (current && Date.now() - current.ts > LEASE_STALE_MS) {
355
1007
  try {
356
- (0, import_node_fs.renameSync)(path, `${path}.stale-${Date.now()}`);
1008
+ (0, import_node_fs5.renameSync)(path, `${path}.stale-${Date.now()}`);
357
1009
  return "stolen-stale";
358
1010
  } catch {
359
1011
  }
@@ -362,7 +1014,7 @@ function tryAcquire(path) {
362
1014
  }
363
1015
  function readLease(path) {
364
1016
  try {
365
- return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
1017
+ return JSON.parse((0, import_node_fs5.readFileSync)(path, "utf8"));
366
1018
  } catch {
367
1019
  return null;
368
1020
  }
@@ -387,10 +1039,10 @@ function withLease(path, fn, waitMs = LEASE_WAIT_MS) {
387
1039
  }
388
1040
  function appendJournal(path, event) {
389
1041
  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);
1042
+ (0, import_node_fs5.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
1043
+ const fd = (0, import_node_fs5.openSync)(path, "a");
1044
+ (0, import_node_fs5.writeFileSync)(fd, JSON.stringify(event) + "\n");
1045
+ (0, import_node_fs5.closeSync)(fd);
394
1046
  return true;
395
1047
  } catch {
396
1048
  return false;
@@ -409,7 +1061,7 @@ function journalHighWater(path, surface, events) {
409
1061
  function parseJournal(path) {
410
1062
  const events = [];
411
1063
  try {
412
- for (const line of (0, import_node_fs.readFileSync)(path, "utf8").split("\n")) {
1064
+ for (const line of (0, import_node_fs5.readFileSync)(path, "utf8").split("\n")) {
413
1065
  if (!line.trim()) continue;
414
1066
  try {
415
1067
  events.push(JSON.parse(line));
@@ -423,7 +1075,7 @@ function parseJournal(path) {
423
1075
  function journalSurfaceRepeatedFail(path, surface, currentRun) {
424
1076
  let lastVerdict = null;
425
1077
  try {
426
- for (const line of (0, import_node_fs.readFileSync)(path, "utf8").split("\n")) {
1078
+ for (const line of (0, import_node_fs5.readFileSync)(path, "utf8").split("\n")) {
427
1079
  if (!line.trim()) continue;
428
1080
  try {
429
1081
  const event = JSON.parse(line);
@@ -466,7 +1118,7 @@ function compareSemver(a, b) {
466
1118
  return va.pre.join(".") < vb.pre.join(".") ? -1 : va.pre.join(".") > vb.pre.join(".") ? 1 : 0;
467
1119
  }
468
1120
  function fileExists(path) {
469
- return (0, import_node_fs.existsSync)(path);
1121
+ return (0, import_node_fs5.existsSync)(path);
470
1122
  }
471
1123
 
472
1124
  // src/reconcile.ts
@@ -474,7 +1126,7 @@ var import_node_perf_hooks2 = require("node:perf_hooks");
474
1126
 
475
1127
  // src/npm.ts
476
1128
  var import_node_child_process = require("node:child_process");
477
- var import_node_fs2 = require("node:fs");
1129
+ var import_node_fs6 = require("node:fs");
478
1130
  var import_node_path2 = require("node:path");
479
1131
  var cachedNpm = null;
480
1132
  function resolveNpm(env = process.env) {
@@ -485,7 +1137,7 @@ function resolveNpm(env = process.env) {
485
1137
  const first = where.stdout.split("\n").map((l) => l.trim()).find(Boolean);
486
1138
  if (where.status === 0 && first) {
487
1139
  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)) {
1140
+ if ((0, import_node_fs6.existsSync)(script)) {
489
1141
  cachedNpm = { kind: "node-script", script };
490
1142
  return cachedNpm;
491
1143
  }
@@ -494,13 +1146,13 @@ function resolveNpm(env = process.env) {
494
1146
  }
495
1147
  }
496
1148
  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)) {
1149
+ if ((0, import_node_fs6.existsSync)(bundled)) {
498
1150
  cachedNpm = { kind: "node-script", script: bundled };
499
1151
  return cachedNpm;
500
1152
  }
501
1153
  const located = (0, import_node_child_process.spawnSync)("/bin/sh", ["-lc", "command -v npm"], { encoding: "utf8", windowsHide: true });
502
1154
  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)) {
1155
+ if (located.status === 0 && resolved && (0, import_node_fs6.existsSync)(resolved)) {
504
1156
  cachedNpm = { kind: "command", command: resolved };
505
1157
  return cachedNpm;
506
1158
  }
@@ -582,7 +1234,7 @@ function npmVersions(packageName, env = process.env) {
582
1234
 
583
1235
  // src/gate.ts
584
1236
  var import_node_child_process2 = require("node:child_process");
585
- var import_node_fs3 = require("node:fs");
1237
+ var import_node_fs7 = require("node:fs");
586
1238
  function parseBom(text) {
587
1239
  const bom = JSON.parse(text);
588
1240
  if (typeof bom?.version !== "string" || typeof bom?.sourceCommit !== "string" || !Array.isArray(bom?.artifacts)) {
@@ -604,7 +1256,7 @@ function ensureBareCache(cachePath, repoUrl) {
604
1256
  if (probe.status !== 0) {
605
1257
  throw new GateEnvError("git is not available \u2014 the release gate needs a git client");
606
1258
  }
607
- if (!(0, import_node_fs3.existsSync)(cachePath)) {
1259
+ if (!(0, import_node_fs7.existsSync)(cachePath)) {
608
1260
  const init = (0, import_node_child_process2.spawnSync)("git", ["init", "--bare", cachePath], { encoding: "utf8", windowsHide: true });
609
1261
  if (init.status !== 0) throw new GateEnvError(`cannot create bare cache at ${cachePath}: ${init.stderr}`);
610
1262
  }
@@ -711,24 +1363,24 @@ async function checkAtomicity(cachePath, tagCommit, bom, candidate, npm) {
711
1363
  }
712
1364
 
713
1365
  // src/surfaces.ts
714
- var import_node_fs8 = require("node:fs");
1366
+ var import_node_fs12 = require("node:fs");
715
1367
  var import_node_os3 = require("node:os");
716
1368
  var import_node_path7 = require("node:path");
717
1369
 
718
1370
  // src/arm-host-plugins.ts
719
1371
  var import_node_child_process5 = require("node:child_process");
720
- var import_node_fs7 = require("node:fs");
1372
+ var import_node_fs11 = require("node:fs");
721
1373
  var import_node_os2 = require("node:os");
722
1374
  var import_node_path6 = require("node:path");
723
1375
 
724
1376
  // src/arm-npm-global.ts
725
- var import_node_fs5 = require("node:fs");
1377
+ var import_node_fs9 = require("node:fs");
726
1378
  var import_node_child_process4 = require("node:child_process");
727
1379
  var import_node_path4 = require("node:path");
728
1380
 
729
1381
  // src/pending.ts
730
1382
  var import_node_child_process3 = require("node:child_process");
731
- var import_node_fs4 = require("node:fs");
1383
+ var import_node_fs8 = require("node:fs");
732
1384
  var import_node_path3 = require("node:path");
733
1385
  function hubDistPath(env) {
734
1386
  const root = globalRoot(env);
@@ -743,7 +1395,7 @@ function watcherLockPath(paths) {
743
1395
  }
744
1396
  function readPending(paths) {
745
1397
  try {
746
- const parsed = JSON.parse((0, import_node_fs4.readFileSync)(pendingPath(paths), "utf8"));
1398
+ const parsed = JSON.parse((0, import_node_fs8.readFileSync)(pendingPath(paths), "utf8"));
747
1399
  if (!Array.isArray(parsed)) return [];
748
1400
  return parsed.filter((row) => Boolean(row) && typeof row === "object" && typeof row.surface === "string" && typeof row.target === "string" && Array.isArray(row.images) && Array.isArray(row.dirs));
749
1401
  } catch {
@@ -751,12 +1403,12 @@ function readPending(paths) {
751
1403
  }
752
1404
  }
753
1405
  function writePending(paths, records) {
754
- (0, import_node_fs4.mkdirSync)(paths.root, { recursive: true });
1406
+ (0, import_node_fs8.mkdirSync)(paths.root, { recursive: true });
755
1407
  const path = pendingPath(paths);
756
1408
  const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
757
- (0, import_node_fs4.writeFileSync)(tmp, `${JSON.stringify(records, null, 2)}
1409
+ (0, import_node_fs8.writeFileSync)(tmp, `${JSON.stringify(records, null, 2)}
758
1410
  `, "utf8");
759
- (0, import_node_fs4.renameSync)(tmp, path);
1411
+ (0, import_node_fs8.renameSync)(tmp, path);
760
1412
  }
761
1413
  function clearPending(paths, surface) {
762
1414
  const records = readPending(paths);
@@ -825,7 +1477,7 @@ function schedulePendingWatcher(paths, env = process.env, seam = {}) {
825
1477
  const live = records.find((row) => row.watcherPid && pidAlive(row.watcherPid));
826
1478
  if (live) return { scheduled: false, reason: `watcher pid ${live.watcherPid} is already waiting` };
827
1479
  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" };
1480
+ if (!hubDist || !(0, import_node_fs8.existsSync)(hubDist)) return { scheduled: false, reason: "the installed hub bundle is unresolvable \u2014 the hourly tick remains the retry" };
829
1481
  const command = buildWatcherCommand({
830
1482
  images: [...new Set(records.flatMap((row) => row.images))],
831
1483
  dirs: [...new Set(records.flatMap((row) => row.dirs))],
@@ -885,7 +1537,7 @@ function tail(output) {
885
1537
  function manifestVersion(packageRoot) {
886
1538
  if (!packageRoot) return null;
887
1539
  try {
888
- const version = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(packageRoot, "package.json"), "utf8")).version;
1540
+ const version = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path4.join)(packageRoot, "package.json"), "utf8")).version;
889
1541
  return typeof version === "string" ? version : null;
890
1542
  } catch {
891
1543
  return null;
@@ -896,8 +1548,8 @@ function stagePackage(options) {
896
1548
  const stageRoot = (0, import_node_path4.join)(paths.staging, surface, target);
897
1549
  const packageRoot = (0, import_node_path4.join)(stageRoot, "node_modules", ...packageName.split("/"));
898
1550
  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");
1551
+ (0, import_node_fs9.mkdirSync)(stageRoot, { recursive: true });
1552
+ (0, import_node_fs9.writeFileSync)((0, import_node_path4.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
901
1553
  const install = runNpm(["install", "--prefix", stageRoot, "--no-audit", "--no-fund", "--no-package-lock", `${packageName}@${target}`], env);
902
1554
  if (install.status !== 0) {
903
1555
  return { defer: `staging install failed: ${tail(install.stderr || install.stdout)}` };
@@ -905,7 +1557,7 @@ function stagePackage(options) {
905
1557
  }
906
1558
  let stagedManifestVersion;
907
1559
  try {
908
- stagedManifestVersion = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(packageRoot, "package.json"), "utf8")).version;
1560
+ stagedManifestVersion = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path4.join)(packageRoot, "package.json"), "utf8")).version;
909
1561
  } catch {
910
1562
  }
911
1563
  if (stagedManifestVersion !== target) {
@@ -963,11 +1615,11 @@ function npmGlobalArm(options) {
963
1615
  }
964
1616
 
965
1617
  // src/launch-staging.ts
966
- var import_node_fs6 = require("node:fs");
1618
+ var import_node_fs10 = require("node:fs");
967
1619
  var import_node_path5 = require("node:path");
968
1620
  function readLaunchTarget(path) {
969
1621
  try {
970
- const parsed = JSON.parse((0, import_node_fs6.readFileSync)(path, "utf8"));
1622
+ const parsed = JSON.parse((0, import_node_fs10.readFileSync)(path, "utf8"));
971
1623
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
972
1624
  const row = parsed;
973
1625
  if (typeof row.surface !== "string" || typeof row.target !== "string" || typeof row.since !== "string" || typeof row.incoming !== "string") return null;
@@ -977,15 +1629,15 @@ function readLaunchTarget(path) {
977
1629
  }
978
1630
  }
979
1631
  function writeLaunchTarget(path, record) {
980
- (0, import_node_fs6.mkdirSync)((0, import_node_path5.join)(path, ".."), { recursive: true });
1632
+ (0, import_node_fs10.mkdirSync)((0, import_node_path5.join)(path, ".."), { recursive: true });
981
1633
  const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
982
- (0, import_node_fs6.writeFileSync)(tmp, `${JSON.stringify(record, null, 2)}
1634
+ (0, import_node_fs10.writeFileSync)(tmp, `${JSON.stringify(record, null, 2)}
983
1635
  `, "utf8");
984
- (0, import_node_fs6.renameSync)(tmp, path);
1636
+ (0, import_node_fs10.renameSync)(tmp, path);
985
1637
  }
986
1638
  function clearLaunchTarget(path) {
987
1639
  try {
988
- (0, import_node_fs6.rmSync)(path, { force: true });
1640
+ (0, import_node_fs10.rmSync)(path, { force: true });
989
1641
  } catch {
990
1642
  }
991
1643
  }
@@ -1006,7 +1658,7 @@ function noteLaunchStaged(paths, env, options, seam = {}) {
1006
1658
  }
1007
1659
  function probeLaunchTargetVersion(launchTargetPath, versionAt) {
1008
1660
  const record = readLaunchTarget(launchTargetPath);
1009
- if (!record || !(0, import_node_fs6.existsSync)(record.incoming)) return null;
1661
+ if (!record || !(0, import_node_fs10.existsSync)(record.incoming)) return null;
1010
1662
  const version = versionAt(record.incoming);
1011
1663
  return version === record.target ? record.target : null;
1012
1664
  }
@@ -1064,7 +1716,7 @@ function ensureClaudeMarketplace(env) {
1064
1716
  const path = claudeMarketplacesPath(env);
1065
1717
  let body;
1066
1718
  try {
1067
- body = JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8"));
1719
+ body = JSON.parse((0, import_node_fs11.readFileSync)(path, "utf8"));
1068
1720
  } catch (error) {
1069
1721
  if (error?.code === "ENOENT") return { ok: false, detail: `${path} does not exist \u2014 mutmutco marketplace is not registered`, unregistered: true };
1070
1722
  return { ok: false, detail: `cannot read ${path}` };
@@ -1092,7 +1744,7 @@ function claudePluginsRoot(env) {
1092
1744
  }
1093
1745
  function claudeMarkerVersion(root) {
1094
1746
  try {
1095
- const version = JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path6.join)(root, ...CLAUDE_MANIFEST.split("/")), "utf8")).version;
1747
+ const version = JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path6.join)(root, ...CLAUDE_MANIFEST.split("/")), "utf8")).version;
1096
1748
  return typeof version === "string" ? version : null;
1097
1749
  } catch {
1098
1750
  return null;
@@ -1189,14 +1841,14 @@ function claudeArm(options) {
1189
1841
  return result(from, target, "defer", `staged Claude manifest is ${claudeMarkerVersion(staged.packageRoot) ?? "unreadable"} or its tree is incomplete (expected ${target})`);
1190
1842
  }
1191
1843
  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 });
1844
+ (0, import_node_fs11.mkdirSync)((0, import_node_path6.join)(pluginsRoot, "staging"), { recursive: true });
1845
+ (0, import_node_fs11.rmSync)(incomingRoot, { recursive: true, force: true });
1846
+ (0, import_node_fs11.cpSync)(staged.packageRoot, incoming, { recursive: true });
1195
1847
  } catch (error) {
1196
1848
  return result(from, target, "defer", `cannot place incoming Claude generation: ${error?.message ?? error}`);
1197
1849
  }
1198
1850
  if (claudeMarkerVersion(incoming) !== target || !claudeTreeHealthy(incoming)) {
1199
- (0, import_node_fs7.rmSync)(incomingRoot, { recursive: true, force: true });
1851
+ (0, import_node_fs11.rmSync)(incomingRoot, { recursive: true, force: true });
1200
1852
  return result(from, target, "defer", "incoming Claude generation did not reproduce the verified payload");
1201
1853
  }
1202
1854
  }
@@ -1228,10 +1880,10 @@ function claudeArm(options) {
1228
1880
  }
1229
1881
  const cacheTarget = (0, import_node_path6.join)(pluginsRoot, "cache", "mutmutco", "mmi", target);
1230
1882
  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 });
1883
+ if (!(0, import_node_fs11.existsSync)(cacheTarget) || claudeMarkerVersion(cacheTarget) !== target || !claudeTreeHealthy(cacheTarget)) {
1884
+ (0, import_node_fs11.rmSync)(cacheTarget, { recursive: true, force: true });
1885
+ (0, import_node_fs11.mkdirSync)((0, import_node_path6.join)(cacheTarget, ".."), { recursive: true });
1886
+ (0, import_node_fs11.cpSync)(incoming, cacheTarget, { recursive: true });
1235
1887
  }
1236
1888
  } catch (error) {
1237
1889
  return result(from, target, "defer", `cannot populate Claude plugin cache: ${error?.message ?? error}`);
@@ -1259,7 +1911,7 @@ function claudeArm(options) {
1259
1911
  }
1260
1912
  clearLaunchTarget(launchTargetPath);
1261
1913
  try {
1262
- (0, import_node_fs7.rmSync)(incomingRoot, { recursive: true, force: true });
1914
+ (0, import_node_fs11.rmSync)(incomingRoot, { recursive: true, force: true });
1263
1915
  } catch {
1264
1916
  }
1265
1917
  return result(from, target, "ok", `catalog refreshed; converged and verified ${from ?? "absent"} -> ${target}`);
@@ -1372,21 +2024,21 @@ function enumerateSurfaces(env = process.env) {
1372
2024
  { id: "hermes", marker: `${hermesHostCommand(env)} --version` }
1373
2025
  ].map((probe) => ({
1374
2026
  ...probe,
1375
- present: probe.id === "hermes" ? hermesHostPresent(env) : (0, import_node_fs8.existsSync)(probe.marker)
2027
+ present: probe.id === "hermes" ? hermesHostPresent(env) : (0, import_node_fs12.existsSync)(probe.marker)
1376
2028
  }));
1377
2029
  }
1378
2030
 
1379
2031
  // src/arm-cursor.ts
1380
- var import_node_fs11 = require("node:fs");
2032
+ var import_node_fs15 = require("node:fs");
1381
2033
  var import_node_child_process6 = require("node:child_process");
1382
2034
  var import_node_os5 = require("node:os");
1383
2035
  var import_node_path8 = require("node:path");
1384
2036
 
1385
2037
  // src/compat.ts
1386
- var import_node_fs9 = require("node:fs");
2038
+ var import_node_fs13 = require("node:fs");
1387
2039
  function readManifestCompat(manifestPath) {
1388
2040
  try {
1389
- const parsed = JSON.parse((0, import_node_fs9.readFileSync)(manifestPath, "utf8"));
2041
+ const parsed = JSON.parse((0, import_node_fs13.readFileSync)(manifestPath, "utf8"));
1390
2042
  return typeof parsed.mmiCompat === "string" ? parsed.mmiCompat : void 0;
1391
2043
  } catch {
1392
2044
  return void 0;
@@ -1394,7 +2046,7 @@ function readManifestCompat(manifestPath) {
1394
2046
  }
1395
2047
 
1396
2048
  // src/host-doc-lock.ts
1397
- var import_node_fs10 = require("node:fs");
2049
+ var import_node_fs14 = require("node:fs");
1398
2050
  var import_node_os4 = require("node:os");
1399
2051
  var SHARED_DOC_LOCK_STALE_MS = 3e4;
1400
2052
  var SHARED_DOC_LOCK_ATTEMPTS = 50;
@@ -1408,7 +2060,7 @@ function lockContent(options) {
1408
2060
  }
1409
2061
  function readHolder(lockPath) {
1410
2062
  try {
1411
- const parsed = JSON.parse((0, import_node_fs10.readFileSync)(lockPath, "utf8"));
2063
+ const parsed = JSON.parse((0, import_node_fs14.readFileSync)(lockPath, "utf8"));
1412
2064
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
1413
2065
  return parsed;
1414
2066
  } catch {
@@ -1420,7 +2072,7 @@ function lockAgeMs(lockPath, now) {
1420
2072
  const declared = typeof holder?.acquiredAt === "string" ? Date.parse(holder.acquiredAt) : Number.NaN;
1421
2073
  if (!Number.isNaN(declared)) return Math.max(0, now - declared);
1422
2074
  try {
1423
- return Math.max(0, now - (0, import_node_fs10.statSync)(lockPath).mtimeMs);
2075
+ return Math.max(0, now - (0, import_node_fs14.statSync)(lockPath).mtimeMs);
1424
2076
  } catch {
1425
2077
  return 0;
1426
2078
  }
@@ -1443,9 +2095,9 @@ function acquireSharedDocLock(documentPath, options) {
1443
2095
  for (let attempt = 0; attempt < attempts; attempt += 1) {
1444
2096
  let fd;
1445
2097
  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);
2098
+ fd = (0, import_node_fs14.openSync)(lockPath, "wx");
2099
+ (0, import_node_fs14.writeSync)(fd, content);
2100
+ (0, import_node_fs14.closeSync)(fd);
1449
2101
  return {
1450
2102
  documentPath,
1451
2103
  lockPath,
@@ -1458,11 +2110,11 @@ function acquireSharedDocLock(documentPath, options) {
1458
2110
  } catch (error) {
1459
2111
  if (fd !== void 0) {
1460
2112
  try {
1461
- (0, import_node_fs10.closeSync)(fd);
2113
+ (0, import_node_fs14.closeSync)(fd);
1462
2114
  } catch {
1463
2115
  }
1464
2116
  try {
1465
- (0, import_node_fs10.unlinkSync)(lockPath);
2117
+ (0, import_node_fs14.unlinkSync)(lockPath);
1466
2118
  } catch {
1467
2119
  }
1468
2120
  }
@@ -1471,12 +2123,12 @@ function acquireSharedDocLock(documentPath, options) {
1471
2123
  if (lockAgeMs(lockPath, Date.now()) >= staleAfterMs) {
1472
2124
  const aside = `${lockPath}.stale-${Date.now()}`;
1473
2125
  try {
1474
- (0, import_node_fs10.renameSync)(lockPath, aside);
2126
+ (0, import_node_fs14.renameSync)(lockPath, aside);
1475
2127
  } catch {
1476
2128
  continue;
1477
2129
  }
1478
2130
  try {
1479
- (0, import_node_fs10.rmSync)(aside, { force: true });
2131
+ (0, import_node_fs14.rmSync)(aside, { force: true });
1480
2132
  } catch {
1481
2133
  }
1482
2134
  continue;
@@ -1489,7 +2141,7 @@ function releaseSharedDocLock(handle) {
1489
2141
  try {
1490
2142
  const holder = readHolder(handle.lockPath);
1491
2143
  if (holder && holder.owner === handle.owner && Number(holder.pid) === handle.pid) {
1492
- (0, import_node_fs10.unlinkSync)(handle.lockPath);
2144
+ (0, import_node_fs14.unlinkSync)(handle.lockPath);
1493
2145
  }
1494
2146
  } catch {
1495
2147
  }
@@ -1514,11 +2166,11 @@ function cursorPluginsRoot() {
1514
2166
  return (0, import_node_path8.join)((0, import_node_os5.homedir)(), ".cursor", "plugins");
1515
2167
  }
1516
2168
  function treeHealthy(root) {
1517
- return TREE_FILES.every((rel) => (0, import_node_fs11.existsSync)((0, import_node_path8.join)(root, ...rel.split("/"))));
2169
+ return TREE_FILES.every((rel) => (0, import_node_fs15.existsSync)((0, import_node_path8.join)(root, ...rel.split("/"))));
1518
2170
  }
1519
2171
  function readJson(root, rel) {
1520
2172
  try {
1521
- return JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path8.join)(root, ...rel.split("/")), "utf8"));
2173
+ return JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path8.join)(root, ...rel.split("/")), "utf8"));
1522
2174
  } catch {
1523
2175
  return null;
1524
2176
  }
@@ -1542,7 +2194,7 @@ function message(error) {
1542
2194
  }
1543
2195
  function discard(path) {
1544
2196
  try {
1545
- (0, import_node_fs11.rmSync)(path, { recursive: true, force: true });
2197
+ (0, import_node_fs15.rmSync)(path, { recursive: true, force: true });
1546
2198
  } catch {
1547
2199
  }
1548
2200
  }
@@ -1596,10 +2248,10 @@ var CURSOR_HOOKS_LOCK_OWNER = "mmi-hub:cursor-hooks";
1596
2248
  function reconcileCursorUserHooks(pluginRoot, homeDir, lock = {}) {
1597
2249
  const path = (0, import_node_path8.join)(homeDir, ".cursor", USER_HOOKS_FILE);
1598
2250
  return withSharedDocLock(path, { owner: CURSOR_HOOKS_LOCK_OWNER, ...lock }, () => {
1599
- if (!(0, import_node_fs11.existsSync)(path)) return "current";
2251
+ if (!(0, import_node_fs15.existsSync)(path)) return "current";
1600
2252
  let existing;
1601
2253
  try {
1602
- existing = JSON.parse((0, import_node_fs11.readFileSync)(path, "utf8"));
2254
+ existing = JSON.parse((0, import_node_fs15.readFileSync)(path, "utf8"));
1603
2255
  } catch {
1604
2256
  return "user-conflict";
1605
2257
  }
@@ -1607,8 +2259,8 @@ function reconcileCursorUserHooks(pluginRoot, homeDir, lock = {}) {
1607
2259
  if (!stripped) return "user-conflict";
1608
2260
  if (JSON.stringify(stripped) === JSON.stringify(existing)) return "current";
1609
2261
  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);
2262
+ (0, import_node_fs15.writeFileSync)(tmp, JSON.stringify(stripped, null, 2) + "\n", "utf8");
2263
+ (0, import_node_fs15.renameSync)(tmp, path);
1612
2264
  return "updated";
1613
2265
  });
1614
2266
  }
@@ -1632,15 +2284,15 @@ function writeRollbackPointer(root, entry) {
1632
2284
  const path = (0, import_node_path8.join)(root, "quarantine", "rollback.json");
1633
2285
  const tmp = `${path}.tmp-${process.pid}`;
1634
2286
  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);
2287
+ (0, import_node_fs15.writeFileSync)(tmp, JSON.stringify({ surface: "cursor", ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry }, null, 2) + "\n", "utf8");
2288
+ (0, import_node_fs15.renameSync)(tmp, path);
1637
2289
  } catch {
1638
2290
  discard(tmp);
1639
2291
  }
1640
2292
  }
1641
2293
  function mtimeMs(path) {
1642
2294
  try {
1643
- return (0, import_node_fs11.statSync)(path).mtimeMs;
2295
+ return (0, import_node_fs15.statSync)(path).mtimeMs;
1644
2296
  } catch {
1645
2297
  return 0;
1646
2298
  }
@@ -1649,13 +2301,13 @@ function pruneQuarantine(root, keep) {
1649
2301
  const dir = (0, import_node_path8.join)(root, "quarantine");
1650
2302
  let generations;
1651
2303
  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));
2304
+ generations = (0, import_node_fs15.readdirSync)(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("mmi-")).map((entry) => (0, import_node_path8.join)(dir, entry.name)).sort((a, b) => mtimeMs(b) - mtimeMs(a));
1653
2305
  } catch {
1654
2306
  return 0;
1655
2307
  }
1656
2308
  let stranded = 0;
1657
2309
  try {
1658
- for (const entry of (0, import_node_fs11.readdirSync)(dir, { withFileTypes: true })) {
2310
+ for (const entry of (0, import_node_fs15.readdirSync)(dir, { withFileTypes: true })) {
1659
2311
  if (!entry.isFile() || !/^rollback\.json\.tmp-\d+$/.test(entry.name)) continue;
1660
2312
  const path = (0, import_node_path8.join)(dir, entry.name);
1661
2313
  if (Date.now() - mtimeMs(path) < STRANDED_TMP_AGE_MS) continue;
@@ -1675,16 +2327,16 @@ function pruneQuarantine(root, keep) {
1675
2327
  }
1676
2328
  function restorePrevious(live, quarantined, broken) {
1677
2329
  try {
1678
- (0, import_node_fs11.renameSync)(live, broken);
2330
+ (0, import_node_fs15.renameSync)(live, broken);
1679
2331
  } catch {
1680
2332
  return "kept-broken";
1681
2333
  }
1682
2334
  try {
1683
- (0, import_node_fs11.renameSync)(quarantined, live);
2335
+ (0, import_node_fs15.renameSync)(quarantined, live);
1684
2336
  return "restored";
1685
2337
  } catch {
1686
2338
  try {
1687
- (0, import_node_fs11.renameSync)(broken, live);
2339
+ (0, import_node_fs15.renameSync)(broken, live);
1688
2340
  return "kept-broken";
1689
2341
  } catch {
1690
2342
  return "lost";
@@ -1719,7 +2371,7 @@ function cursorArm(options) {
1719
2371
  if (installed === target && treeHealthy(live)) {
1720
2372
  return result(installed, target, "ok", `already at target ${target}${userHooksDetail(live, (0, import_node_os5.homedir)())}`);
1721
2373
  }
1722
- if ((0, import_node_fs11.existsSync)(live) && !mmiOwned(live)) {
2374
+ if ((0, import_node_fs15.existsSync)(live) && !mmiOwned(live)) {
1723
2375
  return result(installed, target, "defer", `${live} carries no MMI ownership marker (plugin manifest name, or a ${PACKAGE} package.json) \u2014 refusing to displace an unmanaged directory (mmi-cli plugin heal adopts it)`);
1724
2376
  }
1725
2377
  if (dryRun) {
@@ -1737,12 +2389,12 @@ function cursorArm(options) {
1737
2389
  const priorLaunch = readLaunchTarget(launchTargetPath);
1738
2390
  let incoming = priorLaunch?.target === target && priorLaunch.incoming.startsWith((0, import_node_path8.join)(root, "staging")) && manifest(priorLaunch.incoming)?.version === target && treeHealthy(priorLaunch.incoming) ? priorLaunch.incoming : (0, import_node_path8.join)(root, "staging", `mmi-${safeSegment(target)}-${Date.now()}`);
1739
2391
  const reuseIncoming = incoming === priorLaunch?.incoming;
1740
- if (!reuseIncoming || !(0, import_node_fs11.existsSync)(incoming)) {
2392
+ if (!reuseIncoming || !(0, import_node_fs15.existsSync)(incoming)) {
1741
2393
  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 });
2394
+ for (const dir of ["local", "staging", "quarantine"]) (0, import_node_fs15.mkdirSync)((0, import_node_path8.join)(root, dir), { recursive: true });
1743
2395
  try {
1744
2396
  discard(incoming);
1745
- (0, import_node_fs11.cpSync)(staged.packageRoot, incoming, { recursive: true });
2397
+ (0, import_node_fs15.cpSync)(staged.packageRoot, incoming, { recursive: true });
1746
2398
  } catch (error) {
1747
2399
  discard(incoming);
1748
2400
  return result(installed, target, "defer", `copy into ${root} failed: ${message(error)}`);
@@ -1766,7 +2418,7 @@ function cursorArm(options) {
1766
2418
  }
1767
2419
  const stamp = Date.now();
1768
2420
  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 });
2421
+ for (const dir of ["local", "staging", "quarantine"]) (0, import_node_fs15.mkdirSync)((0, import_node_path8.join)(root, dir), { recursive: true });
1770
2422
  const started = cursorHostEvidence(env);
1771
2423
  if (started) {
1772
2424
  const pending = noteLaunchStaged(paths, env, {
@@ -1781,15 +2433,15 @@ function cursorArm(options) {
1781
2433
  }
1782
2434
  let displaced = false;
1783
2435
  try {
1784
- if ((0, import_node_fs11.existsSync)(live)) {
1785
- (0, import_node_fs11.renameSync)(live, quarantined);
2436
+ if ((0, import_node_fs15.existsSync)(live)) {
2437
+ (0, import_node_fs15.renameSync)(live, quarantined);
1786
2438
  displaced = true;
1787
2439
  }
1788
- (0, import_node_fs11.renameSync)(incoming, live);
2440
+ (0, import_node_fs15.renameSync)(incoming, live);
1789
2441
  } catch (error) {
1790
- if (displaced && !(0, import_node_fs11.existsSync)(live)) {
2442
+ if (displaced && !(0, import_node_fs15.existsSync)(live)) {
1791
2443
  try {
1792
- (0, import_node_fs11.renameSync)(quarantined, live);
2444
+ (0, import_node_fs15.renameSync)(quarantined, live);
1793
2445
  } catch {
1794
2446
  }
1795
2447
  }
@@ -1817,7 +2469,7 @@ function probeCursorLaunchStaging() {
1817
2469
  }
1818
2470
 
1819
2471
  // src/arm-jervcode.ts
1820
- var import_node_fs12 = require("node:fs");
2472
+ var import_node_fs16 = require("node:fs");
1821
2473
  var import_node_path9 = require("node:path");
1822
2474
  var PI_PACKAGE = "@mutmutco/pi-plugin";
1823
2475
  function hostEnv(env) {
@@ -1829,7 +2481,7 @@ function hostCommands(_env) {
1829
2481
  }
1830
2482
  function readPackageSpecs(env) {
1831
2483
  try {
1832
- const settings = JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path9.join)(jervcodeAgentDir(env), "settings.json"), "utf8"));
2484
+ const settings = JSON.parse((0, import_node_fs16.readFileSync)((0, import_node_path9.join)(jervcodeAgentDir(env), "settings.json"), "utf8"));
1833
2485
  return Array.isArray(settings.packages) ? settings.packages.filter((entry) => typeof entry === "string") : [];
1834
2486
  } catch {
1835
2487
  return null;
@@ -1837,7 +2489,7 @@ function readPackageSpecs(env) {
1837
2489
  }
1838
2490
  function installedVersion(env) {
1839
2491
  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"));
2492
+ const manifest2 = JSON.parse((0, import_node_fs16.readFileSync)((0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json"), "utf8"));
1841
2493
  return typeof manifest2.version === "string" ? manifest2.version : null;
1842
2494
  } catch {
1843
2495
  return null;
@@ -1957,7 +2609,7 @@ function jervcodeArm(options) {
1957
2609
  }
1958
2610
 
1959
2611
  // src/arm-hermes.ts
1960
- var import_node_fs13 = require("node:fs");
2612
+ var import_node_fs17 = require("node:fs");
1961
2613
  var import_node_child_process7 = require("node:child_process");
1962
2614
  var import_node_path10 = require("node:path");
1963
2615
  var PACKAGE2 = "@mutmutco/hermes-plugin";
@@ -1965,7 +2617,7 @@ var MANIFEST2 = "plugin.yaml";
1965
2617
  var TREE_MARKERS_FALLBACK = [MANIFEST2, "__init__.py", "skills"];
1966
2618
  function yamlVersion(root) {
1967
2619
  try {
1968
- const text = (0, import_node_fs13.readFileSync)((0, import_node_path10.join)(root, MANIFEST2), "utf8");
2620
+ const text = (0, import_node_fs17.readFileSync)((0, import_node_path10.join)(root, MANIFEST2), "utf8");
1969
2621
  try {
1970
2622
  const version = JSON.parse(text).version;
1971
2623
  if (typeof version === "string" && version.trim()) return version.trim();
@@ -1979,7 +2631,7 @@ function yamlVersion(root) {
1979
2631
  }
1980
2632
  function packageTreeMarkers(root) {
1981
2633
  try {
1982
- const files = JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path10.join)(root, "package.json"), "utf8")).files;
2634
+ const files = JSON.parse((0, import_node_fs17.readFileSync)((0, import_node_path10.join)(root, "package.json"), "utf8")).files;
1983
2635
  if (Array.isArray(files) && files.length && files.every((entry) => typeof entry === "string" && entry.trim())) {
1984
2636
  return files.map((entry) => entry.trim());
1985
2637
  }
@@ -1989,9 +2641,9 @@ function packageTreeMarkers(root) {
1989
2641
  }
1990
2642
  function markerPresent(root, rel) {
1991
2643
  const path = (0, import_node_path10.join)(root, ...rel.split("/"));
1992
- if (!(0, import_node_fs13.existsSync)(path)) return false;
2644
+ if (!(0, import_node_fs17.existsSync)(path)) return false;
1993
2645
  try {
1994
- const stat = (0, import_node_fs13.statSync)(path);
2646
+ const stat = (0, import_node_fs17.statSync)(path);
1995
2647
  return stat.isFile() || stat.isDirectory();
1996
2648
  } catch {
1997
2649
  return false;
@@ -2010,7 +2662,7 @@ function treeIncompleteDetail(root, label) {
2010
2662
  }
2011
2663
  function packageIdentity(root) {
2012
2664
  try {
2013
- return JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path10.join)(root, "package.json"), "utf8")).name === PACKAGE2;
2665
+ return JSON.parse((0, import_node_fs17.readFileSync)((0, import_node_path10.join)(root, "package.json"), "utf8")).name === PACKAGE2;
2014
2666
  } catch {
2015
2667
  return false;
2016
2668
  }
@@ -2025,7 +2677,7 @@ function mmiOwned2(root) {
2025
2677
  }
2026
2678
  function discard2(path) {
2027
2679
  try {
2028
- (0, import_node_fs13.rmSync)(path, { recursive: true, force: true });
2680
+ (0, import_node_fs17.rmSync)(path, { recursive: true, force: true });
2029
2681
  } catch {
2030
2682
  }
2031
2683
  }
@@ -2061,15 +2713,15 @@ function materializeHermesSkills(home) {
2061
2713
  const skillsRoot = (0, import_node_path10.join)(home, "skills");
2062
2714
  const targetRoot = (0, import_node_path10.join)(skillsRoot, "mmi");
2063
2715
  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);
2716
+ if (!(0, import_node_fs17.existsSync)(pluginSkills)) return { ok: false, detail: `plugin skills tree missing at ${pluginSkills}` };
2717
+ const names = (0, import_node_fs17.readdirSync)(pluginSkills, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_")).map((entry) => entry.name);
2066
2718
  if (!names.length) return { ok: false, detail: `plugin skills tree is empty at ${pluginSkills}` };
2067
- (0, import_node_fs13.mkdirSync)(skillsRoot, { recursive: true });
2719
+ (0, import_node_fs17.mkdirSync)(skillsRoot, { recursive: true });
2068
2720
  const incoming = (0, import_node_path10.join)(skillsRoot, ".mmi-incoming");
2069
2721
  discard2(incoming);
2070
- (0, import_node_fs13.cpSync)(pluginSkills, incoming, { recursive: true });
2722
+ (0, import_node_fs17.cpSync)(pluginSkills, incoming, { recursive: true });
2071
2723
  discard2(targetRoot);
2072
- (0, import_node_fs13.renameSync)(incoming, targetRoot);
2724
+ (0, import_node_fs17.renameSync)(incoming, targetRoot);
2073
2725
  return { ok: true, detail: `provisioned ${names.length} skills to ${targetRoot}` };
2074
2726
  } catch (error) {
2075
2727
  return { ok: false, detail: message2(error) };
@@ -2091,7 +2743,7 @@ function hermesHomes(env = process.env) {
2091
2743
  const root = hermesHome(env);
2092
2744
  const homes = [root];
2093
2745
  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))) {
2746
+ for (const entry of (0, import_node_fs17.readdirSync)((0, import_node_path10.join)(root, "profiles"), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
2095
2747
  if (entry.isDirectory() && !entry.name.startsWith(".")) homes.push((0, import_node_path10.join)(root, "profiles", entry.name));
2096
2748
  }
2097
2749
  } catch {
@@ -2140,7 +2792,7 @@ function armHermesHome(home, env, options, primary) {
2140
2792
  detail,
2141
2793
  launchStaged: launchStaged || void 0
2142
2794
  });
2143
- if ((0, import_node_fs13.existsSync)(live) && !mmiOwned2(live)) {
2795
+ if ((0, import_node_fs17.existsSync)(live) && !mmiOwned2(live)) {
2144
2796
  return result(installed, options.target, "defer", `${live} carries no ${PACKAGE2} package identity \u2014 refusing to displace an unmanaged Hermes mmi plugin`);
2145
2797
  }
2146
2798
  if (installed && compareSemver(installed, options.target) > 0) return result(installed, installed, "skip", `installed ${installed} is above candidate ${options.target} (monotonic)`);
@@ -2172,10 +2824,10 @@ function armHermesHome(home, env, options, primary) {
2172
2824
  const reuseIncoming = priorLaunch?.target === options.target && priorLaunch.incoming === incoming && yamlVersion(incoming) === options.target && treeHealthy2(incoming);
2173
2825
  if (!reuseIncoming) {
2174
2826
  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 });
2827
+ (0, import_node_fs17.mkdirSync)(parent, { recursive: true });
2828
+ (0, import_node_fs17.mkdirSync)((0, import_node_path10.join)(scratch, "quarantine"), { recursive: true });
2177
2829
  discard2(incomingRoot);
2178
- (0, import_node_fs13.cpSync)(staged.packageRoot, incoming, { recursive: true });
2830
+ (0, import_node_fs17.cpSync)(staged.packageRoot, incoming, { recursive: true });
2179
2831
  } catch (error) {
2180
2832
  return result(installed, options.target, "defer", `cannot place incoming Hermes generation: ${message2(error)}`);
2181
2833
  }
@@ -2190,7 +2842,7 @@ function armHermesHome(home, env, options, primary) {
2190
2842
  return result(installed, options.target, "defer", incomingTree);
2191
2843
  }
2192
2844
  }
2193
- const displaced = (0, import_node_fs13.existsSync)(live);
2845
+ const displaced = (0, import_node_fs17.existsSync)(live);
2194
2846
  const busy = displaced ? hermesHostEvidence(env, home, options.processListProvider) : null;
2195
2847
  if (busy) {
2196
2848
  const pending = noteLaunchStaged(options.paths, env, {
@@ -2205,11 +2857,11 @@ function armHermesHome(home, env, options, primary) {
2205
2857
  return result(installed, options.target, "ok", `staged ${options.target} for next launch while Hermes is running (${busy}) \u2014 ${pending}`, true);
2206
2858
  }
2207
2859
  try {
2208
- if (displaced) (0, import_node_fs13.renameSync)(live, quarantine);
2209
- (0, import_node_fs13.renameSync)(incoming, live);
2860
+ if (displaced) (0, import_node_fs17.renameSync)(live, quarantine);
2861
+ (0, import_node_fs17.renameSync)(incoming, live);
2210
2862
  } catch (error) {
2211
- if (displaced && !(0, import_node_fs13.existsSync)(live)) try {
2212
- (0, import_node_fs13.renameSync)(quarantine, live);
2863
+ if (displaced && !(0, import_node_fs17.existsSync)(live)) try {
2864
+ (0, import_node_fs17.renameSync)(quarantine, live);
2213
2865
  } catch {
2214
2866
  }
2215
2867
  discard2(incomingRoot);
@@ -2223,9 +2875,9 @@ function armHermesHome(home, env, options, primary) {
2223
2875
  const broken = (0, import_node_path10.join)(scratch, "rejected", `${options.target}-${Date.now()}`);
2224
2876
  const verifyDetail = liveVersion !== options.target ? `manifest is ${liveVersion ?? "unreadable"}, expected ${options.target}` : liveTree;
2225
2877
  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);
2878
+ (0, import_node_fs17.mkdirSync)((0, import_node_path10.join)(scratch, "rejected"), { recursive: true });
2879
+ (0, import_node_fs17.renameSync)(live, broken);
2880
+ if (displaced) (0, import_node_fs17.renameSync)(quarantine, live);
2229
2881
  } catch (error) {
2230
2882
  return result(installed, liveVersion ?? "unknown", "fail", `post-switch Hermes verification failed and recovery failed: ${message2(error)}`);
2231
2883
  }
@@ -2244,7 +2896,7 @@ function probeHermesLaunchStaging(env = process.env) {
2244
2896
  }
2245
2897
 
2246
2898
  // src/reap.ts
2247
- var import_node_fs14 = require("node:fs");
2899
+ var import_node_fs18 = require("node:fs");
2248
2900
  var import_node_path11 = require("node:path");
2249
2901
  var import_node_perf_hooks = require("node:perf_hooks");
2250
2902
  function elapsedMs(startedAt) {
@@ -2254,7 +2906,7 @@ var KEEP_GENERATIONS2 = 2;
2254
2906
  var SCRATCH_AGE_MS = 24 * 60 * 6e4;
2255
2907
  function listDirs(path) {
2256
2908
  try {
2257
- return (0, import_node_fs14.readdirSync)(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
2909
+ return (0, import_node_fs18.readdirSync)(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
2258
2910
  } catch {
2259
2911
  return [];
2260
2912
  }
@@ -2275,7 +2927,7 @@ function pruneGenerations(root, category, dryRun) {
2275
2927
  continue;
2276
2928
  }
2277
2929
  try {
2278
- (0, import_node_fs14.rmSync)((0, import_node_path11.join)(set, version), { recursive: true, force: true });
2930
+ (0, import_node_fs18.rmSync)((0, import_node_path11.join)(set, version), { recursive: true, force: true });
2279
2931
  reaped += 1;
2280
2932
  } catch (error) {
2281
2933
  failures.push(`${version}: ${error?.code ?? error?.message ?? error}`);
@@ -2308,7 +2960,7 @@ function reapScratch(context, dryRun) {
2308
2960
  for (const [dir, patterns] of scanned) {
2309
2961
  let entries;
2310
2962
  try {
2311
- entries = (0, import_node_fs14.readdirSync)(dir, { withFileTypes: true });
2963
+ entries = (0, import_node_fs18.readdirSync)(dir, { withFileTypes: true });
2312
2964
  } catch {
2313
2965
  unreadable.push(dir);
2314
2966
  continue;
@@ -2319,7 +2971,7 @@ function reapScratch(context, dryRun) {
2319
2971
  const path = (0, import_node_path11.join)(dir, entry.name);
2320
2972
  let ageMs;
2321
2973
  try {
2322
- ageMs = now - (0, import_node_fs14.statSync)(path).mtimeMs;
2974
+ ageMs = now - (0, import_node_fs18.statSync)(path).mtimeMs;
2323
2975
  } catch {
2324
2976
  continue;
2325
2977
  }
@@ -2329,7 +2981,7 @@ function reapScratch(context, dryRun) {
2329
2981
  continue;
2330
2982
  }
2331
2983
  try {
2332
- (0, import_node_fs14.rmSync)(path, { force: true });
2984
+ (0, import_node_fs18.rmSync)(path, { force: true });
2333
2985
  reaped += 1;
2334
2986
  } catch (error) {
2335
2987
  failures.push(`${entry.name}: ${error?.code ?? error?.message ?? error}`);
@@ -2386,7 +3038,7 @@ async function reconcile(options) {
2386
3038
  const recordArm = (arm, durationMs) => {
2387
3039
  if (arm.verdict === "fail") arm.repeatFailure = journalSurfaceRepeatedFail(paths.journalPath, arm.surface, run);
2388
3040
  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);
3041
+ options.onArm?.({ ...arm, ...durationMs === void 0 ? {} : { durationMs } });
2390
3042
  if (arm.verdict === "ok" && !options.dryRun && !arm.launchStaged) clearPending(paths, arm.surface);
2391
3043
  if (arm.verdict === "defer") summary.exit = Math.max(summary.exit, 2);
2392
3044
  if (arm.verdict === "fail") summary.exit = Math.max(summary.exit, 3);
@@ -2551,7 +3203,7 @@ async function reconcile(options) {
2551
3203
 
2552
3204
  // src/scheduler.ts
2553
3205
  var import_node_child_process8 = require("node:child_process");
2554
- var import_node_fs15 = require("node:fs");
3206
+ var import_node_fs19 = require("node:fs");
2555
3207
  var import_node_os6 = require("node:os");
2556
3208
  var import_node_path12 = require("node:path");
2557
3209
  var TASK_NAME = "MMI Fleet Updater";
@@ -2711,7 +3363,7 @@ WantedBy=timers.target
2711
3363
  }
2712
3364
  function readFileOrNull(path) {
2713
3365
  try {
2714
- return (0, import_node_fs15.readFileSync)(path, "utf8");
3366
+ return (0, import_node_fs19.readFileSync)(path, "utf8");
2715
3367
  } catch {
2716
3368
  return null;
2717
3369
  }
@@ -2763,22 +3415,22 @@ function windowsSchedulerStatus(env) {
2763
3415
  const launcherPath = (0, import_node_path12.join)(statePaths(env).root, LAUNCHER_FILE);
2764
3416
  const info = taskRunInfo();
2765
3417
  const runInfo = info ?? EMPTY_RUN_INFO;
2766
- const fail = (detail) => ({ ok: false, supported: true, enabled: true, ...runInfo, detail });
3418
+ const fail2 = (detail) => ({ ok: false, supported: true, enabled: true, ...runInfo, detail });
2767
3419
  if (!query.stdout.includes(launcherPath)) {
2768
- return fail(`"${TASK_NAME}" action does not run the hidden launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
3420
+ return fail2(`"${TASK_NAME}" action does not run the hidden launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
2769
3421
  }
2770
3422
  const launcher = readFileOrNull(launcherPath);
2771
3423
  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\``);
3424
+ return fail2(`"${TASK_NAME}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
2773
3425
  }
2774
3426
  if (query.stdout.includes("<LogonTrigger>")) {
2775
- return fail(`"${TASK_NAME}" still carries the dormant logon-only trigger \u2014 run \`mmi-hub autoupdate on\``);
3427
+ return fail2(`"${TASK_NAME}" still carries the dormant logon-only trigger \u2014 run \`mmi-hub autoupdate on\``);
2776
3428
  }
2777
3429
  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\``);
3430
+ return fail2(`"${TASK_NAME}" has no ${REPEAT_INTERVAL}-repeating TimeTrigger \u2014 run \`mmi-hub autoupdate on\``);
2779
3431
  }
2780
3432
  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\``);
3433
+ return fail2(`"${TASK_NAME}" has never run and has no next run time \u2014 run \`mmi-hub autoupdate on\``);
2782
3434
  }
2783
3435
  const points = `points at ${hubDist ?? "the registered Hub dist"}`;
2784
3436
  const ran = !info ? "run history unreadable" : neverRan(info) ? "never run yet" : `last run ${info.lastRunTime} \u2192 exit ${info.lastTaskResult ?? "unknown"}`;
@@ -2792,20 +3444,20 @@ function darwinSchedulerStatus(env) {
2792
3444
  }
2793
3445
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2794
3446
  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 });
3447
+ const fail2 = (detail) => ({ ok: false, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail });
2796
3448
  if (!plist.includes(launcherPath)) {
2797
- return fail(`"${LAUNCHD_LABEL}" does not run the reconcile launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
3449
+ return fail2(`"${LAUNCHD_LABEL}" does not run the reconcile launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
2798
3450
  }
2799
3451
  const launcher = readFileOrNull(launcherPath);
2800
3452
  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\``);
3453
+ return fail2(`"${LAUNCHD_LABEL}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
2802
3454
  }
2803
3455
  if (!plist.includes(`<integer>${STARTINTERVAL_SECONDS}</integer>`)) {
2804
- return fail(`"${LAUNCHD_LABEL}" has no ${STARTINTERVAL_SECONDS}s StartInterval \u2014 run \`mmi-hub autoupdate on\``);
3456
+ return fail2(`"${LAUNCHD_LABEL}" has no ${STARTINTERVAL_SECONDS}s StartInterval \u2014 run \`mmi-hub autoupdate on\``);
2805
3457
  }
2806
3458
  const loaded = launchctl(["print", `gui/${uid()}/${LAUNCHD_LABEL}`]);
2807
3459
  if (loaded.status !== 0) {
2808
- return fail(`"${LAUNCHD_LABEL}" is registered but not loaded \u2014 run \`mmi-hub autoupdate on\``);
3460
+ return fail2(`"${LAUNCHD_LABEL}" is registered but not loaded \u2014 run \`mmi-hub autoupdate on\``);
2809
3461
  }
2810
3462
  const points = `points at ${hubDist ?? "the registered Hub dist"}`;
2811
3463
  return { ok: true, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail: `"${LAUNCHD_LABEL}" registered ${POSIX_CADENCE}; ${points}` };
@@ -2820,20 +3472,20 @@ function linuxSchedulerStatus(env) {
2820
3472
  }
2821
3473
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2822
3474
  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 });
3475
+ const fail2 = (detail) => ({ ok: false, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail });
2824
3476
  if (!service.includes(launcherPath)) {
2825
- return fail(`"${SYSTEMD_SERVICE_NAME}" does not run the reconcile launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
3477
+ return fail2(`"${SYSTEMD_SERVICE_NAME}" does not run the reconcile launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
2826
3478
  }
2827
3479
  const launcher = readFileOrNull(launcherPath);
2828
3480
  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\``);
3481
+ return fail2(`"${SYSTEMD_SERVICE_NAME}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
2830
3482
  }
2831
3483
  if (!timer.includes(`OnUnitActiveSec=${STARTINTERVAL_SECONDS}`)) {
2832
- return fail(`"${SYSTEMD_TIMER_NAME}" has no ${STARTINTERVAL_SECONDS}s OnUnitActiveSec \u2014 run \`mmi-hub autoupdate on\``);
3484
+ return fail2(`"${SYSTEMD_TIMER_NAME}" has no ${STARTINTERVAL_SECONDS}s OnUnitActiveSec \u2014 run \`mmi-hub autoupdate on\``);
2833
3485
  }
2834
3486
  const enabled = systemctlUser(["is-enabled", SYSTEMD_TIMER_NAME]);
2835
3487
  if (enabled.status !== 0) {
2836
- return fail(`"${SYSTEMD_TIMER_NAME}" is registered but not enabled \u2014 run \`mmi-hub autoupdate on\``);
3488
+ return fail2(`"${SYSTEMD_TIMER_NAME}" is registered but not enabled \u2014 run \`mmi-hub autoupdate on\``);
2837
3489
  }
2838
3490
  const points = `points at ${hubDist ?? "the registered Hub dist"}`;
2839
3491
  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 +3511,9 @@ function installWindowsTask(env) {
2859
3511
  const userId = `${env.USERDOMAIN || env.COMPUTERNAME || ""}\\${env.USERNAME || ""}`.replace(/^\\/, "");
2860
3512
  if (!env.USERNAME) return { ok: false, detail: "cannot resolve the registering user (USERNAME unset) \u2014 the task principal must be explicit" };
2861
3513
  const launcherPath = (0, import_node_path12.join)(paths.root, LAUNCHER_FILE);
2862
- (0, import_node_fs15.writeFileSync)(launcherPath, launcherVbs(process.execPath, hubDist), "utf8");
3514
+ (0, import_node_fs19.writeFileSync)(launcherPath, launcherVbs(process.execPath, hubDist), "utf8");
2863
3515
  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");
3516
+ (0, import_node_fs19.writeFileSync)(xmlPath, "\uFEFF" + taskXml(launcherPath, paths.root, userId, localStartBoundary(/* @__PURE__ */ new Date())), "utf16le");
2865
3517
  const create = schtasks(["/create", "/tn", TASK_NAME, "/xml", xmlPath, "/f"]);
2866
3518
  if (create.status !== 0) {
2867
3519
  const denied = /access is denied/i.test(create.stderr + create.stdout);
@@ -2880,8 +3532,8 @@ function refreshPosixLauncher(env) {
2880
3532
  const launcherPath = (0, import_node_path12.join)(paths.root, POSIX_LAUNCHER_FILE);
2881
3533
  const desired = launcherSh(process.execPath, hubDist);
2882
3534
  if (readFileOrNull(launcherPath) === desired) return null;
2883
- (0, import_node_fs15.writeFileSync)(launcherPath, desired, "utf8");
2884
- (0, import_node_fs15.chmodSync)(launcherPath, 493);
3535
+ (0, import_node_fs19.writeFileSync)(launcherPath, desired, "utf8");
3536
+ (0, import_node_fs19.chmodSync)(launcherPath, 493);
2885
3537
  return launcherPath;
2886
3538
  }
2887
3539
  function installLaunchd(env) {
@@ -2894,12 +3546,12 @@ function installLaunchd(env) {
2894
3546
  if (!hubDist) return { ok: false, detail: "cannot resolve the global @mutmutco/hub dist \u2014 run `npm install -g @mutmutco/hub` first" };
2895
3547
  const paths = ensureState(env);
2896
3548
  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);
3549
+ (0, import_node_fs19.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
3550
+ (0, import_node_fs19.chmodSync)(launcherPath, 493);
2899
3551
  const agentsDir = (0, import_node_path12.join)((0, import_node_os6.homedir)(), "Library", "LaunchAgents");
2900
- (0, import_node_fs15.mkdirSync)(agentsDir, { recursive: true });
3552
+ (0, import_node_fs19.mkdirSync)(agentsDir, { recursive: true });
2901
3553
  const plistPath = (0, import_node_path12.join)(agentsDir, LAUNCHD_PLIST_NAME);
2902
- (0, import_node_fs15.writeFileSync)(plistPath, launchdPlist(launcherPath, paths.root), "utf8");
3554
+ (0, import_node_fs19.writeFileSync)(plistPath, launchdPlist(launcherPath, paths.root), "utf8");
2903
3555
  launchctl(["bootout", `gui/${uid()}/${LAUNCHD_LABEL}`]);
2904
3556
  const load = launchctl(["bootstrap", `gui/${uid()}`, plistPath]);
2905
3557
  if (load.status !== 0) {
@@ -2918,12 +3570,12 @@ function installSystemdTimer(env) {
2918
3570
  if (!hubDist) return { ok: false, detail: "cannot resolve the global @mutmutco/hub dist \u2014 run `npm install -g @mutmutco/hub` first" };
2919
3571
  const paths = ensureState(env);
2920
3572
  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);
3573
+ (0, import_node_fs19.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
3574
+ (0, import_node_fs19.chmodSync)(launcherPath, 493);
2923
3575
  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");
3576
+ (0, import_node_fs19.mkdirSync)(unitDir, { recursive: true });
3577
+ (0, import_node_fs19.writeFileSync)((0, import_node_path12.join)(unitDir, SYSTEMD_SERVICE_NAME), systemdServiceUnit(launcherPath, paths.root), "utf8");
3578
+ (0, import_node_fs19.writeFileSync)((0, import_node_path12.join)(unitDir, SYSTEMD_TIMER_NAME), systemdTimerUnit(), "utf8");
2927
3579
  const reload = systemctlUser(["daemon-reload"]);
2928
3580
  if (reload.status !== 0) {
2929
3581
  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 +3615,11 @@ function uninstallWindowsTask(env) {
2963
3615
  function uninstallLaunchd(env) {
2964
3616
  const plistPath = (0, import_node_path12.join)((0, import_node_os6.homedir)(), "Library", "LaunchAgents", LAUNCHD_PLIST_NAME);
2965
3617
  const boot = launchctl(["bootout", `gui/${uid()}/${LAUNCHD_LABEL}`]);
2966
- if (boot.status !== 0 && (0, import_node_fs15.existsSync)(plistPath)) {
3618
+ if (boot.status !== 0 && (0, import_node_fs19.existsSync)(plistPath)) {
2967
3619
  return { ok: false, detail: `launchctl bootout failed: ${(boot.stderr || boot.stdout).trim() || "unknown error"}` };
2968
3620
  }
2969
3621
  try {
2970
- (0, import_node_fs15.rmSync)(plistPath, { force: true });
3622
+ (0, import_node_fs19.rmSync)(plistPath, { force: true });
2971
3623
  } catch {
2972
3624
  }
2973
3625
  dropLauncher(env, POSIX_LAUNCHER_FILE);
@@ -2977,12 +3629,12 @@ function uninstallSystemdTimer(env) {
2977
3629
  const unitDir = (0, import_node_path12.join)((0, import_node_os6.homedir)(), ".config", "systemd", "user");
2978
3630
  const timerPath = (0, import_node_path12.join)(unitDir, SYSTEMD_TIMER_NAME);
2979
3631
  const disable = systemctlUser(["disable", "--now", SYSTEMD_TIMER_NAME]);
2980
- if (disable.status !== 0 && (0, import_node_fs15.existsSync)(timerPath)) {
3632
+ if (disable.status !== 0 && (0, import_node_fs19.existsSync)(timerPath)) {
2981
3633
  return { ok: false, detail: `systemctl --user disable --now ${SYSTEMD_TIMER_NAME} failed: ${(disable.stderr || disable.stdout).trim() || "unknown error"}` };
2982
3634
  }
2983
3635
  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 });
3636
+ (0, import_node_fs19.rmSync)(timerPath, { force: true });
3637
+ (0, import_node_fs19.rmSync)((0, import_node_path12.join)(unitDir, SYSTEMD_SERVICE_NAME), { force: true });
2986
3638
  } catch {
2987
3639
  }
2988
3640
  systemctlUser(["daemon-reload"]);
@@ -2997,16 +3649,16 @@ function uninstallTask(env = process.env) {
2997
3649
  }
2998
3650
  function dropLauncher(env, file) {
2999
3651
  try {
3000
- (0, import_node_fs15.rmSync)((0, import_node_path12.join)(statePaths(env).root, file), { force: true });
3652
+ (0, import_node_fs19.rmSync)((0, import_node_path12.join)(statePaths(env).root, file), { force: true });
3001
3653
  } catch {
3002
3654
  }
3003
3655
  }
3004
3656
 
3005
3657
  // src/status.ts
3006
- var import_node_fs16 = require("node:fs");
3658
+ var import_node_fs20 = require("node:fs");
3007
3659
  function readJournal(path) {
3008
3660
  try {
3009
- const events = (0, import_node_fs16.readFileSync)(path, "utf8").split("\n").filter((line) => line.trim()).map((line) => {
3661
+ const events = (0, import_node_fs20.readFileSync)(path, "utf8").split("\n").filter((line) => line.trim()).map((line) => {
3010
3662
  try {
3011
3663
  return JSON.parse(line);
3012
3664
  } catch {
@@ -3129,31 +3781,6 @@ function formatHubStatus(status) {
3129
3781
  }
3130
3782
 
3131
3783
  // 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
3784
  var RETRY_HINTS = [
3158
3785
  [/applies when the host exits|applies on a quiet tick/, "installs when the app closes"],
3159
3786
  [/in flight|changed while/, "rerun update \u2014 host race"],
@@ -3182,257 +3809,34 @@ function failConsequence(row) {
3182
3809
  }
3183
3810
  return matchFirst(FAIL_CONSEQUENCE, detail) ?? "update failed \u2014 previous version kept";
3184
3811
  }
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
3812
  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
3813
+ const counts2 = {
3814
+ updated: 0,
3815
+ current: 0,
3816
+ retry: 0,
3817
+ failed: 0,
3818
+ total: summary.surfaces.filter((surface) => surface.present && surface.action === "arm-pending").length
3359
3819
  };
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";
3820
+ for (const arm of [summary.updater, summary.cli, ...summary.plugins]) {
3821
+ if (!arm) continue;
3822
+ counts2.total++;
3823
+ if (arm.verdict === "fail") counts2.failed++;
3824
+ else if (arm.verdict === "defer") counts2.retry++;
3825
+ else if (arm.verdict === "skip" || arm.from !== null && arm.from === arm.to && !arm.launchStaged) counts2.current++;
3826
+ else counts2.updated++;
3418
3827
  }
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";
3828
+ return counts2;
3425
3829
  }
3426
3830
 
3427
3831
  // src/stamp.ts
3428
- var import_node_fs17 = require("node:fs");
3832
+ var import_node_fs21 = require("node:fs");
3429
3833
  var ALWAYS_ARMED = ["cli", "updater"];
3430
3834
  var PLUGIN_ARMS = /* @__PURE__ */ new Set(["claude", "codex", "jervcode", "cursor", "hermes"]);
3431
3835
  function fleetStamp(env = process.env) {
3432
3836
  const paths = statePaths(env);
3433
3837
  let events = [];
3434
3838
  try {
3435
- events = (0, import_node_fs17.readFileSync)(paths.journalPath, "utf8").split("\n").filter((line) => line.trim()).map((line) => {
3839
+ events = (0, import_node_fs21.readFileSync)(paths.journalPath, "utf8").split("\n").filter((line) => line.trim()).map((line) => {
3436
3840
  try {
3437
3841
  return JSON.parse(line);
3438
3842
  } catch {
@@ -3482,7 +3886,7 @@ function fleetStamp(env = process.env) {
3482
3886
  // src/index.ts
3483
3887
  function ownVersion() {
3484
3888
  try {
3485
- return JSON.parse((0, import_node_fs18.readFileSync)((0, import_node_path13.join)(__dirname, "..", "package.json"), "utf8")).version;
3889
+ return JSON.parse((0, import_node_fs22.readFileSync)((0, import_node_path13.join)(__dirname, "..", "package.json"), "utf8")).version;
3486
3890
  } catch {
3487
3891
  return "0.0.0";
3488
3892
  }
@@ -3502,147 +3906,34 @@ var HELP = `mmi-hub ${ownVersion()} \u2014 install and maintain MMI tooling
3502
3906
  State remains under $MMI_UPDATER_HOME (default ~/.mmi/updater), and the Windows task name remains
3503
3907
  "MMI Fleet Updater", so existing installations continue in place.
3504
3908
  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) {
3909
+ function handOff(reexec, args, env) {
3553
3910
  const child = (0, import_node_child_process9.spawnSync)(process.execPath, [reexec.dist, ...args], {
3554
3911
  stdio: "inherit",
3555
3912
  windowsHide: true,
3556
- env: { ...process.env, MMI_UPDATER_REEXEC: "1" }
3913
+ env: { ...env, MMI_UPDATER_REEXEC: "1" }
3557
3914
  });
3558
3915
  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;
3916
+ throw new Error(`could not run the updated updater at ${reexec.dist} \u2014 ${child.error?.message ?? "no exit status"}; rerun mmi-hub update`);
3562
3917
  }
3563
3918
  return child.status;
3564
3919
  }
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
3920
  function shippedTargetFrom(args) {
3582
3921
  const index = args.indexOf("--shipped");
3583
3922
  if (index < 0) return void 0;
3584
3923
  const value = (args[index + 1] ?? "").trim();
3585
3924
  return value && !value.startsWith("--") ? value : void 0;
3586
3925
  }
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 };
3926
+ function runMaintenance2(args, dependencies = {}) {
3927
+ return runMaintenance(installer_product_default, args, {
3928
+ engine: dependencies.reconcile ?? reconcile,
3929
+ schedule: dependencies.schedule ?? installTask,
3930
+ handOff: dependencies.handOff ?? handOff,
3931
+ run: dependencies.run,
3932
+ shippedTarget: shippedTargetFrom(args),
3933
+ counts: reportCounts,
3934
+ diagnose: (arm) => arm.verdict === "fail" ? [arm.detail, failConsequence(arm), arm.repeatFailure ? "This failure persisted after repair; do not repeat the same remedy. Inspect the updater journal." : retryHint(arm.detail)].join("\n") : arm.verdict === "defer" ? `${arm.detail}
3935
+ ${retryHint(arm.detail)}` : void 0
3936
+ });
3646
3937
  }
3647
3938
  async function main(args = process.argv.slice(2)) {
3648
3939
  if (args.includes("--version")) {
@@ -3660,41 +3951,7 @@ async function main(args = process.argv.slice(2)) {
3660
3951
  process.stdout.write(json ? JSON.stringify(status, null, 2) + "\n" : formatHubStatus(status) + "\n");
3661
3952
  return status.failures.length ? 1 : 0;
3662
3953
  }
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
- }
3954
+ if (command === "update" || command === "install") return runMaintenance2(args);
3698
3955
  if (command === "autoupdate") {
3699
3956
  const commandIndex = args.indexOf(command);
3700
3957
  const mode = args[commandIndex + 1];
@@ -3719,27 +3976,18 @@ if (typeof require !== "undefined" && require.main === module) {
3719
3976
  }
3720
3977
  // Annotate the CommonJS export names for ESM import in node:
3721
3978
  0 && (module.exports = {
3722
- ACCENT,
3723
- FACE_PRODUCT,
3724
- MM_GREEN,
3725
- RESET,
3726
- accent,
3727
3979
  acquireSharedDocLock,
3728
- attentionRows,
3729
- autoupdateLine,
3730
- brandLine,
3980
+ assertFaceConformance,
3731
3981
  buildWatcherCommand,
3732
3982
  checkAtomicity,
3733
- checklistLine,
3734
3983
  claudeArm,
3735
3984
  claudeHostEvidence,
3736
3985
  clearLaunchTarget,
3737
3986
  clearPending,
3987
+ createInstallerRun,
3738
3988
  enumerateSurfaces,
3739
3989
  failConsequence,
3740
3990
  fleetStamp,
3741
- formatReconcileReport,
3742
- heldOutput,
3743
3991
  hermesArm,
3744
3992
  hermesHome,
3745
3993
  hermesHomes,
@@ -3766,17 +4014,15 @@ if (typeof require !== "undefined" && require.main === module) {
3766
4014
  probeHermesLaunchStaging,
3767
4015
  probeJervcodeInstallation,
3768
4016
  probeLaunchTargetVersion,
3769
- progressiveRow,
3770
4017
  readLaunchTarget,
3771
4018
  readPending,
3772
- receiptText,
3773
4019
  reconcile,
3774
4020
  reconcileCursorUserHooks,
3775
4021
  releaseSharedDocLock,
3776
4022
  reportCounts,
3777
- reportFooterLines,
3778
4023
  retiredSurfaces,
3779
4024
  retryHint,
4025
+ runMaintenance,
3780
4026
  schedulePendingWatcher,
3781
4027
  schedulerStatus,
3782
4028
  sharedDocLockPath,
@@ -3787,11 +4033,8 @@ if (typeof require !== "undefined" && require.main === module) {
3787
4033
  systemdServiceUnit,
3788
4034
  systemdTimerUnit,
3789
4035
  taskXml,
3790
- updateHeader,
3791
- updaterFace,
3792
4036
  watcherLockPath,
3793
4037
  withSharedDocLock,
3794
- wrapWords,
3795
4038
  writeLaunchTarget,
3796
4039
  writePending
3797
4040
  });