@mutmutco/hub 3.139.11 → 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.
- package/README.md +15 -0
- package/dist/index.cjs +383 -35
- 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
|
-
|
|
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
|
-
|
|
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))
|
|
1376
|
-
|
|
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
|
-
|
|
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) =>
|
|
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]
|
|
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
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
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
|
-
|
|
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(`
|
|
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
|
-
|
|
2608
|
+
startSpinner,
|
|
2609
|
+
taskXml,
|
|
2610
|
+
updateHeader,
|
|
2611
|
+
wrapWords
|
|
2264
2612
|
});
|
package/package.json
CHANGED