@mutmutco/installer-launcher 1.0.1 → 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;
@@ -1309,11 +1366,11 @@ function createInstallerRun(value, options = {}) {
1309
1366
  },
1310
1367
  surface(facts) {
1311
1368
  if (finished) throw new Error("installer run already finished");
1312
- 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);
1313
1370
  if (!surface) throw new Error("installer run: undeclared surface");
1314
1371
  start();
1315
1372
  const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} ${tty ? "\u2192" : "->"} ${facts.to}` : ` ${facts.to}` : "";
1316
- 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];
1317
1374
  if (!status) throw new Error("installer run: unknown surface state");
1318
1375
  const separator = tty ? "\xB7" : "-";
1319
1376
  const activation = facts.state === "updated" && surface.activation ? ` ${separator} ${surface.activation}` : "";
@@ -1421,8 +1478,9 @@ function createInstallerRun(value, options = {}) {
1421
1478
  if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
1422
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}.`;
1423
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;
1424
1482
  const body = [
1425
- `${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."}`,
1426
1484
  changed,
1427
1485
  ...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
1428
1486
  ...facts.detail ? [facts.detail] : [],
@@ -1445,6 +1503,89 @@ function createInstallerRun(value, options = {}) {
1445
1503
  return run2;
1446
1504
  }
1447
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
+
1448
1589
  // src/autoupdate.ts
1449
1590
  var import_node_child_process3 = require("node:child_process");
1450
1591
  var import_node_fs10 = require("node:fs");
@@ -1475,19 +1616,33 @@ function scheduleName(config) {
1475
1616
  function scheduleLabel(config) {
1476
1617
  return `${config.binName}-autoupdate`.replace(/[^A-Za-z0-9_-]+/g, "-");
1477
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
+ }
1478
1630
  function quoteWindows(arg) {
1479
1631
  return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
1480
1632
  }
1481
1633
  function enableSchedule(config, command2, options = {}) {
1482
1634
  const platform = schedulePlatform(options.platform);
1483
- 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}.`);
1484
1636
  const exec = options.exec ?? defaultExec;
1485
1637
  const home = homeOf(options);
1486
1638
  if (platform === "win32") {
1487
- const taskLine = command2.map(quoteWindows).join(" ");
1488
- 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"]);
1489
1644
  if (result2.code !== 0) {
1490
- 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}`}`);
1491
1646
  }
1492
1647
  return;
1493
1648
  }
@@ -1500,7 +1655,7 @@ function enableSchedule(config, command2, options = {}) {
1500
1655
  exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
1501
1656
  const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
1502
1657
  if (result2.code !== 0) {
1503
- 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}`}`);
1504
1659
  }
1505
1660
  return;
1506
1661
  }
@@ -1511,23 +1666,24 @@ function enableSchedule(config, command2, options = {}) {
1511
1666
  (0, import_node_fs10.writeFileSync)((0, import_node_path5.join)(dir, `${label}.timer`), linuxTimer(label));
1512
1667
  const reload = exec("systemctl", ["--user", "daemon-reload"]);
1513
1668
  if (reload.code !== 0) {
1514
- 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}`}`);
1515
1670
  }
1516
1671
  const result = exec("systemctl", ["--user", "enable", "--now", `${label}.timer`]);
1517
1672
  if (result.code !== 0) {
1518
- 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}`}`);
1519
1674
  }
1520
1675
  }
1521
1676
  function disableSchedule(config, options = {}) {
1522
1677
  const platform = schedulePlatform(options.platform);
1523
- 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}.`);
1524
1679
  const exec = options.exec ?? defaultExec;
1525
1680
  const home = homeOf(options);
1526
1681
  if (platform === "win32") {
1527
1682
  const result = exec("schtasks", ["/Delete", "/TN", scheduleName(config), "/F"]);
1528
1683
  if (result.code !== 0 && !/cannot find|does not exist/i.test(`${result.stdout} ${result.stderr}`)) {
1529
- 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}`}`);
1530
1685
  }
