@mutmutco/installer-launcher 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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) {
@@ -672,9 +677,10 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
672
677
 
673
678
  // src/index.ts
674
679
  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";
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;
@@ -1196,6 +1252,18 @@ function createInstallerRun(value, options = {}) {
1196
1252
  let chunkColumn = 0;
1197
1253
  const trailingCR = /* @__PURE__ */ new Set();
1198
1254
  let deferredActivity;
1255
+ let runningPhase;
1256
+ const animateRunning = (title, measure) => {
1257
+ if (face.progress(title, measure)) return;
1258
+ if (activeTitle === title) spinner.say(title, measure);
1259
+ else {
1260
+ activeTitle = title;
1261
+ spinner.start(title, measure);
1262
+ }
1263
+ };
1264
+ const resumeRunningPhase = () => {
1265
+ if (runningPhase && !finished) animateRunning(runningPhase.title, runningPhase.measure);
1266
+ };
1199
1267
  const drawnPhaseCompletions = /* @__PURE__ */ new Set();
1200
1268
  const closeChunk = () => {
1201
1269
  if (chunkOpen) emit("\n", chunkChannel);
@@ -1247,14 +1315,11 @@ function createInstallerRun(value, options = {}) {
1247
1315
  throw new Error("installer run: invalid runningTitle");
1248
1316
  }
1249
1317
  const runningTitle = facts.runningTitle?.trim() ?? title;
1250
- if (face.progress(runningTitle, facts.measure ?? null)) return;
1251
- if (activeTitle === runningTitle) spinner.say(runningTitle, facts.measure ?? null);
1252
- else {
1253
- activeTitle = runningTitle;
1254
- spinner.start(runningTitle, facts.measure ?? null);
1255
- }
1318
+ runningPhase = { id: fold ? "check" : id, title: runningTitle, measure: facts.measure ?? null };
1319
+ animateRunning(runningTitle, facts.measure ?? null);
1256
1320
  return;
1257
1321
  }
1322
+ if (runningPhase && runningPhase.id === (fold ? "check" : id)) runningPhase = void 0;
1258
1323
  if (fold && id !== "check") {
1259
1324
  if (facts.detail) run2.relay(facts.detail);
1260
1325
  return;
@@ -1267,17 +1332,18 @@ function createInstallerRun(value, options = {}) {
1267
1332
  },
1268
1333
  surface(facts) {
1269
1334
  if (finished) throw new Error("installer run already finished");
1270
- 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);
1271
1336
  if (!surface) throw new Error("installer run: undeclared surface");
1272
1337
  start();
1273
1338
  const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} ${tty ? "\u2192" : "->"} ${facts.to}` : ` ${facts.to}` : "";
1274
- 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];
1275
1340
  if (!status) throw new Error("installer run: unknown surface state");
1276
1341
  const separator = tty ? "\xB7" : "-";
1277
1342
  const activation = facts.state === "updated" && surface.activation ? ` ${separator} ${surface.activation}` : "";
1278
1343
  const completed = ["updated", "current", "kept", "skipped"].includes(facts.state);
1279
1344
  durable(`${facts.id}${versions} ${separator} ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
1280
1345
  if (facts.detail) run2.relay(facts.detail);
1346
+ resumeRunningPhase();
1281
1347
  },
1282
1348
  milestone({ step, state, ms, measure }) {
1283
1349
  start();
@@ -1296,6 +1362,7 @@ function createInstallerRun(value, options = {}) {
1296
1362
  if (state !== "fail" && drawnPhaseCompletions.has(step)) return;
1297
1363
  durable(step, ms === void 0 ? null : ms / 1e3, state);
1298
1364
  if (state === "ok") drawnPhaseCompletions.add(step);
1365
+ resumeRunningPhase();
1299
1366
  },
1300
1367
  signIn({ url, code }) {
1301
1368
  start();
@@ -1377,8 +1444,9 @@ function createInstallerRun(value, options = {}) {
1377
1444
  if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
1378
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}.`;
1379
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;
1380
1448
  const body = [
1381
- `${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."}`,
1382
1450
  changed,
1383
1451
  ...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
1384
1452
  ...facts.detail ? [facts.detail] : [],
@@ -1394,12 +1462,96 @@ function createInstallerRun(value, options = {}) {
1394
1462
  stop() {
1395
1463
  closeChunk();
1396
1464
  activeTitle = void 0;
1465
+ runningPhase = void 0;
1397
1466
  spinner.stop();
1398
1467
  }
1399
1468
  };
1400
1469
  return run2;
1401
1470
  }
1402
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
+
1403
1555
  // src/autoupdate.ts
1404
1556
  import { spawnSync as spawnSync2 } from "node:child_process";
1405
1557
  import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
@@ -1430,19 +1582,33 @@ function scheduleName(config) {
1430
1582
  function scheduleLabel(config) {
1431
1583
  return `${config.binName}-autoupdate`.replace(/[^A-Za-z0-9_-]+/g, "-");
1432
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
+ }
1433
1596
  function quoteWindows(arg) {
1434
1597
  return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
1435
1598
  }
1436
1599
  function enableSchedule(config, command2, options = {}) {
1437
1600
  const platform = schedulePlatform(options.platform);
1438
- 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}.`);
1439
1602
  const exec = options.exec ?? defaultExec;
1440
1603
  const home = homeOf(options);
1441
1604
  if (platform === "win32") {
1442
- const taskLine = command2.map(quoteWindows).join(" ");
1443
- 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"]);
1444
1610
  if (result2.code !== 0) {
1445
- 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}`}`);
1446
1612
  }
1447
1613
  return;
1448
1614
  }
@@ -1455,7 +1621,7 @@ function enableSchedule(config, command2, options = {}) {
1455
1621
  exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
1456
1622
  const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
1457
1623
  if (result2.code !== 0) {
1458
- 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}`}`);
1459
1625
  }
1460
1626
  return;
1461
1627
  }
@@ -1466,23 +1632,24 @@ function enableSchedule(config, command2, options = {}) {
1466
1632
  writeFileSync6(join5(dir, `${label}.timer`), linuxTimer(label));
1467
1633
  const reload = exec("systemctl", ["--user", "daemon-reload"]);
1468
1634
  if (reload.code !== 0) {
1469
- 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}`}`);
1470
1636
  }
1471
1637
  const result = exec("systemctl", ["--user", "enable", "--now", `${label}.timer`]);
1472
1638
  if (result.code !== 0) {
1473
- 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}`}`);
1474
1640
  }
1475
1641
  }
1476
1642
  function disableSchedule(config, options = {}) {
1477
1643
  const platform = schedulePlatform(options.platform);
1478
- 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}.`);
1479
1645
  const exec = options.exec ?? defaultExec;
