@mutmutco/installer-launcher 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -4
- package/dist/launcher.js +678 -169
- package/dist/launcher.sea.cjs +689 -179
- package/dist/refresh.js +335 -0
- package/package.json +3 -2
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
|
|
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
|
|
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
|
|
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((
|
|
1113
|
-
if (!
|
|
1114
|
-
const
|
|
1115
|
-
const id = text(
|
|
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 (
|
|
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 =
|
|
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." :
|
|
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(`
|
|
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
|
|
1455
|
-
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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,6 +1742,161 @@ 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
1901
|
import { spawn } from "node:child_process";
|
|
1592
1902
|
var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
@@ -1822,7 +2132,7 @@ function page(title, body) {
|
|
|
1822
2132
|
}
|
|
1823
2133
|
|
|
1824
2134
|
// src/index.ts
|
|
1825
|
-
var LAUNCHER_VERSION = true ? "1.0
|
|
2135
|
+
var LAUNCHER_VERSION = true ? "1.1.0" : readVersionFromPackage();
|
|
1826
2136
|
function defaultPrint(message) {
|
|
1827
2137
|
process.stdout.write(`${message}
|
|
1828
2138
|
`);
|
|
@@ -1831,79 +2141,192 @@ function defaultPrintErr(message) {
|
|
|
1831
2141
|
process.stderr.write(`${message}
|
|
1832
2142
|
`);
|
|
1833
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
|
+
}
|
|
1834
2223
|
async function run(rawOptions = {}) {
|
|
1835
2224
|
const print = rawOptions.print ?? defaultPrint;
|
|
1836
2225
|
const printErr = rawOptions.printErr ?? defaultPrintErr;
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1839
|
-
|
|
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
|
+
};
|
|
1840
2232
|
let config;
|
|
1841
2233
|
try {
|
|
1842
|
-
config = loadProductConfig({ configPath: rawOptions.configPath ??
|
|
2234
|
+
config = loadProductConfig({ configPath: rawOptions.configPath ?? parsed.flags.config });
|
|
1843
2235
|
} catch (error) {
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
const
|
|
1848
|
-
|
|
1849
|
-
if (
|
|
1850
|
-
|
|
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));
|
|
1851
2244
|
return 0;
|
|
1852
2245
|
}
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
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;
|
|
1856
2250
|
}
|
|
1857
|
-
const
|
|
2251
|
+
const mutating = ["install", "login", "logout"].includes(command2) || command2 === "update" && !parsed.flags.dryRun || command2 === "autoupdate" && ["on", "off"].includes(parsed.args[0] ?? "");
|
|
1858
2252
|
let unlock;
|
|
1859
2253
|
try {
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
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
|
+
}
|
|
1865
2261
|
switch (command2) {
|
|
1866
2262
|
case "login":
|
|
1867
|
-
unlock();
|
|
1868
|
-
await doLogin(config, dir, rawOptions,
|
|
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`);
|
|
1869
2266
|
return 0;
|
|
1870
2267
|
case "logout":
|
|
1871
2268
|
wipeProductDir(dir);
|
|
1872
|
-
|
|
2269
|
+
ui.notice("ok", `Signed out of ${identityFor(ui.product).name} and removed the downloaded payload.`, `${ui.bin} login`);
|
|
1873
2270
|
return 0;
|
|
1874
2271
|
case "install":
|
|
1875
|
-
return await doInstall(config, dir, rawOptions, print);
|
|
2272
|
+
return await doInstall(config, dir, rawOptions, print, parsed.flags);
|
|
1876
2273
|
case "update":
|
|
1877
|
-
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));
|
|
1878
2277
|
case "doctor":
|
|
1879
|
-
return doDoctor(config, dir, rawOptions,
|
|
2278
|
+
return doDoctor(config, dir, rawOptions, ui);
|
|
1880
2279
|
case "autoupdate":
|
|
1881
|
-
return doAutoupdate(config, rawOptions,
|
|
2280
|
+
return doAutoupdate(config, dir, rawOptions, parsed.args, ui);
|
|
1882
2281
|
default:
|
|
1883
|
-
return doForward(config, dir, rawOptions, command2,
|
|
2282
|
+
return doForward(config, dir, rawOptions, command2, parsed.rest, ui, () => unlock?.());
|
|
1884
2283
|
}
|
|
1885
2284
|
} catch (error) {
|
|
1886
|
-
if (error instanceof NeedsLoginError) {
|
|
1887
|
-
|
|
1888
|
-
|
|
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`);
|
|
1889
2289
|
}
|
|
1890
|
-
|
|
1891
|
-
return 1;
|
|
2290
|
+
return ui.refuse(sentence(message), `${ui.bin} ${command2}`);
|
|
1892
2291
|
} finally {
|
|
1893
2292
|
unlock?.();
|
|
1894
2293
|
}
|
|
1895
2294
|
}
|
|
1896
|
-
function
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
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
|
+
}
|
|
1901
2303
|
}
|
|
1902
|
-
function
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
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);
|
|
1907
2330
|
}
|
|
1908
2331
|
async function runFile(file, args, printErr) {
|
|
1909
2332
|
if (!file) {
|
|
@@ -1911,7 +2334,7 @@ async function runFile(file, args, printErr) {
|
|
|
1911
2334
|
return 1;
|
|
1912
2335
|
}
|
|
1913
2336
|
const abs = resolve2(process.cwd(), file);
|
|
1914
|
-
if (!
|
|
2337
|
+
if (!existsSync8(abs)) {
|
|
1915
2338
|
printErr(`cannot run ${file}: no such file.`);
|
|
1916
2339
|
return 1;
|
|
1917
2340
|
}
|
|
@@ -1967,15 +2390,14 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
|
|
|
1967
2390
|
});
|
|
1968
2391
|
return refreshed.accessToken;
|
|
1969
2392
|
}
|
|
1970
|
-
async function doLogin(config, dir, options,
|
|
2393
|
+
async function doLogin(config, dir, options, prompt, installer) {
|
|
1971
2394
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1972
2395
|
const open = options.open ?? openBrowser;
|
|
1973
2396
|
if (config.loginKind === "github") {
|
|
1974
2397
|
const tokens = await loginGithub(config, {
|
|
1975
2398
|
fetchImpl,
|
|
1976
2399
|
open,
|
|
1977
|
-
|
|
1978
|
-
...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}.`)
|
|
1979
2401
|
});
|
|
1980
2402
|
writeTokens(dir, {
|
|
1981
2403
|
accessToken: tokens.accessToken,
|
|
@@ -1983,7 +2405,12 @@ async function doLogin(config, dir, options, print, installer) {
|
|
|
1983
2405
|
expiresAt: Date.now() + tokens.expiresIn * 1e3
|
|
1984
2406
|
});
|
|
1985
2407
|
} else {
|
|
1986
|
-
const tokens = await loginGoogle({
|
|
2408
|
+
const tokens = await loginGoogle({
|
|
2409
|
+
host: config.host,
|
|
2410
|
+
open,
|
|
2411
|
+
fetchImpl,
|
|
2412
|
+
print: (line) => installer ? installer.relay(line) : prompt(line)
|
|
2413
|
+
});
|
|
1987
2414
|
writeTokens(dir, {
|
|
1988
2415
|
accessToken: tokens.accessToken,
|
|
1989
2416
|
refreshToken: tokens.refreshToken,
|
|
@@ -1991,20 +2418,19 @@ async function doLogin(config, dir, options, print, installer) {
|
|
|
1991
2418
|
clientId: tokens.clientId
|
|
1992
2419
|
});
|
|
1993
2420
|
}
|
|
1994
|
-
if (!installer) print(`signed in to ${config.product}.`);
|
|
1995
2421
|
}
|
|
1996
|
-
async function fetchAccessTokenOrLogin(config, dir, options,
|
|
2422
|
+
async function fetchAccessTokenOrLogin(config, dir, options, installer) {
|
|
1997
2423
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1998
2424
|
const fresh = await ensureFreshToken(config, dir, fetchImpl);
|
|
1999
2425
|
if (fresh) return fresh;
|
|
2000
|
-
await doLogin(config, dir, options,
|
|
2426
|
+
await doLogin(config, dir, options, (line) => installer.relay(line), installer);
|
|
2001
2427
|
const after = readTokens(dir);
|
|
2002
2428
|
if (!after) throw new Error("sign-in did not produce a token");
|
|
2003
2429
|
return after.accessToken;
|
|
2004
2430
|
}
|
|
2005
2431
|
function readPayloadArgv(dir, key) {
|
|
2006
2432
|
try {
|
|
2007
|
-
const parsed = JSON.parse(
|
|
2433
|
+
const parsed = JSON.parse(readFileSync8(join7(payloadDir(dir), "payload.json"), "utf8"));
|
|
2008
2434
|
const entry2 = parsed[key];
|
|
2009
2435
|
if (typeof entry2 === "string") {
|
|
2010
2436
|
const parts = entry2.trim().split(/\s+/).filter(Boolean);
|
|
@@ -2026,7 +2452,7 @@ function readPayloadRun(dir) {
|
|
|
2026
2452
|
}
|
|
2027
2453
|
function readPayloadVerbs(dir) {
|
|
2028
2454
|
try {
|
|
2029
|
-
const parsed = JSON.parse(
|
|
2455
|
+
const parsed = JSON.parse(readFileSync8(join7(payloadDir(dir), "payload.json"), "utf8"));
|
|
2030
2456
|
const verbs = parsed.verbs;
|
|
2031
2457
|
if (verbs === "*") return "*";
|
|
2032
2458
|
if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
|
|
@@ -2043,7 +2469,7 @@ function resolveEntry(entry2) {
|
|
|
2043
2469
|
function needsShell(command2) {
|
|
2044
2470
|
if (process.platform !== "win32") return false;
|
|
2045
2471
|
if (/\.(cmd|bat)$/i.test(command2)) return true;
|
|
2046
|
-
return !
|
|
2472
|
+
return !existsSync8(command2);
|
|
2047
2473
|
}
|
|
2048
2474
|
function quoteForShell(arg) {
|
|
2049
2475
|
return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
|
|
@@ -2109,8 +2535,16 @@ function faceProduct(config) {
|
|
|
2109
2535
|
function formatMb(bytes) {
|
|
2110
2536
|
return (bytes / 1048576).toFixed(1).replace(/\.0$/, "");
|
|
2111
2537
|
}
|
|
2112
|
-
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) {
|
|
2113
2546
|
const product = faceProduct(config);
|
|
2547
|
+
const dryRun = update && Boolean(flags2.dryRun);
|
|
2114
2548
|
const installer = createInstallerRun({
|
|
2115
2549
|
product,
|
|
2116
2550
|
gate: config.host,
|
|
@@ -2120,17 +2554,55 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2120
2554
|
operation: update ? "update" : "install",
|
|
2121
2555
|
tty: options.tty,
|
|
2122
2556
|
animate: !options.print && Boolean(process.stderr.isTTY),
|
|
2557
|
+
dryRun,
|
|
2558
|
+
...flags2.noColor ? { color: false } : {},
|
|
2123
2559
|
...options.print ? { write: (text) => print(text.replace(/\n$/, "")) } : {}
|
|
2124
2560
|
});
|
|
2125
2561
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2126
2562
|
let version = readState(dir)?.version ?? "unknown";
|
|
2127
2563
|
installer.start();
|
|
2128
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
|
+
}
|
|
2129
2594
|
const current = readState(dir);
|
|
2130
|
-
const payloadPresent =
|
|
2595
|
+
const payloadPresent = existsSync8(payloadDir(dir));
|
|
2131
2596
|
let started = Date.now();
|
|
2132
2597
|
installer.phase("resolve", { state: "running" });
|
|
2133
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
|
+
}
|
|
2134
2606
|
if (pointer && current?.version === pointer && payloadPresent) {
|
|
2135
2607
|
version = pointer;
|
|
2136
2608
|
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
@@ -2147,14 +2619,14 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2147
2619
|
}
|
|
2148
2620
|
installer.phase("sign-in", { state: "running" });
|
|
2149
2621
|
started = Date.now();
|
|
2150
|
-
const accessToken = await fetchAccessTokenOrLogin(config, dir, options,
|
|
2622
|
+
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, installer);
|
|
2151
2623
|
installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
|
|
2152
2624
|
installer.phase("resolve", { state: "running" });
|
|
2153
2625
|
started = Date.now();
|
|
2154
2626
|
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
2155
2627
|
version = manifest.version;
|
|
2156
2628
|
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
2157
|
-
const unchanged = current?.version === version &&
|
|
2629
|
+
const unchanged = current?.version === version && existsSync8(payloadDir(dir));
|
|
2158
2630
|
if (unchanged && current.acquired) {
|
|
2159
2631
|
installer.surface({ id: config.product, from: version, to: version, state: "current" });
|
|
2160
2632
|
return await finishLastMile(config, dir, options, installer, version, true, acquisitionRepairCommand(dir, version));
|
|
@@ -2164,14 +2636,14 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2164
2636
|
started = Date.now();
|
|
2165
2637
|
const cached = reusableCandidate(dir, manifest);
|
|
2166
2638
|
const candidate = cached ?? randomUUID4();
|
|
2167
|
-
const candidateDir =
|
|
2639
|
+
const candidateDir = join7(dir, "candidates", candidate);
|
|
2168
2640
|
const onProgress = ({ done, total }) => installer.phase("download", {
|
|
2169
2641
|
state: "running",
|
|
2170
2642
|
measure: total ? `${Math.floor(done / total * 100)}% \xB7 ${formatMb(done)} of ${formatMb(total)} MB` : `${formatMb(done)} MB`,
|
|
2171
2643
|
...total ? { runningTitle: `Downloading ${config.product} ${version} (${formatMb(total)} MB)` } : {}
|
|
2172
2644
|
});
|
|
2173
2645
|
if (!cached) await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl, onProgress, cacheDir: dir });
|
|
2174
|
-
const acquisition =
|
|
2646
|
+
const acquisition = existsSync8(join7(candidateDir, "payload", "payload.json")) ? readAcquisition(join7(candidateDir, "payload")) : null;
|
|
2175
2647
|
if (acquisition) {
|
|
2176
2648
|
rememberCandidate(dir, candidate, manifest);
|
|
2177
2649
|
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
@@ -2194,10 +2666,10 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2194
2666
|
installer.finish(outcome ?? { version, total: 1, updated: 1, failed: 0, installed: true });
|
|
2195
2667
|
return 0;
|
|
2196
2668
|
}
|
|
2197
|
-
const target =
|
|
2669
|
+
const target = join7(dir, "payload");
|
|
2198
2670
|
mkdirSync6(dir, { recursive: true });
|
|
2199
|
-
|
|
2200
|
-
|
|
2671
|
+
rmSync6(target, { recursive: true, force: true });
|
|
2672
|
+
renameSync5(join7(candidateDir, "payload"), target);
|
|
2201
2673
|
writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2202
2674
|
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
2203
2675
|
}
|
|
@@ -2223,11 +2695,11 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2223
2695
|
installer.stop();
|
|
2224
2696
|
}
|
|
2225
2697
|
}
|
|
2226
|
-
function doInstall(config, dir, options, print) {
|
|
2227
|
-
return installOrUpdate(config, dir, options, print, false);
|
|
2698
|
+
function doInstall(config, dir, options, print, flags2) {
|
|
2699
|
+
return installOrUpdate(config, dir, options, print, false, flags2);
|
|
2228
2700
|
}
|
|
2229
|
-
function doUpdate(config, dir, options, print) {
|
|
2230
|
-
return installOrUpdate(config, dir, options, print, true);
|
|
2701
|
+
function doUpdate(config, dir, options, print, flags2) {
|
|
2702
|
+
return installOrUpdate(config, dir, options, print, true, flags2);
|
|
2231
2703
|
}
|
|
2232
2704
|
function payloadEnv(dir) {
|
|
2233
2705
|
const stored = readTokens(dir);
|
|
@@ -2239,8 +2711,8 @@ function payloadFileAsCommand(entry2, payload) {
|
|
|
2239
2711
|
const first = entry2[0];
|
|
2240
2712
|
if (!first || first === "$self") return null;
|
|
2241
2713
|
if (first.includes("/") || first.includes("\\")) return null;
|
|
2242
|
-
const candidate =
|
|
2243
|
-
if (!
|
|
2714
|
+
const candidate = join7(payload, first);
|
|
2715
|
+
if (!existsSync8(candidate)) return null;
|
|
2244
2716
|
if (NEVER_A_PROGRAM.test(first)) return first;
|
|
2245
2717
|
if (process.platform === "win32") return null;
|
|
2246
2718
|
try {
|
|
@@ -2259,7 +2731,7 @@ async function finishLastMile(config, dir, options, installer, version, unchange
|
|
|
2259
2731
|
updated: unchanged ? 0 : 1,
|
|
2260
2732
|
failed: 0,
|
|
2261
2733
|
installed: true,
|
|
2262
|
-
detail: `next step: run ${
|
|
2734
|
+
detail: `next step: run ${join7(payload, config.binName)} to start ${config.product}.`
|
|
2263
2735
|
});
|
|
2264
2736
|
return 0;
|
|
2265
2737
|
}
|
|
@@ -2305,9 +2777,9 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2305
2777
|
const [command2, ...args] = entry2;
|
|
2306
2778
|
const shell = needsShell(command2);
|
|
2307
2779
|
const progress = !(process.platform === "win32" && shell);
|
|
2308
|
-
const outcomeDir = mkdtempSync2(
|
|
2309
|
-
const outcomeFile =
|
|
2310
|
-
const progressFile =
|
|
2780
|
+
const outcomeDir = mkdtempSync2(join7(tmpdir4(), "mm-installer-outcome-"));
|
|
2781
|
+
const outcomeFile = join7(outcomeDir, "outcome.json");
|
|
2782
|
+
const progressFile = join7(outcomeDir, "progress.jsonl");
|
|
2311
2783
|
const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
|
|
2312
2784
|
delete childEnv.MM_FACE_TRANSCRIPT;
|
|
2313
2785
|
delete childEnv.MM_PROGRESS_FILE;
|
|
@@ -2357,8 +2829,8 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2357
2829
|
const pollProgress = () => {
|
|
2358
2830
|
if (progressError) return;
|
|
2359
2831
|
try {
|
|
2360
|
-
if (!
|
|
2361
|
-
const bytes =
|
|
2832
|
+
if (!existsSync8(progressFile)) return;
|
|
2833
|
+
const bytes = readFileSync8(progressFile);
|
|
2362
2834
|
receive(progressDecoder.write(bytes.subarray(progressOffset)));
|
|
2363
2835
|
progressOffset = bytes.length;
|
|
2364
2836
|
} catch {
|
|
@@ -2391,109 +2863,146 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2391
2863
|
return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
|
|
2392
2864
|
}
|
|
2393
2865
|
} finally {
|
|
2394
|
-
if (
|
|
2395
|
-
if (
|
|
2866
|
+
if (existsSync8(progressFile)) unlinkSync2(progressFile);
|
|
2867
|
+
if (existsSync8(outcomeFile)) unlinkSync2(outcomeFile);
|
|
2396
2868
|
rmdirSync(outcomeDir);
|
|
2397
2869
|
}
|
|
2398
2870
|
}
|
|
2399
|
-
function
|
|
2400
|
-
if (
|
|
2401
|
-
|
|
2402
|
-
|
|
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);
|
|
2403
2880
|
}
|
|
2404
2881
|
const acquired = readState(dir)?.acquired;
|
|
2405
2882
|
const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
|
|
2406
2883
|
const verbs = readPayloadVerbs(dir);
|
|
2407
|
-
const declared = verbs === "*" || Array.isArray(verbs) &&
|
|
2408
|
-
if (!target || !declared) {
|
|
2409
|
-
|
|
2410
|
-
printUsage(config, print);
|
|
2411
|
-
return 2;
|
|
2412
|
-
}
|
|
2413
|
-
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];
|
|
2414
2887
|
const cwd = payloadDir(dir);
|
|
2415
2888
|
const inherited = payloadEnv(dir);
|
|
2416
2889
|
const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
|
|
2417
2890
|
unlock();
|
|
2418
2891
|
const result = (options.runEntry ?? defaultRunEntry)(forwarded, cwd, env);
|
|
2419
2892
|
if (!result.ok && result.code === void 0) {
|
|
2420
|
-
|
|
2421
|
-
return 1;
|
|
2893
|
+
return ui.refuse(`Could not start ${config.product}: ${sentence(result.error ?? `could not run ${forwarded[0]}`)}`, `${ui.bin} doctor`);
|
|
2422
2894
|
}
|
|
2423
2895
|
return result.code ?? 0;
|
|
2424
2896
|
}
|
|
2425
|
-
function doAutoupdate(config, options, args,
|
|
2897
|
+
function doAutoupdate(config, dir, options, args, ui) {
|
|
2426
2898
|
const mode = args[0] ?? "status";
|
|
2427
|
-
const scheduleOptions = options.autoupdate
|
|
2899
|
+
const scheduleOptions = { stateDir: dir, ...options.autoupdate };
|
|
2428
2900
|
const command2 = options.autoupdate?.command ?? [process.execPath, "update"];
|
|
2429
|
-
|
|
2430
|
-
|
|
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") {
|
|
2901
|
+
const name = identityFor(ui.product).name;
|
|
2902
|
+
if (mode === "on" || mode === "off") {
|
|
2441
2903
|
try {
|
|
2442
|
-
|
|
2904
|
+
if (mode === "on") enableSchedule(config, command2, scheduleOptions);
|
|
2905
|
+
else disableSchedule(config, scheduleOptions);
|
|
2443
2906
|
} catch (error) {
|
|
2444
|
-
|
|
2445
|
-
return 1;
|
|
2907
|
+
return ui.refuse(sentence(error.message), `${ui.bin} doctor`);
|
|
2446
2908
|
}
|
|
2447
|
-
|
|
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`);
|
|
2448
2911
|
return 0;
|
|
2449
2912
|
}
|
|
2450
|
-
if (mode !== "status") {
|
|
2451
|
-
print(`unknown autoupdate mode: ${mode} \u2014 use on, off or status.`);
|
|
2452
|
-
return 1;
|
|
2453
|
-
}
|
|
2913
|
+
if (mode !== "status") return ui.refuse(`Unknown autoupdate mode: ${mode}.`, `${ui.bin} autoupdate status`, 2);
|
|
2454
2914
|
const state = querySchedule(config, scheduleOptions);
|
|
2455
|
-
if (!state.supported) {
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
}
|
|
2459
|
-
print(`auto-update: ${state.enabled ? `on (${state.cadence ?? "hourly"})` : "off"}`);
|
|
2460
|
-
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`);
|
|
2461
2918
|
return 0;
|
|
2462
2919
|
}
|
|
2463
|
-
function
|
|
2464
|
-
|
|
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;
|
|
2465
2996
|
const acquired = readState(dir)?.acquired;
|
|
2466
2997
|
const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
|
|
2467
2998
|
const verbs = readPayloadVerbs(dir);
|
|
2468
2999
|
const chains = target !== null && (verbs === "*" || Array.isArray(verbs) && verbs.includes("doctor"));
|
|
2469
|
-
if (!chains) return
|
|
3000
|
+
if (!chains) return own;
|
|
2470
3001
|
const cwd = payloadDir(dir);
|
|
2471
3002
|
const inherited = payloadEnv(dir);
|
|
2472
3003
|
const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
|
|
2473
|
-
unlock();
|
|
2474
3004
|
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
|
-
);
|
|
3005
|
+
return (result.code ?? (result.ok ? 0 : 1)) || own;
|
|
2497
3006
|
}
|
|
2498
3007
|
var invokedAsMain = typeof process.argv[1] === "string" && (() => {
|
|
2499
3008
|
try {
|
|
@@ -2512,8 +3021,7 @@ if (invokedAsMain) {
|
|
|
2512
3021
|
process.exitCode = code;
|
|
2513
3022
|
},
|
|
2514
3023
|
(error) => {
|
|
2515
|
-
|
|
2516
|
-
`);
|
|
3024
|
+
printScreen(renderNotice(NO_PRODUCT, { state: "fail", message: sentence(error.message) }, { color: false }), "stderr");
|
|
2517
3025
|
process.exitCode = 1;
|
|
2518
3026
|
}
|
|
2519
3027
|
);
|
|
@@ -2523,6 +3031,7 @@ export {
|
|
|
2523
3031
|
defaultRunEntry,
|
|
2524
3032
|
ensureFreshToken,
|
|
2525
3033
|
formatMb,
|
|
3034
|
+
parseArgv,
|
|
2526
3035
|
readPayloadEntry,
|
|
2527
3036
|
readPayloadRun,
|
|
2528
3037
|
readPayloadVerbs,
|