@mutmutco/installer-launcher 1.0.0 → 1.1.0

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.
@@ -24,6 +24,7 @@ __export(index_exports, {
24
24
  defaultRunEntry: () => defaultRunEntry,
25
25
  ensureFreshToken: () => ensureFreshToken,
26
26
  formatMb: () => formatMb,
27
+ parseArgv: () => parseArgv,
27
28
  readPayloadEntry: () => readPayloadEntry,
28
29
  readPayloadRun: () => readPayloadRun,
29
30
  readPayloadVerbs: () => readPayloadVerbs,
@@ -329,6 +330,11 @@ function parseProductConfig(text) {
329
330
  } catch {
330
331
  throw new Error("product config host must be an http(s) URL");
331
332
  }
333
+ const dl = field(record, "dl").replace(/\/+$/, "");
334
+ if (dl) {
335
+ if (!/^https:\/\//u.test(dl)) throw new Error("product config dl must be an https URL");
336
+ config.dl = dl;
337
+ }
332
338
  return config;
333
339
  }
334
340
  function field(record, key) {
@@ -704,10 +710,11 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
704
710
  }
705
711
 
706
712
  // src/index.ts
707
- var import_node_child_process5 = require("node:child_process");
708
- var import_node_fs11 = require("node:fs");
709
- var import_node_os4 = require("node:os");
710
- var import_node_path6 = require("node:path");
713
+ var import_node_child_process6 = require("node:child_process");
714
+ var import_node_fs12 = require("node:fs");
715
+ var import_node_os5 = require("node:os");
716
+ var import_node_path7 = require("node:path");
717
+ var import_node_sea2 = require("node:sea");
711
718
  var import_node_url2 = require("node:url");
712
719
 
713
720
  // ../face/src/face.ts
@@ -718,6 +725,7 @@ var PRODUCTS = Object.freeze({
718
725
  "mm-strategy": Object.freeze({
719
726
  name: "MM Strategy",
720
727
  installWarm: "Welcome. Let's get you ready.",
728
+ checkWarm: "Here's where things stand.",
721
729
  accent: "38;2;249;115;22",
722
730
  warm: "Welcome back. Checking for updates...",
723
731
  doctor: "mm-strategy doctor"
@@ -725,6 +733,7 @@ var PRODUCTS = Object.freeze({
725
733
  "mmi-hub": Object.freeze({
726
734
  name: "mmi-hub",
727
735
  installWarm: "Welcome. Let's get you ready.",
736
+ checkWarm: "Here's where things stand.",
728
737
  accent: "38;2;125;211;252",
729
738
  warm: "Welcome back. Checking for updates...",
730
739
  doctor: "mmi doctor"
@@ -732,6 +741,7 @@ var PRODUCTS = Object.freeze({
732
741
  "jerv-hub": Object.freeze({
733
742
  name: "jerv-hub",
734
743
  installWarm: "Welcome. Let's get you ready.",
744
+ checkWarm: "Here's where things stand.",
735
745
  accent: "38;2;248;113;113",
736
746
  warm: "Welcome back. Checking for updates...",
737
747
  doctor: "jerv doctor"
@@ -739,6 +749,7 @@ var PRODUCTS = Object.freeze({
739
749
  jervcode: Object.freeze({
740
750
  name: "JervCode",
741
751
  installWarm: "Welcome. Let's get you ready.",
752
+ checkWarm: "Here's where things stand.",
742
753
  accent: "38;2;192;132;252",
743
754
  warm: "Welcome back. Checking for updates...",
744
755
  doctor: "jervcode doctor"
@@ -854,7 +865,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
854
865
  const welcome = () => continuesFace || nested ? [] : [
855
866
  `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
856
867
  bar(),
857
- `${bar()} ${operation === "install" ? identity.installWarm : identity.warm}`,
868
+ `${bar()} ${operation === "install" ? identity.installWarm : operation === "check" ? identity.checkWarm : identity.warm}`,
858
869
  bar()
859
870
  ];
860
871
  const continues = (phase, kind = "ok") => {
@@ -884,7 +895,11 @@ function createFace({ product, color = false, columns, env = process.env, operat
884
895
  const content = Math.min(width - 6, Math.max(...body.map(visibleWidth)));
885
896
  const rule = paint(identity.accent, GLYPH.rule.repeat(content + 4));
886
897
  const frame = (left, right) => `${paint(identity.accent, left)}${rule}${paint(identity.accent, right)}`;
887
- const bodyRow = (line) => line.startsWith(GLYPH.check) ? `${paint(identity.accent, GLYPH.check)}${line.slice(1)}` : line;
898
+ const headGlyph = { [GLYPH.check]: identity.accent, [GLYPH.cross]: identity.accent, [GLYPH.dot]: PALETTE.muted };
899
+ const bodyRow = (line) => {
900
+ const sgr = headGlyph[line.charAt(0)];
901
+ return sgr ? `${paint(sgr, line.charAt(0))}${line.slice(1)}` : line;
902
+ };
888
903
  return [
889
904
  frame(GLYPH.boxTop, GLYPH.boxTopEnd),
890
905
  ...body.map((line) => `${paint(identity.accent, GLYPH.bar)} ${bodyRow(line)}${" ".repeat(Math.max(0, content - visibleWidth(line)))} ${paint(identity.accent, GLYPH.bar)}`),
@@ -910,6 +925,9 @@ function createFace({ product, color = false, columns, env = process.env, operat
910
925
  // ../face/src/shell.ts
911
926
  var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
912
927
 
928
+ // ../face/src/rows.ts
929
+ var plainText = (text) => String(text).replaceAll("\u2192", "->").replaceAll("\xB7", "-").replaceAll("\u2014", "-").replaceAll("\u2026", "...");
930
+
913
931
  // ../face/src/spinner.ts
914
932
  var import_node_fs7 = require("node:fs");
915
933
  var import_node_worker_threads = require("node:worker_threads");
@@ -1142,22 +1160,44 @@ function validateInstallerProduct(value) {
1142
1160
  if (doctor !== identity.doctor) return fail("doctor");
1143
1161
  if (!Array.isArray(input.surfaces) || input.surfaces.length === 0) return fail("surfaces");
1144
1162
  const ids = /* @__PURE__ */ new Set();
1145
- const surfaces = input.surfaces.map((raw) => {
1146
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail("surface");
1147
- const source = raw;
1148
- const id = text(source.id, "surface.id");
1163
+ const surfaces = input.surfaces.map((raw2) => {
1164
+ if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return fail("surface");
1165
+ const source2 = raw2;
1166
+ const id = text(source2.id, "surface.id");
1149
1167
  if (ids.has(id)) return fail("duplicate surface.id");
1150
1168
  ids.add(id);
1151
1169
  const surface = { id };
1152
1170
  for (const field2 of ["npm", "bin", "kind", "activation"]) {
1153
- if (source[field2] !== void 0) surface[field2] = text(source[field2], `surface.${field2}`);
1171
+ if (source2[field2] !== void 0) surface[field2] = text(source2[field2], `surface.${field2}`);
1154
1172
  }
1155
1173
  if (Boolean(surface.npm) !== Boolean(surface.bin)) return fail("surface npm/bin pair");
1156
1174
  if (surface.kind !== void 0 && !["agent-home", "payload"].includes(surface.kind)) return fail("surface.kind");
1157
1175
  if (!surface.npm && !surface.kind) return fail("surface implementation");
1158
1176
  return surface;
1159
1177
  });
1160
- return { product, gate, doctor, surfaces };
1178
+ if (input.launcher === void 0) return { product, gate, doctor, surfaces };
1179
+ const raw = input.launcher;
1180
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail("launcher");
1181
+ const source = raw;
1182
+ const https = (value2, field2) => {
1183
+ const url = text(value2, field2).replace(/\/+$/u, "");
1184
+ let parsed;
1185
+ try {
1186
+ parsed = new URL(url);
1187
+ } catch {
1188
+ return fail(field2);
1189
+ }
1190
+ return parsed.protocol === "https:" && !parsed.username && !parsed.password ? url : fail(field2);
1191
+ };
1192
+ const launcher = {
1193
+ product: text(source.product, "launcher.product"),
1194
+ bin: text(source.bin, "launcher.bin"),
1195
+ key: text(source.key, "launcher.key"),
1196
+ host: https(source.host, "launcher.host")
1197
+ };
1198
+ if (source.dl !== void 0) launcher.dl = https(source.dl, "launcher.dl");
1199
+ if (Buffer.from(launcher.key, "base64").length !== 32) return fail("launcher.key");
1200
+ return { product, gate, doctor, surfaces, launcher };
1161
1201
  }
1162
1202
  var PHASES = {
1163
1203
  preflight: ["Checking prerequisites", "Checked prerequisites"],
@@ -1182,6 +1222,23 @@ var UPDATE_CHECK_PHASES = /* @__PURE__ */ new Set([
1182
1222
  "sign-in",
1183
1223
  "check"
1184
1224
  ]);
1225
+ var SURFACE_WORDS = Object.freeze({
1226
+ updated: "updated",
1227
+ current: "already current",
1228
+ failed: "failed",
1229
+ skipped: "skipped",
1230
+ retry: "retrying",
1231
+ pending: "pending",
1232
+ kept: "kept",
1233
+ /** Read-only report states. */
1234
+ behind: "update available",
1235
+ ahead: "ahead of the release",
1236
+ staged: "next launch",
1237
+ unknown: "unknown",
1238
+ missing: "not installed"
1239
+ });
1240
+ var AUTO_UPDATE = "auto-update";
1241
+ var LAUNCHER_SURFACE = "launcher";
1185
1242
  function createInstallerRun(value, options = {}) {
1186
1243
  const declaration = validateInstallerProduct(value);
1187
1244
  const env = options.env ?? process.env;
@@ -1229,6 +1286,18 @@ function createInstallerRun(value, options = {}) {
1229
1286
  let chunkColumn = 0;
1230
1287
  const trailingCR = /* @__PURE__ */ new Set();
1231
1288
  let deferredActivity;
1289
+ let runningPhase;
1290
+ const animateRunning = (title, measure) => {
1291
+ if (face.progress(title, measure)) return;
1292
+ if (activeTitle === title) spinner.say(title, measure);
1293
+ else {
1294
+ activeTitle = title;
1295
+ spinner.start(title, measure);
1296
+ }
1297
+ };
1298
+ const resumeRunningPhase = () => {
1299
+ if (runningPhase && !finished) animateRunning(runningPhase.title, runningPhase.measure);
1300
+ };
1232
1301
  const drawnPhaseCompletions = /* @__PURE__ */ new Set();
1233
1302
  const closeChunk = () => {
1234
1303
  if (chunkOpen) emit("\n", chunkChannel);
@@ -1280,14 +1349,11 @@ function createInstallerRun(value, options = {}) {
1280
1349
  throw new Error("installer run: invalid runningTitle");
1281
1350
  }
1282
1351
  const runningTitle = facts.runningTitle?.trim() ?? title;
1283
- if (face.progress(runningTitle, facts.measure ?? null)) return;
1284
- if (activeTitle === runningTitle) spinner.say(runningTitle, facts.measure ?? null);
1285
- else {
1286
- activeTitle = runningTitle;
1287
- spinner.start(runningTitle, facts.measure ?? null);
1288
- }
1352
+ runningPhase = { id: fold ? "check" : id, title: runningTitle, measure: facts.measure ?? null };
1353
+ animateRunning(runningTitle, facts.measure ?? null);
1289
1354
  return;
1290
1355
  }
1356
+ if (runningPhase && runningPhase.id === (fold ? "check" : id)) runningPhase = void 0;
1291
1357
  if (fold && id !== "check") {
1292
1358
  if (facts.detail) run2.relay(facts.detail);
1293
1359
  return;
@@ -1300,17 +1366,18 @@ function createInstallerRun(value, options = {}) {
1300
1366
  },
1301
1367
  surface(facts) {
1302
1368
  if (finished) throw new Error("installer run already finished");
1303
- const surface = declaration.surfaces.find((surface2) => surface2.id === facts.id);
1369
+ const surface = declaration.surfaces.find((surface2) => surface2.id === facts.id) ?? (facts.id === LAUNCHER_SURFACE ? { id: LAUNCHER_SURFACE } : void 0);
1304
1370
  if (!surface) throw new Error("installer run: undeclared surface");
1305
1371
  start();
1306
1372
  const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} ${tty ? "\u2192" : "->"} ${facts.to}` : ` ${facts.to}` : "";
1307
- const status = { updated: options.dryRun ? "would update" : "updated", current: "already current", failed: "failed", skipped: "skipped", retry: "retrying", pending: "pending", kept: "kept" }[facts.state];
1373
+ const status = facts.state === "updated" && options.dryRun ? "would update" : SURFACE_WORDS[facts.state];
1308
1374
  if (!status) throw new Error("installer run: unknown surface state");
1309
1375
  const separator = tty ? "\xB7" : "-";
1310
1376
  const activation = facts.state === "updated" && surface.activation ? ` ${separator} ${surface.activation}` : "";
1311
1377
  const completed = ["updated", "current", "kept", "skipped"].includes(facts.state);
1312
1378
  durable(`${facts.id}${versions} ${separator} ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
1313
1379
  if (facts.detail) run2.relay(facts.detail);
1380
+ resumeRunningPhase();
1314
1381
  },
1315
1382
  milestone({ step, state, ms, measure }) {
1316
1383
  start();
@@ -1329,6 +1396,7 @@ function createInstallerRun(value, options = {}) {
1329
1396
  if (state !== "fail" && drawnPhaseCompletions.has(step)) return;
1330
1397
  durable(step, ms === void 0 ? null : ms / 1e3, state);
1331
1398
  if (state === "ok") drawnPhaseCompletions.add(step);
1399
+ resumeRunningPhase();
1332
1400
  },
1333
1401
  signIn({ url, code }) {
1334
1402
  start();
@@ -1410,8 +1478,9 @@ function createInstallerRun(value, options = {}) {
1410
1478
  if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
1411
1479
  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}.`;
1412
1480
  const ready = facts.failed === 0 && !facts.dryRun && !facts.deferred && !facts.operationFailed && Boolean(facts.version);
1481
+ const deferred = Boolean(facts.deferred) && facts.failed === 0 && !facts.operationFailed;
1413
1482
  const body = [
1414
- `${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."}`,
1483
+ `${ready ? GLYPH.check : facts.dryRun || deferred ? GLYPH.dot : GLYPH.cross} ${face.identity.name}${ready ? " is ready." : facts.dryRun ? " preview complete." : deferred ? ` ${operation === "install" ? "install" : "update"} deferred.` : " is not ready yet."}`,
1415
1484
  changed,
1416
1485
  ...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
1417
1486
  ...facts.detail ? [facts.detail] : [],
@@ -1427,12 +1496,96 @@ function createInstallerRun(value, options = {}) {
1427
1496
  stop() {
1428
1497
  closeChunk();
1429
1498
  activeTitle = void 0;
1499
+ runningPhase = void 0;
1430
1500
  spinner.stop();
1431
1501
  }
1432
1502
  };
1433
1503
  return run2;
1434
1504
  }
1435
1505
 
1506
+ // ../face/src/screens.ts
1507
+ function screenLane(render = {}, stream = "stdout") {
1508
+ const env = render.env ?? process.env;
1509
+ const out = stream === "stdout" ? process.stdout : process.stderr;
1510
+ const tty = render.tty ?? Boolean(out.isTTY);
1511
+ const color = tty && render.color !== false && env.NO_COLOR === void 0 && env.TERM !== "dumb";
1512
+ return { tty, color, columns: render.columns ?? out.columns, env };
1513
+ }
1514
+ var faceFor = (product, lane, operation) => createFace({ product, color: lane.color, columns: lane.columns, env: { ...lane.env, MM_OUTER_CONSOLE: void 0, MM_FACE_CONTINUES: void 0, MM_PROGRESS_FD: void 0, MM_PROGRESS_FILE: void 0 }, operation });
1515
+ function renderNotice(product, notice, render) {
1516
+ const lane = screenLane(render, notice.state === "fail" ? "stderr" : "stdout");
1517
+ const face = faceFor(product, lane);
1518
+ const width = face.width - 3;
1519
+ const [first, ...rest] = wrapWords(plain(notice.message, lane), width);
1520
+ if (!lane.tty) {
1521
+ return [`${notice.state === "fail" ? "Error: " : ""}${[first, ...rest].join(" ")}`, ...notice.next ? [`Next: ${notice.next}`] : []];
1522
+ }
1523
+ const glyph = notice.state === "fail" ? face.paint(face.identity.accent, GLYPH.cross) : notice.state === "note" ? face.paint(PALETTE.muted, GLYPH.dot) : face.paint(face.identity.accent, GLYPH.check);
1524
+ return [
1525
+ `${glyph} ${first}`,
1526
+ ...rest.map((line) => ` ${line}`),
1527
+ ...notice.next ? [` ${face.paint(PALETTE.muted, "Next:")} ${notice.next}`] : []
1528
+ ];
1529
+ }
1530
+ function renderReport(product, report, render) {
1531
+ const lane = screenLane(render);
1532
+ const face = faceFor(product, lane, "check");
1533
+ const { verdict } = report;
1534
+ const headline = `${face.identity.name} ${verdict.headline}`;
1535
+ if (!lane.tty) {
1536
+ return [
1537
+ `${face.identity.name} - Mutatis Mutandis`,
1538
+ face.identity.checkWarm,
1539
+ ...report.rows.map((row) => `${row.mark === "fail" ? "Failed: " : ""}${plainText(rowTitle(row))}`),
1540
+ ...report.rows.flatMap((row) => row.note ? [` ${plainText(row.note)}`] : []),
1541
+ `${verdict.state === "fail" ? "Error: " : ""}${plainText(headline)}`,
1542
+ ...(verdict.facts ?? []).map(plainText),
1543
+ `Next: ${verdict.next}`
1544
+ ];
1545
+ }
1546
+ const glyph = verdict.state === "fail" ? GLYPH.cross : verdict.state === "note" ? GLYPH.dot : GLYPH.check;
1547
+ const facts = verdict.facts?.length ? verdict.facts : [verdict.state === "ok" ? "Everything checked passed." : "See the rows above."];
1548
+ return [
1549
+ ...face.welcome(),
1550
+ // A report row IS an update row: glyph, subject, state (#7071). Two layouts for one fact — a
1551
+ // mark mid-row after a subject column here, a leading glyph in `update` — is the drift itself.
1552
+ ...report.rows.flatMap((row) => [face.step(rowTitle(row), null, row.mark ?? "note"), ...row.note ? [face.relay(row.note)] : []]),
1553
+ ...report.rows.length ? [face.paint(PALETTE.muted, GLYPH.bar)] : [],
1554
+ ...face.receipt([`${glyph} ${headline}`, ...facts, `Next: ${verdict.next}`], { ready: verdict.state === "ok" }),
1555
+ face.signOff()
1556
+ ].filter((line) => line !== "");
1557
+ }
1558
+ function renderUsage(product, usage2, render) {
1559
+ const lane = screenLane(render);
1560
+ const face = faceFor(product, lane);
1561
+ const all = [...usage2.commands, ...usage2.options ?? []];
1562
+ const column = Math.min(Math.max(...all.map((entry2) => entry2.usage.length)), 34);
1563
+ const table = (entries) => entries.flatMap((entry2) => {
1564
+ const [first, ...rest] = wrapWords(plain(entry2.text, lane), Math.max(20, face.width - column - 6));
1565
+ const head = entry2.usage.length > column ? [` ${entry2.usage}`, ` ${" ".repeat(column)} ${first}`] : [` ${entry2.usage.padEnd(column)} ${first}`];
1566
+ return [...head, ...rest.map((line) => ` ${" ".repeat(column)} ${line}`)];
1567
+ });
1568
+ return [
1569
+ `${face.paint(face.identity.accent, usage2.bin)} ${usage2.version} ${lane.tty ? "\xB7" : "-"} ${plain(usage2.summary, lane)}`,
1570
+ "",
1571
+ `Usage: ${usage2.bin} <command> [options]`,
1572
+ "",
1573
+ "Commands:",
1574
+ ...table(usage2.commands),
1575
+ ...usage2.options?.length ? ["", "Options:", ...table(usage2.options)] : []
1576
+ ];
1577
+ }
1578
+ function renderVersion(bin, version, launcher) {
1579
+ return `${bin} ${version}${launcher ? ` (launcher ${launcher})` : ""}`;
1580
+ }
1581
+ function printScreen(lines, stream = "stdout") {
1582
+ const out = stream === "stdout" ? process.stdout : process.stderr;
1583
+ out.write(lines.map((line) => `${line}
1584
+ `).join(""));
1585
+ }
1586
+ var plain = (text, lane) => lane.tty ? String(text) : plainText(stripColor(String(text)));
1587
+ var rowTitle = (row) => `${row.surface}${row.aspect ? ` ${row.aspect}` : ""}${row.instance ? ` \xB7 ${row.instance}` : ""} ${row.state}`;
1588
+
1436
1589
  // src/autoupdate.ts
1437
1590
  var import_node_child_process3 = require("node:child_process");
1438
1591
  var import_node_fs10 = require("node:fs");
@@ -1463,19 +1616,33 @@ function scheduleName(config) {
1463
1616
  function scheduleLabel(config) {
1464
1617
  return `${config.binName}-autoupdate`.replace(/[^A-Za-z0-9_-]+/g, "-");
1465
1618
  }
1619
+ var HIDDEN_SCRIPT = "autoupdate-hidden.vbs";
1620
+ function hiddenVbs(command2) {
1621
+ const line = command2.map((arg) => `""${arg.replaceAll('"', '""')}""`).join(" ");
1622
+ return `' Generated by \`autoupdate on\`: wscript //B runs this with no window, so the hourly update
1623
+ ' never flashes a console. The exit code is the update's, for Task Scheduler's last result.
1624
+ Dim shell, code
1625
+ Set shell = CreateObject("WScript.Shell")
1626
+ code = shell.Run("${line}", 0, True)
1627
+ WScript.Quit code
1628
+ `;
1629
+ }
1466
1630
  function quoteWindows(arg) {
1467
1631
  return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
1468
1632
  }
1469
1633
  function enableSchedule(config, command2, options = {}) {
1470
1634
  const platform = schedulePlatform(options.platform);
1471
- if (!platform) throw new Error(`autoupdate is not supported on ${options.platform ?? process.platform}.`);
1635
+ if (!platform) throw new Error(`Auto-update is not supported on ${options.platform ?? process.platform}.`);
1472
1636
  const exec = options.exec ?? defaultExec;
1473
1637
  const home = homeOf(options);
1474
1638
  if (platform === "win32") {
1475
- const taskLine = command2.map(quoteWindows).join(" ");
1476
- const result2 = exec("schtasks", ["/Create", "/TN", scheduleName(config), "/TR", taskLine, "/SC", "HOURLY", "/IT", "/F"]);
1639
+ const stateDir = options.stateDir ?? defaultProductDir(config.product);
1640
+ (0, import_node_fs10.mkdirSync)(stateDir, { recursive: true });
1641
+ const script = (0, import_node_path5.join)(stateDir, HIDDEN_SCRIPT);
1642
+ (0, import_node_fs10.writeFileSync)(script, hiddenVbs(command2));
1643
+ const result2 = exec("schtasks", ["/Create", "/TN", scheduleName(config), "/TR", `wscript.exe //B //Nologo ${quoteWindows(script)}`, "/SC", "HOURLY", "/IT", "/F"]);
1477
1644
  if (result2.code !== 0) {
1478
- throw new Error(`could not turn autoupdate on: ${firstLine(result2.stdout || result2.stderr) || `exit ${result2.code}`}`);
1645
+ throw new Error(`Could not turn auto-update on: ${firstLine(result2.stdout || result2.stderr) || `exit ${result2.code}`}`);
1479
1646
  }
1480
1647
  return;
1481
1648
  }
@@ -1488,7 +1655,7 @@ function enableSchedule(config, command2, options = {}) {
1488
1655
  exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
1489
1656
  const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
1490
1657
  if (result2.code !== 0) {
1491
- throw new Error(`could not turn autoupdate on: ${firstLine(result2.stderr || result2.stdout) || `exit ${result2.code}`}`);
1658
+ throw new Error(`Could not turn auto-update on: ${firstLine(result2.stderr || result2.stdout) || `exit ${result2.code}`}`);
1492
1659
  }
1493
1660
  return;
1494
1661
  }
@@ -1499,23 +1666,24 @@ function enableSchedule(config, command2, options = {}) {
1499
1666
  (0, import_node_fs10.writeFileSync)((0, import_node_path5.join)(dir, `${label}.timer`), linuxTimer(label));
1500
1667
  const reload = exec("systemctl", ["--user", "daemon-reload"]);
1501
1668
  if (reload.code !== 0) {
1502
- throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
1669
+ throw new Error(`Could not turn auto-update on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
1503
1670
  }
1504
1671
  const result = exec("systemctl", ["--user", "enable", "--now", `${label}.timer`]);
1505
1672
  if (result.code !== 0) {
1506
- throw new Error(`could not turn autoupdate on: ${firstLine(result.stderr || result.stdout) || `exit ${result.code}`}`);
1673
+ throw new Error(`Could not turn auto-update on: ${firstLine(result.stderr || result.stdout) || `exit ${result.code}`}`);
1507
1674
  }
1508
1675
  }
1509
1676
  function disableSchedule(config, options = {}) {
1510
1677
  const platform = schedulePlatform(options.platform);
1511
- if (!platform) throw new Error(`autoupdate is not supported on ${options.platform ?? process.platform}.`);
1678
+ if (!platform) throw new Error(`Auto-update is not supported on ${options.platform ?? process.platform}.`);
1512
1679
  const exec = options.exec ?? defaultExec;
1513
1680
  const home = homeOf(options);
1514
1681
  if (platform === "win32") {
1515
1682
  const result = exec("schtasks", ["/Delete", "/TN", scheduleName(config), "/F"]);
1516
1683
  if (result.code !== 0 && !/cannot find|does not exist/i.test(`${result.stdout} ${result.stderr}`)) {
1517
- throw new Error(`could not turn autoupdate off: ${firstLine(result.stdout || result.stderr) || `exit ${result.code}`}`);
1684
+ throw new Error(`Could not turn auto-update off: ${firstLine(result.stdout || result.stderr) || `exit ${result.code}`}`);
1518
1685
  }
1686
+ (0, import_node_fs10.rmSync)((0, import_node_path5.join)(options.stateDir ?? defaultProductDir(config.product), HIDDEN_SCRIPT), { force: true });
1519
1687
  return;
1520
1688
  }
1521
1689
  if (platform === "darwin") {
@@ -1608,15 +1776,170 @@ function firstLine(text) {
1608
1776
  return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
1609
1777
  }
1610
1778
 
1611
- // src/login-github.ts
1779
+ // src/refresh.ts
1612
1780
  var import_node_child_process4 = require("node:child_process");
1781
+ var import_node_fs11 = require("node:fs");
1782
+ var import_node_os4 = require("node:os");
1783
+ var import_node_path6 = require("node:path");
1784
+ var LAUNCHER_RECORD_FILE = "launcher.json";
1785
+ var RECORD_TIMEOUT_MS = 1e4;
1786
+ function platformKey(platform, arch) {
1787
+ if (platform === "win32") return arch === "x64" || arch === "arm64" ? `win-${arch}` : null;
1788
+ return platform === "darwin" && arch === "arm64" ? "darwin-arm64" : null;
1789
+ }
1790
+ function verifierFor(target) {
1791
+ return { product: target.product, host: target.host, loginKind: "github", publicKey: target.key, binName: target.bin };
1792
+ }
1793
+ async function fetchRecord(target, options) {
1794
+ let json;
1795
+ try {
1796
+ const response = await (options.fetch ?? fetch)(
1797
+ `${target.host.replace(/\/+$/u, "")}/release/launcher`,
1798
+ { signal: AbortSignal.timeout(options.timeoutMs ?? RECORD_TIMEOUT_MS) }
1799
+ );
1800
+ if (!response.ok) return null;
1801
+ json = await response.json();
1802
+ } catch {
1803
+ return null;
1804
+ }
1805
+ let record;
1806
+ try {
1807
+ record = parseManifest(json);
1808
+ } catch {
1809
+ return null;
1810
+ }
1811
+ if (!verifyManifest(record, record.signature, verifierFor(target))) return "bad-signature";
1812
+ return { version: record.version, files: record.files };
1813
+ }
1814
+ async function readLauncherRecord(target, options = {}) {
1815
+ const record = await fetchRecord(target, options);
1816
+ return record === "bad-signature" ? null : record;
1817
+ }
1818
+ function compareVersions(a, b) {
1819
+ const split = (v) => {
1820
+ const [core, pre = ""] = v.split("-", 2);
1821
+ return { nums: core.split(".").map((n) => Number.parseInt(n, 10) || 0), pre };
1822
+ };
1823
+ const x = split(a);
1824
+ const y = split(b);
1825
+ for (let i = 0; i < 3; i++) if ((x.nums[i] ?? 0) !== (y.nums[i] ?? 0)) return (x.nums[i] ?? 0) > (y.nums[i] ?? 0) ? 1 : -1;
1826
+ if (x.pre === y.pre) return 0;
1827
+ if (!x.pre) return 1;
1828
+ if (!y.pre) return -1;
1829
+ return x.pre > y.pre ? 1 : -1;
1830
+ }
1831
+ function recordedPath(productDir) {
1832
+ try {
1833
+ const saved = JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path6.join)(productDir, LAUNCHER_RECORD_FILE), "utf8"));
1834
+ return typeof saved.path === "string" && saved.path ? saved.path : null;
1835
+ } catch {
1836
+ return null;
1837
+ }
1838
+ }
1839
+ function installedPath(target, productDir, platform) {
1840
+ return platform === "win32" ? (0, import_node_path6.join)(productDir, "bin", `${target.bin}.exe`) : (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".local", "bin", target.bin);
1841
+ }
1842
+ function parseLauncherVersion(stdout, bin) {
1843
+ const text = String(stdout).trim();
1844
+ if (!text.startsWith(`${bin} `)) return null;
1845
+ return text.match(/launcher (\d+\.\d+\.\d+[^\s)]*)/u)?.[1] ?? null;
1846
+ }
1847
+ function runVersion(path, bin) {
1848
+ return new Promise((resolve3) => {
1849
+ (0, import_node_child_process4.execFile)(path, ["--version"], { timeout: 3e4, windowsHide: true, encoding: "utf8" }, (error, stdout) => {
1850
+ resolve3(error ? null : parseLauncherVersion(stdout, bin));
1851
+ });
1852
+ });
1853
+ }
1854
+ function removeStale(file) {
1855
+ const prefix = `${(0, import_node_path6.basename)(file)}.old-`;
1856
+ let names;
1857
+ try {
1858
+ names = (0, import_node_fs11.readdirSync)((0, import_node_path6.dirname)(file));
1859
+ } catch {
1860
+ return;
1861
+ }
1862
+ for (const name of names) {
1863
+ if (!name.startsWith(prefix)) continue;
1864
+ try {
1865
+ (0, import_node_fs11.rmSync)((0, import_node_path6.join)((0, import_node_path6.dirname)(file), name), { force: true });
1866
+ } catch {
1867
+ }
1868
+ }
1869
+ }
1870
+ async function refreshLauncher(target, options = {}) {
1871
+ const started = Date.now();
1872
+ const platform = options.platform ?? process.platform;
1873
+ const key = platformKey(platform, options.arch ?? process.arch);
1874
+ if (!key) return null;
1875
+ const productDir = options.productDir ?? defaultProductDir(target.product);
1876
+ const recorded = recordedPath(productDir);
1877
+ const file = options.path ?? (recorded && (0, import_node_fs11.existsSync)(recorded) ? recorded : installedPath(target, productDir, platform));
1878
+ if (!(0, import_node_fs11.existsSync)(file) || (0, import_node_fs11.lstatSync)(file).isSymbolicLink()) return null;
1879
+ if (!options.dryRun) removeStale(file);
1880
+ const from = await (options.readVersion ?? ((path) => runVersion(path, target.bin)))(file);
1881
+ if (!from) return null;
1882
+ const arm = (verdict, to, detail) => ({ surface: "launcher", from, to, verdict, detail, durationMs: Date.now() - started });
1883
+ const record = await fetchRecord(target, options);
1884
+ if (record === "bad-signature") return arm("fail", from, "The launcher record failed its signature check.");
1885
+ const entry2 = record?.files.find((candidate) => candidate.path === key);
1886
+ if (!record || !entry2) return arm("skip", from, "No launcher update is offered yet.");
1887
+ if (compareVersions(record.version, from) <= 0) return arm("ok", from, "");
1888
+ if (options.dryRun) return arm("ok", record.version, "");
1889
+ let unlock;
1890
+ if (!options.lockHeld) {
1891
+ try {
1892
+ unlock = lockInstallation(productDir);
1893
+ } catch {
1894
+ return arm("defer", from, "Another install is running; the launcher updates next time.");
1895
+ }
1896
+ }
1897
+ const next = `${file}.new-${process.pid}`;
1898
+ let moved;
1899
+ try {
1900
+ let bytes;
1901
+ try {
1902
+ const response = await (options.fetch ?? fetch)(
1903
+ `${(target.dl ?? target.host).replace(/\/+$/u, "")}/dl/${target.product}/${key}`,
1904
+ { signal: AbortSignal.timeout(3e5) }
1905
+ );
1906
+ if (!response.ok) return arm("defer", from, "The launcher download did not complete; it retries next time.");
1907
+ bytes = await readDownload(response, void 0, entry2.size);
1908
+ } catch {
1909
+ return arm("defer", from, "The launcher download did not complete; it retries next time.");
1910
+ }
1911
+ if (!verifyFileBytes(entry2, bytes)) return arm("fail", from, "The downloaded launcher failed its checksum.");
1912
+ try {
1913
+ (0, import_node_fs11.writeFileSync)(next, bytes, { mode: 493 });
1914
+ if (platform !== "win32") (0, import_node_fs11.chmodSync)(next, 493);
1915
+ if (platform === "win32") {
1916
+ moved = `${file}.old-${process.pid}-${Date.now()}`;
1917
+ (0, import_node_fs11.renameSync)(file, moved);
1918
+ }
1919
+ (0, import_node_fs11.renameSync)(next, file);
1920
+ } catch {
1921
+ try {
1922
+ if (moved && !(0, import_node_fs11.existsSync)(file)) (0, import_node_fs11.renameSync)(moved, file);
1923
+ } catch {
1924
+ }
1925
+ (0, import_node_fs11.rmSync)(next, { force: true });
1926
+ return arm("fail", from, "The launcher could not be replaced.");
1927
+ }
1928
+ return arm("ok", record.version, "");
1929
+ } finally {
1930
+ unlock?.();
1931
+ }
1932
+ }
1933
+
1934
+ // src/login-github.ts
1935
+ var import_node_child_process5 = require("node:child_process");
1613
1936
  var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
1614
1937
  function openBrowser(url) {
1615
1938
  if (process.env.LAUNCHER_NO_OPEN === "1") return;
1616
1939
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
1617
1940
  const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
1618
1941
  try {
1619
- const child = (0, import_node_child_process4.spawn)(opener, args, { detached: true, stdio: "ignore", windowsHide: true });
1942
+ const child = (0, import_node_child_process5.spawn)(opener, args, { detached: true, stdio: "ignore", windowsHide: true });
1620
1943
  child.on("error", () => {
1621
1944
  });
1622
1945
  child.unref();
@@ -1843,7 +2166,7 @@ function page(title, body) {
1843
2166
  }
1844
2167
 
1845
2168
  // src/index.ts
1846
- var LAUNCHER_VERSION = true ? "1.0.0" : readVersionFromPackage();
2169
+ var LAUNCHER_VERSION = true ? "1.1.0" : readVersionFromPackage();
1847
2170
  function defaultPrint(message) {
1848
2171
  process.stdout.write(`${message}
1849
2172
  `);
@@ -1852,87 +2175,200 @@ function defaultPrintErr(message) {
1852
2175
  process.stderr.write(`${message}
1853
2176
  `);
1854
2177
  }
2178
+ var LAUNCHER_VERBS = /* @__PURE__ */ new Set(["install", "update", "status", "doctor", "login", "logout", "autoupdate"]);
2179
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["--config", "--dir"]);
2180
+ var SWITCHES = {
2181
+ "--json": "json",
2182
+ "--no-color": "noColor",
2183
+ "--dry-run": "dryRun",
2184
+ "--help": "help",
2185
+ "-h": "help",
2186
+ "--version": "version",
2187
+ "-v": "version"
2188
+ };
2189
+ function parseArgv(argv2) {
2190
+ const parsed = { flags: {}, args: [], rest: [] };
2191
+ const flag = (i2) => {
2192
+ const arg = argv2[i2];
2193
+ if (VALUE_FLAGS.has(arg)) {
2194
+ const value = argv2[i2 + 1];
2195
+ if (value === void 0 || value.startsWith("-")) {
2196
+ parsed.unknownFlag ??= arg;
2197
+ return i2 + 1;
2198
+ }
2199
+ parsed.flags[arg === "--config" ? "config" : "dir"] = value;
2200
+ return i2 + 2;
2201
+ }
2202
+ const name = SWITCHES[arg];
2203
+ if (!name) return -1;
2204
+ parsed.flags[name] = true;
2205
+ return i2 + 1;
2206
+ };
2207
+ const skip = (i2) => {
2208
+ const next = flag(i2);
2209
+ if (next >= 0) return next;
2210
+ parsed.unknownFlag ??= argv2[i2];
2211
+ return i2 + 1;
2212
+ };
2213
+ let i = 0;
2214
+ while (i < argv2.length && argv2[i].startsWith("-")) {
2215
+ if (argv2[i] === "--run") {
2216
+ parsed.run = { file: argv2[i + 1], args: argv2.slice(i + 2) };
2217
+ return parsed;
2218
+ }
2219
+ i = skip(i);
2220
+ }
2221
+ parsed.command = argv2[i];
2222
+ if (parsed.command === void 0) return parsed;
2223
+ if (!LAUNCHER_VERBS.has(parsed.command)) {
2224
+ parsed.rest = argv2.slice(i + 1);
2225
+ return parsed;
2226
+ }
2227
+ for (i += 1; i < argv2.length; ) {
2228
+ if (argv2[i].startsWith("-")) i = skip(i);
2229
+ else parsed.args.push(argv2[i++]);
2230
+ }
2231
+ return parsed;
2232
+ }
2233
+ var NO_PRODUCT = "mmi-hub";
2234
+ function sentence(message) {
2235
+ const text = message.trim().replace(/\s+—\s+/gu, ". ");
2236
+ if (!text) return "Something went wrong.";
2237
+ return `${text[0].toUpperCase()}${text.slice(1)}${/[.!?]$/u.test(text) ? "" : "."}`;
2238
+ }
2239
+ function screensFor(product, bin, render, options) {
2240
+ const show = (lines, stream = "stdout") => {
2241
+ const sink = stream === "stderr" ? options.printErr : options.print;
2242
+ if (sink) for (const line of lines) sink(line);
2243
+ else printScreen(lines, stream);
2244
+ };
2245
+ return {
2246
+ product,
2247
+ bin,
2248
+ render,
2249
+ show,
2250
+ notice: (state, message, next) => show(renderNotice(product, { state, message, ...next ? { next } : {} }, render)),
2251
+ refuse: (message, next, code = 1) => {
2252
+ show(renderNotice(product, { state: "fail", message, ...next ? { next } : {} }, render), "stderr");
2253
+ return code;
2254
+ }
2255
+ };
2256
+ }
1855
2257
  async function run(rawOptions = {}) {
1856
2258
  const print = rawOptions.print ?? defaultPrint;
1857
2259
  const printErr = rawOptions.printErr ?? defaultPrintErr;
1858
- const argv2 = rawOptions.argv ?? process.argv.slice(2);
1859
- const runAt = argv2.indexOf("--run");
1860
- if (runAt >= 0) return runFile(argv2[runAt + 1], argv2.slice(runAt + 2), printErr);
2260
+ const parsed = parseArgv(rawOptions.argv ?? process.argv.slice(2));
2261
+ if (parsed.run) return runFile(parsed.run.file, parsed.run.args, printErr);
2262
+ const render = {
2263
+ ...rawOptions.tty === void 0 ? {} : { tty: rawOptions.tty },
2264
+ ...parsed.flags.noColor ? { color: false } : {}
2265
+ };
1861
2266
  let config;
1862
2267
  try {
1863
- config = loadProductConfig({ configPath: rawOptions.configPath ?? flagValue(argv2, "--config") });
2268
+ config = loadProductConfig({ configPath: rawOptions.configPath ?? parsed.flags.config });
1864
2269
  } catch (error) {
1865
- print(`cannot start: ${error.message}`);
1866
- return 2;
1867
- }
1868
- const dir = resolveProductDir(config.product, rawOptions.dir ?? flagValue(argv2, "--dir"));
1869
- const positional = argv2.filter((arg) => !arg.startsWith("-"));
1870
- if (argv2.includes("--version") || argv2.includes("-v")) {
1871
- print(`${config.binName} launcher ${LAUNCHER_VERSION}`);
2270
+ return screensFor(NO_PRODUCT, "", { ...render, color: false }, rawOptions).refuse(`The launcher cannot start: ${sentence(error.message)}`, void 0, 2);
2271
+ }
2272
+ const dir = resolveProductDir(config.product, rawOptions.dir ?? parsed.flags.dir);
2273
+ const ui = screensFor(faceProduct(config), config.binName, render, rawOptions);
2274
+ if ((parsed.command === "install" || parsed.command === "update") && !parsed.flags.version) recordSelf(dir);
2275
+ if (parsed.unknownFlag) return ui.refuse(`Unknown option: ${parsed.unknownFlag}.`, `${ui.bin} --help`, 2);
2276
+ if (parsed.flags.version) {
2277
+ print(renderVersion(ui.bin, readState(dir)?.version ?? "not installed", LAUNCHER_VERSION));
1872
2278
  return 0;
1873
2279
  }
1874
- if (argv2.includes("--help") || argv2.includes("-h") || positional.length === 0) {
1875
- printUsage(config, print);
1876
- return positional.length === 0 ? 2 : 0;
2280
+ const command2 = parsed.command;
2281
+ if (parsed.flags.help || command2 === void 0) {
2282
+ ui.show(usage(config, dir, render));
2283
+ return parsed.flags.help ? 0 : 2;
1877
2284
  }
1878
- const command2 = positional[0];
2285
+ const mutating = ["install", "login", "logout"].includes(command2) || command2 === "update" && !parsed.flags.dryRun || command2 === "autoupdate" && ["on", "off"].includes(parsed.args[0] ?? "");
1879
2286
  let unlock;
1880
2287
  try {
1881
- unlock = lockInstallation(dir);
1882
- await recoverAcquisition(dir, async (command3, cwd, env) => {
1883
- const result = (rawOptions.runEntry ?? defaultRunEntry)(command3, cwd, env);
1884
- return result.ok && (result.code === void 0 || result.code === 0);
1885
- }, payloadEnv(dir) ?? process.env);
2288
+ if (mutating) {
2289
+ unlock = lockInstallation(dir);
2290
+ await recoverAcquisition(dir, async (command3, cwd, env) => {
2291
+ const result = (rawOptions.runEntry ?? defaultRunEntry)(command3, cwd, env);
2292
+ return result.ok && (result.code === void 0 || result.code === 0);
2293
+ }, payloadEnv(dir) ?? process.env);
2294
+ }
1886
2295
  switch (command2) {
1887
2296
  case "login":
1888
- unlock();
1889
- await doLogin(config, dir, rawOptions, print);
2297
+ unlock?.();
2298
+ await doLogin(config, dir, rawOptions, (line) => ui.notice("note", line));
2299
+ ui.notice("ok", `Signed in to ${identityFor(ui.product).name}.`, `${ui.bin} install`);
1890
2300
  return 0;
1891
2301
  case "logout":
1892
2302
  wipeProductDir(dir);
1893
- print(`signed out of ${config.product} and removed the downloaded payload.`);
2303
+ ui.notice("ok", `Signed out of ${identityFor(ui.product).name} and removed the downloaded payload.`, `${ui.bin} login`);
1894
2304
  return 0;
1895
2305
  case "install":
1896
- return await doInstall(config, dir, rawOptions, print);
2306
+ return await doInstall(config, dir, rawOptions, print, parsed.flags);
1897
2307
  case "update":
1898
- return await doUpdate(config, dir, rawOptions, print);
2308
+ return await doUpdate(config, dir, rawOptions, print, parsed.flags);
2309
+ case "status":
2310
+ return await doStatus(config, dir, rawOptions, ui, print, Boolean(parsed.flags.json));
1899
2311
  case "doctor":
1900
- return doDoctor(config, dir, rawOptions, print, unlock);
2312
+ return doDoctor(config, dir, rawOptions, ui);
1901
2313
  case "autoupdate":
1902
- return doAutoupdate(config, rawOptions, positional.slice(1), print);
2314
+ return doAutoupdate(config, dir, rawOptions, parsed.args, ui);
1903
2315
  default:
1904
- return doForward(config, dir, rawOptions, command2, argv2, print, unlock);
2316
+ return doForward(config, dir, rawOptions, command2, parsed.rest, ui, () => unlock?.());
1905
2317
  }
1906
2318
  } catch (error) {
1907
- if (error instanceof NeedsLoginError) {
1908
- print("you are signed out \u2014 run login first.");
1909
- return 3;
2319
+ if (error instanceof NeedsLoginError) return ui.refuse("You are signed out.", `${ui.bin} login`, 3);
2320
+ const message = error.message;
2321
+ if (/another installer is running|lock recovery is already pending/u.test(message)) {
2322
+ return ui.refuse("Another install or update is running.", `${ui.bin} status`);
1910
2323
  }
1911
- print(`failed: ${error.message}`);
1912
- return 1;
2324
+ return ui.refuse(sentence(message), `${ui.bin} ${command2}`);
1913
2325
  } finally {
1914
2326
  unlock?.();
1915
2327
  }
1916
2328
  }
1917
- function flagValue(argv2, flag) {
1918
- const index = argv2.indexOf(flag);
1919
- if (index < 0) return void 0;
1920
- const value = argv2[index + 1];
1921
- return value && !value.startsWith("-") ? value : void 0;
2329
+ function recordSelf(dir) {
2330
+ if (!(0, import_node_sea2.isSea)()) return;
2331
+ try {
2332
+ (0, import_node_fs12.mkdirSync)(dir, { recursive: true });
2333
+ (0, import_node_fs12.writeFileSync)((0, import_node_path7.join)(dir, LAUNCHER_RECORD_FILE), `${JSON.stringify({ path: process.execPath, version: LAUNCHER_VERSION })}
2334
+ `);
2335
+ } catch {
2336
+ }
1922
2337
  }
1923
- function printUsage(config, print) {
1924
- print(`${config.binName} launcher ${LAUNCHER_VERSION} \u2014 sign in and install ${config.product}.`);
1925
- print("usage: launcher <login|logout|install|update|doctor|autoupdate on|off|status> [--config <path>] [--dir <path>]");
1926
- print(" launcher --run <file> [args\u2026] (run a payload file with the embedded runtime)");
1927
- print(" launcher <verb> [args\u2026] (forwarded to the payload when it declares run + verbs)");
2338
+ function launcherTarget(config) {
2339
+ return { product: config.product, bin: config.binName, key: config.publicKey, host: config.host, ...config.dl ? { dl: config.dl } : {} };
2340
+ }
2341
+ function usage(config, dir, render) {
2342
+ const name = config.product;
2343
+ return renderUsage(faceProduct(config), {
2344
+ bin: config.binName,
2345
+ version: `${readState(dir)?.version ?? "not installed"} (launcher ${LAUNCHER_VERSION})`,
2346
+ summary: `Sign in, install and update ${name}.`,
2347
+ commands: [
2348
+ { usage: "install", text: `Sign in and install ${name}.` },
2349
+ { usage: "update [--dry-run]", text: `Update ${name} and this launcher.` },
2350
+ { usage: "status", text: `Show versions, sign-in and ${AUTO_UPDATE}.` },
2351
+ { usage: "doctor", text: `Check this machine, then run the ${name} doctor.` },
2352
+ { usage: "login", text: "Sign in." },
2353
+ { usage: "logout", text: "Sign out and remove the downloaded payload." },
2354
+ { usage: "autoupdate on|off|status", text: `Turn hourly ${AUTO_UPDATE} on or off, or show it.` },
2355
+ { usage: "<verb> ...", text: `Passed to ${name} when it declares it.` }
2356
+ ],
2357
+ options: [
2358
+ { usage: "--json", text: "Print status as JSON." },
2359
+ { usage: "--no-color", text: "Print without colour." },
2360
+ { usage: "--config <path>", text: "Use this product config." },
2361
+ { usage: "--dir <path>", text: "Use this folder for sign-in and downloads." }
2362
+ ]
2363
+ }, render);
1928
2364
  }
1929
2365
  async function runFile(file, args, printErr) {
1930
2366
  if (!file) {
1931
2367
  printErr("launcher --run needs a file to run.");
1932
2368
  return 1;
1933
2369
  }
1934
- const abs = (0, import_node_path6.resolve)(process.cwd(), file);
1935
- if (!(0, import_node_fs11.existsSync)(abs)) {
2370
+ const abs = (0, import_node_path7.resolve)(process.cwd(), file);
2371
+ if (!(0, import_node_fs12.existsSync)(abs)) {
1936
2372
  printErr(`cannot run ${file}: no such file.`);
1937
2373
  return 1;
1938
2374
  }
@@ -1988,15 +2424,14 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
1988
2424
  });
1989
2425
  return refreshed.accessToken;
1990
2426
  }
1991
- async function doLogin(config, dir, options, print, installer) {
2427
+ async function doLogin(config, dir, options, prompt, installer) {
1992
2428
  const fetchImpl = options.fetchImpl ?? fetch;
1993
2429
  const open = options.open ?? openBrowser;
1994
2430
  if (config.loginKind === "github") {
1995
2431
  const tokens = await loginGithub(config, {
1996
2432
  fetchImpl,
1997
2433
  open,
1998
- print,
1999
- ...installer ? { onDeviceCode: (prompt) => installer.signIn(prompt) } : {}
2434
+ onDeviceCode: (grant) => installer ? installer.signIn(grant) : prompt(`To sign in, open ${grant.url} and enter the code ${grant.code}.`)
2000
2435
  });
2001
2436
  writeTokens(dir, {
2002
2437
  accessToken: tokens.accessToken,
@@ -2004,7 +2439,12 @@ async function doLogin(config, dir, options, print, installer) {
2004
2439
  expiresAt: Date.now() + tokens.expiresIn * 1e3
2005
2440
  });
2006
2441
  } else {
2007
- const tokens = await loginGoogle({ host: config.host, open, fetchImpl });
2442
+ const tokens = await loginGoogle({
2443
+ host: config.host,
2444
+ open,
2445
+ fetchImpl,
2446
+ print: (line) => installer ? installer.relay(line) : prompt(line)
2447
+ });
2008
2448
  writeTokens(dir, {
2009
2449
  accessToken: tokens.accessToken,
2010
2450
  refreshToken: tokens.refreshToken,
@@ -2012,20 +2452,19 @@ async function doLogin(config, dir, options, print, installer) {
2012
2452
  clientId: tokens.clientId
2013
2453
  });
2014
2454
  }
2015
- if (!installer) print(`signed in to ${config.product}.`);
2016
2455
  }
2017
- async function fetchAccessTokenOrLogin(config, dir, options, print, installer) {
2456
+ async function fetchAccessTokenOrLogin(config, dir, options, installer) {
2018
2457
  const fetchImpl = options.fetchImpl ?? fetch;
2019
2458
  const fresh = await ensureFreshToken(config, dir, fetchImpl);
2020
2459
  if (fresh) return fresh;
2021
- await doLogin(config, dir, options, print, installer);
2460
+ await doLogin(config, dir, options, (line) => installer.relay(line), installer);
2022
2461
  const after = readTokens(dir);
2023
2462
  if (!after) throw new Error("sign-in did not produce a token");
2024
2463
  return after.accessToken;
2025
2464
  }
2026
2465
  function readPayloadArgv(dir, key) {
2027
2466
  try {
2028
- const parsed = JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path6.join)(payloadDir(dir), "payload.json"), "utf8"));
2467
+ const parsed = JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path7.join)(payloadDir(dir), "payload.json"), "utf8"));
2029
2468
  const entry2 = parsed[key];
2030
2469
  if (typeof entry2 === "string") {
2031
2470
  const parts = entry2.trim().split(/\s+/).filter(Boolean);
@@ -2047,7 +2486,7 @@ function readPayloadRun(dir) {
2047
2486
  }
2048
2487
  function readPayloadVerbs(dir) {
2049
2488
  try {
2050
- const parsed = JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path6.join)(payloadDir(dir), "payload.json"), "utf8"));
2489
+ const parsed = JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path7.join)(payloadDir(dir), "payload.json"), "utf8"));
2051
2490
  const verbs = parsed.verbs;
2052
2491
  if (verbs === "*") return "*";
2053
2492
  if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
@@ -2064,7 +2503,7 @@ function resolveEntry(entry2) {
2064
2503
  function needsShell(command2) {
2065
2504
  if (process.platform !== "win32") return false;
2066
2505
  if (/\.(cmd|bat)$/i.test(command2)) return true;
2067
- return !(0, import_node_fs11.existsSync)(command2);
2506
+ return !(0, import_node_fs12.existsSync)(command2);
2068
2507
  }
2069
2508
  function quoteForShell(arg) {
2070
2509
  return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
@@ -2096,7 +2535,7 @@ function defaultRunEntry(entry2, cwd, env) {
2096
2535
  const [command2, ...args] = entry2;
2097
2536
  const shell = needsShell(command2);
2098
2537
  const commandLine = shell ? [command2, ...args].map(quoteForShell).join(" ") : command2;
2099
- const spawnEntry = (progress2) => (0, import_node_child_process5.spawnSync)(commandLine, shell ? [] : args, {
2538
+ const spawnEntry = (progress2) => (0, import_node_child_process6.spawnSync)(commandLine, shell ? [] : args, {
2100
2539
  cwd,
2101
2540
  stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
2102
2541
  shell,
@@ -2130,8 +2569,16 @@ function faceProduct(config) {
2130
2569
  function formatMb(bytes) {
2131
2570
  return (bytes / 1048576).toFixed(1).replace(/\.0$/, "");
2132
2571
  }
2133
- async function installOrUpdate(config, dir, options, print, update) {
2572
+ async function installOrUpdate(config, dir, options, print, update, flags2) {
2573
+ let launcherFailed = false;
2574
+ const code = await installOrUpdateRun(config, dir, options, print, update, flags2, () => {
2575
+ launcherFailed = true;
2576
+ });
2577
+ return launcherFailed ? Math.max(code, 1) : code;
2578
+ }
2579
+ async function installOrUpdateRun(config, dir, options, print, update, flags2, markLauncherFailed) {
2134
2580
  const product = faceProduct(config);
2581
+ const dryRun = update && Boolean(flags2.dryRun);
2135
2582
  const installer = createInstallerRun({
2136
2583
  product,
2137
2584
  gate: config.host,
@@ -2141,17 +2588,55 @@ async function installOrUpdate(config, dir, options, print, update) {
2141
2588
  operation: update ? "update" : "install",
2142
2589
  tty: options.tty,
2143
2590
  animate: !options.print && Boolean(process.stderr.isTTY),
2591
+ dryRun,
2592
+ ...flags2.noColor ? { color: false } : {},
2144
2593
  ...options.print ? { write: (text) => print(text.replace(/\n$/, "")) } : {}
2145
2594
  });
2146
2595
  const fetchImpl = options.fetchImpl ?? fetch;
2147
2596
  let version = readState(dir)?.version ?? "unknown";
2148
2597
  installer.start();
2149
2598
  try {
2599
+ const refresh = options.refresh ?? ((0, import_node_sea2.isSea)() ? refreshLauncher : void 0);
2600
+ const arm = refresh ? await refresh(launcherTarget(config), {
2601
+ lockHeld: true,
2602
+ path: process.execPath,
2603
+ dryRun,
2604
+ productDir: dir,
2605
+ fetch: fetchImpl,
2606
+ readVersion: async () => LAUNCHER_VERSION
2607
+ }) : null;
2608
+ if (arm) {
2609
+ installer.surface({
2610
+ id: LAUNCHER_SURFACE,
2611
+ from: arm.from,
2612
+ to: arm.to,
2613
+ state: armState(arm),
2614
+ ...arm.durationMs === void 0 ? {} : { seconds: arm.durationMs / 1e3 },
2615
+ ...arm.verdict !== "ok" && arm.detail ? { detail: arm.detail } : {}
2616
+ });
2617
+ if (arm.verdict === "fail") {
2618
+ const finish = installer.finish;
2619
+ installer.finish = (facts) => finish({
2620
+ ...facts,
2621
+ operationFailed: true,
2622
+ detail: facts.detail ?? "The launcher could not update itself.",
2623
+ retry: facts.retry ?? `${config.binName} update`
2624
+ });
2625
+ markLauncherFailed();
2626
+ }
2627
+ }
2150
2628
  const current = readState(dir);
2151
- const payloadPresent = (0, import_node_fs11.existsSync)(payloadDir(dir));
2629
+ const payloadPresent = (0, import_node_fs12.existsSync)(payloadDir(dir));
2152
2630
  let started = Date.now();
2153
2631
  installer.phase("resolve", { state: "running" });
2154
2632
  const pointer = await fetchReleaseVersion(config, fetchImpl);
2633
+ if (dryRun) {
2634
+ installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
2635
+ const installed = payloadPresent ? current?.version : void 0;
2636
+ if (pointer) installer.surface({ id: config.product, from: installed, to: pointer, state: installed === pointer ? "current" : "updated" });
2637
+ installer.finish({ ...pointer ? { version: pointer } : {}, total: 1, updated: pointer && installed !== pointer ? 1 : 0, failed: 0, dryRun: true });
2638
+ return 0;
2639
+ }
2155
2640
  if (pointer && current?.version === pointer && payloadPresent) {
2156
2641
  version = pointer;
2157
2642
  installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
@@ -2168,14 +2653,14 @@ async function installOrUpdate(config, dir, options, print, update) {
2168
2653
  }
2169
2654
  installer.phase("sign-in", { state: "running" });
2170
2655
  started = Date.now();
2171
- const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print, installer);
2656
+ const accessToken = await fetchAccessTokenOrLogin(config, dir, options, installer);
2172
2657
  installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
2173
2658
  installer.phase("resolve", { state: "running" });
2174
2659
  started = Date.now();
2175
2660
  const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
2176
2661
  version = manifest.version;
2177
2662
  installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
2178
- const unchanged = current?.version === version && (0, import_node_fs11.existsSync)(payloadDir(dir));
2663
+ const unchanged = current?.version === version && (0, import_node_fs12.existsSync)(payloadDir(dir));
2179
2664
  if (unchanged && current.acquired) {
2180
2665
  installer.surface({ id: config.product, from: version, to: version, state: "current" });
2181
2666
  return await finishLastMile(config, dir, options, installer, version, true, acquisitionRepairCommand(dir, version));
@@ -2185,14 +2670,14 @@ async function installOrUpdate(config, dir, options, print, update) {
2185
2670
  started = Date.now();
2186
2671
  const cached = reusableCandidate(dir, manifest);
2187
2672
  const candidate = cached ?? (0, import_node_crypto6.randomUUID)();
2188
- const candidateDir = (0, import_node_path6.join)(dir, "candidates", candidate);
2673
+ const candidateDir = (0, import_node_path7.join)(dir, "candidates", candidate);
2189
2674
  const onProgress = ({ done, total }) => installer.phase("download", {
2190
2675
  state: "running",
2191
2676
  measure: total ? `${Math.floor(done / total * 100)}% \xB7 ${formatMb(done)} of ${formatMb(total)} MB` : `${formatMb(done)} MB`,
2192
2677
  ...total ? { runningTitle: `Downloading ${config.product} ${version} (${formatMb(total)} MB)` } : {}
2193
2678
  });
2194
2679
  if (!cached) await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl, onProgress, cacheDir: dir });
2195
- const acquisition = (0, import_node_fs11.existsSync)((0, import_node_path6.join)(candidateDir, "payload", "payload.json")) ? readAcquisition((0, import_node_path6.join)(candidateDir, "payload")) : null;
2680
+ const acquisition = (0, import_node_fs12.existsSync)((0, import_node_path7.join)(candidateDir, "payload", "payload.json")) ? readAcquisition((0, import_node_path7.join)(candidateDir, "payload")) : null;
2196
2681
  if (acquisition) {
2197
2682
  rememberCandidate(dir, candidate, manifest);
2198
2683
  installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
@@ -2215,10 +2700,10 @@ async function installOrUpdate(config, dir, options, print, update) {
2215
2700
  installer.finish(outcome ?? { version, total: 1, updated: 1, failed: 0, installed: true });
2216
2701
  return 0;
2217
2702
  }
2218
- const target = (0, import_node_path6.join)(dir, "payload");
2219
- (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
2220
- (0, import_node_fs11.rmSync)(target, { recursive: true, force: true });
2221
- (0, import_node_fs11.renameSync)((0, import_node_path6.join)(candidateDir, "payload"), target);
2703
+ const target = (0, import_node_path7.join)(dir, "payload");
2704
+ (0, import_node_fs12.mkdirSync)(dir, { recursive: true });
2705
+ (0, import_node_fs12.rmSync)(target, { recursive: true, force: true });
2706
+ (0, import_node_fs12.renameSync)((0, import_node_path7.join)(candidateDir, "payload"), target);
2222
2707
  writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
2223
2708
  installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
2224
2709
  }
@@ -2244,11 +2729,11 @@ async function installOrUpdate(config, dir, options, print, update) {
2244
2729
  installer.stop();
2245
2730
  }
2246
2731
  }
2247
- function doInstall(config, dir, options, print) {
2248
- return installOrUpdate(config, dir, options, print, false);
2732
+ function doInstall(config, dir, options, print, flags2) {
2733
+ return installOrUpdate(config, dir, options, print, false, flags2);
2249
2734
  }
2250
- function doUpdate(config, dir, options, print) {
2251
- return installOrUpdate(config, dir, options, print, true);
2735
+ function doUpdate(config, dir, options, print, flags2) {
2736
+ return installOrUpdate(config, dir, options, print, true, flags2);
2252
2737
  }
2253
2738
  function payloadEnv(dir) {
2254
2739
  const stored = readTokens(dir);
@@ -2260,12 +2745,12 @@ function payloadFileAsCommand(entry2, payload) {
2260
2745
  const first = entry2[0];
2261
2746
  if (!first || first === "$self") return null;
2262
2747
  if (first.includes("/") || first.includes("\\")) return null;
2263
- const candidate = (0, import_node_path6.join)(payload, first);
2264
- if (!(0, import_node_fs11.existsSync)(candidate)) return null;
2748
+ const candidate = (0, import_node_path7.join)(payload, first);
2749
+ if (!(0, import_node_fs12.existsSync)(candidate)) return null;
2265
2750
  if (NEVER_A_PROGRAM.test(first)) return first;
2266
2751
  if (process.platform === "win32") return null;
2267
2752
  try {
2268
- return ((0, import_node_fs11.statSync)(candidate).mode & 73) === 0 ? first : null;
2753
+ return ((0, import_node_fs12.statSync)(candidate).mode & 73) === 0 ? first : null;
2269
2754
  } catch {
2270
2755
  return null;
2271
2756
  }
@@ -2280,7 +2765,7 @@ async function finishLastMile(config, dir, options, installer, version, unchange
2280
2765
  updated: unchanged ? 0 : 1,
2281
2766
  failed: 0,
2282
2767
  installed: true,
2283
- detail: `next step: run ${(0, import_node_path6.join)(payload, config.binName)} to start ${config.product}.`
2768
+ detail: `next step: run ${(0, import_node_path7.join)(payload, config.binName)} to start ${config.product}.`
2284
2769
  });
2285
2770
  return 0;
2286
2771
  }
@@ -2326,9 +2811,9 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2326
2811
  const [command2, ...args] = entry2;
2327
2812
  const shell = needsShell(command2);
2328
2813
  const progress = !(process.platform === "win32" && shell);
2329
- const outcomeDir = (0, import_node_fs11.mkdtempSync)((0, import_node_path6.join)((0, import_node_os4.tmpdir)(), "mm-installer-outcome-"));
2330
- const outcomeFile = (0, import_node_path6.join)(outcomeDir, "outcome.json");
2331
- const progressFile = (0, import_node_path6.join)(outcomeDir, "progress.jsonl");
2814
+ const outcomeDir = (0, import_node_fs12.mkdtempSync)((0, import_node_path7.join)((0, import_node_os5.tmpdir)(), "mm-installer-outcome-"));
2815
+ const outcomeFile = (0, import_node_path7.join)(outcomeDir, "outcome.json");
2816
+ const progressFile = (0, import_node_path7.join)(outcomeDir, "progress.jsonl");
2332
2817
  const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
2333
2818
  delete childEnv.MM_FACE_TRANSCRIPT;
2334
2819
  delete childEnv.MM_PROGRESS_FILE;
@@ -2341,7 +2826,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2341
2826
  const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
2342
2827
  try {
2343
2828
  const result = await new Promise((resolve3) => {
2344
- const child = (0, import_node_child_process5.spawn)(shell ? [command2, ...args].map(quoteForShell).join(" ") : command2, shell ? [] : args, {
2829
+ const child = (0, import_node_child_process6.spawn)(shell ? [command2, ...args].map(quoteForShell).join(" ") : command2, shell ? [] : args, {
2345
2830
  cwd,
2346
2831
  shell,
2347
2832
  windowsHide: true,
@@ -2378,8 +2863,8 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2378
2863
  const pollProgress = () => {
2379
2864
  if (progressError) return;
2380
2865
  try {
2381
- if (!(0, import_node_fs11.existsSync)(progressFile)) return;
2382
- const bytes = (0, import_node_fs11.readFileSync)(progressFile);
2866
+ if (!(0, import_node_fs12.existsSync)(progressFile)) return;
2867
+ const bytes = (0, import_node_fs12.readFileSync)(progressFile);
2383
2868
  receive(progressDecoder.write(bytes.subarray(progressOffset)));
2384
2869
  progressOffset = bytes.length;
2385
2870
  } catch {
@@ -2412,109 +2897,146 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2412
2897
  return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
2413
2898
  }
2414
2899
  } finally {
2415
- if ((0, import_node_fs11.existsSync)(progressFile)) (0, import_node_fs11.unlinkSync)(progressFile);
2416
- if ((0, import_node_fs11.existsSync)(outcomeFile)) (0, import_node_fs11.unlinkSync)(outcomeFile);
2417
- (0, import_node_fs11.rmdirSync)(outcomeDir);
2900
+ if ((0, import_node_fs12.existsSync)(progressFile)) (0, import_node_fs12.unlinkSync)(progressFile);
2901
+ if ((0, import_node_fs12.existsSync)(outcomeFile)) (0, import_node_fs12.unlinkSync)(outcomeFile);
2902
+ (0, import_node_fs12.rmdirSync)(outcomeDir);
2418
2903
  }
2419
2904
  }
2420
- function doForward(config, dir, options, command2, argv2, print, unlock) {
2421
- if (command2 && (0, import_node_fs11.existsSync)(command2)) {
2422
- print(`${command2} is a file, not a command \u2014 did you mean \`--run ${command2}\`?`);
2423
- return 2;
2905
+ function armState(arm) {
2906
+ if (arm.verdict === "fail") return "failed";
2907
+ if (arm.verdict === "defer") return "retry";
2908
+ if (arm.verdict === "skip") return "kept";
2909
+ return arm.from === arm.to ? "current" : "updated";
2910
+ }
2911
+ function doForward(config, dir, options, command2, rest, ui, unlock) {
2912
+ if ((0, import_node_fs12.existsSync)(command2)) {
2913
+ return ui.refuse(`${command2} is a file, not a command.`, `${ui.bin} --run ${command2}`, 2);
2424
2914
  }
2425
2915
  const acquired = readState(dir)?.acquired;
2426
2916
  const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
2427
2917
  const verbs = readPayloadVerbs(dir);
2428
- const declared = verbs === "*" || Array.isArray(verbs) && command2 !== void 0 && verbs.includes(command2);
2429
- if (!target || !declared) {
2430
- print(`unknown command: ${command2}`);
2431
- printUsage(config, print);
2432
- return 2;
2433
- }
2434
- const forwarded = [...resolveEntry(target), ...argv2];
2918
+ const declared = verbs === "*" || Array.isArray(verbs) && verbs.includes(command2);
2919
+ if (!target || !declared) return ui.refuse(`Unknown command: ${command2}.`, `${ui.bin} --help`, 2);
2920
+ const forwarded = [...resolveEntry(target), command2, ...rest];
2435
2921
  const cwd = payloadDir(dir);
2436
2922
  const inherited = payloadEnv(dir);
2437
2923
  const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
2438
2924
  unlock();
2439
2925
  const result = (options.runEntry ?? defaultRunEntry)(forwarded, cwd, env);
2440
2926
  if (!result.ok && result.code === void 0) {
2441
- print(`failed: ${result.error ?? `could not run ${forwarded[0]}`}`);
2442
- return 1;
2927
+ return ui.refuse(`Could not start ${config.product}: ${sentence(result.error ?? `could not run ${forwarded[0]}`)}`, `${ui.bin} doctor`);
2443
2928
  }
2444
2929
  return result.code ?? 0;
2445
2930
  }
2446
- function doAutoupdate(config, options, args, print) {
2931
+ function doAutoupdate(config, dir, options, args, ui) {
2447
2932
  const mode = args[0] ?? "status";
2448
- const scheduleOptions = options.autoupdate ?? {};
2933
+ const scheduleOptions = { stateDir: dir, ...options.autoupdate };
2449
2934
  const command2 = options.autoupdate?.command ?? [process.execPath, "update"];
2450
- if (mode === "on") {
2451
- try {
2452
- enableSchedule(config, command2, scheduleOptions);
2453
- } catch (error) {
2454
- print(error.message);
2455
- return 1;
2456
- }
2457
- const state2 = querySchedule(config, scheduleOptions);
2458
- print(`auto-update: on (${state2.cadence ?? "hourly"})`);
2459
- return 0;
2460
- }
2461
- if (mode === "off") {
2935
+ const name = identityFor(ui.product).name;
2936
+ if (mode === "on" || mode === "off") {
2462
2937
  try {
2463
- disableSchedule(config, scheduleOptions);
2938
+ if (mode === "on") enableSchedule(config, command2, scheduleOptions);
2939
+ else disableSchedule(config, scheduleOptions);
2464
2940
  } catch (error) {
2465
- print(error.message);
2466
- return 1;
2941
+ return ui.refuse(sentence(error.message), `${ui.bin} doctor`);
2467
2942
  }
2468
- print("auto-update: off");
2943
+ if (mode === "on") ui.notice("ok", `Auto-update is on. ${name} checks for updates every hour.`, `${ui.bin} autoupdate status`);
2944
+ else ui.notice("ok", "Auto-update is off.", `${ui.bin} autoupdate on`);
2469
2945
  return 0;
2470
2946
  }
2471
- if (mode !== "status") {
2472
- print(`unknown autoupdate mode: ${mode} \u2014 use on, off or status.`);
2473
- return 1;
2474
- }
2947
+ if (mode !== "status") return ui.refuse(`Unknown autoupdate mode: ${mode}.`, `${ui.bin} autoupdate status`, 2);
2475
2948
  const state = querySchedule(config, scheduleOptions);
2476
- if (!state.supported) {
2477
- print("auto-update: unsupported on this platform");
2478
- return 1;
2479
- }
2480
- print(`auto-update: ${state.enabled ? `on (${state.cadence ?? "hourly"})` : "off"}`);
2481
- if (state.lastRun) print(`last run: ${state.lastRun}`);
2949
+ if (!state.supported) return ui.refuse("Auto-update is not supported on this platform.", `${ui.bin} update`);
2950
+ if (state.enabled) ui.notice("ok", `Auto-update is on and runs every hour.${state.lastRun ? ` Last run: ${state.lastRun}.` : ""}`);
2951
+ else ui.notice("note", "Auto-update is off.", `${ui.bin} autoupdate on`);
2482
2952
  return 0;
2483
2953
  }
2484
- function doDoctor(config, dir, options, print, unlock) {
2485
- doLauncherDoctor(config, dir, options, print);
2954
+ function localFacts(config, dir, options) {
2955
+ const state = readState(dir);
2956
+ const tokens = readTokens(dir);
2957
+ const expired = Boolean(tokens && tokens.expiresAt <= Date.now());
2958
+ return {
2959
+ installed: state && (0, import_node_fs12.existsSync)(payloadDir(dir)) ? state.version : null,
2960
+ // An expired access token with a refresh token renews on the next use; it is still signed in.
2961
+ signedIn: Boolean(tokens && (!expired || tokens.refreshToken)),
2962
+ expired,
2963
+ schedule: querySchedule(config, { stateDir: dir, ...options.autoupdate })
2964
+ };
2965
+ }
2966
+ function signInRow(local) {
2967
+ if (local.signedIn) return { surface: "sign-in", state: local.expired ? "signed in \xB7 renews on next use" : "signed in", mark: "ok" };
2968
+ return { surface: "sign-in", state: local.expired ? "expired" : "signed out", mark: "fail" };
2969
+ }
2970
+ function autoUpdateRow({ supported, enabled, cadence, lastRun }) {
2971
+ if (!supported) return { surface: AUTO_UPDATE, state: "unsupported on this platform", mark: "note" };
2972
+ if (!enabled) return { surface: AUTO_UPDATE, state: "off", mark: "note" };
2973
+ return { surface: AUTO_UPDATE, state: `on \xB7 ${cadence ?? "hourly"}`, mark: "ok", ...lastRun ? { note: `Last run: ${lastRun}` } : {} };
2974
+ }
2975
+ function verdictFor(ui, local, updateAvailable, ready) {
2976
+ if (!local.installed) return { state: "fail", headline: "is not installed.", next: `${ui.bin} install` };
2977
+ if (!local.signedIn) return { state: "fail", headline: "is signed out.", next: `${ui.bin} login` };
2978
+ if (updateAvailable) return { state: "note", headline: "has an update available.", next: `${ui.bin} update` };
2979
+ return { state: "ok", headline: "is ready.", next: ready };
2980
+ }
2981
+ function within(fetchImpl, ms) {
2982
+ return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(ms) }));
2983
+ }
2984
+ var STATUS_PROBE_MS = 3e3;
2985
+ async function doStatus(config, dir, options, ui, print, json) {
2986
+ const fetchImpl = within(options.fetchImpl ?? fetch, STATUS_PROBE_MS);
2987
+ const local = localFacts(config, dir, options);
2988
+ const [release, record] = await Promise.all([
2989
+ fetchReleaseVersion(config, fetchImpl),
2990
+ readLauncherRecord(launcherTarget(config), { fetch: fetchImpl, timeoutMs: STATUS_PROBE_MS })
2991
+ ]);
2992
+ const launcherNewer = record !== null && compareVersions(record.version, LAUNCHER_VERSION) > 0;
2993
+ const releaseOrder = local.installed && release ? compareVersions(release, local.installed) : null;
2994
+ const verdict = verdictFor(ui, local, launcherNewer || (releaseOrder ?? 0) > 0, `${ui.bin} doctor`);
2995
+ if (json) {
2996
+ print(JSON.stringify({
2997
+ product: config.product,
2998
+ launcher: { version: LAUNCHER_VERSION, available: launcherNewer ? record.version : null },
2999
+ installed: local.installed,
3000
+ release,
3001
+ signIn: local.signedIn ? local.expired ? "renews" : "signed-in" : local.expired ? "expired" : "signed-out",
3002
+ autoupdate: { supported: local.schedule.supported, enabled: local.schedule.enabled, lastRun: local.schedule.lastRun ?? null },
3003
+ verdict: verdict.state,
3004
+ next: verdict.next
3005
+ }, null, 2));
3006
+ return verdict.state === "fail" ? 1 : 0;
3007
+ }
3008
+ const launcherRow = record === null ? { surface: "launcher", state: `${LAUNCHER_VERSION} \xB7 ${SURFACE_WORDS.unknown}`, mark: "note" } : launcherNewer ? { surface: "launcher", state: `${LAUNCHER_VERSION} \u2192 ${record.version} \xB7 ${SURFACE_WORDS.behind}`, mark: "note" } : { surface: "launcher", state: `${LAUNCHER_VERSION} \xB7 ${SURFACE_WORDS.current}`, mark: "ok" };
3009
+ const productRow = !local.installed ? { surface: config.product, state: SURFACE_WORDS.missing, mark: "fail" } : releaseOrder === null ? { surface: config.product, state: `${local.installed} \xB7 ${SURFACE_WORDS.unknown}`, mark: "note" } : releaseOrder > 0 ? { surface: config.product, state: `${local.installed} \u2192 ${release} \xB7 ${SURFACE_WORDS.behind}`, mark: "note" } : releaseOrder < 0 ? { surface: config.product, state: `${local.installed} \xB7 ${SURFACE_WORDS.ahead}`, mark: "note" } : { surface: config.product, state: `${local.installed} \xB7 ${SURFACE_WORDS.current}`, mark: "ok" };
3010
+ ui.show(renderReport(ui.product, {
3011
+ rows: [launcherRow, productRow, signInRow(local), autoUpdateRow(local.schedule)],
3012
+ verdict: { ...verdict, facts: [release ? `The release is ${release}.` : "The release version could not be read."] }
3013
+ }, ui.render));
3014
+ return verdict.state === "fail" ? 1 : 0;
3015
+ }
3016
+ function doDoctor(config, dir, options, ui) {
3017
+ const local = localFacts(config, dir, options);
3018
+ const verdict = verdictFor(ui, local, false, `${ui.bin} status`);
3019
+ ui.show(renderReport(ui.product, {
3020
+ rows: [
3021
+ { surface: "launcher", state: LAUNCHER_VERSION, mark: "ok" },
3022
+ local.installed ? { surface: config.product, state: local.installed, mark: "ok" } : { surface: config.product, state: SURFACE_WORDS.missing, mark: "fail" },
3023
+ signInRow(local),
3024
+ autoUpdateRow(local.schedule)
3025
+ ],
3026
+ // Paths belong in the receipt, never a row.
3027
+ verdict: { ...verdict, facts: [`Folder: ${dir}`, `Release host: ${config.host}`] }
3028
+ }, ui.render));
3029
+ const own = verdict.state === "fail" ? 1 : 0;
2486
3030
  const acquired = readState(dir)?.acquired;
2487
3031
  const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
2488
3032
  const verbs = readPayloadVerbs(dir);
2489
3033
  const chains = target !== null && (verbs === "*" || Array.isArray(verbs) && verbs.includes("doctor"));
2490
- if (!chains) return 0;
3034
+ if (!chains) return own;
2491
3035
  const cwd = payloadDir(dir);
2492
3036
  const inherited = payloadEnv(dir);
2493
3037
  const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
2494
- unlock();
2495
3038
  const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], cwd, env);
2496
- return result.code ?? (result.ok ? 0 : 1);
2497
- }
2498
- function doLauncherDoctor(config, dir, options, print) {
2499
- const tokens = readTokens(dir);
2500
- const state = readState(dir);
2501
- const payloadPresent = (0, import_node_fs11.existsSync)(payloadDir(dir));
2502
- print(`product: ${config.product}`);
2503
- print(`host: ${config.host}`);
2504
- print(`login: ${config.loginKind}`);
2505
- print(`dir: ${dir}`);
2506
- if (tokens) {
2507
- const remaining = Math.max(0, Math.round((tokens.expiresAt - Date.now()) / 1e3));
2508
- print(`token: present (expires in ${remaining}s)`);
2509
- } else {
2510
- print("token: none \u2014 run login first.");
2511
- }
2512
- print(`payload: ${state ? state.version : "none"}${payloadPresent ? "" : " (not downloaded)"}`);
2513
- print(`paths: tokens ${(0, import_node_path6.join)(dir, "tokens.json")}, state ${(0, import_node_path6.join)(dir, "state.json")}, payload ${payloadDir(dir)}`);
2514
- const schedule = querySchedule(config, options.autoupdate ?? {});
2515
- print(
2516
- `auto-update: ${!schedule.supported ? "unsupported on this platform" : schedule.enabled ? `on (${schedule.cadence ?? "hourly"})` : "off"}`
2517
- );
3039
+ return (result.code ?? (result.ok ? 0 : 1)) || own;
2518
3040
  }
2519
3041
  var invokedAsMain = typeof process.argv[1] === "string" && (() => {
2520
3042
  try {
@@ -2533,8 +3055,7 @@ if (invokedAsMain) {
2533
3055
  process.exitCode = code;
2534
3056
  },
2535
3057
  (error) => {
2536
- process.stderr.write(`failed: ${error.message}
2537
- `);
3058
+ printScreen(renderNotice(NO_PRODUCT, { state: "fail", message: sentence(error.message) }, { color: false }), "stderr");
2538
3059
  process.exitCode = 1;
2539
3060
  }
2540
3061
  );
@@ -2545,6 +3066,7 @@ if (invokedAsMain) {
2545
3066
  defaultRunEntry,
2546
3067
  ensureFreshToken,
2547
3068
  formatMb,
3069
+ parseArgv,
2548
3070
  readPayloadEntry,
2549
3071
  readPayloadRun,
2550
3072
  readPayloadVerbs,