@mutmutco/hub 3.139.10 → 3.139.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +15 -0
  2. package/dist/index.cjs +383 -35
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -24,6 +24,21 @@ mmi-hub autoupdate on|off idempotently enable or disable scheduling; off
24
24
 
25
25
  `status` and the no-argument command are read-only. They never install, heal, create state, change the schedule, or clean a repository.
26
26
 
27
+ `update`/`install` print a lean, human report (glyphs and color only on an interactive terminal, `NO_COLOR` respected; no line ever exceeds the terminal width):
28
+
29
+ ```
30
+ MMI Hub — updating to 3.139.11 (verified)
31
+
32
+ cli 3.139.10 → 3.139.11 updated
33
+ claude 3.139.10 → 3.139.11 updated next session
34
+ codex 3.139.10 → 3.139.11 retry rerun update — host race
35
+ hub 3.139.10 → 3.139.11 updated next run
36
+
37
+ 3/4 updated · 1 will retry
38
+ ```
39
+
40
+ Failures close with an action block — consequence, fix command, journal path — never a raw host error. An up-to-date run prints a single line.
41
+
27
42
  State remains under `~/.mmi/updater` (override with `$MMI_UPDATER_HOME`) so existing journals and the **MMI Fleet Updater** Windows task continue in place.
28
43
 
29
44
  `@mutmutco/updater@3.137.0` was the final historical compatibility release and remains installable for existing lockfiles. Its `mmi-updater` wrapper delegates to the exact-version `@mutmutco/hub`: old `reconcile`/`run`, `install`, `uninstall`, and `verify`/`stamp` calls map to `mmi-hub update`, `install`, `autoupdate off`, and `status` respectively. New installs and all releases after 3.137.0 use only `@mutmutco/hub` / `mmi-hub`.
package/dist/index.cjs CHANGED
@@ -22,6 +22,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
24
  enumerateSurfaces: () => enumerateSurfaces,
25
+ failConsequence: () => failConsequence,
26
+ formatReconcileReport: () => formatReconcileReport,
25
27
  hermesArm: () => hermesArm,
26
28
  hermesHome: () => hermesHome,
27
29
  hubStatus: () => hubStatus,
@@ -30,8 +32,14 @@ __export(index_exports, {
30
32
  main: () => main,
31
33
  ownVersion: () => ownVersion,
32
34
  probeHermesInstallation: () => probeHermesInstallation,
35
+ progressiveRow: () => progressiveRow,
36
+ reportFooterLines: () => reportFooterLines,
37
+ retryHint: () => retryHint,
33
38
  schedulerStatus: () => schedulerStatus,
34
- taskXml: () => taskXml
39
+ startSpinner: () => startSpinner,
40
+ taskXml: () => taskXml,
41
+ updateHeader: () => updateHeader,
42
+ wrapWords: () => wrapWords
35
43
  });
36
44
  module.exports = __toCommonJS(index_exports);
37
45
  var import_node_fs16 = require("node:fs");
@@ -643,6 +651,7 @@ function claudeArm(options) {
643
651
  }
644
652
  return { surface: "claude", from, to: options.target, verdict: "ok", detail: `catalog refreshed; converged and verified ${from ?? "absent"} -> ${options.target}` };
645
653
  }
