@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.
package/dist/launcher.js CHANGED
@@ -1,32 +1,44 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { spawnSync as spawnSync2 } from "node:child_process";
5
- import { existsSync as existsSync4, readFileSync as readFileSync4, statSync } from "node:fs";
4
+ import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
5
+ import { existsSync as existsSync4, readFileSync as readFileSync5, statSync, mkdtempSync, unlinkSync, rmdirSync } from "node:fs";
6
+ import { tmpdir as tmpdir4 } from "node:os";
6
7
  import { dirname as dirname2, join as join4, resolve } from "node:path";
7
8
  import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
8
9
 
9
- // node_modules/@mutmutco/installer-face/dist/index.js
10
+ // ../face/src/face.ts
11
+ import { writeSync } from "node:fs";
12
+
13
+ // ../face/src/products.ts
10
14
  var PRODUCTS = Object.freeze({
11
15
  "mm-strategy": Object.freeze({
12
16
  name: "MM Strategy",
17
+ installWarm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
13
18
  accent: "38;2;249;115;22",
14
- warm: "Welcome. Let's set up MM Strategy \u2014 about a minute."
19
+ warm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
20
+ doctor: "mm-strategy doctor"
15
21
  }),
16
22
  "mmi-hub": Object.freeze({
17
23
  name: "mmi-hub",
24
+ installWarm: "Welcome. Setting up mmi-hub \u2014 about a minute.",
18
25
  accent: "38;2;125;211;252",
19
- warm: "Welcome back. Checking your surfaces\u2026"
26
+ warm: "Welcome back. Checking your surfaces\u2026",
27
+ doctor: "mmi doctor"
20
28
  }),
21
29
  "jerv-hub": Object.freeze({
22
30
  name: "jerv-hub",
31
+ installWarm: "Welcome. Setting up jerv-hub \u2014 about a minute.",
23
32
  accent: "38;2;248;113;113",
24
- warm: "Welcome back. Checking your surfaces\u2026"
33
+ warm: "Welcome back. Checking your surfaces\u2026",
34
+ doctor: "jerv doctor"
25
35
  }),
26
36
  jervcode: Object.freeze({
27
37
  name: "JervCode",
38
+ installWarm: "Welcome. Setting up JervCode \u2014 about a minute.",
28
39
  accent: "38;2;192;132;252",
29
- warm: "Welcome back. Keeping JervCode current\u2026"
40
+ warm: "Welcome back. Keeping JervCode current\u2026",
41
+ doctor: "jervcode doctor"
30
42
  })
31
43
  });
32
44
  function identityFor(product) {
@@ -36,6 +48,8 @@ function identityFor(product) {
36
48
  }
37
49
  return identity;
38
50
  }
