@mutmutco/installer-launcher 0.1.7 → 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,35 +136,64 @@ 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 }) {
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);
125
150
  const bar = () => paint(PALETTE.muted, GLYPH.bar);
126
151
  const indent = " ".repeat(TITLE_COLUMN - 1);
127
- const welcome = () => [
152
+ const continuedPhases = new Set((env.MM_FACE_CONTINUES ?? "").split(",").map((phase) => phase.trim()).filter(Boolean));
153
+ const continuesFace = continuedPhases.size > 0;
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 ? [] : [
128
169
  `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
129
170
  bar(),
130
- `${bar()} ${identity.warm}`,
171
+ `${bar()} ${operation === "install" ? identity.installWarm : identity.warm}`,
131
172
  bar()
132
173
  ];
133
- const step = (title, seconds = null, kind = "ok") => {
134
- const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.hollow) : paint(PALETTE.green, GLYPH.check);
135
- const time = seconds === null || seconds === void 0 ? "" : paint(PALETTE.muted, `${Math.max(0, Math.round(seconds))}s`);
174
+ const continues = (phase, kind = "ok") => {
175
+ const inherited = continuedPhases.delete(String(phase).trim());
176
+ return kind === "fail" ? false : inherited;
177
+ };
178
+ const step = (title, measure = null, kind = "ok") => {
179
+ if (emitMilestone(title, measure, kind)) return "";
180
+ const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
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;
136
183
  const column = Math.min(44, Math.max(0, width - 8));
137
- const reserved = time ? 6 : 0;
184
+ const reserved = time ? Math.max(6, visibleWidth(time) + 2) : 0;
138
185
  const [first, ...rest] = wrapWords(title, width - TITLE_COLUMN - reserved);
139
186
  const head = `${bar()} ${glyph} ${first}`;
140
187
  const pad = Math.max(1, column - visibleWidth(head));
141
188
  return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
142
189
  };
143
- const relay = (text) => String(text).split("\n").map((line) => line.trim() === "" ? bar() : `${bar()}${indent}${line}`).join("\n");
144
- 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 [];
145
193
  const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
146
194
  const line = String(raw);
147
195
  if (visibleWidth(line) <= width - 6) return [line];
148
- const lead = /^\s*/u.exec(line)?.[0] ?? "";
149
- return wrapWords(line, width - 6 - lead.length).map((part) => `${lead}${part}`);
196
+ return wrapWords(line, width - 6);
150
197
  });
151
198
  const content = Math.min(width - 6, Math.max(...body.map(visibleWidth)));
152
199
  const rule = paint(identity.accent, GLYPH.rule.repeat(content + 4));
@@ -158,18 +205,361 @@ function createFace({ product, color = false, columns }) {
158
205
  frame(GLYPH.boxBottom, GLYPH.boxBottomEnd)
159
206
  ];
160
207
  };
161
- 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`)}`;
162
220
  const refusal = (message) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message}`;
163
- return { identity, width, welcome, step, relay, receipt, signOff, refusal, paint };
221
+ return { identity, width, nested, emitsProgress: progressFd !== null, welcome, continues, step, relay, receipt, outcome, signOff, refusal, paint };
164
222
  }
223
+
224
+ // ../face/src/shell.ts
165
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");
166
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
+ }
167
334
  var SPINNER_FRAMES = Object.freeze([...FRAMES]);
335
+
336
+ // ../face/src/conformance.ts
168
337
  var ALLOWED = new Set(Object.values(GLYPH));
169
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
+
170
560
  // src/autoupdate.ts
171
561
  var import_node_child_process = require("node:child_process");
172
- var import_node_fs = require("node:fs");
562
+ var import_node_fs5 = require("node:fs");
173
563
  var import_node_os = require("node:os");
174
564
  var import_node_path = require("node:path");
