@mutmutco/installer-launcher 0.1.8 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,35 +28,48 @@ __export(index_exports, {
28
28
  readPayloadVerbs: () => readPayloadVerbs,
29
29
  resolveEntry: () => resolveEntry,
30
30
  run: () => run,
31
- runFile: () => runFile
31
+ runFile: () => runFile,
32
+ runInstallEntry: () => runInstallEntry
32
33
  });
33
34
  module.exports = __toCommonJS(index_exports);
34
35
  var import_node_child_process3 = require("node:child_process");
35
- var import_node_fs5 = require("node:fs");
36
+ var import_node_fs9 = require("node:fs");
37
+ var import_node_os4 = require("node:os");
36
38
  var import_node_path4 = require("node:path");
37
39
  var import_node_url2 = require("node:url");
38
40
 
39
- // node_modules/@mutmutco/installer-face/dist/index.js
41
+ // ../face/src/face.ts
42
+ var import_node_fs = require("node:fs");
43
+
44
+ // ../face/src/products.ts
40
45
  var PRODUCTS = Object.freeze({
41
46
  "mm-strategy": Object.freeze({
42
47
  name: "MM Strategy",
48
+ installWarm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
43
49
  accent: "38;2;249;115;22",
44
- warm: "Welcome. Let's set up MM Strategy \u2014 about a minute."
50
+ warm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
51
+ doctor: "mm-strategy doctor"
45
52
  }),
46
53
  "mmi-hub": Object.freeze({
47
54
  name: "mmi-hub",
55
+ installWarm: "Welcome. Setting up mmi-hub \u2014 about a minute.",
48
56
  accent: "38;2;125;211;252",
49
- warm: "Welcome back. Checking your surfaces\u2026"
57
+ warm: "Welcome back. Checking your surfaces\u2026",
58
+ doctor: "mmi doctor"
50
59
  }),
51
60
  "jerv-hub": Object.freeze({
52
61
  name: "jerv-hub",
62
+ installWarm: "Welcome. Setting up jerv-hub \u2014 about a minute.",
53
63
  accent: "38;2;248;113;113",
54
- warm: "Welcome back. Checking your surfaces\u2026"
64
+ warm: "Welcome back. Checking your surfaces\u2026",
65
+ doctor: "jerv doctor"
55
66
  }),
56
67
  jervcode: Object.freeze({
57
68
  name: "JervCode",
69
+ installWarm: "Welcome. Setting up JervCode \u2014 about a minute.",
58
70
  accent: "38;2;192;132;252",
59
- warm: "Welcome back. Keeping JervCode current\u2026"
71
+ warm: "Welcome back. Keeping JervCode current\u2026",
72
+ doctor: "jervcode doctor"
60
73
  })
61
74
  });
62
75
  function identityFor(product) {
@@ -66,6 +79,8 @@ function identityFor(product) {
66
79
  }
67
80
  return identity;
68
81
  }