1686
+ (0, import_node_fs10.rmSync)((0, import_node_path5.join)(options.stateDir ?? defaultProductDir(config.product), HIDDEN_SCRIPT), { force: true });
1531
1687
  return;
1532
1688
  }
1533
1689
  if (platform === "darwin") {
@@ -1620,15 +1776,170 @@ function firstLine(text) {
1620
1776
  return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
1621
1777
  }
1622
1778
 
1623
- // src/login-github.ts
1779
+ // src/refresh.ts
1624
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");
1625
1936
  var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
1626
1937
  function openBrowser(url) {
1627
1938
  if (process.env.LAUNCHER_NO_OPEN === "1") return;
1628
1939
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
1629
1940
  const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
1630
1941
  try {
1631
- 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 });
1632
1943
  child.on("error", () => {
1633
1944
  });
1634
1945
  child.unref();
@@ -1855,7 +2166,7 @@ function page(title, body) {
1855
2166
  }
1856
2167
 
1857
2168
  // src/index.ts
1858
- var LAUNCHER_VERSION = true ? "1.0.1" : readVersionFromPackage();
2169
+ var LAUNCHER_VERSION = true ? "1.1.0" : readVersionFromPackage();
1859
2170
  function defaultPrint(message) {
1860
2171
  process.stdout.write(`${message}
1861
2172
  `);
@@ -1864,87 +2175,200 @@ function defaultPrintErr(message) {
1864
2175
  process.stderr.write(`${message}
1865
2176
  `);
1866
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
+ }
1867
2257
  async function run(rawOptions = {}) {
1868
2258
  const print = rawOptions.print ?? defaultPrint;
1869
2259
  const printErr = rawOptions.printErr ?? defaultPrintErr;
1870
- const argv2 = rawOptions.argv ?? process.argv.slice(2);
1871
- const runAt = argv2.indexOf("--run");
1872
- 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
+ };
1873
2266
  let config;
1874
2267
  try {
1875
- config = loadProductConfig({ configPath: rawOptions.configPath ?? flagValue(argv2, "--config") });
2268
+ config = loadProductConfig({ configPath: rawOptions.configPath ?? parsed.flags.config });
1876
2269
  } catch (error) {
1877
- print(`cannot start: ${error.message}`);
1878
- return 2;
1879
- }
1880
- const dir = resolveProductDir(config.product, rawOptions.dir ?? flagValue(argv2, "--dir"));
1881
- const positional = argv2.filter((arg) => !arg.startsWith("-"));
1882
- if (argv2.includes("--version") || argv2.includes("-v")) {
1883
- 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));
1884
2278
  return 0;
1885
2279
  }
1886
- if (argv2.includes("--help") || argv2.includes("-h") || positional.length === 0) {
1887
- printUsage(config, print);
1888
- 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;
1889
2284
  }
1890
- 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] ?? "");
1891
2286
  let unlock;
1892
2287
  try {
1893
- unlock = lockInstallation(dir);
1894
- await recoverAcquisition(dir, async (command3, cwd, env) => {
1895
- const result = (rawOptions.runEntry ?? defaultRunEntry)(command3, cwd, env);
1896
- return result.ok && (result.code === void 0 || result.code === 0);
1897
- }, 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
+ }
1898
2295
  switch (command2) {
1899
2296
  case "login":
1900
- unlock();
1901
- 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`);
1902
2300
  return 0;
1903
2301
  case "logout":
1904
2302
  wipeProductDir(dir);
1905
- 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`);
1906
2304
  return 0;
1907
2305
  case "install":
1908
- return await doInstall(config, dir, rawOptions, print);
2306
+ return await doInstall(config, dir, rawOptions, print, parsed.flags);
1909
2307
  case "update":
1910
- 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));
1911
2311
  case "doctor":
1912
- return doDoctor(config, dir, rawOptions, print, unlock);
2312
+ return doDoctor(config, dir, rawOptions, ui);
1913
2313
  case "autoupdate":
1914
- return doAutoupdate(config, rawOptions, positional.slice(1), print);
2314
+ return doAutoupdate(config, dir, rawOptions, parsed.args, ui);
1915
2315
  default:
