@mutmutco/installer-launcher 0.1.8 → 0.1.10

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,361 @@ 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"];
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];
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
+ return { ...facts };
360
+ }
361
+ function writeInstallerOutcome(path, value) {
362
+ (0, import_node_fs3.writeFileSync)(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
363
+ }
364
+ function readInstallerOutcome(path) {
365
+ let text;
366
+ try {
367
+ text = (0, import_node_fs3.readFileSync)(path, "utf8");
368
+ } catch (error) {
369
+ if (error.code === "ENOENT") return void 0;
370
+ throw error;
371
+ }
372
+ if (text.length > 16384) throw new Error("installer outcome: child result is too large");
373
+ try {
374
+ return validateInstallerOutcome(JSON.parse(text));
375
+ } catch {
376
+ throw new Error("installer outcome: invalid child result");
377
+ }
378
+ }
379
+
380
+ // ../face/src/run.ts
381
+ function validateInstallerProduct(value) {
382
+ const fail = (field2) => {
383
+ throw new Error(`installer product: invalid ${field2}`);
384
+ };
385
+ if (!value || typeof value !== "object" || Array.isArray(value)) return fail("declaration");
386
+ const input = value;
387
+ const text = (value2, field2) => typeof value2 === "string" && value2.trim() && !/[\r\n\x00-\x1f]/u.test(value2) ? value2 : fail(field2);
388
+ const key = text(input.product, "product");
389
+ const product = { mmi: "mmi-hub", jerv: "jerv-hub" }[key] ?? key;
390
+ const identity = identityFor(product);
391
+ const gate = text(input.gate, "gate");
392
+ let gateUrl;
393
+ try {
394
+ gateUrl = new URL(gate);
395
+ } catch {
396
+ return fail("gate");
397
+ }
398
+ if (!["https:", "http:"].includes(gateUrl.protocol) || gateUrl.username || gateUrl.password) return fail("gate");
399
+ const doctor = text(input.doctor, "doctor");
400
+ if (doctor !== identity.doctor) return fail("doctor");
401
+ if (!Array.isArray(input.surfaces) || input.surfaces.length === 0) return fail("surfaces");
402
+ const ids = /* @__PURE__ */ new Set();
403
+ const surfaces = input.surfaces.map((raw) => {
404
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail("surface");
405
+ const source = raw;
406
+ const id = text(source.id, "surface.id");
407
+ if (ids.has(id)) return fail("duplicate surface.id");
408
+ ids.add(id);
409
+ const surface = { id };
410
+ for (const field2 of ["npm", "bin", "kind", "activation"]) {
411
+ if (source[field2] !== void 0) surface[field2] = text(source[field2], `surface.${field2}`);
412
+ }
413
+ if (Boolean(surface.npm) !== Boolean(surface.bin)) return fail("surface npm/bin pair");
414
+ if (surface.kind !== void 0 && !["agent-home", "payload"].includes(surface.kind)) return fail("surface.kind");
415
+ if (!surface.npm && !surface.kind) return fail("surface implementation");
416
+ return surface;
417
+ });
418
+ return { product, gate, doctor, surfaces };
419
+ }
420
+ var PHASES = {
421
+ preflight: ["Checking prerequisites", "Checked prerequisites"],
422
+ resolve: ["Resolving the release", "Resolved the release"],
423
+ download: ["Downloading the payload", "Downloaded the payload"],
424
+ "sign-in": ["Signing in", "Signed in"],
425
+ check: ["Checking surfaces", "Checked surfaces"],
426
+ arm: ["Scheduling updates", "Armed hourly updates"],
427
+ "verify-release": ["Checking the release version", "Verified the release version"],
428
+ verify: ["Verifying the payload", "Verified the payload"],
429
+ install: ["Installing the product", "Installed the product"],
430
+ activate: ["Activating surfaces", "Activated surfaces"],
431
+ doctor: ["Checking health", "Checked health"]
432
+ };
433
+ function createInstallerRun(value, options = {}) {
434
+ const declaration = validateInstallerProduct(value);
435
+ const env = options.env ?? process.env;
436
+ const tty = options.tty ?? Boolean(process.stdout.isTTY);
437
+ const face = createFace({
438
+ operation: options.operation,
439
+ product: declaration.product,
440
+ columns: options.columns,
441
+ env,
442
+ color: tty && options.color !== false && env.NO_COLOR === void 0
443
+ });
444
+ const write = options.write ?? ((text, channel) => {
445
+ (channel === "stdout" ? process.stdout : process.stderr).write(text);
446
+ });
447
+ const emit = (text, channel = "stdout", recorded = text) => {
448
+ if (!text) return;
449
+ write(text, channel);
450
+ if (env.MM_FACE_TRANSCRIPT) {
451
+ (0, import_node_fs4.appendFileSync)(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
452
+ `, "utf8");
453
+ }
454
+ };
455
+ const lines = (rows, channel = "stdout") => {
456
+ for (const row of rows.flatMap((row2) => row2.split("\n"))) if (row) emit(`${row}
457
+ `, channel);
458
+ };
459
+ const spinner = createSpinner(face, {
460
+ animate: tty && env.TERM !== "dumb" && (options.animate ?? Boolean(process.stderr.isTTY)),
461
+ ...options.write ? { stream: { write: (text) => {
462
+ emit(String(text), "spinner");
463
+ return true;
464
+ } } } : { transcriptPath: env.MM_FACE_TRANSCRIPT }
465
+ });
466
+ let started = false;
467
+ let finished = false;
468
+ const start = () => {
469
+ if (started || finished) return;
470
+ started = true;
471
+ const welcome = face.welcome();
472
+ if (tty) lines(welcome);
473
+ else if (welcome.length) lines([`${face.identity.name} \u2014 Mutatis Mutandis`, options.operation === "install" ? face.identity.installWarm : face.identity.warm]);
474
+ };
475
+ const durable = (title, measure, kind) => {
476
+ spinner.stop();
477
+ const rendered = face.step(title, measure, kind);
478
+ if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
479
+ };
480
+ const run2 = {
481
+ start,
482
+ phase(id, facts = {}) {
483
+ if (finished) throw new Error("installer run already finished");
484
+ if (!Object.hasOwn(PHASES, id)) throw new Error("installer run: unknown phase");
485
+ start();
486
+ const state = facts.state ?? "ok";
487
+ const title = PHASES[id][state === "ok" ? 1 : 0];
488
+ if (state === "running") {
489
+ spinner.start(title, facts.measure ?? null);
490
+ return;
491
+ }
492
+ if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
493
+ if (facts.detail) run2.relay(facts.detail);
494
+ },
495
+ surface(facts) {
496
+ if (finished) throw new Error("installer run already finished");
497
+ const surface = declaration.surfaces.find((surface2) => surface2.id === facts.id);
498
+ if (!surface) throw new Error("installer run: undeclared surface");
499
+ start();
500
+ const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} \u2192 ${facts.to}` : ` ${facts.to}` : "";
501
+ const status = { updated: options.dryRun ? "would update" : "updated", current: "already current", failed: "failed", skipped: "skipped", retry: "retrying", pending: "pending", kept: "kept" }[facts.state];
502
+ if (!status) throw new Error("installer run: unknown surface state");
503
+ const activation = facts.state === "updated" && surface.activation ? ` \xB7 ${surface.activation}` : "";
504
+ const completed = ["updated", "current", "kept", "skipped"].includes(facts.state);
505
+ durable(`${facts.id}${versions} \xB7 ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
506
+ if (facts.detail) run2.relay(facts.detail);
507
+ },
508
+ milestone({ step, state, ms }) {
509
+ start();
510
+ durable(step, ms === void 0 ? null : ms / 1e3, state);
511
+ },
512
+ signIn({ url, code }) {
513
+ start();
514
+ spinner.stop();
515
+ for (const text of [`Open ${url}`, `Enter code: ${code}`]) {
516
+ const rendered = `${tty ? face.relay(text) : text}
517
+ `;
518
+ emit(rendered, "stdout", rendered.replace(code, "[redacted]"));
519
+ }
520
+ },
521
+ // Only pass safe diagnostic text, never authentication output or credentials.
522
+ relay(text, channel = "stdout", record = true) {
523
+ spinner.stop();
524
+ const rendered = tty ? face.relay(text) : stripColor(text);
525
+ for (const row of rendered.split("\n")) if (row) {
526
+ emit(`${row}
527
+ `, channel, record ? `${row}
528
+ ` : `${tty ? face.relay("[external output omitted]") : "[external output omitted]"}
529
+ `);
530
+ }
531
+ },
532
+ finish(facts) {
533
+ if (finished) return;
534
+ validateInstallerOutcome(facts);
535
+ if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(env.MM_INSTALLER_OUTCOME_FILE, facts);
536
+ start();
537
+ spinner.stop();
538
+ finished = true;
539
+ 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}.`;
540
+ const ready = facts.failed === 0 && !facts.dryRun && !facts.deferred && !facts.operationFailed && Boolean(facts.version);
541
+ const body = [
542
+ `${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."}`,
543
+ changed,
544
+ ...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
545
+ ...facts.detail ? [facts.detail] : [],
546
+ facts.retry && facts.failed ? `Retry: ${facts.retry}` : `Check health any time: ${declaration.doctor}`
547
+ ];
548
+ if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE && (facts.failed > 0 || facts.operationFailed)) {
549
+ lines(tty ? face.receipt(body, { ready }) : body.map((row) => row.replace(/^[✔✖●] /u, "")));
550
+ }
551
+ if (tty) lines([face.signOff()]);
552
+ },
553
+ stop() {
554
+ spinner.stop();
555
+ }
556
+ };
557
+ return run2;
558
+ }
559
+
175
560
  // src/autoupdate.ts
176
561
  var import_node_child_process = require("node:child_process");
177
- var import_node_fs = require("node:fs");
562
+ var import_node_fs5 = require("node:fs");
178
563
  var import_node_os = require("node:os");
179
564
  var import_node_path = require("node:path");
180
565
  function schedulePlatform(override) {
@@ -221,9 +606,9 @@ function enableSchedule(config, command, options = {}) {
221
606
  if (platform === "darwin") {
222
607
  const label2 = scheduleLabel(config);
223
608
  const dir2 = (0, import_node_path.join)(home, "Library", "LaunchAgents");
224
- (0, import_node_fs.mkdirSync)(dir2, { recursive: true });
609
+ (0, import_node_fs5.mkdirSync)(dir2, { recursive: true });
225
610
  const plist = (0, import_node_path.join)(dir2, `${label2}.plist`);
226
- (0, import_node_fs.writeFileSync)(plist, darwinPlist(label2, command));
611
+ (0, import_node_fs5.writeFileSync)(plist, darwinPlist(label2, command));
227
612
  exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
228
613
  const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
229
614
  if (result2.code !== 0) {
@@ -233,9 +618,9 @@ function enableSchedule(config, command, options = {}) {
233
618
  }
234
619
  const label = scheduleLabel(config);
235
620
  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));
621
+ (0, import_node_fs5.mkdirSync)(dir, { recursive: true });
622
+ (0, import_node_fs5.writeFileSync)((0, import_node_path.join)(dir, `${label}.service`), linuxService(command));
623
+ (0, import_node_fs5.writeFileSync)((0, import_node_path.join)(dir, `${label}.timer`), linuxTimer(label));
239
624
  const reload = exec("systemctl", ["--user", "daemon-reload"]);
240
625
  if (reload.code !== 0) {
241
626
  throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
@@ -260,14 +645,14 @@ function disableSchedule(config, options = {}) {
260
645
  if (platform === "darwin") {
261
646
  const label2 = scheduleLabel(config);
262
647
  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 });
648
+ (0, import_node_fs5.rmSync)((0, import_node_path.join)(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
264
649
  return;
265
650
  }
266
651
  const label = scheduleLabel(config);
267
652
  const dir = (0, import_node_path.join)(home, ".config", "systemd", "user");
268
653
  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 });
654
+ (0, import_node_fs5.rmSync)((0, import_node_path.join)(dir, `${label}.service`), { force: true });
655
+ (0, import_node_fs5.rmSync)((0, import_node_path.join)(dir, `${label}.timer`), { force: true });
271
656
  }
272
657
  function querySchedule(config, options = {}) {
273
658
  const platform = schedulePlatform(options.platform);
@@ -286,11 +671,11 @@ function querySchedule(config, options = {}) {
286
671
  }
287
672
  if (platform === "darwin") {
288
673
  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 };
674
+ if (!(0, import_node_fs5.existsSync)(plist)) return { supported: true, enabled: false };
290
675
  return { supported: true, enabled: true, cadence: "hourly" };
291
676
  }
292
677
  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 };
678
+ if (!(0, import_node_fs5.existsSync)(timer)) return { supported: true, enabled: false };
294
679
  const state = { supported: true, enabled: true, cadence: "hourly" };
295
680
  const shown = exec("systemctl", ["--user", "show", `${scheduleLabel(config)}.service`, "-p", "ExecMainStartTimestamp", "--value"]);
296
681
  const stamp = (shown.stdout ?? "").trim();
@@ -348,7 +733,7 @@ function firstLine(text) {
348
733
  }
349
734
 
350
735
  // src/config.ts
351
- var import_node_fs2 = require("node:fs");
736
+ var import_node_fs6 = require("node:fs");
352
737
  var import_node_sea = require("node:sea");
353
738
 
354
739
  // src/module-url.ts
@@ -371,10 +756,10 @@ function loadProductConfig(options = {}) {
371
756
  const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
372
757
  const devFallback = new URL("../config/product.template.json", moduleUrl());
373
758
  if (explicit) {
374
- return parseProductConfig((0, import_node_fs2.readFileSync)(explicit, "utf8"));
759
+ return parseProductConfig((0, import_node_fs6.readFileSync)(explicit, "utf8"));
375
760
  }
376
761
  try {
377
- return parseProductConfig((0, import_node_fs2.readFileSync)(devFallback, "utf8"));
762
+ return parseProductConfig((0, import_node_fs6.readFileSync)(devFallback, "utf8"));
378
763
  } catch {
379
764
  }
380
765
  try {
@@ -493,7 +878,8 @@ async function loginGithub(config, options = {}) {
493
878
  const sleep = options.sleep ?? realSleep;
494
879
  const issued = await requestDeviceCode(config, fetchImpl);
495
880
  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}.`);
881
+ if (options.onDeviceCode) options.onDeviceCode({ url: issued.verification_uri, code: issued.user_code });
882
+ else print(`To sign in, open ${issued.verification_uri} and enter the code ${issued.user_code}.`);
497
883
  open(openUrl);
498
884
  const deadline = now() + issued.expires_in * 1e3;
499
885
  let intervalMs = Math.max(1, issued.interval) * 1e3;
@@ -663,7 +1049,7 @@ function page(title, body) {
663
1049
 
664
1050
  // src/payload.ts
665
1051
  var import_node_crypto2 = require("node:crypto");
666
- var import_node_fs3 = require("node:fs");
1052
+ var import_node_fs7 = require("node:fs");
667
1053
  var import_node_os2 = require("node:os");
668
1054
  var import_node_path2 = require("node:path");
669
1055
 
@@ -806,7 +1192,7 @@ async function fetchFileBytes(config, accessToken, path, fetchImpl) {
806
1192
  async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
807
1193
  const fetchImpl = options.fetchImpl ?? fetch;
808
1194
  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 });
1195
+ (0, import_node_fs7.mkdirSync)(staging, { recursive: true });
810
1196
  try {
811
1197
  for (const entry of manifest.files) {
812
1198
  const bytes = await fetchFileBytes(config, accessToken, entry.path, fetchImpl);
@@ -814,22 +1200,22 @@ async function downloadAndUnpack(config, dir, manifest, accessToken, options = {
814
1200
  throw new Error(`file ${entry.path} failed its checksum \u2014 refusing to install anything.`);
815
1201
  }
816
1202
  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);
1203
+ (0, import_node_fs7.mkdirSync)((0, import_node_path2.dirname)(dest), { recursive: true });
1204
+ (0, import_node_fs7.writeFileSync)(dest, bytes);
819
1205
  }
820
1206
  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);
1207
+ (0, import_node_fs7.mkdirSync)(dir, { recursive: true });
1208
+ (0, import_node_fs7.rmSync)(target, { force: true, recursive: true });
1209
+ (0, import_node_fs7.renameSync)(staging, target);
824
1210
  } catch (error) {
825
- (0, import_node_fs3.rmSync)(staging, { force: true, recursive: true });
1211
+ (0, import_node_fs7.rmSync)(staging, { force: true, recursive: true });
826
1212
  throw error;
827
1213
  }
828
1214
  return manifest.version;
829
1215
  }
830
1216
 
831
1217
  // src/store.ts
832
- var import_node_fs4 = require("node:fs");
1218
+ var import_node_fs8 = require("node:fs");
833
1219
  var import_node_os3 = require("node:os");
834
1220
  var import_node_path3 = require("node:path");
835
1221
  function defaultProductDir(product) {
@@ -854,7 +1240,7 @@ function payloadDir(dir) {
854
1240
  }
855
1241
  function readTokens(dir) {
856
1242
  try {
857
- const data = JSON.parse((0, import_node_fs4.readFileSync)(tokensPath(dir), "utf8"));
1243
+ const data = JSON.parse((0, import_node_fs8.readFileSync)(tokensPath(dir), "utf8"));
858
1244
  if (typeof data.accessToken !== "string" || !data.accessToken) return null;
859
1245
  if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
860
1246
  const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
@@ -866,21 +1252,21 @@ function readTokens(dir) {
866
1252
  }
867
1253
  }
868
1254
  function writeTokens(dir, tokens) {
869
- (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
1255
+ (0, import_node_fs8.mkdirSync)(dir, { recursive: true });
870
1256
  try {
871
- (0, import_node_fs4.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
1257
+ (0, import_node_fs8.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
872
1258
  `, { mode: 384 });
873
1259
  } catch {
874
- (0, import_node_fs4.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
1260
+ (0, import_node_fs8.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
875
1261
  `);
876
1262
  }
877
1263
  }
878
1264
  function clearTokens(dir) {
879
- (0, import_node_fs4.rmSync)(tokensPath(dir), { force: true });
1265
+ (0, import_node_fs8.rmSync)(tokensPath(dir), { force: true });
880
1266
  }
881
1267
  function readState(dir) {
882
1268
  try {
883
- const data = JSON.parse((0, import_node_fs4.readFileSync)(statePath(dir), "utf8"));
1269
+ const data = JSON.parse((0, import_node_fs8.readFileSync)(statePath(dir), "utf8"));
884
1270
  if (typeof data.version !== "string" || !data.version) return null;
885
1271
  return { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
886
1272
  } catch {
@@ -888,18 +1274,18 @@ function readState(dir) {
888
1274
  }
889
1275
  }
890
1276
  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)}
1277
+ (0, import_node_fs8.mkdirSync)(dir, { recursive: true });
1278
+ (0, import_node_fs8.writeFileSync)(statePath(dir), `${JSON.stringify(state, null, 2)}
893
1279
  `);
894
1280
  }
895
1281
  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 });
1282
+ (0, import_node_fs8.rmSync)(tokensPath(dir), { force: true });
1283
+ (0, import_node_fs8.rmSync)(statePath(dir), { force: true });
1284
+ (0, import_node_fs8.rmSync)(payloadDir(dir), { force: true, recursive: true });
899
1285
  }
900
1286
 
901
1287
  // src/index.ts
902
- var LAUNCHER_VERSION = true ? "0.1.8" : readVersionFromPackage();
1288
+ var LAUNCHER_VERSION = true ? "0.1.10" : readVersionFromPackage();
903
1289
  function defaultPrint(message) {
904
1290
  process.stdout.write(`${message}
905
1291
  `);
@@ -979,7 +1365,7 @@ async function runFile(file, args, printErr) {
979
1365
  return 1;
980
1366
  }
981
1367
  const abs = (0, import_node_path4.resolve)(process.cwd(), file);
982
- if (!(0, import_node_fs5.existsSync)(abs)) {
1368
+ if (!(0, import_node_fs9.existsSync)(abs)) {
983
1369
  printErr(`cannot run ${file}: no such file.`);
984
1370
  return 1;
985
1371
  }
@@ -1035,11 +1421,16 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
1035
1421
  });
1036
1422
  return refreshed.accessToken;
1037
1423
  }
1038
- async function doLogin(config, dir, options, print) {
1424
+ async function doLogin(config, dir, options, print, installer) {
1039
1425
  const fetchImpl = options.fetchImpl ?? fetch;
1040
1426
  const open = options.open ?? openBrowser;
1041
1427
  if (config.loginKind === "github") {
1042
- const tokens = await loginGithub(config, { fetchImpl, open, print });
1428
+ const tokens = await loginGithub(config, {
1429
+ fetchImpl,
1430
+ open,
1431
+ print,
1432
+ ...installer ? { onDeviceCode: (prompt) => installer.signIn(prompt) } : {}
1433
+ });
1043
1434
  writeTokens(dir, {
1044
1435
  accessToken: tokens.accessToken,
1045
1436
  refreshToken: tokens.refreshToken,
@@ -1054,20 +1445,20 @@ async function doLogin(config, dir, options, print) {
1054
1445
  clientId: tokens.clientId
1055
1446
  });
1056
1447
  }
1057
- print(`signed in to ${config.product}.`);
1448
+ if (!installer) print(`signed in to ${config.product}.`);
1058
1449
  }
1059
- async function fetchAccessTokenOrLogin(config, dir, options, print) {
1450
+ async function fetchAccessTokenOrLogin(config, dir, options, print, installer) {
1060
1451
  const fetchImpl = options.fetchImpl ?? fetch;
1061
1452
  const fresh = await ensureFreshToken(config, dir, fetchImpl);
1062
1453
  if (fresh) return fresh;
1063
- await doLogin(config, dir, options, print);
1454
+ await doLogin(config, dir, options, print, installer);
1064
1455
  const after = readTokens(dir);
1065
1456
  if (!after) throw new Error("sign-in did not produce a token");
1066
1457
  return after.accessToken;
1067
1458
  }
1068
1459
  function readPayloadArgv(dir, key) {
1069
1460
  try {
1070
- const parsed = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(payloadDir(dir), "payload.json"), "utf8"));
1461
+ const parsed = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path4.join)(payloadDir(dir), "payload.json"), "utf8"));
1071
1462
  const entry = parsed[key];
1072
1463
  if (typeof entry === "string") {
1073
1464
  const parts = entry.trim().split(/\s+/).filter(Boolean);
@@ -1089,7 +1480,7 @@ function readPayloadRun(dir) {
1089
1480
  }
1090
1481
  function readPayloadVerbs(dir) {
1091
1482
  try {
1092
- const parsed = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(payloadDir(dir), "payload.json"), "utf8"));
1483
+ const parsed = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path4.join)(payloadDir(dir), "payload.json"), "utf8"));
1093
1484
  const verbs = parsed.verbs;
1094
1485
  if (verbs === "*") return "*";
1095
1486
  if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
@@ -1106,7 +1497,7 @@ function resolveEntry(entry) {
1106
1497
  function needsShell(command) {
1107
1498
  if (process.platform !== "win32") return false;
1108
1499
  if (/\.(cmd|bat)$/i.test(command)) return true;
1109
- return !(0, import_node_fs5.existsSync)(command);
1500
+ return !(0, import_node_fs9.existsSync)(command);
1110
1501
  }
1111
1502
  function quoteForShell(arg) {
1112
1503
  return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
@@ -1137,7 +1528,7 @@ function defaultRunEntry(entry, cwd, env) {
1137
1528
  const [command, ...args] = entry;
1138
1529
  const shell = needsShell(command);
1139
1530
  const commandLine = shell ? [command, ...args].map(quoteForShell).join(" ") : command;
1140
- const spawn2 = (progress2) => (0, import_node_child_process3.spawnSync)(commandLine, shell ? [] : args, {
1531
+ const spawnEntry = (progress2) => (0, import_node_child_process3.spawnSync)(commandLine, shell ? [] : args, {
1141
1532
  cwd,
1142
1533
  stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
1143
1534
  shell,
@@ -1146,12 +1537,12 @@ function defaultRunEntry(entry, cwd, env) {
1146
1537
  });
1147
1538
  let result;
1148
1539
  if (process.platform === "win32" && shell) {
1149
- result = spawn2(false);
1540
+ result = spawnEntry(false);
1150
1541
  } else {
1151
1542
  try {
1152
- result = spawn2(true);
1543
+ result = spawnEntry(true);
1153
1544
  } catch {
1154
- result = spawn2(false);
1545
+ result = spawnEntry(false);
1155
1546
  }
1156
1547
  }
1157
1548
  if (result.error) return { ok: false, error: result.error.message };
@@ -1168,73 +1559,71 @@ function faceProduct(config) {
1168
1559
  const known = { mmi: "mmi-hub", jerv: "jerv-hub" };
1169
1560
  return known[config.product] ?? config.product;
1170
1561
  }
1171
- function faceFor(config, options) {
1172
- const tty = options.tty ?? Boolean(process.stdout.isTTY);
1173
- if (!tty) return null;
1562
+ async function installOrUpdate(config, dir, options, print, update) {
1563
+ const product = faceProduct(config);
1564
+ const installer = createInstallerRun({
1565
+ product,
1566
+ gate: config.host,
1567
+ doctor: identityFor(product).doctor,
1568
+ surfaces: [{ id: config.product, kind: "payload" }]
1569
+ }, {
1570
+ operation: update ? "update" : "install",
1571
+ tty: options.tty,
1572
+ animate: !options.print && Boolean(process.stderr.isTTY),
1573
+ write: (text, channel) => {
1574
+ if (options.print) print(text.replace(/\n$/, ""));
1575
+ else (channel === "stdout" ? process.stdout : process.stderr).write(text);
1576
+ }
1577
+ });
1578
+ const fetchImpl = options.fetchImpl ?? fetch;
1579
+ let version = readState(dir)?.version ?? "unknown";
1580
+ installer.start();
1174
1581
  try {
1175
- return createFace({
1176
- product: faceProduct(config),
1177
- color: !process.env.NO_COLOR && process.env.TERM !== "dumb",
1178
- columns: process.stdout.columns
1582
+ let started = Date.now();
1583
+ installer.phase("sign-in", { state: "running" });
1584
+ const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print, installer);
1585
+ installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
1586
+ installer.phase("resolve", { state: "running" });
1587
+ started = Date.now();
1588
+ const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
1589
+ version = manifest.version;
1590
+ installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
1591
+ const current = readState(dir);
1592
+ const unchanged = update && current?.version === version && (0, import_node_fs9.existsSync)(payloadDir(dir));
1593
+ if (!unchanged) {
1594
+ installer.phase("download", { state: "running" });
1595
+ started = Date.now();
1596
+ await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
1597
+ writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1598
+ installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
1599
+ }
1600
+ installer.surface({
1601
+ id: config.product,
1602
+ from: current?.version,
1603
+ to: version,
1604
+ state: unchanged ? "current" : "updated"
1179
1605
  });
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;
1606
+ return await finishLastMile(config, dir, options, installer, version, unchanged);
1607
+ } catch (error) {
1608
+ const code = error instanceof NeedsLoginError ? 3 : 1;
1609
+ installer.finish({
1610
+ version,
1611
+ total: 1,
1612
+ updated: 0,
1613
+ failed: 1,
1614
+ detail: error.message,
1615
+ retry: `${config.binName} ${update ? "update" : "install"}`
1616
+ });
1617
+ return code;
1618
+ } finally {
1619
+ installer.stop();
1198
1620
  }
1199
- const glyph = ready ? "\u2714" : "\u2716";
1200
- for (const line of face.receipt([`${glyph} ${headline}`, ...lines])) print(line);
1201
- print(face.signOff());
1202
1621
  }
1203
- function printStep(face, print, title, seconds, kind = "ok") {
1204
- print(face ? face.step(title, seconds, kind) : title);
1622
+ function doInstall(config, dir, options, print) {
1623
+ return installOrUpdate(config, dir, options, print, false);
1205
1624
  }
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);
1625
+ function doUpdate(config, dir, options, print) {
1626
+ return installOrUpdate(config, dir, options, print, true);
1238
1627
  }
1239
1628
  function payloadEnv(dir) {
1240
1629
  const stored = readTokens(dir);
@@ -1247,63 +1636,136 @@ function payloadFileAsCommand(entry, payload) {
1247
1636
  if (!first || first === "$self") return null;
1248
1637
  if (first.includes("/") || first.includes("\\")) return null;
1249
1638
  const candidate = (0, import_node_path4.join)(payload, first);
1250
- if (!(0, import_node_fs5.existsSync)(candidate)) return null;
1639
+ if (!(0, import_node_fs9.existsSync)(candidate)) return null;
1251
1640
  if (NEVER_A_PROGRAM.test(first)) return first;
1252
1641
  if (process.platform === "win32") return null;
1253
1642
  try {
1254
- return ((0, import_node_fs5.statSync)(candidate).mode & 73) === 0 ? first : null;
1643
+ return ((0, import_node_fs9.statSync)(candidate).mode & 73) === 0 ? first : null;
1255
1644
  } catch {
1256
1645
  return null;
1257
1646
  }
1258
1647
  }
1259
- function finishLastMile(config, dir, options, print, face, version) {
1648
+ async function finishLastMile(config, dir, options, installer, version, unchanged) {
1260
1649
  const entry = readPayloadEntry(dir);
1261
1650
  const payload = payloadDir(dir);
1262
1651
  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
- ]);
1652
+ installer.finish({
1653
+ version,
1654
+ total: 1,
1655
+ updated: unchanged ? 0 : 1,
1656
+ failed: 0,
1657
+ installed: true,
1658
+ detail: `next step: run ${(0, import_node_path4.join)(payload, config.binName)} to start ${config.product}.`
1659
+ });
1267
1660
  return 0;
1268
1661
  }
1269
1662
  const command = resolveEntry(entry);
1270
1663
  const dataFile = payloadFileAsCommand(entry, payload);
1271
1664
  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
- ]);
1665
+ installer.finish({
1666
+ version,
1667
+ total: 1,
1668
+ updated: 0,
1669
+ failed: 1,
1670
+ 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.`
1671
+ });
1278
1672
  return 2;
1279
1673
  }
1674
+ installer.phase("activate", { state: "running" });
1280
1675
  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;
1676
+ const env = { ...payloadEnv(dir) ?? process.env, MM_OUTER_CONSOLE: "1" };
1677
+ const result = options.runEntry ? options.runEntry(command, payload, env) : await runInstallEntry(command, payload, env, installer, readTokens(dir));
1678
+ for (const progress of result.progress ?? []) installer.milestone(progress);
1679
+ const succeeded = result.ok && (result.code === void 0 || result.code === 0);
1680
+ const outcome = result.outcome ? validateInstallerOutcome(result.outcome) : void 0;
1681
+ installer.phase("activate", { state: succeeded ? "ok" : "fail", seconds: (Date.now() - started) / 1e3 });
1682
+ installer.finish({
1683
+ ...outcome ?? {
1684
+ version,
1685
+ total: 1,
1686
+ updated: succeeded && !unchanged ? 1 : 0,
1687
+ failed: succeeded ? 0 : 1,
1688
+ installed: true
1689
+ },
1690
+ ...!succeeded ? {
1691
+ operationFailed: true,
1692
+ 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}` : ""}`,
1693
+ retry: `(cd ${payload} && ${command.join(" ")})`
1694
+ } : {}
1695
+ });
1696
+ return succeeded ? outcome?.operationFailed || outcome?.failed ? 1 : 0 : result.code || 1;
1697
+ }
1698
+ async function runInstallEntry(entry, cwd, env, installer, tokens = null) {
1699
+ const [command, ...args] = entry;
1700
+ const shell = needsShell(command);
1701
+ const progress = !(process.platform === "win32" && shell);
1702
+ const outcomeDir = (0, import_node_fs9.mkdtempSync)((0, import_node_path4.join)((0, import_node_os4.tmpdir)(), "mm-installer-outcome-"));
1703
+ const outcomeFile = (0, import_node_path4.join)(outcomeDir, "outcome.json");
1704
+ const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
1705
+ delete childEnv.MM_FACE_TRANSCRIPT;
1706
+ delete childEnv.MM_PROGRESS_FD;
1707
+ delete childEnv.MM_PROGRESS_PROTOCOL;
1708
+ if (progress) Object.assign(childEnv, { MM_PROGRESS_FD: "3", MM_PROGRESS_PROTOCOL: "1" });
1709
+ const secrets = [tokens?.accessToken, tokens?.refreshToken].filter((value) => Boolean(value));
1710
+ const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
1711
+ try {
1712
+ const result = await new Promise((resolve2) => {
1713
+ const child = (0, import_node_child_process3.spawn)(shell ? [command, ...args].map(quoteForShell).join(" ") : command, shell ? [] : args, {
1714
+ cwd,
1715
+ shell,
1716
+ windowsHide: true,
1717
+ env: childEnv,
1718
+ stdio: progress ? ["inherit", "pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
1719
+ });
1720
+ for (const [stream, channel2] of [[child.stdout, "stdout"], [child.stderr, "stderr"]]) {
1721
+ let partial = "";
1722
+ stream?.setEncoding("utf8").on("data", (text) => {
1723
+ const safe = redact(partial + text);
1724
+ let held = 0;
1725
+ for (const secret of secrets) for (let length = 1; length < secret.length; length++) {
1726
+ if (safe.endsWith(secret.slice(0, length))) held = Math.max(held, length);
1727
+ }
1728
+ partial = held ? safe.slice(-held) : "";
1729
+ const visible = held ? safe.slice(0, -held) : safe;
1730
+ if (visible) installer.relay(visible, channel2, false);
1731
+ }).on("end", () => {
1732
+ if (partial) installer.relay("[redacted]", channel2, false);
1733
+ });
1734
+ }
1735
+ let pending = "";
1736
+ const channel = child.stdio[3];
1737
+ if (channel && "setEncoding" in channel) {
1738
+ channel.setEncoding("utf8");
1739
+ channel.on("data", (text) => {
1740
+ pending += text;
1741
+ const end = pending.lastIndexOf("\n");
1742
+ if (end >= 0) {
1743
+ for (const record of parseProgress(pending.slice(0, end))) installer.milestone({ ...record, step: redact(record.step) });
1744
+ pending = pending.slice(end + 1);
1745
+ }
1746
+ if (pending.length > 65536) pending = "";
1747
+ });
1748
+ }
1749
+ child.on("error", (error) => resolve2({ ok: false, error: error.message }));
1750
+ child.on("close", (code) => resolve2({
1751
+ ok: code === 0,
1752
+ ...code !== null ? { code } : {},
1753
+ ...code !== 0 ? { error: `exit code ${code}` } : {}
1754
+ }));
1755
+ });
1756
+ try {
1757
+ const outcome = readInstallerOutcome(outcomeFile);
1758
+ return { ...result, ...outcome ? { outcome } : {} };
1759
+ } catch {
1760
+ return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
1761
+ }
1762
+ } finally {
1763
+ if ((0, import_node_fs9.existsSync)(outcomeFile)) (0, import_node_fs9.unlinkSync)(outcomeFile);
1764
+ (0, import_node_fs9.rmdirSync)(outcomeDir);
1290
1765
  }
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
1766
  }
1305
1767
  function doForward(config, dir, options, command, argv, print) {
1306
- if (command && (0, import_node_fs5.existsSync)(command)) {
1768
+ if (command && (0, import_node_fs9.existsSync)(command)) {
1307
1769
  print(`${command} is a file, not a command \u2014 did you mean \`--run ${command}\`?`);
1308
1770
  return 2;
1309
1771
  }
@@ -1373,7 +1835,7 @@ function doDoctor(config, dir, options, print) {
1373
1835
  function doLauncherDoctor(config, dir, options, print) {
1374
1836
  const tokens = readTokens(dir);
1375
1837
  const state = readState(dir);
1376
- const payloadPresent = (0, import_node_fs5.existsSync)(payloadDir(dir));
1838
+ const payloadPresent = (0, import_node_fs9.existsSync)(payloadDir(dir));
1377
1839
  print(`product: ${config.product}`);
1378
1840
  print(`host: ${config.host}`);
1379
1841
  print(`login: ${config.loginKind}`);
@@ -1424,5 +1886,6 @@ if (invokedAsMain) {
1424
1886
  readPayloadVerbs,
1425
1887
  resolveEntry,
1426
1888
  run,
1427
- runFile
1889
+ runFile,
1890
+ runInstallEntry
1428
1891
  });