@mutmutco/installer-launcher 1.0.1 → 1.2.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.
package/dist/launcher.js CHANGED
@@ -296,6 +296,11 @@ function parseProductConfig(text) {
296
296
  } catch {
297
297
  throw new Error("product config host must be an http(s) URL");
298
298
  }
299
+ const dl = field(record, "dl").replace(/\/+$/, "");
300
+ if (dl) {
301
+ if (!/^https:\/\//u.test(dl)) throw new Error("product config dl must be an https URL");
302
+ config.dl = dl;
303
+ }
299
304
  return config;
300
305
  }
301
306
  function field(record, key) {
@@ -671,10 +676,11 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
671
676
  }
672
677
 
673
678
  // src/index.ts
674
- import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
675
- import { existsSync as existsSync7, readFileSync as readFileSync7, statSync, mkdtempSync as mkdtempSync2, unlinkSync as unlinkSync2, rmdirSync, mkdirSync as mkdirSync6, renameSync as renameSync4, rmSync as rmSync5 } from "node:fs";
679
+ import { spawn as spawn2, spawnSync as spawnSync4 } from "node:child_process";
680
+ import { existsSync as existsSync8, readFileSync as readFileSync8, statSync, mkdtempSync as mkdtempSync2, unlinkSync as unlinkSync2, rmdirSync, mkdirSync as mkdirSync6, renameSync as renameSync5, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
676
681
  import { tmpdir as tmpdir4 } from "node:os";
677
- import { dirname as dirname3, join as join6, resolve as resolve2 } from "node:path";
682
+ import { dirname as dirname4, join as join7, resolve as resolve2 } from "node:path";
683
+ import { isSea } from "node:sea";
678
684
  import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
679
685
 
680
686
  // ../face/src/face.ts
@@ -685,6 +691,7 @@ var PRODUCTS = Object.freeze({
685
691
  "mm-strategy": Object.freeze({
686
692
  name: "MM Strategy",
687
693
  installWarm: "Welcome. Let's get you ready.",
694
+ checkWarm: "Here's where things stand.",
688
695
  accent: "38;2;249;115;22",
689
696
  warm: "Welcome back. Checking for updates...",
690
697
  doctor: "mm-strategy doctor"
@@ -692,6 +699,7 @@ var PRODUCTS = Object.freeze({
692
699
  "mmi-hub": Object.freeze({
693
700
  name: "mmi-hub",
694
701
  installWarm: "Welcome. Let's get you ready.",
702
+ checkWarm: "Here's where things stand.",
695
703
  accent: "38;2;125;211;252",
696
704
  warm: "Welcome back. Checking for updates...",
697
705
  doctor: "mmi doctor"
@@ -699,6 +707,7 @@ var PRODUCTS = Object.freeze({
699
707
  "jerv-hub": Object.freeze({
700
708
  name: "jerv-hub",
701
709
  installWarm: "Welcome. Let's get you ready.",
710
+ checkWarm: "Here's where things stand.",
702
711
  accent: "38;2;248;113;113",
703
712
  warm: "Welcome back. Checking for updates...",
704
713
  doctor: "jerv doctor"
@@ -706,6 +715,7 @@ var PRODUCTS = Object.freeze({
706
715
  jervcode: Object.freeze({
707
716
  name: "JervCode",
708
717
  installWarm: "Welcome. Let's get you ready.",
718
+ checkWarm: "Here's where things stand.",
709
719
  accent: "38;2;192;132;252",
710
720
  warm: "Welcome back. Checking for updates...",
711
721
  doctor: "jervcode doctor"
@@ -821,7 +831,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
821
831
  const welcome = () => continuesFace || nested ? [] : [
822
832
  `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
823
833
  bar(),
824
- `${bar()} ${operation === "install" ? identity.installWarm : identity.warm}`,
834
+ `${bar()} ${operation === "install" ? identity.installWarm : operation === "check" ? identity.checkWarm : identity.warm}`,
825
835
  bar()
826
836
  ];
827
837
  const continues = (phase, kind = "ok") => {
@@ -851,7 +861,11 @@ function createFace({ product, color = false, columns, env = process.env, operat
851
861
  const content = Math.min(width - 6, Math.max(...body.map(visibleWidth)));
852
862
  const rule = paint(identity.accent, GLYPH.rule.repeat(content + 4));
853
863
  const frame = (left, right) => `${paint(identity.accent, left)}${rule}${paint(identity.accent, right)}`;
854
- const bodyRow = (line) => line.startsWith(GLYPH.check) ? `${paint(identity.accent, GLYPH.check)}${line.slice(1)}` : line;
864
+ const headGlyph = { [GLYPH.check]: identity.accent, [GLYPH.cross]: identity.accent, [GLYPH.dot]: PALETTE.muted };
865
+ const bodyRow = (line) => {
866
+ const sgr = headGlyph[line.charAt(0)];
867
+ return sgr ? `${paint(sgr, line.charAt(0))}${line.slice(1)}` : line;
868
+ };
855
869
  return [
856
870
  frame(GLYPH.boxTop, GLYPH.boxTopEnd),
857
871
  ...body.map((line) => `${paint(identity.accent, GLYPH.bar)} ${bodyRow(line)}${" ".repeat(Math.max(0, content - visibleWidth(line)))} ${paint(identity.accent, GLYPH.bar)}`),
@@ -877,6 +891,9 @@ function createFace({ product, color = false, columns, env = process.env, operat
877
891
  // ../face/src/shell.ts
878
892
  var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
879
893
 
894
+ // ../face/src/rows.ts
895
+ var plainText = (text) => String(text).replaceAll("\u2192", "->").replaceAll("\xB7", "-").replaceAll("\u2014", "-").replaceAll("\u2026", "...");
896
+
880
897
  // ../face/src/spinner.ts
881
898
  import { appendFileSync as appendFileSync2, writeSync as writeSync2 } from "node:fs";
882
899
  import { Worker } from "node:worker_threads";
@@ -1109,22 +1126,44 @@ function validateInstallerProduct(value) {
1109
1126
  if (doctor !== identity.doctor) return fail("doctor");
1110
1127
  if (!Array.isArray(input.surfaces) || input.surfaces.length === 0) return fail("surfaces");
1111
1128
  const ids = /* @__PURE__ */ new Set();
1112
- const surfaces = input.surfaces.map((raw) => {
1113
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail("surface");
1114
- const source = raw;
1115
- const id = text(source.id, "surface.id");
1129
+ const surfaces = input.surfaces.map((raw2) => {
1130
+ if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return fail("surface");
1131
+ const source2 = raw2;
1132
+ const id = text(source2.id, "surface.id");
1116
1133
  if (ids.has(id)) return fail("duplicate surface.id");
1117
1134
  ids.add(id);
1118
1135
  const surface = { id };
1119
1136
  for (const field2 of ["npm", "bin", "kind", "activation"]) {
1120
- if (source[field2] !== void 0) surface[field2] = text(source[field2], `surface.${field2}`);
1137
+ if (source2[field2] !== void 0) surface[field2] = text(source2[field2], `surface.${field2}`);
1121
1138
  }
1122
1139
  if (Boolean(surface.npm) !== Boolean(surface.bin)) return fail("surface npm/bin pair");
1123
1140
  if (surface.kind !== void 0 && !["agent-home", "payload"].includes(surface.kind)) return fail("surface.kind");
1124
1141
  if (!surface.npm && !surface.kind) return fail("surface implementation");
1125
1142
  return surface;
1126
1143
  });
1127
- return { product, gate, doctor, surfaces };
1144
+ if (input.launcher === void 0) return { product, gate, doctor, surfaces };
1145
+ const raw = input.launcher;
1146
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail("launcher");
1147
+ const source = raw;
1148
+ const https = (value2, field2) => {
1149
+ const url = text(value2, field2).replace(/\/+$/u, "");
1150
+ let parsed;
1151
+ try {
1152
+ parsed = new URL(url);
1153
+ } catch {
1154
+ return fail(field2);
1155
+ }
1156
+ return parsed.protocol === "https:" && !parsed.username && !parsed.password ? url : fail(field2);
1157
+ };
1158
+ const launcher = {
1159
+ product: text(source.product, "launcher.product"),
1160
+ bin: text(source.bin, "launcher.bin"),
1161
+ key: text(source.key, "launcher.key"),
1162
+ host: https(source.host, "launcher.host")
1163
+ };
1164
+ if (source.dl !== void 0) launcher.dl = https(source.dl, "launcher.dl");
1165
+ if (Buffer.from(launcher.key, "base64").length !== 32) return fail("launcher.key");
1166
+ return { product, gate, doctor, surfaces, launcher };
1128
1167
  }
1129
1168
  var PHASES = {
1130
1169
  preflight: ["Checking prerequisites", "Checked prerequisites"],
@@ -1149,6 +1188,23 @@ var UPDATE_CHECK_PHASES = /* @__PURE__ */ new Set([
1149
1188
  "sign-in",
1150
1189
  "check"
1151
1190
  ]);
1191
+ var SURFACE_WORDS = Object.freeze({
1192
+ updated: "updated",
1193
+ current: "already current",
1194
+ failed: "failed",
1195
+ skipped: "skipped",
1196
+ retry: "retrying",
1197
+ pending: "pending",
1198
+ kept: "kept",
1199
+ /** Read-only report states. */
1200
+ behind: "update available",
1201
+ ahead: "ahead of the release",
1202
+ staged: "next launch",
1203
+ unknown: "unknown",
1204
+ missing: "not installed"
1205
+ });
1206
+ var AUTO_UPDATE = "auto-update";
1207
+ var LAUNCHER_SURFACE = "launcher";
1152
1208
  function createInstallerRun(value, options = {}) {
1153
1209
  const declaration = validateInstallerProduct(value);
1154
1210
  const env = options.env ?? process.env;
@@ -1276,11 +1332,11 @@ function createInstallerRun(value, options = {}) {
1276
1332
  },
1277
1333
  surface(facts) {
1278
1334
  if (finished) throw new Error("installer run already finished");
1279
- const surface = declaration.surfaces.find((surface2) => surface2.id === facts.id);
1335
+ const surface = declaration.surfaces.find((surface2) => surface2.id === facts.id) ?? (facts.id === LAUNCHER_SURFACE ? { id: LAUNCHER_SURFACE } : void 0);
1280
1336
  if (!surface) throw new Error("installer run: undeclared surface");
1281
1337
  start();
1282
1338
  const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} ${tty ? "\u2192" : "->"} ${facts.to}` : ` ${facts.to}` : "";
1283
- const status = { updated: options.dryRun ? "would update" : "updated", current: "already current", failed: "failed", skipped: "skipped", retry: "retrying", pending: "pending", kept: "kept" }[facts.state];
1339
+ const status = facts.state === "updated" && options.dryRun ? "would update" : SURFACE_WORDS[facts.state];
1284
1340
  if (!status) throw new Error("installer run: unknown surface state");
1285
1341
  const separator = tty ? "\xB7" : "-";
1286
1342
  const activation = facts.state === "updated" && surface.activation ? ` ${separator} ${surface.activation}` : "";
@@ -1388,8 +1444,9 @@ function createInstallerRun(value, options = {}) {
1388
1444
  if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
1389
1445
  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}.`;
1390
1446
  const ready = facts.failed === 0 && !facts.dryRun && !facts.deferred && !facts.operationFailed && Boolean(facts.version);
1447
+ const deferred = Boolean(facts.deferred) && facts.failed === 0 && !facts.operationFailed;
1391
1448
  const body = [
1392
- `${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."}`,
1449
+ `${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."}`,
1393
1450
  changed,
1394
1451
  ...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
1395
1452
  ...facts.detail ? [facts.detail] : [],
@@ -1412,6 +1469,89 @@ function createInstallerRun(value, options = {}) {
1412
1469
  return run2;
1413
1470
  }
1414
1471
 
1472
+ // ../face/src/screens.ts
1473
+ function screenLane(render = {}, stream = "stdout") {
1474
+ const env = render.env ?? process.env;
1475
+ const out = stream === "stdout" ? process.stdout : process.stderr;
1476
+ const tty = render.tty ?? Boolean(out.isTTY);
1477
+ const color = tty && render.color !== false && env.NO_COLOR === void 0 && env.TERM !== "dumb";
1478
+ return { tty, color, columns: render.columns ?? out.columns, env };
1479
+ }
1480
+ 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 });
1481
+ function renderNotice(product, notice, render) {
1482
+ const lane = screenLane(render, notice.state === "fail" ? "stderr" : "stdout");
1483
+ const face = faceFor(product, lane);
1484
+ const width = face.width - 3;
1485
+ const [first, ...rest] = wrapWords(plain(notice.message, lane), width);
1486
+ if (!lane.tty) {
1487
+ return [`${notice.state === "fail" ? "Error: " : ""}${[first, ...rest].join(" ")}`, ...notice.next ? [`Next: ${notice.next}`] : []];
1488
+ }
1489
+ 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);
1490
+ return [
1491
+ `${glyph} ${first}`,
1492
+ ...rest.map((line) => ` ${line}`),
1493
+ ...notice.next ? [` ${face.paint(PALETTE.muted, "Next:")} ${notice.next}`] : []
1494
+ ];
1495
+ }
1496
+ function renderReport(product, report, render) {
1497
+ const lane = screenLane(render);
1498
+ const face = faceFor(product, lane, "check");
1499
+ const { verdict } = report;
1500
+ const headline = `${face.identity.name} ${verdict.headline}`;
1501
+ if (!lane.tty) {
1502
+ return [
1503
+ `${face.identity.name} - Mutatis Mutandis`,
1504
+ face.identity.checkWarm,
1505
+ ...report.rows.map((row) => `${row.mark === "fail" ? "Failed: " : ""}${plainText(rowTitle(row))}`),
1506
+ ...report.rows.flatMap((row) => row.note ? [` ${plainText(row.note)}`] : []),
1507
+ `${verdict.state === "fail" ? "Error: " : ""}${plainText(headline)}`,
1508
+ ...(verdict.facts ?? []).map(plainText),
1509
+ `Next: ${verdict.next}`
1510
+ ];
1511
+ }
1512
+ const glyph = verdict.state === "fail" ? GLYPH.cross : verdict.state === "note" ? GLYPH.dot : GLYPH.check;
1513
+ const facts = verdict.facts?.length ? verdict.facts : [verdict.state === "ok" ? "Everything checked passed." : "See the rows above."];
1514
+ return [
1515
+ ...face.welcome(),
1516
+ // A report row IS an update row: glyph, subject, state (#7071). Two layouts for one fact — a
1517
+ // mark mid-row after a subject column here, a leading glyph in `update` — is the drift itself.
1518
+ ...report.rows.flatMap((row) => [face.step(rowTitle(row), null, row.mark ?? "note"), ...row.note ? [face.relay(row.note)] : []]),
1519
+ ...report.rows.length ? [face.paint(PALETTE.muted, GLYPH.bar)] : [],
1520
+ ...face.receipt([`${glyph} ${headline}`, ...facts, `Next: ${verdict.next}`], { ready: verdict.state === "ok" }),
1521
+ face.signOff()
1522
+ ].filter((line) => line !== "");
1523
+ }
1524
+ function renderUsage(product, usage2, render) {
1525
+ const lane = screenLane(render);
1526
+ const face = faceFor(product, lane);
1527
+ const all = [...usage2.commands, ...usage2.options ?? []];
1528
+ const column = Math.min(Math.max(...all.map((entry2) => entry2.usage.length)), 34);
1529
+ const table = (entries) => entries.flatMap((entry2) => {
1530
+ const [first, ...rest] = wrapWords(plain(entry2.text, lane), Math.max(20, face.width - column - 6));
1531
+ const head = entry2.usage.length > column ? [` ${entry2.usage}`, ` ${" ".repeat(column)} ${first}`] : [` ${entry2.usage.padEnd(column)} ${first}`];
1532
+ return [...head, ...rest.map((line) => ` ${" ".repeat(column)} ${line}`)];
1533
+ });
1534
+ return [
1535
+ `${face.paint(face.identity.accent, usage2.bin)} ${usage2.version} ${lane.tty ? "\xB7" : "-"} ${plain(usage2.summary, lane)}`,
1536
+ "",
1537
+ `Usage: ${usage2.bin} <command> [options]`,
1538
+ "",
1539
+ "Commands:",
1540
+ ...table(usage2.commands),
1541
+ ...usage2.options?.length ? ["", "Options:", ...table(usage2.options)] : []
1542
+ ];
1543
+ }
1544
+ function renderVersion(bin, version, launcher) {
1545
+ return `${bin} ${version}${launcher ? ` (launcher ${launcher})` : ""}`;
1546
+ }
1547
+ function printScreen(lines, stream = "stdout") {
1548
+ const out = stream === "stdout" ? process.stdout : process.stderr;
1549
+ out.write(lines.map((line) => `${line}
1550
+ `).join(""));
1551
+ }
1552
+ var plain = (text, lane) => lane.tty ? String(text) : plainText(stripColor(String(text)));
1553
+ var rowTitle = (row) => `${row.surface}${row.aspect ? ` ${row.aspect}` : ""}${row.instance ? ` \xB7 ${row.instance}` : ""} ${row.state}`;
1554
+
1415
1555
  // src/autoupdate.ts
1416
1556
  import { spawnSync as spawnSync2 } from "node:child_process";
1417
1557
  import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
@@ -1442,19 +1582,33 @@ function scheduleName(config) {
1442
1582
  function scheduleLabel(config) {
1443
1583
  return `${config.binName}-autoupdate`.replace(/[^A-Za-z0-9_-]+/g, "-");
1444
1584
  }
1585
+ var HIDDEN_SCRIPT = "autoupdate-hidden.vbs";
1586
+ function hiddenVbs(command2) {
1587
+ const line = command2.map((arg) => `""${arg.replaceAll('"', '""')}""`).join(" ");
1588
+ return `' Generated by \`autoupdate on\`: wscript //B runs this with no window, so the hourly update
1589
+ ' never flashes a console. The exit code is the update's, for Task Scheduler's last result.
1590
+ Dim shell, code
1591
+ Set shell = CreateObject("WScript.Shell")
1592
+ code = shell.Run("${line}", 0, True)
1593
+ WScript.Quit code
1594
+ `;
1595
+ }
1445
1596
  function quoteWindows(arg) {
1446
1597
  return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
1447
1598
  }
1448
1599
  function enableSchedule(config, command2, options = {}) {
1449
1600
  const platform = schedulePlatform(options.platform);
1450
- if (!platform) throw new Error(`autoupdate is not supported on ${options.platform ?? process.platform}.`);
1601
+ if (!platform) throw new Error(`Auto-update is not supported on ${options.platform ?? process.platform}.`);
1451
1602
  const exec = options.exec ?? defaultExec;
1452
1603
  const home = homeOf(options);
1453
1604
  if (platform === "win32") {
1454
- const taskLine = command2.map(quoteWindows).join(" ");
1455
- const result2 = exec("schtasks", ["/Create", "/TN", scheduleName(config), "/TR", taskLine, "/SC", "HOURLY", "/IT", "/F"]);
1605
+ const stateDir = options.stateDir ?? defaultProductDir(config.product);
1606
+ mkdirSync5(stateDir, { recursive: true });
1607
+ const script = join5(stateDir, HIDDEN_SCRIPT);
1608
+ writeFileSync6(script, hiddenVbs(command2));
1609
+ const result2 = exec("schtasks", ["/Create", "/TN", scheduleName(config), "/TR", `wscript.exe //B //Nologo ${quoteWindows(script)}`, "/SC", "HOURLY", "/IT", "/F"]);
1456
1610
  if (result2.code !== 0) {
1457
- throw new Error(`could not turn autoupdate on: ${firstLine(result2.stdout || result2.stderr) || `exit ${result2.code}`}`);
1611
+ throw new Error(`Could not turn auto-update on: ${firstLine(result2.stdout || result2.stderr) || `exit ${result2.code}`}`);
1458
1612
  }
1459
1613
  return;
1460
1614
  }
@@ -1467,7 +1621,7 @@ function enableSchedule(config, command2, options = {}) {
1467
1621
  exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
1468
1622
  const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
1469
1623
  if (result2.code !== 0) {
1470
- throw new Error(`could not turn autoupdate on: ${firstLine(result2.stderr || result2.stdout) || `exit ${result2.code}`}`);
1624
+ throw new Error(`Could not turn auto-update on: ${firstLine(result2.stderr || result2.stdout) || `exit ${result2.code}`}`);
1471
1625
  }
1472
1626
  return;
1473
1627
  }
@@ -1478,23 +1632,24 @@ function enableSchedule(config, command2, options = {}) {
1478
1632
  writeFileSync6(join5(dir, `${label}.timer`), linuxTimer(label));
1479
1633
  const reload = exec("systemctl", ["--user", "daemon-reload"]);
1480
1634
  if (reload.code !== 0) {
1481
- throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
1635
+ throw new Error(`Could not turn auto-update on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
1482
1636
  }
1483
1637
  const result = exec("systemctl", ["--user", "enable", "--now", `${label}.timer`]);
1484
1638
  if (result.code !== 0) {
1485
- throw new Error(`could not turn autoupdate on: ${firstLine(result.stderr || result.stdout) || `exit ${result.code}`}`);
1639
+ throw new Error(`Could not turn auto-update on: ${firstLine(result.stderr || result.stdout) || `exit ${result.code}`}`);
1486
1640
  }
1487
1641
  }
1488
1642
  function disableSchedule(config, options = {}) {
1489
1643
  const platform = schedulePlatform(options.platform);
1490
- if (!platform) throw new Error(`autoupdate is not supported on ${options.platform ?? process.platform}.`);
1644
+ if (!platform) throw new Error(`Auto-update is not supported on ${options.platform ?? process.platform}.`);
1491
1645
  const exec = options.exec ?? defaultExec;
1492
1646
  const home = homeOf(options);
1493
1647
  if (platform === "win32") {
1494
1648
  const result = exec("schtasks", ["/Delete", "/TN", scheduleName(config), "/F"]);
1495
1649
  if (result.code !== 0 && !/cannot find|does not exist/i.test(`${result.stdout} ${result.stderr}`)) {
1496
- throw new Error(`could not turn autoupdate off: ${firstLine(result.stdout || result.stderr) || `exit ${result.code}`}`);
1650
+ throw new Error(`Could not turn auto-update off: ${firstLine(result.stdout || result.stderr) || `exit ${result.code}`}`);
1497
1651
  }
1652
+ rmSync4(join5(options.stateDir ?? defaultProductDir(config.product), HIDDEN_SCRIPT), { force: true });
1498
1653
  return;
1499
1654
  }
1500
1655
  if (platform === "darwin") {
@@ -1587,8 +1742,163 @@ function firstLine(text) {
1587
1742
  return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
1588
1743
  }
1589
1744
 
1745
+ // src/refresh.ts
1746
+ import { execFile } from "node:child_process";
1747
+ import { chmodSync, existsSync as existsSync7, lstatSync, readdirSync, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
1748
+ import { homedir } from "node:os";
1749
+ import { basename, dirname as dirname3, join as join6 } from "node:path";
1750
+ var LAUNCHER_RECORD_FILE = "launcher.json";
1751
+ var RECORD_TIMEOUT_MS = 1e4;
1752
+ function platformKey(platform, arch) {
1753
+ if (platform === "win32") return arch === "x64" || arch === "arm64" ? `win-${arch}` : null;
1754
+ return platform === "darwin" && arch === "arm64" ? "darwin-arm64" : null;
1755
+ }
1756
+ function verifierFor(target) {
1757
+ return { product: target.product, host: target.host, loginKind: "github", publicKey: target.key, binName: target.bin };
1758
+ }
1759
+ async function fetchRecord(target, options) {
1760
+ let json;
1761
+ try {
1762
+ const response = await (options.fetch ?? fetch)(
1763
+ `${target.host.replace(/\/+$/u, "")}/release/launcher`,
1764
+ { signal: AbortSignal.timeout(options.timeoutMs ?? RECORD_TIMEOUT_MS) }
1765
+ );
1766
+ if (!response.ok) return null;
1767
+ json = await response.json();
1768
+ } catch {
1769
+ return null;
1770
+ }
1771
+ let record;
1772
+ try {
1773
+ record = parseManifest(json);
1774
+ } catch {
1775
+ return null;
1776
+ }
1777
+ if (!verifyManifest(record, record.signature, verifierFor(target))) return "bad-signature";
1778
+ return { version: record.version, files: record.files };
1779
+ }
1780
+ async function readLauncherRecord(target, options = {}) {
1781
+ const record = await fetchRecord(target, options);
1782
+ return record === "bad-signature" ? null : record;
1783
+ }
1784
+ function compareVersions(a, b) {
1785
+ const split = (v) => {
1786
+ const [core, pre = ""] = v.split("-", 2);
1787
+ return { nums: core.split(".").map((n) => Number.parseInt(n, 10) || 0), pre };
1788
+ };
1789
+ const x = split(a);
1790
+ const y = split(b);
1791
+ 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;
1792
+ if (x.pre === y.pre) return 0;
1793
+ if (!x.pre) return 1;
1794
+ if (!y.pre) return -1;
1795
+ return x.pre > y.pre ? 1 : -1;
1796
+ }
1797
+ function recordedPath(productDir) {
1798
+ try {
1799
+ const saved = JSON.parse(readFileSync7(join6(productDir, LAUNCHER_RECORD_FILE), "utf8"));
1800
+ return typeof saved.path === "string" && saved.path ? saved.path : null;
1801
+ } catch {
1802
+ return null;
1803
+ }
1804
+ }
1805
+ function installedPath(target, productDir, platform) {
1806
+ return platform === "win32" ? join6(productDir, "bin", `${target.bin}.exe`) : join6(homedir(), ".local", "bin", target.bin);
1807
+ }
1808
+ function parseLauncherVersion(stdout, bin) {
1809
+ const text = String(stdout).trim();
1810
+ if (!text.startsWith(`${bin} `)) return null;
1811
+ return text.match(/launcher (\d+\.\d+\.\d+[^\s)]*)/u)?.[1] ?? null;
1812
+ }
1813
+ function runVersion(path, bin) {
1814
+ return new Promise((resolve3) => {
1815
+ execFile(path, ["--version"], { timeout: 3e4, windowsHide: true, encoding: "utf8" }, (error, stdout) => {
1816
+ resolve3(error ? null : parseLauncherVersion(stdout, bin));
1817
+ });
1818
+ });
1819
+ }
1820
+ function removeStale(file) {
1821
+ const prefix = `${basename(file)}.old-`;
1822
+ let names;
1823
+ try {
1824
+ names = readdirSync(dirname3(file));
1825
+ } catch {
1826
+ return;
1827
+ }
1828
+ for (const name of names) {
1829
+ if (!name.startsWith(prefix)) continue;
1830
+ try {
1831
+ rmSync5(join6(dirname3(file), name), { force: true });
1832
+ } catch {
1833
+ }
1834
+ }
1835
+ }
1836
+ async function refreshLauncher(target, options = {}) {
1837
+ const started = Date.now();
1838
+ const platform = options.platform ?? process.platform;
1839
+ const key = platformKey(platform, options.arch ?? process.arch);
1840
+ if (!key) return null;
1841
+ const productDir = options.productDir ?? defaultProductDir(target.product);
1842
+ const recorded = recordedPath(productDir);
1843
+ const file = options.path ?? (recorded && existsSync7(recorded) ? recorded : installedPath(target, productDir, platform));
1844
+ if (!existsSync7(file) || lstatSync(file).isSymbolicLink()) return null;
1845
+ if (!options.dryRun) removeStale(file);
1846
+ const from = await (options.readVersion ?? ((path) => runVersion(path, target.bin)))(file);
1847
+ if (!from) return null;
1848
+ const arm = (verdict, to, detail) => ({ surface: "launcher", from, to, verdict, detail, durationMs: Date.now() - started });
1849
+ const record = await fetchRecord(target, options);
1850
+ if (record === "bad-signature") return arm("fail", from, "The launcher record failed its signature check.");
1851
+ const entry2 = record?.files.find((candidate) => candidate.path === key);
1852
+ if (!record || !entry2) return arm("skip", from, "No launcher update is offered yet.");
1853
+ if (compareVersions(record.version, from) <= 0) return arm("ok", from, "");
1854
+ if (options.dryRun) return arm("ok", record.version, "");
1855
+ let unlock;
1856
+ if (!options.lockHeld) {
1857
+ try {
1858
+ unlock = lockInstallation(productDir);
1859
+ } catch {
1860
+ return arm("defer", from, "Another install is running; the launcher updates next time.");
1861
+ }
1862
+ }
1863
+ const next = `${file}.new-${process.pid}`;
1864
+ let moved;
1865
+ try {
1866
+ let bytes;
1867
+ try {
1868
+ const response = await (options.fetch ?? fetch)(
1869
+ `${(target.dl ?? target.host).replace(/\/+$/u, "")}/dl/${target.product}/${key}`,
1870
+ { signal: AbortSignal.timeout(3e5) }
1871
+ );
1872
+ if (!response.ok) return arm("defer", from, "The launcher download did not complete; it retries next time.");
1873
+ bytes = await readDownload(response, void 0, entry2.size);
1874
+ } catch {
1875
+ return arm("defer", from, "The launcher download did not complete; it retries next time.");
1876
+ }
1877
+ if (!verifyFileBytes(entry2, bytes)) return arm("fail", from, "The downloaded launcher failed its checksum.");
1878
+ try {
1879
+ writeFileSync7(next, bytes, { mode: 493 });
1880
+ if (platform !== "win32") chmodSync(next, 493);
1881
+ if (platform === "win32") {
1882
+ moved = `${file}.old-${process.pid}-${Date.now()}`;
1883
+ renameSync4(file, moved);
1884
+ }
1885
+ renameSync4(next, file);
1886
+ } catch {
1887
+ try {
1888
+ if (moved && !existsSync7(file)) renameSync4(moved, file);
1889
+ } catch {
1890
+ }
1891
+ rmSync5(next, { force: true });
1892
+ return arm("fail", from, "The launcher could not be replaced.");
1893
+ }
1894
+ return arm("ok", record.version, "");
1895
+ } finally {
1896
+ unlock?.();
1897
+ }
1898
+ }
1899
+
1590
1900
  // src/login-github.ts
1591
- import { spawn } from "node:child_process";
1901
+ import { spawn, spawnSync as spawnSync3 } from "node:child_process";
1592
1902
  var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
1593
1903
  function openBrowser(url) {
1594
1904
  if (process.env.LAUNCHER_NO_OPEN === "1") return;
@@ -1705,7 +2015,33 @@ async function refreshAccessToken(config, refreshToken, fetchImpl = fetch) {
1705
2015
  if (typeof data.access_token !== "string" || typeof data.expires_in !== "number") {
1706
2016
  throw new Error("the sign-in server returned a malformed refresh");
1707
2017
  }
1708
- return { accessToken: data.access_token, expiresIn: data.expires_in };
2018
+ return {
2019
+ accessToken: data.access_token,
2020
+ expiresIn: data.expires_in,
2021
+ ...typeof data.refresh_token === "string" ? { refreshToken: data.refresh_token } : {}
2022
+ };
2023
+ }
2024
+ function readGhToken() {
2025
+ try {
2026
+ const result = spawnSync3("gh", ["auth", "token", "--hostname", "github.com"], { encoding: "utf8", timeout: 1e4, windowsHide: true });
2027
+ const token = result.status === 0 ? String(result.stdout).trim() : "";
2028
+ return token && !/\s/u.test(token) ? token : null;
2029
+ } catch {
2030
+ return null;
2031
+ }
2032
+ }
2033
+ async function loginWithGhToken(config, fetchImpl = fetch, readToken = readGhToken) {
2034
+ const token = readToken();
2035
+ if (!token) return null;
2036
+ try {
2037
+ const response = await postJson(`${config.host}/gate/github-token`, { token }, fetchImpl);
2038
+ if (!response.ok) return null;
2039
+ const data = await response.json();
2040
+ if (typeof data.access_token !== "string" || typeof data.refresh_token !== "string" || typeof data.expires_in !== "number") return null;
2041
+ return { accessToken: data.access_token, refreshToken: data.refresh_token, expiresIn: data.expires_in };
2042
+ } catch {
2043
+ return null;
2044
+ }
1709
2045
  }
1710
2046
 
1711
2047
  // src/login-google.ts
@@ -1822,7 +2158,7 @@ function page(title, body) {
1822
2158
  }
1823
2159
 
1824
2160
  // src/index.ts
1825
- var LAUNCHER_VERSION = true ? "1.0.1" : readVersionFromPackage();
2161
+ var LAUNCHER_VERSION = true ? "1.2.0" : readVersionFromPackage();
1826
2162
  function defaultPrint(message) {
1827
2163
  process.stdout.write(`${message}
1828
2164
  `);
@@ -1831,79 +2167,192 @@ function defaultPrintErr(message) {
1831
2167
  process.stderr.write(`${message}
1832
2168
  `);
1833
2169
  }
2170
+ var LAUNCHER_VERBS = /* @__PURE__ */ new Set(["install", "update", "status", "doctor", "login", "logout", "autoupdate"]);
2171
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["--config", "--dir"]);
2172
+ var SWITCHES = {
2173
+ "--json": "json",
2174
+ "--no-color": "noColor",
2175
+ "--dry-run": "dryRun",
2176
+ "--help": "help",
2177
+ "-h": "help",
2178
+ "--version": "version",
2179
+ "-v": "version"
2180
+ };
2181
+ function parseArgv(argv2) {
2182
+ const parsed = { flags: {}, args: [], rest: [] };
2183
+ const flag = (i2) => {
2184
+ const arg = argv2[i2];
2185
+ if (VALUE_FLAGS.has(arg)) {
2186
+ const value = argv2[i2 + 1];
2187
+ if (value === void 0 || value.startsWith("-")) {
2188
+ parsed.unknownFlag ??= arg;
2189
+ return i2 + 1;
2190
+ }
2191
+ parsed.flags[arg === "--config" ? "config" : "dir"] = value;
2192
+ return i2 + 2;
2193
+ }
2194
+ const name = SWITCHES[arg];
2195
+ if (!name) return -1;
2196
+ parsed.flags[name] = true;
2197
+ return i2 + 1;
2198
+ };
2199
+ const skip = (i2) => {
2200
+ const next = flag(i2);
2201
+ if (next >= 0) return next;
2202
+ parsed.unknownFlag ??= argv2[i2];
2203
+ return i2 + 1;
2204
+ };
2205
+ let i = 0;
2206
+ while (i < argv2.length && argv2[i].startsWith("-")) {
2207
+ if (argv2[i] === "--run") {
2208
+ parsed.run = { file: argv2[i + 1], args: argv2.slice(i + 2) };
2209
+ return parsed;
2210
+ }
2211
+ i = skip(i);
2212
+ }
2213
+ parsed.command = argv2[i];
2214
+ if (parsed.command === void 0) return parsed;
2215
+ if (!LAUNCHER_VERBS.has(parsed.command)) {
2216
+ parsed.rest = argv2.slice(i + 1);
2217
+ return parsed;
2218
+ }
2219
+ for (i += 1; i < argv2.length; ) {
2220
+ if (argv2[i].startsWith("-")) i = skip(i);
2221
+ else parsed.args.push(argv2[i++]);
2222
+ }
2223
+ return parsed;
2224
+ }
2225
+ var NO_PRODUCT = "mmi-hub";
2226
+ function sentence(message) {
2227
+ const text = message.trim().replace(/\s+—\s+/gu, ". ");
2228
+ if (!text) return "Something went wrong.";
2229
+ return `${text[0].toUpperCase()}${text.slice(1)}${/[.!?]$/u.test(text) ? "" : "."}`;
2230
+ }
2231
+ function screensFor(product, bin, render, options) {
2232
+ const show = (lines, stream = "stdout") => {
2233
+ const sink = stream === "stderr" ? options.printErr : options.print;
2234
+ if (sink) for (const line of lines) sink(line);
2235
+ else printScreen(lines, stream);
2236
+ };
2237
+ return {
2238
+ product,
2239
+ bin,
2240
+ render,
2241
+ show,
2242
+ notice: (state, message, next) => show(renderNotice(product, { state, message, ...next ? { next } : {} }, render)),
2243
+ refuse: (message, next, code = 1) => {
2244
+ show(renderNotice(product, { state: "fail", message, ...next ? { next } : {} }, render), "stderr");
2245
+ return code;
2246
+ }
2247
+ };
2248
+ }
1834
2249
  async function run(rawOptions = {}) {
1835
2250
  const print = rawOptions.print ?? defaultPrint;
1836
2251
  const printErr = rawOptions.printErr ?? defaultPrintErr;
1837
- const argv2 = rawOptions.argv ?? process.argv.slice(2);
1838
- const runAt = argv2.indexOf("--run");
1839
- if (runAt >= 0) return runFile(argv2[runAt + 1], argv2.slice(runAt + 2), printErr);
2252
+ const parsed = parseArgv(rawOptions.argv ?? process.argv.slice(2));
2253
+ if (parsed.run) return runFile(parsed.run.file, parsed.run.args, printErr);
2254
+ const render = {
2255
+ ...rawOptions.tty === void 0 ? {} : { tty: rawOptions.tty },
2256
+ ...parsed.flags.noColor ? { color: false } : {}
2257
+ };
1840
2258
  let config;
1841
2259
  try {
1842
- config = loadProductConfig({ configPath: rawOptions.configPath ?? flagValue(argv2, "--config") });
2260
+ config = loadProductConfig({ configPath: rawOptions.configPath ?? parsed.flags.config });
1843
2261
  } catch (error) {
1844
- print(`cannot start: ${error.message}`);
1845
- return 2;
1846
- }
1847
- const dir = resolveProductDir(config.product, rawOptions.dir ?? flagValue(argv2, "--dir"));
1848
- const positional = argv2.filter((arg) => !arg.startsWith("-"));
1849
- if (argv2.includes("--version") || argv2.includes("-v")) {
1850
- print(`${config.binName} launcher ${LAUNCHER_VERSION}`);
2262
+ return screensFor(NO_PRODUCT, "", { ...render, color: false }, rawOptions).refuse(`The launcher cannot start: ${sentence(error.message)}`, void 0, 2);
2263
+ }
2264
+ const dir = resolveProductDir(config.product, rawOptions.dir ?? parsed.flags.dir);
2265
+ const ui = screensFor(faceProduct(config), config.binName, render, rawOptions);
2266
+ if ((parsed.command === "install" || parsed.command === "update") && !parsed.flags.version) recordSelf(dir);
2267
+ if (parsed.unknownFlag) return ui.refuse(`Unknown option: ${parsed.unknownFlag}.`, `${ui.bin} --help`, 2);
2268
+ if (parsed.flags.version) {
2269
+ print(renderVersion(ui.bin, readState(dir)?.version ?? "not installed", LAUNCHER_VERSION));
1851
2270
  return 0;
1852
2271
  }
1853
- if (argv2.includes("--help") || argv2.includes("-h") || positional.length === 0) {
1854
- printUsage(config, print);
1855
- return positional.length === 0 ? 2 : 0;
2272
+ const command2 = parsed.command;
2273
+ if (parsed.flags.help || command2 === void 0) {
2274
+ ui.show(usage(config, dir, render));
2275
+ return parsed.flags.help ? 0 : 2;
1856
2276
  }
1857
- const command2 = positional[0];
2277
+ const mutating = ["install", "login", "logout"].includes(command2) || command2 === "update" && !parsed.flags.dryRun || command2 === "autoupdate" && ["on", "off"].includes(parsed.args[0] ?? "");
1858
2278
  let unlock;
1859
2279
  try {
1860
- unlock = lockInstallation(dir);
1861
- await recoverAcquisition(dir, async (command3, cwd, env) => {
1862
- const result = (rawOptions.runEntry ?? defaultRunEntry)(command3, cwd, env);
1863
- return result.ok && (result.code === void 0 || result.code === 0);
1864
- }, payloadEnv(dir) ?? process.env);
2280
+ if (mutating) {
2281
+ unlock = lockInstallation(dir);
2282
+ await recoverAcquisition(dir, async (command3, cwd, env) => {
2283
+ const result = (rawOptions.runEntry ?? defaultRunEntry)(command3, cwd, env);
2284
+ return result.ok && (result.code === void 0 || result.code === 0);
2285
+ }, payloadEnv(dir) ?? process.env);
2286
+ }
1865
2287
  switch (command2) {
1866
2288
  case "login":
1867
- unlock();
1868
- await doLogin(config, dir, rawOptions, print);
2289
+ unlock?.();
2290
+ await doLogin(config, dir, rawOptions, (line) => ui.notice("note", line));
2291
+ ui.notice("ok", `Signed in to ${identityFor(ui.product).name}.`, `${ui.bin} install`);
1869
2292
  return 0;
1870
2293
  case "logout":
1871
2294
  wipeProductDir(dir);
1872
- print(`signed out of ${config.product} and removed the downloaded payload.`);
2295
+ ui.notice("ok", `Signed out of ${identityFor(ui.product).name} and removed the downloaded payload.`, `${ui.bin} login`);
1873
2296
  return 0;
1874
2297
  case "install":
1875
- return await doInstall(config, dir, rawOptions, print);
2298
+ return await doInstall(config, dir, rawOptions, print, parsed.flags);
1876
2299
  case "update":
1877
- return await doUpdate(config, dir, rawOptions, print);
2300
+ return await doUpdate(config, dir, rawOptions, print, parsed.flags);
2301
+ case "status":
2302
+ return await doStatus(config, dir, rawOptions, ui, print, Boolean(parsed.flags.json));
1878
2303
  case "doctor":
1879
- return doDoctor(config, dir, rawOptions, print, unlock);
2304
+ return doDoctor(config, dir, rawOptions, ui);
1880
2305
  case "autoupdate":
1881
- return doAutoupdate(config, rawOptions, positional.slice(1), print);
2306
+ return doAutoupdate(config, dir, rawOptions, parsed.args, ui);
1882
2307
  default:
1883
- return doForward(config, dir, rawOptions, command2, argv2, print, unlock);
2308
+ return doForward(config, dir, rawOptions, command2, parsed.rest, ui, () => unlock?.());
1884
2309
  }
1885
2310
  } catch (error) {
1886
- if (error instanceof NeedsLoginError) {
1887
- print("you are signed out \u2014 run login first.");
1888
- return 3;
2311
+ if (error instanceof NeedsLoginError) return ui.refuse("You are signed out.", `${ui.bin} login`, 3);
2312
+ const message = error.message;
2313
+ if (/another installer is running|lock recovery is already pending/u.test(message)) {
2314
+ return ui.refuse("Another install or update is running.", `${ui.bin} status`);
1889
2315
  }
1890
- print(`failed: ${error.message}`);
1891
- return 1;
2316
+ return ui.refuse(sentence(message), `${ui.bin} ${command2}`);
1892
2317
  } finally {
1893
2318
  unlock?.();
1894
2319
  }
1895
2320
  }
1896
- function flagValue(argv2, flag) {
1897
- const index = argv2.indexOf(flag);
1898
- if (index < 0) return void 0;
1899
- const value = argv2[index + 1];
1900
- return value && !value.startsWith("-") ? value : void 0;
2321
+ function recordSelf(dir) {
2322
+ if (!isSea()) return;
2323
+ try {
2324
+ mkdirSync6(dir, { recursive: true });
2325
+ writeFileSync8(join7(dir, LAUNCHER_RECORD_FILE), `${JSON.stringify({ path: process.execPath, version: LAUNCHER_VERSION })}
2326
+ `);
2327
+ } catch {
2328
+ }
1901
2329
  }
1902
- function printUsage(config, print) {
1903
- print(`${config.binName} launcher ${LAUNCHER_VERSION} \u2014 sign in and install ${config.product}.`);
1904
- print("usage: launcher <login|logout|install|update|doctor|autoupdate on|off|status> [--config <path>] [--dir <path>]");
1905
- print(" launcher --run <file> [args\u2026] (run a payload file with the embedded runtime)");
1906
- print(" launcher <verb> [args\u2026] (forwarded to the payload when it declares run + verbs)");
2330
+ function launcherTarget(config) {
2331
+ return { product: config.product, bin: config.binName, key: config.publicKey, host: config.host, ...config.dl ? { dl: config.dl } : {} };
2332
+ }
2333
+ function usage(config, dir, render) {
2334
+ const name = config.product;
2335
+ return renderUsage(faceProduct(config), {
2336
+ bin: config.binName,
2337
+ version: `${readState(dir)?.version ?? "not installed"} (launcher ${LAUNCHER_VERSION})`,
2338
+ summary: `Sign in, install and update ${name}.`,
2339
+ commands: [
2340
+ { usage: "install", text: `Sign in and install ${name}.` },
2341
+ { usage: "update [--dry-run]", text: `Update ${name} and this launcher.` },
2342
+ { usage: "status", text: `Show versions, sign-in and ${AUTO_UPDATE}.` },
2343
+ { usage: "doctor", text: `Check this machine, then run the ${name} doctor.` },
2344
+ { usage: "login", text: "Sign in." },
2345
+ { usage: "logout", text: "Sign out and remove the downloaded payload." },
2346
+ { usage: "autoupdate on|off|status", text: `Turn hourly ${AUTO_UPDATE} on or off, or show it.` },
2347
+ { usage: "<verb> ...", text: `Passed to ${name} when it declares it.` }
2348
+ ],
2349
+ options: [
2350
+ { usage: "--json", text: "Print status as JSON." },
2351
+ { usage: "--no-color", text: "Print without colour." },
2352
+ { usage: "--config <path>", text: "Use this product config." },
2353
+ { usage: "--dir <path>", text: "Use this folder for sign-in and downloads." }
2354
+ ]
2355
+ }, render);
1907
2356
  }
1908
2357
  async function runFile(file, args, printErr) {
1909
2358
  if (!file) {
@@ -1911,7 +2360,7 @@ async function runFile(file, args, printErr) {
1911
2360
  return 1;
1912
2361
  }
1913
2362
  const abs = resolve2(process.cwd(), file);
1914
- if (!existsSync7(abs)) {
2363
+ if (!existsSync8(abs)) {
1915
2364
  printErr(`cannot run ${file}: no such file.`);
1916
2365
  return 1;
1917
2366
  }
@@ -1945,7 +2394,7 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
1945
2394
  }
1946
2395
  writeTokens(dir, {
1947
2396
  accessToken: refreshed2.accessToken,
1948
- refreshToken: stored.refreshToken,
2397
+ refreshToken: refreshed2.refreshToken ?? stored.refreshToken,
1949
2398
  expiresAt: Date.now() + refreshed2.expiresIn * 1e3
1950
2399
  });
1951
2400
  return refreshed2.accessToken;
@@ -1967,15 +2416,14 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
1967
2416
  });
1968
2417
  return refreshed.accessToken;
1969
2418
  }
1970
- async function doLogin(config, dir, options, print, installer) {
2419
+ async function doLogin(config, dir, options, prompt, installer) {
1971
2420
  const fetchImpl = options.fetchImpl ?? fetch;
1972
2421
  const open = options.open ?? openBrowser;
1973
2422
  if (config.loginKind === "github") {
1974
2423
  const tokens = await loginGithub(config, {
1975
2424
  fetchImpl,
1976
2425
  open,
1977
- print,
1978
- ...installer ? { onDeviceCode: (prompt) => installer.signIn(prompt) } : {}
2426
+ onDeviceCode: (grant) => installer ? installer.signIn(grant) : prompt(`To sign in, open ${grant.url} and enter the code ${grant.code}.`)
1979
2427
  });
1980
2428
  writeTokens(dir, {
1981
2429
  accessToken: tokens.accessToken,
@@ -1983,7 +2431,12 @@ async function doLogin(config, dir, options, print, installer) {
1983
2431
  expiresAt: Date.now() + tokens.expiresIn * 1e3
1984
2432
  });
1985
2433
  } else {
1986
- const tokens = await loginGoogle({ host: config.host, open, fetchImpl });
2434
+ const tokens = await loginGoogle({
2435
+ host: config.host,
2436
+ open,
2437
+ fetchImpl,
2438
+ print: (line) => installer ? installer.relay(line) : prompt(line)
2439
+ });
1987
2440
  writeTokens(dir, {
1988
2441
  accessToken: tokens.accessToken,
1989
2442
  refreshToken: tokens.refreshToken,
@@ -1991,20 +2444,26 @@ async function doLogin(config, dir, options, print, installer) {
1991
2444
  clientId: tokens.clientId
1992
2445
  });
1993
2446
  }
1994
- if (!installer) print(`signed in to ${config.product}.`);
1995
2447
  }
1996
- async function fetchAccessTokenOrLogin(config, dir, options, print, installer) {
2448
+ async function fetchAccessTokenOrLogin(config, dir, options, installer) {
1997
2449
  const fetchImpl = options.fetchImpl ?? fetch;
1998
2450
  const fresh = await ensureFreshToken(config, dir, fetchImpl);
1999
2451
  if (fresh) return fresh;
2000
- await doLogin(config, dir, options, print, installer);
2452
+ if (config.loginKind === "github") {
2453
+ const reused = await loginWithGhToken(config, fetchImpl, options.readGhToken ?? (isSea() ? readGhToken : () => null));
2454
+ if (reused) {
2455
+ writeTokens(dir, { accessToken: reused.accessToken, refreshToken: reused.refreshToken, expiresAt: Date.now() + reused.expiresIn * 1e3 });
2456
+ return reused.accessToken;
2457
+ }
2458
+ }
2459
+ await doLogin(config, dir, options, (line) => installer.relay(line), installer);
2001
2460
  const after = readTokens(dir);
2002
2461
  if (!after) throw new Error("sign-in did not produce a token");
2003
2462
  return after.accessToken;
2004
2463
  }
2005
2464
  function readPayloadArgv(dir, key) {
2006
2465
  try {
2007
- const parsed = JSON.parse(readFileSync7(join6(payloadDir(dir), "payload.json"), "utf8"));
2466
+ const parsed = JSON.parse(readFileSync8(join7(payloadDir(dir), "payload.json"), "utf8"));
2008
2467
  const entry2 = parsed[key];
2009
2468
  if (typeof entry2 === "string") {
2010
2469
  const parts = entry2.trim().split(/\s+/).filter(Boolean);
@@ -2026,7 +2485,7 @@ function readPayloadRun(dir) {
2026
2485
  }
2027
2486
  function readPayloadVerbs(dir) {
2028
2487
  try {
2029
- const parsed = JSON.parse(readFileSync7(join6(payloadDir(dir), "payload.json"), "utf8"));
2488
+ const parsed = JSON.parse(readFileSync8(join7(payloadDir(dir), "payload.json"), "utf8"));
2030
2489
  const verbs = parsed.verbs;
2031
2490
  if (verbs === "*") return "*";
2032
2491
  if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
@@ -2043,7 +2502,7 @@ function resolveEntry(entry2) {
2043
2502
  function needsShell(command2) {
2044
2503
  if (process.platform !== "win32") return false;
2045
2504
  if (/\.(cmd|bat)$/i.test(command2)) return true;
2046
- return !existsSync7(command2);
2505
+ return !existsSync8(command2);
2047
2506
  }
2048
2507
  function quoteForShell(arg) {
2049
2508
  return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
@@ -2075,7 +2534,7 @@ function defaultRunEntry(entry2, cwd, env) {
2075
2534
  const [command2, ...args] = entry2;
2076
2535
  const shell = needsShell(command2);
2077
2536
  const commandLine = shell ? [command2, ...args].map(quoteForShell).join(" ") : command2;
2078
- const spawnEntry = (progress2) => spawnSync3(commandLine, shell ? [] : args, {
2537
+ const spawnEntry = (progress2) => spawnSync4(commandLine, shell ? [] : args, {
2079
2538
  cwd,
2080
2539
  stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
2081
2540
  shell,
@@ -2109,8 +2568,16 @@ function faceProduct(config) {
2109
2568
  function formatMb(bytes) {
2110
2569
  return (bytes / 1048576).toFixed(1).replace(/\.0$/, "");
2111
2570
  }
2112
- async function installOrUpdate(config, dir, options, print, update) {
2571
+ async function installOrUpdate(config, dir, options, print, update, flags2) {
2572
+ let launcherFailed = false;
2573
+ const code = await installOrUpdateRun(config, dir, options, print, update, flags2, () => {
2574
+ launcherFailed = true;
2575
+ });
2576
+ return launcherFailed ? Math.max(code, 1) : code;
2577
+ }
2578
+ async function installOrUpdateRun(config, dir, options, print, update, flags2, markLauncherFailed) {
2113
2579
  const product = faceProduct(config);
2580
+ const dryRun = update && Boolean(flags2.dryRun);
2114
2581
  const installer = createInstallerRun({
2115
2582
  product,
2116
2583
  gate: config.host,
@@ -2120,17 +2587,55 @@ async function installOrUpdate(config, dir, options, print, update) {
2120
2587
  operation: update ? "update" : "install",
2121
2588
  tty: options.tty,
2122
2589
  animate: !options.print && Boolean(process.stderr.isTTY),
2590
+ dryRun,
2591
+ ...flags2.noColor ? { color: false } : {},
2123
2592
  ...options.print ? { write: (text) => print(text.replace(/\n$/, "")) } : {}
2124
2593
  });
2125
2594
  const fetchImpl = options.fetchImpl ?? fetch;
2126
2595
  let version = readState(dir)?.version ?? "unknown";
2127
2596
  installer.start();
2128
2597
  try {
2598
+ const refresh = options.refresh ?? (isSea() ? refreshLauncher : void 0);
2599
+ const arm = refresh ? await refresh(launcherTarget(config), {
2600
+ lockHeld: true,
2601
+ path: process.execPath,
2602
+ dryRun,
2603
+ productDir: dir,
2604
+ fetch: fetchImpl,
2605
+ readVersion: async () => LAUNCHER_VERSION
2606
+ }) : null;
2607
+ if (arm) {
2608
+ installer.surface({
2609
+ id: LAUNCHER_SURFACE,
2610
+ from: arm.from,
2611
+ to: arm.to,
2612
+ state: armState(arm),
2613
+ ...arm.durationMs === void 0 ? {} : { seconds: arm.durationMs / 1e3 },
2614
+ ...arm.verdict !== "ok" && arm.detail ? { detail: arm.detail } : {}
2615
+ });
2616
+ if (arm.verdict === "fail") {
2617
+ const finish = installer.finish;
2618
+ installer.finish = (facts) => finish({
2619
+ ...facts,
2620
+ operationFailed: true,
2621
+ detail: facts.detail ?? "The launcher could not update itself.",
2622
+ retry: facts.retry ?? `${config.binName} update`
2623
+ });
2624
+ markLauncherFailed();
2625
+ }
2626
+ }
2129
2627
  const current = readState(dir);
2130
- const payloadPresent = existsSync7(payloadDir(dir));
2628
+ const payloadPresent = existsSync8(payloadDir(dir));
2131
2629
  let started = Date.now();
2132
2630
  installer.phase("resolve", { state: "running" });
2133
2631
  const pointer = await fetchReleaseVersion(config, fetchImpl);
2632
+ if (dryRun) {
2633
+ installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
2634
+ const installed = payloadPresent ? current?.version : void 0;
2635
+ if (pointer) installer.surface({ id: config.product, from: installed, to: pointer, state: installed === pointer ? "current" : "updated" });
2636
+ installer.finish({ ...pointer ? { version: pointer } : {}, total: 1, updated: pointer && installed !== pointer ? 1 : 0, failed: 0, dryRun: true });
2637
+ return 0;
2638
+ }
2134
2639
  if (pointer && current?.version === pointer && payloadPresent) {
2135
2640
  version = pointer;
2136
2641
  installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
@@ -2147,14 +2652,14 @@ async function installOrUpdate(config, dir, options, print, update) {
2147
2652
  }
2148
2653
  installer.phase("sign-in", { state: "running" });
2149
2654
  started = Date.now();
2150
- const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print, installer);
2655
+ const accessToken = await fetchAccessTokenOrLogin(config, dir, options, installer);
2151
2656
  installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
2152
2657
  installer.phase("resolve", { state: "running" });
2153
2658
  started = Date.now();
2154
2659
  const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
2155
2660
  version = manifest.version;
2156
2661
  installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
2157
- const unchanged = current?.version === version && existsSync7(payloadDir(dir));
2662
+ const unchanged = current?.version === version && existsSync8(payloadDir(dir));
2158
2663
  if (unchanged && current.acquired) {
2159
2664
  installer.surface({ id: config.product, from: version, to: version, state: "current" });
2160
2665
  return await finishLastMile(config, dir, options, installer, version, true, acquisitionRepairCommand(dir, version));
@@ -2164,14 +2669,14 @@ async function installOrUpdate(config, dir, options, print, update) {
2164
2669
  started = Date.now();
2165
2670
  const cached = reusableCandidate(dir, manifest);
2166
2671
  const candidate = cached ?? randomUUID4();
2167
- const candidateDir = join6(dir, "candidates", candidate);
2672
+ const candidateDir = join7(dir, "candidates", candidate);
2168
2673
  const onProgress = ({ done, total }) => installer.phase("download", {
2169
2674
  state: "running",
2170
2675
  measure: total ? `${Math.floor(done / total * 100)}% \xB7 ${formatMb(done)} of ${formatMb(total)} MB` : `${formatMb(done)} MB`,
2171
2676
  ...total ? { runningTitle: `Downloading ${config.product} ${version} (${formatMb(total)} MB)` } : {}
2172
2677
  });
2173
2678
  if (!cached) await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl, onProgress, cacheDir: dir });
2174
- const acquisition = existsSync7(join6(candidateDir, "payload", "payload.json")) ? readAcquisition(join6(candidateDir, "payload")) : null;
2679
+ const acquisition = existsSync8(join7(candidateDir, "payload", "payload.json")) ? readAcquisition(join7(candidateDir, "payload")) : null;
2175
2680
  if (acquisition) {
2176
2681
  rememberCandidate(dir, candidate, manifest);
2177
2682
  installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
@@ -2194,10 +2699,10 @@ async function installOrUpdate(config, dir, options, print, update) {
2194
2699
  installer.finish(outcome ?? { version, total: 1, updated: 1, failed: 0, installed: true });
2195
2700
  return 0;
2196
2701
  }
2197
- const target = join6(dir, "payload");
2702
+ const target = join7(dir, "payload");
2198
2703
  mkdirSync6(dir, { recursive: true });
2199
- rmSync5(target, { recursive: true, force: true });
2200
- renameSync4(join6(candidateDir, "payload"), target);
2704
+ rmSync6(target, { recursive: true, force: true });
2705
+ renameSync5(join7(candidateDir, "payload"), target);
2201
2706
  writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
2202
2707
  installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
2203
2708
  }
@@ -2223,11 +2728,11 @@ async function installOrUpdate(config, dir, options, print, update) {
2223
2728
  installer.stop();
2224
2729
  }
2225
2730
  }
2226
- function doInstall(config, dir, options, print) {
2227
- return installOrUpdate(config, dir, options, print, false);
2731
+ function doInstall(config, dir, options, print, flags2) {
2732
+ return installOrUpdate(config, dir, options, print, false, flags2);
2228
2733
  }
2229
- function doUpdate(config, dir, options, print) {
2230
- return installOrUpdate(config, dir, options, print, true);
2734
+ function doUpdate(config, dir, options, print, flags2) {
2735
+ return installOrUpdate(config, dir, options, print, true, flags2);
2231
2736
  }
2232
2737
  function payloadEnv(dir) {
2233
2738
  const stored = readTokens(dir);
@@ -2239,8 +2744,8 @@ function payloadFileAsCommand(entry2, payload) {
2239
2744
  const first = entry2[0];
2240
2745
  if (!first || first === "$self") return null;
2241
2746
  if (first.includes("/") || first.includes("\\")) return null;
2242
- const candidate = join6(payload, first);
2243
- if (!existsSync7(candidate)) return null;
2747
+ const candidate = join7(payload, first);
2748
+ if (!existsSync8(candidate)) return null;
2244
2749
  if (NEVER_A_PROGRAM.test(first)) return first;
2245
2750
  if (process.platform === "win32") return null;
2246
2751
  try {
@@ -2259,7 +2764,7 @@ async function finishLastMile(config, dir, options, installer, version, unchange
2259
2764
  updated: unchanged ? 0 : 1,
2260
2765
  failed: 0,
2261
2766
  installed: true,
2262
- detail: `next step: run ${join6(payload, config.binName)} to start ${config.product}.`
2767
+ detail: `next step: run ${join7(payload, config.binName)} to start ${config.product}.`
2263
2768
  });
2264
2769
  return 0;
2265
2770
  }
@@ -2305,9 +2810,9 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2305
2810
  const [command2, ...args] = entry2;
2306
2811
  const shell = needsShell(command2);
2307
2812
  const progress = !(process.platform === "win32" && shell);
2308
- const outcomeDir = mkdtempSync2(join6(tmpdir4(), "mm-installer-outcome-"));
2309
- const outcomeFile = join6(outcomeDir, "outcome.json");
2310
- const progressFile = join6(outcomeDir, "progress.jsonl");
2813
+ const outcomeDir = mkdtempSync2(join7(tmpdir4(), "mm-installer-outcome-"));
2814
+ const outcomeFile = join7(outcomeDir, "outcome.json");
2815
+ const progressFile = join7(outcomeDir, "progress.jsonl");
2311
2816
  const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
2312
2817
  delete childEnv.MM_FACE_TRANSCRIPT;
2313
2818
  delete childEnv.MM_PROGRESS_FILE;
@@ -2357,8 +2862,8 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2357
2862
  const pollProgress = () => {
2358
2863
  if (progressError) return;
2359
2864
  try {
2360
- if (!existsSync7(progressFile)) return;
2361
- const bytes = readFileSync7(progressFile);
2865
+ if (!existsSync8(progressFile)) return;
2866
+ const bytes = readFileSync8(progressFile);
2362
2867
  receive(progressDecoder.write(bytes.subarray(progressOffset)));
2363
2868
  progressOffset = bytes.length;
2364
2869
  } catch {
@@ -2391,109 +2896,146 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2391
2896
  return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
2392
2897
  }
2393
2898
  } finally {
2394
- if (existsSync7(progressFile)) unlinkSync2(progressFile);
2395
- if (existsSync7(outcomeFile)) unlinkSync2(outcomeFile);
2899
+ if (existsSync8(progressFile)) unlinkSync2(progressFile);
2900
+ if (existsSync8(outcomeFile)) unlinkSync2(outcomeFile);
2396
2901
  rmdirSync(outcomeDir);
2397
2902
  }
2398
2903
  }
2399
- function doForward(config, dir, options, command2, argv2, print, unlock) {
2400
- if (command2 && existsSync7(command2)) {
2401
- print(`${command2} is a file, not a command \u2014 did you mean \`--run ${command2}\`?`);
2402
- return 2;
2904
+ function armState(arm) {
2905
+ if (arm.verdict === "fail") return "failed";
2906
+ if (arm.verdict === "defer") return "retry";
2907
+ if (arm.verdict === "skip") return "kept";
2908
+ return arm.from === arm.to ? "current" : "updated";
2909
+ }
2910
+ function doForward(config, dir, options, command2, rest, ui, unlock) {
2911
+ if (existsSync8(command2)) {
2912
+ return ui.refuse(`${command2} is a file, not a command.`, `${ui.bin} --run ${command2}`, 2);
2403
2913
  }
2404
2914
  const acquired = readState(dir)?.acquired;
2405
2915
  const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
2406
2916
  const verbs = readPayloadVerbs(dir);
2407
- const declared = verbs === "*" || Array.isArray(verbs) && command2 !== void 0 && verbs.includes(command2);
2408
- if (!target || !declared) {
2409
- print(`unknown command: ${command2}`);
2410
- printUsage(config, print);
2411
- return 2;
2412
- }
2413
- const forwarded = [...resolveEntry(target), ...argv2];
2917
+ const declared = verbs === "*" || Array.isArray(verbs) && verbs.includes(command2);
2918
+ if (!target || !declared) return ui.refuse(`Unknown command: ${command2}.`, `${ui.bin} --help`, 2);
2919
+ const forwarded = [...resolveEntry(target), command2, ...rest];
2414
2920
  const cwd = payloadDir(dir);
2415
2921
  const inherited = payloadEnv(dir);
2416
2922
  const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
2417
2923
  unlock();
2418
2924
  const result = (options.runEntry ?? defaultRunEntry)(forwarded, cwd, env);
2419
2925
  if (!result.ok && result.code === void 0) {
2420
- print(`failed: ${result.error ?? `could not run ${forwarded[0]}`}`);
2421
- return 1;
2926
+ return ui.refuse(`Could not start ${config.product}: ${sentence(result.error ?? `could not run ${forwarded[0]}`)}`, `${ui.bin} doctor`);
2422
2927
  }
2423
2928
  return result.code ?? 0;
2424
2929
  }
2425
- function doAutoupdate(config, options, args, print) {
2930
+ function doAutoupdate(config, dir, options, args, ui) {
2426
2931
  const mode = args[0] ?? "status";
2427
- const scheduleOptions = options.autoupdate ?? {};
2932
+ const scheduleOptions = { stateDir: dir, ...options.autoupdate };
2428
2933
  const command2 = options.autoupdate?.command ?? [process.execPath, "update"];
2429
- if (mode === "on") {
2430
- try {
2431
- enableSchedule(config, command2, scheduleOptions);
2432
- } catch (error) {
2433
- print(error.message);
2434
- return 1;
2435
- }
2436
- const state2 = querySchedule(config, scheduleOptions);
2437
- print(`auto-update: on (${state2.cadence ?? "hourly"})`);
2438
- return 0;
2439
- }
2440
- if (mode === "off") {
2934
+ const name = identityFor(ui.product).name;
2935
+ if (mode === "on" || mode === "off") {
2441
2936
  try {
2442
- disableSchedule(config, scheduleOptions);
2937
+ if (mode === "on") enableSchedule(config, command2, scheduleOptions);
2938
+ else disableSchedule(config, scheduleOptions);
2443
2939
  } catch (error) {
2444
- print(error.message);
2445
- return 1;
2940
+ return ui.refuse(sentence(error.message), `${ui.bin} doctor`);
2446
2941
  }
2447
- print("auto-update: off");
2942
+ if (mode === "on") ui.notice("ok", `Auto-update is on. ${name} checks for updates every hour.`, `${ui.bin} autoupdate status`);
2943
+ else ui.notice("ok", "Auto-update is off.", `${ui.bin} autoupdate on`);
2448
2944
  return 0;
2449
2945
  }
2450
- if (mode !== "status") {
2451
- print(`unknown autoupdate mode: ${mode} \u2014 use on, off or status.`);
2452
- return 1;
2453
- }
2946
+ if (mode !== "status") return ui.refuse(`Unknown autoupdate mode: ${mode}.`, `${ui.bin} autoupdate status`, 2);
2454
2947
  const state = querySchedule(config, scheduleOptions);
2455
- if (!state.supported) {
2456
- print("auto-update: unsupported on this platform");
2457
- return 1;
2458
- }
2459
- print(`auto-update: ${state.enabled ? `on (${state.cadence ?? "hourly"})` : "off"}`);
2460
- if (state.lastRun) print(`last run: ${state.lastRun}`);
2948
+ if (!state.supported) return ui.refuse("Auto-update is not supported on this platform.", `${ui.bin} update`);
2949
+ if (state.enabled) ui.notice("ok", `Auto-update is on and runs every hour.${state.lastRun ? ` Last run: ${state.lastRun}.` : ""}`);
2950
+ else ui.notice("note", "Auto-update is off.", `${ui.bin} autoupdate on`);
2461
2951
  return 0;
2462
2952
  }
2463
- function doDoctor(config, dir, options, print, unlock) {
2464
- doLauncherDoctor(config, dir, options, print);
2953
+ function localFacts(config, dir, options) {
2954
+ const state = readState(dir);
2955
+ const tokens = readTokens(dir);
2956
+ const expired = Boolean(tokens && tokens.expiresAt <= Date.now());
2957
+ return {
2958
+ installed: state && existsSync8(payloadDir(dir)) ? state.version : null,
2959
+ // An expired access token with a refresh token renews on the next use; it is still signed in.
2960
+ signedIn: Boolean(tokens && (!expired || tokens.refreshToken)),
2961
+ expired,
2962
+ schedule: querySchedule(config, { stateDir: dir, ...options.autoupdate })
2963
+ };
2964
+ }
2965
+ function signInRow(local) {
2966
+ if (local.signedIn) return { surface: "sign-in", state: local.expired ? "signed in \xB7 renews on next use" : "signed in", mark: "ok" };
2967
+ return { surface: "sign-in", state: local.expired ? "expired" : "signed out", mark: "fail" };
2968
+ }
2969
+ function autoUpdateRow({ supported, enabled, cadence, lastRun }) {
2970
+ if (!supported) return { surface: AUTO_UPDATE, state: "unsupported on this platform", mark: "note" };
2971
+ if (!enabled) return { surface: AUTO_UPDATE, state: "off", mark: "note" };
2972
+ return { surface: AUTO_UPDATE, state: `on \xB7 ${cadence ?? "hourly"}`, mark: "ok", ...lastRun ? { note: `Last run: ${lastRun}` } : {} };
2973
+ }
2974
+ function verdictFor(ui, local, updateAvailable, ready) {
2975
+ if (!local.installed) return { state: "fail", headline: "is not installed.", next: `${ui.bin} install` };
2976
+ if (!local.signedIn) return { state: "fail", headline: "is signed out.", next: `${ui.bin} login` };
2977
+ if (updateAvailable) return { state: "note", headline: "has an update available.", next: `${ui.bin} update` };
2978
+ return { state: "ok", headline: "is ready.", next: ready };
2979
+ }
2980
+ function within(fetchImpl, ms) {
2981
+ return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(ms) }));
2982
+ }
2983
+ var STATUS_PROBE_MS = 3e3;
2984
+ async function doStatus(config, dir, options, ui, print, json) {
2985
+ const fetchImpl = within(options.fetchImpl ?? fetch, STATUS_PROBE_MS);
2986
+ const local = localFacts(config, dir, options);
2987
+ const [release, record] = await Promise.all([
2988
+ fetchReleaseVersion(config, fetchImpl),
2989
+ readLauncherRecord(launcherTarget(config), { fetch: fetchImpl, timeoutMs: STATUS_PROBE_MS })
2990
+ ]);
2991
+ const launcherNewer = record !== null && compareVersions(record.version, LAUNCHER_VERSION) > 0;
2992
+ const releaseOrder = local.installed && release ? compareVersions(release, local.installed) : null;
2993
+ const verdict = verdictFor(ui, local, launcherNewer || (releaseOrder ?? 0) > 0, `${ui.bin} doctor`);
2994
+ if (json) {
2995
+ print(JSON.stringify({
2996
+ product: config.product,
2997
+ launcher: { version: LAUNCHER_VERSION, available: launcherNewer ? record.version : null },
2998
+ installed: local.installed,
2999
+ release,
3000
+ signIn: local.signedIn ? local.expired ? "renews" : "signed-in" : local.expired ? "expired" : "signed-out",
3001
+ autoupdate: { supported: local.schedule.supported, enabled: local.schedule.enabled, lastRun: local.schedule.lastRun ?? null },
3002
+ verdict: verdict.state,
3003
+ next: verdict.next
3004
+ }, null, 2));
3005
+ return verdict.state === "fail" ? 1 : 0;
3006
+ }
3007
+ 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" };
3008
+ 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" };
3009
+ ui.show(renderReport(ui.product, {
3010
+ rows: [launcherRow, productRow, signInRow(local), autoUpdateRow(local.schedule)],
3011
+ verdict: { ...verdict, facts: [release ? `The release is ${release}.` : "The release version could not be read."] }
3012
+ }, ui.render));
3013
+ return verdict.state === "fail" ? 1 : 0;
3014
+ }
3015
+ function doDoctor(config, dir, options, ui) {
3016
+ const local = localFacts(config, dir, options);
3017
+ const verdict = verdictFor(ui, local, false, `${ui.bin} status`);
3018
+ ui.show(renderReport(ui.product, {
3019
+ rows: [
3020
+ { surface: "launcher", state: LAUNCHER_VERSION, mark: "ok" },
3021
+ local.installed ? { surface: config.product, state: local.installed, mark: "ok" } : { surface: config.product, state: SURFACE_WORDS.missing, mark: "fail" },
3022
+ signInRow(local),
3023
+ autoUpdateRow(local.schedule)
3024
+ ],
3025
+ // Paths belong in the receipt, never a row.
3026
+ verdict: { ...verdict, facts: [`Folder: ${dir}`, `Release host: ${config.host}`] }
3027
+ }, ui.render));
3028
+ const own = verdict.state === "fail" ? 1 : 0;
2465
3029
  const acquired = readState(dir)?.acquired;
2466
3030
  const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
2467
3031
  const verbs = readPayloadVerbs(dir);
2468
3032
  const chains = target !== null && (verbs === "*" || Array.isArray(verbs) && verbs.includes("doctor"));
2469
- if (!chains) return 0;
3033
+ if (!chains) return own;
2470
3034
  const cwd = payloadDir(dir);
2471
3035
  const inherited = payloadEnv(dir);
2472
3036
  const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
2473
- unlock();
2474
3037
  const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], cwd, env);
2475
- return result.code ?? (result.ok ? 0 : 1);
2476
- }
2477
- function doLauncherDoctor(config, dir, options, print) {
2478
- const tokens = readTokens(dir);
2479
- const state = readState(dir);
2480
- const payloadPresent = existsSync7(payloadDir(dir));
2481
- print(`product: ${config.product}`);
2482
- print(`host: ${config.host}`);
2483
- print(`login: ${config.loginKind}`);
2484
- print(`dir: ${dir}`);
2485
- if (tokens) {
2486
- const remaining = Math.max(0, Math.round((tokens.expiresAt - Date.now()) / 1e3));
2487
- print(`token: present (expires in ${remaining}s)`);
2488
- } else {
2489
- print("token: none \u2014 run login first.");
2490
- }
2491
- print(`payload: ${state ? state.version : "none"}${payloadPresent ? "" : " (not downloaded)"}`);
2492
- print(`paths: tokens ${join6(dir, "tokens.json")}, state ${join6(dir, "state.json")}, payload ${payloadDir(dir)}`);
2493
- const schedule = querySchedule(config, options.autoupdate ?? {});
2494
- print(
2495
- `auto-update: ${!schedule.supported ? "unsupported on this platform" : schedule.enabled ? `on (${schedule.cadence ?? "hourly"})` : "off"}`
2496
- );
3038
+ return (result.code ?? (result.ok ? 0 : 1)) || own;
2497
3039
  }
2498
3040
  var invokedAsMain = typeof process.argv[1] === "string" && (() => {
2499
3041
  try {
@@ -2512,8 +3054,7 @@ if (invokedAsMain) {
2512
3054
  process.exitCode = code;
2513
3055
  },
2514
3056
  (error) => {
2515
- process.stderr.write(`failed: ${error.message}
2516
- `);
3057
+ printScreen(renderNotice(NO_PRODUCT, { state: "fail", message: sentence(error.message) }, { color: false }), "stderr");
2517
3058
  process.exitCode = 1;
2518
3059
  }
2519
3060
  );
@@ -2523,6 +3064,7 @@ export {
2523
3064
  defaultRunEntry,
2524
3065
  ensureFreshToken,
2525
3066
  formatMb,
3067
+ parseArgv,
2526
3068
  readPayloadEntry,
2527
3069
  readPayloadRun,
2528
3070
  readPayloadVerbs,