@mutmutco/hub 4.4.6 → 4.4.9
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/dist/index.cjs +486 -243
- package/package.json +4 -1
package/dist/index.cjs
CHANGED
|
@@ -22,12 +22,16 @@ 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,
|
|
31
|
+
autoupdateLine: () => autoupdateLine,
|
|
29
32
|
brandLine: () => brandLine,
|
|
30
33
|
buildWatcherCommand: () => buildWatcherCommand,
|
|
34
|
+
checkAtomicity: () => checkAtomicity,
|
|
31
35
|
checklistLine: () => checklistLine,
|
|
32
36
|
claudeArm: () => claudeArm,
|
|
33
37
|
claudeHostEvidence: () => claudeHostEvidence,
|
|
@@ -67,7 +71,6 @@ __export(index_exports, {
|
|
|
67
71
|
progressiveRow: () => progressiveRow,
|
|
68
72
|
readLaunchTarget: () => readLaunchTarget,
|
|
69
73
|
readPending: () => readPending,
|
|
70
|
-
receipt: () => receipt,
|
|
71
74
|
receiptText: () => receiptText,
|
|
72
75
|
reconcile: () => reconcile,
|
|
73
76
|
reconcileCursorUserHooks: () => reconcileCursorUserHooks,
|
|
@@ -82,17 +85,15 @@ __export(index_exports, {
|
|
|
82
85
|
sharedDocLockTimeoutReceipt: () => sharedDocLockTimeoutReceipt,
|
|
83
86
|
shippedTargetFrom: () => shippedTargetFrom,
|
|
84
87
|
shouldHandOff: () => shouldHandOff,
|
|
85
|
-
signOff: () => signOff,
|
|
86
|
-
startSpinner: () => startSpinner,
|
|
87
88
|
surfaceArmPlan: () => surfaceArmPlan,
|
|
88
89
|
systemdServiceUnit: () => systemdServiceUnit,
|
|
89
90
|
systemdTimerUnit: () => systemdTimerUnit,
|
|
90
91
|
taskXml: () => taskXml,
|
|
91
92
|
updateHeader: () => updateHeader,
|
|
93
|
+
updaterFace: () => updaterFace,
|
|
92
94
|
watcherLockPath: () => watcherLockPath,
|
|
93
|
-
welcome: () => welcome,
|
|
94
95
|
withSharedDocLock: () => withSharedDocLock,
|
|
95
|
-
wrapWords: () =>
|
|
96
|
+
wrapWords: () => wrapWords2,
|
|
96
97
|
writeLaunchTarget: () => writeLaunchTarget,
|
|
97
98
|
writePending: () => writePending
|
|
98
99
|
});
|
|
@@ -101,6 +102,207 @@ var import_node_child_process9 = require("node:child_process");
|
|
|
101
102
|
var import_node_fs18 = require("node:fs");
|
|
102
103
|
var import_node_path13 = require("node:path");
|
|
103
104
|
|
|
105
|
+
// node_modules/@mutmutco/installer-face/dist/index.js
|
|
106
|
+
var PRODUCTS = Object.freeze({
|
|
107
|
+
"mm-strategy": Object.freeze({
|
|
108
|
+
name: "MM Strategy",
|
|
109
|
+
accent: "38;2;249;115;22",
|
|
110
|
+
warm: "Welcome. Let's set up MM Strategy \u2014 about a minute."
|
|
111
|
+
}),
|
|
112
|
+
"mmi-hub": Object.freeze({
|
|
113
|
+
name: "mmi-hub",
|
|
114
|
+
accent: "38;2;125;211;252",
|
|
115
|
+
warm: "Welcome back. Checking your surfaces\u2026"
|
|
116
|
+
}),
|
|
117
|
+
"jerv-hub": Object.freeze({
|
|
118
|
+
name: "jerv-hub",
|
|
119
|
+
accent: "38;2;248;113;113",
|
|
120
|
+
warm: "Welcome back. Checking your surfaces\u2026"
|
|
121
|
+
}),
|
|
122
|
+
jervcode: Object.freeze({
|
|
123
|
+
name: "JervCode",
|
|
124
|
+
accent: "38;2;192;132;252",
|
|
125
|
+
warm: "Welcome back. Keeping JervCode current\u2026"
|
|
126
|
+
})
|
|
127
|
+
});
|
|
128
|
+
function identityFor(product) {
|
|
129
|
+
const identity = PRODUCTS[product];
|
|
130
|
+
if (!identity) {
|
|
131
|
+
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(", ")})`);
|
|
132
|
+
}
|
|
133
|
+
return identity;
|
|
134
|
+
}
|
|
135
|
+
var GLYPH = Object.freeze({
|
|
136
|
+
diamond: "\u25C6",
|
|
137
|
+
hollow: "\u25C7",
|
|
138
|
+
bar: "\u2502",
|
|
139
|
+
check: "\u2714",
|
|
140
|
+
cross: "\u2716",
|
|
141
|
+
dot: "\u25CF",
|
|
142
|
+
boxTop: "\u256D",
|
|
143
|
+
boxTopEnd: "\u256E",
|
|
144
|
+
boxBottom: "\u2570",
|
|
145
|
+
boxBottomEnd: "\u256F",
|
|
146
|
+
rule: "\u2500"
|
|
147
|
+
});
|
|
148
|
+
var PALETTE = Object.freeze({
|
|
149
|
+
green: "38;2;31;209;138",
|
|
150
|
+
ink: "38;2;237;237;237",
|
|
151
|
+
muted: "38;2;150;150;150",
|
|
152
|
+
line: "38;2;42;42;42"
|
|
153
|
+
});
|
|
154
|
+
var TITLE_COLUMN = 6;
|
|
155
|
+
var ANSI = /\u001b\[[0-9;]*m/g;
|
|
156
|
+
function visibleWidth(text) {
|
|
157
|
+
return [...String(text).replace(ANSI, "")].length;
|
|
158
|
+
}
|
|
159
|
+
function faceWidth(columns) {
|
|
160
|
+
const raw = Number(columns);
|
|
161
|
+
return Math.max(40, Math.min(100, Number.isFinite(raw) && raw > 0 ? raw : 100));
|
|
162
|
+
}
|
|
163
|
+
function wrapWords(text, width) {
|
|
164
|
+
const limit = Math.max(8, Math.floor(width));
|
|
165
|
+
const lines = [];
|
|
166
|
+
let line = "";
|
|
167
|
+
for (const word of String(text).split(/\s+/u).filter(Boolean)) {
|
|
168
|
+
let token = word;
|
|
169
|
+
while (visibleWidth(token) > limit) {
|
|
170
|
+
if (line) {
|
|
171
|
+
lines.push(line);
|
|
172
|
+
line = "";
|
|
173
|
+
}
|
|
174
|
+
lines.push([...token].slice(0, limit).join(""));
|
|
175
|
+
token = [...token].slice(limit).join("");
|
|
176
|
+
}
|
|
177
|
+
if (!line) line = token;
|
|
178
|
+
else if (visibleWidth(line) + 1 + visibleWidth(token) <= limit) line += ` ${token}`;
|
|
179
|
+
else {
|
|
180
|
+
lines.push(line);
|
|
181
|
+
line = token;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (line) lines.push(line);
|
|
185
|
+
return lines.length ? lines : [""];
|
|
186
|
+
}
|
|
187
|
+
function createFace({ product, color = false, columns }) {
|
|
188
|
+
const identity = identityFor(product);
|
|
189
|
+
const width = faceWidth(columns);
|
|
190
|
+
const paint = (sgr, text) => color ? `\x1B[${sgr}m${text}\x1B[0m` : String(text);
|
|
191
|
+
const bar = () => paint(PALETTE.muted, GLYPH.bar);
|
|
192
|
+
const indent = " ".repeat(TITLE_COLUMN - 1);
|
|
193
|
+
const welcome = () => [
|
|
194
|
+
`${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
|
|
195
|
+
bar(),
|
|
196
|
+
`${bar()} ${identity.warm}`,
|
|
197
|
+
bar()
|
|
198
|
+
];
|
|
199
|
+
const step = (title, seconds = null, kind = "ok") => {
|
|
200
|
+
const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.hollow) : paint(PALETTE.green, GLYPH.check);
|
|
201
|
+
const time = seconds === null || seconds === void 0 ? "" : paint(PALETTE.muted, `${Math.max(0, Math.round(seconds))}s`);
|
|
202
|
+
const column = Math.min(44, Math.max(0, width - 8));
|
|
203
|
+
const reserved = time ? 6 : 0;
|
|
204
|
+
const [first, ...rest] = wrapWords(title, width - TITLE_COLUMN - reserved);
|
|
205
|
+
const head = `${bar()} ${glyph} ${first}`;
|
|
206
|
+
const pad = Math.max(1, column - visibleWidth(head));
|
|
207
|
+
return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
|
|
208
|
+
};
|
|
209
|
+
const relay = (text) => String(text).split("\n").map((line) => line.trim() === "" ? bar() : `${bar()}${indent}${line}`).join("\n");
|
|
210
|
+
const receipt = (lines) => {
|
|
211
|
+
const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
|
|
212
|
+
const line = String(raw);
|
|
213
|
+
if (visibleWidth(line) <= width - 6) return [line];
|
|
214
|
+
const lead = /^\s*/u.exec(line)?.[0] ?? "";
|
|
215
|
+
return wrapWords(line, width - 6 - lead.length).map((part) => `${lead}${part}`);
|
|
216
|
+
});
|
|
217
|
+
const content = Math.min(width - 6, Math.max(...body.map(visibleWidth)));
|
|
218
|
+
const rule = paint(identity.accent, GLYPH.rule.repeat(content + 4));
|
|
219
|
+
const frame = (left, right) => `${paint(identity.accent, left)}${rule}${paint(identity.accent, right)}`;
|
|
220
|
+
const bodyRow = (line) => line.startsWith(GLYPH.check) ? `${paint(PALETTE.green, GLYPH.check)}${line.slice(1)}` : line;
|
|
221
|
+
return [
|
|
222
|
+
frame(GLYPH.boxTop, GLYPH.boxTopEnd),
|
|
223
|
+
...body.map((line) => `${paint(identity.accent, GLYPH.bar)} ${bodyRow(line)}${" ".repeat(Math.max(0, content - visibleWidth(line)))} ${paint(identity.accent, GLYPH.bar)}`),
|
|
224
|
+
frame(GLYPH.boxBottom, GLYPH.boxBottomEnd)
|
|
225
|
+
];
|
|
226
|
+
};
|
|
227
|
+
const signOff = () => `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
|
|
228
|
+
const refusal = (message3) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message3}`;
|
|
229
|
+
return { identity, width, welcome, step, relay, receipt, signOff, refusal, paint };
|
|
230
|
+
}
|
|
231
|
+
var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
|
|
232
|
+
var SEPARATOR = "\xB7";
|
|
233
|
+
var subjectOf = (row) => row.aspect ? `${row.surface} ${row.aspect}` : row.surface;
|
|
234
|
+
var groupKey = (row) => `${subjectOf(row)}\0${row.state}\0${row.mark ?? ""}\0${row.note ?? ""}`;
|
|
235
|
+
function renderRows(face, rows) {
|
|
236
|
+
if (rows.length === 0) return [];
|
|
237
|
+
const groups = /* @__PURE__ */ new Map();
|
|
238
|
+
for (const row of rows) {
|
|
239
|
+
const key = groupKey(row);
|
|
240
|
+
const found = groups.get(key);
|
|
241
|
+
if (found) {
|
|
242
|
+
if (row.instance) found.instances.push(row.instance);
|
|
243
|
+
} else {
|
|
244
|
+
groups.set(key, { row, instances: row.instance ? [row.instance] : [] });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const subjects = [...groups.values()].map(({ row, instances }) => instances.length > 1 ? subjectOf(row) : [subjectOf(row), instances[0]].filter(Boolean).join(` ${SEPARATOR} `));
|
|
248
|
+
const column = Math.min(Math.max(...subjects.map(visibleWidth)), Math.max(12, face.width - 24));
|
|
249
|
+
const lines = [];
|
|
250
|
+
for (const [index, { row, instances }] of [...groups.values()].entries()) {
|
|
251
|
+
const subject = subjects[index];
|
|
252
|
+
const tail2 = instances.length > 1 ? `${row.state} ${SEPARATOR} ${instances.join(", ")}` : row.state;
|
|
253
|
+
const body = `${clip(subject, column).padEnd(column)} ${row.mark ? `${markGlyph(face, row.mark)} ` : ""}${tail2}`;
|
|
254
|
+
lines.push(...face.relay(body).split("\n"));
|
|
255
|
+
if (row.note) {
|
|
256
|
+
for (const part of wrapWords(row.note, Math.max(16, face.width - TITLE_COLUMN))) {
|
|
257
|
+
lines.push(...face.relay(part).split("\n"));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return lines;
|
|
262
|
+
}
|
|
263
|
+
function markGlyph(face, kind) {
|
|
264
|
+
const glyph = kind === "fail" ? GLYPH.cross : kind === "note" ? GLYPH.hollow : GLYPH.check;
|
|
265
|
+
return face.paint(kind === "fail" ? face.identity.accent : kind === "note" ? "38;2;150;150;150" : "38;2;31;209;138", glyph);
|
|
266
|
+
}
|
|
267
|
+
function clip(text, width) {
|
|
268
|
+
if (visibleWidth(text) <= width) return text;
|
|
269
|
+
return `\u2026${[...text].slice(-(width - 1)).join("")}`;
|
|
270
|
+
}
|
|
271
|
+
var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
|
|
272
|
+
function createSpinner(face, { animate, stream = process.stderr, intervalMs = 90 }) {
|
|
273
|
+
let timer = null;
|
|
274
|
+
let text = "";
|
|
275
|
+
let frame = 0;
|
|
276
|
+
const clear = () => {
|
|
277
|
+
if (animate) stream.write("\r\x1B[2K");
|
|
278
|
+
};
|
|
279
|
+
const draw = () => {
|
|
280
|
+
if (!animate) return;
|
|
281
|
+
const line = face.step(text, null, "note").split("\n")[0].replace(GLYPH.hollow, FRAMES[frame % FRAMES.length]);
|
|
282
|
+
stream.write(`\r\x1B[2K${line}`);
|
|
283
|
+
frame += 1;
|
|
284
|
+
};
|
|
285
|
+
return {
|
|
286
|
+
start(next) {
|
|
287
|
+
text = next;
|
|
288
|
+
frame = 0;
|
|
289
|
+
draw();
|
|
290
|
+
if (animate && !timer) timer = setInterval(draw, intervalMs).unref();
|
|
291
|
+
},
|
|
292
|
+
say(next) {
|
|
293
|
+
text = next;
|
|
294
|
+
draw();
|
|
295
|
+
},
|
|
296
|
+
stop() {
|
|
297
|
+
if (timer) clearInterval(timer);
|
|
298
|
+
timer = null;
|
|
299
|
+
clear();
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
var SPINNER_FRAMES = Object.freeze([...FRAMES]);
|
|
304
|
+
var ALLOWED = new Set(Object.values(GLYPH));
|
|
305
|
+
|
|
104
306
|
// src/state.ts
|
|
105
307
|
var import_node_fs = require("node:fs");
|
|
106
308
|
var import_node_os = require("node:os");
|
|
@@ -165,11 +367,12 @@ function withLease(path, fn, waitMs = LEASE_WAIT_MS) {
|
|
|
165
367
|
for (; ; ) {
|
|
166
368
|
const attempt = tryAcquire(path);
|
|
167
369
|
if (typeof attempt === "object") {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
attempt.release();
|
|
370
|
+
const result = fn();
|
|
371
|
+
if (result && typeof result.then === "function") {
|
|
372
|
+
return result.finally(() => attempt.release());
|
|
172
373
|
}
|
|
374
|
+
attempt.release();
|
|
375
|
+
return result;
|
|
173
376
|
}
|
|
174
377
|
if (Date.now() >= deadline) {
|
|
175
378
|
throw new UpdaterBusy(`another updater holds ${path} (waited ${waitMs}ms) \u2014 deferring`);
|
|
@@ -188,23 +391,29 @@ function appendJournal(path, event) {
|
|
|
188
391
|
return false;
|
|
189
392
|
}
|
|
190
393
|
}
|
|
191
|
-
function journalHighWater(path, surface) {
|
|
394
|
+
function journalHighWater(path, surface, events) {
|
|
192
395
|
let high = null;
|
|
396
|
+
for (const event of events ?? parseJournal(path)) {
|
|
397
|
+
if (event.dryRun) continue;
|
|
398
|
+
if (event.surface === surface && event.verdict === "ok" && event.to && (!high || compareSemver(event.to, high) > 0)) {
|
|
399
|
+
high = event.to;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return high;
|
|
403
|
+
}
|
|
404
|
+
function parseJournal(path) {
|
|
405
|
+
const events = [];
|
|
193
406
|
try {
|
|
194
407
|
for (const line of (0, import_node_fs.readFileSync)(path, "utf8").split("\n")) {
|
|
195
408
|
if (!line.trim()) continue;
|
|
196
409
|
try {
|
|
197
|
-
|
|
198
|
-
if (event.dryRun) continue;
|
|
199
|
-
if (event.surface === surface && event.verdict === "ok" && event.to && (!high || compareSemver(event.to, high) > 0)) {
|
|
200
|
-
high = event.to;
|
|
201
|
-
}
|
|
410
|
+
events.push(JSON.parse(line));
|
|
202
411
|
} catch {
|
|
203
412
|
}
|
|
204
413
|
}
|
|
205
414
|
} catch {
|
|
206
415
|
}
|
|
207
|
-
return
|
|
416
|
+
return events;
|
|
208
417
|
}
|
|
209
418
|
function journalSurfaceRepeatedFail(path, surface, currentRun) {
|
|
210
419
|
let lastVerdict = null;
|
|
@@ -255,6 +464,9 @@ function fileExists(path) {
|
|
|
255
464
|
return (0, import_node_fs.existsSync)(path);
|
|
256
465
|
}
|
|
257
466
|
|
|
467
|
+
// src/reconcile.ts
|
|
468
|
+
var import_node_perf_hooks2 = require("node:perf_hooks");
|
|
469
|
+
|
|
258
470
|
// src/npm.ts
|
|
259
471
|
var import_node_child_process = require("node:child_process");
|
|
260
472
|
var import_node_fs2 = require("node:fs");
|
|
@@ -295,10 +507,40 @@ function runNpm(args, env = process.env, options = {}) {
|
|
|
295
507
|
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 });
|
|
296
508
|
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
297
509
|
}
|
|
510
|
+
function runNpmAsync(args, env = process.env, options = {}) {
|
|
511
|
+
const npm = resolveNpm(env);
|
|
512
|
+
const [file, argv, shell] = npm.kind === "node-script" ? [process.execPath, [npm.script, ...args], false] : [npm.command, args, process.platform === "win32"];
|
|
513
|
+
return new Promise((resolve) => {
|
|
514
|
+
let stdout = "";
|
|
515
|
+
let stderr = "";
|
|
516
|
+
let child;
|
|
517
|
+
try {
|
|
518
|
+
child = (0, import_node_child_process.spawn)(file, argv, { env, cwd: options.cwd, windowsHide: true, shell });
|
|
519
|
+
} catch (error) {
|
|
520
|
+
resolve({ status: null, stdout, stderr: String(error?.message ?? error) });
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
child.stdout?.setEncoding("utf8");
|
|
524
|
+
child.stderr?.setEncoding("utf8");
|
|
525
|
+
child.stdout?.on("data", (chunk) => {
|
|
526
|
+
stdout += chunk;
|
|
527
|
+
});
|
|
528
|
+
child.stderr?.on("data", (chunk) => {
|
|
529
|
+
stderr += chunk;
|
|
530
|
+
});
|
|
531
|
+
child.on("error", (error) => resolve({ status: null, stdout, stderr: String(error?.message ?? error) }));
|
|
532
|
+
child.on("close", (status) => resolve({ status, stdout, stderr }));
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
var globalRootMemo = /* @__PURE__ */ new WeakMap();
|
|
298
536
|
function globalRoot(env = process.env) {
|
|
537
|
+
const key = env;
|
|
538
|
+
const memoized = globalRootMemo.get(key);
|
|
539
|
+
if (memoized !== void 0) return memoized;
|
|
299
540
|
const root = runNpm(["root", "-g"], env);
|
|
300
|
-
|
|
301
|
-
|
|
541
|
+
const resolved = root.status !== 0 || !root.stdout.trim() ? null : root.stdout.trim();
|
|
542
|
+
globalRootMemo.set(key, resolved);
|
|
543
|
+
return resolved;
|
|
302
544
|
}
|
|
303
545
|
function npmView(spec, field, env = process.env) {
|
|
304
546
|
const args = ["view", spec, "--json"];
|
|
@@ -318,8 +560,15 @@ function parseNpmDistIntegrity(value) {
|
|
|
318
560
|
return typeof only === "string" && only.length > 0 ? only : null;
|
|
319
561
|
}
|
|
320
562
|
var npmStringField = parseNpmDistIntegrity;
|
|
321
|
-
function
|
|
322
|
-
|
|
563
|
+
async function npmDistIntegrityAsync(spec, env = process.env) {
|
|
564
|
+
const args = ["view", spec, "dist.integrity", "--json"];
|
|
565
|
+
const result = await runNpmAsync(args, env);
|
|
566
|
+
if (result.status !== 0 || !result.stdout.trim()) return null;
|
|
567
|
+
try {
|
|
568
|
+
return parseNpmDistIntegrity(JSON.parse(result.stdout));
|
|
569
|
+
} catch {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
323
572
|
}
|
|
324
573
|
function npmVersions(packageName, env = process.env) {
|
|
325
574
|
const versions = npmView(packageName, "versions", env);
|
|
@@ -399,38 +648,60 @@ function gateCandidate(cachePath, repoUrl, candidate) {
|
|
|
399
648
|
}
|
|
400
649
|
return { ok: true, tagCommit: commit, bom };
|
|
401
650
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
651
|
+
var ATOMICITY_CONCURRENCY = 4;
|
|
652
|
+
async function mapBounded(items, bound, fn) {
|
|
653
|
+
const results = new Array(items.length);
|
|
654
|
+
let next = 0;
|
|
655
|
+
const workers = Array.from({ length: Math.min(bound, items.length) }, async () => {
|
|
656
|
+
for (; ; ) {
|
|
657
|
+
const index = next++;
|
|
658
|
+
if (index >= items.length) return;
|
|
659
|
+
results[index] = await fn(items[index], index);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
await Promise.all(workers);
|
|
663
|
+
return results;
|
|
664
|
+
}
|
|
665
|
+
async function checkAtomicity(cachePath, tagCommit, bom, candidate, npm) {
|
|
666
|
+
const bomRows = npmPackRows(bom);
|
|
667
|
+
const verdicts = new Array(bomRows.length).fill(null);
|
|
668
|
+
const pending = [];
|
|
669
|
+
bomRows.forEach((row, index) => {
|
|
405
670
|
if (row.version !== candidate) {
|
|
406
|
-
|
|
407
|
-
|
|
671
|
+
verdicts[index] = { id: row.id, ok: false, detail: `BOM row at ${row.version}, candidate ${candidate}` };
|
|
672
|
+
return;
|
|
408
673
|
}
|
|
409
674
|
const manifestText = showFile(cachePath, tagCommit, `${row.identity.packagePath}/package.json`);
|
|
410
675
|
let name;
|
|
411
|
-
let
|
|
676
|
+
let manifestVersion2;
|
|
412
677
|
try {
|
|
413
678
|
const manifest2 = JSON.parse(manifestText ?? "");
|
|
414
679
|
name = manifest2.name;
|
|
415
|
-
|
|
680
|
+
manifestVersion2 = manifest2.version;
|
|
416
681
|
} catch {
|
|
417
682
|
}
|
|
418
|
-
if (!name ||
|
|
419
|
-
|
|
420
|
-
|
|
683
|
+
if (!name || manifestVersion2 !== candidate) {
|
|
684
|
+
verdicts[index] = { id: row.id, name, ok: false, detail: `package.json at ${row.identity.packagePath} is not stamped ${candidate}` };
|
|
685
|
+
return;
|
|
421
686
|
}
|
|
422
|
-
|
|
687
|
+
pending.push({ index, id: row.id, name });
|
|
688
|
+
});
|
|
689
|
+
const integrities = await mapBounded(pending, ATOMICITY_CONCURRENCY, (entry) => Promise.resolve(npm.distIntegrity(`${entry.name}@${candidate}`)));
|
|
690
|
+
pending.forEach((entry, i) => {
|
|
691
|
+
const integrity = integrities[i];
|
|
423
692
|
if (!integrity) {
|
|
424
|
-
|
|
425
|
-
|
|
693
|
+
verdicts[entry.index] = { id: entry.id, name: entry.name, ok: false, detail: `${entry.name}@${candidate} not on npm` };
|
|
694
|
+
return;
|
|
426
695
|
}
|
|
696
|
+
const row = bomRows[entry.index];
|
|
427
697
|
const legacySsri = /^sha\d+-/.test(row.identity.value ?? "");
|
|
428
698
|
if (legacySsri && integrity !== row.identity.value) {
|
|
429
|
-
|
|
430
|
-
|
|
699
|
+
verdicts[entry.index] = { id: entry.id, name: entry.name, ok: false, detail: `${entry.name}@${candidate} dist.integrity differs from BOM identity` };
|
|
700
|
+
return;
|
|
431
701
|
}
|
|
432
|
-
|
|
433
|
-
}
|
|
702
|
+
verdicts[entry.index] = { id: entry.id, name: entry.name, ok: true, detail: legacySsri ? `${entry.name}@${candidate} published, integrity matches` : `${entry.name}@${candidate} published` };
|
|
703
|
+
});
|
|
704
|
+
const rows = verdicts.map((row, index) => row ?? { id: bomRows[index].id, ok: false, detail: "atomicity probe produced no verdict" });
|
|
434
705
|
return { ok: rows.length > 0 && rows.every((row) => row.ok), rows };
|
|
435
706
|
}
|
|
436
707
|
|
|
@@ -606,6 +877,15 @@ function probeVersion(distPath) {
|
|
|
606
877
|
function tail(output) {
|
|
607
878
|
return output.split("\n").filter(Boolean).slice(-3).join(" | ");
|
|
608
879
|
}
|
|
880
|
+
function manifestVersion(packageRoot) {
|
|
881
|
+
if (!packageRoot) return null;
|
|
882
|
+
try {
|
|
883
|
+
const version = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(packageRoot, "package.json"), "utf8")).version;
|
|
884
|
+
return typeof version === "string" ? version : null;
|
|
885
|
+
} catch {
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
609
889
|
function stagePackage(options) {
|
|
610
890
|
const { surface, packageName, target, witness, paths, env } = options;
|
|
611
891
|
const stageRoot = (0, import_node_path4.join)(paths.staging, surface, target);
|
|
@@ -633,12 +913,17 @@ function npmGlobalArm(options) {
|
|
|
633
913
|
const { surface, packageName, distRel, target, dryRun, paths } = options;
|
|
634
914
|
const spec = `${packageName}@${target}`;
|
|
635
915
|
const result = (from, to, verdict, detail) => ({ surface, from, to, verdict, detail });
|
|
916
|
+
const highWater = journalHighWater(paths.journalPath, surface, options.journalEvents);
|
|
917
|
+
const localRoot = globalRoot(env);
|
|
918
|
+
const localVersion = localRoot ? manifestVersion((0, import_node_path4.join)(localRoot, ...packageName.split("/"))) : null;
|
|
919
|
+
if (localVersion === target && highWater === target) {
|
|
920
|
+
return result(localVersion, target, "ok", `already at target ${target} (installed manifest + journal high-water)`);
|
|
921
|
+
}
|
|
636
922
|
const dist = globalDist(env, packageName, distRel);
|
|
637
923
|
const installed = dist ? probeVersion(dist) : null;
|
|
638
924
|
if (installed && compareSemver(installed, target) > 0) {
|
|
639
925
|
return result(installed, installed, "skip", `installed ${installed} is above candidate ${target} (monotonic)`);
|
|
640
926
|
}
|
|
641
|
-
const highWater = journalHighWater(paths.journalPath, surface);
|
|
642
927
|
if (highWater && compareSemver(highWater, target) > 0) {
|
|
643
928
|
return result(installed, highWater, "skip", `journal high-water ${highWater} is above candidate ${target} (monotonic)`);
|
|
644
929
|
}
|
|
@@ -862,7 +1147,7 @@ function claudeArm(options) {
|
|
|
862
1147
|
if (from && compareSemver(from, target) > 0) {
|
|
863
1148
|
return result(from, from, "skip", `installed ${from} is above candidate ${target} (monotonic)`);
|
|
864
1149
|
}
|
|
865
|
-
const highWater = journalHighWater(paths.journalPath, "claude");
|
|
1150
|
+
const highWater = journalHighWater(paths.journalPath, "claude", options.journalEvents);
|
|
866
1151
|
if (highWater && compareSemver(highWater, target) > 0) {
|
|
867
1152
|
return result(from, highWater, "skip", `journal high-water ${highWater} is above candidate ${target} (monotonic)`);
|
|
868
1153
|
}
|
|
@@ -1422,7 +1707,7 @@ function cursorArm(options) {
|
|
|
1422
1707
|
if (installed && compareSemver(installed, target) > 0) {
|
|
1423
1708
|
return result(installed, installed, "skip", `installed ${installed} is above candidate ${target} (monotonic)`);
|
|
1424
1709
|
}
|
|
1425
|
-
const highWater = journalHighWater(paths.journalPath, "cursor");
|
|
1710
|
+
const highWater = journalHighWater(paths.journalPath, "cursor", options.journalEvents);
|
|
1426
1711
|
if (highWater && compareSemver(highWater, target) > 0) {
|
|
1427
1712
|
return result(installed, highWater, "skip", `journal high-water ${highWater} is above candidate ${target} (monotonic)`);
|
|
1428
1713
|
}
|
|
@@ -1566,11 +1851,11 @@ function parseListText(stdout) {
|
|
|
1566
1851
|
}
|
|
1567
1852
|
function probeJervcodeInstallation(env = process.env, runner = runHostCommand) {
|
|
1568
1853
|
const manifestLocation = materializedManifestLocation(env);
|
|
1569
|
-
const
|
|
1854
|
+
const manifestVersion2 = installedVersion(env);
|
|
1570
1855
|
const manifestEvidence = () => ({
|
|
1571
|
-
version:
|
|
1856
|
+
version: manifestVersion2,
|
|
1572
1857
|
location: manifestLocation,
|
|
1573
|
-
detail:
|
|
1858
|
+
detail: manifestVersion2 ? "materialized pi package manifest" : "materialized pi package manifest was unreadable"
|
|
1574
1859
|
});
|
|
1575
1860
|
const hosted = hostEnv(env);
|
|
1576
1861
|
const command = hostCommands(env).find((bin) => runner(bin, ["--version"], hosted).status === 0);
|
|
@@ -1586,9 +1871,9 @@ function probeJervcodeInstallation(env = process.env, runner = runHostCommand) {
|
|
|
1586
1871
|
};
|
|
1587
1872
|
}
|
|
1588
1873
|
const hostError = result.status !== 0 ? `${hostLocation} failed: ${failure2(result)}` : `${hostLocation} did not list npm:${PI_PACKAGE}`;
|
|
1589
|
-
if (
|
|
1874
|
+
if (manifestVersion2) {
|
|
1590
1875
|
return {
|
|
1591
|
-
version:
|
|
1876
|
+
version: manifestVersion2,
|
|
1592
1877
|
location: manifestLocation,
|
|
1593
1878
|
detail: `${hostError}; using materialized pi package manifest`
|
|
1594
1879
|
};
|
|
@@ -1619,7 +1904,7 @@ function jervcodeArm(options) {
|
|
|
1619
1904
|
if (!before) return result(null, options.target, "defer", `cannot read ${(0, import_node_path9.join)(jervcodeAgentDir(env), "settings.json")}`);
|
|
1620
1905
|
const from = installedVersion(env);
|
|
1621
1906
|
const legacy = before.filter(isLegacyMmiPiPath);
|
|
1622
|
-
const highWater = journalHighWater(options.paths.journalPath, "jervcode");
|
|
1907
|
+
const highWater = journalHighWater(options.paths.journalPath, "jervcode", options.journalEvents);
|
|
1623
1908
|
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;
|
|
1624
1909
|
if (options.dryRun) {
|
|
1625
1910
|
const action = floor ? `hold ${from ?? "the install"} and skip the install (${floor})` : before.includes(exact) && from === options.target ? "keep exact pin" : `install ${exact}`;
|
|
@@ -1811,7 +2096,7 @@ function hermesHomes(env = process.env) {
|
|
|
1811
2096
|
var VERDICT_RANK = { fail: 3, defer: 2, ok: 1, skip: 0 };
|
|
1812
2097
|
function hermesArm(options) {
|
|
1813
2098
|
const env = options.env ?? process.env;
|
|
1814
|
-
if (!hermesHostPresent(env, options.hostCommandRunner)) {
|
|
2099
|
+
if (!(options.hostPresent ?? hermesHostPresent(env, options.hostCommandRunner))) {
|
|
1815
2100
|
return {
|
|
1816
2101
|
surface: "hermes",
|
|
1817
2102
|
from: null,
|
|
@@ -1854,7 +2139,7 @@ function armHermesHome(home, env, options, primary) {
|
|
|
1854
2139
|
return result(installed, options.target, "defer", `${live} carries no ${PACKAGE2} package identity \u2014 refusing to displace an unmanaged Hermes mmi plugin`);
|
|
1855
2140
|
}
|
|
1856
2141
|
if (installed && compareSemver(installed, options.target) > 0) return result(installed, installed, "skip", `installed ${installed} is above candidate ${options.target} (monotonic)`);
|
|
1857
|
-
const highWater = primary ? journalHighWater(options.paths.journalPath, "hermes") : null;
|
|
2142
|
+
const highWater = primary ? journalHighWater(options.paths.journalPath, "hermes", options.journalEvents) : null;
|
|
1858
2143
|
if (highWater && compareSemver(highWater, options.target) > 0) return result(installed, highWater, "skip", `journal high-water ${highWater} is above candidate ${options.target} (monotonic)`);
|
|
1859
2144
|
const hostRunner = options.hostCommandRunner ?? runHostCommand;
|
|
1860
2145
|
if (installed === options.target && treeHealthy2(live)) {
|
|
@@ -1956,6 +2241,10 @@ function probeHermesLaunchStaging(env = process.env) {
|
|
|
1956
2241
|
// src/reap.ts
|
|
1957
2242
|
var import_node_fs14 = require("node:fs");
|
|
1958
2243
|
var import_node_path11 = require("node:path");
|
|
2244
|
+
var import_node_perf_hooks = require("node:perf_hooks");
|
|
2245
|
+
function elapsedMs(startedAt) {
|
|
2246
|
+
return Math.round((import_node_perf_hooks.performance.now() - startedAt) * 10) / 10;
|
|
2247
|
+
}
|
|
1959
2248
|
var KEEP_GENERATIONS2 = 2;
|
|
1960
2249
|
var SCRATCH_AGE_MS = 24 * 60 * 6e4;
|
|
1961
2250
|
function listDirs(path) {
|
|
@@ -2052,10 +2341,11 @@ function reap(options) {
|
|
|
2052
2341
|
const { paths, dryRun } = options;
|
|
2053
2342
|
const env = options.env ?? process.env;
|
|
2054
2343
|
const guarded = (category, run) => {
|
|
2344
|
+
const startedAt = import_node_perf_hooks.performance.now();
|
|
2055
2345
|
try {
|
|
2056
|
-
return run();
|
|
2346
|
+
return { ...run(), durationMs: elapsedMs(startedAt) };
|
|
2057
2347
|
} catch (error) {
|
|
2058
|
-
return { category, reaped: 0, verdict: "fail", detail: `reap failed: ${error?.message ?? error}
|
|
2348
|
+
return { category, reaped: 0, verdict: "fail", detail: `reap failed: ${error?.message ?? error}`, durationMs: elapsedMs(startedAt) };
|
|
2059
2349
|
}
|
|
2060
2350
|
};
|
|
2061
2351
|
const atomicWritePaths = [claudeMarketplacesPath(env)];
|
|
@@ -2072,7 +2362,7 @@ function shouldHandOff(self, options) {
|
|
|
2072
2362
|
if (!options.dist) return false;
|
|
2073
2363
|
return self.verdict === "ok" && Boolean(self.from) && self.from !== self.to;
|
|
2074
2364
|
}
|
|
2075
|
-
function reconcile(options) {
|
|
2365
|
+
async function reconcile(options) {
|
|
2076
2366
|
const env = options.env ?? process.env;
|
|
2077
2367
|
const shipped = options.shippedTarget?.trim() || null;
|
|
2078
2368
|
const say = (line) => {
|
|
@@ -2088,9 +2378,9 @@ function reconcile(options) {
|
|
|
2088
2378
|
if (!appendJournal(paths.journalPath, { ts: (/* @__PURE__ */ new Date()).toISOString(), run, ...stamped })) summary.journalOk = false;
|
|
2089
2379
|
};
|
|
2090
2380
|
const summary = { run, target: null, gate: null, managedSurfaces: null, atomicity: null, cli: null, updater: null, plugins: [], reap: [], surfaces: [], journalOk: true, reexec: null, exit: 0 };
|
|
2091
|
-
const recordArm = (arm) => {
|
|
2381
|
+
const recordArm = (arm, durationMs) => {
|
|
2092
2382
|
if (arm.verdict === "fail") arm.repeatFailure = journalSurfaceRepeatedFail(paths.journalPath, arm.surface, run);
|
|
2093
|
-
journal({ kind: "arm", surface: arm.surface, from: arm.from, to: arm.to, verdict: arm.verdict, detail: arm.detail, compat: arm.compat });
|
|
2383
|
+
journal({ kind: "arm", surface: arm.surface, from: arm.from, to: arm.to, verdict: arm.verdict, detail: arm.detail, compat: arm.compat, durationMs });
|
|
2094
2384
|
options.onArm?.(arm);
|
|
2095
2385
|
if (arm.verdict === "ok" && !options.dryRun && !arm.launchStaged) clearPending(paths, arm.surface);
|
|
2096
2386
|
if (arm.verdict === "defer") summary.exit = Math.max(summary.exit, 2);
|
|
@@ -2099,7 +2389,8 @@ function reconcile(options) {
|
|
|
2099
2389
|
try {
|
|
2100
2390
|
say("acquiring single-writer lease");
|
|
2101
2391
|
const leaseWaitMs = Number(env.MMI_UPDATER_LEASE_WAIT_MS) || void 0;
|
|
2102
|
-
return withLease(paths.leasePath, () => {
|
|
2392
|
+
return await withLease(paths.leasePath, async () => {
|
|
2393
|
+
const journalEvents = parseJournal(paths.journalPath);
|
|
2103
2394
|
if (!options.dryRun) {
|
|
2104
2395
|
const pendingBefore = readPending(paths);
|
|
2105
2396
|
if (pendingBefore.length) {
|
|
@@ -2108,7 +2399,7 @@ function reconcile(options) {
|
|
|
2108
2399
|
}
|
|
2109
2400
|
}
|
|
2110
2401
|
summary.reap = reap({ paths, dryRun: options.dryRun, env });
|
|
2111
|
-
for (const row of summary.reap) journal({ kind: "reap", surface: row.category, verdict: row.verdict, count: row.reaped, detail: row.detail });
|
|
2402
|
+
for (const row of summary.reap) journal({ kind: "reap", surface: row.category, verdict: row.verdict, count: row.reaped, detail: row.detail, durationMs: row.durationMs });
|
|
2112
2403
|
for (const row of summary.reap.filter((entry) => entry.verdict !== "skip")) say(`reap ${row.category}: ${row.verdict}${row.detail ? " \u2014 " + row.detail : ""}`);
|
|
2113
2404
|
if (shipped) {
|
|
2114
2405
|
say(`shipped target v${shipped} \u2014 the payload is the release; no npm poll, no gate`);
|
|
@@ -2118,41 +2409,48 @@ function reconcile(options) {
|
|
|
2118
2409
|
journal({ kind: "target", to: shipped, verdict: "ok", detail: "shipped payload" });
|
|
2119
2410
|
} else {
|
|
2120
2411
|
say("polling npm for @mutmutco/cli versions");
|
|
2412
|
+
const pollStartedAt = import_node_perf_hooks2.performance.now();
|
|
2121
2413
|
const versions = npmVersions("@mutmutco/cli", env).sort((a, b) => compareSemver(b, a));
|
|
2414
|
+
const pollMs = elapsedMs(pollStartedAt);
|
|
2122
2415
|
if (!versions.length) {
|
|
2123
|
-
journal({ kind: "poll", verdict: "fail", detail: "@mutmutco/cli has no npm versions \u2014 cannot discover a candidate" });
|
|
2416
|
+
journal({ kind: "poll", verdict: "fail", durationMs: pollMs, detail: "@mutmutco/cli has no npm versions \u2014 cannot discover a candidate" });
|
|
2124
2417
|
summary.exit = 1;
|
|
2125
2418
|
summary.detail = "npm poll returned no versions for @mutmutco/cli";
|
|
2126
2419
|
return summary;
|
|
2127
2420
|
}
|
|
2421
|
+
journal({ kind: "poll", verdict: "ok", count: versions.length, durationMs: pollMs, detail: `${versions.length} versions; newest ${versions[0]}` });
|
|
2128
2422
|
say("fetching release BOM gate cache");
|
|
2423
|
+
const fetchStartedAt = import_node_perf_hooks2.performance.now();
|
|
2129
2424
|
ensureBareCache(paths.gitCache, repoUrl);
|
|
2130
2425
|
fetchIntoCache(paths.gitCache, repoUrl, "refs/heads/main:refs/heads/main");
|
|
2426
|
+
journal({ kind: "gate", surface: "cache", verdict: "ok", durationMs: elapsedMs(fetchStartedAt), detail: "release BOM gate cache: refs/heads/main fetched" });
|
|
2427
|
+
const walkStartedAt = import_node_perf_hooks2.performance.now();
|
|
2131
2428
|
for (const candidate of versions.slice(0, bound)) {
|
|
2132
2429
|
say(`gating candidate v${candidate}`);
|
|
2430
|
+
const candidateStartedAt = import_node_perf_hooks2.performance.now();
|
|
2133
2431
|
const gate = gateCandidate(paths.gitCache, repoUrl, candidate);
|
|
2134
2432
|
if (!gate.ok) {
|
|
2135
2433
|
say(`candidate v${candidate} rejected (${gate.reason ?? "gate"})`);
|
|
2136
|
-
journal({ kind: "candidate-reject", to: candidate, verdict: "skip", detail: gate.reason ?? "gate rejected" });
|
|
2434
|
+
journal({ kind: "candidate-reject", to: candidate, verdict: "skip", durationMs: elapsedMs(candidateStartedAt), detail: gate.reason ?? "gate rejected" });
|
|
2137
2435
|
continue;
|
|
2138
2436
|
}
|
|
2139
|
-
const atomicity = checkAtomicity(paths.gitCache, gate.tagCommit, gate.bom, candidate, {
|
|
2140
|
-
distIntegrity: (spec) =>
|
|
2437
|
+
const atomicity = await checkAtomicity(paths.gitCache, gate.tagCommit, gate.bom, candidate, {
|
|
2438
|
+
distIntegrity: (spec) => npmDistIntegrityAsync(spec, env)
|
|
2141
2439
|
});
|
|
2142
2440
|
if (!atomicity.ok) {
|
|
2143
|
-
journal({ kind: "candidate-reject", to: candidate, verdict: "skip", detail: `atomicity: ${atomicity.rows.filter((row) => !row.ok).map((row) => row.detail).join("; ")}` });
|
|
2441
|
+
journal({ kind: "candidate-reject", to: candidate, verdict: "skip", durationMs: elapsedMs(candidateStartedAt), detail: `atomicity: ${atomicity.rows.filter((row) => !row.ok).map((row) => row.detail).join("; ")}` });
|
|
2144
2442
|
continue;
|
|
2145
2443
|
}
|
|
2146
2444
|
summary.target = candidate;
|
|
2147
2445
|
summary.managedSurfaces = Array.isArray(gate.bom?.managedSurfaces) ? gate.bom.managedSurfaces.map(String) : null;
|
|
2148
2446
|
summary.gate = `tag v${candidate} reachable from main, BOM stamp coherent`;
|
|
2149
2447
|
summary.atomicity = atomicity;
|
|
2150
|
-
journal({ kind: "target", to: candidate, verdict: "ok", detail: summary.gate });
|
|
2448
|
+
journal({ kind: "target", to: candidate, verdict: "ok", durationMs: elapsedMs(candidateStartedAt), detail: summary.gate });
|
|
2151
2449
|
break;
|
|
2152
2450
|
}
|
|
2153
2451
|
if (!summary.target) {
|
|
2154
2452
|
say("no green target \u2014 deferred");
|
|
2155
|
-
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` });
|
|
2453
|
+
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` });
|
|
2156
2454
|
for (const probe of enumerateSurfaces(env)) {
|
|
2157
2455
|
summary.surfaces.push({ id: probe.id, present: probe.present, action: probe.present ? "deferred-no-target" : "absent-skip" });
|
|
2158
2456
|
journal({ kind: "surface", surface: probe.id, present: probe.present, verdict: "skip", detail: probe.present ? "present; no green target yet" : `absent (${probe.marker}) \u2014 skipped` });
|
|
@@ -2163,6 +2461,7 @@ function reconcile(options) {
|
|
|
2163
2461
|
say(`target v${summary.target} (${summary.gate})`);
|
|
2164
2462
|
options.onTargetResolved?.(summary.target, summary.gate);
|
|
2165
2463
|
say("updater: self-bootstrap");
|
|
2464
|
+
const selfStartedAt = import_node_perf_hooks2.performance.now();
|
|
2166
2465
|
const self = npmGlobalArm({
|
|
2167
2466
|
surface: "updater",
|
|
2168
2467
|
packageName: "@mutmutco/hub",
|
|
@@ -2170,10 +2469,11 @@ function reconcile(options) {
|
|
|
2170
2469
|
target: summary.target,
|
|
2171
2470
|
dryRun: options.dryRun,
|
|
2172
2471
|
paths,
|
|
2173
|
-
env
|
|
2472
|
+
env,
|
|
2473
|
+
journalEvents
|
|
2174
2474
|
});
|
|
2175
2475
|
summary.updater = self;
|
|
2176
|
-
recordArm(self);
|
|
2476
|
+
recordArm(self, elapsedMs(selfStartedAt));
|
|
2177
2477
|
say(`updater: ${self.verdict}${self.detail ? " \u2014 " + self.detail : ""}`);
|
|
2178
2478
|
const handoffDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
|
|
2179
2479
|
if (shouldHandOff(self, { dryRun: options.dryRun, dist: handoffDist, guard: env.MMI_UPDATER_REEXEC })) {
|
|
@@ -2183,6 +2483,7 @@ function reconcile(options) {
|
|
|
2183
2483
|
return summary;
|
|
2184
2484
|
}
|
|
2185
2485
|
say(`cli: converging to v${summary.target}`);
|
|
2486
|
+
const cliStartedAt = import_node_perf_hooks2.performance.now();
|
|
2186
2487
|
const cli = npmGlobalArm({
|
|
2187
2488
|
surface: "cli",
|
|
2188
2489
|
packageName: "@mutmutco/cli",
|
|
@@ -2190,10 +2491,11 @@ function reconcile(options) {
|
|
|
2190
2491
|
target: summary.target,
|
|
2191
2492
|
dryRun: options.dryRun,
|
|
2192
2493
|
paths,
|
|
2193
|
-
env
|
|
2494
|
+
env,
|
|
2495
|
+
journalEvents
|
|
2194
2496
|
});
|
|
2195
2497
|
summary.cli = cli;
|
|
2196
|
-
recordArm(cli);
|
|
2498
|
+
recordArm(cli, elapsedMs(cliStartedAt));
|
|
2197
2499
|
say(`cli: ${cli.verdict}${cli.detail ? " \u2014 " + cli.detail : ""}`);
|
|
2198
2500
|
for (const probe of enumerateSurfaces(env)) {
|
|
2199
2501
|
if (!probe.present) {
|
|
@@ -2208,11 +2510,12 @@ function reconcile(options) {
|
|
|
2208
2510
|
continue;
|
|
2209
2511
|
}
|
|
2210
2512
|
say(`${probe.id}: converging to v${summary.target}`);
|
|
2211
|
-
const
|
|
2513
|
+
const armStartedAt = import_node_perf_hooks2.performance.now();
|
|
2514
|
+
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;
|
|
2212
2515
|
if (arm) {
|
|
2213
2516
|
summary.plugins.push(arm);
|
|
2214
2517
|
summary.surfaces.push({ id: probe.id, present: true, action: `arm-${arm.verdict}` });
|
|
2215
|
-
recordArm(arm);
|
|
2518
|
+
recordArm(arm, elapsedMs(armStartedAt));
|
|
2216
2519
|
say(`${probe.id}: ${arm.verdict}${arm.detail ? " \u2014 " + arm.detail : ""}`);
|
|
2217
2520
|
} else {
|
|
2218
2521
|
summary.surfaces.push({ id: probe.id, present: true, action: "arm-pending" });
|
|
@@ -2924,28 +3227,53 @@ var ARM_TITLE = {
|
|
|
2924
3227
|
hermes: "Arming Hermes",
|
|
2925
3228
|
jervcode: "Arming JervCode"
|
|
2926
3229
|
};
|
|
2927
|
-
var
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
3230
|
+
var FACE_PRODUCT = "mmi-hub";
|
|
3231
|
+
function updaterFace(options = {}) {
|
|
3232
|
+
return createFace({ product: FACE_PRODUCT, color: options.color ?? false, columns: options.width });
|
|
3233
|
+
}
|
|
3234
|
+
var STATE_WORD = {
|
|
3235
|
+
updated: "updated",
|
|
3236
|
+
current: "current",
|
|
3237
|
+
keep: "kept",
|
|
3238
|
+
retry: "will retry",
|
|
3239
|
+
failed: "failed",
|
|
3240
|
+
pending: "pending"
|
|
2934
3241
|
};
|
|
2935
|
-
function
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
const kind = classify(arm)
|
|
2940
|
-
const
|
|
2941
|
-
const
|
|
2942
|
-
const
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
3242
|
+
function stepKindFor(kind) {
|
|
3243
|
+
return kind === "failed" ? "fail" : kind === "retry" ? "note" : "ok";
|
|
3244
|
+
}
|
|
3245
|
+
function checklistLine(arm, seconds, face, options = {}) {
|
|
3246
|
+
const { kind } = classify(arm);
|
|
3247
|
+
const state = options.dryRun && kind === "updated" ? "would update" : STATE_WORD[kind];
|
|
3248
|
+
const title = `${ARM_TITLE[arm.surface] ?? `Arming ${arm.surface}`} \u2014 ${state}`;
|
|
3249
|
+
const truthful = kind === "failed" || kind === "retry" ? null : seconds;
|
|
3250
|
+
return face.step(title, truthful, stepKindFor(kind));
|
|
3251
|
+
}
|
|
3252
|
+
function receiptText(summary, autoupdate, options = {}) {
|
|
3253
|
+
const dry = options.dryRun ?? false;
|
|
3254
|
+
const counts = reportCounts(summary);
|
|
3255
|
+
const health = "Check health any time: mmi-hub status";
|
|
3256
|
+
if (!summary.target) {
|
|
3257
|
+
return [`${summary.exit >= 1 ? "\u2716" : "\u25CF"} mmi-hub is not ready \u2014 no green release yet (hourly retry)`, "", health];
|
|
3258
|
+
}
|
|
3259
|
+
const ready = counts.failed === 0;
|
|
3260
|
+
const glyph = counts.failed > 0 ? "\u2716" : counts.retry > 0 ? "\u25CF" : dry ? "\u25C7" : "\u2714";
|
|
3261
|
+
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`;
|
|
3262
|
+
const tail2 = autoupdate ? ` \xB7 ${autoupdate.ok ? "auto-update on" : `auto-update NOT enabled \u2014 ${autoupdate.detail}`}` : ` \xB7 ${summary.target}${dry ? " (planned)" : " (verified)"}`;
|
|
3263
|
+
return [`${glyph} mmi-hub is ${ready ? "ready" : "not ready"} \u2014 ${state}${tail2}`, "", health];
|
|
3264
|
+
}
|
|
3265
|
+
function autoupdateLine(state) {
|
|
3266
|
+
return state.ok ? 'Auto-update on \u2014 hourly task "MMI Fleet Updater".' : `Auto-update NOT enabled \u2014 ${state.detail}`;
|
|
3267
|
+
}
|
|
3268
|
+
function attentionRows(summary) {
|
|
3269
|
+
return collectRows(summary).filter((row) => row.kind === "failed" || row.kind === "retry" || row.kind === "pending").map((row) => ({
|
|
3270
|
+
surface: row.name,
|
|
3271
|
+
state: statusText(row.kind, false),
|
|
3272
|
+
mark: row.kind === "failed" ? "fail" : "note",
|
|
3273
|
+
note: row.kind === "failed" ? failConsequence(row) : row.kind === "retry" ? retryHint(row.detail) : "arm not landed"
|
|
3274
|
+
}));
|
|
3275
|
+
}
|
|
3276
|
+
function wrapWords2(text, available) {
|
|
2949
3277
|
if (available < 1) return [text];
|
|
2950
3278
|
const lines = [];
|
|
2951
3279
|
let current = "";
|
|
@@ -2966,7 +3294,7 @@ function noteLines(prefix, note, width) {
|
|
|
2966
3294
|
const available = width - used - 1;
|
|
2967
3295
|
const indent = available >= MIN_NOTE_SPACE ? " ".repeat(used + 1) : " ".repeat(Math.min(used + 1, Math.max(2, width - MIN_NOTE_SPACE)));
|
|
2968
3296
|
const room = available >= MIN_NOTE_SPACE ? available : width - indent.length;
|
|
2969
|
-
const parts =
|
|
3297
|
+
const parts = wrapWords2(note, room);
|
|
2970
3298
|
const first = available >= MIN_NOTE_SPACE ? prefix + " " + parts[0] : prefix.replace(/\s+$/, "");
|
|
2971
3299
|
const lines = [first];
|
|
2972
3300
|
for (const part of parts.slice(available >= MIN_NOTE_SPACE ? 1 : 0)) lines.push(indent + part);
|
|
@@ -2989,7 +3317,7 @@ function collectRows(summary) {
|
|
|
2989
3317
|
}
|
|
2990
3318
|
function headline(detail, width) {
|
|
2991
3319
|
const first = (detail ?? "").split(/(?<=[.;:])\s/)[0]?.replace(/\s+/g, " ").trim() ?? "";
|
|
2992
|
-
return
|
|
3320
|
+
return wrapWords2(first || "blocked", Math.max(10, width - 4))[0];
|
|
2993
3321
|
}
|
|
2994
3322
|
function updateHeader(target, dryRun = false, options) {
|
|
2995
3323
|
return brandLine(
|
|
@@ -3091,124 +3419,6 @@ function formatReconcileReport(summary, options = {}) {
|
|
|
3091
3419
|
return out.join("\n") + "\n";
|
|
3092
3420
|
}
|
|
3093
3421
|
|
|
3094
|
-
// src/face.ts
|
|
3095
|
-
var DEFAULT_WIDTH2 = 100;
|
|
3096
|
-
function welcome(color, output = process.stdout) {
|
|
3097
|
-
const mark = color ? MM_GREEN + "\u25C6" + RESET : "\u25C6";
|
|
3098
|
-
output.write(`${mark} ${accent("mmi-hub", color)} \u2014 Mutatis Mutandis
|
|
3099
|
-
`);
|
|
3100
|
-
output.write("\u2502\n");
|
|
3101
|
-
output.write("\u2502 Welcome back. Checking your surfaces\u2026\n");
|
|
3102
|
-
output.write("\u2502\n");
|
|
3103
|
-
}
|
|
3104
|
-
var TONES = { "\u2714": MM_GREEN, "\u2716": RED, "\u25CF": YELLOW };
|
|
3105
|
-
function paintGlyph(line, color) {
|
|
3106
|
-
if (!color) return line;
|
|
3107
|
-
const glyph = line.trimStart()[0];
|
|
3108
|
-
const tone = TONES[glyph];
|
|
3109
|
-
if (!tone) return line;
|
|
3110
|
-
const at = line.indexOf(glyph);
|
|
3111
|
-
return line.slice(0, at) + tone + glyph + RESET + line.slice(at + 1);
|
|
3112
|
-
}
|
|
3113
|
-
function receipt(text, options = {}) {
|
|
3114
|
-
const color = options.color ?? false;
|
|
3115
|
-
const width = Math.max(40, options.width ?? DEFAULT_WIDTH2);
|
|
3116
|
-
const output = options.output ?? process.stdout;
|
|
3117
|
-
const edges = 2 * 2 + 2;
|
|
3118
|
-
const available = width - edges;
|
|
3119
|
-
const lines = [];
|
|
3120
|
-
for (const paragraph of text.split("\n")) {
|
|
3121
|
-
if (!paragraph.trim()) lines.push(null);
|
|
3122
|
-
else if (paragraph.length <= available) lines.push(paragraph);
|
|
3123
|
-
else for (const line of wrapWords(paragraph, available)) lines.push(line);
|
|
3124
|
-
}
|
|
3125
|
-
const inner = Math.min(available, Math.max(20, ...lines.map((line) => line?.length ?? 0)));
|
|
3126
|
-
const rule = "\u2500".repeat(inner + 2 * 2);
|
|
3127
|
-
const frame = (text2) => color ? ACCENT + text2 + RESET : text2;
|
|
3128
|
-
const blank = `${frame("\u2502")}${" ".repeat(inner + 2 * 2)}${frame("\u2502")}`;
|
|
3129
|
-
output.write(frame(`\u256D${rule}\u256E`) + "\n");
|
|
3130
|
-
for (const line of lines) {
|
|
3131
|
-
output.write(line === null ? blank + "\n" : `${frame("\u2502")} ${paintGlyph(line.padEnd(inner), color)} ${frame("\u2502")}
|
|
3132
|
-
`);
|
|
3133
|
-
}
|
|
3134
|
-
output.write(frame(`\u2570${rule}\u256F`) + "\n");
|
|
3135
|
-
}
|
|
3136
|
-
function signOff(color, output = process.stdout) {
|
|
3137
|
-
output.write(`\u2570 ${accent("mmi-hub \xB7 Mutatis Mutandis", color)}
|
|
3138
|
-
`);
|
|
3139
|
-
}
|
|
3140
|
-
function receiptText(summary, autoupdate, options = {}) {
|
|
3141
|
-
const dry = options.dryRun ?? false;
|
|
3142
|
-
const counts = reportCounts(summary);
|
|
3143
|
-
const health = "Check health any time: mmi-hub status";
|
|
3144
|
-
if (!summary.target) {
|
|
3145
|
-
return `${summary.exit >= 1 ? "\u2716" : "\u25CF"} mmi-hub is not ready \u2014 no green release yet (hourly retry)
|
|
3146
|
-
${health}`;
|
|
3147
|
-
}
|
|
3148
|
-
const ready = counts.failed === 0;
|
|
3149
|
-
const glyph = counts.failed > 0 ? "\u2716" : counts.retry > 0 ? "\u25CF" : dry ? "\u25C7" : "\u2714";
|
|
3150
|
-
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`;
|
|
3151
|
-
const tail2 = autoupdate ? ` \xB7 ${autoupdate.ok ? "auto-update on" : `auto-update NOT enabled \u2014 ${autoupdate.detail}`}` : ` \xB7 ${summary.target}${dry ? " (planned)" : " (verified)"}`;
|
|
3152
|
-
return `${glyph} mmi-hub is ${ready ? "ready" : "not ready"} \u2014 ${state}${tail2}
|
|
3153
|
-
${health}`;
|
|
3154
|
-
}
|
|
3155
|
-
|
|
3156
|
-
// src/spinner.ts
|
|
3157
|
-
var import_node_worker_threads = require("node:worker_threads");
|
|
3158
|
-
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3159
|
-
var WORKER_SRC = `
|
|
3160
|
-
const { parentPort, workerData } = require('node:worker_threads');
|
|
3161
|
-
const frames = ${JSON.stringify(FRAMES)};
|
|
3162
|
-
const control = new Int32Array(workerData.sab);
|
|
3163
|
-
let frame = 0, label = '', timer = null;
|
|
3164
|
-
function tick() {
|
|
3165
|
-
if (Atomics.load(control, 0) === 1) return; // paused by the main thread
|
|
3166
|
-
if (!label) return;
|
|
3167
|
-
process.stderr.write('\\r' + frames[frame++ % frames.length] + ' ' + label + '\\x1b[K');
|
|
3168
|
-
}
|
|
3169
|
-
parentPort.on('message', (message) => {
|
|
3170
|
-
if (message && message.type === 'label') { label = message.label; if (!timer) timer = setInterval(tick, 80); }
|
|
3171
|
-
else if (message === 'stop') { if (timer) clearInterval(timer); process.stderr.write('\\r\\x1b[K'); process.exit(0); }
|
|
3172
|
-
});
|
|
3173
|
-
`;
|
|
3174
|
-
var NOOP = { label() {
|
|
3175
|
-
}, persist() {
|
|
3176
|
-
}, stop() {
|
|
3177
|
-
}, active: false };
|
|
3178
|
-
function startSpinner(enabled) {
|
|
3179
|
-
if (!enabled) return NOOP;
|
|
3180
|
-
let worker;
|
|
3181
|
-
let control;
|
|
3182
|
-
try {
|
|
3183
|
-
const sab = new SharedArrayBuffer(4);
|
|
3184
|
-
control = new Int32Array(sab);
|
|
3185
|
-
worker = new import_node_worker_threads.Worker(WORKER_SRC, { eval: true, workerData: { sab } });
|
|
3186
|
-
worker.unref();
|
|
3187
|
-
worker.on("error", () => {
|
|
3188
|
-
});
|
|
3189
|
-
} catch {
|
|
3190
|
-
return NOOP;
|
|
3191
|
-
}
|
|
3192
|
-
let stopped = false;
|
|
3193
|
-
return {
|
|
3194
|
-
active: true,
|
|
3195
|
-
label: (text) => {
|
|
3196
|
-
if (!stopped) worker.postMessage({ type: "label", label: text });
|
|
3197
|
-
},
|
|
3198
|
-
persist: (line) => {
|
|
3199
|
-
Atomics.store(control, 0, 1);
|
|
3200
|
-
process.stderr.write("\r\x1B[K");
|
|
3201
|
-
process.stdout.write(line + "\n");
|
|
3202
|
-
Atomics.store(control, 0, 0);
|
|
3203
|
-
},
|
|
3204
|
-
stop: () => {
|
|
3205
|
-
if (stopped) return;
|
|
3206
|
-
stopped = true;
|
|
3207
|
-
worker.postMessage("stop");
|
|
3208
|
-
}
|
|
3209
|
-
};
|
|
3210
|
-
}
|
|
3211
|
-
|
|
3212
3422
|
// src/stamp.ts
|
|
3213
3423
|
var import_node_fs17 = require("node:fs");
|
|
3214
3424
|
var ALWAYS_ARMED = ["cli", "updater"];
|
|
@@ -3306,17 +3516,33 @@ function printReconcile(summary, json, dryRun = false) {
|
|
|
3306
3516
|
function faceEnabled(args, json) {
|
|
3307
3517
|
return !json && !args.includes("--verbose") && !process.env.NO_COLOR && process.env.TERM !== "dumb" && Boolean(process.stdout.isTTY) && Boolean(process.stderr.isTTY);
|
|
3308
3518
|
}
|
|
3309
|
-
function openFace(args, json) {
|
|
3310
|
-
|
|
3311
|
-
|
|
3519
|
+
function openFace(args, json, dryRun) {
|
|
3520
|
+
if (!faceEnabled(args, json)) return null;
|
|
3521
|
+
const face = updaterFace(humanOptions(dryRun));
|
|
3522
|
+
if (!process.env.MMI_UPDATER_REEXEC) for (const line of face.welcome()) process.stdout.write(line + "\n");
|
|
3312
3523
|
return face;
|
|
3313
3524
|
}
|
|
3314
|
-
function finishFace(summary, autoupdate, dryRun) {
|
|
3525
|
+
function finishFace(face, summary, autoupdate, dryRun) {
|
|
3315
3526
|
const options = humanOptions(dryRun);
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3527
|
+
const write = (line) => {
|
|
3528
|
+
process.stdout.write(line + "\n");
|
|
3529
|
+
};
|
|
3530
|
+
if (!summary.target) {
|
|
3531
|
+
process.stdout.write(formatReconcileReport(summary, { ...options, tty: false }));
|
|
3532
|
+
} else {
|
|
3533
|
+
for (const line of renderRows(face, attentionRows(summary))) write(line);
|
|
3534
|
+
}
|
|
3535
|
+
const body = receiptText(summary, autoupdate, { dryRun });
|
|
3536
|
+
const counts = reportCounts(summary);
|
|
3537
|
+
if (counts.failed > 0) {
|
|
3538
|
+
body.push("", "Fix: mmi-cli doctor, then mmi-hub update", `Log: ${options.journalPath}`);
|
|
3539
|
+
}
|
|
3540
|
+
const self = summary.updater;
|
|
3541
|
+
if (!dryRun && self && self.from && self.to && self.from !== self.to && self.verdict !== "fail") {
|
|
3542
|
+
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}`);
|
|
3543
|
+
}
|
|
3544
|
+
for (const line of face.receipt(body)) write(line);
|
|
3545
|
+
write(face.signOff());
|
|
3320
3546
|
}
|
|
3321
3547
|
function handOff(reexec, args) {
|
|
3322
3548
|
const child = (0, import_node_child_process9.spawnSync)(process.execPath, [reexec.dist, ...args], {
|
|
@@ -3353,7 +3579,7 @@ function shippedTargetFrom(args) {
|
|
|
3353
3579
|
const value = (args[index + 1] ?? "").trim();
|
|
3354
3580
|
return value && !value.startsWith("--") ? value : void 0;
|
|
3355
3581
|
}
|
|
3356
|
-
function runUpdate(args, json, face =
|
|
3582
|
+
async function runUpdate(args, json, face = null, shippedTarget) {
|
|
3357
3583
|
const dryRun = args.includes("--dry-run");
|
|
3358
3584
|
const verbose = args.includes("--verbose");
|
|
3359
3585
|
const narrate = (line) => {
|
|
@@ -3362,20 +3588,35 @@ function runUpdate(args, json, face = false, shippedTarget) {
|
|
|
3362
3588
|
};
|
|
3363
3589
|
const progressive = !json && !verbose && Boolean(process.stderr.isTTY);
|
|
3364
3590
|
if (!progressive) {
|
|
3365
|
-
return
|
|
3591
|
+
return reconcile({ dryRun, narrate: verbose ? narrate : void 0, shippedTarget }).then((summary2) => ({ summary: summary2, rendered: false }));
|
|
3366
3592
|
}
|
|
3367
3593
|
const reportOptions = humanOptions(dryRun);
|
|
3368
|
-
const spinner =
|
|
3594
|
+
const spinner = createSpinner(face ?? updaterFace(reportOptions), { animate: true, stream: process.stderr });
|
|
3595
|
+
let spinning = false;
|
|
3596
|
+
const phase = (line) => {
|
|
3597
|
+
if (spinning) spinner.say(line);
|
|
3598
|
+
else {
|
|
3599
|
+
spinner.start(line);
|
|
3600
|
+
spinning = true;
|
|
3601
|
+
}
|
|
3602
|
+
};
|
|
3603
|
+
const persist = (line) => {
|
|
3604
|
+
if (spinning) {
|
|
3605
|
+
spinner.stop();
|
|
3606
|
+
spinning = false;
|
|
3607
|
+
}
|
|
3608
|
+
process.stdout.write(line + "\n");
|
|
3609
|
+
};
|
|
3369
3610
|
let headerEmitted = false;
|
|
3370
3611
|
let summary;
|
|
3371
|
-
const { emit, stream } = heldOutput(
|
|
3612
|
+
const { emit, stream } = heldOutput(persist);
|
|
3372
3613
|
let arms = 0;
|
|
3373
3614
|
let mark = Date.now();
|
|
3374
3615
|
try {
|
|
3375
|
-
summary = reconcile({
|
|
3616
|
+
summary = await reconcile({
|
|
3376
3617
|
dryRun,
|
|
3377
3618
|
shippedTarget,
|
|
3378
|
-
onPhase: (line) =>
|
|
3619
|
+
onPhase: (line) => phase(line),
|
|
3379
3620
|
onTargetResolved: face ? void 0 : (target) => {
|
|
3380
3621
|
emit(updateHeader(target, dryRun, reportOptions));
|
|
3381
3622
|
emit("");
|
|
@@ -3385,7 +3626,7 @@ function runUpdate(args, json, face = false, shippedTarget) {
|
|
|
3385
3626
|
const seconds = (Date.now() - mark) / 1e3;
|
|
3386
3627
|
mark = Date.now();
|
|
3387
3628
|
if (arms++) stream();
|
|
3388
|
-
emit(face ? checklistLine(arm, seconds, reportOptions) : progressiveRow(arm, reportOptions));
|
|
3629
|
+
emit(face ? checklistLine(arm, seconds, face, reportOptions) : progressiveRow(arm, reportOptions));
|
|
3389
3630
|
}
|
|
3390
3631
|
});
|
|
3391
3632
|
} finally {
|
|
@@ -3398,7 +3639,7 @@ function runUpdate(args, json, face = false, shippedTarget) {
|
|
|
3398
3639
|
else process.stdout.write(formatReconcileReport(summary, reportOptions));
|
|
3399
3640
|
return { summary, rendered: true };
|
|
3400
3641
|
}
|
|
3401
|
-
function main(args = process.argv.slice(2)) {
|
|
3642
|
+
async function main(args = process.argv.slice(2)) {
|
|
3402
3643
|
if (args.includes("--version")) {
|
|
3403
3644
|
process.stdout.write(ownVersion() + "\n");
|
|
3404
3645
|
return 0;
|
|
@@ -3415,25 +3656,24 @@ function main(args = process.argv.slice(2)) {
|
|
|
3415
3656
|
return status.failures.length ? 1 : 0;
|
|
3416
3657
|
}
|
|
3417
3658
|
if (command === "update") {
|
|
3418
|
-
const
|
|
3419
|
-
const
|
|
3659
|
+
const dryRun = args.includes("--dry-run");
|
|
3660
|
+
const face = openFace(args, json, dryRun);
|
|
3661
|
+
const { summary, rendered } = await runUpdate(args, json, face);
|
|
3420
3662
|
if (summary.reexec) return handOff(summary.reexec, args);
|
|
3421
|
-
if (face) finishFace(summary, null,
|
|
3422
|
-
else if (!rendered) printReconcile(summary, json,
|
|
3663
|
+
if (face) finishFace(face, summary, null, dryRun);
|
|
3664
|
+
else if (!rendered) printReconcile(summary, json, dryRun);
|
|
3423
3665
|
return summary.exit;
|
|
3424
3666
|
}
|
|
3425
3667
|
if (command === "install") {
|
|
3426
|
-
const face = openFace(args, json);
|
|
3427
3668
|
const dryRun = args.includes("--dry-run");
|
|
3669
|
+
const face = openFace(args, json, dryRun);
|
|
3428
3670
|
const shippedTarget = shippedTargetFrom(args);
|
|
3429
|
-
const { summary, rendered } = runUpdate(args, json, face, shippedTarget);
|
|
3671
|
+
const { summary, rendered } = await runUpdate(args, json, face, shippedTarget);
|
|
3430
3672
|
if (summary.reexec) {
|
|
3431
3673
|
const handedOff = handOff(summary.reexec, ["update", ...args.filter((arg) => arg !== "install" && arg !== "--shipped" && arg !== shippedTarget)]);
|
|
3432
3674
|
const scheduled = installTask();
|
|
3433
3675
|
if (!json) {
|
|
3434
|
-
process.stdout.write(scheduled
|
|
3435
|
-
` : ` Auto-update NOT enabled \u2014 ${scheduled.detail}
|
|
3436
|
-
`);
|
|
3676
|
+
process.stdout.write(autoupdateLine(scheduled) + "\n");
|
|
3437
3677
|
} else {
|
|
3438
3678
|
process.stdout.write(JSON.stringify({ convergence: summary, autoupdate: scheduled }, null, 2) + "\n");
|
|
3439
3679
|
}
|
|
@@ -3443,12 +3683,10 @@ function main(args = process.argv.slice(2)) {
|
|
|
3443
3683
|
if (json) {
|
|
3444
3684
|
process.stdout.write(JSON.stringify({ convergence: summary, autoupdate: schedule }, null, 2) + "\n");
|
|
3445
3685
|
} else if (face) {
|
|
3446
|
-
finishFace(summary, schedule, dryRun);
|
|
3686
|
+
finishFace(face, summary, schedule, dryRun);
|
|
3447
3687
|
} else {
|
|
3448
3688
|
if (!rendered) printReconcile(summary, false, dryRun);
|
|
3449
|
-
process.stdout.write(schedule
|
|
3450
|
-
` : ` Auto-update NOT enabled \u2014 ${schedule.detail}
|
|
3451
|
-
`);
|
|
3689
|
+
process.stdout.write(autoupdateLine(schedule) + "\n");
|
|
3452
3690
|
}
|
|
3453
3691
|
return Math.max(summary.exit, schedule.ok ? 0 : 1);
|
|
3454
3692
|
}
|
|
@@ -3468,17 +3706,25 @@ function main(args = process.argv.slice(2)) {
|
|
|
3468
3706
|
return 1;
|
|
3469
3707
|
}
|
|
3470
3708
|
if (typeof require !== "undefined" && require.main === module) {
|
|
3471
|
-
process.exit(
|
|
3709
|
+
main().then((code) => process.exit(code)).catch((error) => {
|
|
3710
|
+
process.stderr.write(`mmi-hub: ${error?.stack ?? error}
|
|
3711
|
+
`);
|
|
3712
|
+
process.exit(1);
|
|
3713
|
+
});
|
|
3472
3714
|
}
|
|
3473
3715
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3474
3716
|
0 && (module.exports = {
|
|
3475
3717
|
ACCENT,
|
|
3718
|
+
FACE_PRODUCT,
|
|
3476
3719
|
MM_GREEN,
|
|
3477
3720
|
RESET,
|
|
3478
3721
|
accent,
|
|
3479
3722
|
acquireSharedDocLock,
|
|
3723
|
+
attentionRows,
|
|
3724
|
+
autoupdateLine,
|
|
3480
3725
|
brandLine,
|
|
3481
3726
|
buildWatcherCommand,
|
|
3727
|
+
checkAtomicity,
|
|
3482
3728
|
checklistLine,
|
|
3483
3729
|
claudeArm,
|
|
3484
3730
|
claudeHostEvidence,
|
|
@@ -3518,7 +3764,6 @@ if (typeof require !== "undefined" && require.main === module) {
|
|
|
3518
3764
|
progressiveRow,
|
|
3519
3765
|
readLaunchTarget,
|
|
3520
3766
|
readPending,
|
|
3521
|
-
receipt,
|
|
3522
3767
|
receiptText,
|
|
3523
3768
|
reconcile,
|
|
3524
3769
|
reconcileCursorUserHooks,
|
|
@@ -3533,15 +3778,13 @@ if (typeof require !== "undefined" && require.main === module) {
|
|
|
3533
3778
|
sharedDocLockTimeoutReceipt,
|
|
3534
3779
|
shippedTargetFrom,
|
|
3535
3780
|
shouldHandOff,
|
|
3536
|
-
signOff,
|
|
3537
|
-
startSpinner,
|
|
3538
3781
|
surfaceArmPlan,
|
|
3539
3782
|
systemdServiceUnit,
|
|
3540
3783
|
systemdTimerUnit,
|
|
3541
3784
|
taskXml,
|
|
3542
3785
|
updateHeader,
|
|
3786
|
+
updaterFace,
|
|
3543
3787
|
watcherLockPath,
|
|
3544
|
-
welcome,
|
|
3545
3788
|
withSharedDocLock,
|
|
3546
3789
|
wrapWords,
|
|
3547
3790
|
writeLaunchTarget,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/hub",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.9",
|
|
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.1"
|
|
30
|
+
},
|
|
28
31
|
"devDependencies": {
|
|
29
32
|
"@types/node": "^22.0.0",
|
|
30
33
|
"esbuild": "^0.28.1",
|