82
+
83
+ // ../face/src/face.ts
69
84
  var GLYPH = Object.freeze({
70
85
  diamond: "\u25C6",
71
86
  hollow: "\u25C7",
@@ -90,6 +105,9 @@ var ANSI = /\u001b\[[0-9;]*m/g;
90
105
  function visibleWidth(text) {
91
106
  return [...String(text).replace(ANSI, "")].length;
92
107
  }
108
+ function stripColor(text) {
109
+ return String(text).replace(ANSI, "");
110
+ }
93
111
  function faceWidth(columns) {
94
112
  const raw = Number(columns);
95
113
  return Math.max(40, Math.min(100, Number.isFinite(raw) && raw > 0 ? raw : 100));
@@ -118,7 +136,14 @@ function wrapWords(text, width) {
118
136
  if (line) lines.push(line);
119
137
  return lines.length ? lines : [""];
120
138
  }
121
- function createFace({ product, color = false, columns, env = process.env }) {
139
+ var PROGRESS_PROTOCOL = 1;
140
+ function readProgressFd(env) {
141
+ const fd = Number.parseInt(String(env.MM_PROGRESS_FD ?? ""), 10);
142
+ if (!Number.isInteger(fd) || fd <= 0) return null;
143
+ const protocol = Number.parseInt(String(env.MM_PROGRESS_PROTOCOL ?? PROGRESS_PROTOCOL), 10);
144
+ return protocol === PROGRESS_PROTOCOL ? fd : null;
145
+ }
146
+ function createFace({ product, color = false, columns, env = process.env, operation }) {
122
147
  const identity = identityFor(product);
123
148
  const width = faceWidth(columns);
124
149
  const paint = (sgr, text) => color ? `\x1B[${sgr}m${text}\x1B[0m` : String(text);
@@ -126,28 +151,45 @@ function createFace({ product, color = false, columns, env = process.env }) {
126
151
  const indent = " ".repeat(TITLE_COLUMN - 1);
127
152
  const continuedPhases = new Set((env.MM_FACE_CONTINUES ?? "").split(",").map((phase) => phase.trim()).filter(Boolean));
128
153
  const continuesFace = continuedPhases.size > 0;
129
- const welcome = () => continuesFace ? [] : [
154
+ const nested = env.MM_OUTER_CONSOLE === "1";
155
+ const progressFd = readProgressFd(env);
156
+ const emitMilestone = (title, measure, kind) => {
157
+ if (progressFd === null) return false;
158
+ const record = { v: PROGRESS_PROTOCOL, step: stripColor(title), state: kind };
159
+ if (typeof measure === "number") record.ms = Math.max(0, Math.round(measure * 1e3));
160
+ try {
161
+ (0, import_node_fs.writeSync)(progressFd, `${JSON.stringify(record)}
162
+ `);
163
+ return true;
164
+ } catch {
165
+ return false;
166
+ }
167
+ };
168
+ const welcome = () => continuesFace || nested ? [] : [
130
169
  `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
131
170
  bar(),
132
- `${bar()} ${identity.warm}`,
171
+ `${bar()} ${operation === "install" ? identity.installWarm : identity.warm}`,
133
172
  bar()
134
173
  ];
135
174
  const continues = (phase, kind = "ok") => {
136
175
  const inherited = continuedPhases.delete(String(phase).trim());
137
176
  return kind === "fail" ? false : inherited;
138
177
  };
139
- const step = (title, seconds = null, kind = "ok") => {
178
+ const step = (title, measure = null, kind = "ok") => {
179
+ if (emitMilestone(title, measure, kind)) return "";
140
180
  const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
141
- const time = seconds === null || seconds === void 0 ? "" : paint(PALETTE.muted, `${Math.max(0, Math.round(seconds))}s`);
181
+ const measured = measure === null || measure === void 0 ? "" : paint(PALETTE.muted, typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : String(measure).trim());
182
+ const time = measured;
142
183
  const column = Math.min(44, Math.max(0, width - 8));
143
- const reserved = time ? 6 : 0;
184
+ const reserved = time ? Math.max(6, visibleWidth(time) + 2) : 0;
144
185
  const [first, ...rest] = wrapWords(title, width - TITLE_COLUMN - reserved);
145
186
  const head = `${bar()} ${glyph} ${first}`;
146
187
  const pad = Math.max(1, column - visibleWidth(head));
147
188
  return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
148
189
  };
149
- const relay = (text) => String(text).split("\n").map((line) => line.trim() === "" ? bar() : `${bar()}${indent}${line}`).join("\n");
150
- const receipt = (lines) => {
190
+ const relay = (text) => String(text).split("\n").flatMap((line) => line.trim() === "" ? [bar()] : (visibleWidth(line) <= width - TITLE_COLUMN ? [line] : wrapWords(line, width - TITLE_COLUMN)).map((part) => `${bar()}${indent}${part}`)).join("\n");
191
+ const receipt = (lines, { ready = true } = {}) => {
192
+ if (ready && nested) return [];
151
193
  const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
152
194
  const line = String(raw);
153
195
  if (visibleWidth(line) <= width - 6) return [line];
@@ -163,18 +205,377 @@ function createFace({ product, color = false, columns, env = process.env }) {
163
205
  frame(GLYPH.boxBottom, GLYPH.boxBottomEnd)
164
206
  ];
165
207
  };
166
- const signOff = () => `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
208
+ const outcome = (changed, options = {}) => {
209
+ const fact = String(changed ?? "").trim();
210
+ if (!fact) {
211
+ throw new Error(`installer face: the receipt must say what changed \u2014 pass the one measured fact this run produced, such as "Updated 2 of 7 surfaces to 4.4.10." (${identity.name})`);
212
+ }
213
+ return receipt([
214
+ `${GLYPH.check} ${identity.name} is ready.`,
215
+ fact,
216
+ `Check health any time: ${identity.doctor}`
217
+ ], options);
218
+ };
219
+ const signOff = () => nested ? "" : `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
167
220
  const refusal = (message) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message}`;
168
- return { identity, width, welcome, continues, step, relay, receipt, signOff, refusal, paint };
221
+ return { identity, width, nested, emitsProgress: progressFd !== null, welcome, continues, step, relay, receipt, outcome, signOff, refusal, paint };
169
222
  }
223
+
224
+ // ../face/src/shell.ts
170
225
  var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
226
+
227
+ // ../face/src/spinner.ts
228
+ var import_node_fs2 = require("node:fs");
229
+ var import_node_worker_threads = require("node:worker_threads");
171
230
  var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
231
+ var WORKER_SOURCE = `
232
+ const { parentPort, workerData } = require('node:worker_threads');
233
+ const { writeSync, appendFileSync } = require('node:fs');
234
+ const control = new Int32Array(workerData.control);
235
+ let frames = workerData.frames, frame = workerData.frame;
236
+ function draw() {
237
+ if (Atomics.compareExchange(control, 1, 0, 1) !== 0) return;
238
+ try {
239
+ if (Atomics.load(control, 0) || Atomics.load(control, 2) || !frames.length) return;
240
+ const text = frames[frame++ % frames.length];
241
+ writeSync(2, text);
242
+ if (workerData.transcriptPath) appendFileSync(workerData.transcriptPath, JSON.stringify({ channel: 'spinner', text }) + '\\n');
243
+ } finally {
244
+ Atomics.store(control, 1, 0);
245
+ Atomics.notify(control, 1);
246
+ }
247
+ }
248
+ parentPort.on('message', (next) => {
249
+ if (Atomics.load(control, 2)) return;
250
+ frames = next.frames;
251
+ frame = next.frame;
252
+ Atomics.store(control, 0, 0);
253
+ });
254
+ setInterval(draw, workerData.intervalMs);
255
+ `;
256
+ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath }) {
257
+ animate = animate && !face.nested && !face.emitsProgress;
258
+ let timer = null;
259
+ let worker = null;
260
+ let control = null;
261
+ let frames = [];
262
+ let frame = 0;
263
+ const write = (text) => {
264
+ if (stream) stream.write(text);
265
+ else {
266
+ (0, import_node_fs2.writeSync)(2, text);
267
+ if (transcriptPath) (0, import_node_fs2.appendFileSync)(transcriptPath, `${JSON.stringify({ channel: "spinner", text })}
268
+ `);
269
+ }
270
+ };
271
+ const render = (text, measure) => {
272
+ const line = face.step(text, measure, "note").split("\n")[0];
273
+ frames = line ? FRAMES.map((glyph) => `\r\x1B[2K${line.replace(GLYPH.dot, glyph)}`) : [];
274
+ };
275
+ const draw = () => {
276
+ if (animate && frames.length) write(frames[frame++ % frames.length]);
277
+ };
278
+ const pause = () => {
279
+ if (!control) return;
280
+ Atomics.store(control, 0, 1);
281
+ while (Atomics.load(control, 1)) Atomics.wait(control, 1, 1);
282
+ };
283
+ const halt = () => {
284
+ if (timer) clearInterval(timer);
285
+ timer = null;
286
+ if (control) Atomics.store(control, 2, 1);
287
+ pause();
288
+ if (worker) void worker.terminate();
289
+ worker = null;
290
+ control = null;
291
+ };
292
+ return {
293
+ start(text, measure = null) {
294
+ if (!animate) return;
295
+ halt();
296
+ render(text, measure);
297
+ frame = 0;
298
+ draw();
299
+ if (!frames.length) return;
300
+ if (stream) timer = setInterval(draw, intervalMs).unref();
301
+ else {
302
+ control = new Int32Array(new SharedArrayBuffer(12));
303
+ try {
304
+ worker = new import_node_worker_threads.Worker(WORKER_SOURCE, { eval: true, workerData: {
305
+ control: control.buffer,
306
+ frames,
307
+ frame,
308
+ intervalMs,
309
+ transcriptPath
310
+ } });
311
+ const active = worker;
312
+ worker.on("error", () => {
313
+ if (worker === active) halt();
314
+ });
315
+ worker.unref();
316
+ } catch {
317
+ halt();
318
+ }
319
+ }
320
+ },
321
+ say(text, measure = null) {
322
+ if (!animate) return;
323
+ pause();
324
+ render(text, measure);
325
+ draw();
326
+ worker?.postMessage({ frames, frame });
327
+ },
328
+ stop() {
329
+ halt();
330
+ if (animate) write("\r\x1B[2K");
331
+ }
332
+ };
333
+ }
172
334
  var SPINNER_FRAMES = Object.freeze([...FRAMES]);
335
+
336
+ // ../face/src/conformance.ts
173
337
  var ALLOWED = new Set(Object.values(GLYPH));
174
338
 
339
+ // ../face/src/run.ts
340
+ var import_node_fs4 = require("node:fs");
341
+
342
+ // ../face/src/outcome.ts
343
+ var import_node_fs3 = require("node:fs");
344
+ var counts = ["total", "updated", "failed"];
345
+ var strings = ["version", "retry", "detail", "logPath"];
346
+ var flags = ["dryRun", "installed", "deferred", "operationFailed"];
347
+ function validateInstallerOutcome(value) {
348
+ const invalid = () => {
349
+ throw new Error("installer outcome: invalid child result");
350
+ };
351
+ if (!value || typeof value !== "object" || Array.isArray(value)) return invalid();
352
+ const facts = value;
353
+ const allowed = [...counts, ...strings, ...flags, "rollback", "logState"];
354
+ if (Object.keys(facts).some((key) => !allowed.includes(key))) return invalid();
355
+ for (const key of counts) if (!Number.isSafeInteger(facts[key]) || facts[key] < 0) return invalid();
356
+ if (facts.updated + facts.failed > facts.total) return invalid();
357
+ for (const key of strings) if (facts[key] !== void 0 && (typeof facts[key] !== "string" || facts[key].length > 4096)) return invalid();
358
+ for (const key of flags) if (facts[key] !== void 0 && typeof facts[key] !== "boolean") return invalid();
359
+ if (facts.rollback !== void 0 && !["completed", "partial", "not-needed", "unknown"].includes(facts.rollback)) return invalid();
360
+ if (facts.logState !== void 0 && !["unavailable", "omitted"].includes(facts.logState)) return invalid();
361
+ if (facts.logPath !== void 0 && (!facts.logPath.trim() || /[\x00-\x1f\x7f]/u.test(facts.logPath))) return invalid();
362
+ return { ...facts };
363
+ }
364
+ function writeInstallerOutcome(path, value) {
365
+ (0, import_node_fs3.writeFileSync)(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
366
+ }
367
+ function readInstallerOutcome(path) {
368
+ let text;
369
+ try {
370
+ text = (0, import_node_fs3.readFileSync)(path, "utf8");
371
+ } catch (error) {
372
+ if (error.code === "ENOENT") return void 0;
373
+ throw error;
374
+ }
375
+ if (text.length > 16384) throw new Error("installer outcome: child result is too large");
376
+ try {
377
+ return validateInstallerOutcome(JSON.parse(text));
378
+ } catch {
379
+ throw new Error("installer outcome: invalid child result");
380
+ }
381
+ }
382
+
383
+ // ../face/src/run.ts
384
+ function validateInstallerProduct(value) {
385
+ const fail = (field2) => {
386
+ throw new Error(`installer product: invalid ${field2}`);
387
+ };
388
+ if (!value || typeof value !== "object" || Array.isArray(value)) return fail("declaration");
389
+ const input = value;
390
+ const text = (value2, field2) => typeof value2 === "string" && value2.trim() && !/[\r\n\x00-\x1f]/u.test(value2) ? value2 : fail(field2);
391
+ const key = text(input.product, "product");
392
+ const product = { mmi: "mmi-hub", jerv: "jerv-hub" }[key] ?? key;
393
+ const identity = identityFor(product);
394
+ const gate = text(input.gate, "gate");
395
+ let gateUrl;
396
+ try {
397
+ gateUrl = new URL(gate);
398
+ } catch {
399
+ return fail("gate");
400
+ }
401
+ if (!["https:", "http:"].includes(gateUrl.protocol) || gateUrl.username || gateUrl.password) return fail("gate");
402
+ const doctor = text(input.doctor, "doctor");
403
+ if (doctor !== identity.doctor) return fail("doctor");
404
+ if (!Array.isArray(input.surfaces) || input.surfaces.length === 0) return fail("surfaces");
405
+ const ids = /* @__PURE__ */ new Set();
406
+ const surfaces = input.surfaces.map((raw) => {
407
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail("surface");
408
+ const source = raw;
409
+ const id = text(source.id, "surface.id");
410
+ if (ids.has(id)) return fail("duplicate surface.id");
411
+ ids.add(id);
412
+ const surface = { id };
413
+ for (const field2 of ["npm", "bin", "kind", "activation"]) {
414
+ if (source[field2] !== void 0) surface[field2] = text(source[field2], `surface.${field2}`);
415
+ }
416
+ if (Boolean(surface.npm) !== Boolean(surface.bin)) return fail("surface npm/bin pair");
417
+ if (surface.kind !== void 0 && !["agent-home", "payload"].includes(surface.kind)) return fail("surface.kind");
418
+ if (!surface.npm && !surface.kind) return fail("surface implementation");
419
+ return surface;
420
+ });
421
+ return { product, gate, doctor, surfaces };
422
+ }
423
+ var PHASES = {
424
+ preflight: ["Checking prerequisites", "Checked prerequisites"],
425
+ resolve: ["Resolving the release", "Resolved the release"],
426
+ download: ["Downloading the payload", "Downloaded the payload"],
427
+ "sign-in": ["Signing in", "Signed in"],
428
+ check: ["Checking surfaces", "Checked surfaces"],
429
+ arm: ["Scheduling updates", "Armed hourly updates"],
430
+ "verify-release": ["Checking the release version", "Verified the release version"],
431
+ verify: ["Verifying the payload", "Verified the payload"],
432
+ install: ["Installing the product", "Installed the product"],
433
+ activate: ["Activating surfaces", "Activated surfaces"],
434
+ doctor: ["Checking health", "Checked health"],
435
+ rollback: ["Restoring the previous version", "Restored the previous version"]
436
+ };
437
+ function createInstallerRun(value, options = {}) {
438
+ const declaration = validateInstallerProduct(value);
439
+ const env = options.env ?? process.env;
440
+ const tty = options.tty ?? Boolean(process.stdout.isTTY);
441
+ const face = createFace({
442
+ operation: options.operation,
443
+ product: declaration.product,
444
+ columns: options.columns,
445
+ env,
446
+ color: tty && options.color !== false && env.NO_COLOR === void 0
447
+ });
448
+ const errors = [];
449
+ const write = options.write ? (text, channel) => {
450
+ try {
451
+ options.write(text, channel);
452
+ } catch (error) {
453
+ errors.push(`installer output observer: ${error instanceof Error ? error.message : String(error)}`);
454
+ }
455
+ } : (text, channel) => {
456
+ (channel === "stdout" ? process.stdout : process.stderr).write(text);
457
+ };
458
+ const emit = (text, channel = "stdout", recorded = text) => {
459
+ if (!text) return;
460
+ write(text, channel);
461
+ if (env.MM_FACE_TRANSCRIPT) {
462
+ (0, import_node_fs4.appendFileSync)(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
463
+ `, "utf8");
464
+ }
465
+ };
466
+ const lines = (rows, channel = "stdout") => {
467
+ for (const row of rows.flatMap((row2) => row2.split("\n"))) if (row) emit(`${row}
468
+ `, channel);
469
+ };
470
+ const spinner = createSpinner(face, {
471
+ animate: tty && env.TERM !== "dumb" && (options.animate ?? Boolean(process.stderr.isTTY)),
472
+ ...options.write ? { stream: { write: (text) => {
473
+ emit(String(text), "spinner");
474
+ return true;
475
+ } } } : { transcriptPath: env.MM_FACE_TRANSCRIPT }
476
+ });
477
+ let started = false;
478
+ let finished = false;
479
+ const start = () => {
480
+ if (started || finished) return;
481
+ started = true;
482
+ const welcome = face.welcome();
483
+ if (tty) lines(welcome);
484
+ else if (welcome.length) lines([`${face.identity.name} \u2014 Mutatis Mutandis`, options.operation === "install" ? face.identity.installWarm : face.identity.warm]);
485
+ };
486
+ const durable = (title, measure, kind) => {
487
+ spinner.stop();
488
+ const rendered = face.step(title, measure, kind);
489
+ if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
490
+ };
491
+ const run2 = {
492
+ get errors() {
493
+ return [...errors];
494
+ },
495
+ start,
496
+ phase(id, facts = {}) {
497
+ if (finished) throw new Error("installer run already finished");
498
+ if (!Object.hasOwn(PHASES, id)) throw new Error("installer run: unknown phase");
499
+ start();
500
+ const state = facts.state ?? "ok";
501
+ const title = PHASES[id][state === "ok" ? 1 : 0];
502
+ if (state === "running") {
503
+ spinner.start(title, facts.measure ?? null);
504
+ return;
505
+ }
506
+ if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
507
+ if (facts.detail) run2.relay(facts.detail);
508
+ },
509
+ surface(facts) {
510
+ if (finished) throw new Error("installer run already finished");
511
+ const surface = declaration.surfaces.find((surface2) => surface2.id === facts.id);
512
+ if (!surface) throw new Error("installer run: undeclared surface");
513
+ start();
514
+ const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} \u2192 ${facts.to}` : ` ${facts.to}` : "";
515
+ const status = { updated: options.dryRun ? "would update" : "updated", current: "already current", failed: "failed", skipped: "skipped", retry: "retrying", pending: "pending", kept: "kept" }[facts.state];
516
+ if (!status) throw new Error("installer run: unknown surface state");
517
+ const activation = facts.state === "updated" && surface.activation ? ` \xB7 ${surface.activation}` : "";
518
+ const completed = ["updated", "current", "kept", "skipped"].includes(facts.state);
519
+ durable(`${facts.id}${versions} \xB7 ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
520
+ if (facts.detail) run2.relay(facts.detail);
521
+ },
522
+ milestone({ step, state, ms }) {
523
+ start();
524
+ durable(step, ms === void 0 ? null : ms / 1e3, state);
525
+ },
526
+ signIn({ url, code }) {
527
+ start();
528
+ spinner.stop();
529
+ for (const text of [`Open ${url}`, `Enter code: ${code}`]) {
530
+ const rendered = `${tty ? face.relay(text) : text}
531
+ `;
532
+ emit(rendered, "stdout", rendered.replace(code, "[redacted]"));
533
+ }
534
+ },
535
+ // Only pass safe diagnostic text, never authentication output or credentials.
536
+ relay(text, channel = "stdout", record = true) {
537
+ spinner.stop();
538
+ const rendered = tty ? face.relay(text) : stripColor(text);
539
+ for (const row of rendered.split("\n")) if (row) {
540
+ emit(`${row}
541
+ `, channel, record ? `${row}
542
+ ` : `${tty ? face.relay("[external output omitted]") : "[external output omitted]"}
543
+ `);
544
+ }
545
+ },
546
+ finish(facts) {
547
+ if (finished) return;
548
+ validateInstallerOutcome(facts);
549
+ if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(env.MM_INSTALLER_OUTCOME_FILE, facts);
550
+ start();
551
+ spinner.stop();
552
+ finished = true;
553
+ const changed = !facts.version ? "No release target is available." : facts.dryRun ? `Would update ${facts.updated} of ${facts.total} surfaces to ${facts.version}.` : facts.installed ? `Installed ${facts.version} across ${facts.total} surfaces.` : `Updated ${facts.updated} of ${facts.total} surfaces to ${facts.version}.`;
554
+ const ready = facts.failed === 0 && !facts.dryRun && !facts.deferred && !facts.operationFailed && Boolean(facts.version);
555
+ const body = [
556
+ `${ready ? GLYPH.check : facts.dryRun ? GLYPH.dot : GLYPH.cross} ${face.identity.name}${ready ? " is ready." : facts.dryRun ? " preview complete." : facts.deferred ? " update deferred." : " is not ready yet."}`,
557
+ changed,
558
+ ...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
559
+ ...facts.detail ? [facts.detail] : [],
560
+ ...facts.rollback ? [`Rollback: ${facts.rollback}`] : [],
561
+ ...facts.logPath ? [`Log: ${facts.logPath}`] : facts.logState ? [`Log: ${facts.logState}`] : [],
562
+ facts.retry && (facts.failed > 0 || facts.operationFailed) ? `Retry: ${facts.retry}` : `Check health any time: ${declaration.doctor}`
563
+ ];
564
+ if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE && (facts.failed > 0 || facts.operationFailed)) {
565
+ lines(tty ? face.receipt(body, { ready }) : body.map((row) => row.replace(/^[✔✖●] /u, "")));
566
+ }
567
+ if (tty) lines([face.signOff()]);
568
+ },
569
+ stop() {
570
+ spinner.stop();
571
+ }
572
+ };
573
+ return run2;
574
+ }
575
+
175
576
  // src/autoupdate.ts
176
577
  var import_node_child_process = require("node:child_process");
177
- var import_node_fs = require("node:fs");
578
+ var import_node_fs5 = require("node:fs");
178
579
  var import_node_os = require("node:os");
179
580
  var import_node_path = require("node:path");
180
581
  function schedulePlatform(override) {
@@ -221,9 +622,9 @@ function enableSchedule(config, command, options = {}) {
221
622
  if (platform === "darwin") {
222
623
  const label2 = scheduleLabel(config);
223
624
  const dir2 = (0, import_node_path.join)(home, "Library", "LaunchAgents");
224
- (0, import_node_fs.mkdirSync)(dir2, { recursive: true });
625
+ (0, import_node_fs5.mkdirSync)(dir2, { recursive: true });
225
626
  const plist = (0, import_node_path.join)(dir2, `${label2}.plist`);
226
- (0, import_node_fs.writeFileSync)(plist, darwinPlist(label2, command));
627
+ (0, import_node_fs5.writeFileSync)(plist, darwinPlist(label2, command));
227
628
  exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
228
629
  const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
229
630
  if (result2.code !== 0) {
@@ -233,9 +634,9 @@ function enableSchedule(config, command, options = {}) {
233
634
  }
234
635
  const label = scheduleLabel(config);
235
636
  const dir = (0, import_node_path.join)(home, ".config", "systemd", "user");
236
- (0, import_node_fs.mkdirSync)(dir, { recursive: true });
237
- (0, import_node_fs.writeFileSync)((0, import_node_path.join)(dir, `${label}.service`), linuxService(command));
238
- (0, import_node_fs.writeFileSync)((0, import_node_path.join)(dir, `${label}.timer`), linuxTimer(label));
637
+ (0, import_node_fs5.mkdirSync)(dir, { recursive: true });
638
+ (0, import_node_fs5.writeFileSync)((0, import_node_path.join)(dir, `${label}.service`), linuxService(command));
639
+ (0, import_node_fs5.writeFileSync)((0, import_node_path.join)(dir, `${label}.timer`), linuxTimer(label));
239
640
  const reload = exec("systemctl", ["--user", "daemon-reload"]);
240
641
  if (reload.code !== 0) {
241
642
  throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
@@ -260,14 +661,14 @@ function disableSchedule(config, options = {}) {
260
661
  if (platform === "darwin") {
261
662
  const label2 = scheduleLabel(config);
262
663
  exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
263
- (0, import_node_fs.rmSync)((0, import_node_path.join)(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
664
+ (0, import_node_fs5.rmSync)((0, import_node_path.join)(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
264
665
  return;
265
666
  }
266
667
  const label = scheduleLabel(config);
267
668
  const dir = (0, import_node_path.join)(home, ".config", "systemd", "user");
268
669
  exec("systemctl", ["--user", "disable", "--now", `${label}.timer`]);
269
- (0, import_node_fs.rmSync)((0, import_node_path.join)(dir, `${label}.service`), { force: true });
270
- (0, import_node_fs.rmSync)((0, import_node_path.join)(dir, `${label}.timer`), { force: true });
670
+ (0, import_node_fs5.rmSync)((0, import_node_path.join)(dir, `${label}.service`), { force: true });
671
+ (0, import_node_fs5.rmSync)((0, import_node_path.join)(dir, `${label}.timer`), { force: true });
271
672
  }
272
673
  function querySchedule(config, options = {}) {
273
674
  const platform = schedulePlatform(options.platform);
@@ -286,11 +687,11 @@ function querySchedule(config, options = {}) {
286
687
  }
287
688
  if (platform === "darwin") {
288
689
  const plist = (0, import_node_path.join)(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
289
- if (!(0, import_node_fs.existsSync)(plist)) return { supported: true, enabled: false };
690
+ if (!(0, import_node_fs5.existsSync)(plist)) return { supported: true, enabled: false };
290
691
  return { supported: true, enabled: true, cadence: "hourly" };
291
692
  }
292
693
  const timer = (0, import_node_path.join)(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
293
- if (!(0, import_node_fs.existsSync)(timer)) return { supported: true, enabled: false };
694
+ if (!(0, import_node_fs5.existsSync)(timer)) return { supported: true, enabled: false };
294
695
  const state = { supported: true, enabled: true, cadence: "hourly" };
295
696
  const shown = exec("systemctl", ["--user", "show", `${scheduleLabel(config)}.service`, "-p", "ExecMainStartTimestamp", "--value"]);
296
697
  const stamp = (shown.stdout ?? "").trim();
@@ -348,7 +749,7 @@ function firstLine(text) {
348
749
  }
349
750
 
350
751
  // src/config.ts
351
- var import_node_fs2 = require("node:fs");
752
+ var import_node_fs6 = require("node:fs");
352
753
  var import_node_sea = require("node:sea");
353
754
 
354
755
  // src/module-url.ts
@@ -371,10 +772,10 @@ function loadProductConfig(options = {}) {
371
772
  const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
372
773
  const devFallback = new URL("../config/product.template.json", moduleUrl());
373
774
  if (explicit) {
374
- return parseProductConfig((0, import_node_fs2.readFileSync)(explicit, "utf8"));
775
+ return parseProductConfig((0, import_node_fs6.readFileSync)(explicit, "utf8"));
375
776
  }
376
777
  try {
377
- return parseProductConfig((0, import_node_fs2.readFileSync)(devFallback, "utf8"));
778
+ return parseProductConfig((0, import_node_fs6.readFileSync)(devFallback, "utf8"));
378
779
  } catch {
379
780
  }
380
781
  try {
@@ -493,7 +894,8 @@ async function loginGithub(config, options = {}) {
493
894
  const sleep = options.sleep ?? realSleep;
494
895
  const issued = await requestDeviceCode(config, fetchImpl);
495
896
  const openUrl = issued.verification_uri_complete ?? issued.verification_uri;
496
- print(`To sign in, open ${issued.verification_uri} and enter the code ${issued.user_code}.`);
897
+ if (options.onDeviceCode) options.onDeviceCode({ url: issued.verification_uri, code: issued.user_code });
898
+ else print(`To sign in, open ${issued.verification_uri} and enter the code ${issued.user_code}.`);
497
899
  open(openUrl);
498
900
  const deadline = now() + issued.expires_in * 1e3;
499
901
  let intervalMs = Math.max(1, issued.interval) * 1e3;
@@ -663,7 +1065,7 @@ function page(title, body) {
663
1065
 
664
1066
  // src/payload.ts
665
1067
  var import_node_crypto2 = require("node:crypto");
666
- var import_node_fs3 = require("node:fs");
1068
+ var import_node_fs7 = require("node:fs");
667
1069
  var import_node_os2 = require("node:os");
668
1070
  var import_node_path2 = require("node:path");
669
1071
 
@@ -806,7 +1208,7 @@ async function fetchFileBytes(config, accessToken, path, fetchImpl) {
806
1208
  async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
807
1209
  const fetchImpl = options.fetchImpl ?? fetch;
808
1210
  const staging = (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
809
- (0, import_node_fs3.mkdirSync)(staging, { recursive: true });
1211
+ (0, import_node_fs7.mkdirSync)(staging, { recursive: true });
810
1212
  try {
811
1213
  for (const entry of manifest.files) {
812
1214
  const bytes = await fetchFileBytes(config, accessToken, entry.path, fetchImpl);
@@ -814,22 +1216,22 @@ async function downloadAndUnpack(config, dir, manifest, accessToken, options = {
814
1216
  throw new Error(`file ${entry.path} failed its checksum \u2014 refusing to install anything.`);
815
1217
  }
816
1218
  const dest = (0, import_node_path2.join)(staging, entry.path);
817
- (0, import_node_fs3.mkdirSync)((0, import_node_path2.dirname)(dest), { recursive: true });
818
- (0, import_node_fs3.writeFileSync)(dest, bytes);
1219
+ (0, import_node_fs7.mkdirSync)((0, import_node_path2.dirname)(dest), { recursive: true });
1220
+ (0, import_node_fs7.writeFileSync)(dest, bytes);
819
1221
  }
820
1222
  const target = (0, import_node_path2.join)(dir, "payload");
821
- (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
822
- (0, import_node_fs3.rmSync)(target, { force: true, recursive: true });
823
- (0, import_node_fs3.renameSync)(staging, target);
1223
+ (0, import_node_fs7.mkdirSync)(dir, { recursive: true });
1224
+ (0, import_node_fs7.rmSync)(target, { force: true, recursive: true });
1225
+ (0, import_node_fs7.renameSync)(staging, target);
824
1226
  } catch (error) {
825
- (0, import_node_fs3.rmSync)(staging, { force: true, recursive: true });
1227
+ (0, import_node_fs7.rmSync)(staging, { force: true, recursive: true });
826
1228
  throw error;
827
1229
  }
828
1230
  return manifest.version;
829
1231
  }
830
1232
 
831
1233
  // src/store.ts
832
- var import_node_fs4 = require("node:fs");
1234
+ var import_node_fs8 = require("node:fs");
833
1235
  var import_node_os3 = require("node:os");
834
1236
  var import_node_path3 = require("node:path");
835
1237
  function defaultProductDir(product) {
@@ -854,7 +1256,7 @@ function payloadDir(dir) {
854
1256
  }
855
1257
  function readTokens(dir) {
856
1258
  try {
857
- const data = JSON.parse((0, import_node_fs4.readFileSync)(tokensPath(dir), "utf8"));
1259
+ const data = JSON.parse((0, import_node_fs8.readFileSync)(tokensPath(dir), "utf8"));
858
1260
  if (typeof data.accessToken !== "string" || !data.accessToken) return null;
859
1261
  if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
860
1262
  const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
@@ -866,21 +1268,21 @@ function readTokens(dir) {
866
1268
  }
867
1269
  }
868
1270
  function writeTokens(dir, tokens) {
869
- (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
1271
+ (0, import_node_fs8.mkdirSync)(dir, { recursive: true });
870
1272
  try {
871
- (0, import_node_fs4.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
1273
+ (0, import_node_fs8.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
872
1274
  `, { mode: 384 });
873
1275
  } catch {
874
- (0, import_node_fs4.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
1276
+ (0, import_node_fs8.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
875
1277
  `);
876
1278
  }
877
1279
  }
878
1280
  function clearTokens(dir) {
879
- (0, import_node_fs4.rmSync)(tokensPath(dir), { force: true });
1281
+ (0, import_node_fs8.rmSync)(tokensPath(dir), { force: true });
880
1282
  }
881
1283
  function readState(dir) {
882
1284
  try {
883
- const data = JSON.parse((0, import_node_fs4.readFileSync)(statePath(dir), "utf8"));
1285
+ const data = JSON.parse((0, import_node_fs8.readFileSync)(statePath(dir), "utf8"));
884
1286
  if (typeof data.version !== "string" || !data.version) return null;
885
1287
  return { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
886
1288
  } catch {
@@ -888,18 +1290,18 @@ function readState(dir) {
888
1290
  }
889
1291
  }
890
1292
  function writeState(dir, state) {
891
- (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
892
- (0, import_node_fs4.writeFileSync)(statePath(dir), `${JSON.stringify(state, null, 2)}
1293
+ (0, import_node_fs8.mkdirSync)(dir, { recursive: true });
1294
+ (0, import_node_fs8.writeFileSync)(statePath(dir), `${JSON.stringify(state, null, 2)}
893
1295
  `);
894
1296
  }
895
1297
  function wipeProductDir(dir) {
896
- (0, import_node_fs4.rmSync)(tokensPath(dir), { force: true });
897
- (0, import_node_fs4.rmSync)(statePath(dir), { force: true });
898
- (0, import_node_fs4.rmSync)(payloadDir(dir), { force: true, recursive: true });
1298
+ (0, import_node_fs8.rmSync)(tokensPath(dir), { force: true });
1299
+ (0, import_node_fs8.rmSync)(statePath(dir), { force: true });
1300
+ (0, import_node_fs8.rmSync)(payloadDir(dir), { force: true, recursive: true });
899
1301
  }
900
1302
 
901
1303
  // src/index.ts
902
- var LAUNCHER_VERSION = true ? "0.1.8" : readVersionFromPackage();
1304
+ var LAUNCHER_VERSION = true ? "0.1.11" : readVersionFromPackage();
903
1305
  function defaultPrint(message) {
904
1306
  process.stdout.write(`${message}
905
1307
  `);
@@ -979,7 +1381,7 @@ async function runFile(file, args, printErr) {
979
1381
  return 1;
980
1382
  }
981
1383
  const abs = (0, import_node_path4.resolve)(process.cwd(), file);
982
- if (!(0, import_node_fs5.existsSync)(abs)) {
1384
+ if (!(0, import_node_fs9.existsSync)(abs)) {
983
1385
  printErr(`cannot run ${file}: no such file.`);
984
1386
  return 1;
985
1387
  }
@@ -1035,11 +1437,16 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
1035
1437
  });
1036
1438
  return refreshed.accessToken;
1037
1439
  }
1038
- async function doLogin(config, dir, options, print) {
1440
+ async function doLogin(config, dir, options, print, installer) {
1039
1441
  const fetchImpl = options.fetchImpl ?? fetch;
1040
1442
  const open = options.open ?? openBrowser;
1041
1443
  if (config.loginKind === "github") {
1042
- const tokens = await loginGithub(config, { fetchImpl, open, print });
1444
+ const tokens = await loginGithub(config, {
1445
+ fetchImpl,
1446
+ open,
1447
+ print,
1448
+ ...installer ? { onDeviceCode: (prompt) => installer.signIn(prompt) } : {}
1449
+ });
1043
1450
  writeTokens(dir, {
1044
1451
  accessToken: tokens.accessToken,
1045
1452
  refreshToken: tokens.refreshToken,
@@ -1054,20 +1461,20 @@ async function doLogin(config, dir, options, print) {
1054
1461
  clientId: tokens.clientId
1055
1462
  });
1056
1463
  }
1057
- print(`signed in to ${config.product}.`);
1464
+ if (!installer) print(`signed in to ${config.product}.`);
1058
1465
  }
1059
- async function fetchAccessTokenOrLogin(config, dir, options, print) {
1466
+ async function fetchAccessTokenOrLogin(config, dir, options, print, installer) {
1060
1467
  const fetchImpl = options.fetchImpl ?? fetch;
1061
1468
  const fresh = await ensureFreshToken(config, dir, fetchImpl);
1062
1469
  if (fresh) return fresh;
1063
- await doLogin(config, dir, options, print);
1470
+ await doLogin(config, dir, options, print, installer);
1064
1471
  const after = readTokens(dir);
1065
1472
  if (!after) throw new Error("sign-in did not produce a token");
1066
1473
  return after.accessToken;
1067
1474
  }
1068
1475
  function readPayloadArgv(dir, key) {
1069
1476
  try {
1070
- const parsed = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(payloadDir(dir), "payload.json"), "utf8"));
1477
+ const parsed = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path4.join)(payloadDir(dir), "payload.json"), "utf8"));
1071
1478
  const entry = parsed[key];
1072
1479
  if (typeof entry === "string") {
1073
1480
  const parts = entry.trim().split(/\s+/).filter(Boolean);
@@ -1089,7 +1496,7 @@ function readPayloadRun(dir) {
1089
1496
  }
1090
1497
  function readPayloadVerbs(dir) {
1091
1498
  try {
1092
- const parsed = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(payloadDir(dir), "payload.json"), "utf8"));
1499
+ const parsed = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path4.join)(payloadDir(dir), "payload.json"), "utf8"));
1093
1500
  const verbs = parsed.verbs;
1094
1501
  if (verbs === "*") return "*";
1095
1502
  if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
@@ -1106,7 +1513,7 @@ function resolveEntry(entry) {
1106
1513
  function needsShell(command) {
1107
1514
  if (process.platform !== "win32") return false;
1108
1515
  if (/\.(cmd|bat)$/i.test(command)) return true;
1109
- return !(0, import_node_fs5.existsSync)(command);
1516
+ return !(0, import_node_fs9.existsSync)(command);
1110
1517
  }
1111
1518
  function quoteForShell(arg) {
1112
1519
  return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
@@ -1137,7 +1544,7 @@ function defaultRunEntry(entry, cwd, env) {
1137
1544
  const [command, ...args] = entry;
1138
1545
  const shell = needsShell(command);
1139
1546
  const commandLine = shell ? [command, ...args].map(quoteForShell).join(" ") : command;
1140
- const spawn2 = (progress2) => (0, import_node_child_process3.spawnSync)(commandLine, shell ? [] : args, {
1547
+ const spawnEntry = (progress2) => (0, import_node_child_process3.spawnSync)(commandLine, shell ? [] : args, {
1141
1548
  cwd,
1142
1549
  stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
1143
1550
  shell,
@@ -1146,12 +1553,12 @@ function defaultRunEntry(entry, cwd, env) {
1146
1553
  });
1147
1554
  let result;
1148
1555
  if (process.platform === "win32" && shell) {
1149
- result = spawn2(false);
1556
+ result = spawnEntry(false);
1150
1557
  } else {
1151
1558
  try {
1152
- result = spawn2(true);
1559
+ result = spawnEntry(true);
1153
1560
  } catch {
1154
- result = spawn2(false);
1561
+ result = spawnEntry(false);
1155
1562
  }
1156
1563
  }
1157
1564
  if (result.error) return { ok: false, error: result.error.message };
@@ -1168,73 +1575,68 @@ function faceProduct(config) {
1168
1575
  const known = { mmi: "mmi-hub", jerv: "jerv-hub" };
1169
1576
  return known[config.product] ?? config.product;
1170
1577
  }
1171
- function faceFor(config, options) {
1172
- const tty = options.tty ?? Boolean(process.stdout.isTTY);
1173
- if (!tty) return null;
1578
+ async function installOrUpdate(config, dir, options, print, update) {
1579
+ const product = faceProduct(config);
1580
+ const installer = createInstallerRun({
1581
+ product,
1582
+ gate: config.host,
1583
+ doctor: identityFor(product).doctor,
1584
+ surfaces: [{ id: config.product, kind: "payload" }]
1585
+ }, {
1586
+ operation: update ? "update" : "install",
1587
+ tty: options.tty,
1588
+ animate: !options.print && Boolean(process.stderr.isTTY),
1589
+ ...options.print ? { write: (text) => print(text.replace(/\n$/, "")) } : {}
1590
+ });
1591
+ const fetchImpl = options.fetchImpl ?? fetch;
1592
+ let version = readState(dir)?.version ?? "unknown";
1593
+ installer.start();
1174
1594
  try {
1175
- return createFace({
1176
- product: faceProduct(config),
1177
- color: !process.env.NO_COLOR && process.env.TERM !== "dumb",
1178
- columns: process.stdout.columns
1595
+ let started = Date.now();
1596
+ installer.phase("sign-in", { state: "running" });
1597
+ const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print, installer);
1598
+ installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
1599
+ installer.phase("resolve", { state: "running" });
1600
+ started = Date.now();
1601
+ const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
1602
+ version = manifest.version;
1603
+ installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
1604
+ const current = readState(dir);
1605
+ const unchanged = update && current?.version === version && (0, import_node_fs9.existsSync)(payloadDir(dir));
1606
+ if (!unchanged) {
1607
+ installer.phase("download", { state: "running" });
1608
+ started = Date.now();
1609
+ await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
1610
+ writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1611
+ installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
1612
+ }
1613
+ installer.surface({
1614
+ id: config.product,
1615
+ from: current?.version,
1616
+ to: version,
1617
+ state: unchanged ? "current" : "updated"
1179
1618
  });
1180
- } catch {
1181
- return null;
1182
- }
1183
- }
1184
- function since(start) {
1185
- return (Date.now() - start) / 1e3;
1186
- }
1187
- function outerConsoleOwnsOutcome() {
1188
- return process.env.MM_OUTER_CONSOLE === "1";
1189
- }
1190
- function printReceipt(face, config, print, ready, lines) {
1191
- const name = face ? face.identity.name : config.product;
1192
- const headline = ready ? `${name} is ready.` : `${name} is installed, but not ready yet.`;
1193
- if (ready && outerConsoleOwnsOutcome()) return;
1194
- if (!face) {
1195
- print(headline);
1196
- for (const line of lines) print(line.trim());
1197
- return;
1619
+ return await finishLastMile(config, dir, options, installer, version, unchanged);
1620
+ } catch (error) {
1621
+ const code = error instanceof NeedsLoginError ? 3 : 1;
1622
+ installer.finish({
1623
+ version,
1624
+ total: 1,
1625
+ updated: 0,
1626
+ failed: 1,
1627
+ detail: error.message,
1628
+ retry: `${config.binName} ${update ? "update" : "install"}`
1629
+ });
1630
+ return code;
1631
+ } finally {
1632
+ installer.stop();
1198
1633
  }
1199
- const glyph = ready ? "\u2714" : "\u2716";
1200
- for (const line of face.receipt([`${glyph} ${headline}`, ...lines])) print(line);
1201
- print(face.signOff());
1202
1634
  }
1203
- function printStep(face, print, title, seconds, kind = "ok") {
1204
- print(face ? face.step(title, seconds, kind) : title);
1635
+ function doInstall(config, dir, options, print) {
1636
+ return installOrUpdate(config, dir, options, print, false);
1205
1637
  }
1206
- function printProgress(face, print, progress) {
1207
- for (const message of progress ?? []) {
1208
- printStep(face, print, message.step, message.ms === void 0 ? null : message.ms / 1e3, message.state);
1209
- }
1210
- }
1211
- async function doInstall(config, dir, options, print) {
1212
- const fetchImpl = options.fetchImpl ?? fetch;
1213
- const face = faceFor(config, options);
1214
- const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
1215
- const started = Date.now();
1216
- const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
1217
- await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
1218
- writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1219
- printStep(face, print, `installed ${config.product} ${manifest.version}`, since(started));
1220
- return finishLastMile(config, dir, options, print, face, manifest.version);
1221
- }
1222
- async function doUpdate(config, dir, options, print) {
1223
- const fetchImpl = options.fetchImpl ?? fetch;
1224
- const face = faceFor(config, options);
1225
- if (face && !outerConsoleOwnsOutcome()) for (const line of face.welcome()) print(line);
1226
- const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
1227
- const started = Date.now();
1228
- const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
1229
- const current = readState(dir);
1230
- if (current && current.version === manifest.version && (0, import_node_fs5.existsSync)(payloadDir(dir))) {
1231
- printStep(face, print, `${config.product} is already up to date at ${manifest.version}`, since(started));
1232
- return finishLastMile(config, dir, options, print, face, manifest.version);
1233
- }
1234
- await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
1235
- writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1236
- printStep(face, print, `updated ${config.product} to ${manifest.version}`, since(started));
1237
- return finishLastMile(config, dir, options, print, face, manifest.version);
1638
+ function doUpdate(config, dir, options, print) {
1639
+ return installOrUpdate(config, dir, options, print, true);
1238
1640
  }
1239
1641
  function payloadEnv(dir) {
1240
1642
  const stored = readTokens(dir);
@@ -1247,63 +1649,136 @@ function payloadFileAsCommand(entry, payload) {
1247
1649
  if (!first || first === "$self") return null;
1248
1650
  if (first.includes("/") || first.includes("\\")) return null;
1249
1651
  const candidate = (0, import_node_path4.join)(payload, first);
1250
- if (!(0, import_node_fs5.existsSync)(candidate)) return null;
1652
+ if (!(0, import_node_fs9.existsSync)(candidate)) return null;
1251
1653
  if (NEVER_A_PROGRAM.test(first)) return first;
1252
1654
  if (process.platform === "win32") return null;
1253
1655
  try {
1254
- return ((0, import_node_fs5.statSync)(candidate).mode & 73) === 0 ? first : null;
1656
+ return ((0, import_node_fs9.statSync)(candidate).mode & 73) === 0 ? first : null;
1255
1657
  } catch {
1256
1658
  return null;
1257
1659
  }
1258
1660
  }
1259
- function finishLastMile(config, dir, options, print, face, version) {
1661
+ async function finishLastMile(config, dir, options, installer, version, unchanged) {
1260
1662
  const entry = readPayloadEntry(dir);
1261
1663
  const payload = payloadDir(dir);
1262
1664
  if (!entry) {
1263
- printReceipt(face, config, print, true, [
1264
- `Installed ${version} into ${dir}`,
1265
- `next step: run ${(0, import_node_path4.join)(payload, config.binName)} to start ${config.product}.`
1266
- ]);
1665
+ installer.finish({
1666
+ version,
1667
+ total: 1,
1668
+ updated: unchanged ? 0 : 1,
1669
+ failed: 0,
1670
+ installed: true,
1671
+ detail: `next step: run ${(0, import_node_path4.join)(payload, config.binName)} to start ${config.product}.`
1672
+ });
1267
1673
  return 0;
1268
1674
  }
1269
1675
  const command = resolveEntry(entry);
1270
1676
  const dataFile = payloadFileAsCommand(entry, payload);
1271
1677
  if (dataFile) {
1272
- printStep(face, print, `The payload names ${dataFile} as its command, but that is a file, not a program`, null, "fail");
1273
- printReceipt(face, config, print, false, [
1274
- `Downloaded ${version} into ${dir}`,
1275
- `This payload was built wrong: its entry must be a command, not one of its own files.`,
1276
- `Nothing on this machine can finish it \u2014 report it to the ${config.product} maintainers.`
1277
- ]);
1678
+ installer.finish({
1679
+ version,
1680
+ total: 1,
1681
+ updated: 0,
1682
+ failed: 1,
1683
+ detail: `The payload names ${dataFile} as its command, but that is a file, not a program. This payload was built wrong: its entry must be a command.`
1684
+ });
1278
1685
  return 2;
1279
1686
  }
1687
+ installer.phase("activate", { state: "running" });
1280
1688
  const started = Date.now();
1281
- const result = (options.runEntry ?? defaultRunEntry)(command, payload, payloadEnv(dir));
1282
- printProgress(face, print, result.progress);
1283
- if (result.ok) {
1284
- printStep(face, print, "Armed this machine", since(started));
1285
- printReceipt(face, config, print, true, [
1286
- `Installed ${version} into ${dir}`,
1287
- `Check health any time: ${config.binName} doctor`
1288
- ]);
1289
- return 0;
1689
+ const env = { ...payloadEnv(dir) ?? process.env, MM_OUTER_CONSOLE: "1" };
1690
+ const result = options.runEntry ? options.runEntry(command, payload, env) : await runInstallEntry(command, payload, env, installer, readTokens(dir));
1691
+ for (const progress of result.progress ?? []) installer.milestone(progress);
1692
+ const succeeded = result.ok && (result.code === void 0 || result.code === 0);
1693
+ const outcome = result.outcome ? validateInstallerOutcome(result.outcome) : void 0;
1694
+ installer.phase("activate", { state: succeeded ? "ok" : "fail", seconds: (Date.now() - started) / 1e3 });
1695
+ installer.finish({
1696
+ ...outcome ?? {
1697
+ version,
1698
+ total: 1,
1699
+ updated: succeeded && !unchanged ? 1 : 0,
1700
+ failed: succeeded ? 0 : 1,
1701
+ installed: true
1702
+ },
1703
+ ...!succeeded ? {
1704
+ operationFailed: true,
1705
+ detail: result.code === void 0 ? `Arming this machine could not start: ${result.error ?? command[0]}` : `Arming this machine did not finish (exit code ${result.code})${result.error?.startsWith("installer outcome:") ? `: ${result.error}` : ""}`,
1706
+ retry: `(cd ${payload} && ${command.join(" ")})`
1707
+ } : {}
1708
+ });
1709
+ return succeeded ? outcome?.operationFailed || outcome?.failed ? 1 : 0 : result.code || 1;
1710
+ }
1711
+ async function runInstallEntry(entry, cwd, env, installer, tokens = null) {
1712
+ const [command, ...args] = entry;
1713
+ const shell = needsShell(command);
1714
+ const progress = !(process.platform === "win32" && shell);
1715
+ const outcomeDir = (0, import_node_fs9.mkdtempSync)((0, import_node_path4.join)((0, import_node_os4.tmpdir)(), "mm-installer-outcome-"));
1716
+ const outcomeFile = (0, import_node_path4.join)(outcomeDir, "outcome.json");
1717
+ const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
1718
+ delete childEnv.MM_FACE_TRANSCRIPT;
1719
+ delete childEnv.MM_PROGRESS_FD;
1720
+ delete childEnv.MM_PROGRESS_PROTOCOL;
1721
+ if (progress) Object.assign(childEnv, { MM_PROGRESS_FD: "3", MM_PROGRESS_PROTOCOL: "1" });
1722
+ const secrets = [tokens?.accessToken, tokens?.refreshToken].filter((value) => Boolean(value));
1723
+ const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
1724
+ try {
1725
+ const result = await new Promise((resolve2) => {
1726
+ const child = (0, import_node_child_process3.spawn)(shell ? [command, ...args].map(quoteForShell).join(" ") : command, shell ? [] : args, {
1727
+ cwd,
1728
+ shell,
1729
+ windowsHide: true,
1730
+ env: childEnv,
1731
+ stdio: progress ? ["inherit", "pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
1732
+ });
1733
+ for (const [stream, channel2] of [[child.stdout, "stdout"], [child.stderr, "stderr"]]) {
1734
+ let partial = "";
1735
+ stream?.setEncoding("utf8").on("data", (text) => {
1736
+ const safe = redact(partial + text);
1737
+ let held = 0;
1738
+ for (const secret of secrets) for (let length = 1; length < secret.length; length++) {
1739
+ if (safe.endsWith(secret.slice(0, length))) held = Math.max(held, length);
1740
+ }
1741
+ partial = held ? safe.slice(-held) : "";
1742
+ const visible = held ? safe.slice(0, -held) : safe;
1743
+ if (visible) installer.relay(visible, channel2, false);
1744
+ }).on("end", () => {
1745
+ if (partial) installer.relay("[redacted]", channel2, false);
1746
+ });
1747
+ }
1748
+ let pending = "";
1749
+ const channel = child.stdio[3];
1750
+ if (channel && "setEncoding" in channel) {
1751
+ channel.setEncoding("utf8");
1752
+ channel.on("data", (text) => {
1753
+ pending += text;
1754
+ const end = pending.lastIndexOf("\n");
1755
+ if (end >= 0) {
1756
+ for (const record of parseProgress(pending.slice(0, end))) installer.milestone({ ...record, step: redact(record.step) });
1757
+ pending = pending.slice(end + 1);
1758
+ }
1759
+ if (pending.length > 65536) pending = "";
1760
+ });
1761
+ }
1762
+ child.on("error", (error) => resolve2({ ok: false, error: error.message }));
1763
+ child.on("close", (code) => resolve2({
1764
+ ok: code === 0,
1765
+ ...code !== null ? { code } : {},
1766
+ ...code !== 0 ? { error: `exit code ${code}` } : {}
1767
+ }));
1768
+ });
1769
+ try {
1770
+ const outcome = readInstallerOutcome(outcomeFile);
1771
+ return { ...result, ...outcome ? { outcome } : {} };
1772
+ } catch {
1773
+ return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
1774
+ }
1775
+ } finally {
1776
+ if ((0, import_node_fs9.existsSync)(outcomeFile)) (0, import_node_fs9.unlinkSync)(outcomeFile);
1777
+ (0, import_node_fs9.rmdirSync)(outcomeDir);
1290
1778
  }
1291
- const code = result.code ?? 1;
1292
- printStep(
1293
- face,
1294
- print,
1295
- result.code === void 0 ? `Arming this machine could not start: ${result.error ?? `could not run ${command[0]}`}` : `Arming this machine did not finish (exit code ${code})`,
1296
- null,
1297
- "fail"
1298
- );
1299
- printReceipt(face, config, print, false, [
1300
- `Downloaded ${version} into ${dir}`,
1301
- `Finish it with: (cd ${payload} && ${command.join(" ")})`
1302
- ]);
1303
- return code;
1304
1779
  }
1305
1780
  function doForward(config, dir, options, command, argv, print) {
1306
- if (command && (0, import_node_fs5.existsSync)(command)) {
1781
+ if (command && (0, import_node_fs9.existsSync)(command)) {
1307
1782
  print(`${command} is a file, not a command \u2014 did you mean \`--run ${command}\`?`);
1308
1783
  return 2;
1309
1784
  }
@@ -1373,7 +1848,7 @@ function doDoctor(config, dir, options, print) {
1373
1848
  function doLauncherDoctor(config, dir, options, print) {
1374
1849
  const tokens = readTokens(dir);
1375
1850
  const state = readState(dir);
1376
- const payloadPresent = (0, import_node_fs5.existsSync)(payloadDir(dir));
1851
+ const payloadPresent = (0, import_node_fs9.existsSync)(payloadDir(dir));
1377
1852
  print(`product: ${config.product}`);
1378
1853
  print(`host: ${config.host}`);
1379
1854
  print(`login: ${config.loginKind}`);
@@ -1424,5 +1899,6 @@ if (invokedAsMain) {
1424
1899
  readPayloadVerbs,
1425
1900
  resolveEntry,
1426
1901
  run,
1427
- runFile
1902
+ runFile,
1903
+ runInstallEntry
1428
1904
  });