1916
- return doForward(config, dir, rawOptions, command2, argv2, print, unlock);
2316
+ return doForward(config, dir, rawOptions, command2, parsed.rest, ui, () => unlock?.());
1917
2317
  }
1918
2318
  } catch (error) {
1919
- if (error instanceof NeedsLoginError) {
1920
- print("you are signed out \u2014 run login first.");
1921
- 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`);
1922
2323
  }
1923
- print(`failed: ${error.message}`);
1924
- return 1;
2324
+ return ui.refuse(sentence(message), `${ui.bin} ${command2}`);
1925
2325
  } finally {
1926
2326
  unlock?.();
1927
2327
  }
1928
2328
  }
1929
- function flagValue(argv2, flag) {
1930
- const index = argv2.indexOf(flag);
1931
- if (index < 0) return void 0;
1932
- const value = argv2[index + 1];
1933
- 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
+ }
1934
2337
  }
1935
- function printUsage(config, print) {
1936
- print(`${config.binName} launcher ${LAUNCHER_VERSION} \u2014 sign in and install ${config.product}.`);
1937
- print("usage: launcher <login|logout|install|update|doctor|autoupdate on|off|status> [--config <path>] [--dir <path>]");
1938
- print(" launcher --run <file> [args\u2026] (run a payload file with the embedded runtime)");
1939
- 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);
1940
2364
  }
1941
2365
  async function runFile(file, args, printErr) {
1942
2366
  if (!file) {
1943
2367
  printErr("launcher --run needs a file to run.");
1944
2368
  return 1;
1945
2369
  }
1946
- const abs = (0, import_node_path6.resolve)(process.cwd(), file);
1947
- 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)) {
1948
2372
  printErr(`cannot run ${file}: no such file.`);
1949
2373
  return 1;
1950
2374
  }
@@ -2000,15 +2424,14 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
2000
2424
  });
2001
2425
  return refreshed.accessToken;
2002
2426
  }
2003
- async function doLogin(config, dir, options, print, installer) {
2427
+ async function doLogin(config, dir, options, prompt, installer) {
2004
2428
  const fetchImpl = options.fetchImpl ?? fetch;
2005
2429
  const open = options.open ?? openBrowser;
2006
2430
  if (config.loginKind === "github") {
2007
2431
  const tokens = await loginGithub(config, {
2008
2432
  fetchImpl,
2009
2433
  open,
2010
- print,
2011
- ...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}.`)
2012
2435
  });
2013
2436
  writeTokens(dir, {
2014
2437
  accessToken: tokens.accessToken,
@@ -2016,7 +2439,12 @@ async function doLogin(config, dir, options, print, installer) {
2016
2439
  expiresAt: Date.now() + tokens.expiresIn * 1e3
2017
2440
  });
2018
2441
  } else {
2019
- 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
+ });
2020
2448
  writeTokens(dir, {
2021
2449
  accessToken: tokens.accessToken,
2022
2450
  refreshToken: tokens.refreshToken,
@@ -2024,20 +2452,19 @@ async function doLogin(config, dir, options, print, installer) {
2024
2452
  clientId: tokens.clientId
2025
2453
  });
2026
2454
  }
2027
- if (!installer) print(`signed in to ${config.product}.`);
2028
2455
  }
