@mutmutco/installer-launcher 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -4
- package/dist/launcher.js +716 -174
- package/dist/launcher.sea.cjs +724 -181
- package/dist/refresh.js +335 -0
- package/package.json +3 -2
package/dist/launcher.sea.cjs
CHANGED
|
@@ -24,6 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
defaultRunEntry: () => defaultRunEntry,
|
|
25
25
|
ensureFreshToken: () => ensureFreshToken,
|
|
26
26
|
formatMb: () => formatMb,
|
|
27
|
+
parseArgv: () => parseArgv,
|
|
27
28
|
readPayloadEntry: () => readPayloadEntry,
|
|
28
29
|
readPayloadRun: () => readPayloadRun,
|
|
29
30
|
readPayloadVerbs: () => readPayloadVerbs,
|
|
@@ -329,6 +330,11 @@ function parseProductConfig(text) {
|
|
|
329
330
|
} catch {
|
|
330
331
|
throw new Error("product config host must be an http(s) URL");
|
|
331
332
|
}
|
|
333
|
+
const dl = field(record, "dl").replace(/\/+$/, "");
|
|
334
|
+
if (dl) {
|
|
335
|
+
if (!/^https:\/\//u.test(dl)) throw new Error("product config dl must be an https URL");
|
|
336
|
+
config.dl = dl;
|
|
337
|
+
}
|
|
332
338
|
return config;
|
|
333
339
|
}
|
|
334
340
|
function field(record, key) {
|
|
@@ -704,10 +710,11 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
|
|
|
704
710
|
}
|
|
705
711
|
|
|
706
712
|
// src/index.ts
|
|
707
|
-
var
|
|
708
|
-
var
|
|
709
|
-
var
|
|
710
|
-
var
|
|
713
|
+
var import_node_child_process6 = require("node:child_process");
|
|
714
|
+
var import_node_fs12 = require("node:fs");
|
|
715
|
+
var import_node_os5 = require("node:os");
|
|
716
|
+
var import_node_path7 = require("node:path");
|
|
717
|
+
var import_node_sea2 = require("node:sea");
|
|
711
718
|
var import_node_url2 = require("node:url");
|
|
712
719
|
|
|
713
720
|
// ../face/src/face.ts
|
|
@@ -718,6 +725,7 @@ var PRODUCTS = Object.freeze({
|
|
|
718
725
|
"mm-strategy": Object.freeze({
|
|
719
726
|
name: "MM Strategy",
|
|
720
727
|
installWarm: "Welcome. Let's get you ready.",
|
|
728
|
+
checkWarm: "Here's where things stand.",
|
|
721
729
|
accent: "38;2;249;115;22",
|
|
722
730
|
warm: "Welcome back. Checking for updates...",
|
|
723
731
|
doctor: "mm-strategy doctor"
|
|
@@ -725,6 +733,7 @@ var PRODUCTS = Object.freeze({
|
|
|
725
733
|
"mmi-hub": Object.freeze({
|
|
726
734
|
name: "mmi-hub",
|
|
727
735
|
installWarm: "Welcome. Let's get you ready.",
|
|
736
|
+
checkWarm: "Here's where things stand.",
|
|
728
737
|
accent: "38;2;125;211;252",
|
|
729
738
|
warm: "Welcome back. Checking for updates...",
|
|
730
739
|
doctor: "mmi doctor"
|
|
@@ -732,6 +741,7 @@ var PRODUCTS = Object.freeze({
|
|
|
732
741
|
"jerv-hub": Object.freeze({
|
|
733
742
|
name: "jerv-hub",
|
|
734
743
|
installWarm: "Welcome. Let's get you ready.",
|
|
744
|
+
checkWarm: "Here's where things stand.",
|
|
735
745
|
accent: "38;2;248;113;113",
|
|
736
746
|
warm: "Welcome back. Checking for updates...",
|
|
737
747
|
doctor: "jerv doctor"
|
|
@@ -739,6 +749,7 @@ var PRODUCTS = Object.freeze({
|
|
|
739
749
|
jervcode: Object.freeze({
|
|
740
750
|
name: "JervCode",
|
|
741
751
|
installWarm: "Welcome. Let's get you ready.",
|
|
752
|
+
checkWarm: "Here's where things stand.",
|
|
742
753
|
accent: "38;2;192;132;252",
|
|
743
754
|
warm: "Welcome back. Checking for updates...",
|
|
744
755
|
doctor: "jervcode doctor"
|
|
@@ -854,7 +865,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
854
865
|
const welcome = () => continuesFace || nested ? [] : [
|
|
855
866
|
`${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
|
|
856
867
|
bar(),
|
|
857
|
-
`${bar()} ${operation === "install" ? identity.installWarm : identity.warm}`,
|
|
868
|
+
`${bar()} ${operation === "install" ? identity.installWarm : operation === "check" ? identity.checkWarm : identity.warm}`,
|
|
858
869
|
bar()
|
|
859
870
|
];
|
|
860
871
|
const continues = (phase, kind = "ok") => {
|
|
@@ -884,7 +895,11 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
884
895
|
const content = Math.min(width - 6, Math.max(...body.map(visibleWidth)));
|
|
885
896
|
const rule = paint(identity.accent, GLYPH.rule.repeat(content + 4));
|
|
886
897
|
const frame = (left, right) => `${paint(identity.accent, left)}${rule}${paint(identity.accent, right)}`;
|
|
887
|
-
const
|
|
898
|
+
const headGlyph = { [GLYPH.check]: identity.accent, [GLYPH.cross]: identity.accent, [GLYPH.dot]: PALETTE.muted };
|
|
899
|
+
const bodyRow = (line) => {
|
|
900
|
+
const sgr = headGlyph[line.charAt(0)];
|
|
901
|
+
return sgr ? `${paint(sgr, line.charAt(0))}${line.slice(1)}` : line;
|
|
902
|
+
};
|
|
888
903
|
return [
|
|
889
904
|
frame(GLYPH.boxTop, GLYPH.boxTopEnd),
|
|
890
905
|
...body.map((line) => `${paint(identity.accent, GLYPH.bar)} ${bodyRow(line)}${" ".repeat(Math.max(0, content - visibleWidth(line)))} ${paint(identity.accent, GLYPH.bar)}`),
|
|
@@ -910,6 +925,9 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
910
925
|
// ../face/src/shell.ts
|
|
911
926
|
var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
|
|
912
927
|
|
|
928
|
+
// ../face/src/rows.ts
|
|
929
|
+
var plainText = (text) => String(text).replaceAll("\u2192", "->").replaceAll("\xB7", "-").replaceAll("\u2014", "-").replaceAll("\u2026", "...");
|
|
930
|
+
|
|
913
931
|
// ../face/src/spinner.ts
|
|
914
932
|
var import_node_fs7 = require("node:fs");
|
|
915
933
|
var import_node_worker_threads = require("node:worker_threads");
|
|
@@ -1142,22 +1160,44 @@ function validateInstallerProduct(value) {
|
|
|
1142
1160
|
if (doctor !== identity.doctor) return fail("doctor");
|
|
1143
1161
|
if (!Array.isArray(input.surfaces) || input.surfaces.length === 0) return fail("surfaces");
|
|
1144
1162
|
const ids = /* @__PURE__ */ new Set();
|
|
1145
|
-
const surfaces = input.surfaces.map((
|
|
1146
|
-
if (!
|
|
1147
|
-
const
|
|
1148
|
-
const id = text(
|
|
1163
|
+
const surfaces = input.surfaces.map((raw2) => {
|
|
1164
|
+
if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return fail("surface");
|
|
1165
|
+
const source2 = raw2;
|
|
1166
|
+
const id = text(source2.id, "surface.id");
|
|
1149
1167
|
if (ids.has(id)) return fail("duplicate surface.id");
|
|
1150
1168
|
ids.add(id);
|
|
1151
1169
|
const surface = { id };
|
|
1152
1170
|
for (const field2 of ["npm", "bin", "kind", "activation"]) {
|
|
1153
|
-
if (
|
|
1171
|
+
if (source2[field2] !== void 0) surface[field2] = text(source2[field2], `surface.${field2}`);
|
|
1154
1172
|
}
|
|
1155
1173
|
if (Boolean(surface.npm) !== Boolean(surface.bin)) return fail("surface npm/bin pair");
|
|
1156
1174
|
if (surface.kind !== void 0 && !["agent-home", "payload"].includes(surface.kind)) return fail("surface.kind");
|
|
1157
1175
|
if (!surface.npm && !surface.kind) return fail("surface implementation");
|
|
1158
1176
|
return surface;
|
|
1159
1177
|
});
|
|
1160
|
-
return { product, gate, doctor, surfaces };
|
|
1178
|
+
if (input.launcher === void 0) return { product, gate, doctor, surfaces };
|
|
1179
|
+
const raw = input.launcher;
|
|
1180
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail("launcher");
|
|
1181
|
+
const source = raw;
|
|
1182
|
+
const https = (value2, field2) => {
|
|
1183
|
+
const url = text(value2, field2).replace(/\/+$/u, "");
|
|
1184
|
+
let parsed;
|
|
1185
|
+
try {
|
|
1186
|
+
parsed = new URL(url);
|
|
1187
|
+
} catch {
|
|
1188
|
+
return fail(field2);
|
|
1189
|
+
}
|
|
1190
|
+
return parsed.protocol === "https:" && !parsed.username && !parsed.password ? url : fail(field2);
|
|
1191
|
+
};
|
|
1192
|
+
const launcher = {
|
|
1193
|
+
product: text(source.product, "launcher.product"),
|
|
1194
|
+
bin: text(source.bin, "launcher.bin"),
|
|
1195
|
+
key: text(source.key, "launcher.key"),
|
|
1196
|
+
host: https(source.host, "launcher.host")
|
|
1197
|
+
};
|
|
1198
|
+
if (source.dl !== void 0) launcher.dl = https(source.dl, "launcher.dl");
|
|
1199
|
+
if (Buffer.from(launcher.key, "base64").length !== 32) return fail("launcher.key");
|
|
1200
|
+
return { product, gate, doctor, surfaces, launcher };
|
|
1161
1201
|
}
|
|
1162
1202
|
var PHASES = {
|
|
1163
1203
|
preflight: ["Checking prerequisites", "Checked prerequisites"],
|
|
@@ -1182,6 +1222,23 @@ var UPDATE_CHECK_PHASES = /* @__PURE__ */ new Set([
|
|
|
1182
1222
|
"sign-in",
|
|
1183
1223
|
"check"
|
|
1184
1224
|
]);
|
|
1225
|
+
var SURFACE_WORDS = Object.freeze({
|
|
1226
|
+
updated: "updated",
|
|
1227
|
+
current: "already current",
|
|
1228
|
+
failed: "failed",
|
|
1229
|
+
skipped: "skipped",
|
|
1230
|
+
retry: "retrying",
|
|
1231
|
+
pending: "pending",
|
|
1232
|
+
kept: "kept",
|
|
1233
|
+
/** Read-only report states. */
|
|
1234
|
+
behind: "update available",
|
|
1235
|
+
ahead: "ahead of the release",
|
|
1236
|
+
staged: "next launch",
|
|
1237
|
+
unknown: "unknown",
|
|
1238
|
+
missing: "not installed"
|
|
1239
|
+
});
|
|
1240
|
+
var AUTO_UPDATE = "auto-update";
|
|
1241
|
+
var LAUNCHER_SURFACE = "launcher";
|
|
1185
1242
|
function createInstallerRun(value, options = {}) {
|
|
1186
1243
|
const declaration = validateInstallerProduct(value);
|
|
1187
1244
|
const env = options.env ?? process.env;
|
|
@@ -1309,11 +1366,11 @@ function createInstallerRun(value, options = {}) {
|
|
|
1309
1366
|
},
|
|
1310
1367
|
surface(facts) {
|
|
1311
1368
|
if (finished) throw new Error("installer run already finished");
|
|
1312
|
-
const surface = declaration.surfaces.find((surface2) => surface2.id === facts.id);
|
|
1369
|
+
const surface = declaration.surfaces.find((surface2) => surface2.id === facts.id) ?? (facts.id === LAUNCHER_SURFACE ? { id: LAUNCHER_SURFACE } : void 0);
|
|
1313
1370
|
if (!surface) throw new Error("installer run: undeclared surface");
|
|
1314
1371
|
start();
|
|
1315
1372
|
const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} ${tty ? "\u2192" : "->"} ${facts.to}` : ` ${facts.to}` : "";
|
|
1316
|
-
const status =
|
|
1373
|
+
const status = facts.state === "updated" && options.dryRun ? "would update" : SURFACE_WORDS[facts.state];
|
|
1317
1374
|
if (!status) throw new Error("installer run: unknown surface state");
|
|
1318
1375
|
const separator = tty ? "\xB7" : "-";
|
|
1319
1376
|
const activation = facts.state === "updated" && surface.activation ? ` ${separator} ${surface.activation}` : "";
|
|
@@ -1421,8 +1478,9 @@ function createInstallerRun(value, options = {}) {
|
|
|
1421
1478
|
if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
|
|
1422
1479
|
const changed = !facts.version ? "No release target is available." : facts.dryRun ? `Would update ${facts.updated} of ${facts.total} surfaces to ${facts.version}.` : facts.installed ? `Installed ${facts.version} across ${facts.total} surfaces.` : `Updated ${facts.updated} of ${facts.total} surfaces to ${facts.version}.`;
|
|
1423
1480
|
const ready = facts.failed === 0 && !facts.dryRun && !facts.deferred && !facts.operationFailed && Boolean(facts.version);
|
|
1481
|
+
const deferred = Boolean(facts.deferred) && facts.failed === 0 && !facts.operationFailed;
|
|
1424
1482
|
const body = [
|
|
1425
|
-
`${ready ? GLYPH.check : facts.dryRun ? GLYPH.dot : GLYPH.cross} ${face.identity.name}${ready ? " is ready." : facts.dryRun ? " preview complete." :
|
|
1483
|
+
`${ready ? GLYPH.check : facts.dryRun || deferred ? GLYPH.dot : GLYPH.cross} ${face.identity.name}${ready ? " is ready." : facts.dryRun ? " preview complete." : deferred ? ` ${operation === "install" ? "install" : "update"} deferred.` : " is not ready yet."}`,
|
|
1426
1484
|
changed,
|
|
1427
1485
|
...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
|
|
1428
1486
|
...facts.detail ? [facts.detail] : [],
|
|
@@ -1445,6 +1503,89 @@ function createInstallerRun(value, options = {}) {
|
|
|
1445
1503
|
return run2;
|
|
1446
1504
|
}
|
|
1447
1505
|
|
|
1506
|
+
// ../face/src/screens.ts
|
|
1507
|
+
function screenLane(render = {}, stream = "stdout") {
|
|
1508
|
+
const env = render.env ?? process.env;
|
|
1509
|
+
const out = stream === "stdout" ? process.stdout : process.stderr;
|
|
1510
|
+
const tty = render.tty ?? Boolean(out.isTTY);
|
|
1511
|
+
const color = tty && render.color !== false && env.NO_COLOR === void 0 && env.TERM !== "dumb";
|
|
1512
|
+
return { tty, color, columns: render.columns ?? out.columns, env };
|
|
1513
|
+
}
|
|
1514
|
+
var faceFor = (product, lane, operation) => createFace({ product, color: lane.color, columns: lane.columns, env: { ...lane.env, MM_OUTER_CONSOLE: void 0, MM_FACE_CONTINUES: void 0, MM_PROGRESS_FD: void 0, MM_PROGRESS_FILE: void 0 }, operation });
|
|
1515
|
+
function renderNotice(product, notice, render) {
|
|
1516
|
+
const lane = screenLane(render, notice.state === "fail" ? "stderr" : "stdout");
|
|
1517
|
+
const face = faceFor(product, lane);
|
|
1518
|
+
const width = face.width - 3;
|
|
1519
|
+
const [first, ...rest] = wrapWords(plain(notice.message, lane), width);
|
|
1520
|
+
if (!lane.tty) {
|
|
1521
|
+
return [`${notice.state === "fail" ? "Error: " : ""}${[first, ...rest].join(" ")}`, ...notice.next ? [`Next: ${notice.next}`] : []];
|
|
1522
|
+
}
|
|
1523
|
+
const glyph = notice.state === "fail" ? face.paint(face.identity.accent, GLYPH.cross) : notice.state === "note" ? face.paint(PALETTE.muted, GLYPH.dot) : face.paint(face.identity.accent, GLYPH.check);
|
|
1524
|
+
return [
|
|
1525
|
+
`${glyph} ${first}`,
|
|
1526
|
+
...rest.map((line) => ` ${line}`),
|
|
1527
|
+
...notice.next ? [` ${face.paint(PALETTE.muted, "Next:")} ${notice.next}`] : []
|
|
1528
|
+
];
|
|
1529
|
+
}
|
|
1530
|
+
function renderReport(product, report, render) {
|
|
1531
|
+
const lane = screenLane(render);
|
|
1532
|
+
const face = faceFor(product, lane, "check");
|
|
1533
|
+
const { verdict } = report;
|
|
1534
|
+
const headline = `${face.identity.name} ${verdict.headline}`;
|
|
1535
|
+
if (!lane.tty) {
|
|
1536
|
+
return [
|
|
1537
|
+
`${face.identity.name} - Mutatis Mutandis`,
|
|
1538
|
+
face.identity.checkWarm,
|
|
1539
|
+
...report.rows.map((row) => `${row.mark === "fail" ? "Failed: " : ""}${plainText(rowTitle(row))}`),
|
|
1540
|
+
...report.rows.flatMap((row) => row.note ? [` ${plainText(row.note)}`] : []),
|
|
1541
|
+
`${verdict.state === "fail" ? "Error: " : ""}${plainText(headline)}`,
|
|
1542
|
+
...(verdict.facts ?? []).map(plainText),
|
|
1543
|
+
`Next: ${verdict.next}`
|
|
1544
|
+
];
|
|
1545
|
+
}
|
|
1546
|
+
const glyph = verdict.state === "fail" ? GLYPH.cross : verdict.state === "note" ? GLYPH.dot : GLYPH.check;
|
|
1547
|
+
const facts = verdict.facts?.length ? verdict.facts : [verdict.state === "ok" ? "Everything checked passed." : "See the rows above."];
|
|
1548
|
+
return [
|
|
1549
|
+
...face.welcome(),
|
|
1550
|
+
// A report row IS an update row: glyph, subject, state (#7071). Two layouts for one fact — a
|
|
1551
|
+
// mark mid-row after a subject column here, a leading glyph in `update` — is the drift itself.
|
|
1552
|
+
...report.rows.flatMap((row) => [face.step(rowTitle(row), null, row.mark ?? "note"), ...row.note ? [face.relay(row.note)] : []]),
|
|
1553
|
+
...report.rows.length ? [face.paint(PALETTE.muted, GLYPH.bar)] : [],
|
|
1554
|
+
...face.receipt([`${glyph} ${headline}`, ...facts, `Next: ${verdict.next}`], { ready: verdict.state === "ok" }),
|
|
1555
|
+
face.signOff()
|
|
1556
|
+
].filter((line) => line !== "");
|
|
1557
|
+
}
|
|
1558
|
+
function renderUsage(product, usage2, render) {
|
|
1559
|
+
const lane = screenLane(render);
|
|
1560
|
+
const face = faceFor(product, lane);
|
|
1561
|
+
const all = [...usage2.commands, ...usage2.options ?? []];
|
|
1562
|
+
const column = Math.min(Math.max(...all.map((entry2) => entry2.usage.length)), 34);
|
|
1563
|
+
const table = (entries) => entries.flatMap((entry2) => {
|
|
1564
|
+
const [first, ...rest] = wrapWords(plain(entry2.text, lane), Math.max(20, face.width - column - 6));
|
|
1565
|
+
const head = entry2.usage.length > column ? [` ${entry2.usage}`, ` ${" ".repeat(column)} ${first}`] : [` ${entry2.usage.padEnd(column)} ${first}`];
|
|
1566
|
+
return [...head, ...rest.map((line) => ` ${" ".repeat(column)} ${line}`)];
|
|
1567
|
+
});
|
|
1568
|
+
return [
|
|
1569
|
+
`${face.paint(face.identity.accent, usage2.bin)} ${usage2.version} ${lane.tty ? "\xB7" : "-"} ${plain(usage2.summary, lane)}`,
|
|
1570
|
+
"",
|
|
1571
|
+
`Usage: ${usage2.bin} <command> [options]`,
|
|
1572
|
+
"",
|
|
1573
|
+
"Commands:",
|
|
1574
|
+
...table(usage2.commands),
|
|
1575
|
+
...usage2.options?.length ? ["", "Options:", ...table(usage2.options)] : []
|
|
1576
|
+
];
|
|
1577
|
+
}
|
|
1578
|
+
function renderVersion(bin, version, launcher) {
|
|
1579
|
+
return `${bin} ${version}${launcher ? ` (launcher ${launcher})` : ""}`;
|
|
1580
|
+
}
|
|
1581
|
+
function printScreen(lines, stream = "stdout") {
|
|
1582
|
+
const out = stream === "stdout" ? process.stdout : process.stderr;
|
|
1583
|
+
out.write(lines.map((line) => `${line}
|
|
1584
|
+
`).join(""));
|
|
1585
|
+
}
|
|
1586
|
+
var plain = (text, lane) => lane.tty ? String(text) : plainText(stripColor(String(text)));
|
|
1587
|
+
var rowTitle = (row) => `${row.surface}${row.aspect ? ` ${row.aspect}` : ""}${row.instance ? ` \xB7 ${row.instance}` : ""} ${row.state}`;
|
|
1588
|
+
|
|
1448
1589
|
// src/autoupdate.ts
|
|
1449
1590
|
var import_node_child_process3 = require("node:child_process");
|
|
1450
1591
|
var import_node_fs10 = require("node:fs");
|
|
@@ -1475,19 +1616,33 @@ function scheduleName(config) {
|
|
|
1475
1616
|
function scheduleLabel(config) {
|
|
1476
1617
|
return `${config.binName}-autoupdate`.replace(/[^A-Za-z0-9_-]+/g, "-");
|
|
1477
1618
|
}
|
|
1619
|
+
var HIDDEN_SCRIPT = "autoupdate-hidden.vbs";
|
|
1620
|
+
function hiddenVbs(command2) {
|
|
1621
|
+
const line = command2.map((arg) => `""${arg.replaceAll('"', '""')}""`).join(" ");
|
|
1622
|
+
return `' Generated by \`autoupdate on\`: wscript //B runs this with no window, so the hourly update
|
|
1623
|
+
' never flashes a console. The exit code is the update's, for Task Scheduler's last result.
|
|
1624
|
+
Dim shell, code
|
|
1625
|
+
Set shell = CreateObject("WScript.Shell")
|
|
1626
|
+
code = shell.Run("${line}", 0, True)
|
|
1627
|
+
WScript.Quit code
|
|
1628
|
+
`;
|
|
1629
|
+
}
|
|
1478
1630
|
function quoteWindows(arg) {
|
|
1479
1631
|
return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
|
|
1480
1632
|
}
|
|
1481
1633
|
function enableSchedule(config, command2, options = {}) {
|
|
1482
1634
|
const platform = schedulePlatform(options.platform);
|
|
1483
|
-
if (!platform) throw new Error(`
|
|
1635
|
+
if (!platform) throw new Error(`Auto-update is not supported on ${options.platform ?? process.platform}.`);
|
|
1484
1636
|
const exec = options.exec ?? defaultExec;
|
|
1485
1637
|
const home = homeOf(options);
|
|
1486
1638
|
if (platform === "win32") {
|
|
1487
|
-
const
|
|
1488
|
-
|
|
1639
|
+
const stateDir = options.stateDir ?? defaultProductDir(config.product);
|
|
1640
|
+
(0, import_node_fs10.mkdirSync)(stateDir, { recursive: true });
|
|
1641
|
+
const script = (0, import_node_path5.join)(stateDir, HIDDEN_SCRIPT);
|
|
1642
|
+
(0, import_node_fs10.writeFileSync)(script, hiddenVbs(command2));
|
|
1643
|
+
const result2 = exec("schtasks", ["/Create", "/TN", scheduleName(config), "/TR", `wscript.exe //B //Nologo ${quoteWindows(script)}`, "/SC", "HOURLY", "/IT", "/F"]);
|
|
1489
1644
|
if (result2.code !== 0) {
|
|
1490
|
-
throw new Error(`
|
|
1645
|
+
throw new Error(`Could not turn auto-update on: ${firstLine(result2.stdout || result2.stderr) || `exit ${result2.code}`}`);
|
|
1491
1646
|
}
|
|
1492
1647
|
return;
|
|
1493
1648
|
}
|
|
@@ -1500,7 +1655,7 @@ function enableSchedule(config, command2, options = {}) {
|
|
|
1500
1655
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
1501
1656
|
const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
|
|
1502
1657
|
if (result2.code !== 0) {
|
|
1503
|
-
throw new Error(`
|
|
1658
|
+
throw new Error(`Could not turn auto-update on: ${firstLine(result2.stderr || result2.stdout) || `exit ${result2.code}`}`);
|
|
1504
1659
|
}
|
|
1505
1660
|
return;
|
|
1506
1661
|
}
|
|
@@ -1511,23 +1666,24 @@ function enableSchedule(config, command2, options = {}) {
|
|
|
1511
1666
|
(0, import_node_fs10.writeFileSync)((0, import_node_path5.join)(dir, `${label}.timer`), linuxTimer(label));
|
|
1512
1667
|
const reload = exec("systemctl", ["--user", "daemon-reload"]);
|
|
1513
1668
|
if (reload.code !== 0) {
|
|
1514
|
-
throw new Error(`
|
|
1669
|
+
throw new Error(`Could not turn auto-update on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
|
|
1515
1670
|
}
|
|
1516
1671
|
const result = exec("systemctl", ["--user", "enable", "--now", `${label}.timer`]);
|
|
1517
1672
|
if (result.code !== 0) {
|
|
1518
|
-
throw new Error(`
|
|
1673
|
+
throw new Error(`Could not turn auto-update on: ${firstLine(result.stderr || result.stdout) || `exit ${result.code}`}`);
|
|
1519
1674
|
}
|
|
1520
1675
|
}
|
|
1521
1676
|
function disableSchedule(config, options = {}) {
|
|
1522
1677
|
const platform = schedulePlatform(options.platform);
|
|
1523
|
-
if (!platform) throw new Error(`
|
|
1678
|
+
if (!platform) throw new Error(`Auto-update is not supported on ${options.platform ?? process.platform}.`);
|
|
1524
1679
|
const exec = options.exec ?? defaultExec;
|
|
1525
1680
|
const home = homeOf(options);
|
|
1526
1681
|
if (platform === "win32") {
|
|
1527
1682
|
const result = exec("schtasks", ["/Delete", "/TN", scheduleName(config), "/F"]);
|
|
1528
1683
|
if (result.code !== 0 && !/cannot find|does not exist/i.test(`${result.stdout} ${result.stderr}`)) {
|
|
1529
|
-
throw new Error(`
|
|
1684
|
+
throw new Error(`Could not turn auto-update off: ${firstLine(result.stdout || result.stderr) || `exit ${result.code}`}`);
|
|
1530
1685
|
}
|
|
1686
|
+
(0, import_node_fs10.rmSync)((0, import_node_path5.join)(options.stateDir ?? defaultProductDir(config.product), HIDDEN_SCRIPT), { force: true });
|
|
1531
1687
|
return;
|
|
1532
1688
|
}
|
|
1533
1689
|
if (platform === "darwin") {
|
|
@@ -1620,15 +1776,170 @@ function firstLine(text) {
|
|
|
1620
1776
|
return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
|
|
1621
1777
|
}
|
|
1622
1778
|
|
|
1623
|
-
// src/
|
|
1779
|
+
// src/refresh.ts
|
|
1624
1780
|
var import_node_child_process4 = require("node:child_process");
|
|
1781
|
+
var import_node_fs11 = require("node:fs");
|
|
1782
|
+
var import_node_os4 = require("node:os");
|
|
1783
|
+
var import_node_path6 = require("node:path");
|
|
1784
|
+
var LAUNCHER_RECORD_FILE = "launcher.json";
|
|
1785
|
+
var RECORD_TIMEOUT_MS = 1e4;
|
|
1786
|
+
function platformKey(platform, arch) {
|
|
1787
|
+
if (platform === "win32") return arch === "x64" || arch === "arm64" ? `win-${arch}` : null;
|
|
1788
|
+
return platform === "darwin" && arch === "arm64" ? "darwin-arm64" : null;
|
|
1789
|
+
}
|
|
1790
|
+
function verifierFor(target) {
|
|
1791
|
+
return { product: target.product, host: target.host, loginKind: "github", publicKey: target.key, binName: target.bin };
|
|
1792
|
+
}
|
|
1793
|
+
async function fetchRecord(target, options) {
|
|
1794
|
+
let json;
|
|
1795
|
+
try {
|
|
1796
|
+
const response = await (options.fetch ?? fetch)(
|
|
1797
|
+
`${target.host.replace(/\/+$/u, "")}/release/launcher`,
|
|
1798
|
+
{ signal: AbortSignal.timeout(options.timeoutMs ?? RECORD_TIMEOUT_MS) }
|
|
1799
|
+
);
|
|
1800
|
+
if (!response.ok) return null;
|
|
1801
|
+
json = await response.json();
|
|
1802
|
+
} catch {
|
|
1803
|
+
return null;
|
|
1804
|
+
}
|
|
1805
|
+
let record;
|
|
1806
|
+
try {
|
|
1807
|
+
record = parseManifest(json);
|
|
1808
|
+
} catch {
|
|
1809
|
+
return null;
|
|
1810
|
+
}
|
|
1811
|
+
if (!verifyManifest(record, record.signature, verifierFor(target))) return "bad-signature";
|
|
1812
|
+
return { version: record.version, files: record.files };
|
|
1813
|
+
}
|
|
1814
|
+
async function readLauncherRecord(target, options = {}) {
|
|
1815
|
+
const record = await fetchRecord(target, options);
|
|
1816
|
+
return record === "bad-signature" ? null : record;
|
|
1817
|
+
}
|
|
1818
|
+
function compareVersions(a, b) {
|
|
1819
|
+
const split = (v) => {
|
|
1820
|
+
const [core, pre = ""] = v.split("-", 2);
|
|
1821
|
+
return { nums: core.split(".").map((n) => Number.parseInt(n, 10) || 0), pre };
|
|
1822
|
+
};
|
|
1823
|
+
const x = split(a);
|
|
1824
|
+
const y = split(b);
|
|
1825
|
+
for (let i = 0; i < 3; i++) if ((x.nums[i] ?? 0) !== (y.nums[i] ?? 0)) return (x.nums[i] ?? 0) > (y.nums[i] ?? 0) ? 1 : -1;
|
|
1826
|
+
if (x.pre === y.pre) return 0;
|
|
1827
|
+
if (!x.pre) return 1;
|
|
1828
|
+
if (!y.pre) return -1;
|
|
1829
|
+
return x.pre > y.pre ? 1 : -1;
|
|
1830
|
+
}
|
|
1831
|
+
function recordedPath(productDir) {
|
|
1832
|
+
try {
|
|
1833
|
+
const saved = JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path6.join)(productDir, LAUNCHER_RECORD_FILE), "utf8"));
|
|
1834
|
+
return typeof saved.path === "string" && saved.path ? saved.path : null;
|
|
1835
|
+
} catch {
|
|
1836
|
+
return null;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
function installedPath(target, productDir, platform) {
|
|
1840
|
+
return platform === "win32" ? (0, import_node_path6.join)(productDir, "bin", `${target.bin}.exe`) : (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".local", "bin", target.bin);
|
|
1841
|
+
}
|
|
1842
|
+
function parseLauncherVersion(stdout, bin) {
|
|
1843
|
+
const text = String(stdout).trim();
|
|
1844
|
+
if (!text.startsWith(`${bin} `)) return null;
|
|
1845
|
+
return text.match(/launcher (\d+\.\d+\.\d+[^\s)]*)/u)?.[1] ?? null;
|
|
1846
|
+
}
|
|
1847
|
+
function runVersion(path, bin) {
|
|
1848
|
+
return new Promise((resolve3) => {
|
|
1849
|
+
(0, import_node_child_process4.execFile)(path, ["--version"], { timeout: 3e4, windowsHide: true, encoding: "utf8" }, (error, stdout) => {
|
|
1850
|
+
resolve3(error ? null : parseLauncherVersion(stdout, bin));
|
|
1851
|
+
});
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
function removeStale(file) {
|
|
1855
|
+
const prefix = `${(0, import_node_path6.basename)(file)}.old-`;
|
|
1856
|
+
let names;
|
|
1857
|
+
try {
|
|
1858
|
+
names = (0, import_node_fs11.readdirSync)((0, import_node_path6.dirname)(file));
|
|
1859
|
+
} catch {
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
for (const name of names) {
|
|
1863
|
+
if (!name.startsWith(prefix)) continue;
|
|
1864
|
+
try {
|
|
1865
|
+
(0, import_node_fs11.rmSync)((0, import_node_path6.join)((0, import_node_path6.dirname)(file), name), { force: true });
|
|
1866
|
+
} catch {
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
async function refreshLauncher(target, options = {}) {
|
|
1871
|
+
const started = Date.now();
|
|
1872
|
+
const platform = options.platform ?? process.platform;
|
|
1873
|
+
const key = platformKey(platform, options.arch ?? process.arch);
|
|
1874
|
+
if (!key) return null;
|
|
1875
|
+
const productDir = options.productDir ?? defaultProductDir(target.product);
|
|
1876
|
+
const recorded = recordedPath(productDir);
|
|
1877
|
+
const file = options.path ?? (recorded && (0, import_node_fs11.existsSync)(recorded) ? recorded : installedPath(target, productDir, platform));
|
|
1878
|
+
if (!(0, import_node_fs11.existsSync)(file) || (0, import_node_fs11.lstatSync)(file).isSymbolicLink()) return null;
|
|
1879
|
+
if (!options.dryRun) removeStale(file);
|
|
1880
|
+
const from = await (options.readVersion ?? ((path) => runVersion(path, target.bin)))(file);
|
|
1881
|
+
if (!from) return null;
|
|
1882
|
+
const arm = (verdict, to, detail) => ({ surface: "launcher", from, to, verdict, detail, durationMs: Date.now() - started });
|
|
1883
|
+
const record = await fetchRecord(target, options);
|
|
1884
|
+
if (record === "bad-signature") return arm("fail", from, "The launcher record failed its signature check.");
|
|
1885
|
+
const entry2 = record?.files.find((candidate) => candidate.path === key);
|
|
1886
|
+
if (!record || !entry2) return arm("skip", from, "No launcher update is offered yet.");
|
|
1887
|
+
if (compareVersions(record.version, from) <= 0) return arm("ok", from, "");
|
|
1888
|
+
if (options.dryRun) return arm("ok", record.version, "");
|
|
1889
|
+
let unlock;
|
|
1890
|
+
if (!options.lockHeld) {
|
|
1891
|
+
try {
|
|
1892
|
+
unlock = lockInstallation(productDir);
|
|
1893
|
+
} catch {
|
|
1894
|
+
return arm("defer", from, "Another install is running; the launcher updates next time.");
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
const next = `${file}.new-${process.pid}`;
|
|
1898
|
+
let moved;
|
|
1899
|
+
try {
|
|
1900
|
+
let bytes;
|
|
1901
|
+
try {
|
|
1902
|
+
const response = await (options.fetch ?? fetch)(
|
|
1903
|
+
`${(target.dl ?? target.host).replace(/\/+$/u, "")}/dl/${target.product}/${key}`,
|
|
1904
|
+
{ signal: AbortSignal.timeout(3e5) }
|
|
1905
|
+
);
|
|
1906
|
+
if (!response.ok) return arm("defer", from, "The launcher download did not complete; it retries next time.");
|
|
1907
|
+
bytes = await readDownload(response, void 0, entry2.size);
|
|
1908
|
+
} catch {
|
|
1909
|
+
return arm("defer", from, "The launcher download did not complete; it retries next time.");
|
|
1910
|
+
}
|
|
1911
|
+
if (!verifyFileBytes(entry2, bytes)) return arm("fail", from, "The downloaded launcher failed its checksum.");
|
|
1912
|
+
try {
|
|
1913
|
+
(0, import_node_fs11.writeFileSync)(next, bytes, { mode: 493 });
|
|
1914
|
+
if (platform !== "win32") (0, import_node_fs11.chmodSync)(next, 493);
|
|
1915
|
+
if (platform === "win32") {
|
|
1916
|
+
moved = `${file}.old-${process.pid}-${Date.now()}`;
|
|
1917
|
+
(0, import_node_fs11.renameSync)(file, moved);
|
|
1918
|
+
}
|
|
1919
|
+
(0, import_node_fs11.renameSync)(next, file);
|
|
1920
|
+
} catch {
|
|
1921
|
+
try {
|
|
1922
|
+
if (moved && !(0, import_node_fs11.existsSync)(file)) (0, import_node_fs11.renameSync)(moved, file);
|
|
1923
|
+
} catch {
|
|
1924
|
+
}
|
|
1925
|
+
(0, import_node_fs11.rmSync)(next, { force: true });
|
|
1926
|
+
return arm("fail", from, "The launcher could not be replaced.");
|
|
1927
|
+
}
|
|
1928
|
+
return arm("ok", record.version, "");
|
|
1929
|
+
} finally {
|
|
1930
|
+
unlock?.();
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// src/login-github.ts
|
|
1935
|
+
var import_node_child_process5 = require("node:child_process");
|
|
1625
1936
|
var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1626
1937
|
function openBrowser(url) {
|
|
1627
1938
|
if (process.env.LAUNCHER_NO_OPEN === "1") return;
|
|
1628
1939
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
|
|
1629
1940
|
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
1630
1941
|
try {
|
|
1631
|
-
const child = (0,
|
|
1942
|
+
const child = (0, import_node_child_process5.spawn)(opener, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
1632
1943
|
child.on("error", () => {
|
|
1633
1944
|
});
|
|
1634
1945
|
child.unref();
|
|
@@ -1738,7 +2049,33 @@ async function refreshAccessToken(config, refreshToken, fetchImpl = fetch) {
|
|
|
1738
2049
|
if (typeof data.access_token !== "string" || typeof data.expires_in !== "number") {
|
|
1739
2050
|
throw new Error("the sign-in server returned a malformed refresh");
|
|
1740
2051
|
}
|
|
1741
|
-
return {
|
|
2052
|
+
return {
|
|
2053
|
+
accessToken: data.access_token,
|
|
2054
|
+
expiresIn: data.expires_in,
|
|
2055
|
+
...typeof data.refresh_token === "string" ? { refreshToken: data.refresh_token } : {}
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
function readGhToken() {
|
|
2059
|
+
try {
|
|
2060
|
+
const result = (0, import_node_child_process5.spawnSync)("gh", ["auth", "token", "--hostname", "github.com"], { encoding: "utf8", timeout: 1e4, windowsHide: true });
|
|
2061
|
+
const token = result.status === 0 ? String(result.stdout).trim() : "";
|
|
2062
|
+
return token && !/\s/u.test(token) ? token : null;
|
|
2063
|
+
} catch {
|
|
2064
|
+
return null;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
async function loginWithGhToken(config, fetchImpl = fetch, readToken = readGhToken) {
|
|
2068
|
+
const token = readToken();
|
|
2069
|
+
if (!token) return null;
|
|
2070
|
+
try {
|
|
2071
|
+
const response = await postJson(`${config.host}/gate/github-token`, { token }, fetchImpl);
|
|
2072
|
+
if (!response.ok) return null;
|
|
2073
|
+
const data = await response.json();
|
|
2074
|
+
if (typeof data.access_token !== "string" || typeof data.refresh_token !== "string" || typeof data.expires_in !== "number") return null;
|
|
2075
|
+
return { accessToken: data.access_token, refreshToken: data.refresh_token, expiresIn: data.expires_in };
|
|
2076
|
+
} catch {
|
|
2077
|
+
return null;
|
|
2078
|
+
}
|
|
1742
2079
|
}
|
|
1743
2080
|
|
|
1744
2081
|
// src/login-google.ts
|
|
@@ -1855,7 +2192,7 @@ function page(title, body) {
|
|
|
1855
2192
|
}
|
|
1856
2193
|
|
|
1857
2194
|
// src/index.ts
|
|
1858
|
-
var LAUNCHER_VERSION = true ? "1.0
|
|
2195
|
+
var LAUNCHER_VERSION = true ? "1.2.0" : readVersionFromPackage();
|
|
1859
2196
|
function defaultPrint(message) {
|
|
1860
2197
|
process.stdout.write(`${message}
|
|
1861
2198
|
`);
|
|
@@ -1864,87 +2201,200 @@ function defaultPrintErr(message) {
|
|
|
1864
2201
|
process.stderr.write(`${message}
|
|
1865
2202
|
`);
|
|
1866
2203
|
}
|
|
2204
|
+
var LAUNCHER_VERBS = /* @__PURE__ */ new Set(["install", "update", "status", "doctor", "login", "logout", "autoupdate"]);
|
|
2205
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set(["--config", "--dir"]);
|
|
2206
|
+
var SWITCHES = {
|
|
2207
|
+
"--json": "json",
|
|
2208
|
+
"--no-color": "noColor",
|
|
2209
|
+
"--dry-run": "dryRun",
|
|
2210
|
+
"--help": "help",
|
|
2211
|
+
"-h": "help",
|
|
2212
|
+
"--version": "version",
|
|
2213
|
+
"-v": "version"
|
|
2214
|
+
};
|
|
2215
|
+
function parseArgv(argv2) {
|
|
2216
|
+
const parsed = { flags: {}, args: [], rest: [] };
|
|
2217
|
+
const flag = (i2) => {
|
|
2218
|
+
const arg = argv2[i2];
|
|
2219
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
2220
|
+
const value = argv2[i2 + 1];
|
|
2221
|
+
if (value === void 0 || value.startsWith("-")) {
|
|
2222
|
+
parsed.unknownFlag ??= arg;
|
|
2223
|
+
return i2 + 1;
|
|
2224
|
+
}
|
|
2225
|
+
parsed.flags[arg === "--config" ? "config" : "dir"] = value;
|
|
2226
|
+
return i2 + 2;
|
|
2227
|
+
}
|
|
2228
|
+
const name = SWITCHES[arg];
|
|
2229
|
+
if (!name) return -1;
|
|
2230
|
+
parsed.flags[name] = true;
|
|
2231
|
+
return i2 + 1;
|
|
2232
|
+
};
|
|
2233
|
+
const skip = (i2) => {
|
|
2234
|
+
const next = flag(i2);
|
|
2235
|
+
if (next >= 0) return next;
|
|
2236
|
+
parsed.unknownFlag ??= argv2[i2];
|
|
2237
|
+
return i2 + 1;
|
|
2238
|
+
};
|
|
2239
|
+
let i = 0;
|
|
2240
|
+
while (i < argv2.length && argv2[i].startsWith("-")) {
|
|
2241
|
+
if (argv2[i] === "--run") {
|
|
2242
|
+
parsed.run = { file: argv2[i + 1], args: argv2.slice(i + 2) };
|
|
2243
|
+
return parsed;
|
|
2244
|
+
}
|
|
2245
|
+
i = skip(i);
|
|
2246
|
+
}
|
|
2247
|
+
parsed.command = argv2[i];
|
|
2248
|
+
if (parsed.command === void 0) return parsed;
|
|
2249
|
+
if (!LAUNCHER_VERBS.has(parsed.command)) {
|
|
2250
|
+
parsed.rest = argv2.slice(i + 1);
|
|
2251
|
+
return parsed;
|
|
2252
|
+
}
|
|
2253
|
+
for (i += 1; i < argv2.length; ) {
|
|
2254
|
+
if (argv2[i].startsWith("-")) i = skip(i);
|
|
2255
|
+
else parsed.args.push(argv2[i++]);
|
|
2256
|
+
}
|
|
2257
|
+
return parsed;
|
|
2258
|
+
}
|
|
2259
|
+
var NO_PRODUCT = "mmi-hub";
|
|
2260
|
+
function sentence(message) {
|
|
2261
|
+
const text = message.trim().replace(/\s+—\s+/gu, ". ");
|
|
2262
|
+
if (!text) return "Something went wrong.";
|
|
2263
|
+
return `${text[0].toUpperCase()}${text.slice(1)}${/[.!?]$/u.test(text) ? "" : "."}`;
|
|
2264
|
+
}
|
|
2265
|
+
function screensFor(product, bin, render, options) {
|
|
2266
|
+
const show = (lines, stream = "stdout") => {
|
|
2267
|
+
const sink = stream === "stderr" ? options.printErr : options.print;
|
|
2268
|
+
if (sink) for (const line of lines) sink(line);
|
|
2269
|
+
else printScreen(lines, stream);
|
|
2270
|
+
};
|
|
2271
|
+
return {
|
|
2272
|
+
product,
|
|
2273
|
+
bin,
|
|
2274
|
+
render,
|
|
2275
|
+
show,
|
|
2276
|
+
notice: (state, message, next) => show(renderNotice(product, { state, message, ...next ? { next } : {} }, render)),
|
|
2277
|
+
refuse: (message, next, code = 1) => {
|
|
2278
|
+
show(renderNotice(product, { state: "fail", message, ...next ? { next } : {} }, render), "stderr");
|
|
2279
|
+
return code;
|
|
2280
|
+
}
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
1867
2283
|
async function run(rawOptions = {}) {
|
|
1868
2284
|
const print = rawOptions.print ?? defaultPrint;
|
|
1869
2285
|
const printErr = rawOptions.printErr ?? defaultPrintErr;
|
|
1870
|
-
const
|
|
1871
|
-
|
|
1872
|
-
|
|
2286
|
+
const parsed = parseArgv(rawOptions.argv ?? process.argv.slice(2));
|
|
2287
|
+
if (parsed.run) return runFile(parsed.run.file, parsed.run.args, printErr);
|
|
2288
|
+
const render = {
|
|
2289
|
+
...rawOptions.tty === void 0 ? {} : { tty: rawOptions.tty },
|
|
2290
|
+
...parsed.flags.noColor ? { color: false } : {}
|
|
2291
|
+
};
|
|
1873
2292
|
let config;
|
|
1874
2293
|
try {
|
|
1875
|
-
config = loadProductConfig({ configPath: rawOptions.configPath ??
|
|
2294
|
+
config = loadProductConfig({ configPath: rawOptions.configPath ?? parsed.flags.config });
|
|
1876
2295
|
} catch (error) {
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
const
|
|
1881
|
-
|
|
1882
|
-
if (
|
|
1883
|
-
|
|
2296
|
+
return screensFor(NO_PRODUCT, "", { ...render, color: false }, rawOptions).refuse(`The launcher cannot start: ${sentence(error.message)}`, void 0, 2);
|
|
2297
|
+
}
|
|
2298
|
+
const dir = resolveProductDir(config.product, rawOptions.dir ?? parsed.flags.dir);
|
|
2299
|
+
const ui = screensFor(faceProduct(config), config.binName, render, rawOptions);
|
|
2300
|
+
if ((parsed.command === "install" || parsed.command === "update") && !parsed.flags.version) recordSelf(dir);
|
|
2301
|
+
if (parsed.unknownFlag) return ui.refuse(`Unknown option: ${parsed.unknownFlag}.`, `${ui.bin} --help`, 2);
|
|
2302
|
+
if (parsed.flags.version) {
|
|
2303
|
+
print(renderVersion(ui.bin, readState(dir)?.version ?? "not installed", LAUNCHER_VERSION));
|
|
1884
2304
|
return 0;
|
|
1885
2305
|
}
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
2306
|
+
const command2 = parsed.command;
|
|
2307
|
+
if (parsed.flags.help || command2 === void 0) {
|
|
2308
|
+
ui.show(usage(config, dir, render));
|
|
2309
|
+
return parsed.flags.help ? 0 : 2;
|
|
1889
2310
|
}
|
|
1890
|
-
const
|
|
2311
|
+
const mutating = ["install", "login", "logout"].includes(command2) || command2 === "update" && !parsed.flags.dryRun || command2 === "autoupdate" && ["on", "off"].includes(parsed.args[0] ?? "");
|
|
1891
2312
|
let unlock;
|
|
1892
2313
|
try {
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
2314
|
+
if (mutating) {
|
|
2315
|
+
unlock = lockInstallation(dir);
|
|
2316
|
+
await recoverAcquisition(dir, async (command3, cwd, env) => {
|
|
2317
|
+
const result = (rawOptions.runEntry ?? defaultRunEntry)(command3, cwd, env);
|
|
2318
|
+
return result.ok && (result.code === void 0 || result.code === 0);
|
|
2319
|
+
}, payloadEnv(dir) ?? process.env);
|
|
2320
|
+
}
|
|
1898
2321
|
switch (command2) {
|
|
1899
2322
|
case "login":
|
|
1900
|
-
unlock();
|
|
1901
|
-
await doLogin(config, dir, rawOptions,
|
|
2323
|
+
unlock?.();
|
|
2324
|
+
await doLogin(config, dir, rawOptions, (line) => ui.notice("note", line));
|
|
2325
|
+
ui.notice("ok", `Signed in to ${identityFor(ui.product).name}.`, `${ui.bin} install`);
|
|
1902
2326
|
return 0;
|
|
1903
2327
|
case "logout":
|
|
1904
2328
|
wipeProductDir(dir);
|
|
1905
|
-
|
|
2329
|
+
ui.notice("ok", `Signed out of ${identityFor(ui.product).name} and removed the downloaded payload.`, `${ui.bin} login`);
|
|
1906
2330
|
return 0;
|
|
1907
2331
|
case "install":
|
|
1908
|
-
return await doInstall(config, dir, rawOptions, print);
|
|
2332
|
+
return await doInstall(config, dir, rawOptions, print, parsed.flags);
|
|
1909
2333
|
case "update":
|
|
1910
|
-
return await doUpdate(config, dir, rawOptions, print);
|
|
2334
|
+
return await doUpdate(config, dir, rawOptions, print, parsed.flags);
|
|
2335
|
+
case "status":
|
|
2336
|
+
return await doStatus(config, dir, rawOptions, ui, print, Boolean(parsed.flags.json));
|
|
1911
2337
|
case "doctor":
|
|
1912
|
-
return doDoctor(config, dir, rawOptions,
|
|
2338
|
+
return doDoctor(config, dir, rawOptions, ui);
|
|
1913
2339
|
case "autoupdate":
|
|
1914
|
-
return doAutoupdate(config, rawOptions,
|
|
2340
|
+
return doAutoupdate(config, dir, rawOptions, parsed.args, ui);
|
|
1915
2341
|
default:
|
|
1916
|
-
return doForward(config, dir, rawOptions, command2,
|
|
2342
|
+
return doForward(config, dir, rawOptions, command2, parsed.rest, ui, () => unlock?.());
|
|
1917
2343
|
}
|
|
1918
2344
|
} catch (error) {
|
|
1919
|
-
if (error instanceof NeedsLoginError) {
|
|
1920
|
-
|
|
1921
|
-
|
|
2345
|
+
if (error instanceof NeedsLoginError) return ui.refuse("You are signed out.", `${ui.bin} login`, 3);
|
|
2346
|
+
const message = error.message;
|
|
2347
|
+
if (/another installer is running|lock recovery is already pending/u.test(message)) {
|
|
2348
|
+
return ui.refuse("Another install or update is running.", `${ui.bin} status`);
|
|
1922
2349
|
}
|
|
1923
|
-
|
|
1924
|
-
return 1;
|
|
2350
|
+
return ui.refuse(sentence(message), `${ui.bin} ${command2}`);
|
|
1925
2351
|
} finally {
|
|
1926
2352
|
unlock?.();
|
|
1927
2353
|
}
|
|
1928
2354
|
}
|
|
1929
|
-
function
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
2355
|
+
function recordSelf(dir) {
|
|
2356
|
+
if (!(0, import_node_sea2.isSea)()) return;
|
|
2357
|
+
try {
|
|
2358
|
+
(0, import_node_fs12.mkdirSync)(dir, { recursive: true });
|
|
2359
|
+
(0, import_node_fs12.writeFileSync)((0, import_node_path7.join)(dir, LAUNCHER_RECORD_FILE), `${JSON.stringify({ path: process.execPath, version: LAUNCHER_VERSION })}
|
|
2360
|
+
`);
|
|
2361
|
+
} catch {
|
|
2362
|
+
}
|
|
1934
2363
|
}
|
|
1935
|
-
function
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
2364
|
+
function launcherTarget(config) {
|
|
2365
|
+
return { product: config.product, bin: config.binName, key: config.publicKey, host: config.host, ...config.dl ? { dl: config.dl } : {} };
|
|
2366
|
+
}
|
|
2367
|
+
function usage(config, dir, render) {
|
|
2368
|
+
const name = config.product;
|
|
2369
|
+
return renderUsage(faceProduct(config), {
|
|
2370
|
+
bin: config.binName,
|
|
2371
|
+
version: `${readState(dir)?.version ?? "not installed"} (launcher ${LAUNCHER_VERSION})`,
|
|
2372
|
+
summary: `Sign in, install and update ${name}.`,
|
|
2373
|
+
commands: [
|
|
2374
|
+
{ usage: "install", text: `Sign in and install ${name}.` },
|
|
2375
|
+
{ usage: "update [--dry-run]", text: `Update ${name} and this launcher.` },
|
|
2376
|
+
{ usage: "status", text: `Show versions, sign-in and ${AUTO_UPDATE}.` },
|
|
2377
|
+
{ usage: "doctor", text: `Check this machine, then run the ${name} doctor.` },
|
|
2378
|
+
{ usage: "login", text: "Sign in." },
|
|
2379
|
+
{ usage: "logout", text: "Sign out and remove the downloaded payload." },
|
|
2380
|
+
{ usage: "autoupdate on|off|status", text: `Turn hourly ${AUTO_UPDATE} on or off, or show it.` },
|
|
2381
|
+
{ usage: "<verb> ...", text: `Passed to ${name} when it declares it.` }
|
|
2382
|
+
],
|
|
2383
|
+
options: [
|
|
2384
|
+
{ usage: "--json", text: "Print status as JSON." },
|
|
2385
|
+
{ usage: "--no-color", text: "Print without colour." },
|
|
2386
|
+
{ usage: "--config <path>", text: "Use this product config." },
|
|
2387
|
+
{ usage: "--dir <path>", text: "Use this folder for sign-in and downloads." }
|
|
2388
|
+
]
|
|
2389
|
+
}, render);
|
|
1940
2390
|
}
|
|
1941
2391
|
async function runFile(file, args, printErr) {
|
|
1942
2392
|
if (!file) {
|
|
1943
2393
|
printErr("launcher --run needs a file to run.");
|
|
1944
2394
|
return 1;
|
|
1945
2395
|
}
|
|
1946
|
-
const abs = (0,
|
|
1947
|
-
if (!(0,
|
|
2396
|
+
const abs = (0, import_node_path7.resolve)(process.cwd(), file);
|
|
2397
|
+
if (!(0, import_node_fs12.existsSync)(abs)) {
|
|
1948
2398
|
printErr(`cannot run ${file}: no such file.`);
|
|
1949
2399
|
return 1;
|
|
1950
2400
|
}
|
|
@@ -1978,7 +2428,7 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
|
|
|
1978
2428
|
}
|
|
1979
2429
|
writeTokens(dir, {
|
|
1980
2430
|
accessToken: refreshed2.accessToken,
|
|
1981
|
-
refreshToken: stored.refreshToken,
|
|
2431
|
+
refreshToken: refreshed2.refreshToken ?? stored.refreshToken,
|
|
1982
2432
|
expiresAt: Date.now() + refreshed2.expiresIn * 1e3
|
|
1983
2433
|
});
|
|
1984
2434
|
return refreshed2.accessToken;
|
|
@@ -2000,15 +2450,14 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
|
|
|
2000
2450
|
});
|
|
2001
2451
|
return refreshed.accessToken;
|
|
2002
2452
|
}
|
|
2003
|
-
async function doLogin(config, dir, options,
|
|
2453
|
+
async function doLogin(config, dir, options, prompt, installer) {
|
|
2004
2454
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2005
2455
|
const open = options.open ?? openBrowser;
|
|
2006
2456
|
if (config.loginKind === "github") {
|
|
2007
2457
|
const tokens = await loginGithub(config, {
|
|
2008
2458
|
fetchImpl,
|
|
2009
2459
|
open,
|
|
2010
|
-
|
|
2011
|
-
...installer ? { onDeviceCode: (prompt) => installer.signIn(prompt) } : {}
|
|
2460
|
+
onDeviceCode: (grant) => installer ? installer.signIn(grant) : prompt(`To sign in, open ${grant.url} and enter the code ${grant.code}.`)
|
|
2012
2461
|
});
|
|
2013
2462
|
writeTokens(dir, {
|
|
2014
2463
|
accessToken: tokens.accessToken,
|
|
@@ -2016,7 +2465,12 @@ async function doLogin(config, dir, options, print, installer) {
|
|
|
2016
2465
|
expiresAt: Date.now() + tokens.expiresIn * 1e3
|
|
2017
2466
|
});
|
|
2018
2467
|
} else {
|
|
2019
|
-
const tokens = await loginGoogle({
|
|
2468
|
+
const tokens = await loginGoogle({
|
|
2469
|
+
host: config.host,
|
|
2470
|
+
open,
|
|
2471
|
+
fetchImpl,
|
|
2472
|
+
print: (line) => installer ? installer.relay(line) : prompt(line)
|
|
2473
|
+
});
|
|
2020
2474
|
writeTokens(dir, {
|
|
2021
2475
|
accessToken: tokens.accessToken,
|
|
2022
2476
|
refreshToken: tokens.refreshToken,
|
|
@@ -2024,20 +2478,26 @@ async function doLogin(config, dir, options, print, installer) {
|
|
|
2024
2478
|
clientId: tokens.clientId
|
|
2025
2479
|
});
|
|
2026
2480
|
}
|
|
2027
|
-
if (!installer) print(`signed in to ${config.product}.`);
|
|
2028
2481
|
}
|
|
2029
|
-
async function fetchAccessTokenOrLogin(config, dir, options,
|
|
2482
|
+
async function fetchAccessTokenOrLogin(config, dir, options, installer) {
|
|
2030
2483
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2031
2484
|
const fresh = await ensureFreshToken(config, dir, fetchImpl);
|
|
2032
2485
|
if (fresh) return fresh;
|
|
2033
|
-
|
|
2486
|
+
if (config.loginKind === "github") {
|
|
2487
|
+
const reused = await loginWithGhToken(config, fetchImpl, options.readGhToken ?? ((0, import_node_sea2.isSea)() ? readGhToken : () => null));
|
|
2488
|
+
if (reused) {
|
|
2489
|
+
writeTokens(dir, { accessToken: reused.accessToken, refreshToken: reused.refreshToken, expiresAt: Date.now() + reused.expiresIn * 1e3 });
|
|
2490
|
+
return reused.accessToken;
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
await doLogin(config, dir, options, (line) => installer.relay(line), installer);
|
|
2034
2494
|
const after = readTokens(dir);
|
|
2035
2495
|
if (!after) throw new Error("sign-in did not produce a token");
|
|
2036
2496
|
return after.accessToken;
|
|
2037
2497
|
}
|
|
2038
2498
|
function readPayloadArgv(dir, key) {
|
|
2039
2499
|
try {
|
|
2040
|
-
const parsed = JSON.parse((0,
|
|
2500
|
+
const parsed = JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path7.join)(payloadDir(dir), "payload.json"), "utf8"));
|
|
2041
2501
|
const entry2 = parsed[key];
|
|
2042
2502
|
if (typeof entry2 === "string") {
|
|
2043
2503
|
const parts = entry2.trim().split(/\s+/).filter(Boolean);
|
|
@@ -2059,7 +2519,7 @@ function readPayloadRun(dir) {
|
|
|
2059
2519
|
}
|
|
2060
2520
|
function readPayloadVerbs(dir) {
|
|
2061
2521
|
try {
|
|
2062
|
-
const parsed = JSON.parse((0,
|
|
2522
|
+
const parsed = JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path7.join)(payloadDir(dir), "payload.json"), "utf8"));
|
|
2063
2523
|
const verbs = parsed.verbs;
|
|
2064
2524
|
if (verbs === "*") return "*";
|
|
2065
2525
|
if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
|
|
@@ -2076,7 +2536,7 @@ function resolveEntry(entry2) {
|
|
|
2076
2536
|
function needsShell(command2) {
|
|
2077
2537
|
if (process.platform !== "win32") return false;
|
|
2078
2538
|
if (/\.(cmd|bat)$/i.test(command2)) return true;
|
|
2079
|
-
return !(0,
|
|
2539
|
+
return !(0, import_node_fs12.existsSync)(command2);
|
|
2080
2540
|
}
|
|
2081
2541
|
function quoteForShell(arg) {
|
|
2082
2542
|
return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
|
|
@@ -2108,7 +2568,7 @@ function defaultRunEntry(entry2, cwd, env) {
|
|
|
2108
2568
|
const [command2, ...args] = entry2;
|
|
2109
2569
|
const shell = needsShell(command2);
|
|
2110
2570
|
const commandLine = shell ? [command2, ...args].map(quoteForShell).join(" ") : command2;
|
|
2111
|
-
const spawnEntry = (progress2) => (0,
|
|
2571
|
+
const spawnEntry = (progress2) => (0, import_node_child_process6.spawnSync)(commandLine, shell ? [] : args, {
|
|
2112
2572
|
cwd,
|
|
2113
2573
|
stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
|
|
2114
2574
|
shell,
|
|
@@ -2142,8 +2602,16 @@ function faceProduct(config) {
|
|
|
2142
2602
|
function formatMb(bytes) {
|
|
2143
2603
|
return (bytes / 1048576).toFixed(1).replace(/\.0$/, "");
|
|
2144
2604
|
}
|
|
2145
|
-
async function installOrUpdate(config, dir, options, print, update) {
|
|
2605
|
+
async function installOrUpdate(config, dir, options, print, update, flags2) {
|
|
2606
|
+
let launcherFailed = false;
|
|
2607
|
+
const code = await installOrUpdateRun(config, dir, options, print, update, flags2, () => {
|
|
2608
|
+
launcherFailed = true;
|
|
2609
|
+
});
|
|
2610
|
+
return launcherFailed ? Math.max(code, 1) : code;
|
|
2611
|
+
}
|
|
2612
|
+
async function installOrUpdateRun(config, dir, options, print, update, flags2, markLauncherFailed) {
|
|
2146
2613
|
const product = faceProduct(config);
|
|
2614
|
+
const dryRun = update && Boolean(flags2.dryRun);
|
|
2147
2615
|
const installer = createInstallerRun({
|
|
2148
2616
|
product,
|
|
2149
2617
|
gate: config.host,
|
|
@@ -2153,17 +2621,55 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2153
2621
|
operation: update ? "update" : "install",
|
|
2154
2622
|
tty: options.tty,
|
|
2155
2623
|
animate: !options.print && Boolean(process.stderr.isTTY),
|
|
2624
|
+
dryRun,
|
|
2625
|
+
...flags2.noColor ? { color: false } : {},
|
|
2156
2626
|
...options.print ? { write: (text) => print(text.replace(/\n$/, "")) } : {}
|
|
2157
2627
|
});
|
|
2158
2628
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2159
2629
|
let version = readState(dir)?.version ?? "unknown";
|
|
2160
2630
|
installer.start();
|
|
2161
2631
|
try {
|
|
2632
|
+
const refresh = options.refresh ?? ((0, import_node_sea2.isSea)() ? refreshLauncher : void 0);
|
|
2633
|
+
const arm = refresh ? await refresh(launcherTarget(config), {
|
|
2634
|
+
lockHeld: true,
|
|
2635
|
+
path: process.execPath,
|
|
2636
|
+
dryRun,
|
|
2637
|
+
productDir: dir,
|
|
2638
|
+
fetch: fetchImpl,
|
|
2639
|
+
readVersion: async () => LAUNCHER_VERSION
|
|
2640
|
+
}) : null;
|
|
2641
|
+
if (arm) {
|
|
2642
|
+
installer.surface({
|
|
2643
|
+
id: LAUNCHER_SURFACE,
|
|
2644
|
+
from: arm.from,
|
|
2645
|
+
to: arm.to,
|
|
2646
|
+
state: armState(arm),
|
|
2647
|
+
...arm.durationMs === void 0 ? {} : { seconds: arm.durationMs / 1e3 },
|
|
2648
|
+
...arm.verdict !== "ok" && arm.detail ? { detail: arm.detail } : {}
|
|
2649
|
+
});
|
|
2650
|
+
if (arm.verdict === "fail") {
|
|
2651
|
+
const finish = installer.finish;
|
|
2652
|
+
installer.finish = (facts) => finish({
|
|
2653
|
+
...facts,
|
|
2654
|
+
operationFailed: true,
|
|
2655
|
+
detail: facts.detail ?? "The launcher could not update itself.",
|
|
2656
|
+
retry: facts.retry ?? `${config.binName} update`
|
|
2657
|
+
});
|
|
2658
|
+
markLauncherFailed();
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2162
2661
|
const current = readState(dir);
|
|
2163
|
-
const payloadPresent = (0,
|
|
2662
|
+
const payloadPresent = (0, import_node_fs12.existsSync)(payloadDir(dir));
|
|
2164
2663
|
let started = Date.now();
|
|
2165
2664
|
installer.phase("resolve", { state: "running" });
|
|
2166
2665
|
const pointer = await fetchReleaseVersion(config, fetchImpl);
|
|
2666
|
+
if (dryRun) {
|
|
2667
|
+
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
2668
|
+
const installed = payloadPresent ? current?.version : void 0;
|
|
2669
|
+
if (pointer) installer.surface({ id: config.product, from: installed, to: pointer, state: installed === pointer ? "current" : "updated" });
|
|
2670
|
+
installer.finish({ ...pointer ? { version: pointer } : {}, total: 1, updated: pointer && installed !== pointer ? 1 : 0, failed: 0, dryRun: true });
|
|
2671
|
+
return 0;
|
|
2672
|
+
}
|
|
2167
2673
|
if (pointer && current?.version === pointer && payloadPresent) {
|
|
2168
2674
|
version = pointer;
|
|
2169
2675
|
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
@@ -2180,14 +2686,14 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2180
2686
|
}
|
|
2181
2687
|
installer.phase("sign-in", { state: "running" });
|
|
2182
2688
|
started = Date.now();
|
|
2183
|
-
const accessToken = await fetchAccessTokenOrLogin(config, dir, options,
|
|
2689
|
+
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, installer);
|
|
2184
2690
|
installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
|
|
2185
2691
|
installer.phase("resolve", { state: "running" });
|
|
2186
2692
|
started = Date.now();
|
|
2187
2693
|
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
2188
2694
|
version = manifest.version;
|
|
2189
2695
|
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
2190
|
-
const unchanged = current?.version === version && (0,
|
|
2696
|
+
const unchanged = current?.version === version && (0, import_node_fs12.existsSync)(payloadDir(dir));
|
|
2191
2697
|
if (unchanged && current.acquired) {
|
|
2192
2698
|
installer.surface({ id: config.product, from: version, to: version, state: "current" });
|
|
2193
2699
|
return await finishLastMile(config, dir, options, installer, version, true, acquisitionRepairCommand(dir, version));
|
|
@@ -2197,14 +2703,14 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2197
2703
|
started = Date.now();
|
|
2198
2704
|
const cached = reusableCandidate(dir, manifest);
|
|
2199
2705
|
const candidate = cached ?? (0, import_node_crypto6.randomUUID)();
|
|
2200
|
-
const candidateDir = (0,
|
|
2706
|
+
const candidateDir = (0, import_node_path7.join)(dir, "candidates", candidate);
|
|
2201
2707
|
const onProgress = ({ done, total }) => installer.phase("download", {
|
|
2202
2708
|
state: "running",
|
|
2203
2709
|
measure: total ? `${Math.floor(done / total * 100)}% \xB7 ${formatMb(done)} of ${formatMb(total)} MB` : `${formatMb(done)} MB`,
|
|
2204
2710
|
...total ? { runningTitle: `Downloading ${config.product} ${version} (${formatMb(total)} MB)` } : {}
|
|
2205
2711
|
});
|
|
2206
2712
|
if (!cached) await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl, onProgress, cacheDir: dir });
|
|
2207
|
-
const acquisition = (0,
|
|
2713
|
+
const acquisition = (0, import_node_fs12.existsSync)((0, import_node_path7.join)(candidateDir, "payload", "payload.json")) ? readAcquisition((0, import_node_path7.join)(candidateDir, "payload")) : null;
|
|
2208
2714
|
if (acquisition) {
|
|
2209
2715
|
rememberCandidate(dir, candidate, manifest);
|
|
2210
2716
|
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
@@ -2227,10 +2733,10 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2227
2733
|
installer.finish(outcome ?? { version, total: 1, updated: 1, failed: 0, installed: true });
|
|
2228
2734
|
return 0;
|
|
2229
2735
|
}
|
|
2230
|
-
const target = (0,
|
|
2231
|
-
(0,
|
|
2232
|
-
(0,
|
|
2233
|
-
(0,
|
|
2736
|
+
const target = (0, import_node_path7.join)(dir, "payload");
|
|
2737
|
+
(0, import_node_fs12.mkdirSync)(dir, { recursive: true });
|
|
2738
|
+
(0, import_node_fs12.rmSync)(target, { recursive: true, force: true });
|
|
2739
|
+
(0, import_node_fs12.renameSync)((0, import_node_path7.join)(candidateDir, "payload"), target);
|
|
2234
2740
|
writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2235
2741
|
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
2236
2742
|
}
|
|
@@ -2256,11 +2762,11 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2256
2762
|
installer.stop();
|
|
2257
2763
|
}
|
|
2258
2764
|
}
|
|
2259
|
-
function doInstall(config, dir, options, print) {
|
|
2260
|
-
return installOrUpdate(config, dir, options, print, false);
|
|
2765
|
+
function doInstall(config, dir, options, print, flags2) {
|
|
2766
|
+
return installOrUpdate(config, dir, options, print, false, flags2);
|
|
2261
2767
|
}
|
|
2262
|
-
function doUpdate(config, dir, options, print) {
|
|
2263
|
-
return installOrUpdate(config, dir, options, print, true);
|
|
2768
|
+
function doUpdate(config, dir, options, print, flags2) {
|
|
2769
|
+
return installOrUpdate(config, dir, options, print, true, flags2);
|
|
2264
2770
|
}
|
|
2265
2771
|
function payloadEnv(dir) {
|
|
2266
2772
|
const stored = readTokens(dir);
|
|
@@ -2272,12 +2778,12 @@ function payloadFileAsCommand(entry2, payload) {
|
|
|
2272
2778
|
const first = entry2[0];
|
|
2273
2779
|
if (!first || first === "$self") return null;
|
|
2274
2780
|
if (first.includes("/") || first.includes("\\")) return null;
|
|
2275
|
-
const candidate = (0,
|
|
2276
|
-
if (!(0,
|
|
2781
|
+
const candidate = (0, import_node_path7.join)(payload, first);
|
|
2782
|
+
if (!(0, import_node_fs12.existsSync)(candidate)) return null;
|
|
2277
2783
|
if (NEVER_A_PROGRAM.test(first)) return first;
|
|
2278
2784
|
if (process.platform === "win32") return null;
|
|
2279
2785
|
try {
|
|
2280
|
-
return ((0,
|
|
2786
|
+
return ((0, import_node_fs12.statSync)(candidate).mode & 73) === 0 ? first : null;
|
|
2281
2787
|
} catch {
|
|
2282
2788
|
return null;
|
|
2283
2789
|
}
|
|
@@ -2292,7 +2798,7 @@ async function finishLastMile(config, dir, options, installer, version, unchange
|
|
|
2292
2798
|
updated: unchanged ? 0 : 1,
|
|
2293
2799
|
failed: 0,
|
|
2294
2800
|
installed: true,
|
|
2295
|
-
detail: `next step: run ${(0,
|
|
2801
|
+
detail: `next step: run ${(0, import_node_path7.join)(payload, config.binName)} to start ${config.product}.`
|
|
2296
2802
|
});
|
|
2297
2803
|
return 0;
|
|
2298
2804
|
}
|
|
@@ -2338,9 +2844,9 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2338
2844
|
const [command2, ...args] = entry2;
|
|
2339
2845
|
const shell = needsShell(command2);
|
|
2340
2846
|
const progress = !(process.platform === "win32" && shell);
|
|
2341
|
-
const outcomeDir = (0,
|
|
2342
|
-
const outcomeFile = (0,
|
|
2343
|
-
const progressFile = (0,
|
|
2847
|
+
const outcomeDir = (0, import_node_fs12.mkdtempSync)((0, import_node_path7.join)((0, import_node_os5.tmpdir)(), "mm-installer-outcome-"));
|
|
2848
|
+
const outcomeFile = (0, import_node_path7.join)(outcomeDir, "outcome.json");
|
|
2849
|
+
const progressFile = (0, import_node_path7.join)(outcomeDir, "progress.jsonl");
|
|
2344
2850
|
const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
|
|
2345
2851
|
delete childEnv.MM_FACE_TRANSCRIPT;
|
|
2346
2852
|
delete childEnv.MM_PROGRESS_FILE;
|
|
@@ -2353,7 +2859,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2353
2859
|
const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
|
|
2354
2860
|
try {
|
|
2355
2861
|
const result = await new Promise((resolve3) => {
|
|
2356
|
-
const child = (0,
|
|
2862
|
+
const child = (0, import_node_child_process6.spawn)(shell ? [command2, ...args].map(quoteForShell).join(" ") : command2, shell ? [] : args, {
|
|
2357
2863
|
cwd,
|
|
2358
2864
|
shell,
|
|
2359
2865
|
windowsHide: true,
|
|
@@ -2390,8 +2896,8 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2390
2896
|
const pollProgress = () => {
|
|
2391
2897
|
if (progressError) return;
|
|
2392
2898
|
try {
|
|
2393
|
-
if (!(0,
|
|
2394
|
-
const bytes = (0,
|
|
2899
|
+
if (!(0, import_node_fs12.existsSync)(progressFile)) return;
|
|
2900
|
+
const bytes = (0, import_node_fs12.readFileSync)(progressFile);
|
|
2395
2901
|
receive(progressDecoder.write(bytes.subarray(progressOffset)));
|
|
2396
2902
|
progressOffset = bytes.length;
|
|
2397
2903
|
} catch {
|
|
@@ -2424,109 +2930,146 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2424
2930
|
return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
|
|
2425
2931
|
}
|
|
2426
2932
|
} finally {
|
|
2427
|
-
if ((0,
|
|
2428
|
-
if ((0,
|
|
2429
|
-
(0,
|
|
2933
|
+
if ((0, import_node_fs12.existsSync)(progressFile)) (0, import_node_fs12.unlinkSync)(progressFile);
|
|
2934
|
+
if ((0, import_node_fs12.existsSync)(outcomeFile)) (0, import_node_fs12.unlinkSync)(outcomeFile);
|
|
2935
|
+
(0, import_node_fs12.rmdirSync)(outcomeDir);
|
|
2430
2936
|
}
|
|
2431
2937
|
}
|
|
2432
|
-
function
|
|
2433
|
-
if (
|
|
2434
|
-
|
|
2435
|
-
|
|
2938
|
+
function armState(arm) {
|
|
2939
|
+
if (arm.verdict === "fail") return "failed";
|
|
2940
|
+
if (arm.verdict === "defer") return "retry";
|
|
2941
|
+
if (arm.verdict === "skip") return "kept";
|
|
2942
|
+
return arm.from === arm.to ? "current" : "updated";
|
|
2943
|
+
}
|
|
2944
|
+
function doForward(config, dir, options, command2, rest, ui, unlock) {
|
|
2945
|
+
if ((0, import_node_fs12.existsSync)(command2)) {
|
|
2946
|
+
return ui.refuse(`${command2} is a file, not a command.`, `${ui.bin} --run ${command2}`, 2);
|
|
2436
2947
|
}
|
|
2437
2948
|
const acquired = readState(dir)?.acquired;
|
|
2438
2949
|
const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
|
|
2439
2950
|
const verbs = readPayloadVerbs(dir);
|
|
2440
|
-
const declared = verbs === "*" || Array.isArray(verbs) &&
|
|
2441
|
-
if (!target || !declared) {
|
|
2442
|
-
|
|
2443
|
-
printUsage(config, print);
|
|
2444
|
-
return 2;
|
|
2445
|
-
}
|
|
2446
|
-
const forwarded = [...resolveEntry(target), ...argv2];
|
|
2951
|
+
const declared = verbs === "*" || Array.isArray(verbs) && verbs.includes(command2);
|
|
2952
|
+
if (!target || !declared) return ui.refuse(`Unknown command: ${command2}.`, `${ui.bin} --help`, 2);
|
|
2953
|
+
const forwarded = [...resolveEntry(target), command2, ...rest];
|
|
2447
2954
|
const cwd = payloadDir(dir);
|
|
2448
2955
|
const inherited = payloadEnv(dir);
|
|
2449
2956
|
const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
|
|
2450
2957
|
unlock();
|
|
2451
2958
|
const result = (options.runEntry ?? defaultRunEntry)(forwarded, cwd, env);
|
|
2452
2959
|
if (!result.ok && result.code === void 0) {
|
|
2453
|
-
|
|
2454
|
-
return 1;
|
|
2960
|
+
return ui.refuse(`Could not start ${config.product}: ${sentence(result.error ?? `could not run ${forwarded[0]}`)}`, `${ui.bin} doctor`);
|
|
2455
2961
|
}
|
|
2456
2962
|
return result.code ?? 0;
|
|
2457
2963
|
}
|
|
2458
|
-
function doAutoupdate(config, options, args,
|
|
2964
|
+
function doAutoupdate(config, dir, options, args, ui) {
|
|
2459
2965
|
const mode = args[0] ?? "status";
|
|
2460
|
-
const scheduleOptions = options.autoupdate
|
|
2966
|
+
const scheduleOptions = { stateDir: dir, ...options.autoupdate };
|
|
2461
2967
|
const command2 = options.autoupdate?.command ?? [process.execPath, "update"];
|
|
2462
|
-
|
|
2968
|
+
const name = identityFor(ui.product).name;
|
|
2969
|
+
if (mode === "on" || mode === "off") {
|
|
2463
2970
|
try {
|
|
2464
|
-
enableSchedule(config, command2, scheduleOptions);
|
|
2971
|
+
if (mode === "on") enableSchedule(config, command2, scheduleOptions);
|
|
2972
|
+
else disableSchedule(config, scheduleOptions);
|
|
2465
2973
|
} catch (error) {
|
|
2466
|
-
|
|
2467
|
-
return 1;
|
|
2974
|
+
return ui.refuse(sentence(error.message), `${ui.bin} doctor`);
|
|
2468
2975
|
}
|
|
2469
|
-
|
|
2470
|
-
|
|
2976
|
+
if (mode === "on") ui.notice("ok", `Auto-update is on. ${name} checks for updates every hour.`, `${ui.bin} autoupdate status`);
|
|
2977
|
+
else ui.notice("ok", "Auto-update is off.", `${ui.bin} autoupdate on`);
|
|
2471
2978
|
return 0;
|
|
2472
2979
|
}
|
|
2473
|
-
if (mode
|
|
2474
|
-
try {
|
|
2475
|
-
disableSchedule(config, scheduleOptions);
|
|
2476
|
-
} catch (error) {
|
|
2477
|
-
print(error.message);
|
|
2478
|
-
return 1;
|
|
2479
|
-
}
|
|
2480
|
-
print("auto-update: off");
|
|
2481
|
-
return 0;
|
|
2482
|
-
}
|
|
2483
|
-
if (mode !== "status") {
|
|
2484
|
-
print(`unknown autoupdate mode: ${mode} \u2014 use on, off or status.`);
|
|
2485
|
-
return 1;
|
|
2486
|
-
}
|
|
2980
|
+
if (mode !== "status") return ui.refuse(`Unknown autoupdate mode: ${mode}.`, `${ui.bin} autoupdate status`, 2);
|
|
2487
2981
|
const state = querySchedule(config, scheduleOptions);
|
|
2488
|
-
if (!state.supported) {
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
}
|
|
2492
|
-
print(`auto-update: ${state.enabled ? `on (${state.cadence ?? "hourly"})` : "off"}`);
|
|
2493
|
-
if (state.lastRun) print(`last run: ${state.lastRun}`);
|
|
2982
|
+
if (!state.supported) return ui.refuse("Auto-update is not supported on this platform.", `${ui.bin} update`);
|
|
2983
|
+
if (state.enabled) ui.notice("ok", `Auto-update is on and runs every hour.${state.lastRun ? ` Last run: ${state.lastRun}.` : ""}`);
|
|
2984
|
+
else ui.notice("note", "Auto-update is off.", `${ui.bin} autoupdate on`);
|
|
2494
2985
|
return 0;
|
|
2495
2986
|
}
|
|
2496
|
-
function
|
|
2497
|
-
|
|
2987
|
+
function localFacts(config, dir, options) {
|
|
2988
|
+
const state = readState(dir);
|
|
2989
|
+
const tokens = readTokens(dir);
|
|
2990
|
+
const expired = Boolean(tokens && tokens.expiresAt <= Date.now());
|
|
2991
|
+
return {
|
|
2992
|
+
installed: state && (0, import_node_fs12.existsSync)(payloadDir(dir)) ? state.version : null,
|
|
2993
|
+
// An expired access token with a refresh token renews on the next use; it is still signed in.
|
|
2994
|
+
signedIn: Boolean(tokens && (!expired || tokens.refreshToken)),
|
|
2995
|
+
expired,
|
|
2996
|
+
schedule: querySchedule(config, { stateDir: dir, ...options.autoupdate })
|
|
2997
|
+
};
|
|
2998
|
+
}
|
|
2999
|
+
function signInRow(local) {
|
|
3000
|
+
if (local.signedIn) return { surface: "sign-in", state: local.expired ? "signed in \xB7 renews on next use" : "signed in", mark: "ok" };
|
|
3001
|
+
return { surface: "sign-in", state: local.expired ? "expired" : "signed out", mark: "fail" };
|
|
3002
|
+
}
|
|
3003
|
+
function autoUpdateRow({ supported, enabled, cadence, lastRun }) {
|
|
3004
|
+
if (!supported) return { surface: AUTO_UPDATE, state: "unsupported on this platform", mark: "note" };
|
|
3005
|
+
if (!enabled) return { surface: AUTO_UPDATE, state: "off", mark: "note" };
|
|
3006
|
+
return { surface: AUTO_UPDATE, state: `on \xB7 ${cadence ?? "hourly"}`, mark: "ok", ...lastRun ? { note: `Last run: ${lastRun}` } : {} };
|
|
3007
|
+
}
|
|
3008
|
+
function verdictFor(ui, local, updateAvailable, ready) {
|
|
3009
|
+
if (!local.installed) return { state: "fail", headline: "is not installed.", next: `${ui.bin} install` };
|
|
3010
|
+
if (!local.signedIn) return { state: "fail", headline: "is signed out.", next: `${ui.bin} login` };
|
|
3011
|
+
if (updateAvailable) return { state: "note", headline: "has an update available.", next: `${ui.bin} update` };
|
|
3012
|
+
return { state: "ok", headline: "is ready.", next: ready };
|
|
3013
|
+
}
|
|
3014
|
+
function within(fetchImpl, ms) {
|
|
3015
|
+
return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(ms) }));
|
|
3016
|
+
}
|
|
3017
|
+
var STATUS_PROBE_MS = 3e3;
|
|
3018
|
+
async function doStatus(config, dir, options, ui, print, json) {
|
|
3019
|
+
const fetchImpl = within(options.fetchImpl ?? fetch, STATUS_PROBE_MS);
|
|
3020
|
+
const local = localFacts(config, dir, options);
|
|
3021
|
+
const [release, record] = await Promise.all([
|
|
3022
|
+
fetchReleaseVersion(config, fetchImpl),
|
|
3023
|
+
readLauncherRecord(launcherTarget(config), { fetch: fetchImpl, timeoutMs: STATUS_PROBE_MS })
|
|
3024
|
+
]);
|
|
3025
|
+
const launcherNewer = record !== null && compareVersions(record.version, LAUNCHER_VERSION) > 0;
|
|
3026
|
+
const releaseOrder = local.installed && release ? compareVersions(release, local.installed) : null;
|
|
3027
|
+
const verdict = verdictFor(ui, local, launcherNewer || (releaseOrder ?? 0) > 0, `${ui.bin} doctor`);
|
|
3028
|
+
if (json) {
|
|
3029
|
+
print(JSON.stringify({
|
|
3030
|
+
product: config.product,
|
|
3031
|
+
launcher: { version: LAUNCHER_VERSION, available: launcherNewer ? record.version : null },
|
|
3032
|
+
installed: local.installed,
|
|
3033
|
+
release,
|
|
3034
|
+
signIn: local.signedIn ? local.expired ? "renews" : "signed-in" : local.expired ? "expired" : "signed-out",
|
|
3035
|
+
autoupdate: { supported: local.schedule.supported, enabled: local.schedule.enabled, lastRun: local.schedule.lastRun ?? null },
|
|
3036
|
+
verdict: verdict.state,
|
|
3037
|
+
next: verdict.next
|
|
3038
|
+
}, null, 2));
|
|
3039
|
+
return verdict.state === "fail" ? 1 : 0;
|
|
3040
|
+
}
|
|
3041
|
+
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" };
|
|
3042
|
+
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" };
|
|
3043
|
+
ui.show(renderReport(ui.product, {
|
|
3044
|
+
rows: [launcherRow, productRow, signInRow(local), autoUpdateRow(local.schedule)],
|
|
3045
|
+
verdict: { ...verdict, facts: [release ? `The release is ${release}.` : "The release version could not be read."] }
|
|
3046
|
+
}, ui.render));
|
|
3047
|
+
return verdict.state === "fail" ? 1 : 0;
|
|
3048
|
+
}
|
|
3049
|
+
function doDoctor(config, dir, options, ui) {
|
|
3050
|
+
const local = localFacts(config, dir, options);
|
|
3051
|
+
const verdict = verdictFor(ui, local, false, `${ui.bin} status`);
|
|
3052
|
+
ui.show(renderReport(ui.product, {
|
|
3053
|
+
rows: [
|
|
3054
|
+
{ surface: "launcher", state: LAUNCHER_VERSION, mark: "ok" },
|
|
3055
|
+
local.installed ? { surface: config.product, state: local.installed, mark: "ok" } : { surface: config.product, state: SURFACE_WORDS.missing, mark: "fail" },
|
|
3056
|
+
signInRow(local),
|
|
3057
|
+
autoUpdateRow(local.schedule)
|
|
3058
|
+
],
|
|
3059
|
+
// Paths belong in the receipt, never a row.
|
|
3060
|
+
verdict: { ...verdict, facts: [`Folder: ${dir}`, `Release host: ${config.host}`] }
|
|
3061
|
+
}, ui.render));
|
|
3062
|
+
const own = verdict.state === "fail" ? 1 : 0;
|
|
2498
3063
|
const acquired = readState(dir)?.acquired;
|
|
2499
3064
|
const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
|
|
2500
3065
|
const verbs = readPayloadVerbs(dir);
|
|
2501
3066
|
const chains = target !== null && (verbs === "*" || Array.isArray(verbs) && verbs.includes("doctor"));
|
|
2502
|
-
if (!chains) return
|
|
3067
|
+
if (!chains) return own;
|
|
2503
3068
|
const cwd = payloadDir(dir);
|
|
2504
3069
|
const inherited = payloadEnv(dir);
|
|
2505
3070
|
const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
|
|
2506
|
-
unlock();
|
|
2507
3071
|
const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], cwd, env);
|
|
2508
|
-
return result.code ?? (result.ok ? 0 : 1);
|
|
2509
|
-
}
|
|
2510
|
-
function doLauncherDoctor(config, dir, options, print) {
|
|
2511
|
-
const tokens = readTokens(dir);
|
|
2512
|
-
const state = readState(dir);
|
|
2513
|
-
const payloadPresent = (0, import_node_fs11.existsSync)(payloadDir(dir));
|
|
2514
|
-
print(`product: ${config.product}`);
|
|
2515
|
-
print(`host: ${config.host}`);
|
|
2516
|
-
print(`login: ${config.loginKind}`);
|
|
2517
|
-
print(`dir: ${dir}`);
|
|
2518
|
-
if (tokens) {
|
|
2519
|
-
const remaining = Math.max(0, Math.round((tokens.expiresAt - Date.now()) / 1e3));
|
|
2520
|
-
print(`token: present (expires in ${remaining}s)`);
|
|
2521
|
-
} else {
|
|
2522
|
-
print("token: none \u2014 run login first.");
|
|
2523
|
-
}
|
|
2524
|
-
print(`payload: ${state ? state.version : "none"}${payloadPresent ? "" : " (not downloaded)"}`);
|
|
2525
|
-
print(`paths: tokens ${(0, import_node_path6.join)(dir, "tokens.json")}, state ${(0, import_node_path6.join)(dir, "state.json")}, payload ${payloadDir(dir)}`);
|
|
2526
|
-
const schedule = querySchedule(config, options.autoupdate ?? {});
|
|
2527
|
-
print(
|
|
2528
|
-
`auto-update: ${!schedule.supported ? "unsupported on this platform" : schedule.enabled ? `on (${schedule.cadence ?? "hourly"})` : "off"}`
|
|
2529
|
-
);
|
|
3072
|
+
return (result.code ?? (result.ok ? 0 : 1)) || own;
|
|
2530
3073
|
}
|
|
2531
3074
|
var invokedAsMain = typeof process.argv[1] === "string" && (() => {
|
|
2532
3075
|
try {
|
|
@@ -2545,8 +3088,7 @@ if (invokedAsMain) {
|
|
|
2545
3088
|
process.exitCode = code;
|
|
2546
3089
|
},
|
|
2547
3090
|
(error) => {
|
|
2548
|
-
|
|
2549
|
-
`);
|
|
3091
|
+
printScreen(renderNotice(NO_PRODUCT, { state: "fail", message: sentence(error.message) }, { color: false }), "stderr");
|
|
2550
3092
|
process.exitCode = 1;
|
|
2551
3093
|
}
|
|
2552
3094
|
);
|
|
@@ -2557,6 +3099,7 @@ if (invokedAsMain) {
|
|
|
2557
3099
|
defaultRunEntry,
|
|
2558
3100
|
ensureFreshToken,
|
|
2559
3101
|
formatMb,
|
|
3102
|
+
parseArgv,
|
|
2560
3103
|
readPayloadEntry,
|
|
2561
3104
|
readPayloadRun,
|
|
2562
3105
|
readPayloadVerbs,
|