175
565
  function schedulePlatform(override) {
@@ -216,9 +606,9 @@ function enableSchedule(config, command, options = {}) {
216
606
  if (platform === "darwin") {
217
607
  const label2 = scheduleLabel(config);
218
608
  const dir2 = (0, import_node_path.join)(home, "Library", "LaunchAgents");
219
- (0, import_node_fs.mkdirSync)(dir2, { recursive: true });
609
+ (0, import_node_fs5.mkdirSync)(dir2, { recursive: true });
220
610
  const plist = (0, import_node_path.join)(dir2, `${label2}.plist`);
221
- (0, import_node_fs.writeFileSync)(plist, darwinPlist(label2, command));
611
+ (0, import_node_fs5.writeFileSync)(plist, darwinPlist(label2, command));
222
612
  exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
223
613
  const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
224
614
  if (result2.code !== 0) {
@@ -228,9 +618,9 @@ function enableSchedule(config, command, options = {}) {
228
618
  }
229
619
  const label = scheduleLabel(config);
230
620
  const dir = (0, import_node_path.join)(home, ".config", "systemd", "user");
231
- (0, import_node_fs.mkdirSync)(dir, { recursive: true });
232
- (0, import_node_fs.writeFileSync)((0, import_node_path.join)(dir, `${label}.service`), linuxService(command));
233
- (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));
234
624
  const reload = exec("systemctl", ["--user", "daemon-reload"]);
235
625
  if (reload.code !== 0) {
236
626
  throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
@@ -255,14 +645,14 @@ function disableSchedule(config, options = {}) {
255
645
  if (platform === "darwin") {
256
646
  const label2 = scheduleLabel(config);
257
647
  exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
258
- (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 });
259
649
  return;
260
650
  }
261
651
  const label = scheduleLabel(config);
262
652
  const dir = (0, import_node_path.join)(home, ".config", "systemd", "user");
263
653
  exec("systemctl", ["--user", "disable", "--now", `${label}.timer`]);
264
- (0, import_node_fs.rmSync)((0, import_node_path.join)(dir, `${label}.service`), { force: true });
265
- (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 });
266
656
  }
267
657
  function querySchedule(config, options = {}) {
268
658
  const platform = schedulePlatform(options.platform);
@@ -281,11 +671,11 @@ function querySchedule(config, options = {}) {
281
671
  }
282
672
  if (platform === "darwin") {
283
673
  const plist = (0, import_node_path.join)(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
284
- 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 };
285
675
  return { supported: true, enabled: true, cadence: "hourly" };
286
676
  }
287
677
  const timer = (0, import_node_path.join)(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
288
- 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 };
289
679
  const state = { supported: true, enabled: true, cadence: "hourly" };
290
680
  const shown = exec("systemctl", ["--user", "show", `${scheduleLabel(config)}.service`, "-p", "ExecMainStartTimestamp", "--value"]);
291
681
  const stamp = (shown.stdout ?? "").trim();
@@ -343,7 +733,7 @@ function firstLine(text) {
343
733
  }
344
734
 
345
735
  // src/config.ts
346
- var import_node_fs2 = require("node:fs");
736
+ var import_node_fs6 = require("node:fs");
347
737
  var import_node_sea = require("node:sea");
348
738
 
349
739
  // src/module-url.ts
@@ -366,10 +756,10 @@ function loadProductConfig(options = {}) {
366
756
  const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
367
757
  const devFallback = new URL("../config/product.template.json", moduleUrl());
368
758
  if (explicit) {
369
- return parseProductConfig((0, import_node_fs2.readFileSync)(explicit, "utf8"));
759
+ return parseProductConfig((0, import_node_fs6.readFileSync)(explicit, "utf8"));
370
760
  }
371
761
  try {
372
- return parseProductConfig((0, import_node_fs2.readFileSync)(devFallback, "utf8"));
762
+ return parseProductConfig((0, import_node_fs6.readFileSync)(devFallback, "utf8"));
373
763
  } catch {
374
764
  }
375
765
  try {
@@ -488,7 +878,8 @@ async function loginGithub(config, options = {}) {
488
878
  const sleep = options.sleep ?? realSleep;
489
879
  const issued = await requestDeviceCode(config, fetchImpl);
490
880
  const openUrl = issued.verification_uri_complete ?? issued.verification_uri;
491
- 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}.`);
492
883
  open(openUrl);
493
884
  const deadline = now() + issued.expires_in * 1e3;
494
885
  let intervalMs = Math.max(1, issued.interval) * 1e3;
@@ -658,7 +1049,7 @@ function page(title, body) {
658
1049
 
659
1050
  // src/payload.ts
660
1051
  var import_node_crypto2 = require("node:crypto");
661
- var import_node_fs3 = require("node:fs");
1052
+ var import_node_fs7 = require("node:fs");
662
1053
  var import_node_os2 = require("node:os");
663
1054
  var import_node_path2 = require("node:path");
664
1055
 
@@ -801,7 +1192,7 @@ async function fetchFileBytes(config, accessToken, path, fetchImpl) {
801
1192
  async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
802
1193
  const fetchImpl = options.fetchImpl ?? fetch;
803
1194
  const staging = (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
804
- (0, import_node_fs3.mkdirSync)(staging, { recursive: true });
1195
+ (0, import_node_fs7.mkdirSync)(staging, { recursive: true });
805
1196
  try {
806
1197
  for (const entry of manifest.files) {
807
1198
  const bytes = await fetchFileBytes(config, accessToken, entry.path, fetchImpl);
@@ -809,22 +1200,22 @@ async function downloadAndUnpack(config, dir, manifest, accessToken, options = {
809
1200
  throw new Error(`file ${entry.path} failed its checksum \u2014 refusing to install anything.`);
810
1201
  }
811
1202
  const dest = (0, import_node_path2.join)(staging, entry.path);
812
- (0, import_node_fs3.mkdirSync)((0, import_node_path2.dirname)(dest), { recursive: true });
813
- (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);
814
1205
  }
815
1206
  const target = (0, import_node_path2.join)(dir, "payload");
816
- (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
817
- (0, import_node_fs3.rmSync)(target, { force: true, recursive: true });
818
- (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);
819
1210
  } catch (error) {
820
- (0, import_node_fs3.rmSync)(staging, { force: true, recursive: true });
1211
+ (0, import_node_fs7.rmSync)(staging, { force: true, recursive: true });
821
1212
  throw error;
822
1213
  }
823
1214
  return manifest.version;
824
1215
  }
825
1216
 
826
1217
  // src/store.ts
827
- var import_node_fs4 = require("node:fs");
1218
+ var import_node_fs8 = require("node:fs");
828
1219
  var import_node_os3 = require("node:os");
829
1220
  var import_node_path3 = require("node:path");
830
1221
  function defaultProductDir(product) {
@@ -849,7 +1240,7 @@ function payloadDir(dir) {
849
1240
  }
850
1241
  function readTokens(dir) {
851
1242
  try {
852
- 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"));
853
1244
  if (typeof data.accessToken !== "string" || !data.accessToken) return null;
854
1245
  if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
855
1246
  const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
@@ -861,21 +1252,21 @@ function readTokens(dir) {
861
1252
  }
862
1253
  }
863
1254
  function writeTokens(dir, tokens) {
864
- (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
1255
+ (0, import_node_fs8.mkdirSync)(dir, { recursive: true });
865
1256
  try {
866
- (0, import_node_fs4.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
1257
+ (0, import_node_fs8.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
867
1258
  `, { mode: 384 });
868
1259
  } catch {
869
- (0, import_node_fs4.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
1260
+ (0, import_node_fs8.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
870
1261
  `);
871
1262
  }
872
1263
  }
873
1264
  function clearTokens(dir) {
874
- (0, import_node_fs4.rmSync)(tokensPath(dir), { force: true });
1265
+ (0, import_node_fs8.rmSync)(tokensPath(dir), { force: true });
875
1266
  }
876
1267
  function readState(dir) {
877
1268
  try {
878
- 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"));
879
1270
  if (typeof data.version !== "string" || !data.version) return null;
880
1271
  return { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
881
1272
  } catch {
@@ -883,18 +1274,18 @@ function readState(dir) {
883
1274
  }
884
1275
  }
885
1276
  function writeState(dir, state) {
886
- (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
887
- (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)}
888
1279
  `);
889
1280
  }
890
1281
  function wipeProductDir(dir) {
891
- (0, import_node_fs4.rmSync)(tokensPath(dir), { force: true });
892
- (0, import_node_fs4.rmSync)(statePath(dir), { force: true });
893
- (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 });
894
1285
  }
895
1286
 
896
1287
  // src/index.ts
897
- var LAUNCHER_VERSION = true ? "0.1.7" : readVersionFromPackage();
1288
+ var LAUNCHER_VERSION = true ? "0.1.10" : readVersionFromPackage();
898
1289
  function defaultPrint(message) {
899
1290
  process.stdout.write(`${message}
900
1291
  `);
@@ -974,7 +1365,7 @@ async function runFile(file, args, printErr) {
974
1365
  return 1;
975
1366
  }
976
1367
  const abs = (0, import_node_path4.resolve)(process.cwd(), file);
977
- if (!(0, import_node_fs5.existsSync)(abs)) {
1368
+ if (!(0, import_node_fs9.existsSync)(abs)) {
978
1369
  printErr(`cannot run ${file}: no such file.`);
979
1370
  return 1;
980
1371
  }
@@ -1030,11 +1421,16 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
1030
1421
  });
1031
1422
  return refreshed.accessToken;
1032
1423
  }
1033
- async function doLogin(config, dir, options, print) {
1424
+ async function doLogin(config, dir, options, print, installer) {
1034
1425
  const fetchImpl = options.fetchImpl ?? fetch;
1035
1426
  const open = options.open ?? openBrowser;
1036
1427
  if (config.loginKind === "github") {
1037
- 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
+ });
1038
1434
  writeTokens(dir, {
1039
1435
  accessToken: tokens.accessToken,
1040
1436
  refreshToken: tokens.refreshToken,
@@ -1049,20 +1445,20 @@ async function doLogin(config, dir, options, print) {
1049
1445
  clientId: tokens.clientId
1050
1446
  });
1051
1447
  }
1052
- print(`signed in to ${config.product}.`);
1448
+ if (!installer) print(`signed in to ${config.product}.`);
1053
1449
  }
1054
- async function fetchAccessTokenOrLogin(config, dir, options, print) {
1450
+ async function fetchAccessTokenOrLogin(config, dir, options, print, installer) {
1055
1451
  const fetchImpl = options.fetchImpl ?? fetch;
1056
1452
  const fresh = await ensureFreshToken(config, dir, fetchImpl);
1057
1453
  if (fresh) return fresh;
1058
- await doLogin(config, dir, options, print);
1454
+ await doLogin(config, dir, options, print, installer);
1059
1455
  const after = readTokens(dir);
1060
1456
  if (!after) throw new Error("sign-in did not produce a token");
1061
1457
  return after.accessToken;
1062
1458
  }
1063
1459
  function readPayloadArgv(dir, key) {
1064
1460
  try {
1065
- 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"));
1066
1462
  const entry = parsed[key];
1067
1463
  if (typeof entry === "string") {
1068
1464
  const parts = entry.trim().split(/\s+/).filter(Boolean);
@@ -1084,7 +1480,7 @@ function readPayloadRun(dir) {
1084
1480
  }
1085
1481
  function readPayloadVerbs(dir) {
1086
1482
  try {
1087
- 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"));
1088
1484
  const verbs = parsed.verbs;
1089
1485
  if (verbs === "*") return "*";
1090
1486
  if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
@@ -1101,7 +1497,7 @@ function resolveEntry(entry) {
1101
1497
  function needsShell(command) {
1102
1498
  if (process.platform !== "win32") return false;
1103
1499
  if (/\.(cmd|bat)$/i.test(command)) return true;
1104
- return !(0, import_node_fs5.existsSync)(command);
1500
+ return !(0, import_node_fs9.existsSync)(command);
1105
1501
  }
1106
1502
  function quoteForShell(arg) {
1107
1503
  return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
@@ -1132,7 +1528,7 @@ function defaultRunEntry(entry, cwd, env) {
1132
1528
  const [command, ...args] = entry;
1133
1529
  const shell = needsShell(command);
1134
1530
  const commandLine = shell ? [command, ...args].map(quoteForShell).join(" ") : command;
1135
- 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, {
1136
1532
  cwd,
1137
1533
  stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
1138
1534
  shell,
@@ -1141,12 +1537,12 @@ function defaultRunEntry(entry, cwd, env) {
1141
1537
  });
1142
1538
  let result;
1143
1539
  if (process.platform === "win32" && shell) {
1144
- result = spawn2(false);
1540
+ result = spawnEntry(false);
1145
1541
  } else {
1146
1542
  try {
1147
- result = spawn2(true);
1543
+ result = spawnEntry(true);
1148
1544
  } catch {
1149
- result = spawn2(false);
1545
+ result = spawnEntry(false);
1150
1546
  }
1151
1547
  }
1152
1548
  if (result.error) return { ok: false, error: result.error.message };
@@ -1163,73 +1559,71 @@ function faceProduct(config) {
1163
1559
  const known = { mmi: "mmi-hub", jerv: "jerv-hub" };
1164
1560
  return known[config.product] ?? config.product;
1165
1561
  }
1166
- function faceFor(config, options) {
1167
- const tty = options.tty ?? Boolean(process.stdout.isTTY);
1168
- 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();
1169
1581
  try {
1170
- return createFace({
1171
- product: faceProduct(config),
1172
- color: !process.env.NO_COLOR && process.env.TERM !== "dumb",
1173
- 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"
1174
1605
  });
1175
- } catch {
1176
- return null;
1177
- }
1178
- }
1179
- function since(start) {
1180
- return (Date.now() - start) / 1e3;
1181
- }
1182
- function outerConsoleOwnsOutcome() {
1183
- return process.env.MM_OUTER_CONSOLE === "1";
1184
- }
1185
- function printReceipt(face, config, print, ready, lines) {
1186
- const name = face ? face.identity.name : config.product;
1187
- const headline = ready ? `${name} is ready.` : `${name} is installed, but not ready yet.`;
1188
- if (ready && outerConsoleOwnsOutcome()) return;
1189
- if (!face) {
1190
- print(headline);
1191
- for (const line of lines) print(line.trim());
1192
- 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();
1193
1620
  }
1194
- const glyph = ready ? "\u2714" : "\u2716";
1195
- for (const line of face.receipt([`${glyph} ${headline}`, ...lines])) print(line);
1196
- print(face.signOff());
1197
1621
  }
1198
- function printStep(face, print, title, seconds, kind = "ok") {
1199
- print(face ? face.step(title, seconds, kind) : title);
1622
+ function doInstall(config, dir, options, print) {
1623
+ return installOrUpdate(config, dir, options, print, false);
1200
1624
  }
1201
- function printProgress(face, print, progress) {
1202
- for (const message of progress ?? []) {
1203
- printStep(face, print, message.step, message.ms === void 0 ? null : message.ms / 1e3, message.state);
1204
- }
1205
- }
1206
- async function doInstall(config, dir, options, print) {
1207
- const fetchImpl = options.fetchImpl ?? fetch;
1208
- const face = faceFor(config, options);
1209
- const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
1210
- const started = Date.now();
1211
- const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
1212
- await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
1213
- writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1214
- printStep(face, print, `installed ${config.product} ${manifest.version}`, since(started));
1215
- return finishLastMile(config, dir, options, print, face, manifest.version);
1216
- }
1217
- async function doUpdate(config, dir, options, print) {
1218
- const fetchImpl = options.fetchImpl ?? fetch;
1219
- const face = faceFor(config, options);
1220
- if (face && !outerConsoleOwnsOutcome()) for (const line of face.welcome()) print(line);
1221
- const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
1222
- const started = Date.now();
1223
- const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
1224
- const current = readState(dir);
1225
- if (current && current.version === manifest.version && (0, import_node_fs5.existsSync)(payloadDir(dir))) {
1226
- printStep(face, print, `${config.product} is already up to date at ${manifest.version}`, since(started));
1227
- return finishLastMile(config, dir, options, print, face, manifest.version);
1228
- }
1229
- await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
1230
- writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1231
- printStep(face, print, `updated ${config.product} to ${manifest.version}`, since(started));
1232
- return finishLastMile(config, dir, options, print, face, manifest.version);
1625
+ function doUpdate(config, dir, options, print) {
1626
+ return installOrUpdate(config, dir, options, print, true);
1233
1627
  }
1234
1628
  function payloadEnv(dir) {
1235
1629
  const stored = readTokens(dir);
@@ -1242,63 +1636,136 @@ function payloadFileAsCommand(entry, payload) {
1242
1636
  if (!first || first === "$self") return null;
1243
1637
  if (first.includes("/") || first.includes("\\")) return null;
1244
1638
  const candidate = (0, import_node_path4.join)(payload, first);
1245
- if (!(0, import_node_fs5.existsSync)(candidate)) return null;
1639
+ if (!(0, import_node_fs9.existsSync)(candidate)) return null;
1246
1640
  if (NEVER_A_PROGRAM.test(first)) return first;
1247
1641
  if (process.platform === "win32") return null;
1248
1642
  try {
1249
- 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;
1250
1644
  } catch {
1251
1645
  return null;
1252
1646
  }
1253
1647
  }
1254
- function finishLastMile(config, dir, options, print, face, version) {
1648
+ async function finishLastMile(config, dir, options, installer, version, unchanged) {
1255
1649
  const entry = readPayloadEntry(dir);
1256
1650
  const payload = payloadDir(dir);
1257
1651
  if (!entry) {
1258
- printReceipt(face, config, print, true, [
1259
- `Installed ${version} into ${dir}`,
1260
- `next step: run ${(0, import_node_path4.join)(payload, config.binName)} to start ${config.product}.`
1261
- ]);
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
+ });
1262
1660
  return 0;
1263
1661
  }
1264
1662
  const command = resolveEntry(entry);
1265
1663
  const dataFile = payloadFileAsCommand(entry, payload);
1266
1664
  if (dataFile) {
1267
- printStep(face, print, `The payload names ${dataFile} as its command, but that is a file, not a program`, null, "fail");
1268
- printReceipt(face, config, print, false, [
1269
- `Downloaded ${version} into ${dir}`,
1270
- `This payload was built wrong: its entry must be a command, not one of its own files.`,
1271
- `Nothing on this machine can finish it \u2014 report it to the ${config.product} maintainers.`
1272
- ]);
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
+ });
1273
1672
  return 2;
1274
1673
  }
1674
+ installer.phase("activate", { state: "running" });
1275
1675
  const started = Date.now();
1276
- const result = (options.runEntry ?? defaultRunEntry)(command, payload, payloadEnv(dir));
1277
- printProgress(face, print, result.progress);
1278
- if (result.ok) {
1279
- printStep(face, print, "Armed this machine", since(started));
1280
- printReceipt(face, config, print, true, [
1281
- `Installed ${version} into ${dir}`,
1282
- `Check health any time: ${config.binName} doctor`
1283
- ]);
1284
- 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);
1285
1765
  }
1286
- const code = result.code ?? 1;
1287
- printStep(
1288
- face,
1289
- print,
1290
- 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})`,
1291
- null,
1292
- "fail"
1293
- );
1294
- printReceipt(face, config, print, false, [
1295
- `Downloaded ${version} into ${dir}`,
1296
- `Finish it with: (cd ${payload} && ${command.join(" ")})`
1297
- ]);
1298
- return code;
1299
1766
  }
1300
1767
  function doForward(config, dir, options, command, argv, print) {
1301
- if (command && (0, import_node_fs5.existsSync)(command)) {
1768
+ if (command && (0, import_node_fs9.existsSync)(command)) {
1302
1769
  print(`${command} is a file, not a command \u2014 did you mean \`--run ${command}\`?`);
1303
1770
  return 2;
1304
1771
  }
@@ -1368,7 +1835,7 @@ function doDoctor(config, dir, options, print) {
1368
1835
  function doLauncherDoctor(config, dir, options, print) {
1369
1836
  const tokens = readTokens(dir);
1370
1837
  const state = readState(dir);
1371
- const payloadPresent = (0, import_node_fs5.existsSync)(payloadDir(dir));
1838
+ const payloadPresent = (0, import_node_fs9.existsSync)(payloadDir(dir));
1372
1839
  print(`product: ${config.product}`);
1373
1840
  print(`host: ${config.host}`);
1374
1841
  print(`login: ${config.loginKind}`);
@@ -1419,5 +1886,6 @@ if (invokedAsMain) {
1419
1886
  readPayloadVerbs,
1420
1887
  resolveEntry,
1421
1888
  run,
1422
- runFile
1889
+ runFile,
1890
+ runInstallEntry
1423
1891
  });