@mutmutco/hub 4.4.5 → 4.4.7

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 +529 -269
  2. package/package.json +4 -1
package/dist/index.cjs CHANGED
@@ -22,12 +22,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
24
  ACCENT: () => ACCENT,
25
+ FACE_PRODUCT: () => FACE_PRODUCT,
25
26
  MM_GREEN: () => MM_GREEN,
26
27
  RESET: () => RESET,
27
28
  accent: () => accent,
28
29
  acquireSharedDocLock: () => acquireSharedDocLock,
30
+ attentionRows: () => attentionRows,
29
31
  brandLine: () => brandLine,
30
32
  buildWatcherCommand: () => buildWatcherCommand,
33
+ checkAtomicity: () => checkAtomicity,
31
34
  checklistLine: () => checklistLine,
32
35
  claudeArm: () => claudeArm,
33
36
  claudeHostEvidence: () => claudeHostEvidence,
@@ -67,7 +70,6 @@ __export(index_exports, {
67
70
  progressiveRow: () => progressiveRow,
68
71
  readLaunchTarget: () => readLaunchTarget,
69
72
  readPending: () => readPending,
70
- receipt: () => receipt,
71
73
  receiptText: () => receiptText,
72
74
  reconcile: () => reconcile,
73
75
  reconcileCursorUserHooks: () => reconcileCursorUserHooks,
@@ -80,18 +82,17 @@ __export(index_exports, {
80
82
  schedulerStatus: () => schedulerStatus,
81
83
  sharedDocLockPath: () => sharedDocLockPath,
82
84
  sharedDocLockTimeoutReceipt: () => sharedDocLockTimeoutReceipt,
85
+ shippedTargetFrom: () => shippedTargetFrom,
83
86
  shouldHandOff: () => shouldHandOff,
84
- signOff: () => signOff,
85
- startSpinner: () => startSpinner,
86
87
  surfaceArmPlan: () => surfaceArmPlan,
87
88
  systemdServiceUnit: () => systemdServiceUnit,
88
89
  systemdTimerUnit: () => systemdTimerUnit,
89
90
  taskXml: () => taskXml,
90
91
  updateHeader: () => updateHeader,
92
+ updaterFace: () => updaterFace,
91
93
  watcherLockPath: () => watcherLockPath,
92
- welcome: () => welcome,
93
94
  withSharedDocLock: () => withSharedDocLock,
94
- wrapWords: () => wrapWords,
95
+ wrapWords: () => wrapWords2,
95
96
  writeLaunchTarget: () => writeLaunchTarget,
96
97
  writePending: () => writePending
97
98
  });
@@ -100,6 +101,206 @@ var import_node_child_process9 = require("node:child_process");
100
101
  var import_node_fs18 = require("node:fs");
101
102
  var import_node_path13 = require("node:path");
102
103
 
104
+ // node_modules/@mutmutco/installer-face/dist/index.js
105
+ var PRODUCTS = Object.freeze({
106
+ "mm-strategy": Object.freeze({
107
+ name: "MM Strategy",
108
+ accent: "38;2;249;115;22",
109
+ warm: "Welcome. Let's set up MM Strategy \u2014 about a minute."
110
+ }),
111
+ "mmi-hub": Object.freeze({
112
+ name: "mmi-hub",
113
+ accent: "38;2;125;211;252",
114
+ warm: "Welcome back. Checking your surfaces\u2026"
115
+ }),
116
+ "jerv-hub": Object.freeze({
117
+ name: "jerv-hub",
118
+ accent: "38;2;248;113;113",
119
+ warm: "Welcome back. Checking your surfaces\u2026"
120
+ }),
121
+ jervcode: Object.freeze({
122
+ name: "JervCode",
123
+ accent: "38;2;192;132;252",
124
+ warm: "Welcome back. Keeping JervCode current\u2026"
125
+ })
126
+ });
127
+ function identityFor(product) {
128
+ const identity = PRODUCTS[product];
129
+ if (!identity) {
130
+ throw new Error(`unknown product ${JSON.stringify(product)} \u2014 add it to installer/face/src/products.ts rather than composing a greeting locally (known: ${Object.keys(PRODUCTS).join(", ")})`);
131
+ }
132
+ return identity;
133
+ }
134
+ var GLYPH = Object.freeze({
135
+ diamond: "\u25C6",
136
+ hollow: "\u25C7",
137
+ bar: "\u2502",
138
+ check: "\u2714",
139
+ cross: "\u2716",
140
+ dot: "\u25CF",
141
+ boxTop: "\u256D",
142
+ boxTopEnd: "\u256E",
143
+ boxBottom: "\u2570",
144
+ boxBottomEnd: "\u256F",
145
+ rule: "\u2500"
146
+ });
147
+ var PALETTE = Object.freeze({
148
+ green: "38;2;31;209;138",
149
+ ink: "38;2;237;237;237",
150
+ muted: "38;2;150;150;150",
151
+ line: "38;2;42;42;42"
152
+ });
153
+ var TITLE_COLUMN = 6;
154
+ var ANSI = /\u001b\[[0-9;]*m/g;
155
+ function visibleWidth(text) {
156
+ return [...String(text).replace(ANSI, "")].length;
157
+ }
158
+ function faceWidth(columns) {
159
+ const raw = Number(columns);
160
+ return Math.max(40, Math.min(100, Number.isFinite(raw) && raw > 0 ? raw : 100));
161
+ }
162
+ function wrapWords(text, width) {
163
+ const limit = Math.max(8, Math.floor(width));
164
+ const lines = [];
165
+ let line = "";
166
+ for (const word of String(text).split(/\s+/u).filter(Boolean)) {
167
+ let token = word;
168
+ while (visibleWidth(token) > limit) {
169
+ if (line) {
170
+ lines.push(line);
171
+ line = "";
172
+ }
173
+ lines.push([...token].slice(0, limit).join(""));
174
+ token = [...token].slice(limit).join("");
175
+ }
176
+ if (!line) line = token;
177
+ else if (visibleWidth(line) + 1 + visibleWidth(token) <= limit) line += ` ${token}`;
178
+ else {
179
+ lines.push(line);
180
+ line = token;
181
+ }
182
+ }
183
+ if (line) lines.push(line);
184
+ return lines.length ? lines : [""];
185
+ }
186
+ function createFace({ product, color = false, columns }) {
187
+ const identity = identityFor(product);
188
+ const width = faceWidth(columns);
189
+ const paint = (sgr, text) => color ? `\x1B[${sgr}m${text}\x1B[0m` : String(text);
190
+ const bar = () => paint(PALETTE.muted, GLYPH.bar);
191
+ const indent = " ".repeat(TITLE_COLUMN - 1);
192
+ const welcome = () => [
193
+ `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
194
+ bar(),
195
+ `${bar()} ${identity.warm}`,
196
+ bar()
197
+ ];
198
+ const step = (title, seconds = null, kind = "ok") => {
199
+ const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.hollow) : paint(PALETTE.green, GLYPH.check);
200
+ const time = seconds === null || seconds === void 0 ? "" : paint(PALETTE.muted, `${Math.max(0, Math.round(seconds))}s`);
201
+ const column = Math.min(44, Math.max(0, width - 8));
202
+ const reserved = time ? 6 : 0;
203
+ const [first, ...rest] = wrapWords(title, width - TITLE_COLUMN - reserved);
204
+ const head = `${bar()} ${glyph} ${first}`;
205
+ const pad = Math.max(1, column - visibleWidth(head));
206
+ return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
207
+ };
208
+ const relay = (text) => String(text).split("\n").map((line) => line.trim() === "" ? bar() : `${bar()}${indent}${line}`).join("\n");
209
+ const receipt = (lines) => {
210
+ const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
211
+ const line = String(raw);
212
+ if (visibleWidth(line) <= width - 6) return [line];
213
+ const lead = /^\s*/u.exec(line)?.[0] ?? "";
214
+ return wrapWords(line, width - 6 - lead.length).map((part) => `${lead}${part}`);
215
+ });
216
+ const content = Math.min(width - 6, Math.max(...body.map(visibleWidth)));
217
+ const rule = paint(identity.accent, GLYPH.rule.repeat(content + 4));
218
+ const frame = (left, right) => `${paint(identity.accent, left)}${rule}${paint(identity.accent, right)}`;
219
+ return [
220
+ frame(GLYPH.boxTop, GLYPH.boxTopEnd),
221
+ ...body.map((line) => `${paint(identity.accent, GLYPH.bar)} ${line}${" ".repeat(Math.max(0, content - visibleWidth(line)))} ${paint(identity.accent, GLYPH.bar)}`),
222
+ frame(GLYPH.boxBottom, GLYPH.boxBottomEnd)
223
+ ];
224
+ };
225
+ const signOff = () => `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
226
+ const refusal = (message3) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message3}`;
227
+ return { identity, width, welcome, step, relay, receipt, signOff, refusal, paint };
228
+ }
229
+ var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
230
+ var SEPARATOR = "\xB7";
231
+ var subjectOf = (row) => row.aspect ? `${row.surface} ${row.aspect}` : row.surface;
232
+ var groupKey = (row) => `${subjectOf(row)}\0${row.state}\0${row.mark ?? ""}\0${row.note ?? ""}`;
233
+ function renderRows(face, rows) {
234
+ if (rows.length === 0) return [];
235
+ const groups = /* @__PURE__ */ new Map();
236
+ for (const row of rows) {
237
+ const key = groupKey(row);
238
+ const found = groups.get(key);
239
+ if (found) {
240
+ if (row.instance) found.instances.push(row.instance);
241
+ } else {
242
+ groups.set(key, { row, instances: row.instance ? [row.instance] : [] });
243
+ }
244
+ }
245
+ const subjects = [...groups.values()].map(({ row, instances }) => instances.length > 1 ? subjectOf(row) : [subjectOf(row), instances[0]].filter(Boolean).join(` ${SEPARATOR} `));
246
+ const column = Math.min(Math.max(...subjects.map(visibleWidth)), Math.max(12, face.width - 24));
247
+ const lines = [];
248
+ for (const [index, { row, instances }] of [...groups.values()].entries()) {
249
+ const subject = subjects[index];
250
+ const tail2 = instances.length > 1 ? `${row.state} ${SEPARATOR} ${instances.join(", ")}` : row.state;
251
+ const body = `${clip(subject, column).padEnd(column)} ${row.mark ? `${markGlyph(face, row.mark)} ` : ""}${tail2}`;
252
+ lines.push(...face.relay(body).split("\n"));
253
+ if (row.note) {
254
+ for (const part of wrapWords(row.note, Math.max(16, face.width - column - 10))) {
255
+ lines.push(...face.relay(`${" ".repeat(column)} ${part}`).split("\n"));
256
+ }
257
+ }
258
+ }
259
+ return lines;
260
+ }
261
+ function markGlyph(face, kind) {
262
+ const glyph = kind === "fail" ? GLYPH.cross : kind === "note" ? GLYPH.hollow : GLYPH.check;
263
+ return face.paint(kind === "fail" ? face.identity.accent : kind === "note" ? "38;2;150;150;150" : "38;2;31;209;138", glyph);
264
+ }
265
+ function clip(text, width) {
266
+ if (visibleWidth(text) <= width) return text;
267
+ return `\u2026${[...text].slice(-(width - 1)).join("")}`;
268
+ }
269
+ var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
270
+ function createSpinner(face, { animate, stream = process.stderr, intervalMs = 90 }) {
271
+ let timer = null;
272
+ let text = "";
273
+ let frame = 0;
274
+ const clear = () => {
275
+ if (animate) stream.write("\r\x1B[2K");
276
+ };
277
+ const draw = () => {
278
+ if (!animate) return;
279
+ const line = face.step(text, null, "note").split("\n")[0].replace(GLYPH.hollow, FRAMES[frame % FRAMES.length]);
280
+ stream.write(`\r\x1B[2K${line}`);
281
+ frame += 1;
282
+ };
283
+ return {
284
+ start(next) {
285
+ text = next;
286
+ frame = 0;
287
+ draw();
288
+ if (animate && !timer) timer = setInterval(draw, intervalMs).unref();
289
+ },
290
+ say(next) {
291
+ text = next;
292
+ draw();
293
+ },
294
+ stop() {
295
+ if (timer) clearInterval(timer);
296
+ timer = null;
297
+ clear();
298
+ }
299
+ };
300
+ }
301
+ var SPINNER_FRAMES = Object.freeze([...FRAMES]);
302
+ var ALLOWED = new Set(Object.values(GLYPH));
303
+
103
304
  // src/state.ts
104
305
  var import_node_fs = require("node:fs");
105
306
  var import_node_os = require("node:os");
@@ -164,11 +365,12 @@ function withLease(path, fn, waitMs = LEASE_WAIT_MS) {
164
365
  for (; ; ) {
165
366
  const attempt = tryAcquire(path);
166
367
  if (typeof attempt === "object") {
167
- try {
168
- return fn();
169
- } finally {
170
- attempt.release();
368
+ const result = fn();
369
+ if (result && typeof result.then === "function") {
370
+ return result.finally(() => attempt.release());
171
371
  }
372
+ attempt.release();
373
+ return result;
172
374
  }
173
375
  if (Date.now() >= deadline) {
174
376
  throw new UpdaterBusy(`another updater holds ${path} (waited ${waitMs}ms) \u2014 deferring`);
@@ -187,23 +389,29 @@ function appendJournal(path, event) {
187
389
  return false;
188
390
  }
189
391
  }
190
- function journalHighWater(path, surface) {
392
+ function journalHighWater(path, surface, events) {
191
393
  let high = null;
394
+ for (const event of events ?? parseJournal(path)) {
395
+ if (event.dryRun) continue;
396
+ if (event.surface === surface && event.verdict === "ok" && event.to && (!high || compareSemver(event.to, high) > 0)) {
397
+ high = event.to;
398
+ }
399
+ }
400
+ return high;
401
+ }
402
+ function parseJournal(path) {
403
+ const events = [];
192
404
  try {
193
405
  for (const line of (0, import_node_fs.readFileSync)(path, "utf8").split("\n")) {
194
406
  if (!line.trim()) continue;
195
407
  try {
196
- const event = JSON.parse(line);
197
- if (event.dryRun) continue;
198
- if (event.surface === surface && event.verdict === "ok" && event.to && (!high || compareSemver(event.to, high) > 0)) {
199
- high = event.to;
200
- }
408
+ events.push(JSON.parse(line));
201
409
  } catch {
202
410
  }
203
411
  }
204
412
  } catch {
205
413
  }
206
- return high;
414
+ return events;
207
415
  }
208
416
  function journalSurfaceRepeatedFail(path, surface, currentRun) {
209
417
  let lastVerdict = null;
@@ -254,6 +462,9 @@ function fileExists(path) {
254
462
  return (0, import_node_fs.existsSync)(path);
255
463
  }
256
464
 
465
+ // src/reconcile.ts
466
+ var import_node_perf_hooks2 = require("node:perf_hooks");
467
+
257
468
  // src/npm.ts
258
469
  var import_node_child_process = require("node:child_process");
259
470
  var import_node_fs2 = require("node:fs");
@@ -294,10 +505,40 @@ function runNpm(args, env = process.env, options = {}) {
294
505
  const result = npm.kind === "node-script" ? (0, import_node_child_process.spawnSync)(process.execPath, [npm.script, ...args], { encoding: "utf8", env, cwd: options.cwd, windowsHide: true }) : (0, import_node_child_process.spawnSync)(npm.command, args, { encoding: "utf8", env, cwd: options.cwd, shell: process.platform === "win32", windowsHide: true });
295
506
  return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
296
507
  }
508
+ function runNpmAsync(args, env = process.env, options = {}) {
509
+ const npm = resolveNpm(env);
510
+ const [file, argv, shell] = npm.kind === "node-script" ? [process.execPath, [npm.script, ...args], false] : [npm.command, args, process.platform === "win32"];
511
+ return new Promise((resolve) => {
512
+ let stdout = "";
513
+ let stderr = "";
514
+ let child;
515
+ try {
516
+ child = (0, import_node_child_process.spawn)(file, argv, { env, cwd: options.cwd, windowsHide: true, shell });
517
+ } catch (error) {
518
+ resolve({ status: null, stdout, stderr: String(error?.message ?? error) });
519
+ return;
520
+ }
521
+ child.stdout?.setEncoding("utf8");
522
+ child.stderr?.setEncoding("utf8");
523
+ child.stdout?.on("data", (chunk) => {
524
+ stdout += chunk;
525
+ });
526
+ child.stderr?.on("data", (chunk) => {
527
+ stderr += chunk;
528
+ });
529
+ child.on("error", (error) => resolve({ status: null, stdout, stderr: String(error?.message ?? error) }));
530
+ child.on("close", (status) => resolve({ status, stdout, stderr }));
531
+ });
532
+ }
533
+ var globalRootMemo = /* @__PURE__ */ new WeakMap();
297
534
  function globalRoot(env = process.env) {
535
+ const key = env;
536
+ const memoized = globalRootMemo.get(key);
537
+ if (memoized !== void 0) return memoized;
298
538
  const root = runNpm(["root", "-g"], env);
299
- if (root.status !== 0 || !root.stdout.trim()) return null;
300
- return root.stdout.trim();
539
+ const resolved = root.status !== 0 || !root.stdout.trim() ? null : root.stdout.trim();
540
+ globalRootMemo.set(key, resolved);
541
+ return resolved;
301
542
  }
302
543
  function npmView(spec, field, env = process.env) {
303
544
  const args = ["view", spec, "--json"];
@@ -317,8 +558,15 @@ function parseNpmDistIntegrity(value) {
317
558
  return typeof only === "string" && only.length > 0 ? only : null;
318
559
  }
319
560
  var npmStringField = parseNpmDistIntegrity;
320
- function npmDistIntegrity(spec, env = process.env) {
321
- return parseNpmDistIntegrity(npmView(spec, "dist.integrity", env));
561
+ async function npmDistIntegrityAsync(spec, env = process.env) {
562
+ const args = ["view", spec, "dist.integrity", "--json"];
563
+ const result = await runNpmAsync(args, env);
564
+ if (result.status !== 0 || !result.stdout.trim()) return null;
565
+ try {
566
+ return parseNpmDistIntegrity(JSON.parse(result.stdout));
567
+ } catch {
568
+ return null;
569
+ }
322
570
  }
323
571
  function npmVersions(packageName, env = process.env) {
324
572
  const versions = npmView(packageName, "versions", env);
@@ -398,38 +646,60 @@ function gateCandidate(cachePath, repoUrl, candidate) {
398
646
  }
399
647
  return { ok: true, tagCommit: commit, bom };
400
648
  }
401
- function checkAtomicity(cachePath, tagCommit, bom, candidate, npm) {
402
- const rows = [];
403
- for (const row of npmPackRows(bom)) {
649
+ var ATOMICITY_CONCURRENCY = 4;
650
+ async function mapBounded(items, bound, fn) {
651
+ const results = new Array(items.length);
652
+ let next = 0;
653
+ const workers = Array.from({ length: Math.min(bound, items.length) }, async () => {
654
+ for (; ; ) {
655
+ const index = next++;
656
+ if (index >= items.length) return;
657
+ results[index] = await fn(items[index], index);
658
+ }
659
+ });
660
+ await Promise.all(workers);
661
+ return results;
662
+ }
663
+ async function checkAtomicity(cachePath, tagCommit, bom, candidate, npm) {
664
+ const bomRows = npmPackRows(bom);
665
+ const verdicts = new Array(bomRows.length).fill(null);
666
+ const pending = [];
667
+ bomRows.forEach((row, index) => {
404
668
  if (row.version !== candidate) {
405
- rows.push({ id: row.id, ok: false, detail: `BOM row at ${row.version}, candidate ${candidate}` });
406
- continue;
669
+ verdicts[index] = { id: row.id, ok: false, detail: `BOM row at ${row.version}, candidate ${candidate}` };
670
+ return;
407
671
  }
408
672
  const manifestText = showFile(cachePath, tagCommit, `${row.identity.packagePath}/package.json`);
409
673
  let name;
410
- let manifestVersion;
674
+ let manifestVersion2;
411
675
  try {
412
676
  const manifest2 = JSON.parse(manifestText ?? "");
413
677
  name = manifest2.name;
414
- manifestVersion = manifest2.version;
678
+ manifestVersion2 = manifest2.version;
415
679
  } catch {
416
680
  }
417
- if (!name || manifestVersion !== candidate) {
418
- rows.push({ id: row.id, name, ok: false, detail: `package.json at ${row.identity.packagePath} is not stamped ${candidate}` });
419
- continue;
681
+ if (!name || manifestVersion2 !== candidate) {
682
+ verdicts[index] = { id: row.id, name, ok: false, detail: `package.json at ${row.identity.packagePath} is not stamped ${candidate}` };
683
+ return;
420
684
  }
421
- const integrity = npm.distIntegrity(`${name}@${candidate}`);
685
+ pending.push({ index, id: row.id, name });
686
+ });
687
+ const integrities = await mapBounded(pending, ATOMICITY_CONCURRENCY, (entry) => Promise.resolve(npm.distIntegrity(`${entry.name}@${candidate}`)));
688
+ pending.forEach((entry, i) => {
689
+ const integrity = integrities[i];
422
690
  if (!integrity) {
423
- rows.push({ id: row.id, name, ok: false, detail: `${name}@${candidate} not on npm` });
424
- continue;
691
+ verdicts[entry.index] = { id: entry.id, name: entry.name, ok: false, detail: `${entry.name}@${candidate} not on npm` };
692
+ return;
425
693
  }
694
+ const row = bomRows[entry.index];
426
695
  const legacySsri = /^sha\d+-/.test(row.identity.value ?? "");
427
696
  if (legacySsri && integrity !== row.identity.value) {
428
- rows.push({ id: row.id, name, ok: false, detail: `${name}@${candidate} dist.integrity differs from BOM identity` });
429
- continue;
697
+ verdicts[entry.index] = { id: entry.id, name: entry.name, ok: false, detail: `${entry.name}@${candidate} dist.integrity differs from BOM identity` };
698
+ return;
430
699
  }
431
- rows.push({ id: row.id, name, ok: true, detail: legacySsri ? `${name}@${candidate} published, integrity matches` : `${name}@${candidate} published` });
432
- }
700
+ verdicts[entry.index] = { id: entry.id, name: entry.name, ok: true, detail: legacySsri ? `${entry.name}@${candidate} published, integrity matches` : `${entry.name}@${candidate} published` };
701
+ });
702
+ const rows = verdicts.map((row, index) => row ?? { id: bomRows[index].id, ok: false, detail: "atomicity probe produced no verdict" });
433
703
  return { ok: rows.length > 0 && rows.every((row) => row.ok), rows };
434
704
  }
435
705
 
@@ -605,6 +875,15 @@ function probeVersion(distPath) {
605
875
  function tail(output) {
606
876
  return output.split("\n").filter(Boolean).slice(-3).join(" | ");
607
877
  }
878
+ function manifestVersion(packageRoot) {
879
+ if (!packageRoot) return null;
880
+ try {
881
+ const version = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(packageRoot, "package.json"), "utf8")).version;
882
+ return typeof version === "string" ? version : null;
883
+ } catch {
884
+ return null;
885
+ }
886
+ }
608
887
  function stagePackage(options) {
609
888
  const { surface, packageName, target, witness, paths, env } = options;
610
889
  const stageRoot = (0, import_node_path4.join)(paths.staging, surface, target);
@@ -632,12 +911,17 @@ function npmGlobalArm(options) {
632
911
  const { surface, packageName, distRel, target, dryRun, paths } = options;
633
912
  const spec = `${packageName}@${target}`;
634
913
  const result = (from, to, verdict, detail) => ({ surface, from, to, verdict, detail });
914
+ const highWater = journalHighWater(paths.journalPath, surface, options.journalEvents);
915
+ const localRoot = globalRoot(env);
916
+ const localVersion = localRoot ? manifestVersion((0, import_node_path4.join)(localRoot, ...packageName.split("/"))) : null;
917
+ if (localVersion === target && highWater === target) {
918
+ return result(localVersion, target, "ok", `already at target ${target} (installed manifest + journal high-water)`);
919
+ }
635
920
  const dist = globalDist(env, packageName, distRel);
636
921
  const installed = dist ? probeVersion(dist) : null;
637
922
  if (installed && compareSemver(installed, target) > 0) {
638
923
  return result(installed, installed, "skip", `installed ${installed} is above candidate ${target} (monotonic)`);
639
924
  }
640
- const highWater = journalHighWater(paths.journalPath, surface);
641
925
  if (highWater && compareSemver(highWater, target) > 0) {
642
926
  return result(installed, highWater, "skip", `journal high-water ${highWater} is above candidate ${target} (monotonic)`);
643
927
  }
@@ -861,7 +1145,7 @@ function claudeArm(options) {
861
1145
  if (from && compareSemver(from, target) > 0) {
862
1146
  return result(from, from, "skip", `installed ${from} is above candidate ${target} (monotonic)`);
863
1147
  }
864
- const highWater = journalHighWater(paths.journalPath, "claude");
1148
+ const highWater = journalHighWater(paths.journalPath, "claude", options.journalEvents);
865
1149
  if (highWater && compareSemver(highWater, target) > 0) {
866
1150
  return result(from, highWater, "skip", `journal high-water ${highWater} is above candidate ${target} (monotonic)`);
867
1151
  }
@@ -1421,7 +1705,7 @@ function cursorArm(options) {
1421
1705
  if (installed && compareSemver(installed, target) > 0) {
1422
1706
  return result(installed, installed, "skip", `installed ${installed} is above candidate ${target} (monotonic)`);
1423
1707
  }
1424
- const highWater = journalHighWater(paths.journalPath, "cursor");
1708
+ const highWater = journalHighWater(paths.journalPath, "cursor", options.journalEvents);
1425
1709
  if (highWater && compareSemver(highWater, target) > 0) {
1426
1710
  return result(installed, highWater, "skip", `journal high-water ${highWater} is above candidate ${target} (monotonic)`);
1427
1711
  }
@@ -1565,11 +1849,11 @@ function parseListText(stdout) {
1565
1849
  }
1566
1850
  function probeJervcodeInstallation(env = process.env, runner = runHostCommand) {
1567
1851
  const manifestLocation = materializedManifestLocation(env);
1568
- const manifestVersion = installedVersion(env);
1852
+ const manifestVersion2 = installedVersion(env);
1569
1853
  const manifestEvidence = () => ({
1570
- version: manifestVersion,
1854
+ version: manifestVersion2,
1571
1855
  location: manifestLocation,
1572
- detail: manifestVersion ? "materialized pi package manifest" : "materialized pi package manifest was unreadable"
1856
+ detail: manifestVersion2 ? "materialized pi package manifest" : "materialized pi package manifest was unreadable"
1573
1857
  });
1574
1858
  const hosted = hostEnv(env);
1575
1859
  const command = hostCommands(env).find((bin) => runner(bin, ["--version"], hosted).status === 0);
@@ -1585,9 +1869,9 @@ function probeJervcodeInstallation(env = process.env, runner = runHostCommand) {
1585
1869
  };
1586
1870
  }
1587
1871
  const hostError = result.status !== 0 ? `${hostLocation} failed: ${failure2(result)}` : `${hostLocation} did not list npm:${PI_PACKAGE}`;
1588
- if (manifestVersion) {
1872
+ if (manifestVersion2) {
1589
1873
  return {
1590
- version: manifestVersion,
1874
+ version: manifestVersion2,
1591
1875
  location: manifestLocation,
1592
1876
  detail: `${hostError}; using materialized pi package manifest`
1593
1877
  };
@@ -1618,7 +1902,7 @@ function jervcodeArm(options) {
1618
1902
  if (!before) return result(null, options.target, "defer", `cannot read ${(0, import_node_path9.join)(jervcodeAgentDir(env), "settings.json")}`);
1619
1903
  const from = installedVersion(env);
1620
1904
  const legacy = before.filter(isLegacyMmiPiPath);
1621
- const highWater = journalHighWater(options.paths.journalPath, "jervcode");
1905
+ const highWater = journalHighWater(options.paths.journalPath, "jervcode", options.journalEvents);
1622
1906
  const floor = from && compareSemver(from, options.target) > 0 ? `installed ${from} is above candidate ${options.target} (monotonic)` : highWater && compareSemver(highWater, options.target) > 0 ? `journal high-water ${highWater} is above candidate ${options.target} (monotonic)` : null;
1623
1907
  if (options.dryRun) {
1624
1908
  const action = floor ? `hold ${from ?? "the install"} and skip the install (${floor})` : before.includes(exact) && from === options.target ? "keep exact pin" : `install ${exact}`;
@@ -1810,7 +2094,7 @@ function hermesHomes(env = process.env) {
1810
2094
  var VERDICT_RANK = { fail: 3, defer: 2, ok: 1, skip: 0 };
1811
2095
  function hermesArm(options) {
1812
2096
  const env = options.env ?? process.env;
1813
- if (!hermesHostPresent(env, options.hostCommandRunner)) {
2097
+ if (!(options.hostPresent ?? hermesHostPresent(env, options.hostCommandRunner))) {
1814
2098
  return {
1815
2099
  surface: "hermes",
1816
2100
  from: null,
@@ -1853,7 +2137,7 @@ function armHermesHome(home, env, options, primary) {
1853
2137
  return result(installed, options.target, "defer", `${live} carries no ${PACKAGE2} package identity \u2014 refusing to displace an unmanaged Hermes mmi plugin`);
1854
2138
  }
1855
2139
  if (installed && compareSemver(installed, options.target) > 0) return result(installed, installed, "skip", `installed ${installed} is above candidate ${options.target} (monotonic)`);
1856
- const highWater = primary ? journalHighWater(options.paths.journalPath, "hermes") : null;
2140
+ const highWater = primary ? journalHighWater(options.paths.journalPath, "hermes", options.journalEvents) : null;
1857
2141
  if (highWater && compareSemver(highWater, options.target) > 0) return result(installed, highWater, "skip", `journal high-water ${highWater} is above candidate ${options.target} (monotonic)`);
1858
2142
  const hostRunner = options.hostCommandRunner ?? runHostCommand;
1859
2143
  if (installed === options.target && treeHealthy2(live)) {
@@ -1955,6 +2239,10 @@ function probeHermesLaunchStaging(env = process.env) {
1955
2239
  // src/reap.ts
1956
2240
  var import_node_fs14 = require("node:fs");
1957
2241
  var import_node_path11 = require("node:path");
2242
+ var import_node_perf_hooks = require("node:perf_hooks");
2243
+ function elapsedMs(startedAt) {
2244
+ return Math.round((import_node_perf_hooks.performance.now() - startedAt) * 10) / 10;
2245
+ }
1958
2246
  var KEEP_GENERATIONS2 = 2;
1959
2247
  var SCRATCH_AGE_MS = 24 * 60 * 6e4;
1960
2248
  function listDirs(path) {
@@ -2051,10 +2339,11 @@ function reap(options) {
2051
2339
  const { paths, dryRun } = options;
2052
2340
  const env = options.env ?? process.env;
2053
2341
  const guarded = (category, run) => {
2342
+ const startedAt = import_node_perf_hooks.performance.now();
2054
2343
  try {
2055
- return run();
2344
+ return { ...run(), durationMs: elapsedMs(startedAt) };
2056
2345
  } catch (error) {
2057
- return { category, reaped: 0, verdict: "fail", detail: `reap failed: ${error?.message ?? error}` };
2346
+ return { category, reaped: 0, verdict: "fail", detail: `reap failed: ${error?.message ?? error}`, durationMs: elapsedMs(startedAt) };
2058
2347
  }
2059
2348
  };
2060
2349
  const atomicWritePaths = [claudeMarketplacesPath(env)];
@@ -2071,8 +2360,9 @@ function shouldHandOff(self, options) {
2071
2360
  if (!options.dist) return false;
2072
2361
  return self.verdict === "ok" && Boolean(self.from) && self.from !== self.to;
2073
2362
  }
2074
- function reconcile(options) {
2363
+ async function reconcile(options) {
2075
2364
  const env = options.env ?? process.env;
2365
+ const shipped = options.shippedTarget?.trim() || null;
2076
2366
  const say = (line) => {
2077
2367
  options.narrate?.(line);
2078
2368
  options.onPhase?.(line);
@@ -2086,9 +2376,9 @@ function reconcile(options) {
2086
2376
  if (!appendJournal(paths.journalPath, { ts: (/* @__PURE__ */ new Date()).toISOString(), run, ...stamped })) summary.journalOk = false;
2087
2377
  };
2088
2378
  const summary = { run, target: null, gate: null, managedSurfaces: null, atomicity: null, cli: null, updater: null, plugins: [], reap: [], surfaces: [], journalOk: true, reexec: null, exit: 0 };
2089
- const recordArm = (arm) => {
2379
+ const recordArm = (arm, durationMs) => {
2090
2380
  if (arm.verdict === "fail") arm.repeatFailure = journalSurfaceRepeatedFail(paths.journalPath, arm.surface, run);
2091
- journal({ kind: "arm", surface: arm.surface, from: arm.from, to: arm.to, verdict: arm.verdict, detail: arm.detail, compat: arm.compat });
2381
+ journal({ kind: "arm", surface: arm.surface, from: arm.from, to: arm.to, verdict: arm.verdict, detail: arm.detail, compat: arm.compat, durationMs });
2092
2382
  options.onArm?.(arm);
2093
2383
  if (arm.verdict === "ok" && !options.dryRun && !arm.launchStaged) clearPending(paths, arm.surface);
2094
2384
  if (arm.verdict === "defer") summary.exit = Math.max(summary.exit, 2);
@@ -2097,7 +2387,8 @@ function reconcile(options) {
2097
2387
  try {
2098
2388
  say("acquiring single-writer lease");
2099
2389
  const leaseWaitMs = Number(env.MMI_UPDATER_LEASE_WAIT_MS) || void 0;
2100
- return withLease(paths.leasePath, () => {
2390
+ return await withLease(paths.leasePath, async () => {
2391
+ const journalEvents = parseJournal(paths.journalPath);
2101
2392
  if (!options.dryRun) {
2102
2393
  const pendingBefore = readPending(paths);
2103
2394
  if (pendingBefore.length) {
@@ -2106,53 +2397,69 @@ function reconcile(options) {
2106
2397
  }
2107
2398
  }
2108
2399
  summary.reap = reap({ paths, dryRun: options.dryRun, env });
2109
- for (const row of summary.reap) journal({ kind: "reap", surface: row.category, verdict: row.verdict, count: row.reaped, detail: row.detail });
2400
+ for (const row of summary.reap) journal({ kind: "reap", surface: row.category, verdict: row.verdict, count: row.reaped, detail: row.detail, durationMs: row.durationMs });
2110
2401
  for (const row of summary.reap.filter((entry) => entry.verdict !== "skip")) say(`reap ${row.category}: ${row.verdict}${row.detail ? " \u2014 " + row.detail : ""}`);
2111
- say("polling npm for @mutmutco/cli versions");
2112
- const versions = npmVersions("@mutmutco/cli", env).sort((a, b) => compareSemver(b, a));
2113
- if (!versions.length) {
2114
- journal({ kind: "poll", verdict: "fail", detail: "@mutmutco/cli has no npm versions \u2014 cannot discover a candidate" });
2115
- summary.exit = 1;
2116
- summary.detail = "npm poll returned no versions for @mutmutco/cli";
2117
- return summary;
2118
- }
2119
- say("fetching release BOM gate cache");
2120
- ensureBareCache(paths.gitCache, repoUrl);
2121
- fetchIntoCache(paths.gitCache, repoUrl, "refs/heads/main:refs/heads/main");
2122
- for (const candidate of versions.slice(0, bound)) {
2123
- say(`gating candidate v${candidate}`);
2124
- const gate = gateCandidate(paths.gitCache, repoUrl, candidate);
2125
- if (!gate.ok) {
2126
- say(`candidate v${candidate} rejected (${gate.reason ?? "gate"})`);
2127
- journal({ kind: "candidate-reject", to: candidate, verdict: "skip", detail: gate.reason ?? "gate rejected" });
2128
- continue;
2402
+ if (shipped) {
2403
+ say(`shipped target v${shipped} \u2014 the payload is the release; no npm poll, no gate`);
2404
+ summary.target = shipped;
2405
+ summary.gate = "shipped payload";
2406
+ summary.managedSurfaces = null;
2407
+ journal({ kind: "target", to: shipped, verdict: "ok", detail: "shipped payload" });
2408
+ } else {
2409
+ say("polling npm for @mutmutco/cli versions");
2410
+ const pollStartedAt = import_node_perf_hooks2.performance.now();
2411
+ const versions = npmVersions("@mutmutco/cli", env).sort((a, b) => compareSemver(b, a));
2412
+ const pollMs = elapsedMs(pollStartedAt);
2413
+ if (!versions.length) {
2414
+ journal({ kind: "poll", verdict: "fail", durationMs: pollMs, detail: "@mutmutco/cli has no npm versions \u2014 cannot discover a candidate" });
2415
+ summary.exit = 1;
2416
+ summary.detail = "npm poll returned no versions for @mutmutco/cli";
2417
+ return summary;
2129
2418
  }
2130
- const atomicity = checkAtomicity(paths.gitCache, gate.tagCommit, gate.bom, candidate, {
2131
- distIntegrity: (spec) => npmDistIntegrity(spec, env)
2132
- });
2133
- if (!atomicity.ok) {
2134
- journal({ kind: "candidate-reject", to: candidate, verdict: "skip", detail: `atomicity: ${atomicity.rows.filter((row) => !row.ok).map((row) => row.detail).join("; ")}` });
2135
- continue;
2419
+ journal({ kind: "poll", verdict: "ok", count: versions.length, durationMs: pollMs, detail: `${versions.length} versions; newest ${versions[0]}` });
2420
+ say("fetching release BOM gate cache");
2421
+ const fetchStartedAt = import_node_perf_hooks2.performance.now();
2422
+ ensureBareCache(paths.gitCache, repoUrl);
2423
+ fetchIntoCache(paths.gitCache, repoUrl, "refs/heads/main:refs/heads/main");
2424
+ journal({ kind: "gate", surface: "cache", verdict: "ok", durationMs: elapsedMs(fetchStartedAt), detail: "release BOM gate cache: refs/heads/main fetched" });
2425
+ const walkStartedAt = import_node_perf_hooks2.performance.now();
2426
+ for (const candidate of versions.slice(0, bound)) {
2427
+ say(`gating candidate v${candidate}`);
2428
+ const candidateStartedAt = import_node_perf_hooks2.performance.now();
2429
+ const gate = gateCandidate(paths.gitCache, repoUrl, candidate);
2430
+ if (!gate.ok) {
2431
+ say(`candidate v${candidate} rejected (${gate.reason ?? "gate"})`);
2432
+ journal({ kind: "candidate-reject", to: candidate, verdict: "skip", durationMs: elapsedMs(candidateStartedAt), detail: gate.reason ?? "gate rejected" });
2433
+ continue;
2434
+ }
2435
+ const atomicity = await checkAtomicity(paths.gitCache, gate.tagCommit, gate.bom, candidate, {
2436
+ distIntegrity: (spec) => npmDistIntegrityAsync(spec, env)
2437
+ });
2438
+ if (!atomicity.ok) {
2439
+ journal({ kind: "candidate-reject", to: candidate, verdict: "skip", durationMs: elapsedMs(candidateStartedAt), detail: `atomicity: ${atomicity.rows.filter((row) => !row.ok).map((row) => row.detail).join("; ")}` });
2440
+ continue;
2441
+ }
2442
+ summary.target = candidate;
2443
+ summary.managedSurfaces = Array.isArray(gate.bom?.managedSurfaces) ? gate.bom.managedSurfaces.map(String) : null;
2444
+ summary.gate = `tag v${candidate} reachable from main, BOM stamp coherent`;
2445
+ summary.atomicity = atomicity;
2446
+ journal({ kind: "target", to: candidate, verdict: "ok", durationMs: elapsedMs(candidateStartedAt), detail: summary.gate });
2447
+ break;
2136
2448
  }
2137
- summary.target = candidate;
2138
- summary.managedSurfaces = Array.isArray(gate.bom?.managedSurfaces) ? gate.bom.managedSurfaces.map(String) : null;
2139
- summary.gate = `tag v${candidate} reachable from main, BOM stamp coherent`;
2140
- summary.atomicity = atomicity;
2141
- journal({ kind: "target", to: candidate, verdict: "ok", detail: summary.gate });
2142
- break;
2143
- }
2144
- if (!summary.target) {
2145
- say("no green target \u2014 deferred");
2146
- journal({ kind: "target", verdict: "defer", detail: `no green candidate in the newest ${Math.min(bound, versions.length)} versions \u2014 registry not ready for this fleet stamp yet` });
2147
- for (const probe of enumerateSurfaces(env)) {
2148
- summary.surfaces.push({ id: probe.id, present: probe.present, action: probe.present ? "deferred-no-target" : "absent-skip" });
2149
- journal({ kind: "surface", surface: probe.id, present: probe.present, verdict: "skip", detail: probe.present ? "present; no green target yet" : `absent (${probe.marker}) \u2014 skipped` });
2449
+ if (!summary.target) {
2450
+ say("no green target \u2014 deferred");
2451
+ journal({ kind: "target", verdict: "defer", durationMs: elapsedMs(walkStartedAt), detail: `no green candidate in the newest ${Math.min(bound, versions.length)} versions \u2014 registry not ready for this fleet stamp yet` });
2452
+ for (const probe of enumerateSurfaces(env)) {
2453
+ summary.surfaces.push({ id: probe.id, present: probe.present, action: probe.present ? "deferred-no-target" : "absent-skip" });
2454
+ journal({ kind: "surface", surface: probe.id, present: probe.present, verdict: "skip", detail: probe.present ? "present; no green target yet" : `absent (${probe.marker}) \u2014 skipped` });
2455
+ }
2456
+ return summary;
2150
2457
  }
2151
- return summary;
2152
2458
  }
2153
2459
  say(`target v${summary.target} (${summary.gate})`);
2154
2460
  options.onTargetResolved?.(summary.target, summary.gate);
2155
2461
  say("updater: self-bootstrap");
2462
+ const selfStartedAt = import_node_perf_hooks2.performance.now();
2156
2463
  const self = npmGlobalArm({
2157
2464
  surface: "updater",
2158
2465
  packageName: "@mutmutco/hub",
@@ -2160,10 +2467,11 @@ function reconcile(options) {
2160
2467
  target: summary.target,
2161
2468
  dryRun: options.dryRun,
2162
2469
  paths,
2163
- env
2470
+ env,
2471
+ journalEvents
2164
2472
  });
2165
2473
  summary.updater = self;
2166
- recordArm(self);
2474
+ recordArm(self, elapsedMs(selfStartedAt));
2167
2475
  say(`updater: ${self.verdict}${self.detail ? " \u2014 " + self.detail : ""}`);
2168
2476
  const handoffDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2169
2477
  if (shouldHandOff(self, { dryRun: options.dryRun, dist: handoffDist, guard: env.MMI_UPDATER_REEXEC })) {
@@ -2173,6 +2481,7 @@ function reconcile(options) {
2173
2481
  return summary;
2174
2482
  }
2175
2483
  say(`cli: converging to v${summary.target}`);
2484
+ const cliStartedAt = import_node_perf_hooks2.performance.now();
2176
2485
  const cli = npmGlobalArm({
2177
2486
  surface: "cli",
2178
2487
  packageName: "@mutmutco/cli",
@@ -2180,10 +2489,11 @@ function reconcile(options) {
2180
2489
  target: summary.target,
2181
2490
  dryRun: options.dryRun,
2182
2491
  paths,
2183
- env
2492
+ env,
2493
+ journalEvents
2184
2494
  });
2185
2495
  summary.cli = cli;
2186
- recordArm(cli);
2496
+ recordArm(cli, elapsedMs(cliStartedAt));
2187
2497
  say(`cli: ${cli.verdict}${cli.detail ? " \u2014 " + cli.detail : ""}`);
2188
2498
  for (const probe of enumerateSurfaces(env)) {
2189
2499
  if (!probe.present) {
@@ -2198,11 +2508,12 @@ function reconcile(options) {
2198
2508
  continue;
2199
2509
  }
2200
2510
  say(`${probe.id}: converging to v${summary.target}`);
2201
- const arm = probe.id === "claude" ? claudeArm({ target: summary.target, dryRun: options.dryRun, paths, env, repoUrl }) : probe.id === "codex" ? codexArm({ target: summary.target, dryRun: options.dryRun, env, repoUrl }) : probe.id === "cursor" ? cursorArm({ target: summary.target, dryRun: options.dryRun, paths, env }) : probe.id === "jervcode" ? jervcodeArm({ target: summary.target, dryRun: options.dryRun, paths, env }) : probe.id === "hermes" ? hermesArm({ target: summary.target, dryRun: options.dryRun, paths, env }) : null;
2511
+ const armStartedAt = import_node_perf_hooks2.performance.now();
2512
+ const arm = probe.id === "claude" ? claudeArm({ target: summary.target, dryRun: options.dryRun, paths, env, repoUrl, journalEvents }) : probe.id === "codex" ? codexArm({ target: summary.target, dryRun: options.dryRun, env, repoUrl }) : probe.id === "cursor" ? cursorArm({ target: summary.target, dryRun: options.dryRun, paths, env, journalEvents }) : probe.id === "jervcode" ? jervcodeArm({ target: summary.target, dryRun: options.dryRun, paths, env, journalEvents }) : probe.id === "hermes" ? hermesArm({ target: summary.target, dryRun: options.dryRun, paths, env, hostPresent: probe.present, journalEvents }) : null;
2202
2513
  if (arm) {
2203
2514
  summary.plugins.push(arm);
2204
2515
  summary.surfaces.push({ id: probe.id, present: true, action: `arm-${arm.verdict}` });
2205
- recordArm(arm);
2516
+ recordArm(arm, elapsedMs(armStartedAt));
2206
2517
  say(`${probe.id}: ${arm.verdict}${arm.detail ? " \u2014 " + arm.detail : ""}`);
2207
2518
  } else {
2208
2519
  summary.surfaces.push({ id: probe.id, present: true, action: "arm-pending" });
@@ -2914,28 +3225,50 @@ var ARM_TITLE = {
2914
3225
  hermes: "Arming Hermes",
2915
3226
  jervcode: "Arming JervCode"
2916
3227
  };
2917
- var CHECKLIST_TAIL = {
2918
- updated: "",
2919
- current: " current",
2920
- keep: " keep",
2921
- retry: " will retry",
2922
- failed: " failed",
2923
- pending: " pending"
3228
+ var FACE_PRODUCT = "mmi-hub";
3229
+ function updaterFace(options = {}) {
3230
+ return createFace({ product: FACE_PRODUCT, color: options.color ?? false, columns: options.width });
3231
+ }
3232
+ var STATE_WORD = {
3233
+ updated: "updated",
3234
+ current: "current",
3235
+ keep: "kept",
3236
+ retry: "will retry",
3237
+ failed: "failed",
3238
+ pending: "pending"
2924
3239
  };
2925
- function checklistLine(arm, seconds, options = {}) {
2926
- const tty = options.tty ?? false;
2927
- const color = tty && (options.color ?? true);
2928
- const dryRun = options.dryRun ?? false;
2929
- const kind = classify(arm).kind;
2930
- const tone = kind === "failed" ? RED : kind === "retry" ? YELLOW : MM_GREEN;
2931
- const paint = (text) => color ? tone + text + RESET : text;
2932
- const glyph = kind === "failed" ? "\u2716" : kind === "retry" ? "\u25CF" : "\u2714";
2933
- const title = ARM_TITLE[arm.surface] ?? `Arming ${arm.surface}`;
2934
- const tail2 = dryRun && kind === "updated" ? " would update" : CHECKLIST_TAIL[kind];
2935
- const time = seconds !== null && kind !== "retry" && kind !== "failed" ? ` ${Math.max(1, Math.round(seconds))}s` : "";
2936
- return ` ${paint(glyph)} ${title}${tail2}${time}`;
2937
- }
2938
- function wrapWords(text, available) {
3240
+ function stepKindFor(kind) {
3241
+ return kind === "failed" ? "fail" : kind === "retry" ? "note" : "ok";
3242
+ }
3243
+ function checklistLine(arm, seconds, face, options = {}) {
3244
+ const { kind } = classify(arm);
3245
+ const state = options.dryRun && kind === "updated" ? "would update" : STATE_WORD[kind];
3246
+ const title = `${ARM_TITLE[arm.surface] ?? `Arming ${arm.surface}`} \u2014 ${state}`;
3247
+ const truthful = kind === "failed" || kind === "retry" ? null : seconds;
3248
+ return face.step(title, truthful, stepKindFor(kind));
3249
+ }
3250
+ function receiptText(summary, autoupdate, options = {}) {
3251
+ const dry = options.dryRun ?? false;
3252
+ const counts = reportCounts(summary);
3253
+ const health = "Check health any time: mmi-hub status";
3254
+ if (!summary.target) {
3255
+ return [`${summary.exit >= 1 ? "\u2716" : "\u25CF"} mmi-hub is not ready \u2014 no green release yet (hourly retry)`, "", health];
3256
+ }
3257
+ const ready = counts.failed === 0;
3258
+ const glyph = counts.failed > 0 ? "\u2716" : counts.retry > 0 ? "\u25CF" : dry ? "\u25C7" : "\u2714";
3259
+ 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`;
3260
+ const tail2 = autoupdate ? ` \xB7 ${autoupdate.ok ? "auto-update on" : `auto-update NOT enabled \u2014 ${autoupdate.detail}`}` : ` \xB7 ${summary.target}${dry ? " (planned)" : " (verified)"}`;
3261
+ return [`${glyph} mmi-hub is ${ready ? "ready" : "not ready"} \u2014 ${state}${tail2}`, "", health];
3262
+ }
3263
+ function attentionRows(summary) {
3264
+ return collectRows(summary).filter((row) => row.kind === "failed" || row.kind === "retry" || row.kind === "pending").map((row) => ({
3265
+ surface: row.name,
3266
+ state: statusText(row.kind, false),
3267
+ mark: row.kind === "failed" ? "fail" : "note",
3268
+ note: row.kind === "failed" ? failConsequence(row) : row.kind === "retry" ? retryHint(row.detail) : "arm not landed"
3269
+ }));
3270
+ }
3271
+ function wrapWords2(text, available) {
2939
3272
  if (available < 1) return [text];
2940
3273
  const lines = [];
2941
3274
  let current = "";
@@ -2956,7 +3289,7 @@ function noteLines(prefix, note, width) {
2956
3289
  const available = width - used - 1;
2957
3290
  const indent = available >= MIN_NOTE_SPACE ? " ".repeat(used + 1) : " ".repeat(Math.min(used + 1, Math.max(2, width - MIN_NOTE_SPACE)));
2958
3291
  const room = available >= MIN_NOTE_SPACE ? available : width - indent.length;
2959
- const parts = wrapWords(note, room);
3292
+ const parts = wrapWords2(note, room);
2960
3293
  const first = available >= MIN_NOTE_SPACE ? prefix + " " + parts[0] : prefix.replace(/\s+$/, "");
2961
3294
  const lines = [first];
2962
3295
  for (const part of parts.slice(available >= MIN_NOTE_SPACE ? 1 : 0)) lines.push(indent + part);
@@ -2979,7 +3312,7 @@ function collectRows(summary) {
2979
3312
  }
2980
3313
  function headline(detail, width) {
2981
3314
  const first = (detail ?? "").split(/(?<=[.;:])\s/)[0]?.replace(/\s+/g, " ").trim() ?? "";
2982
- return wrapWords(first || "blocked", Math.max(10, width - 4))[0];
3315
+ return wrapWords2(first || "blocked", Math.max(10, width - 4))[0];
2983
3316
  }
2984
3317
  function updateHeader(target, dryRun = false, options) {
2985
3318
  return brandLine(
@@ -3081,124 +3414,6 @@ function formatReconcileReport(summary, options = {}) {
3081
3414
  return out.join("\n") + "\n";
3082
3415
  }
3083
3416
 
3084
- // src/face.ts
3085
- var DEFAULT_WIDTH2 = 100;
3086
- function welcome(color, output = process.stdout) {
3087
- const mark = color ? MM_GREEN + "\u25C6" + RESET : "\u25C6";
3088
- output.write(`${mark} ${accent("mmi-hub", color)} \u2014 Mutatis Mutandis
3089
- `);
3090
- output.write("\u2502\n");
3091
- output.write("\u2502 Welcome back. Checking your surfaces\u2026\n");
3092
- output.write("\u2502\n");
3093
- }
3094
- var TONES = { "\u2714": MM_GREEN, "\u2716": RED, "\u25CF": YELLOW };
3095
- function paintGlyph(line, color) {
3096
- if (!color) return line;
3097
- const glyph = line.trimStart()[0];
3098
- const tone = TONES[glyph];
3099
- if (!tone) return line;
3100
- const at = line.indexOf(glyph);
3101
- return line.slice(0, at) + tone + glyph + RESET + line.slice(at + 1);
3102
- }
3103
- function receipt(text, options = {}) {
3104
- const color = options.color ?? false;
3105
- const width = Math.max(40, options.width ?? DEFAULT_WIDTH2);
3106
- const output = options.output ?? process.stdout;
3107
- const edges = 2 * 2 + 2;
3108
- const available = width - edges;
3109
- const lines = [];
3110
- for (const paragraph of text.split("\n")) {
3111
- if (!paragraph.trim()) lines.push(null);
3112
- else if (paragraph.length <= available) lines.push(paragraph);
3113
- else for (const line of wrapWords(paragraph, available)) lines.push(line);
3114
- }
3115
- const inner = Math.min(available, Math.max(20, ...lines.map((line) => line?.length ?? 0)));
3116
- const rule = "\u2500".repeat(inner + 2 * 2);
3117
- const frame = (text2) => color ? ACCENT + text2 + RESET : text2;
3118
- const blank = `${frame("\u2502")}${" ".repeat(inner + 2 * 2)}${frame("\u2502")}`;
3119
- output.write(frame(`\u256D${rule}\u256E`) + "\n");
3120
- for (const line of lines) {
3121
- output.write(line === null ? blank + "\n" : `${frame("\u2502")} ${paintGlyph(line.padEnd(inner), color)} ${frame("\u2502")}
3122
- `);
3123
- }
3124
- output.write(frame(`\u2570${rule}\u256F`) + "\n");
3125
- }
3126
- function signOff(color, output = process.stdout) {
3127
- output.write(`\u2570 ${accent("mmi-hub \xB7 Mutatis Mutandis", color)}
3128
- `);
3129
- }
3130
- function receiptText(summary, autoupdate, options = {}) {
3131
- const dry = options.dryRun ?? false;
3132
- const counts = reportCounts(summary);
3133
- const health = "Check health any time: mmi-hub status";
3134
- if (!summary.target) {
3135
- return `${summary.exit >= 1 ? "\u2716" : "\u25CF"} mmi-hub is not ready \u2014 no green release yet (hourly retry)
3136
- ${health}`;
3137
- }
3138
- const ready = counts.failed === 0;
3139
- const glyph = counts.failed > 0 ? "\u2716" : counts.retry > 0 ? "\u25CF" : dry ? "\u25C7" : "\u2714";
3140
- 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`;
3141
- const tail2 = autoupdate ? ` \xB7 ${autoupdate.ok ? "auto-update on" : `auto-update NOT enabled \u2014 ${autoupdate.detail}`}` : ` \xB7 ${summary.target}${dry ? " (planned)" : " (verified)"}`;
3142
- return `${glyph} mmi-hub is ${ready ? "ready" : "not ready"} \u2014 ${state}${tail2}
3143
- ${health}`;
3144
- }
3145
-
3146
- // src/spinner.ts
3147
- var import_node_worker_threads = require("node:worker_threads");
3148
- var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3149
- var WORKER_SRC = `
3150
- const { parentPort, workerData } = require('node:worker_threads');
3151
- const frames = ${JSON.stringify(FRAMES)};
3152
- const control = new Int32Array(workerData.sab);
3153
- let frame = 0, label = '', timer = null;
3154
- function tick() {
3155
- if (Atomics.load(control, 0) === 1) return; // paused by the main thread
3156
- if (!label) return;
3157
- process.stderr.write('\\r' + frames[frame++ % frames.length] + ' ' + label + '\\x1b[K');
3158
- }
3159
- parentPort.on('message', (message) => {
3160
- if (message && message.type === 'label') { label = message.label; if (!timer) timer = setInterval(tick, 80); }
3161
- else if (message === 'stop') { if (timer) clearInterval(timer); process.stderr.write('\\r\\x1b[K'); process.exit(0); }
3162
- });
3163
- `;
3164
- var NOOP = { label() {
3165
- }, persist() {
3166
- }, stop() {
3167
- }, active: false };
3168
- function startSpinner(enabled) {
3169
- if (!enabled) return NOOP;
3170
- let worker;
3171
- let control;
3172
- try {
3173
- const sab = new SharedArrayBuffer(4);
3174
- control = new Int32Array(sab);
3175
- worker = new import_node_worker_threads.Worker(WORKER_SRC, { eval: true, workerData: { sab } });
3176
- worker.unref();
3177
- worker.on("error", () => {
3178
- });
3179
- } catch {
3180
- return NOOP;
3181
- }
3182
- let stopped = false;
3183
- return {
3184
- active: true,
3185
- label: (text) => {
3186
- if (!stopped) worker.postMessage({ type: "label", label: text });
3187
- },
3188
- persist: (line) => {
3189
- Atomics.store(control, 0, 1);
3190
- process.stderr.write("\r\x1B[K");
3191
- process.stdout.write(line + "\n");
3192
- Atomics.store(control, 0, 0);
3193
- },
3194
- stop: () => {
3195
- if (stopped) return;
3196
- stopped = true;
3197
- worker.postMessage("stop");
3198
- }
3199
- };
3200
- }
3201
-
3202
3417
  // src/stamp.ts
3203
3418
  var import_node_fs17 = require("node:fs");
3204
3419
  var ALWAYS_ARMED = ["cli", "updater"];
@@ -3296,17 +3511,33 @@ function printReconcile(summary, json, dryRun = false) {
3296
3511
  function faceEnabled(args, json) {
3297
3512
  return !json && !args.includes("--verbose") && !process.env.NO_COLOR && process.env.TERM !== "dumb" && Boolean(process.stdout.isTTY) && Boolean(process.stderr.isTTY);
3298
3513
  }
3299
- function openFace(args, json) {
3300
- const face = faceEnabled(args, json);
3301
- if (face && !process.env.MMI_UPDATER_REEXEC) welcome(true);
3514
+ function openFace(args, json, dryRun) {
3515
+ if (!faceEnabled(args, json)) return null;
3516
+ const face = updaterFace(humanOptions(dryRun));
3517
+ if (!process.env.MMI_UPDATER_REEXEC) for (const line of face.welcome()) process.stdout.write(line + "\n");
3302
3518
  return face;
3303
3519
  }
3304
- function finishFace(summary, autoupdate, dryRun) {
3520
+ function finishFace(face, summary, autoupdate, dryRun) {
3305
3521
  const options = humanOptions(dryRun);
3306
- if (!summary.target) process.stdout.write(formatReconcileReport(summary, options));
3307
- else if (summary.exit >= 1) process.stdout.write(reportFooterLines(summary, options).join("\n") + "\n");
3308
- receipt(receiptText(summary, autoupdate, { dryRun }), { color: options.color, width: options.width });
3309
- signOff(true);
3522
+ const write = (line) => {
3523
+ process.stdout.write(line + "\n");
3524
+ };
3525
+ if (!summary.target) {
3526
+ process.stdout.write(formatReconcileReport(summary, { ...options, tty: false }));
3527
+ } else {
3528
+ for (const line of renderRows(face, attentionRows(summary))) write(line);
3529
+ }
3530
+ const body = receiptText(summary, autoupdate, { dryRun });
3531
+ const counts = reportCounts(summary);
3532
+ if (counts.failed > 0) {
3533
+ body.push("", "Fix: mmi-cli doctor, then mmi-hub update", `Log: ${options.journalPath}`);
3534
+ }
3535
+ const self = summary.updater;
3536
+ if (!dryRun && self && self.from && self.to && self.from !== self.to && self.verdict !== "fail") {
3537
+ 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}`);
3538
+ }
3539
+ for (const line of face.receipt(body)) write(line);
3540
+ write(face.signOff());
3310
3541
  }
3311
3542
  function handOff(reexec, args) {
3312
3543
  const child = (0, import_node_child_process9.spawnSync)(process.execPath, [reexec.dist, ...args], {
@@ -3337,7 +3568,13 @@ function heldOutput(write) {
3337
3568
  }
3338
3569
  };
3339
3570
  }
3340
- function runUpdate(args, json, face = false) {
3571
+ function shippedTargetFrom(args) {
3572
+ const index = args.indexOf("--shipped");
3573
+ if (index < 0) return void 0;
3574
+ const value = (args[index + 1] ?? "").trim();
3575
+ return value && !value.startsWith("--") ? value : void 0;
3576
+ }
3577
+ async function runUpdate(args, json, face = null, shippedTarget) {
3341
3578
  const dryRun = args.includes("--dry-run");
3342
3579
  const verbose = args.includes("--verbose");
3343
3580
  const narrate = (line) => {
@@ -3346,19 +3583,35 @@ function runUpdate(args, json, face = false) {
3346
3583
  };
3347
3584
  const progressive = !json && !verbose && Boolean(process.stderr.isTTY);
3348
3585
  if (!progressive) {
3349
- return { summary: reconcile({ dryRun, narrate: verbose ? narrate : void 0 }), rendered: false };
3586
+ return reconcile({ dryRun, narrate: verbose ? narrate : void 0, shippedTarget }).then((summary2) => ({ summary: summary2, rendered: false }));
3350
3587
  }
3351
3588
  const reportOptions = humanOptions(dryRun);
3352
- const spinner = startSpinner(true);
3589
+ const spinner = createSpinner(face ?? updaterFace(reportOptions), { animate: true, stream: process.stderr });
3590
+ let spinning = false;
3591
+ const phase = (line) => {
3592
+ if (spinning) spinner.say(line);
3593
+ else {
3594
+ spinner.start(line);
3595
+ spinning = true;
3596
+ }
3597
+ };
3598
+ const persist = (line) => {
3599
+ if (spinning) {
3600
+ spinner.stop();
3601
+ spinning = false;
3602
+ }
3603
+ process.stdout.write(line + "\n");
3604
+ };
3353
3605
  let headerEmitted = false;
3354
3606
  let summary;
3355
- const { emit, stream } = heldOutput((line) => spinner.persist(line));
3607
+ const { emit, stream } = heldOutput(persist);
3356
3608
  let arms = 0;
3357
3609
  let mark = Date.now();
3358
3610
  try {
3359
- summary = reconcile({
3611
+ summary = await reconcile({
3360
3612
  dryRun,
3361
- onPhase: (line) => spinner.label(line),
3613
+ shippedTarget,
3614
+ onPhase: (line) => phase(line),
3362
3615
  onTargetResolved: face ? void 0 : (target) => {
3363
3616
  emit(updateHeader(target, dryRun, reportOptions));
3364
3617
  emit("");
@@ -3368,7 +3621,7 @@ function runUpdate(args, json, face = false) {
3368
3621
  const seconds = (Date.now() - mark) / 1e3;
3369
3622
  mark = Date.now();
3370
3623
  if (arms++) stream();
3371
- emit(face ? checklistLine(arm, seconds, reportOptions) : progressiveRow(arm, reportOptions));
3624
+ emit(face ? checklistLine(arm, seconds, face, reportOptions) : progressiveRow(arm, reportOptions));
3372
3625
  }
3373
3626
  });
3374
3627
  } finally {
@@ -3381,7 +3634,7 @@ function runUpdate(args, json, face = false) {
3381
3634
  else process.stdout.write(formatReconcileReport(summary, reportOptions));
3382
3635
  return { summary, rendered: true };
3383
3636
  }
3384
- function main(args = process.argv.slice(2)) {
3637
+ async function main(args = process.argv.slice(2)) {
3385
3638
  if (args.includes("--version")) {
3386
3639
  process.stdout.write(ownVersion() + "\n");
3387
3640
  return 0;
@@ -3398,19 +3651,21 @@ function main(args = process.argv.slice(2)) {
3398
3651
  return status.failures.length ? 1 : 0;
3399
3652
  }
3400
3653
  if (command === "update") {
3401
- const face = openFace(args, json);
3402
- const { summary, rendered } = runUpdate(args, json, face);
3654
+ const dryRun = args.includes("--dry-run");
3655
+ const face = openFace(args, json, dryRun);
3656
+ const { summary, rendered } = await runUpdate(args, json, face);
3403
3657
  if (summary.reexec) return handOff(summary.reexec, args);
3404
- if (face) finishFace(summary, null, args.includes("--dry-run"));
3405
- else if (!rendered) printReconcile(summary, json, args.includes("--dry-run"));
3658
+ if (face) finishFace(face, summary, null, dryRun);
3659
+ else if (!rendered) printReconcile(summary, json, dryRun);
3406
3660
  return summary.exit;
3407
3661
  }
3408
3662
  if (command === "install") {
3409
- const face = openFace(args, json);
3410
3663
  const dryRun = args.includes("--dry-run");
3411
- const { summary, rendered } = runUpdate(args, json, face);
3664
+ const face = openFace(args, json, dryRun);
3665
+ const shippedTarget = shippedTargetFrom(args);
3666
+ const { summary, rendered } = await runUpdate(args, json, face, shippedTarget);
3412
3667
  if (summary.reexec) {
3413
- const handedOff = handOff(summary.reexec, ["update", ...args.filter((arg) => arg !== "install")]);
3668
+ const handedOff = handOff(summary.reexec, ["update", ...args.filter((arg) => arg !== "install" && arg !== "--shipped" && arg !== shippedTarget)]);
3414
3669
  const scheduled = installTask();
3415
3670
  if (!json) {
3416
3671
  process.stdout.write(scheduled.ok ? ` Auto-update on \u2014 hourly task "MMI Fleet Updater".
@@ -3425,7 +3680,7 @@ function main(args = process.argv.slice(2)) {
3425
3680
  if (json) {
3426
3681
  process.stdout.write(JSON.stringify({ convergence: summary, autoupdate: schedule }, null, 2) + "\n");
3427
3682
  } else if (face) {
3428
- finishFace(summary, schedule, dryRun);
3683
+ finishFace(face, summary, schedule, dryRun);
3429
3684
  } else {
3430
3685
  if (!rendered) printReconcile(summary, false, dryRun);
3431
3686
  process.stdout.write(schedule.ok ? ` Auto-update on \u2014 hourly task "MMI Fleet Updater".
@@ -3450,17 +3705,24 @@ function main(args = process.argv.slice(2)) {
3450
3705
  return 1;
3451
3706
  }
3452
3707
  if (typeof require !== "undefined" && require.main === module) {
3453
- process.exit(main());
3708
+ main().then((code) => process.exit(code)).catch((error) => {
3709
+ process.stderr.write(`mmi-hub: ${error?.stack ?? error}
3710
+ `);
3711
+ process.exit(1);
3712
+ });
3454
3713
  }
3455
3714
  // Annotate the CommonJS export names for ESM import in node:
3456
3715
  0 && (module.exports = {
3457
3716
  ACCENT,
3717
+ FACE_PRODUCT,
3458
3718
  MM_GREEN,
3459
3719
  RESET,
3460
3720
  accent,
3461
3721
  acquireSharedDocLock,
3722
+ attentionRows,
3462
3723
  brandLine,
3463
3724
  buildWatcherCommand,
3725
+ checkAtomicity,
3464
3726
  checklistLine,
3465
3727
  claudeArm,
3466
3728
  claudeHostEvidence,
@@ -3500,7 +3762,6 @@ if (typeof require !== "undefined" && require.main === module) {
3500
3762
  progressiveRow,
3501
3763
  readLaunchTarget,
3502
3764
  readPending,
3503
- receipt,
3504
3765
  receiptText,
3505
3766
  reconcile,
3506
3767
  reconcileCursorUserHooks,
@@ -3513,16 +3774,15 @@ if (typeof require !== "undefined" && require.main === module) {
3513
3774
  schedulerStatus,
3514
3775
  sharedDocLockPath,
3515
3776
  sharedDocLockTimeoutReceipt,
3777
+ shippedTargetFrom,
3516
3778
  shouldHandOff,
3517
- signOff,
3518
- startSpinner,
3519
3779
  surfaceArmPlan,
3520
3780
  systemdServiceUnit,
3521
3781
  systemdTimerUnit,
3522
3782
  taskXml,
3523
3783
  updateHeader,
3784
+ updaterFace,
3524
3785
  watcherLockPath,
3525
- welcome,
3526
3786
  withSharedDocLock,
3527
3787
  wrapWords,
3528
3788
  writeLaunchTarget,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/hub",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
4
4
  "description": "Install and maintain the MMI CLI and every present host surface from one release-gated Hub command.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -25,6 +25,9 @@
25
25
  "test": "node build.mjs && node --test test/*.test.cjs",
26
26
  "typecheck": "tsc --noEmit"
27
27
  },
28
+ "dependencies": {
29
+ "@mutmutco/installer-face": "^0.2.0"
30
+ },
28
31
  "devDependencies": {
29
32
  "@types/node": "^22.0.0",
30
33
  "esbuild": "^0.28.1",