2029
- async function fetchAccessTokenOrLogin(config, dir, options, print, installer) {
2456
+ async function fetchAccessTokenOrLogin(config, dir, options, installer) {
2030
2457
  const fetchImpl = options.fetchImpl ?? fetch;
2031
2458
  const fresh = await ensureFreshToken(config, dir, fetchImpl);
2032
2459
  if (fresh) return fresh;
2033
- await doLogin(config, dir, options, print, installer);
2460
+ await doLogin(config, dir, options, (line) => installer.relay(line), installer);
2034
2461
  const after = readTokens(dir);
2035
2462
  if (!after) throw new Error("sign-in did not produce a token");
2036
2463
  return after.accessToken;
2037
2464
  }
2038
2465
  function readPayloadArgv(dir, key) {
2039
2466
  try {
2040
- 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"));
2041
2468
  const entry2 = parsed[key];
2042
2469
  if (typeof entry2 === "string") {
2043
2470
  const parts = entry2.trim().split(/\s+/).filter(Boolean);
@@ -2059,7 +2486,7 @@ function readPayloadRun(dir) {
2059
2486
  }
2060
2487
  function readPayloadVerbs(dir) {
2061
2488
  try {
2062
- 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"));
2063
2490
  const verbs = parsed.verbs;
2064
2491
  if (verbs === "*") return "*";
2065
2492
  if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
@@ -2076,7 +2503,7 @@ function resolveEntry(entry2) {
2076
2503
  function needsShell(command2) {
2077
2504
  if (process.platform !== "win32") return false;
2078
2505
  if (/\.(cmd|bat)$/i.test(command2)) return true;
2079
- return !(0, import_node_fs11.existsSync)(command2);
2506
+ return !(0, import_node_fs12.existsSync)(command2);
2080
2507
  }
2081
2508
  function quoteForShell(arg) {
2082
2509
  return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
@@ -2108,7 +2535,7 @@ function defaultRunEntry(entry2, cwd, env) {
2108
2535
  const [command2, ...args] = entry2;
2109
2536
  const shell = needsShell(command2);
2110
2537
  const commandLine = shell ? [command2, ...args].map(quoteForShell).join(" ") : command2;
2111
- 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, {
2112
2539
  cwd,
2113
2540
  stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
2114
2541
  shell,
@@ -2142,8 +2569,16 @@ function faceProduct(config) {
2142
2569
  function formatMb(bytes) {
2143
2570
  return (bytes / 1048576).toFixed(1).replace(/\.0$/, "");
2144
2571
  }
2145
- 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) {
2146
2580
  const product = faceProduct(config);
2581
+ const dryRun = update && Boolean(flags2.dryRun);
2147
2582
  const installer = createInstallerRun({
2148
2583
  product,
2149
2584
  gate: config.host,
@@ -2153,17 +2588,55 @@ async function installOrUpdate(config, dir, options, print, update) {
2153
2588
  operation: update ? "update" : "install",
2154
2589
  tty: options.tty,
2155
2590
  animate: !options.print && Boolean(process.stderr.isTTY),
2591
+ dryRun,
2592
+ ...flags2.noColor ? { color: false } : {},
2156
2593
  ...options.print ? { write: (text) => print(text.replace(/\n$/, "")) } : {}
2157
2594
  });
2158
2595
  const fetchImpl = options.fetchImpl ?? fetch;
2159
2596
  let version = readState(dir)?.version ?? "unknown";
2160
2597
  installer.start();
2161
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
+ }
2162
2628
  const current = readState(dir);
2163
- const payloadPresent = (0, import_node_fs11.existsSync)(payloadDir(dir));
2629
+ const payloadPresent = (0, import_node_fs12.existsSync)(payloadDir(dir));
2164
2630
  let started = Date.now();
2165
2631
  installer.phase("resolve", { state: "running" });
2166
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
+ }
2167
2640
  if (pointer && current?.version === pointer && payloadPresent) {
2168
2641
  version = pointer;
2169
2642
  installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
@@ -2180,14 +2653,14 @@ async function installOrUpdate(config, dir, options, print, update) {
2180
2653
  }
2181
2654
  installer.phase("sign-in", { state: "running" });
2182
2655
  started = Date.now();
2183
- const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print, installer);
2656
+ const accessToken = await fetchAccessTokenOrLogin(config, dir, options, installer);
2184
2657
  installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
2185
2658
  installer.phase("resolve", { state: "running" });
2186
2659
  started = Date.now();
2187
2660
  const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
2188
2661
  version = manifest.version;
2189
2662
  installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
2190
- 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));
2191
2664
  if (unchanged && current.acquired) {
2192
2665
  installer.surface({ id: config.product, from: version, to: version, state: "current" });
2193
2666
  return await finishLastMile(config, dir, options, installer, version, true, acquisitionRepairCommand(dir, version));
@@ -2197,14 +2670,14 @@ async function installOrUpdate(config, dir, options, print, update) {
2197
2670
  started = Date.now();
2198
2671
  const cached = reusableCandidate(dir, manifest);
2199
2672
  const candidate = cached ?? (0, import_node_crypto6.randomUUID)();
2200
- const candidateDir = (0, import_node_path6.join)(dir, "candidates", candidate);
2673
+ const candidateDir = (0, import_node_path7.join)(dir, "candidates", candidate);
2201
2674
  const onProgress = ({ done, total }) => installer.phase("download", {
2202
2675
  state: "running",
2203
2676
  measure: total ? `${Math.floor(done / total * 100)}% \xB7 ${formatMb(done)} of ${formatMb(total)} MB` : `${formatMb(done)} MB`,
2204
2677
  ...total ? { runningTitle: `Downloading ${config.product} ${version} (${formatMb(total)} MB)` } : {}
2205
2678
  });
2206
2679
  if (!cached) await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl, onProgress, cacheDir: dir });
2207
- 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;
2208
2681
  if (acquisition) {
2209
2682
  rememberCandidate(dir, candidate, manifest);
2210
2683
  installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
@@ -2227,10 +2700,10 @@ async function installOrUpdate(config, dir, options, print, update) {
2227
2700
  installer.finish(outcome ?? { version, total: 1, updated: 1, failed: 0, installed: true });
2228
2701
  return 0;
2229
2702
  }
2230
- const target = (0, import_node_path6.join)(dir, "payload");
2231
- (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
2232
- (0, import_node_fs11.rmSync)(target, { recursive: true, force: true });
2233
- (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);
2234
2707
  writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
2235
2708
  installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
2236
2709
  }
@@ -2256,11 +2729,11 @@ async function installOrUpdate(config, dir, options, print, update) {
2256
2729
  installer.stop();
2257
2730
  }
2258
2731
  }
2259
- function doInstall(config, dir, options, print) {
2260
- return installOrUpdate(config, dir, options, print, false);
2732
+ function doInstall(config, dir, options, print, flags2) {
2733
+ return installOrUpdate(config, dir, options, print, false, flags2);
2261
2734
  }
2262
- function doUpdate(config, dir, options, print) {
2263
- return installOrUpdate(config, dir, options, print, true);
2735
+ function doUpdate(config, dir, options, print, flags2) {
2736
+ return installOrUpdate(config, dir, options, print, true, flags2);
2264
2737
  }
2265
2738
  function payloadEnv(dir) {
2266
2739
  const stored = readTokens(dir);
@@ -2272,12 +2745,12 @@ function payloadFileAsCommand(entry2, payload) {
2272
2745
  const first = entry2[0];
2273
2746
  if (!first || first === "$self") return null;
2274
2747
  if (first.includes("/") || first.includes("\\")) return null;
2275
- const candidate = (0, import_node_path6.join)(payload, first);
2276
- 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;
2277
2750
  if (NEVER_A_PROGRAM.test(first)) return first;
2278
2751
  if (process.platform === "win32") return null;
2279
2752
  try {
2280
- 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;
2281
2754
  } catch {
2282
2755
  return null;
2283
2756
  }
@@ -2292,7 +2765,7 @@ async function finishLastMile(config, dir, options, installer, version, unchange
2292
2765
  updated: unchanged ? 0 : 1,
2293
2766
  failed: 0,
2294
2767
  installed: true,
2295
- 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}.`
2296
2769
  });
2297
2770
  return 0;
2298
2771
  }
@@ -2338,9 +2811,9 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2338
2811
  const [command2, ...args] = entry2;
2339
2812
  const shell = needsShell(command2);
2340
2813
  const progress = !(process.platform === "win32" && shell);
2341
- const outcomeDir = (0, import_node_fs11.mkdtempSync)((0, import_node_path6.join)((0, import_node_os4.tmpdir)(), "mm-installer-outcome-"));
2342
- const outcomeFile = (0, import_node_path6.join)(outcomeDir, "outcome.json");
2343
- 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");
2344
2817
  const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
2345
2818
  delete childEnv.MM_FACE_TRANSCRIPT;
2346
2819
  delete childEnv.MM_PROGRESS_FILE;
@@ -2353,7 +2826,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2353
2826
  const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
2354
2827
  try {
2355
2828
  const result = await new Promise((resolve3) => {
2356
- 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, {
2357
2830
  cwd,
2358
2831
  shell,
2359
2832
  windowsHide: true,
@@ -2390,8 +2863,8 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2390
2863
  const pollProgress = () => {
2391
2864
  if (progressError) return;
2392
2865
  try {
2393
- if (!(0, import_node_fs11.existsSync)(progressFile)) return;
2394
- 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);
2395
2868
  receive(progressDecoder.write(bytes.subarray(progressOffset)));
2396
2869
  progressOffset = bytes.length;
2397
2870
  } catch {
@@ -2424,109 +2897,146 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2424
2897
  return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
2425
2898
  }
2426
2899
  } finally {
2427
- if ((0, import_node_fs11.existsSync)(progressFile)) (0, import_node_fs11.unlinkSync)(progressFile);
2428
- if ((0, import_node_fs11.existsSync)(outcomeFile)) (0, import_node_fs11.unlinkSync)(outcomeFile);
2429
- (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);
2430
2903
  }
2431
2904
  }
2432
- function doForward(config, dir, options, command2, argv2, print, unlock) {
2433
- if (command2 && (0, import_node_fs11.existsSync)(command2)) {
2434
- print(`${command2} is a file, not a command \u2014 did you mean \`--run ${command2}\`?`);
2435
- 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);
2436
2914
  }
2437
2915
  const acquired = readState(dir)?.acquired;
2438
2916
  const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
2439
2917
  const verbs = readPayloadVerbs(dir);
2440
- const declared = verbs === "*" || Array.isArray(verbs) && command2 !== void 0 && verbs.includes(command2);
2441
- if (!target || !declared) {
2442
- print(`unknown command: ${command2}`);
2443
- printUsage(config, print);
2444
- return 2;
2445
- }
2446
- 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];
2447
2921
  const cwd = payloadDir(dir);
2448
2922
  const inherited = payloadEnv(dir);
2449
2923
  const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
2450
2924
  unlock();
2451
2925
  const result = (options.runEntry ?? defaultRunEntry)(forwarded, cwd, env);
2452
2926
  if (!result.ok && result.code === void 0) {
2453
- print(`failed: ${result.error ?? `could not run ${forwarded[0]}`}`);
2454
- return 1;
2927
+ return ui.refuse(`Could not start ${config.product}: ${sentence(result.error ?? `could not run ${forwarded[0]}`)}`, `${ui.bin} doctor`);
2455
2928
  }
2456
2929
  return result.code ?? 0;
2457
2930
  }
2458
- function doAutoupdate(config, options, args, print) {
2931
+ function doAutoupdate(config, dir, options, args, ui) {
2459
2932
  const mode = args[0] ?? "status";
2460
- const scheduleOptions = options.autoupdate ?? {};
2933
+ const scheduleOptions = { stateDir: dir, ...options.autoupdate };
2461
2934
  const command2 = options.autoupdate?.command ?? [process.execPath, "update"];
2462
- if (mode === "on") {
2935
+ const name = identityFor(ui.product).name;
2936
+ if (mode === "on" || mode === "off") {
2463
2937
  try {
2464
- enableSchedule(config, command2, scheduleOptions);
2938
+ if (mode === "on") enableSchedule(config, command2, scheduleOptions);
2939
+ else disableSchedule(config, scheduleOptions);
2465
2940
  } catch (error) {
2466
- print(error.message);
2467
- return 1;
2941
+ return ui.refuse(sentence(error.message), `${ui.bin} doctor`);
2468
2942
  }
2469
- const state2 = querySchedule(config, scheduleOptions);
2470
- print(`auto-update: on (${state2.cadence ?? "hourly"})`);
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`);
2471
2945
  return 0;
2472
2946
  }
2473
- if (mode === "off") {
2474
- try {
2475
- disableSchedule(config, scheduleOptions);
2476
- } catch (error) {
2477
- print(error.message);
2478
- return 1;
2479
- }
2480
- print("auto-update: off");
2481
- return 0;
2482
- }
2483
- if (mode !== "status") {
2484
- print(`unknown autoupdate mode: ${mode} \u2014 use on, off or status.`);
2485
- return 1;
2486
- }
2947
+ if (mode !== "status") return ui.refuse(`Unknown autoupdate mode: ${mode}.`, `${ui.bin} autoupdate status`, 2);
2487
2948
  const state = querySchedule(config, scheduleOptions);
2488
- if (!state.supported) {
2489
- print("auto-update: unsupported on this platform");
2490
- return 1;
2491
- }
2492
- print(`auto-update: ${state.enabled ? `on (${state.cadence ?? "hourly"})` : "off"}`);
2493
- 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`);
2494
2952
  return 0;
2495
2953
  }
2496
- function doDoctor(config, dir, options, print, unlock) {
2497
- 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;
2498
3030
  const acquired = readState(dir)?.acquired;
2499
3031
  const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
2500
3032
  const verbs = readPayloadVerbs(dir);
2501
3033
  const chains = target !== null && (verbs === "*" || Array.isArray(verbs) && verbs.includes("doctor"));
2502
- if (!chains) return 0;
3034
+ if (!chains) return own;
2503
3035
  const cwd = payloadDir(dir);
2504
3036
  const inherited = payloadEnv(dir);
2505
3037
  const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
2506
- unlock();
2507
3038
  const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], cwd, env);
2508
- return result.code ?? (result.ok ? 0 : 1);
2509
- }
2510
- function doLauncherDoctor(config, dir, options, print) {
2511
- const tokens = readTokens(dir);
2512
- const state = readState(dir);
2513
- const payloadPresent = (0, import_node_fs11.existsSync)(payloadDir(dir));
2514
- print(`product: ${config.product}`);
2515
- print(`host: ${config.host}`);
2516
- print(`login: ${config.loginKind}`);
2517
- print(`dir: ${dir}`);
2518
- if (tokens) {
2519
- const remaining = Math.max(0, Math.round((tokens.expiresAt - Date.now()) / 1e3));
2520
- print(`token: present (expires in ${remaining}s)`);
2521
- } else {
2522
- print("token: none \u2014 run login first.");
2523
- }
2524
- print(`payload: ${state ? state.version : "none"}${payloadPresent ? "" : " (not downloaded)"}`);
2525
- print(`paths: tokens ${(0, import_node_path6.join)(dir, "tokens.json")}, state ${(0, import_node_path6.join)(dir, "state.json")}, payload ${payloadDir(dir)}`);
2526
- const schedule = querySchedule(config, options.autoupdate ?? {});
2527
- print(
2528
- `auto-update: ${!schedule.supported ? "unsupported on this platform" : schedule.enabled ? `on (${schedule.cadence ?? "hourly"})` : "off"}`
2529
- );
3039
+ return (result.code ?? (result.ok ? 0 : 1)) || own;
2530
3040
  }
2531
3041
  var invokedAsMain = typeof process.argv[1] === "string" && (() => {
2532
3042
  try {
@@ -2545,8 +3055,7 @@ if (invokedAsMain) {
2545
3055
  process.exitCode = code;
2546
3056
  },
2547
3057
  (error) => {
2548
- process.stderr.write(`failed: ${error.message}
2549
- `);
3058
+ printScreen(renderNotice(NO_PRODUCT, { state: "fail", message: sentence(error.message) }, { color: false }), "stderr");
2550
3059
  process.exitCode = 1;
2551
3060
  }
2552
3061
  );
@@ -2557,6 +3066,7 @@ if (invokedAsMain) {
2557
3066
  defaultRunEntry,
2558
3067
  ensureFreshToken,
2559
3068
  formatMb,
3069
+ parseArgv,
2560
3070
  readPayloadEntry,
2561
3071
  readPayloadRun,
2562
3072
  readPayloadVerbs,