51
+
52
+ // ../face/src/face.ts
39
53
  var GLYPH = Object.freeze({
40
54
  diamond: "\u25C6",
41
55
  hollow: "\u25C7",
@@ -60,6 +74,9 @@ var ANSI = /\u001b\[[0-9;]*m/g;
60
74
  function visibleWidth(text) {
61
75
  return [...String(text).replace(ANSI, "")].length;
62
76
  }
77
+ function stripColor(text) {
78
+ return String(text).replace(ANSI, "");
79
+ }
63
80
  function faceWidth(columns) {
64
81
  const raw = Number(columns);
65
82
  return Math.max(40, Math.min(100, Number.isFinite(raw) && raw > 0 ? raw : 100));
@@ -88,7 +105,14 @@ function wrapWords(text, width) {
88
105
  if (line) lines.push(line);
89
106
  return lines.length ? lines : [""];
90
107
  }
91
- function createFace({ product, color = false, columns, env = process.env }) {
108
+ var PROGRESS_PROTOCOL = 1;
109
+ function readProgressFd(env) {
110
+ const fd = Number.parseInt(String(env.MM_PROGRESS_FD ?? ""), 10);
111
+ if (!Number.isInteger(fd) || fd <= 0) return null;
112
+ const protocol = Number.parseInt(String(env.MM_PROGRESS_PROTOCOL ?? PROGRESS_PROTOCOL), 10);
113
+ return protocol === PROGRESS_PROTOCOL ? fd : null;
114
+ }
115
+ function createFace({ product, color = false, columns, env = process.env, operation }) {
92
116
  const identity = identityFor(product);
93
117
  const width = faceWidth(columns);
94
118
  const paint = (sgr, text) => color ? `\x1B[${sgr}m${text}\x1B[0m` : String(text);
@@ -96,28 +120,45 @@ function createFace({ product, color = false, columns, env = process.env }) {
96
120
  const indent = " ".repeat(TITLE_COLUMN - 1);
97
121
  const continuedPhases = new Set((env.MM_FACE_CONTINUES ?? "").split(",").map((phase) => phase.trim()).filter(Boolean));
98
122
  const continuesFace = continuedPhases.size > 0;
99
- const welcome = () => continuesFace ? [] : [
123
+ const nested = env.MM_OUTER_CONSOLE === "1";
124
+ const progressFd = readProgressFd(env);
125
+ const emitMilestone = (title, measure, kind) => {
126
+ if (progressFd === null) return false;
127
+ const record = { v: PROGRESS_PROTOCOL, step: stripColor(title), state: kind };
128
+ if (typeof measure === "number") record.ms = Math.max(0, Math.round(measure * 1e3));
129
+ try {
130
+ writeSync(progressFd, `${JSON.stringify(record)}
131
+ `);
132
+ return true;
133
+ } catch {
134
+ return false;
135
+ }
136
+ };
137
+ const welcome = () => continuesFace || nested ? [] : [
100
138
  `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
101
139
  bar(),
102
- `${bar()} ${identity.warm}`,
140
+ `${bar()} ${operation === "install" ? identity.installWarm : identity.warm}`,
103
141
  bar()
104
142
  ];
105
143
  const continues = (phase, kind = "ok") => {
106
144
  const inherited = continuedPhases.delete(String(phase).trim());
107
145
  return kind === "fail" ? false : inherited;
108
146
  };
109
- const step = (title, seconds = null, kind = "ok") => {
147
+ const step = (title, measure = null, kind = "ok") => {
148
+ if (emitMilestone(title, measure, kind)) return "";
110
149
  const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
111
- const time = seconds === null || seconds === void 0 ? "" : paint(PALETTE.muted, `${Math.max(0, Math.round(seconds))}s`);
150
+ const measured = measure === null || measure === void 0 ? "" : paint(PALETTE.muted, typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : String(measure).trim());
151
+ const time = measured;
112
152
  const column = Math.min(44, Math.max(0, width - 8));
113
- const reserved = time ? 6 : 0;
153
+ const reserved = time ? Math.max(6, visibleWidth(time) + 2) : 0;
114
154
  const [first, ...rest] = wrapWords(title, width - TITLE_COLUMN - reserved);
115
155
  const head = `${bar()} ${glyph} ${first}`;
116
156
  const pad = Math.max(1, column - visibleWidth(head));
117
157
  return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
118
158
  };
119
- const relay = (text) => String(text).split("\n").map((line) => line.trim() === "" ? bar() : `${bar()}${indent}${line}`).join("\n");
120
- const receipt = (lines) => {
159
+ 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");
160
+ const receipt = (lines, { ready = true } = {}) => {
161
+ if (ready && nested) return [];
121
162
  const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
122
163
  const line = String(raw);
123
164
  if (visibleWidth(line) <= width - 6) return [line];
@@ -133,18 +174,361 @@ function createFace({ product, color = false, columns, env = process.env }) {
133
174
  frame(GLYPH.boxBottom, GLYPH.boxBottomEnd)
134
175
  ];
135
176
  };
136
- const signOff = () => `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
177
+ const outcome = (changed, options = {}) => {
178
+ const fact = String(changed ?? "").trim();
179
+ if (!fact) {
180
+ 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})`);
181
+ }
182
+ return receipt([
183
+ `${GLYPH.check} ${identity.name} is ready.`,
184
+ fact,
185
+ `Check health any time: ${identity.doctor}`
186
+ ], options);
187
+ };
188
+ const signOff = () => nested ? "" : `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
137
189
  const refusal = (message) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message}`;
138
- return { identity, width, welcome, continues, step, relay, receipt, signOff, refusal, paint };
190
+ return { identity, width, nested, emitsProgress: progressFd !== null, welcome, continues, step, relay, receipt, outcome, signOff, refusal, paint };
139
191
  }
192
+
193
+ // ../face/src/shell.ts
140
194
  var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
195
+
196
+ // ../face/src/spinner.ts
197
+ import { appendFileSync, writeSync as writeSync2 } from "node:fs";
198
+ import { Worker } from "node:worker_threads";
141
199
  var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
200
+ var WORKER_SOURCE = `
201
+ const { parentPort, workerData } = require('node:worker_threads');
202
+ const { writeSync, appendFileSync } = require('node:fs');
203
+ const control = new Int32Array(workerData.control);
204
+ let frames = workerData.frames, frame = workerData.frame;
205
+ function draw() {
206
+ if (Atomics.compareExchange(control, 1, 0, 1) !== 0) return;
207
+ try {
208
+ if (Atomics.load(control, 0) || Atomics.load(control, 2) || !frames.length) return;
209
+ const text = frames[frame++ % frames.length];
210
+ writeSync(2, text);
211
+ if (workerData.transcriptPath) appendFileSync(workerData.transcriptPath, JSON.stringify({ channel: 'spinner', text }) + '\\n');
212
+ } finally {
213
+ Atomics.store(control, 1, 0);
214
+ Atomics.notify(control, 1);
215
+ }
216
+ }
217
+ parentPort.on('message', (next) => {
218
+ if (Atomics.load(control, 2)) return;
219
+ frames = next.frames;
220
+ frame = next.frame;
221
+ Atomics.store(control, 0, 0);
222
+ });
223
+ setInterval(draw, workerData.intervalMs);
224
+ `;
225
+ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath }) {
226
+ animate = animate && !face.nested && !face.emitsProgress;
227
+ let timer = null;
228
+ let worker = null;
229
+ let control = null;
230
+ let frames = [];
231
+ let frame = 0;
232
+ const write = (text) => {
233
+ if (stream) stream.write(text);
234
+ else {
235
+ writeSync2(2, text);
236
+ if (transcriptPath) appendFileSync(transcriptPath, `${JSON.stringify({ channel: "spinner", text })}
237
+ `);
238
+ }
239
+ };
240
+ const render = (text, measure) => {
241
+ const line = face.step(text, measure, "note").split("\n")[0];
242
+ frames = line ? FRAMES.map((glyph) => `\r\x1B[2K${line.replace(GLYPH.dot, glyph)}`) : [];
243
+ };
244
+ const draw = () => {
245
+ if (animate && frames.length) write(frames[frame++ % frames.length]);
246
+ };
247
+ const pause = () => {
248
+ if (!control) return;
249
+ Atomics.store(control, 0, 1);
250
+ while (Atomics.load(control, 1)) Atomics.wait(control, 1, 1);
251
+ };
252
+ const halt = () => {
253
+ if (timer) clearInterval(timer);
254
+ timer = null;
255
+ if (control) Atomics.store(control, 2, 1);
256
+ pause();
257
+ if (worker) void worker.terminate();
258
+ worker = null;
259
+ control = null;
260
+ };
261
+ return {
262
+ start(text, measure = null) {
263
+ if (!animate) return;
264
+ halt();
265
+ render(text, measure);
266
+ frame = 0;
267
+ draw();
268
+ if (!frames.length) return;
269
+ if (stream) timer = setInterval(draw, intervalMs).unref();
270
+ else {
271
+ control = new Int32Array(new SharedArrayBuffer(12));
272
+ try {
273
+ worker = new Worker(WORKER_SOURCE, { eval: true, workerData: {
274
+ control: control.buffer,
275
+ frames,
276
+ frame,
277
+ intervalMs,
278
+ transcriptPath
279
+ } });
280
+ const active = worker;
281
+ worker.on("error", () => {
282
+ if (worker === active) halt();
283
+ });
284
+ worker.unref();
285
+ } catch {
286
+ halt();
287
+ }
288
+ }
289
+ },
290
+ say(text, measure = null) {
291
+ if (!animate) return;
292
+ pause();
293
+ render(text, measure);
294
+ draw();
295
+ worker?.postMessage({ frames, frame });
296
+ },
297
+ stop() {
298
+ halt();
299
+ if (animate) write("\r\x1B[2K");
300
+ }
301
+ };
302
+ }
142
303
  var SPINNER_FRAMES = Object.freeze([...FRAMES]);
304
+
305
+ // ../face/src/conformance.ts
143
306
  var ALLOWED = new Set(Object.values(GLYPH));
144
307
 
308
+ // ../face/src/run.ts
309
+ import { appendFileSync as appendFileSync2 } from "node:fs";
310
+
311
+ // ../face/src/outcome.ts
312
+ import { readFileSync, writeFileSync } from "node:fs";
313
+ var counts = ["total", "updated", "failed"];
314
+ var strings = ["version", "retry", "detail"];
315
+ var flags = ["dryRun", "installed", "deferred", "operationFailed"];
316
+ function validateInstallerOutcome(value) {
317
+ const invalid = () => {
318
+ throw new Error("installer outcome: invalid child result");
319
+ };
320
+ if (!value || typeof value !== "object" || Array.isArray(value)) return invalid();
321
+ const facts = value;
322
+ const allowed = [...counts, ...strings, ...flags];
323
+ if (Object.keys(facts).some((key) => !allowed.includes(key))) return invalid();
324
+ for (const key of counts) if (!Number.isSafeInteger(facts[key]) || facts[key] < 0) return invalid();
325
+ if (facts.updated + facts.failed > facts.total) return invalid();
326
+ for (const key of strings) if (facts[key] !== void 0 && (typeof facts[key] !== "string" || facts[key].length > 4096)) return invalid();
327
+ for (const key of flags) if (facts[key] !== void 0 && typeof facts[key] !== "boolean") return invalid();
328
+ return { ...facts };
329
+ }
330
+ function writeInstallerOutcome(path, value) {
331
+ writeFileSync(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
332
+ }
333
+ function readInstallerOutcome(path) {
334
+ let text;
335
+ try {
336
+ text = readFileSync(path, "utf8");
337
+ } catch (error) {
338
+ if (error.code === "ENOENT") return void 0;
339
+ throw error;
340
+ }
341
+ if (text.length > 16384) throw new Error("installer outcome: child result is too large");
342
+ try {
343
+ return validateInstallerOutcome(JSON.parse(text));
344
+ } catch {
345
+ throw new Error("installer outcome: invalid child result");
346
+ }
347
+ }
348
+
349
+ // ../face/src/run.ts
350
+ function validateInstallerProduct(value) {
351
+ const fail = (field2) => {
352
+ throw new Error(`installer product: invalid ${field2}`);
353
+ };
354
+ if (!value || typeof value !== "object" || Array.isArray(value)) return fail("declaration");
355
+ const input = value;
356
+ const text = (value2, field2) => typeof value2 === "string" && value2.trim() && !/[\r\n\x00-\x1f]/u.test(value2) ? value2 : fail(field2);
357
+ const key = text(input.product, "product");
358
+ const product = { mmi: "mmi-hub", jerv: "jerv-hub" }[key] ?? key;
359
+ const identity = identityFor(product);
360
+ const gate = text(input.gate, "gate");
361
+ let gateUrl;
362
+ try {
363
+ gateUrl = new URL(gate);
364
+ } catch {
365
+ return fail("gate");
366
+ }
367
+ if (!["https:", "http:"].includes(gateUrl.protocol) || gateUrl.username || gateUrl.password) return fail("gate");
368
+ const doctor = text(input.doctor, "doctor");
369
+ if (doctor !== identity.doctor) return fail("doctor");
370
+ if (!Array.isArray(input.surfaces) || input.surfaces.length === 0) return fail("surfaces");
371
+ const ids = /* @__PURE__ */ new Set();
372
+ const surfaces = input.surfaces.map((raw) => {
373
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail("surface");
374
+ const source = raw;
375
+ const id = text(source.id, "surface.id");
376
+ if (ids.has(id)) return fail("duplicate surface.id");
377
+ ids.add(id);
378
+ const surface = { id };
379
+ for (const field2 of ["npm", "bin", "kind", "activation"]) {
380
+ if (source[field2] !== void 0) surface[field2] = text(source[field2], `surface.${field2}`);
381
+ }
382
+ if (Boolean(surface.npm) !== Boolean(surface.bin)) return fail("surface npm/bin pair");
383
+ if (surface.kind !== void 0 && !["agent-home", "payload"].includes(surface.kind)) return fail("surface.kind");
384
+ if (!surface.npm && !surface.kind) return fail("surface implementation");
385
+ return surface;
386
+ });
387
+ return { product, gate, doctor, surfaces };
388
+ }
389
+ var PHASES = {
390
+ preflight: ["Checking prerequisites", "Checked prerequisites"],
391
+ resolve: ["Resolving the release", "Resolved the release"],
392
+ download: ["Downloading the payload", "Downloaded the payload"],
393
+ "sign-in": ["Signing in", "Signed in"],
394
+ check: ["Checking surfaces", "Checked surfaces"],
395
+ arm: ["Scheduling updates", "Armed hourly updates"],
396
+ "verify-release": ["Checking the release version", "Verified the release version"],
397
+ verify: ["Verifying the payload", "Verified the payload"],
398
+ install: ["Installing the product", "Installed the product"],
399
+ activate: ["Activating surfaces", "Activated surfaces"],
400
+ doctor: ["Checking health", "Checked health"]
401
+ };
402
+ function createInstallerRun(value, options = {}) {
403
+ const declaration = validateInstallerProduct(value);
404
+ const env = options.env ?? process.env;
405
+ const tty = options.tty ?? Boolean(process.stdout.isTTY);
406
+ const face = createFace({
407
+ operation: options.operation,
408
+ product: declaration.product,
409
+ columns: options.columns,
410
+ env,
411
+ color: tty && options.color !== false && env.NO_COLOR === void 0
412
+ });
413
+ const write = options.write ?? ((text, channel) => {
414
+ (channel === "stdout" ? process.stdout : process.stderr).write(text);
415
+ });
416
+ const emit = (text, channel = "stdout", recorded = text) => {
417
+ if (!text) return;
418
+ write(text, channel);
419
+ if (env.MM_FACE_TRANSCRIPT) {
420
+ appendFileSync2(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
421
+ `, "utf8");
422
+ }
423
+ };
424
+ const lines = (rows, channel = "stdout") => {
425
+ for (const row of rows.flatMap((row2) => row2.split("\n"))) if (row) emit(`${row}
426
+ `, channel);
427
+ };
428
+ const spinner = createSpinner(face, {
429
+ animate: tty && env.TERM !== "dumb" && (options.animate ?? Boolean(process.stderr.isTTY)),
430
+ ...options.write ? { stream: { write: (text) => {
431
+ emit(String(text), "spinner");
432
+ return true;
433
+ } } } : { transcriptPath: env.MM_FACE_TRANSCRIPT }
434
+ });
435
+ let started = false;
436
+ let finished = false;
437
+ const start = () => {
438
+ if (started || finished) return;
439
+ started = true;
440
+ const welcome = face.welcome();
441
+ if (tty) lines(welcome);
442
+ else if (welcome.length) lines([`${face.identity.name} \u2014 Mutatis Mutandis`, options.operation === "install" ? face.identity.installWarm : face.identity.warm]);
443
+ };
444
+ const durable = (title, measure, kind) => {
445
+ spinner.stop();
446
+ const rendered = face.step(title, measure, kind);
447
+ if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
448
+ };
449
+ const run2 = {
450
+ start,
451
+ phase(id, facts = {}) {
452
+ if (finished) throw new Error("installer run already finished");
453
+ if (!Object.hasOwn(PHASES, id)) throw new Error("installer run: unknown phase");
454
+ start();
455
+ const state = facts.state ?? "ok";
456
+ const title = PHASES[id][state === "ok" ? 1 : 0];
457
+ if (state === "running") {
458
+ spinner.start(title, facts.measure ?? null);
459
+ return;
460
+ }
461
+ if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
462
+ if (facts.detail) run2.relay(facts.detail);
463
+ },
464
+ surface(facts) {
465
+ if (finished) throw new Error("installer run already finished");
466
+ const surface = declaration.surfaces.find((surface2) => surface2.id === facts.id);
467
+ if (!surface) throw new Error("installer run: undeclared surface");
468
+ start();
469
+ const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} \u2192 ${facts.to}` : ` ${facts.to}` : "";
470
+ const status = { updated: options.dryRun ? "would update" : "updated", current: "already current", failed: "failed", skipped: "skipped", retry: "retrying", pending: "pending", kept: "kept" }[facts.state];
471
+ if (!status) throw new Error("installer run: unknown surface state");
472
+ const activation = facts.state === "updated" && surface.activation ? ` \xB7 ${surface.activation}` : "";
473
+ const completed = ["updated", "current", "kept", "skipped"].includes(facts.state);
474
+ durable(`${facts.id}${versions} \xB7 ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
475
+ if (facts.detail) run2.relay(facts.detail);
476
+ },
477
+ milestone({ step, state, ms }) {
478
+ start();
479
+ durable(step, ms === void 0 ? null : ms / 1e3, state);
480
+ },
481
+ signIn({ url, code }) {
482
+ start();
483
+ spinner.stop();
484
+ for (const text of [`Open ${url}`, `Enter code: ${code}`]) {
485
+ const rendered = `${tty ? face.relay(text) : text}
486
+ `;
487
+ emit(rendered, "stdout", rendered.replace(code, "[redacted]"));
488
+ }
489
+ },
490
+ // Only pass safe diagnostic text, never authentication output or credentials.
491
+ relay(text, channel = "stdout", record = true) {
492
+ spinner.stop();
493
+ const rendered = tty ? face.relay(text) : stripColor(text);
494
+ for (const row of rendered.split("\n")) if (row) {
495
+ emit(`${row}
496
+ `, channel, record ? `${row}
497
+ ` : `${tty ? face.relay("[external output omitted]") : "[external output omitted]"}
498
+ `);
499
+ }
500
+ },
501
+ finish(facts) {
502
+ if (finished) return;
503
+ validateInstallerOutcome(facts);
504
+ if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(env.MM_INSTALLER_OUTCOME_FILE, facts);
505
+ start();
506
+ spinner.stop();
507
+ finished = true;
508
+ 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}.`;
509
+ const ready = facts.failed === 0 && !facts.dryRun && !facts.deferred && !facts.operationFailed && Boolean(facts.version);
510
+ const body = [
511
+ `${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."}`,
512
+ changed,
513
+ ...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
514
+ ...facts.detail ? [facts.detail] : [],
515
+ facts.retry && facts.failed ? `Retry: ${facts.retry}` : `Check health any time: ${declaration.doctor}`
516
+ ];
517
+ if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE && (facts.failed > 0 || facts.operationFailed)) {
518
+ lines(tty ? face.receipt(body, { ready }) : body.map((row) => row.replace(/^[✔✖●] /u, "")));
519
+ }
520
+ if (tty) lines([face.signOff()]);
521
+ },
522
+ stop() {
523
+ spinner.stop();
524
+ }
525
+ };
526
+ return run2;
527
+ }
528
+
145
529
  // src/autoupdate.ts
146
530
  import { spawnSync } from "node:child_process";
147
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
531
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
148
532
  import { tmpdir } from "node:os";
149
533
  import { join } from "node:path";
150
534
  function schedulePlatform(override) {
@@ -193,7 +577,7 @@ function enableSchedule(config, command, options = {}) {
193
577
  const dir2 = join(home, "Library", "LaunchAgents");
194
578
  mkdirSync(dir2, { recursive: true });
195
579
  const plist = join(dir2, `${label2}.plist`);
196
- writeFileSync(plist, darwinPlist(label2, command));
580
+ writeFileSync2(plist, darwinPlist(label2, command));
197
581
  exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
198
582
  const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
199
583
  if (result2.code !== 0) {
@@ -204,8 +588,8 @@ function enableSchedule(config, command, options = {}) {
204
588
  const label = scheduleLabel(config);
205
589
  const dir = join(home, ".config", "systemd", "user");
206
590
  mkdirSync(dir, { recursive: true });
207
- writeFileSync(join(dir, `${label}.service`), linuxService(command));
208
- writeFileSync(join(dir, `${label}.timer`), linuxTimer(label));
591
+ writeFileSync2(join(dir, `${label}.service`), linuxService(command));
592
+ writeFileSync2(join(dir, `${label}.timer`), linuxTimer(label));
209
593
  const reload = exec("systemctl", ["--user", "daemon-reload"]);
210
594
  if (reload.code !== 0) {
211
595
  throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
@@ -318,7 +702,7 @@ function firstLine(text) {
318
702
  }
319
703
 
320
704
  // src/config.ts
321
- import { readFileSync as readFileSync2 } from "node:fs";
705
+ import { readFileSync as readFileSync3 } from "node:fs";
322
706
  import { getAsset } from "node:sea";
323
707
 
324
708
  // src/module-url.ts
@@ -340,10 +724,10 @@ function loadProductConfig(options = {}) {
340
724
  const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
341
725
  const devFallback = new URL("../config/product.template.json", moduleUrl());
342
726
  if (explicit) {
343
- return parseProductConfig(readFileSync2(explicit, "utf8"));
727
+ return parseProductConfig(readFileSync3(explicit, "utf8"));
344
728
  }
345
729
  try {
346
- return parseProductConfig(readFileSync2(devFallback, "utf8"));
730
+ return parseProductConfig(readFileSync3(devFallback, "utf8"));
347
731
  } catch {
348
732
  }
349
733
  try {
@@ -462,7 +846,8 @@ async function loginGithub(config, options = {}) {
462
846
  const sleep = options.sleep ?? realSleep;
463
847
  const issued = await requestDeviceCode(config, fetchImpl);
464
848
  const openUrl = issued.verification_uri_complete ?? issued.verification_uri;
465
- print(`To sign in, open ${issued.verification_uri} and enter the code ${issued.user_code}.`);
849
+ if (options.onDeviceCode) options.onDeviceCode({ url: issued.verification_uri, code: issued.user_code });
850
+ else print(`To sign in, open ${issued.verification_uri} and enter the code ${issued.user_code}.`);
466
851
  open(openUrl);
467
852
  const deadline = now() + issued.expires_in * 1e3;
468
853
  let intervalMs = Math.max(1, issued.interval) * 1e3;
@@ -632,7 +1017,7 @@ function page(title, body) {
632
1017
 
633
1018
  // src/payload.ts
634
1019
  import { createHash as createHash2, createPublicKey, verify } from "node:crypto";
635
- import { mkdirSync as mkdirSync2, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
1020
+ import { mkdirSync as mkdirSync2, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
636
1021
  import { tmpdir as tmpdir2 } from "node:os";
637
1022
  import { dirname, join as join2 } from "node:path";
638
1023
 
@@ -784,7 +1169,7 @@ async function downloadAndUnpack(config, dir, manifest, accessToken, options = {
784
1169
  }
785
1170
  const dest = join2(staging, entry.path);
786
1171
  mkdirSync2(dirname(dest), { recursive: true });
787
- writeFileSync2(dest, bytes);
1172
+ writeFileSync3(dest, bytes);
788
1173
  }
789
1174
  const target = join2(dir, "payload");
790
1175
  mkdirSync2(dir, { recursive: true });
@@ -798,7 +1183,7 @@ async function downloadAndUnpack(config, dir, manifest, accessToken, options = {
798
1183
  }
799
1184
 
800
1185
  // src/store.ts
801
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
1186
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
802
1187
  import { tmpdir as tmpdir3 } from "node:os";
803
1188
  import { join as join3 } from "node:path";
804
1189
  function defaultProductDir(product) {
@@ -823,7 +1208,7 @@ function payloadDir(dir) {
823
1208
  }
824
1209
  function readTokens(dir) {
825
1210
  try {
826
- const data = JSON.parse(readFileSync3(tokensPath(dir), "utf8"));
1211
+ const data = JSON.parse(readFileSync4(tokensPath(dir), "utf8"));
827
1212
  if (typeof data.accessToken !== "string" || !data.accessToken) return null;
828
1213
  if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
829
1214
  const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
@@ -837,10 +1222,10 @@ function readTokens(dir) {
837
1222
  function writeTokens(dir, tokens) {
838
1223
  mkdirSync3(dir, { recursive: true });
839
1224
  try {
840
- writeFileSync3(tokensPath(dir), `${JSON.stringify(tokens)}
1225
+ writeFileSync4(tokensPath(dir), `${JSON.stringify(tokens)}
841
1226
  `, { mode: 384 });
842
1227
  } catch {
843
- writeFileSync3(tokensPath(dir), `${JSON.stringify(tokens)}
1228
+ writeFileSync4(tokensPath(dir), `${JSON.stringify(tokens)}
844
1229
  `);
845
1230
  }
846
1231
  }
@@ -849,7 +1234,7 @@ function clearTokens(dir) {
849
1234
  }
850
1235
  function readState(dir) {
851
1236
  try {
852
- const data = JSON.parse(readFileSync3(statePath(dir), "utf8"));
1237
+ const data = JSON.parse(readFileSync4(statePath(dir), "utf8"));
853
1238
  if (typeof data.version !== "string" || !data.version) return null;
854
1239
  return { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
855
1240
  } catch {
@@ -858,7 +1243,7 @@ function readState(dir) {
858
1243
  }
859
1244
  function writeState(dir, state) {
860
1245
  mkdirSync3(dir, { recursive: true });
861
- writeFileSync3(statePath(dir), `${JSON.stringify(state, null, 2)}
1246
+ writeFileSync4(statePath(dir), `${JSON.stringify(state, null, 2)}
862
1247
  `);
863
1248
  }
864
1249
  function wipeProductDir(dir) {
@@ -868,7 +1253,7 @@ function wipeProductDir(dir) {
868
1253
  }
869
1254
 
870
1255
  // src/index.ts
871
- var LAUNCHER_VERSION = true ? "0.1.8" : readVersionFromPackage();
1256
+ var LAUNCHER_VERSION = true ? "0.1.10" : readVersionFromPackage();
872
1257
  function defaultPrint(message) {
873
1258
  process.stdout.write(`${message}
874
1259
  `);
@@ -1004,11 +1389,16 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
1004
1389
  });
1005
1390
  return refreshed.accessToken;
1006
1391
  }
1007
- async function doLogin(config, dir, options, print) {
1392
+ async function doLogin(config, dir, options, print, installer) {
1008
1393
  const fetchImpl = options.fetchImpl ?? fetch;
1009
1394
  const open = options.open ?? openBrowser;
1010
1395
  if (config.loginKind === "github") {
1011
- const tokens = await loginGithub(config, { fetchImpl, open, print });
1396
+ const tokens = await loginGithub(config, {
1397
+ fetchImpl,
1398
+ open,
1399
+ print,
1400
+ ...installer ? { onDeviceCode: (prompt) => installer.signIn(prompt) } : {}
1401
+ });
1012
1402
  writeTokens(dir, {
1013
1403
  accessToken: tokens.accessToken,
1014
1404
  refreshToken: tokens.refreshToken,
@@ -1023,20 +1413,20 @@ async function doLogin(config, dir, options, print) {
1023
1413
  clientId: tokens.clientId
1024
1414
  });
1025
1415
  }
1026
- print(`signed in to ${config.product}.`);
1416
+ if (!installer) print(`signed in to ${config.product}.`);
1027
1417
  }
1028
- async function fetchAccessTokenOrLogin(config, dir, options, print) {
1418
+ async function fetchAccessTokenOrLogin(config, dir, options, print, installer) {
1029
1419
  const fetchImpl = options.fetchImpl ?? fetch;
1030
1420
  const fresh = await ensureFreshToken(config, dir, fetchImpl);
1031
1421
  if (fresh) return fresh;
1032
- await doLogin(config, dir, options, print);
1422
+ await doLogin(config, dir, options, print, installer);
1033
1423
  const after = readTokens(dir);
1034
1424
  if (!after) throw new Error("sign-in did not produce a token");
1035
1425
  return after.accessToken;
1036
1426
  }
1037
1427
  function readPayloadArgv(dir, key) {
1038
1428
  try {
1039
- const parsed = JSON.parse(readFileSync4(join4(payloadDir(dir), "payload.json"), "utf8"));
1429
+ const parsed = JSON.parse(readFileSync5(join4(payloadDir(dir), "payload.json"), "utf8"));
1040
1430
  const entry = parsed[key];
1041
1431
  if (typeof entry === "string") {
1042
1432
  const parts = entry.trim().split(/\s+/).filter(Boolean);
@@ -1058,7 +1448,7 @@ function readPayloadRun(dir) {
1058
1448
  }
1059
1449
  function readPayloadVerbs(dir) {
1060
1450
  try {
1061
- const parsed = JSON.parse(readFileSync4(join4(payloadDir(dir), "payload.json"), "utf8"));
1451
+ const parsed = JSON.parse(readFileSync5(join4(payloadDir(dir), "payload.json"), "utf8"));
1062
1452
  const verbs = parsed.verbs;
1063
1453
  if (verbs === "*") return "*";
1064
1454
  if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
@@ -1106,7 +1496,7 @@ function defaultRunEntry(entry, cwd, env) {
1106
1496
  const [command, ...args] = entry;
1107
1497
  const shell = needsShell(command);
1108
1498
  const commandLine = shell ? [command, ...args].map(quoteForShell).join(" ") : command;
1109
- const spawn2 = (progress2) => spawnSync2(commandLine, shell ? [] : args, {
1499
+ const spawnEntry = (progress2) => spawnSync2(commandLine, shell ? [] : args, {
1110
1500
  cwd,
1111
1501
  stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
1112
1502
  shell,
@@ -1115,12 +1505,12 @@ function defaultRunEntry(entry, cwd, env) {
1115
1505
  });
1116
1506
  let result;
1117
1507
  if (process.platform === "win32" && shell) {
1118
- result = spawn2(false);
1508
+ result = spawnEntry(false);
1119
1509
  } else {
1120
1510
  try {
1121
- result = spawn2(true);
1511
+ result = spawnEntry(true);
1122
1512
  } catch {
1123
- result = spawn2(false);
1513
+ result = spawnEntry(false);
1124
1514
  }
1125
1515
  }
1126
1516
  if (result.error) return { ok: false, error: result.error.message };
@@ -1137,73 +1527,71 @@ function faceProduct(config) {
1137
1527
  const known = { mmi: "mmi-hub", jerv: "jerv-hub" };
1138
1528
  return known[config.product] ?? config.product;
1139
1529
  }
1140
- function faceFor(config, options) {
1141
- const tty = options.tty ?? Boolean(process.stdout.isTTY);
1142
- if (!tty) return null;
1530
+ async function installOrUpdate(config, dir, options, print, update) {
1531
+ const product = faceProduct(config);
1532
+ const installer = createInstallerRun({
1533
+ product,
1534
+ gate: config.host,
1535
+ doctor: identityFor(product).doctor,
1536
+ surfaces: [{ id: config.product, kind: "payload" }]
1537
+ }, {
1538
+ operation: update ? "update" : "install",
1539
+ tty: options.tty,
1540
+ animate: !options.print && Boolean(process.stderr.isTTY),
1541
+ write: (text, channel) => {
1542
+ if (options.print) print(text.replace(/\n$/, ""));
1543
+ else (channel === "stdout" ? process.stdout : process.stderr).write(text);
1544
+ }
1545
+ });
1546
+ const fetchImpl = options.fetchImpl ?? fetch;
1547
+ let version = readState(dir)?.version ?? "unknown";
1548
+ installer.start();
1143
1549
  try {
1144
- return createFace({
1145
- product: faceProduct(config),
1146
- color: !process.env.NO_COLOR && process.env.TERM !== "dumb",
1147
- columns: process.stdout.columns
1550
+ let started = Date.now();
1551
+ installer.phase("sign-in", { state: "running" });
1552
+ const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print, installer);
1553
+ installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
1554
+ installer.phase("resolve", { state: "running" });
1555
+ started = Date.now();
1556
+ const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
1557
+ version = manifest.version;
1558
+ installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
1559
+ const current = readState(dir);
1560
+ const unchanged = update && current?.version === version && existsSync4(payloadDir(dir));
1561
+ if (!unchanged) {
1562
+ installer.phase("download", { state: "running" });
1563
+ started = Date.now();
1564
+ await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
1565
+ writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1566
+ installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
1567
+ }
1568
+ installer.surface({
1569
+ id: config.product,
1570
+ from: current?.version,
1571
+ to: version,
1572
+ state: unchanged ? "current" : "updated"
1148
1573
  });
1149
- } catch {
1150
- return null;
1151
- }
1152
- }
1153
- function since(start) {
1154
- return (Date.now() - start) / 1e3;
1155
- }
1156
- function outerConsoleOwnsOutcome() {
1157
- return process.env.MM_OUTER_CONSOLE === "1";
1158
- }
1159
- function printReceipt(face, config, print, ready, lines) {
1160
- const name = face ? face.identity.name : config.product;
1161
- const headline = ready ? `${name} is ready.` : `${name} is installed, but not ready yet.`;
1162
- if (ready && outerConsoleOwnsOutcome()) return;
1163
- if (!face) {
1164
- print(headline);
1165
- for (const line of lines) print(line.trim());
1166
- return;
1574
+ return await finishLastMile(config, dir, options, installer, version, unchanged);
1575
+ } catch (error) {
1576
+ const code = error instanceof NeedsLoginError ? 3 : 1;
1577
+ installer.finish({
1578
+ version,
1579
+ total: 1,
1580
+ updated: 0,
1581
+ failed: 1,
1582
+ detail: error.message,
1583
+ retry: `${config.binName} ${update ? "update" : "install"}`
1584
+ });
1585
+ return code;
1586
+ } finally {
1587
+ installer.stop();
1167
1588
  }
1168
- const glyph = ready ? "\u2714" : "\u2716";
1169
- for (const line of face.receipt([`${glyph} ${headline}`, ...lines])) print(line);
1170
- print(face.signOff());
1171
- }
1172
- function printStep(face, print, title, seconds, kind = "ok") {
1173
- print(face ? face.step(title, seconds, kind) : title);
1174
1589
  }
1175
- function printProgress(face, print, progress) {
1176
- for (const message of progress ?? []) {
1177
- printStep(face, print, message.step, message.ms === void 0 ? null : message.ms / 1e3, message.state);
1178
- }
1590
+ function doInstall(config, dir, options, print) {
1591
+ return installOrUpdate(config, dir, options, print, false);
1179
1592
  }
1180
- async function doInstall(config, dir, options, print) {
1181
- const fetchImpl = options.fetchImpl ?? fetch;
1182
- const face = faceFor(config, options);
1183
- const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
1184
- const started = Date.now();
1185
- const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
1186
- await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
1187
- writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1188
- printStep(face, print, `installed ${config.product} ${manifest.version}`, since(started));
1189
- return finishLastMile(config, dir, options, print, face, manifest.version);
1190
- }
1191
- async function doUpdate(config, dir, options, print) {
1192
- const fetchImpl = options.fetchImpl ?? fetch;
1193
- const face = faceFor(config, options);
1194
- if (face && !outerConsoleOwnsOutcome()) for (const line of face.welcome()) print(line);
1195
- const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
1196
- const started = Date.now();
1197
- const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
1198
- const current = readState(dir);
1199
- if (current && current.version === manifest.version && existsSync4(payloadDir(dir))) {
1200
- printStep(face, print, `${config.product} is already up to date at ${manifest.version}`, since(started));
1201
- return finishLastMile(config, dir, options, print, face, manifest.version);
1202
- }
1203
- await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
1204
- writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1205
- printStep(face, print, `updated ${config.product} to ${manifest.version}`, since(started));
1206
- return finishLastMile(config, dir, options, print, face, manifest.version);
1593
+ function doUpdate(config, dir, options, print) {
1594
+ return installOrUpdate(config, dir, options, print, true);
1207
1595
  }
1208
1596
  function payloadEnv(dir) {
1209
1597
  const stored = readTokens(dir);
@@ -1225,51 +1613,124 @@ function payloadFileAsCommand(entry, payload) {
1225
1613
  return null;
1226
1614
  }
1227
1615
  }
1228
- function finishLastMile(config, dir, options, print, face, version) {
1616
+ async function finishLastMile(config, dir, options, installer, version, unchanged) {
1229
1617
  const entry = readPayloadEntry(dir);
1230
1618
  const payload = payloadDir(dir);
1231
1619
  if (!entry) {
1232
- printReceipt(face, config, print, true, [
1233
- `Installed ${version} into ${dir}`,
1234
- `next step: run ${join4(payload, config.binName)} to start ${config.product}.`
1235
- ]);
1620
+ installer.finish({
1621
+ version,
1622
+ total: 1,
1623
+ updated: unchanged ? 0 : 1,
1624
+ failed: 0,
1625
+ installed: true,
1626
+ detail: `next step: run ${join4(payload, config.binName)} to start ${config.product}.`
1627
+ });
1236
1628
  return 0;
1237
1629
  }
1238
1630
  const command = resolveEntry(entry);
1239
1631
  const dataFile = payloadFileAsCommand(entry, payload);
1240
1632
  if (dataFile) {
1241
- printStep(face, print, `The payload names ${dataFile} as its command, but that is a file, not a program`, null, "fail");
1242
- printReceipt(face, config, print, false, [
1243
- `Downloaded ${version} into ${dir}`,
1244
- `This payload was built wrong: its entry must be a command, not one of its own files.`,
1245
- `Nothing on this machine can finish it \u2014 report it to the ${config.product} maintainers.`
1246
- ]);
1633
+ installer.finish({
1634
+ version,
1635
+ total: 1,
1636
+ updated: 0,
1637
+ failed: 1,
1638
+ 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.`
1639
+ });
1247
1640
  return 2;
1248
1641
  }
1642
+ installer.phase("activate", { state: "running" });
1249
1643
  const started = Date.now();
1250
- const result = (options.runEntry ?? defaultRunEntry)(command, payload, payloadEnv(dir));
1251
- printProgress(face, print, result.progress);
1252
- if (result.ok) {
1253
- printStep(face, print, "Armed this machine", since(started));
1254
- printReceipt(face, config, print, true, [
1255
- `Installed ${version} into ${dir}`,
1256
- `Check health any time: ${config.binName} doctor`
1257
- ]);
1258
- return 0;
1644
+ const env = { ...payloadEnv(dir) ?? process.env, MM_OUTER_CONSOLE: "1" };
1645
+ const result = options.runEntry ? options.runEntry(command, payload, env) : await runInstallEntry(command, payload, env, installer, readTokens(dir));
1646
+ for (const progress of result.progress ?? []) installer.milestone(progress);
1647
+ const succeeded = result.ok && (result.code === void 0 || result.code === 0);
1648
+ const outcome = result.outcome ? validateInstallerOutcome(result.outcome) : void 0;
1649
+ installer.phase("activate", { state: succeeded ? "ok" : "fail", seconds: (Date.now() - started) / 1e3 });
1650
+ installer.finish({
1651
+ ...outcome ?? {
1652
+ version,
1653
+ total: 1,
1654
+ updated: succeeded && !unchanged ? 1 : 0,
1655
+ failed: succeeded ? 0 : 1,
1656
+ installed: true
1657
+ },
1658
+ ...!succeeded ? {
1659
+ operationFailed: true,
1660
+ 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}` : ""}`,
1661
+ retry: `(cd ${payload} && ${command.join(" ")})`
1662
+ } : {}
1663
+ });
1664
+ return succeeded ? outcome?.operationFailed || outcome?.failed ? 1 : 0 : result.code || 1;
1665
+ }
1666
+ async function runInstallEntry(entry, cwd, env, installer, tokens = null) {
1667
+ const [command, ...args] = entry;
1668
+ const shell = needsShell(command);
1669
+ const progress = !(process.platform === "win32" && shell);
1670
+ const outcomeDir = mkdtempSync(join4(tmpdir4(), "mm-installer-outcome-"));
1671
+ const outcomeFile = join4(outcomeDir, "outcome.json");
1672
+ const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
1673
+ delete childEnv.MM_FACE_TRANSCRIPT;
1674
+ delete childEnv.MM_PROGRESS_FD;
1675
+ delete childEnv.MM_PROGRESS_PROTOCOL;
1676
+ if (progress) Object.assign(childEnv, { MM_PROGRESS_FD: "3", MM_PROGRESS_PROTOCOL: "1" });
1677
+ const secrets = [tokens?.accessToken, tokens?.refreshToken].filter((value) => Boolean(value));
1678
+ const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
1679
+ try {
1680
+ const result = await new Promise((resolve2) => {
1681
+ const child = spawn2(shell ? [command, ...args].map(quoteForShell).join(" ") : command, shell ? [] : args, {
1682
+ cwd,
1683
+ shell,
1684
+ windowsHide: true,
1685
+ env: childEnv,
1686
+ stdio: progress ? ["inherit", "pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
1687
+ });
1688
+ for (const [stream, channel2] of [[child.stdout, "stdout"], [child.stderr, "stderr"]]) {
1689
+ let partial = "";
1690
+ stream?.setEncoding("utf8").on("data", (text) => {
1691
+ const safe = redact(partial + text);
1692
+ let held = 0;
1693
+ for (const secret of secrets) for (let length = 1; length < secret.length; length++) {
1694
+ if (safe.endsWith(secret.slice(0, length))) held = Math.max(held, length);
1695
+ }
1696
+ partial = held ? safe.slice(-held) : "";
1697
+ const visible = held ? safe.slice(0, -held) : safe;
1698
+ if (visible) installer.relay(visible, channel2, false);
1699
+ }).on("end", () => {
1700
+ if (partial) installer.relay("[redacted]", channel2, false);
1701
+ });
1702
+ }
1703
+ let pending = "";
1704
+ const channel = child.stdio[3];
1705
+ if (channel && "setEncoding" in channel) {
1706
+ channel.setEncoding("utf8");
1707
+ channel.on("data", (text) => {
1708
+ pending += text;
1709
+ const end = pending.lastIndexOf("\n");
1710
+ if (end >= 0) {
1711
+ for (const record of parseProgress(pending.slice(0, end))) installer.milestone({ ...record, step: redact(record.step) });
1712
+ pending = pending.slice(end + 1);
1713
+ }
1714
+ if (pending.length > 65536) pending = "";
1715
+ });
1716
+ }
1717
+ child.on("error", (error) => resolve2({ ok: false, error: error.message }));
1718
+ child.on("close", (code) => resolve2({
1719
+ ok: code === 0,
1720
+ ...code !== null ? { code } : {},
1721
+ ...code !== 0 ? { error: `exit code ${code}` } : {}
1722
+ }));
1723
+ });
1724
+ try {
1725
+ const outcome = readInstallerOutcome(outcomeFile);
1726
+ return { ...result, ...outcome ? { outcome } : {} };
1727
+ } catch {
1728
+ return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
1729
+ }
1730
+ } finally {
1731
+ if (existsSync4(outcomeFile)) unlinkSync(outcomeFile);
1732
+ rmdirSync(outcomeDir);
1259
1733
  }
1260
- const code = result.code ?? 1;
1261
- printStep(
1262
- face,
1263
- print,
1264
- 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})`,
1265
- null,
1266
- "fail"
1267
- );
1268
- printReceipt(face, config, print, false, [
1269
- `Downloaded ${version} into ${dir}`,
1270
- `Finish it with: (cd ${payload} && ${command.join(" ")})`
1271
- ]);
1272
- return code;
1273
1734
  }
1274
1735
  function doForward(config, dir, options, command, argv, print) {
1275
1736
  if (command && existsSync4(command)) {
@@ -1392,5 +1853,6 @@ export {
1392
1853
  readPayloadVerbs,
1393
1854
  resolveEntry,
1394
1855
  run,
1395
- runFile
1856
+ runFile,
1857
+ runInstallEntry
1396
1858
  };