1480
1646
  const home = homeOf(options);
1481
1647
  if (platform === "win32") {
1482
1648
  const result = exec("schtasks", ["/Delete", "/TN", scheduleName(config), "/F"]);
1483
1649
  if (result.code !== 0 && !/cannot find|does not exist/i.test(`${result.stdout} ${result.stderr}`)) {
1484
- 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}`}`);
1485
1651
  }
1652
+ rmSync4(join5(options.stateDir ?? defaultProductDir(config.product), HIDDEN_SCRIPT), { force: true });
1486
1653
  return;
1487
1654
  }
1488
1655
  if (platform === "darwin") {
@@ -1575,6 +1742,161 @@ function firstLine(text) {
1575
1742
  return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
1576
1743
  }
1577
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
+
1578
1900
  // src/login-github.ts
1579
1901
  import { spawn } from "node:child_process";
1580
1902
  var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
@@ -1810,7 +2132,7 @@ function page(title, body) {
1810
2132
  }
1811
2133
 
1812
2134
  // src/index.ts
1813
- var LAUNCHER_VERSION = true ? "1.0.0" : readVersionFromPackage();
2135
+ var LAUNCHER_VERSION = true ? "1.1.0" : readVersionFromPackage();
1814
2136
  function defaultPrint(message) {
1815
2137
  process.stdout.write(`${message}
1816
2138
  `);
@@ -1819,79 +2141,192 @@ function defaultPrintErr(message) {
1819
2141
  process.stderr.write(`${message}
1820
2142
  `);
1821
2143
  }
2144
+ var LAUNCHER_VERBS = /* @__PURE__ */ new Set(["install", "update", "status", "doctor", "login", "logout", "autoupdate"]);
2145
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["--config", "--dir"]);
2146
+ var SWITCHES = {
2147
+ "--json": "json",
2148
+ "--no-color": "noColor",
2149
+ "--dry-run": "dryRun",
2150
+ "--help": "help",
2151
+ "-h": "help",
2152
+ "--version": "version",
2153
+ "-v": "version"
2154
+ };
2155
+ function parseArgv(argv2) {
2156
+ const parsed = { flags: {}, args: [], rest: [] };
2157
+ const flag = (i2) => {
2158
+ const arg = argv2[i2];
2159
+ if (VALUE_FLAGS.has(arg)) {
2160
+ const value = argv2[i2 + 1];
2161
+ if (value === void 0 || value.startsWith("-")) {
2162
+ parsed.unknownFlag ??= arg;
2163
+ return i2 + 1;
2164
+ }
2165
+ parsed.flags[arg === "--config" ? "config" : "dir"] = value;
2166
+ return i2 + 2;
2167
+ }
2168
+ const name = SWITCHES[arg];
2169
+ if (!name) return -1;
2170
+ parsed.flags[name] = true;
2171
+ return i2 + 1;
2172
+ };
2173
+ const skip = (i2) => {
2174
+ const next = flag(i2);
2175
+ if (next >= 0) return next;
2176
+ parsed.unknownFlag ??= argv2[i2];
2177
+ return i2 + 1;
2178
+ };
2179
+ let i = 0;
2180
+ while (i < argv2.length && argv2[i].startsWith("-")) {
2181
+ if (argv2[i] === "--run") {
2182
+ parsed.run = { file: argv2[i + 1], args: argv2.slice(i + 2) };
2183
+ return parsed;
2184
+ }
2185
+ i = skip(i);
2186
+ }
2187
+ parsed.command = argv2[i];
2188
+ if (parsed.command === void 0) return parsed;
2189
+ if (!LAUNCHER_VERBS.has(parsed.command)) {
2190
+ parsed.rest = argv2.slice(i + 1);
2191
+ return parsed;
2192
+ }
2193
+ for (i += 1; i < argv2.length; ) {
2194
+ if (argv2[i].startsWith("-")) i = skip(i);
2195
+ else parsed.args.push(argv2[i++]);
2196
+ }
2197
+ return parsed;
2198
+ }
2199
+ var NO_PRODUCT = "mmi-hub";
2200
+ function sentence(message) {
2201
+ const text = message.trim().replace(/\s+—\s+/gu, ". ");
2202
+ if (!text) return "Something went wrong.";
2203
+ return `${text[0].toUpperCase()}${text.slice(1)}${/[.!?]$/u.test(text) ? "" : "."}`;
2204
+ }
2205
+ function screensFor(product, bin, render, options) {
2206
+ const show = (lines, stream = "stdout") => {
2207
+ const sink = stream === "stderr" ? options.printErr : options.print;
2208
+ if (sink) for (const line of lines) sink(line);
2209
+ else printScreen(lines, stream);
2210
+ };
2211
+ return {
2212
+ product,
2213
+ bin,
2214
+ render,
2215
+ show,
2216
+ notice: (state, message, next) => show(renderNotice(product, { state, message, ...next ? { next } : {} }, render)),
2217
+ refuse: (message, next, code = 1) => {
2218
+ show(renderNotice(product, { state: "fail", message, ...next ? { next } : {} }, render), "stderr");
2219
+ return code;
2220
+ }
2221
+ };
2222
+ }
1822
2223
  async function run(rawOptions = {}) {
1823
2224
  const print = rawOptions.print ?? defaultPrint;
1824
2225
  const printErr = rawOptions.printErr ?? defaultPrintErr;
1825
- const argv2 = rawOptions.argv ?? process.argv.slice(2);
1826
- const runAt = argv2.indexOf("--run");
1827
- if (runAt >= 0) return runFile(argv2[runAt + 1], argv2.slice(runAt + 2), printErr);
2226
+ const parsed = parseArgv(rawOptions.argv ?? process.argv.slice(2));
2227
+ if (parsed.run) return runFile(parsed.run.file, parsed.run.args, printErr);
2228
+ const render = {
2229
+ ...rawOptions.tty === void 0 ? {} : { tty: rawOptions.tty },
2230
+ ...parsed.flags.noColor ? { color: false } : {}
2231
+ };
1828
2232
  let config;
1829
2233
  try {
1830
- config = loadProductConfig({ configPath: rawOptions.configPath ?? flagValue(argv2, "--config") });
2234
+ config = loadProductConfig({ configPath: rawOptions.configPath ?? parsed.flags.config });
1831
2235
  } catch (error) {
1832
- print(`cannot start: ${error.message}`);
1833
- return 2;
1834
- }
1835
- const dir = resolveProductDir(config.product, rawOptions.dir ?? flagValue(argv2, "--dir"));
1836
- const positional = argv2.filter((arg) => !arg.startsWith("-"));
1837
- if (argv2.includes("--version") || argv2.includes("-v")) {
1838
- print(`${config.binName} launcher ${LAUNCHER_VERSION}`);
2236
+ return screensFor(NO_PRODUCT, "", { ...render, color: false }, rawOptions).refuse(`The launcher cannot start: ${sentence(error.message)}`, void 0, 2);
2237
+ }
2238
+ const dir = resolveProductDir(config.product, rawOptions.dir ?? parsed.flags.dir);
2239
+ const ui = screensFor(faceProduct(config), config.binName, render, rawOptions);
2240
+ if ((parsed.command === "install" || parsed.command === "update") && !parsed.flags.version) recordSelf(dir);
2241
+ if (parsed.unknownFlag) return ui.refuse(`Unknown option: ${parsed.unknownFlag}.`, `${ui.bin} --help`, 2);
2242
+ if (parsed.flags.version) {
2243
+ print(renderVersion(ui.bin, readState(dir)?.version ?? "not installed", LAUNCHER_VERSION));
1839
2244
  return 0;
1840
2245
  }
1841
- if (argv2.includes("--help") || argv2.includes("-h") || positional.length === 0) {
1842
- printUsage(config, print);
1843
- return positional.length === 0 ? 2 : 0;
2246
+ const command2 = parsed.command;
2247
+ if (parsed.flags.help || command2 === void 0) {
2248
+ ui.show(usage(config, dir, render));
2249
+ return parsed.flags.help ? 0 : 2;
1844
2250
  }
1845
- const command2 = positional[0];
2251
+ const mutating = ["install", "login", "logout"].includes(command2) || command2 === "update" && !parsed.flags.dryRun || command2 === "autoupdate" && ["on", "off"].includes(parsed.args[0] ?? "");
1846
2252
  let unlock;
1847
2253
  try {
1848
- unlock = lockInstallation(dir);
1849
- await recoverAcquisition(dir, async (command3, cwd, env) => {
1850
- const result = (rawOptions.runEntry ?? defaultRunEntry)(command3, cwd, env);
1851
- return result.ok && (result.code === void 0 || result.code === 0);
1852
- }, payloadEnv(dir) ?? process.env);
2254
+ if (mutating) {
2255
+ unlock = lockInstallation(dir);
2256
+ await recoverAcquisition(dir, async (command3, cwd, env) => {
2257
+ const result = (rawOptions.runEntry ?? defaultRunEntry)(command3, cwd, env);
2258
+ return result.ok && (result.code === void 0 || result.code === 0);
2259
+ }, payloadEnv(dir) ?? process.env);
2260
+ }
1853
2261
  switch (command2) {
1854
2262
  case "login":
1855
- unlock();
1856
- await doLogin(config, dir, rawOptions, print);
2263
+ unlock?.();
2264
+ await doLogin(config, dir, rawOptions, (line) => ui.notice("note", line));
2265
+ ui.notice("ok", `Signed in to ${identityFor(ui.product).name}.`, `${ui.bin} install`);
1857
2266
  return 0;
1858
2267
  case "logout":
1859
2268
  wipeProductDir(dir);
1860
- print(`signed out of ${config.product} and removed the downloaded payload.`);
2269
+ ui.notice("ok", `Signed out of ${identityFor(ui.product).name} and removed the downloaded payload.`, `${ui.bin} login`);
1861
2270
  return 0;
1862
2271
  case "install":
1863
- return await doInstall(config, dir, rawOptions, print);
2272
+ return await doInstall(config, dir, rawOptions, print, parsed.flags);
1864
2273
  case "update":
1865
- return await doUpdate(config, dir, rawOptions, print);
2274
+ return await doUpdate(config, dir, rawOptions, print, parsed.flags);
2275
+ case "status":
2276
+ return await doStatus(config, dir, rawOptions, ui, print, Boolean(parsed.flags.json));
1866
2277
  case "doctor":
1867
- return doDoctor(config, dir, rawOptions, print, unlock);
2278
+ return doDoctor(config, dir, rawOptions, ui);
1868
2279
  case "autoupdate":
1869
- return doAutoupdate(config, rawOptions, positional.slice(1), print);
2280
+ return doAutoupdate(config, dir, rawOptions, parsed.args, ui);
1870
2281
  default:
1871
- return doForward(config, dir, rawOptions, command2, argv2, print, unlock);
2282
+ return doForward(config, dir, rawOptions, command2, parsed.rest, ui, () => unlock?.());
1872
2283
  }
1873
2284
  } catch (error) {
1874
- if (error instanceof NeedsLoginError) {
1875
- print("you are signed out \u2014 run login first.");
1876
- return 3;
2285
+ if (error instanceof NeedsLoginError) return ui.refuse("You are signed out.", `${ui.bin} login`, 3);
2286
+ const message = error.message;
2287
+ if (/another installer is running|lock recovery is already pending/u.test(message)) {
2288
+ return ui.refuse("Another install or update is running.", `${ui.bin} status`);
1877
2289
  }
1878
- print(`failed: ${error.message}`);
1879
- return 1;
2290
+ return ui.refuse(sentence(message), `${ui.bin} ${command2}`);
1880
2291
  } finally {
1881
2292
  unlock?.();
1882
2293
  }
1883
2294
  }
1884
- function flagValue(argv2, flag) {
1885
- const index = argv2.indexOf(flag);
1886
- if (index < 0) return void 0;
1887
- const value = argv2[index + 1];
1888
- return value && !value.startsWith("-") ? value : void 0;
2295
+ function recordSelf(dir) {
2296
+ if (!isSea()) return;
2297
+ try {
2298
+ mkdirSync6(dir, { recursive: true });
2299
+ writeFileSync8(join7(dir, LAUNCHER_RECORD_FILE), `${JSON.stringify({ path: process.execPath, version: LAUNCHER_VERSION })}
2300
+ `);
2301
+ } catch {
2302
+ }
1889
2303
  }
1890
- function printUsage(config, print) {
1891
- print(`${config.binName} launcher ${LAUNCHER_VERSION} \u2014 sign in and install ${config.product}.`);
1892
- print("usage: launcher <login|logout|install|update|doctor|autoupdate on|off|status> [--config <path>] [--dir <path>]");
1893
- print(" launcher --run <file> [args\u2026] (run a payload file with the embedded runtime)");
1894
- print(" launcher <verb> [args\u2026] (forwarded to the payload when it declares run + verbs)");
2304
+ function launcherTarget(config) {
2305
+ return { product: config.product, bin: config.binName, key: config.publicKey, host: config.host, ...config.dl ? { dl: config.dl } : {} };
2306
+ }
2307
+ function usage(config, dir, render) {
2308
+ const name = config.product;
2309
+ return renderUsage(faceProduct(config), {
2310
+ bin: config.binName,
2311
+ version: `${readState(dir)?.version ?? "not installed"} (launcher ${LAUNCHER_VERSION})`,
2312
+ summary: `Sign in, install and update ${name}.`,
2313
+ commands: [
2314
+ { usage: "install", text: `Sign in and install ${name}.` },
2315
+ { usage: "update [--dry-run]", text: `Update ${name} and this launcher.` },
2316
+ { usage: "status", text: `Show versions, sign-in and ${AUTO_UPDATE}.` },
2317
+ { usage: "doctor", text: `Check this machine, then run the ${name} doctor.` },
2318
+ { usage: "login", text: "Sign in." },
2319
+ { usage: "logout", text: "Sign out and remove the downloaded payload." },
2320
+ { usage: "autoupdate on|off|status", text: `Turn hourly ${AUTO_UPDATE} on or off, or show it.` },
2321
+ { usage: "<verb> ...", text: `Passed to ${name} when it declares it.` }
2322
+ ],
2323
+ options: [
2324
+ { usage: "--json", text: "Print status as JSON." },
2325
+ { usage: "--no-color", text: "Print without colour." },
2326
+ { usage: "--config <path>", text: "Use this product config." },
2327
+ { usage: "--dir <path>", text: "Use this folder for sign-in and downloads." }
2328
+ ]
2329
+ }, render);
1895
2330
  }
1896
2331
  async function runFile(file, args, printErr) {
1897
2332
  if (!file) {
@@ -1899,7 +2334,7 @@ async function runFile(file, args, printErr) {
1899
2334
  return 1;
1900
2335
  }
1901
2336
  const abs = resolve2(process.cwd(), file);
1902
- if (!existsSync7(abs)) {
2337
+ if (!existsSync8(abs)) {
1903
2338
  printErr(`cannot run ${file}: no such file.`);
1904
2339
  return 1;
1905
2340
  }
@@ -1955,15 +2390,14 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
1955
2390
  });
1956
2391
  return refreshed.accessToken;
1957
2392
  }
1958
- async function doLogin(config, dir, options, print, installer) {
2393
+ async function doLogin(config, dir, options, prompt, installer) {
1959
2394
  const fetchImpl = options.fetchImpl ?? fetch;
1960
2395
  const open = options.open ?? openBrowser;
1961
2396
  if (config.loginKind === "github") {
1962
2397
  const tokens = await loginGithub(config, {
1963
2398
  fetchImpl,
1964
2399
  open,
1965
- print,
1966
- ...installer ? { onDeviceCode: (prompt) => installer.signIn(prompt) } : {}
2400
+ onDeviceCode: (grant) => installer ? installer.signIn(grant) : prompt(`To sign in, open ${grant.url} and enter the code ${grant.code}.`)
1967
2401
  });
1968
2402
  writeTokens(dir, {
1969
2403
  accessToken: tokens.accessToken,
@@ -1971,7 +2405,12 @@ async function doLogin(config, dir, options, print, installer) {
1971
2405
  expiresAt: Date.now() + tokens.expiresIn * 1e3
1972
2406
  });
1973
2407
  } else {
1974
- const tokens = await loginGoogle({ host: config.host, open, fetchImpl });
2408
+ const tokens = await loginGoogle({
2409
+ host: config.host,
2410
+ open,
2411
+ fetchImpl,
2412
+ print: (line) => installer ? installer.relay(line) : prompt(line)
2413
+ });
1975
2414
  writeTokens(dir, {
1976
2415
  accessToken: tokens.accessToken,
1977
2416
  refreshToken: tokens.refreshToken,
@@ -1979,20 +2418,19 @@ async function doLogin(config, dir, options, print, installer) {
1979
2418
  clientId: tokens.clientId
1980
2419
  });
1981
2420
  }
1982
- if (!installer) print(`signed in to ${config.product}.`);
1983
2421
  }
1984
- async function fetchAccessTokenOrLogin(config, dir, options, print, installer) {
2422
+ async function fetchAccessTokenOrLogin(config, dir, options, installer) {
1985
2423
  const fetchImpl = options.fetchImpl ?? fetch;
1986
2424
  const fresh = await ensureFreshToken(config, dir, fetchImpl);
1987
2425
  if (fresh) return fresh;
1988
- await doLogin(config, dir, options, print, installer);
2426
+ await doLogin(config, dir, options, (line) => installer.relay(line), installer);
1989
2427
  const after = readTokens(dir);
1990
2428
  if (!after) throw new Error("sign-in did not produce a token");
1991
2429
  return after.accessToken;
1992
2430
  }
1993
2431
  function readPayloadArgv(dir, key) {
1994
2432
  try {
1995
- const parsed = JSON.parse(readFileSync7(join6(payloadDir(dir), "payload.json"), "utf8"));
2433
+ const parsed = JSON.parse(readFileSync8(join7(payloadDir(dir), "payload.json"), "utf8"));
1996
2434
  const entry2 = parsed[key];
1997
2435
  if (typeof entry2 === "string") {
1998
2436
  const parts = entry2.trim().split(/\s+/).filter(Boolean);
@@ -2014,7 +2452,7 @@ function readPayloadRun(dir) {
2014
2452
  }
2015
2453
  function readPayloadVerbs(dir) {
2016
2454
  try {
2017
- const parsed = JSON.parse(readFileSync7(join6(payloadDir(dir), "payload.json"), "utf8"));
2455
+ const parsed = JSON.parse(readFileSync8(join7(payloadDir(dir), "payload.json"), "utf8"));
2018
2456
  const verbs = parsed.verbs;
2019
2457
  if (verbs === "*") return "*";
2020
2458
  if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
@@ -2031,7 +2469,7 @@ function resolveEntry(entry2) {
2031
2469
  function needsShell(command2) {
2032
2470
  if (process.platform !== "win32") return false;
2033
2471
  if (/\.(cmd|bat)$/i.test(command2)) return true;
2034
- return !existsSync7(command2);
2472
+ return !existsSync8(command2);
2035
2473
  }
2036
2474
  function quoteForShell(arg) {
2037
2475
  return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
@@ -2097,8 +2535,16 @@ function faceProduct(config) {
2097
2535
  function formatMb(bytes) {
2098
2536
  return (bytes / 1048576).toFixed(1).replace(/\.0$/, "");
2099
2537
  }
2100
- async function installOrUpdate(config, dir, options, print, update) {
2538
+ async function installOrUpdate(config, dir, options, print, update, flags2) {
2539
+ let launcherFailed = false;
2540
+ const code = await installOrUpdateRun(config, dir, options, print, update, flags2, () => {
2541
+ launcherFailed = true;
2542
+ });
2543
+ return launcherFailed ? Math.max(code, 1) : code;
2544
+ }
2545
+ async function installOrUpdateRun(config, dir, options, print, update, flags2, markLauncherFailed) {
2101
2546
  const product = faceProduct(config);
2547
+ const dryRun = update && Boolean(flags2.dryRun);
2102
2548
  const installer = createInstallerRun({
2103
2549
  product,
2104
2550
  gate: config.host,
@@ -2108,17 +2554,55 @@ async function installOrUpdate(config, dir, options, print, update) {
2108
2554
  operation: update ? "update" : "install",
2109
2555
  tty: options.tty,
2110
2556
  animate: !options.print && Boolean(process.stderr.isTTY),
2557
+ dryRun,
2558
+ ...flags2.noColor ? { color: false } : {},
2111
2559
  ...options.print ? { write: (text) => print(text.replace(/\n$/, "")) } : {}
2112
2560
  });
2113
2561
  const fetchImpl = options.fetchImpl ?? fetch;
2114
2562
  let version = readState(dir)?.version ?? "unknown";
2115
2563
  installer.start();
2116
2564
  try {
2565
+ const refresh = options.refresh ?? (isSea() ? refreshLauncher : void 0);
2566
+ const arm = refresh ? await refresh(launcherTarget(config), {
2567
+ lockHeld: true,
2568
+ path: process.execPath,
2569
+ dryRun,
2570
+ productDir: dir,
2571
+ fetch: fetchImpl,
2572
+ readVersion: async () => LAUNCHER_VERSION
2573
+ }) : null;
2574
+ if (arm) {
2575
+ installer.surface({
2576
+ id: LAUNCHER_SURFACE,
2577
+ from: arm.from,
2578
+ to: arm.to,
2579
+ state: armState(arm),
2580
+ ...arm.durationMs === void 0 ? {} : { seconds: arm.durationMs / 1e3 },
2581
+ ...arm.verdict !== "ok" && arm.detail ? { detail: arm.detail } : {}
2582
+ });
2583
+ if (arm.verdict === "fail") {
2584
+ const finish = installer.finish;
2585
+ installer.finish = (facts) => finish({
2586
+ ...facts,
2587
+ operationFailed: true,
2588
+ detail: facts.detail ?? "The launcher could not update itself.",
2589
+ retry: facts.retry ?? `${config.binName} update`
2590
+ });
2591
+ markLauncherFailed();
2592
+ }
2593
+ }
2117
2594
  const current = readState(dir);
2118
- const payloadPresent = existsSync7(payloadDir(dir));
2595
+ const payloadPresent = existsSync8(payloadDir(dir));
2119
2596
  let started = Date.now();
2120
2597
  installer.phase("resolve", { state: "running" });
2121
2598
  const pointer = await fetchReleaseVersion(config, fetchImpl);
2599
+ if (dryRun) {
2600
+ installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
2601
+ const installed = payloadPresent ? current?.version : void 0;
2602
+ if (pointer) installer.surface({ id: config.product, from: installed, to: pointer, state: installed === pointer ? "current" : "updated" });
2603
+ installer.finish({ ...pointer ? { version: pointer } : {}, total: 1, updated: pointer && installed !== pointer ? 1 : 0, failed: 0, dryRun: true });
2604
+ return 0;
2605
+ }
2122
2606
  if (pointer && current?.version === pointer && payloadPresent) {
2123
2607
  version = pointer;
2124
2608
  installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
@@ -2135,14 +2619,14 @@ async function installOrUpdate(config, dir, options, print, update) {
2135
2619
  }
2136
2620
  installer.phase("sign-in", { state: "running" });
2137
2621
  started = Date.now();
2138
- const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print, installer);
2622
+ const accessToken = await fetchAccessTokenOrLogin(config, dir, options, installer);
2139
2623
  installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
2140
2624
  installer.phase("resolve", { state: "running" });
2141
2625
  started = Date.now();
2142
2626
  const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
2143
2627
  version = manifest.version;
2144
2628
  installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
2145
- const unchanged = current?.version === version && existsSync7(payloadDir(dir));
2629
+ const unchanged = current?.version === version && existsSync8(payloadDir(dir));
2146
2630
  if (unchanged && current.acquired) {
2147
2631
  installer.surface({ id: config.product, from: version, to: version, state: "current" });
2148
2632
  return await finishLastMile(config, dir, options, installer, version, true, acquisitionRepairCommand(dir, version));
@@ -2152,14 +2636,14 @@ async function installOrUpdate(config, dir, options, print, update) {
2152
2636
  started = Date.now();
2153
2637
  const cached = reusableCandidate(dir, manifest);
2154
2638
  const candidate = cached ?? randomUUID4();
2155
- const candidateDir = join6(dir, "candidates", candidate);
2639
+ const candidateDir = join7(dir, "candidates", candidate);
2156
2640
  const onProgress = ({ done, total }) => installer.phase("download", {
2157
2641
  state: "running",
2158
2642
  measure: total ? `${Math.floor(done / total * 100)}% \xB7 ${formatMb(done)} of ${formatMb(total)} MB` : `${formatMb(done)} MB`,
2159
2643
  ...total ? { runningTitle: `Downloading ${config.product} ${version} (${formatMb(total)} MB)` } : {}
2160
2644
  });
2161
2645
  if (!cached) await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl, onProgress, cacheDir: dir });
2162
- const acquisition = existsSync7(join6(candidateDir, "payload", "payload.json")) ? readAcquisition(join6(candidateDir, "payload")) : null;
2646
+ const acquisition = existsSync8(join7(candidateDir, "payload", "payload.json")) ? readAcquisition(join7(candidateDir, "payload")) : null;
2163
2647
  if (acquisition) {
2164
2648
  rememberCandidate(dir, candidate, manifest);
2165
2649
  installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
@@ -2182,10 +2666,10 @@ async function installOrUpdate(config, dir, options, print, update) {
2182
2666
  installer.finish(outcome ?? { version, total: 1, updated: 1, failed: 0, installed: true });
2183
2667
  return 0;
2184
2668
  }
2185
- const target = join6(dir, "payload");
2669
+ const target = join7(dir, "payload");
2186
2670
  mkdirSync6(dir, { recursive: true });
2187
- rmSync5(target, { recursive: true, force: true });
2188
- renameSync4(join6(candidateDir, "payload"), target);
2671
+ rmSync6(target, { recursive: true, force: true });
2672
+ renameSync5(join7(candidateDir, "payload"), target);
2189
2673
  writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
2190
2674
  installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
2191
2675
  }
@@ -2211,11 +2695,11 @@ async function installOrUpdate(config, dir, options, print, update) {
2211
2695
  installer.stop();
2212
2696
  }
2213
2697
  }
2214
- function doInstall(config, dir, options, print) {
2215
- return installOrUpdate(config, dir, options, print, false);
2698
+ function doInstall(config, dir, options, print, flags2) {
2699
+ return installOrUpdate(config, dir, options, print, false, flags2);
2216
2700
  }
2217
- function doUpdate(config, dir, options, print) {
2218
- return installOrUpdate(config, dir, options, print, true);
2701
+ function doUpdate(config, dir, options, print, flags2) {
2702
+ return installOrUpdate(config, dir, options, print, true, flags2);
2219
2703
  }
2220
2704
  function payloadEnv(dir) {
2221
2705
  const stored = readTokens(dir);
@@ -2227,8 +2711,8 @@ function payloadFileAsCommand(entry2, payload) {
2227
2711
  const first = entry2[0];
2228
2712
  if (!first || first === "$self") return null;
2229
2713
  if (first.includes("/") || first.includes("\\")) return null;
2230
- const candidate = join6(payload, first);
2231
- if (!existsSync7(candidate)) return null;
2714
+ const candidate = join7(payload, first);
2715
+ if (!existsSync8(candidate)) return null;
2232
2716
  if (NEVER_A_PROGRAM.test(first)) return first;
2233
2717
  if (process.platform === "win32") return null;
2234
2718
  try {
@@ -2247,7 +2731,7 @@ async function finishLastMile(config, dir, options, installer, version, unchange
2247
2731
  updated: unchanged ? 0 : 1,
2248
2732
  failed: 0,
2249
2733
  installed: true,
2250
- detail: `next step: run ${join6(payload, config.binName)} to start ${config.product}.`
2734
+ detail: `next step: run ${join7(payload, config.binName)} to start ${config.product}.`
2251
2735
  });
2252
2736
  return 0;
2253
2737
  }
@@ -2293,9 +2777,9 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2293
2777
  const [command2, ...args] = entry2;
2294
2778
  const shell = needsShell(command2);
2295
2779
  const progress = !(process.platform === "win32" && shell);
2296
- const outcomeDir = mkdtempSync2(join6(tmpdir4(), "mm-installer-outcome-"));
2297
- const outcomeFile = join6(outcomeDir, "outcome.json");
2298
- const progressFile = join6(outcomeDir, "progress.jsonl");
2780
+ const outcomeDir = mkdtempSync2(join7(tmpdir4(), "mm-installer-outcome-"));
2781
+ const outcomeFile = join7(outcomeDir, "outcome.json");
2782
+ const progressFile = join7(outcomeDir, "progress.jsonl");
2299
2783
  const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
2300
2784
  delete childEnv.MM_FACE_TRANSCRIPT;
2301
2785
  delete childEnv.MM_PROGRESS_FILE;
@@ -2345,8 +2829,8 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2345
2829
  const pollProgress = () => {
2346
2830
  if (progressError) return;
2347
2831
  try {
2348
- if (!existsSync7(progressFile)) return;
2349
- const bytes = readFileSync7(progressFile);
2832
+ if (!existsSync8(progressFile)) return;
2833
+ const bytes = readFileSync8(progressFile);
2350
2834
  receive(progressDecoder.write(bytes.subarray(progressOffset)));
2351
2835
  progressOffset = bytes.length;
2352
2836
  } catch {
@@ -2379,109 +2863,146 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2379
2863
  return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
2380
2864
  }
2381
2865
  } finally {
2382
- if (existsSync7(progressFile)) unlinkSync2(progressFile);
2383
- if (existsSync7(outcomeFile)) unlinkSync2(outcomeFile);
2866
+ if (existsSync8(progressFile)) unlinkSync2(progressFile);
2867
+ if (existsSync8(outcomeFile)) unlinkSync2(outcomeFile);
2384
2868
  rmdirSync(outcomeDir);
2385
2869
  }
2386
2870
  }
2387
- function doForward(config, dir, options, command2, argv2, print, unlock) {
2388
- if (command2 && existsSync7(command2)) {
2389
- print(`${command2} is a file, not a command \u2014 did you mean \`--run ${command2}\`?`);
2390
- return 2;
2871
+ function armState(arm) {
2872
+ if (arm.verdict === "fail") return "failed";
2873
+ if (arm.verdict === "defer") return "retry";
2874
+ if (arm.verdict === "skip") return "kept";
2875
+ return arm.from === arm.to ? "current" : "updated";
2876
+ }
2877
+ function doForward(config, dir, options, command2, rest, ui, unlock) {
2878
+ if (existsSync8(command2)) {
2879
+ return ui.refuse(`${command2} is a file, not a command.`, `${ui.bin} --run ${command2}`, 2);
2391
2880
  }
2392
2881
  const acquired = readState(dir)?.acquired;
2393
2882
  const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
2394
2883
  const verbs = readPayloadVerbs(dir);
2395
- const declared = verbs === "*" || Array.isArray(verbs) && command2 !== void 0 && verbs.includes(command2);
2396
- if (!target || !declared) {
2397
- print(`unknown command: ${command2}`);
2398
- printUsage(config, print);
2399
- return 2;
2400
- }
2401
- const forwarded = [...resolveEntry(target), ...argv2];
2884
+ const declared = verbs === "*" || Array.isArray(verbs) && verbs.includes(command2);
2885
+ if (!target || !declared) return ui.refuse(`Unknown command: ${command2}.`, `${ui.bin} --help`, 2);
2886
+ const forwarded = [...resolveEntry(target), command2, ...rest];
2402
2887
  const cwd = payloadDir(dir);
2403
2888
  const inherited = payloadEnv(dir);
2404
2889
  const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
2405
2890
  unlock();
2406
2891
  const result = (options.runEntry ?? defaultRunEntry)(forwarded, cwd, env);
2407
2892
  if (!result.ok && result.code === void 0) {
2408
- print(`failed: ${result.error ?? `could not run ${forwarded[0]}`}`);
2409
- return 1;
2893
+ return ui.refuse(`Could not start ${config.product}: ${sentence(result.error ?? `could not run ${forwarded[0]}`)}`, `${ui.bin} doctor`);
2410
2894
  }
2411
2895
  return result.code ?? 0;
2412
2896
  }
2413
- function doAutoupdate(config, options, args, print) {
2897
+ function doAutoupdate(config, dir, options, args, ui) {
2414
2898
  const mode = args[0] ?? "status";
2415
- const scheduleOptions = options.autoupdate ?? {};
2899
+ const scheduleOptions = { stateDir: dir, ...options.autoupdate };
2416
2900
  const command2 = options.autoupdate?.command ?? [process.execPath, "update"];
2417
- if (mode === "on") {
2418
- try {
2419
- enableSchedule(config, command2, scheduleOptions);
2420
- } catch (error) {
2421
- print(error.message);
2422
- return 1;
2423
- }
2424
- const state2 = querySchedule(config, scheduleOptions);
2425
- print(`auto-update: on (${state2.cadence ?? "hourly"})`);
2426
- return 0;
2427
- }
2428
- if (mode === "off") {
2901
+ const name = identityFor(ui.product).name;
2902
+ if (mode === "on" || mode === "off") {
2429
2903
  try {
2430
- disableSchedule(config, scheduleOptions);
2904
+ if (mode === "on") enableSchedule(config, command2, scheduleOptions);
2905
+ else disableSchedule(config, scheduleOptions);
2431
2906
  } catch (error) {
2432
- print(error.message);
2433
- return 1;
2907
+ return ui.refuse(sentence(error.message), `${ui.bin} doctor`);
2434
2908
  }
2435
- print("auto-update: off");
2909
+ if (mode === "on") ui.notice("ok", `Auto-update is on. ${name} checks for updates every hour.`, `${ui.bin} autoupdate status`);
2910
+ else ui.notice("ok", "Auto-update is off.", `${ui.bin} autoupdate on`);
2436
2911
  return 0;
2437
2912
  }
2438
- if (mode !== "status") {
2439
- print(`unknown autoupdate mode: ${mode} \u2014 use on, off or status.`);
2440
- return 1;
2441
- }
2913
+ if (mode !== "status") return ui.refuse(`Unknown autoupdate mode: ${mode}.`, `${ui.bin} autoupdate status`, 2);
2442
2914
  const state = querySchedule(config, scheduleOptions);
2443
- if (!state.supported) {
2444
- print("auto-update: unsupported on this platform");
2445
- return 1;
2446
- }
2447
- print(`auto-update: ${state.enabled ? `on (${state.cadence ?? "hourly"})` : "off"}`);
2448
- if (state.lastRun) print(`last run: ${state.lastRun}`);
2915
+ if (!state.supported) return ui.refuse("Auto-update is not supported on this platform.", `${ui.bin} update`);
2916
+ if (state.enabled) ui.notice("ok", `Auto-update is on and runs every hour.${state.lastRun ? ` Last run: ${state.lastRun}.` : ""}`);
2917
+ else ui.notice("note", "Auto-update is off.", `${ui.bin} autoupdate on`);
2449
2918
  return 0;
2450
2919
  }
2451
- function doDoctor(config, dir, options, print, unlock) {
2452
- doLauncherDoctor(config, dir, options, print);
2920
+ function localFacts(config, dir, options) {
2921
+ const state = readState(dir);
2922
+ const tokens = readTokens(dir);
2923
+ const expired = Boolean(tokens && tokens.expiresAt <= Date.now());
2924
+ return {
2925
+ installed: state && existsSync8(payloadDir(dir)) ? state.version : null,
2926
+ // An expired access token with a refresh token renews on the next use; it is still signed in.
2927
+ signedIn: Boolean(tokens && (!expired || tokens.refreshToken)),
2928
+ expired,
2929
+ schedule: querySchedule(config, { stateDir: dir, ...options.autoupdate })
2930
+ };
2931
+ }
2932
+ function signInRow(local) {
2933
+ if (local.signedIn) return { surface: "sign-in", state: local.expired ? "signed in \xB7 renews on next use" : "signed in", mark: "ok" };
2934
+ return { surface: "sign-in", state: local.expired ? "expired" : "signed out", mark: "fail" };
2935
+ }
2936
+ function autoUpdateRow({ supported, enabled, cadence, lastRun }) {
2937
+ if (!supported) return { surface: AUTO_UPDATE, state: "unsupported on this platform", mark: "note" };
2938
+ if (!enabled) return { surface: AUTO_UPDATE, state: "off", mark: "note" };
2939
+ return { surface: AUTO_UPDATE, state: `on \xB7 ${cadence ?? "hourly"}`, mark: "ok", ...lastRun ? { note: `Last run: ${lastRun}` } : {} };
2940
+ }
2941
+ function verdictFor(ui, local, updateAvailable, ready) {
2942
+ if (!local.installed) return { state: "fail", headline: "is not installed.", next: `${ui.bin} install` };
2943
+ if (!local.signedIn) return { state: "fail", headline: "is signed out.", next: `${ui.bin} login` };
2944
+ if (updateAvailable) return { state: "note", headline: "has an update available.", next: `${ui.bin} update` };
2945
+ return { state: "ok", headline: "is ready.", next: ready };
2946
+ }
2947
+ function within(fetchImpl, ms) {
2948
+ return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(ms) }));
2949
+ }
2950
+ var STATUS_PROBE_MS = 3e3;
2951
+ async function doStatus(config, dir, options, ui, print, json) {
2952
+ const fetchImpl = within(options.fetchImpl ?? fetch, STATUS_PROBE_MS);
2953
+ const local = localFacts(config, dir, options);
2954
+ const [release, record] = await Promise.all([
2955
+ fetchReleaseVersion(config, fetchImpl),
2956
+ readLauncherRecord(launcherTarget(config), { fetch: fetchImpl, timeoutMs: STATUS_PROBE_MS })
2957
+ ]);
2958
+ const launcherNewer = record !== null && compareVersions(record.version, LAUNCHER_VERSION) > 0;
2959
+ const releaseOrder = local.installed && release ? compareVersions(release, local.installed) : null;
2960
+ const verdict = verdictFor(ui, local, launcherNewer || (releaseOrder ?? 0) > 0, `${ui.bin} doctor`);
2961
+ if (json) {
2962
+ print(JSON.stringify({
2963
+ product: config.product,
2964
+ launcher: { version: LAUNCHER_VERSION, available: launcherNewer ? record.version : null },
2965
+ installed: local.installed,
2966
+ release,
2967
+ signIn: local.signedIn ? local.expired ? "renews" : "signed-in" : local.expired ? "expired" : "signed-out",
2968
+ autoupdate: { supported: local.schedule.supported, enabled: local.schedule.enabled, lastRun: local.schedule.lastRun ?? null },
2969
+ verdict: verdict.state,
2970
+ next: verdict.next
2971
+ }, null, 2));
2972
+ return verdict.state === "fail" ? 1 : 0;
2973
+ }
2974
+ 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" };
2975
+ 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" };
2976
+ ui.show(renderReport(ui.product, {
2977
+ rows: [launcherRow, productRow, signInRow(local), autoUpdateRow(local.schedule)],
2978
+ verdict: { ...verdict, facts: [release ? `The release is ${release}.` : "The release version could not be read."] }
2979
+ }, ui.render));
2980
+ return verdict.state === "fail" ? 1 : 0;
2981
+ }
2982
+ function doDoctor(config, dir, options, ui) {
2983
+ const local = localFacts(config, dir, options);
2984
+ const verdict = verdictFor(ui, local, false, `${ui.bin} status`);
2985
+ ui.show(renderReport(ui.product, {
2986
+ rows: [
2987
+ { surface: "launcher", state: LAUNCHER_VERSION, mark: "ok" },
2988
+ local.installed ? { surface: config.product, state: local.installed, mark: "ok" } : { surface: config.product, state: SURFACE_WORDS.missing, mark: "fail" },
2989
+ signInRow(local),
2990
+ autoUpdateRow(local.schedule)
2991
+ ],
2992
+ // Paths belong in the receipt, never a row.
2993
+ verdict: { ...verdict, facts: [`Folder: ${dir}`, `Release host: ${config.host}`] }
2994
+ }, ui.render));
2995
+ const own = verdict.state === "fail" ? 1 : 0;
2453
2996
  const acquired = readState(dir)?.acquired;
2454
2997
  const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
2455
2998
  const verbs = readPayloadVerbs(dir);
2456
2999
  const chains = target !== null && (verbs === "*" || Array.isArray(verbs) && verbs.includes("doctor"));
2457
- if (!chains) return 0;
3000
+ if (!chains) return own;
2458
3001
  const cwd = payloadDir(dir);
2459
3002
  const inherited = payloadEnv(dir);
2460
3003
  const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
2461
- unlock();
2462
3004
  const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], cwd, env);
2463
- return result.code ?? (result.ok ? 0 : 1);
2464
- }
2465
- function doLauncherDoctor(config, dir, options, print) {
2466
- const tokens = readTokens(dir);
2467
- const state = readState(dir);
2468
- const payloadPresent = existsSync7(payloadDir(dir));
2469
- print(`product: ${config.product}`);
2470
- print(`host: ${config.host}`);
2471
- print(`login: ${config.loginKind}`);
2472
- print(`dir: ${dir}`);
2473
- if (tokens) {
2474
- const remaining = Math.max(0, Math.round((tokens.expiresAt - Date.now()) / 1e3));
2475
- print(`token: present (expires in ${remaining}s)`);
2476
- } else {
2477
- print("token: none \u2014 run login first.");
2478
- }
2479
- print(`payload: ${state ? state.version : "none"}${payloadPresent ? "" : " (not downloaded)"}`);
2480
- print(`paths: tokens ${join6(dir, "tokens.json")}, state ${join6(dir, "state.json")}, payload ${payloadDir(dir)}`);
2481
- const schedule = querySchedule(config, options.autoupdate ?? {});
2482
- print(
2483
- `auto-update: ${!schedule.supported ? "unsupported on this platform" : schedule.enabled ? `on (${schedule.cadence ?? "hourly"})` : "off"}`
2484
- );
3005
+ return (result.code ?? (result.ok ? 0 : 1)) || own;
2485
3006
  }
2486
3007
  var invokedAsMain = typeof process.argv[1] === "string" && (() => {
2487
3008
  try {
@@ -2500,8 +3021,7 @@ if (invokedAsMain) {
2500
3021
  process.exitCode = code;
2501
3022
  },
2502
3023
  (error) => {
2503
- process.stderr.write(`failed: ${error.message}
2504
- `);
3024
+ printScreen(renderNotice(NO_PRODUCT, { state: "fail", message: sentence(error.message) }, { color: false }), "stderr");
2505
3025
  process.exitCode = 1;
2506
3026
  }
2507
3027
  );
@@ -2511,6 +3031,7 @@ export {
2511
3031
  defaultRunEntry,
2512
3032
  ensureFreshToken,
2513
3033
  formatMb,
3034
+ parseArgv,
2514
3035
  readPayloadEntry,
2515
3036
  readPayloadRun,
2516
3037
  readPayloadVerbs,