654
+ var SNAPSHOT_MISSING = /marketplace snapshot|marketplace root does not contain/i;
646
655
  function codexMmi(runner, env) {
647
656
  const result = runner("codex", ["plugin", "list", "--json"], env);
648
657
  const parsed = parseJson(result);
@@ -652,7 +661,14 @@ function codexMmi(runner, env) {
652
661
  function codexArm(options) {
653
662
  const env = options.env ?? process.env;
654
663
  const runner = options.runner ?? runHostCommand;
655
- const before = codexMmi(runner, env);
664
+ let before = codexMmi(runner, env);
665
+ if (before.error && SNAPSHOT_MISSING.test(before.error) && !options.dryRun) {
666
+ const restore = runner("codex", ["plugin", "marketplace", "upgrade"], env);
667
+ if (restore.status !== 0) {
668
+ return { surface: "codex", from: null, to: options.target, verdict: "defer", detail: `marketplace snapshots missing and refresh failed: ${failure(restore)}` };
669
+ }
670
+ before = codexMmi(runner, env);
671
+ }
656
672
  if (before.error) return { surface: "codex", from: null, to: options.target, verdict: "defer", detail: before.error };
657
673
  const from = before.row?.version ?? null;
658
674
  if (options.dryRun) {
@@ -1352,6 +1368,25 @@ function discard2(path) {
1352
1368
  function message2(error) {
1353
1369
  return String(error?.message ?? error).replace(/\s+/g, " ").slice(0, 200);
1354
1370
  }
1371
+ function materializeHermesSkills(home) {
1372
+ const pluginSkills = (0, import_node_path10.join)(home, "plugins", "mmi", "skills");
1373
+ const skillsRoot = (0, import_node_path10.join)(home, "skills");
1374
+ const targetRoot = (0, import_node_path10.join)(skillsRoot, "mmi");
1375
+ try {
1376
+ if (!(0, import_node_fs12.existsSync)(pluginSkills)) return { ok: false, detail: `plugin skills tree missing at ${pluginSkills}` };
1377
+ const names = (0, import_node_fs12.readdirSync)(pluginSkills, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_")).map((entry) => entry.name);
1378
+ if (!names.length) return { ok: false, detail: `plugin skills tree is empty at ${pluginSkills}` };
1379
+ (0, import_node_fs12.mkdirSync)(skillsRoot, { recursive: true });
1380
+ const incoming = (0, import_node_path10.join)(skillsRoot, ".mmi-incoming");
1381
+ discard2(incoming);
1382
+ (0, import_node_fs12.cpSync)(pluginSkills, incoming, { recursive: true });
1383
+ discard2(targetRoot);
1384
+ (0, import_node_fs12.renameSync)(incoming, targetRoot);
1385
+ return { ok: true, detail: `provisioned ${names.length} skills to ${targetRoot}` };
1386
+ } catch (error) {
1387
+ return { ok: false, detail: message2(error) };
1388
+ }
1389
+ }
1355
1390
  function hermesArm(options) {
1356
1391
  const env = options.env ?? process.env;
1357
1392
  const home = hermesHome(env);
@@ -1372,8 +1407,12 @@ function hermesArm(options) {
1372
1407
  if (installed && compareSemver(installed, options.target) > 0) return result(installed, installed, "skip", `installed ${installed} is above candidate ${options.target} (monotonic)`);
1373
1408
  const highWater = journalHighWater(options.paths.journalPath, "hermes");
1374
1409
  if (highWater && compareSemver(highWater, options.target) > 0) return result(installed, highWater, "skip", `journal high-water ${highWater} is above candidate ${options.target} (monotonic)`);
1375
- if (installed === options.target && treeHealthy3(live)) return result(installed, options.target, "ok", `already at target ${options.target}`);
1376
- if (options.dryRun) return result(installed, options.target, "ok", `dry-run: would stage ${PACKAGE2}@${options.target} and atomically switch ${installed ?? "absent"} -> ${options.target}`);
1410
+ if (installed === options.target && treeHealthy3(live)) {
1411
+ const skills2 = materializeHermesSkills(home);
1412
+ if (!skills2.ok) return result(installed, options.target, "fail", `plugin at target ${options.target}, but skills provisioning failed: ${skills2.detail}`);
1413
+ return result(installed, options.target, "ok", `already at target ${options.target}; ${skills2.detail}`);
1414
+ }
1415
+ if (options.dryRun) return result(installed, options.target, "ok", `dry-run: would stage ${PACKAGE2}@${options.target}, atomically switch ${installed ?? "absent"} -> ${options.target}, then provision skills into ${(0, import_node_path10.join)(home, "skills", "mmi")}`);
1377
1416
  const staged = stagePackage({ surface: "hermes", packageName: PACKAGE2, target: options.target, witness: MANIFEST2, paths: options.paths, env });
1378
1417
  if ("defer" in staged) return result(installed, options.target, "defer", staged.defer);
1379
1418
  if (yamlVersion(staged.packageRoot) !== options.target || !treeHealthy3(staged.packageRoot)) {
@@ -1419,7 +1458,9 @@ function hermesArm(options) {
1419
1458
  return result(installed, displaced ? installed ?? "unknown" : "none", "fail", `post-switch Hermes verification failed; rejected generation parked at ${broken}${displaced ? "; previous generation restored" : ""}`);
1420
1459
  }
1421
1460
  discard2(incomingRoot);
1422
- return result(installed, options.target, "ok", `converged ${installed ?? "absent"} -> ${options.target} (staged, verified, atomically switched${displaced ? `; previous generation quarantined at ${quarantine}` : ""})`);
1461
+ const skills = materializeHermesSkills(home);
1462
+ if (!skills.ok) return result(installed, options.target, "fail", `plugin converged to ${options.target}, but skills provisioning failed: ${skills.detail}`);
1463
+ return result(installed, options.target, "ok", `converged ${installed ?? "absent"} -> ${options.target} (staged, verified, atomically switched${displaced ? `; previous generation quarantined at ${quarantine}` : ""}); ${skills.detail}`);
1423
1464
  }
1424
1465
 
1425
1466
  // src/reap.ts
@@ -1732,7 +1773,10 @@ var REPO_URL_DEFAULT = "https://github.com/mutmutco/MMI-Hub.git";
1732
1773
  var CANDIDATE_BOUND_DEFAULT = 12;
1733
1774
  function reconcile(options) {
1734
1775
  const env = options.env ?? process.env;
1735
- const say = (line) => options.narrate?.(line);
1776
+ const say = (line) => {
1777
+ options.narrate?.(line);
1778
+ options.onPhase?.(line);
1779
+ };
1736
1780
  const run = `${(/* @__PURE__ */ new Date()).toISOString()}-${process.pid}`;
1737
1781
  const paths = ensureState(env);
1738
1782
  const repoUrl = env.MMI_UPDATER_REPO || REPO_URL_DEFAULT;
@@ -1744,6 +1788,7 @@ function reconcile(options) {
1744
1788
  const summary = { run, target: null, gate: null, atomicity: null, cli: null, updater: null, plugins: [], reap: [], surfaces: [], journalOk: true, exit: 0 };
1745
1789
  const recordArm = (arm) => {
1746
1790
  journal({ kind: "arm", surface: arm.surface, from: arm.from, to: arm.to, verdict: arm.verdict, detail: arm.detail, compat: arm.compat });
1791
+ options.onArm?.(arm);
1747
1792
  if (arm.verdict === "defer") summary.exit = Math.max(summary.exit, 2);
1748
1793
  if (arm.verdict === "fail") summary.exit = Math.max(summary.exit, 3);
1749
1794
  };
@@ -1798,6 +1843,7 @@ function reconcile(options) {
1798
1843
  return summary;
1799
1844
  }
1800
1845
  say(`target v${summary.target} (${summary.gate})`);
1846
+ options.onTargetResolved?.(summary.target, summary.gate);
1801
1847
  say(`cli: converging to v${summary.target}`);
1802
1848
  const cli = cliArm({ target: summary.target, dryRun: options.dryRun, paths, env });
1803
1849
  summary.cli = cli;
@@ -2145,6 +2191,276 @@ function formatHubStatus(status) {
2145
2191
  return lines.join("\n");
2146
2192
  }
2147
2193
 
2194
+ // src/report.ts
2195
+ var NAME_WIDTH = 8;
2196
+ var VERSION_WIDTH = 15;
2197
+ var DEFAULT_WIDTH = 100;
2198
+ var MIN_NOTE_SPACE = 8;
2199
+ var ACTIVATION_NOTE = {
2200
+ claude: "next session",
2201
+ codex: "next session",
2202
+ kimi: "next session",
2203
+ jervcode: "next launch",
2204
+ cursor: "on reload",
2205
+ kilo: "after restart",
2206
+ hermes: "after restart",
2207
+ updater: "next run"
2208
+ };
2209
+ var GREEN = "\x1B[32m";
2210
+ var YELLOW = "\x1B[33m";
2211
+ var RED = "\x1B[31m";
2212
+ var RESET = "\x1B[0m";
2213
+ var RETRY_HINTS = [
2214
+ [/in flight|changed while/, "rerun update \u2014 host race"],
2215
+ [/is running|live process|inside a .* hook/, "close app, rerun update"],
2216
+ [/staging install failed|etarget|e404|network|fetch failed/, "check network/login, rerun"],
2217
+ [/not on npm|no green|registry not ready/, "release still publishing"],
2218
+ [/elevat|access is denied/, "run from elevated terminal"]
2219
+ ];
2220
+ var FAIL_CONSEQUENCE = [
2221
+ [/post-switch probe|post-update evidence|post-install evidence|expected .*\/true/, "not verified \u2014 previous version restored, safe to use"],
2222
+ [/rollback|did not restore/, "rollback incomplete \u2014 run doctor now"]
2223
+ ];
2224
+ function matchFirst(patterns, detail) {
2225
+ const haystack = (detail ?? "").toLowerCase();
2226
+ for (const [pattern, text] of patterns) if (pattern.test(haystack)) return text;
2227
+ return null;
2228
+ }
2229
+ function retryHint(detail) {
2230
+ return matchFirst(RETRY_HINTS, detail) ?? "retries next hourly run";
2231
+ }
2232
+ function failConsequence(detail) {
2233
+ return matchFirst(FAIL_CONSEQUENCE, detail) ?? "update failed \u2014 previous version kept";
2234
+ }
2235
+ function classify(arm) {
2236
+ const kind = arm.verdict === "ok" ? arm.from !== null && arm.from === arm.to ? "current" : "updated" : arm.verdict === "skip" ? "keep" : arm.verdict === "defer" ? "retry" : "failed";
2237
+ const note = kind === "updated" ? ACTIVATION_NOTE[arm.surface] ?? null : kind === "keep" ? "above target" : kind === "retry" ? retryHint(arm.detail) : null;
2238
+ return { name: arm.surface === "updater" ? "hub" : arm.surface, from: arm.from, to: arm.to, kind, note, detail: arm.detail };
2239
+ }
2240
+ function statusText(kind, dryRun) {
2241
+ if (dryRun && kind === "updated") return "would update";
2242
+ switch (kind) {
2243
+ case "updated":
2244
+ return "updated";
2245
+ case "current":
2246
+ return "current";
2247
+ case "keep":
2248
+ return "keep";
2249
+ case "retry":
2250
+ return "retry";
2251
+ case "failed":
2252
+ return "failed";
2253
+ case "pending":
2254
+ return "pending";
2255
+ }
2256
+ }
2257
+ function glyphFor(kind) {
2258
+ switch (kind) {
2259
+ case "updated":
2260
+ return "\u2713";
2261
+ case "current":
2262
+ return "\u2713";
2263
+ case "keep":
2264
+ return "\u2713";
2265
+ case "retry":
2266
+ return "\u21BB";
2267
+ case "failed":
2268
+ return "\u2717";
2269
+ case "pending":
2270
+ return "\xB7";
2271
+ }
2272
+ }
2273
+ function colorFor(kind) {
2274
+ return kind === "failed" ? RED : kind === "retry" ? YELLOW : GREEN;
2275
+ }
2276
+ function wrapWords(text, available) {
2277
+ if (available < 1) return [text];
2278
+ const lines = [];
2279
+ let current = "";
2280
+ for (const word of text.split(/\s+/).filter(Boolean)) {
2281
+ const piece = word.length > available ? word.slice(0, Math.max(1, available - 1)) + "\u2026" : word;
2282
+ if (!current) current = piece;
2283
+ else if (current.length + 1 + piece.length <= available) current += " " + piece;
2284
+ else {
2285
+ lines.push(current);
2286
+ current = piece;
2287
+ }
2288
+ }
2289
+ if (current) lines.push(current);
2290
+ return lines.length ? lines : [""];
2291
+ }
2292
+ function noteLines(prefix, note, width) {
2293
+ const used = stripAnsi(prefix).length;
2294
+ const available = width - used - 1;
2295
+ const indent = available >= MIN_NOTE_SPACE ? " ".repeat(used + 1) : " ".repeat(Math.min(used + 1, Math.max(2, width - MIN_NOTE_SPACE)));
2296
+ const room = available >= MIN_NOTE_SPACE ? available : width - indent.length;
2297
+ const parts = wrapWords(note, room);
2298
+ const first = available >= MIN_NOTE_SPACE ? prefix + " " + parts[0] : prefix.replace(/\s+$/, "");
2299
+ const lines = [first];
2300
+ for (const part of parts.slice(available >= MIN_NOTE_SPACE ? 1 : 0)) lines.push(indent + part);
2301
+ return lines;
2302
+ }
2303
+ function stripAnsi(text) {
2304
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
2305
+ }
2306
+ function collectRows(summary) {
2307
+ const rows = [];
2308
+ if (summary.cli) rows.push(classify(summary.cli));
2309
+ for (const arm of summary.plugins) rows.push(classify(arm));
2310
+ for (const surface of summary.surfaces) {
2311
+ if (surface.present && surface.action === "arm-pending") {
2312
+ rows.push({ name: surface.id, from: null, to: summary.target ?? "?", kind: "pending", note: "arm not landed", detail: "" });
2313
+ }
2314
+ }
2315
+ if (summary.updater) rows.push(classify(summary.updater));
2316
+ return rows;
2317
+ }
2318
+ function headline(detail, width) {
2319
+ const first = (detail ?? "").split(/(?<=[.;:])\s/)[0]?.replace(/\s+/g, " ").trim() ?? "";
2320
+ return wrapWords(first || "blocked", Math.max(10, width - 4))[0];
2321
+ }
2322
+ function updateHeader(target, dryRun = false) {
2323
+ return dryRun ? `MMI Hub \u2014 update plan to ${target} (dry run)` : `MMI Hub \u2014 updating to ${target} (verified)`;
2324
+ }
2325
+ function renderRowLines(row, ctx) {
2326
+ const paint = (kind, text) => ctx.color ? colorFor(kind) + text + RESET : text;
2327
+ const versions = row.from !== null && row.from !== row.to ? `${row.from} \u2192 ${row.to}` : `${row.to}`;
2328
+ const glyph = ctx.tty ? glyphFor(row.kind) + (row.kind === "updated" || row.kind === "retry" ? "" : " ") : "";
2329
+ const status = statusText(row.kind, ctx.dryRun).padEnd(ctx.statusWidth);
2330
+ const prefix = " " + row.name.padEnd(NAME_WIDTH) + " " + versions.padEnd(VERSION_WIDTH) + " " + paint(row.kind, glyph + status);
2331
+ return row.note ? noteLines(prefix, row.note, ctx.width) : [prefix.replace(/\s+$/, "")];
2332
+ }
2333
+ function progressiveStatusWidth(dryRun) {
2334
+ return Math.max((dryRun ? "would update" : "updated").length, 7);
2335
+ }
2336
+ function progressiveRow(arm, options = {}) {
2337
+ const width = Math.max(40, options.width ?? DEFAULT_WIDTH);
2338
+ const tty = options.tty ?? false;
2339
+ const color = tty && (options.color ?? true);
2340
+ const dryRun = options.dryRun ?? false;
2341
+ return renderRowLines(classify(arm), { tty, color, dryRun, width, statusWidth: progressiveStatusWidth(dryRun) }).join("\n");
2342
+ }
2343
+ function reportFooterLines(summary, options = {}) {
2344
+ const width = Math.max(40, options.width ?? DEFAULT_WIDTH);
2345
+ const tty = options.tty ?? false;
2346
+ const color = tty && (options.color ?? true);
2347
+ const dryRun = options.dryRun ?? false;
2348
+ const journalPath = options.journalPath ?? "~/.mmi/updater/journal.jsonl";
2349
+ const paint = (kind, text) => color ? colorFor(kind) + text + RESET : text;
2350
+ const rows = collectRows(summary);
2351
+ const counts = {
2352
+ updated: rows.filter((row) => row.kind === "updated").length,
2353
+ retry: rows.filter((row) => row.kind === "retry").length,
2354
+ failed: rows.filter((row) => row.kind === "failed").length,
2355
+ total: rows.length
2356
+ };
2357
+ 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` : "");
2358
+ const out = ["", " " + footer];
2359
+ if (counts.failed) {
2360
+ out.push("");
2361
+ for (const row of rows.filter((row2) => row2.kind === "failed")) {
2362
+ const mark = tty ? paint("failed", "\u2717 ") : "";
2363
+ out.push(...noteLines(" " + mark + row.name, failConsequence(row.detail), width));
2364
+ out.push(...noteLines(" fix: ", `mmi-cli doctor, then mmi-hub update`, width));
2365
+ out.push(...noteLines(" log: ", journalPath, width));
2366
+ }
2367
+ }
2368
+ return out;
2369
+ }
2370
+ function formatReconcileReport(summary, options = {}) {
2371
+ const width = Math.max(40, options.width ?? DEFAULT_WIDTH);
2372
+ const tty = options.tty ?? false;
2373
+ const color = tty && (options.color ?? true);
2374
+ const dryRun = options.dryRun ?? false;
2375
+ const out = [];
2376
+ const paint = (kind, text) => color ? colorFor(kind) + text + RESET : text;
2377
+ const rows = collectRows(summary);
2378
+ const counts = {
2379
+ updated: rows.filter((row) => row.kind === "updated").length,
2380
+ current: rows.filter((row) => row.kind === "current" || row.kind === "keep").length,
2381
+ retry: rows.filter((row) => row.kind === "retry").length,
2382
+ failed: rows.filter((row) => row.kind === "failed").length,
2383
+ total: rows.length
2384
+ };
2385
+ if (!summary.target) {
2386
+ if (summary.exit >= 1 && summary.detail) {
2387
+ out.push("MMI Hub \u2014 cannot update now");
2388
+ out.push(...noteLines(" " + paint("failed", "blocked"), headline(summary.detail, width), width));
2389
+ out.push(...noteLines(" fix:", retryHint(summary.detail), width));
2390
+ } else {
2391
+ out.push("MMI Hub \u2014 release still publishing, nothing changed (hourly retry)");
2392
+ }
2393
+ return out.join("\n") + "\n";
2394
+ }
2395
+ const allCurrent = rows.length > 0 && counts.updated === 0 && counts.retry === 0 && counts.failed === 0 && rows.every((row) => row.kind === "current" || row.kind === "keep");
2396
+ if (allCurrent && !dryRun) {
2397
+ out.push(`MMI Hub \u2014 MMI is up to date (${summary.target} \xB7 verified)`);
2398
+ return out.join("\n") + "\n";
2399
+ }
2400
+ out.push(updateHeader(summary.target, dryRun));
2401
+ out.push("");
2402
+ const statusWidth = Math.max(...rows.map((row) => statusText(row.kind, dryRun).length), 7);
2403
+ for (const row of rows) out.push(...renderRowLines(row, { tty, color, dryRun, width, statusWidth }));
2404
+ out.push(...reportFooterLines(summary, options));
2405
+ return out.join("\n") + "\n";
2406
+ }
2407
+
2408
+ // src/spinner.ts
2409
+ var import_node_worker_threads = require("node:worker_threads");
2410
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2411
+ var WORKER_SRC = `
2412
+ const { parentPort, workerData } = require('node:worker_threads');
2413
+ const frames = ${JSON.stringify(FRAMES)};
2414
+ const control = new Int32Array(workerData.sab);
2415
+ let frame = 0, label = '', timer = null;
2416
+ function tick() {
2417
+ if (Atomics.load(control, 0) === 1) return; // paused by the main thread
2418
+ if (!label) return;
2419
+ process.stderr.write('\\r' + frames[frame++ % frames.length] + ' ' + label + '\\x1b[K');
2420
+ }
2421
+ parentPort.on('message', (message) => {
2422
+ if (message && message.type === 'label') { label = message.label; if (!timer) timer = setInterval(tick, 80); }
2423
+ else if (message === 'stop') { if (timer) clearInterval(timer); process.stderr.write('\\r\\x1b[K'); process.exit(0); }
2424
+ });
2425
+ `;
2426
+ var NOOP = { label() {
2427
+ }, persist() {
2428
+ }, stop() {
2429
+ }, active: false };
2430
+ function startSpinner(enabled) {
2431
+ if (!enabled) return NOOP;
2432
+ let worker;
2433
+ let control;
2434
+ try {
2435
+ const sab = new SharedArrayBuffer(4);
2436
+ control = new Int32Array(sab);
2437
+ worker = new import_node_worker_threads.Worker(WORKER_SRC, { eval: true, workerData: { sab } });
2438
+ worker.unref();
2439
+ worker.on("error", () => {
2440
+ });
2441
+ } catch {
2442
+ return NOOP;
2443
+ }
2444
+ let stopped = false;
2445
+ return {
2446
+ active: true,
2447
+ label: (text) => {
2448
+ if (!stopped) worker.postMessage({ type: "label", label: text });
2449
+ },
2450
+ persist: (line) => {
2451
+ Atomics.store(control, 0, 1);
2452
+ process.stderr.write("\r\x1B[K");
2453
+ process.stdout.write(line + "\n");
2454
+ Atomics.store(control, 0, 0);
2455
+ },
2456
+ stop: () => {
2457
+ if (stopped) return;
2458
+ stopped = true;
2459
+ worker.postMessage("stop");
2460
+ }
2461
+ };
2462
+ }
2463
+
2148
2464
  // src/index.ts
2149
2465
  function ownVersion() {
2150
2466
  try {
@@ -2159,43 +2475,66 @@ var HELP = `mmi-hub ${ownVersion()} \u2014 install and maintain MMI tooling
2159
2475
  and actionable failures (default; never heals)
2160
2476
  install [--json] converge the CLI and every present host surface now, then enable
2161
2477
  hourly automatic updates
2162
- update [--dry-run] [--json] converge immediately to the newest gated release
2478
+ update [--dry-run] [--json] [--verbose]
2479
+ converge immediately to the newest gated release: one line per
2480
+ surface, a short reason only when not ok; --verbose traces steps
2163
2481
  autoupdate on|off idempotently enable or disable hourly convergence; off keeps tools
2164
2482
  --version print the Hub maintenance package version
2165
2483
 
2166
2484
  State remains under $MMI_UPDATER_HOME (default ~/.mmi/updater), and the Windows task name remains
2167
2485
  "MMI Fleet Updater", so existing installations continue in place.
2168
2486
  Status performs no npm install, host repair, scheduler write, state mkdir, or repository cleanup.`;
2169
- function printReconcile(summary, json) {
2487
+ function printReconcile(summary, json, dryRun = false) {
2170
2488
  if (json) {
2171
2489
  process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
2172
2490
  return;
2173
2491
  }
2174
- const target = summary.target ?? "none (deferred)";
2175
- process.stdout.write(`mmi-hub: target ${target}
2176
- `);
2177
- if (summary.cli) process.stdout.write(` cli: ${summary.cli.verdict} \u2014 ${summary.cli.detail}
2178
- `);
2179
- for (const arm of summary.plugins) process.stdout.write(` ${arm.surface}: ${arm.verdict} \u2014 ${arm.detail}
2180
- `);
2181
- if (summary.updater) process.stdout.write(` hub: ${summary.updater.verdict} \u2014 ${summary.updater.detail}
2182
- `);
2183
- for (const row of summary.reap.filter((entry) => entry.verdict !== "skip")) {
2184
- process.stdout.write(` maintenance ${row.category}: ${row.verdict} \u2014 ${row.detail}
2185
- `);
2186
- }
2187
- const armed = new Set(summary.plugins.map((arm) => arm.surface));
2188
- for (const surface of summary.surfaces.filter((surface2) => !armed.has(surface2.id))) {
2189
- process.stdout.write(` ${surface.id}: ${surface.present ? "present" : "absent"} (${surface.action})
2190
- `);
2191
- }
2192
- }
2193
- function runUpdate(args) {
2492
+ process.stdout.write(formatReconcileReport(summary, {
2493
+ tty: Boolean(process.stdout.isTTY),
2494
+ color: !process.env.NO_COLOR,
2495
+ width: process.stdout.columns ?? 100,
2496
+ dryRun,
2497
+ journalPath: statePaths(process.env).journalPath
2498
+ }));
2499
+ }
2500
+ function runUpdate(args, json) {
2501
+ const dryRun = args.includes("--dry-run");
2502
+ const verbose = args.includes("--verbose");
2194
2503
  const narrate = (line) => {
2195
2504
  process.stderr.write(`mmi-hub: ${line}
2196
2505
  `);
2197
2506
  };
2198
- return reconcile({ dryRun: args.includes("--dry-run"), narrate });
2507
+ const progressive = !json && !verbose && Boolean(process.stderr.isTTY);
2508
+ if (!progressive) {
2509
+ return { summary: reconcile({ dryRun, narrate: verbose ? narrate : void 0 }), rendered: false };
2510
+ }
2511
+ const reportOptions = {
2512
+ tty: Boolean(process.stdout.isTTY),
2513
+ color: !process.env.NO_COLOR,
2514
+ width: process.stdout.columns ?? 100,
2515
+ dryRun,
2516
+ journalPath: statePaths(process.env).journalPath
2517
+ };
2518
+ const spinner = startSpinner(true);
2519
+ let headerEmitted = false;
2520
+ let summary;
2521
+ try {
2522
+ summary = reconcile({
2523
+ dryRun,
2524
+ onPhase: (line) => spinner.label(line),
2525
+ onTargetResolved: (target) => {
2526
+ spinner.persist(updateHeader(target, dryRun));
2527
+ spinner.persist("");
2528
+ headerEmitted = true;
2529
+ },
2530
+ onArm: (arm) => spinner.persist(progressiveRow(arm, reportOptions))
2531
+ });
2532
+ } finally {
2533
+ spinner.stop();
2534
+ }
2535
+ if (headerEmitted) process.stdout.write(reportFooterLines(summary, reportOptions).join("\n") + "\n");
2536
+ else process.stdout.write(formatReconcileReport(summary, reportOptions));
2537
+ return { summary, rendered: true };
2199
2538
  }
2200
2539
  function main(args = process.argv.slice(2)) {
2201
2540
  if (args.includes("--version")) {
@@ -2214,18 +2553,19 @@ function main(args = process.argv.slice(2)) {
2214
2553
  return status.failures.length ? 1 : 0;
2215
2554
  }
2216
2555
  if (command === "update") {
2217
- const summary = runUpdate(args);
2218
- printReconcile(summary, json);
2556
+ const { summary, rendered } = runUpdate(args, json);
2557
+ if (!rendered) printReconcile(summary, json, args.includes("--dry-run"));
2219
2558
  return summary.exit;
2220
2559
  }
2221
2560
  if (command === "install") {
2222
- const summary = runUpdate(args);
2561
+ const { summary, rendered } = runUpdate(args, json);
2223
2562
  const schedule = installTask();
2224
2563
  if (json) {
2225
2564
  process.stdout.write(JSON.stringify({ convergence: summary, autoupdate: schedule }, null, 2) + "\n");
2226
2565
  } else {
2227
- printReconcile(summary, false);
2228
- process.stdout.write(`mmi-hub install: ${schedule.detail}
2566
+ if (!rendered) printReconcile(summary, false, args.includes("--dry-run"));
2567
+ process.stdout.write(schedule.ok ? ` Auto-update on \u2014 hourly task "MMI Fleet Updater".
2568
+ ` : ` Auto-update NOT enabled \u2014 ${schedule.detail}
2229
2569
  `);
2230
2570
  }
2231
2571
  return Math.max(summary.exit, schedule.ok ? 0 : 1);
@@ -2251,6 +2591,8 @@ if (typeof require !== "undefined" && require.main === module) {
2251
2591
  // Annotate the CommonJS export names for ESM import in node:
2252
2592
  0 && (module.exports = {
2253
2593
  enumerateSurfaces,
2594
+ failConsequence,
2595
+ formatReconcileReport,
2254
2596
  hermesArm,
2255
2597
  hermesHome,
2256
2598
  hubStatus,
@@ -2259,6 +2601,12 @@ if (typeof require !== "undefined" && require.main === module) {
2259
2601
  main,
2260
2602
  ownVersion,
2261
2603
  probeHermesInstallation,
2604
+ progressiveRow,
2605
+ reportFooterLines,
2606
+ retryHint,
2262
2607
  schedulerStatus,
2263
- taskXml
2608
+ startSpinner,
2609
+ taskXml,
2610
+ updateHeader,
2611
+ wrapWords
2264
2612
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/hub",
3
- "version": "3.139.10",
3
+ "version": "3.139.12",
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",