@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.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();
|
|
@@ -1855,7 +2166,7 @@ function page(title, body) {
|
|
|
1855
2166
|
}
|
|
1856
2167
|
|
|
1857
2168
|
// src/index.ts
|
|
1858
|
-
var LAUNCHER_VERSION = true ? "1.0
|
|
2169
|
+
var LAUNCHER_VERSION = true ? "1.1.0" : readVersionFromPackage();
|
|
1859
2170
|
function defaultPrint(message) {
|
|
1860
2171
|
process.stdout.write(`${message}
|
|
1861
2172
|
`);
|
|
@@ -1864,87 +2175,200 @@ function defaultPrintErr(message) {
|
|
|
1864
2175
|
process.stderr.write(`${message}
|
|
1865
2176
|
`);
|
|
1866
2177
|
}
|
|
2178
|
+
var LAUNCHER_VERBS = /* @__PURE__ */ new Set(["install", "update", "status", "doctor", "login", "logout", "autoupdate"]);
|
|
2179
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set(["--config", "--dir"]);
|
|
2180
|
+
var SWITCHES = {
|
|
2181
|
+
"--json": "json",
|
|
2182
|
+
"--no-color": "noColor",
|
|
2183
|
+
"--dry-run": "dryRun",
|
|
2184
|
+
"--help": "help",
|
|
2185
|
+
"-h": "help",
|
|
2186
|
+
"--version": "version",
|
|
2187
|
+
"-v": "version"
|
|
2188
|
+
};
|
|
2189
|
+
function parseArgv(argv2) {
|
|
2190
|
+
const parsed = { flags: {}, args: [], rest: [] };
|
|
2191
|
+
const flag = (i2) => {
|
|
2192
|
+
const arg = argv2[i2];
|
|
2193
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
2194
|
+
const value = argv2[i2 + 1];
|
|
2195
|
+
if (value === void 0 || value.startsWith("-")) {
|
|
2196
|
+
parsed.unknownFlag ??= arg;
|
|
2197
|
+
return i2 + 1;
|
|
2198
|
+
}
|
|
2199
|
+
parsed.flags[arg === "--config" ? "config" : "dir"] = value;
|
|
2200
|
+
return i2 + 2;
|
|
2201
|
+
}
|
|
2202
|
+
const name = SWITCHES[arg];
|
|
2203
|
+
if (!name) return -1;
|
|
2204
|
+
parsed.flags[name] = true;
|
|
2205
|
+
return i2 + 1;
|
|
2206
|
+
};
|
|
2207
|
+
const skip = (i2) => {
|
|
2208
|
+
const next = flag(i2);
|
|
2209
|
+
if (next >= 0) return next;
|
|
2210
|
+
parsed.unknownFlag ??= argv2[i2];
|
|
2211
|
+
return i2 + 1;
|
|
2212
|
+
};
|
|
2213
|
+
let i = 0;
|
|
2214
|
+
while (i < argv2.length && argv2[i].startsWith("-")) {
|
|
2215
|
+
if (argv2[i] === "--run") {
|
|
2216
|
+
parsed.run = { file: argv2[i + 1], args: argv2.slice(i + 2) };
|
|
2217
|
+
return parsed;
|
|
2218
|
+
}
|
|
2219
|
+
i = skip(i);
|
|
2220
|
+
}
|
|
2221
|
+
parsed.command = argv2[i];
|
|
2222
|
+
if (parsed.command === void 0) return parsed;
|
|
2223
|
+
if (!LAUNCHER_VERBS.has(parsed.command)) {
|
|
2224
|
+
parsed.rest = argv2.slice(i + 1);
|
|
2225
|
+
return parsed;
|
|
2226
|
+
}
|
|
2227
|
+
for (i += 1; i < argv2.length; ) {
|
|
2228
|
+
if (argv2[i].startsWith("-")) i = skip(i);
|
|
2229
|
+
else parsed.args.push(argv2[i++]);
|
|
2230
|
+
}
|
|
2231
|
+
return parsed;
|
|
2232
|
+
}
|
|
2233
|
+
var NO_PRODUCT = "mmi-hub";
|
|
2234
|
+
function sentence(message) {
|
|
2235
|
+
const text = message.trim().replace(/\s+—\s+/gu, ". ");
|
|
2236
|
+
if (!text) return "Something went wrong.";
|
|
2237
|
+
return `${text[0].toUpperCase()}${text.slice(1)}${/[.!?]$/u.test(text) ? "" : "."}`;
|
|
2238
|
+
}
|
|
2239
|
+
function screensFor(product, bin, render, options) {
|
|
2240
|
+
const show = (lines, stream = "stdout") => {
|
|
2241
|
+
const sink = stream === "stderr" ? options.printErr : options.print;
|
|
2242
|
+
if (sink) for (const line of lines) sink(line);
|
|
2243
|
+
else printScreen(lines, stream);
|
|
2244
|
+
};
|
|
2245
|
+
return {
|
|
2246
|
+
product,
|
|
2247
|
+
bin,
|
|
2248
|
+
render,
|
|
2249
|
+
show,
|
|
2250
|
+
notice: (state, message, next) => show(renderNotice(product, { state, message, ...next ? { next } : {} }, render)),
|
|
2251
|
+
refuse: (message, next, code = 1) => {
|
|
2252
|
+
show(renderNotice(product, { state: "fail", message, ...next ? { next } : {} }, render), "stderr");
|
|
2253
|
+
return code;
|
|
2254
|
+
}
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
1867
2257
|
async function run(rawOptions = {}) {
|
|
1868
2258
|
const print = rawOptions.print ?? defaultPrint;
|
|
1869
2259
|
const printErr = rawOptions.printErr ?? defaultPrintErr;
|
|
1870
|
-
const
|
|
1871
|
-
|
|
1872
|
-
|
|
2260
|
+
const parsed = parseArgv(rawOptions.argv ?? process.argv.slice(2));
|
|
2261
|
+
if (parsed.run) return runFile(parsed.run.file, parsed.run.args, printErr);
|
|
2262
|
+
const render = {
|
|
2263
|
+
...rawOptions.tty === void 0 ? {} : { tty: rawOptions.tty },
|
|
2264
|
+
...parsed.flags.noColor ? { color: false } : {}
|
|
2265
|
+
};
|
|
1873
2266
|
let config;
|
|
1874
2267
|
try {
|
|
1875
|
-
config = loadProductConfig({ configPath: rawOptions.configPath ??
|
|
2268
|
+
config = loadProductConfig({ configPath: rawOptions.configPath ?? parsed.flags.config });
|
|
1876
2269
|
} catch (error) {
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
const
|
|
1881
|
-
|
|
1882
|
-
if (
|
|
1883
|
-
|
|
2270
|
+
return screensFor(NO_PRODUCT, "", { ...render, color: false }, rawOptions).refuse(`The launcher cannot start: ${sentence(error.message)}`, void 0, 2);
|
|
2271
|
+
}
|
|
2272
|
+
const dir = resolveProductDir(config.product, rawOptions.dir ?? parsed.flags.dir);
|
|
2273
|
+
const ui = screensFor(faceProduct(config), config.binName, render, rawOptions);
|
|
2274
|
+
if ((parsed.command === "install" || parsed.command === "update") && !parsed.flags.version) recordSelf(dir);
|
|
2275
|
+
if (parsed.unknownFlag) return ui.refuse(`Unknown option: ${parsed.unknownFlag}.`, `${ui.bin} --help`, 2);
|
|
2276
|
+
if (parsed.flags.version) {
|
|
2277
|
+
print(renderVersion(ui.bin, readState(dir)?.version ?? "not installed", LAUNCHER_VERSION));
|
|
1884
2278
|
return 0;
|
|
1885
2279
|
}
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
2280
|
+
const command2 = parsed.command;
|
|
2281
|
+
if (parsed.flags.help || command2 === void 0) {
|
|
2282
|
+
ui.show(usage(config, dir, render));
|
|
2283
|
+
return parsed.flags.help ? 0 : 2;
|
|
1889
2284
|
}
|
|
1890
|
-
const
|
|
2285
|
+
const mutating = ["install", "login", "logout"].includes(command2) || command2 === "update" && !parsed.flags.dryRun || command2 === "autoupdate" && ["on", "off"].includes(parsed.args[0] ?? "");
|
|
1891
2286
|
let unlock;
|
|
1892
2287
|
try {
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
2288
|
+
if (mutating) {
|
|
2289
|
+
unlock = lockInstallation(dir);
|
|
2290
|
+
await recoverAcquisition(dir, async (command3, cwd, env) => {
|
|
2291
|
+
const result = (rawOptions.runEntry ?? defaultRunEntry)(command3, cwd, env);
|
|
2292
|
+
return result.ok && (result.code === void 0 || result.code === 0);
|
|
2293
|
+
}, payloadEnv(dir) ?? process.env);
|
|
2294
|
+
}
|
|
1898
2295
|
switch (command2) {
|
|
1899
2296
|
case "login":
|
|
1900
|
-
unlock();
|
|
1901
|
-
await doLogin(config, dir, rawOptions,
|
|
2297
|
+
unlock?.();
|
|
2298
|
+
await doLogin(config, dir, rawOptions, (line) => ui.notice("note", line));
|
|
2299
|
+
ui.notice("ok", `Signed in to ${identityFor(ui.product).name}.`, `${ui.bin} install`);
|
|
1902
2300
|
return 0;
|
|
1903
2301
|
case "logout":
|
|
1904
2302
|
wipeProductDir(dir);
|
|
1905
|
-
|
|
2303
|
+
ui.notice("ok", `Signed out of ${identityFor(ui.product).name} and removed the downloaded payload.`, `${ui.bin} login`);
|
|
1906
2304
|
return 0;
|
|
1907
2305
|
case "install":
|
|
1908
|
-
return await doInstall(config, dir, rawOptions, print);
|
|
2306
|
+
return await doInstall(config, dir, rawOptions, print, parsed.flags);
|
|
1909
2307
|
case "update":
|
|
1910
|
-
return await doUpdate(config, dir, rawOptions, print);
|
|
2308
|
+
return await doUpdate(config, dir, rawOptions, print, parsed.flags);
|
|
2309
|
+
case "status":
|
|
2310
|
+
return await doStatus(config, dir, rawOptions, ui, print, Boolean(parsed.flags.json));
|
|
1911
2311
|
case "doctor":
|
|
1912
|
-
return doDoctor(config, dir, rawOptions,
|
|
2312
|
+
return doDoctor(config, dir, rawOptions, ui);
|
|
1913
2313
|
case "autoupdate":
|
|
1914
|
-
return doAutoupdate(config, rawOptions,
|
|
2314
|
+
return doAutoupdate(config, dir, rawOptions, parsed.args, ui);
|
|
1915
2315
|
default:
|
|
1916
|
-
return doForward(config, dir, rawOptions, command2,
|
|
2316
|
+
return doForward(config, dir, rawOptions, command2, parsed.rest, ui, () => unlock?.());
|
|
1917
2317
|
}
|
|
1918
2318
|
} catch (error) {
|
|
1919
|
-
if (error instanceof NeedsLoginError) {
|
|
1920
|
-
|
|
1921
|
-
|
|
2319
|
+
if (error instanceof NeedsLoginError) return ui.refuse("You are signed out.", `${ui.bin} login`, 3);
|
|
2320
|
+
const message = error.message;
|
|
2321
|
+
if (/another installer is running|lock recovery is already pending/u.test(message)) {
|
|
2322
|
+
return ui.refuse("Another install or update is running.", `${ui.bin} status`);
|
|
1922
2323
|
}
|
|
1923
|
-
|
|
1924
|
-
return 1;
|
|
2324
|
+
return ui.refuse(sentence(message), `${ui.bin} ${command2}`);
|
|
1925
2325
|
} finally {
|
|
1926
2326
|
unlock?.();
|
|
1927
2327
|
}
|
|
1928
2328
|
}
|
|
1929
|
-
function
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
2329
|
+
function recordSelf(dir) {
|
|
2330
|
+
if (!(0, import_node_sea2.isSea)()) return;
|
|
2331
|
+
try {
|
|
2332
|
+
(0, import_node_fs12.mkdirSync)(dir, { recursive: true });
|
|
2333
|
+
(0, import_node_fs12.writeFileSync)((0, import_node_path7.join)(dir, LAUNCHER_RECORD_FILE), `${JSON.stringify({ path: process.execPath, version: LAUNCHER_VERSION })}
|
|
2334
|
+
`);
|
|
2335
|
+
} catch {
|
|
2336
|
+
}
|
|
1934
2337
|
}
|
|
1935
|
-
function
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
2338
|
+
function launcherTarget(config) {
|
|
2339
|
+
return { product: config.product, bin: config.binName, key: config.publicKey, host: config.host, ...config.dl ? { dl: config.dl } : {} };
|
|
2340
|
+
}
|
|
2341
|
+
function usage(config, dir, render) {
|
|
2342
|
+
const name = config.product;
|
|
2343
|
+
return renderUsage(faceProduct(config), {
|
|
2344
|
+
bin: config.binName,
|
|
2345
|
+
version: `${readState(dir)?.version ?? "not installed"} (launcher ${LAUNCHER_VERSION})`,
|
|
2346
|
+
summary: `Sign in, install and update ${name}.`,
|
|
2347
|
+
commands: [
|
|
2348
|
+
{ usage: "install", text: `Sign in and install ${name}.` },
|
|
2349
|
+
{ usage: "update [--dry-run]", text: `Update ${name} and this launcher.` },
|
|
2350
|
+
{ usage: "status", text: `Show versions, sign-in and ${AUTO_UPDATE}.` },
|
|
2351
|
+
{ usage: "doctor", text: `Check this machine, then run the ${name} doctor.` },
|
|
2352
|
+
{ usage: "login", text: "Sign in." },
|
|
2353
|
+
{ usage: "logout", text: "Sign out and remove the downloaded payload." },
|
|
2354
|
+
{ usage: "autoupdate on|off|status", text: `Turn hourly ${AUTO_UPDATE} on or off, or show it.` },
|
|
2355
|
+
{ usage: "<verb> ...", text: `Passed to ${name} when it declares it.` }
|
|
2356
|
+
],
|
|
2357
|
+
options: [
|
|
2358
|
+
{ usage: "--json", text: "Print status as JSON." },
|
|
2359
|
+
{ usage: "--no-color", text: "Print without colour." },
|
|
2360
|
+
{ usage: "--config <path>", text: "Use this product config." },
|
|
2361
|
+
{ usage: "--dir <path>", text: "Use this folder for sign-in and downloads." }
|
|
2362
|
+
]
|
|
2363
|
+
}, render);
|
|
1940
2364
|
}
|
|
1941
2365
|
async function runFile(file, args, printErr) {
|
|
1942
2366
|
if (!file) {
|
|
1943
2367
|
printErr("launcher --run needs a file to run.");
|
|
1944
2368
|
return 1;
|
|
1945
2369
|
}
|
|
1946
|
-
const abs = (0,
|
|
1947
|
-
if (!(0,
|
|
2370
|
+
const abs = (0, import_node_path7.resolve)(process.cwd(), file);
|
|
2371
|
+
if (!(0, import_node_fs12.existsSync)(abs)) {
|
|
1948
2372
|
printErr(`cannot run ${file}: no such file.`);
|
|
1949
2373
|
return 1;
|
|
1950
2374
|
}
|
|
@@ -2000,15 +2424,14 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
|
|
|
2000
2424
|
});
|
|
2001
2425
|
return refreshed.accessToken;
|
|
2002
2426
|
}
|
|
2003
|
-
async function doLogin(config, dir, options,
|
|
2427
|
+
async function doLogin(config, dir, options, prompt, installer) {
|
|
2004
2428
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2005
2429
|
const open = options.open ?? openBrowser;
|
|
2006
2430
|
if (config.loginKind === "github") {
|
|
2007
2431
|
const tokens = await loginGithub(config, {
|
|
2008
2432
|
fetchImpl,
|
|
2009
2433
|
open,
|
|
2010
|
-
|
|
2011
|
-
...installer ? { onDeviceCode: (prompt) => installer.signIn(prompt) } : {}
|
|
2434
|
+
onDeviceCode: (grant) => installer ? installer.signIn(grant) : prompt(`To sign in, open ${grant.url} and enter the code ${grant.code}.`)
|
|
2012
2435
|
});
|
|
2013
2436
|
writeTokens(dir, {
|
|
2014
2437
|
accessToken: tokens.accessToken,
|
|
@@ -2016,7 +2439,12 @@ async function doLogin(config, dir, options, print, installer) {
|
|
|
2016
2439
|
expiresAt: Date.now() + tokens.expiresIn * 1e3
|
|
2017
2440
|
});
|
|
2018
2441
|
} else {
|
|
2019
|
-
const tokens = await loginGoogle({
|
|
2442
|
+
const tokens = await loginGoogle({
|
|
2443
|
+
host: config.host,
|
|
2444
|
+
open,
|
|
2445
|
+
fetchImpl,
|
|
2446
|
+
print: (line) => installer ? installer.relay(line) : prompt(line)
|
|
2447
|
+
});
|
|
2020
2448
|
writeTokens(dir, {
|
|
2021
2449
|
accessToken: tokens.accessToken,
|
|
2022
2450
|
refreshToken: tokens.refreshToken,
|
|
@@ -2024,20 +2452,19 @@ async function doLogin(config, dir, options, print, installer) {
|
|
|
2024
2452
|
clientId: tokens.clientId
|
|
2025
2453
|
});
|
|
2026
2454
|
}
|
|
2027
|
-
if (!installer) print(`signed in to ${config.product}.`);
|
|
2028
2455
|
}
|
|
2029
|
-
async function fetchAccessTokenOrLogin(config, dir, options,
|
|
2456
|
+
async function fetchAccessTokenOrLogin(config, dir, options, installer) {
|
|
2030
2457
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2031
2458
|
const fresh = await ensureFreshToken(config, dir, fetchImpl);
|
|
2032
2459
|
if (fresh) return fresh;
|
|
2033
|
-
await doLogin(config, dir, options,
|
|
2460
|
+
await doLogin(config, dir, options, (line) => installer.relay(line), installer);
|
|
2034
2461
|
const after = readTokens(dir);
|
|
2035
2462
|
if (!after) throw new Error("sign-in did not produce a token");
|
|
2036
2463
|
return after.accessToken;
|
|
2037
2464
|
}
|
|
2038
2465
|
function readPayloadArgv(dir, key) {
|
|
2039
2466
|
try {
|
|
2040
|
-
const parsed = JSON.parse((0,
|
|
2467
|
+
const parsed = JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path7.join)(payloadDir(dir), "payload.json"), "utf8"));
|
|
2041
2468
|
const entry2 = parsed[key];
|
|
2042
2469
|
if (typeof entry2 === "string") {
|
|
2043
2470
|
const parts = entry2.trim().split(/\s+/).filter(Boolean);
|
|
@@ -2059,7 +2486,7 @@ function readPayloadRun(dir) {
|
|
|
2059
2486
|
}
|
|
2060
2487
|
function readPayloadVerbs(dir) {
|
|
2061
2488
|
try {
|
|
2062
|
-
const parsed = JSON.parse((0,
|
|
2489
|
+
const parsed = JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path7.join)(payloadDir(dir), "payload.json"), "utf8"));
|
|
2063
2490
|
const verbs = parsed.verbs;
|
|
2064
2491
|
if (verbs === "*") return "*";
|
|
2065
2492
|
if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
|
|
@@ -2076,7 +2503,7 @@ function resolveEntry(entry2) {
|
|
|
2076
2503
|
function needsShell(command2) {
|
|
2077
2504
|
if (process.platform !== "win32") return false;
|
|
2078
2505
|
if (/\.(cmd|bat)$/i.test(command2)) return true;
|
|
2079
|
-
return !(0,
|
|
2506
|
+
return !(0, import_node_fs12.existsSync)(command2);
|
|
2080
2507
|
}
|
|
2081
2508
|
function quoteForShell(arg) {
|
|
2082
2509
|
return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
|
|
@@ -2108,7 +2535,7 @@ function defaultRunEntry(entry2, cwd, env) {
|
|
|
2108
2535
|
const [command2, ...args] = entry2;
|
|
2109
2536
|
const shell = needsShell(command2);
|
|
2110
2537
|
const commandLine = shell ? [command2, ...args].map(quoteForShell).join(" ") : command2;
|
|
2111
|
-
const spawnEntry = (progress2) => (0,
|
|
2538
|
+
const spawnEntry = (progress2) => (0, import_node_child_process6.spawnSync)(commandLine, shell ? [] : args, {
|
|
2112
2539
|
cwd,
|
|
2113
2540
|
stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
|
|
2114
2541
|
shell,
|
|
@@ -2142,8 +2569,16 @@ function faceProduct(config) {
|
|
|
2142
2569
|
function formatMb(bytes) {
|
|
2143
2570
|
return (bytes / 1048576).toFixed(1).replace(/\.0$/, "");
|
|
2144
2571
|
}
|
|
2145
|
-
async function installOrUpdate(config, dir, options, print, update) {
|
|
2572
|
+
async function installOrUpdate(config, dir, options, print, update, flags2) {
|
|
2573
|
+
let launcherFailed = false;
|
|
2574
|
+
const code = await installOrUpdateRun(config, dir, options, print, update, flags2, () => {
|
|
2575
|
+
launcherFailed = true;
|
|
2576
|
+
});
|
|
2577
|
+
return launcherFailed ? Math.max(code, 1) : code;
|
|
2578
|
+
}
|
|
2579
|
+
async function installOrUpdateRun(config, dir, options, print, update, flags2, markLauncherFailed) {
|
|
2146
2580
|
const product = faceProduct(config);
|
|
2581
|
+
const dryRun = update && Boolean(flags2.dryRun);
|
|
2147
2582
|
const installer = createInstallerRun({
|
|
2148
2583
|
product,
|
|
2149
2584
|
gate: config.host,
|
|
@@ -2153,17 +2588,55 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2153
2588
|
operation: update ? "update" : "install",
|
|
2154
2589
|
tty: options.tty,
|
|
2155
2590
|
animate: !options.print && Boolean(process.stderr.isTTY),
|
|
2591
|
+
dryRun,
|
|
2592
|
+
...flags2.noColor ? { color: false } : {},
|
|
2156
2593
|
...options.print ? { write: (text) => print(text.replace(/\n$/, "")) } : {}
|
|
2157
2594
|
});
|
|
2158
2595
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2159
2596
|
let version = readState(dir)?.version ?? "unknown";
|
|
2160
2597
|
installer.start();
|
|
2161
2598
|
try {
|
|
2599
|
+
const refresh = options.refresh ?? ((0, import_node_sea2.isSea)() ? refreshLauncher : void 0);
|
|
2600
|
+
const arm = refresh ? await refresh(launcherTarget(config), {
|
|
2601
|
+
lockHeld: true,
|
|
2602
|
+
path: process.execPath,
|
|
2603
|
+
dryRun,
|
|
2604
|
+
productDir: dir,
|
|
2605
|
+
fetch: fetchImpl,
|
|
2606
|
+
readVersion: async () => LAUNCHER_VERSION
|
|
2607
|
+
}) : null;
|
|
2608
|
+
if (arm) {
|
|
2609
|
+
installer.surface({
|
|
2610
|
+
id: LAUNCHER_SURFACE,
|
|
2611
|
+
from: arm.from,
|
|
2612
|
+
to: arm.to,
|
|
2613
|
+
state: armState(arm),
|
|
2614
|
+
...arm.durationMs === void 0 ? {} : { seconds: arm.durationMs / 1e3 },
|
|
2615
|
+
...arm.verdict !== "ok" && arm.detail ? { detail: arm.detail } : {}
|
|
2616
|
+
});
|
|
2617
|
+
if (arm.verdict === "fail") {
|
|
2618
|
+
const finish = installer.finish;
|
|
2619
|
+
installer.finish = (facts) => finish({
|
|
2620
|
+
...facts,
|
|
2621
|
+
operationFailed: true,
|
|
2622
|
+
detail: facts.detail ?? "The launcher could not update itself.",
|
|
2623
|
+
retry: facts.retry ?? `${config.binName} update`
|
|
2624
|
+
});
|
|
2625
|
+
markLauncherFailed();
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2162
2628
|
const current = readState(dir);
|
|
2163
|
-
const payloadPresent = (0,
|
|
2629
|
+
const payloadPresent = (0, import_node_fs12.existsSync)(payloadDir(dir));
|
|
2164
2630
|
let started = Date.now();
|
|
2165
2631
|
installer.phase("resolve", { state: "running" });
|
|
2166
2632
|
const pointer = await fetchReleaseVersion(config, fetchImpl);
|
|
2633
|
+
if (dryRun) {
|
|
2634
|
+
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
2635
|
+
const installed = payloadPresent ? current?.version : void 0;
|
|
2636
|
+
if (pointer) installer.surface({ id: config.product, from: installed, to: pointer, state: installed === pointer ? "current" : "updated" });
|
|
2637
|
+
installer.finish({ ...pointer ? { version: pointer } : {}, total: 1, updated: pointer && installed !== pointer ? 1 : 0, failed: 0, dryRun: true });
|
|
2638
|
+
return 0;
|
|
2639
|
+
}
|
|
2167
2640
|
if (pointer && current?.version === pointer && payloadPresent) {
|
|
2168
2641
|
version = pointer;
|
|
2169
2642
|
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
@@ -2180,14 +2653,14 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2180
2653
|
}
|
|
2181
2654
|
installer.phase("sign-in", { state: "running" });
|
|
2182
2655
|
started = Date.now();
|
|
2183
|
-
const accessToken = await fetchAccessTokenOrLogin(config, dir, options,
|
|
2656
|
+
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, installer);
|
|
2184
2657
|
installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
|
|
2185
2658
|
installer.phase("resolve", { state: "running" });
|
|
2186
2659
|
started = Date.now();
|
|
2187
2660
|
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
2188
2661
|
version = manifest.version;
|
|
2189
2662
|
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
2190
|
-
const unchanged = current?.version === version && (0,
|
|
2663
|
+
const unchanged = current?.version === version && (0, import_node_fs12.existsSync)(payloadDir(dir));
|
|
2191
2664
|
if (unchanged && current.acquired) {
|
|
2192
2665
|
installer.surface({ id: config.product, from: version, to: version, state: "current" });
|
|
2193
2666
|
return await finishLastMile(config, dir, options, installer, version, true, acquisitionRepairCommand(dir, version));
|
|
@@ -2197,14 +2670,14 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2197
2670
|
started = Date.now();
|
|
2198
2671
|
const cached = reusableCandidate(dir, manifest);
|
|
2199
2672
|
const candidate = cached ?? (0, import_node_crypto6.randomUUID)();
|
|
2200
|
-
const candidateDir = (0,
|
|
2673
|
+
const candidateDir = (0, import_node_path7.join)(dir, "candidates", candidate);
|
|
2201
2674
|
const onProgress = ({ done, total }) => installer.phase("download", {
|
|
2202
2675
|
state: "running",
|
|
2203
2676
|
measure: total ? `${Math.floor(done / total * 100)}% \xB7 ${formatMb(done)} of ${formatMb(total)} MB` : `${formatMb(done)} MB`,
|
|
2204
2677
|
...total ? { runningTitle: `Downloading ${config.product} ${version} (${formatMb(total)} MB)` } : {}
|
|
2205
2678
|
});
|
|
2206
2679
|
if (!cached) await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl, onProgress, cacheDir: dir });
|
|
2207
|
-
const acquisition = (0,
|
|
2680
|
+
const acquisition = (0, import_node_fs12.existsSync)((0, import_node_path7.join)(candidateDir, "payload", "payload.json")) ? readAcquisition((0, import_node_path7.join)(candidateDir, "payload")) : null;
|
|
2208
2681
|
if (acquisition) {
|
|
2209
2682
|
rememberCandidate(dir, candidate, manifest);
|
|
2210
2683
|
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
@@ -2227,10 +2700,10 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2227
2700
|
installer.finish(outcome ?? { version, total: 1, updated: 1, failed: 0, installed: true });
|
|
2228
2701
|
return 0;
|
|
2229
2702
|
}
|
|
2230
|
-
const target = (0,
|
|
2231
|
-
(0,
|
|
2232
|
-
(0,
|
|
2233
|
-
(0,
|
|
2703
|
+
const target = (0, import_node_path7.join)(dir, "payload");
|
|
2704
|
+
(0, import_node_fs12.mkdirSync)(dir, { recursive: true });
|
|
2705
|
+
(0, import_node_fs12.rmSync)(target, { recursive: true, force: true });
|
|
2706
|
+
(0, import_node_fs12.renameSync)((0, import_node_path7.join)(candidateDir, "payload"), target);
|
|
2234
2707
|
writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2235
2708
|
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
2236
2709
|
}
|
|
@@ -2256,11 +2729,11 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2256
2729
|
installer.stop();
|
|
2257
2730
|
}
|
|
2258
2731
|
}
|
|
2259
|
-
function doInstall(config, dir, options, print) {
|
|
2260
|
-
return installOrUpdate(config, dir, options, print, false);
|
|
2732
|
+
function doInstall(config, dir, options, print, flags2) {
|
|
2733
|
+
return installOrUpdate(config, dir, options, print, false, flags2);
|
|
2261
2734
|
}
|
|
2262
|
-
function doUpdate(config, dir, options, print) {
|
|
2263
|
-
return installOrUpdate(config, dir, options, print, true);
|
|
2735
|
+
function doUpdate(config, dir, options, print, flags2) {
|
|
2736
|
+
return installOrUpdate(config, dir, options, print, true, flags2);
|
|
2264
2737
|
}
|
|
2265
2738
|
function payloadEnv(dir) {
|
|
2266
2739
|
const stored = readTokens(dir);
|
|
@@ -2272,12 +2745,12 @@ function payloadFileAsCommand(entry2, payload) {
|
|
|
2272
2745
|
const first = entry2[0];
|
|
2273
2746
|
if (!first || first === "$self") return null;
|
|
2274
2747
|
if (first.includes("/") || first.includes("\\")) return null;
|
|
2275
|
-
const candidate = (0,
|
|
2276
|
-
if (!(0,
|
|
2748
|
+
const candidate = (0, import_node_path7.join)(payload, first);
|
|
2749
|
+
if (!(0, import_node_fs12.existsSync)(candidate)) return null;
|
|
2277
2750
|
if (NEVER_A_PROGRAM.test(first)) return first;
|
|
2278
2751
|
if (process.platform === "win32") return null;
|
|
2279
2752
|
try {
|
|
2280
|
-
return ((0,
|
|
2753
|
+
return ((0, import_node_fs12.statSync)(candidate).mode & 73) === 0 ? first : null;
|
|
2281
2754
|
} catch {
|
|
2282
2755
|
return null;
|
|
2283
2756
|
}
|
|
@@ -2292,7 +2765,7 @@ async function finishLastMile(config, dir, options, installer, version, unchange
|
|
|
2292
2765
|
updated: unchanged ? 0 : 1,
|
|
2293
2766
|
failed: 0,
|
|
2294
2767
|
installed: true,
|
|
2295
|
-
detail: `next step: run ${(0,
|
|
2768
|
+
detail: `next step: run ${(0, import_node_path7.join)(payload, config.binName)} to start ${config.product}.`
|
|
2296
2769
|
});
|
|
2297
2770
|
return 0;
|
|
2298
2771
|
}
|
|
@@ -2338,9 +2811,9 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2338
2811
|
const [command2, ...args] = entry2;
|
|
2339
2812
|
const shell = needsShell(command2);
|
|
2340
2813
|
const progress = !(process.platform === "win32" && shell);
|
|
2341
|
-
const outcomeDir = (0,
|
|
2342
|
-
const outcomeFile = (0,
|
|
2343
|
-
const progressFile = (0,
|
|
2814
|
+
const outcomeDir = (0, import_node_fs12.mkdtempSync)((0, import_node_path7.join)((0, import_node_os5.tmpdir)(), "mm-installer-outcome-"));
|
|
2815
|
+
const outcomeFile = (0, import_node_path7.join)(outcomeDir, "outcome.json");
|
|
2816
|
+
const progressFile = (0, import_node_path7.join)(outcomeDir, "progress.jsonl");
|
|
2344
2817
|
const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
|
|
2345
2818
|
delete childEnv.MM_FACE_TRANSCRIPT;
|
|
2346
2819
|
delete childEnv.MM_PROGRESS_FILE;
|
|
@@ -2353,7 +2826,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2353
2826
|
const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
|
|
2354
2827
|
try {
|
|
2355
2828
|
const result = await new Promise((resolve3) => {
|
|
2356
|
-
const child = (0,
|
|
2829
|
+
const child = (0, import_node_child_process6.spawn)(shell ? [command2, ...args].map(quoteForShell).join(" ") : command2, shell ? [] : args, {
|
|
2357
2830
|
cwd,
|
|
2358
2831
|
shell,
|
|
2359
2832
|
windowsHide: true,
|
|
@@ -2390,8 +2863,8 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2390
2863
|
const pollProgress = () => {
|
|
2391
2864
|
if (progressError) return;
|
|
2392
2865
|
try {
|
|
2393
|
-
if (!(0,
|
|
2394
|
-
const bytes = (0,
|
|
2866
|
+
if (!(0, import_node_fs12.existsSync)(progressFile)) return;
|
|
2867
|
+
const bytes = (0, import_node_fs12.readFileSync)(progressFile);
|
|
2395
2868
|
receive(progressDecoder.write(bytes.subarray(progressOffset)));
|
|
2396
2869
|
progressOffset = bytes.length;
|
|
2397
2870
|
} catch {
|
|
@@ -2424,109 +2897,146 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2424
2897
|
return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
|
|
2425
2898
|
}
|
|
2426
2899
|
} finally {
|
|
2427
|
-
if ((0,
|
|
2428
|
-
if ((0,
|
|
2429
|
-
(0,
|
|
2900
|
+
if ((0, import_node_fs12.existsSync)(progressFile)) (0, import_node_fs12.unlinkSync)(progressFile);
|
|
2901
|
+
if ((0, import_node_fs12.existsSync)(outcomeFile)) (0, import_node_fs12.unlinkSync)(outcomeFile);
|
|
2902
|
+
(0, import_node_fs12.rmdirSync)(outcomeDir);
|
|
2430
2903
|
}
|
|
2431
2904
|
}
|
|
2432
|
-
function
|
|
2433
|
-
if (
|
|
2434
|
-
|
|
2435
|
-
|
|
2905
|
+
function armState(arm) {
|
|
2906
|
+
if (arm.verdict === "fail") return "failed";
|
|
2907
|
+
if (arm.verdict === "defer") return "retry";
|
|
2908
|
+
if (arm.verdict === "skip") return "kept";
|
|
2909
|
+
return arm.from === arm.to ? "current" : "updated";
|
|
2910
|
+
}
|
|
2911
|
+
function doForward(config, dir, options, command2, rest, ui, unlock) {
|
|
2912
|
+
if ((0, import_node_fs12.existsSync)(command2)) {
|
|
2913
|
+
return ui.refuse(`${command2} is a file, not a command.`, `${ui.bin} --run ${command2}`, 2);
|
|
2436
2914
|
}
|
|
2437
2915
|
const acquired = readState(dir)?.acquired;
|
|
2438
2916
|
const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
|
|
2439
2917
|
const verbs = readPayloadVerbs(dir);
|
|
2440
|
-
const declared = verbs === "*" || Array.isArray(verbs) &&
|
|
2441
|
-
if (!target || !declared) {
|
|
2442
|
-
|
|
2443
|
-
printUsage(config, print);
|
|
2444
|
-
return 2;
|
|
2445
|
-
}
|
|
2446
|
-
const forwarded = [...resolveEntry(target), ...argv2];
|
|
2918
|
+
const declared = verbs === "*" || Array.isArray(verbs) && verbs.includes(command2);
|
|
2919
|
+
if (!target || !declared) return ui.refuse(`Unknown command: ${command2}.`, `${ui.bin} --help`, 2);
|
|
2920
|
+
const forwarded = [...resolveEntry(target), command2, ...rest];
|
|
2447
2921
|
const cwd = payloadDir(dir);
|
|
2448
2922
|
const inherited = payloadEnv(dir);
|
|
2449
2923
|
const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
|
|
2450
2924
|
unlock();
|
|
2451
2925
|
const result = (options.runEntry ?? defaultRunEntry)(forwarded, cwd, env);
|
|
2452
2926
|
if (!result.ok && result.code === void 0) {
|
|
2453
|
-
|
|
2454
|
-
return 1;
|
|
2927
|
+
return ui.refuse(`Could not start ${config.product}: ${sentence(result.error ?? `could not run ${forwarded[0]}`)}`, `${ui.bin} doctor`);
|
|
2455
2928
|
}
|
|
2456
2929
|
return result.code ?? 0;
|
|
2457
2930
|
}
|
|
2458
|
-
function doAutoupdate(config, options, args,
|
|
2931
|
+
function doAutoupdate(config, dir, options, args, ui) {
|
|
2459
2932
|
const mode = args[0] ?? "status";
|
|
2460
|
-
const scheduleOptions = options.autoupdate
|
|
2933
|
+
const scheduleOptions = { stateDir: dir, ...options.autoupdate };
|
|
2461
2934
|
const command2 = options.autoupdate?.command ?? [process.execPath, "update"];
|
|
2462
|
-
|
|
2935
|
+
const name = identityFor(ui.product).name;
|
|
2936
|
+
if (mode === "on" || mode === "off") {
|
|
2463
2937
|
try {
|
|
2464
|
-
enableSchedule(config, command2, scheduleOptions);
|
|
2938
|
+
if (mode === "on") enableSchedule(config, command2, scheduleOptions);
|
|
2939
|
+
else disableSchedule(config, scheduleOptions);
|
|
2465
2940
|
} catch (error) {
|
|
2466
|
-
|
|
2467
|
-
return 1;
|
|
2941
|
+
return ui.refuse(sentence(error.message), `${ui.bin} doctor`);
|
|
2468
2942
|
}
|
|
2469
|
-
|
|
2470
|
-
|
|
2943
|
+
if (mode === "on") ui.notice("ok", `Auto-update is on. ${name} checks for updates every hour.`, `${ui.bin} autoupdate status`);
|
|
2944
|
+
else ui.notice("ok", "Auto-update is off.", `${ui.bin} autoupdate on`);
|
|
2471
2945
|
return 0;
|
|
2472
2946
|
}
|
|
2473
|
-
if (mode
|
|
2474
|
-
try {
|
|
2475
|
-
disableSchedule(config, scheduleOptions);
|
|
2476
|
-
} catch (error) {
|
|
2477
|
-
print(error.message);
|
|
2478
|
-
return 1;
|
|
2479
|
-
}
|
|
2480
|
-
print("auto-update: off");
|
|
2481
|
-
return 0;
|
|
2482
|
-
}
|
|
2483
|
-
if (mode !== "status") {
|
|
2484
|
-
print(`unknown autoupdate mode: ${mode} \u2014 use on, off or status.`);
|
|
2485
|
-
return 1;
|
|
2486
|
-
}
|
|
2947
|
+
if (mode !== "status") return ui.refuse(`Unknown autoupdate mode: ${mode}.`, `${ui.bin} autoupdate status`, 2);
|
|
2487
2948
|
const state = querySchedule(config, scheduleOptions);
|
|
2488
|
-
if (!state.supported) {
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
}
|
|
2492
|
-
print(`auto-update: ${state.enabled ? `on (${state.cadence ?? "hourly"})` : "off"}`);
|
|
2493
|
-
if (state.lastRun) print(`last run: ${state.lastRun}`);
|
|
2949
|
+
if (!state.supported) return ui.refuse("Auto-update is not supported on this platform.", `${ui.bin} update`);
|
|
2950
|
+
if (state.enabled) ui.notice("ok", `Auto-update is on and runs every hour.${state.lastRun ? ` Last run: ${state.lastRun}.` : ""}`);
|
|
2951
|
+
else ui.notice("note", "Auto-update is off.", `${ui.bin} autoupdate on`);
|
|
2494
2952
|
return 0;
|
|
2495
2953
|
}
|
|
2496
|
-
function
|
|
2497
|
-
|
|
2954
|
+
function localFacts(config, dir, options) {
|
|
2955
|
+
const state = readState(dir);
|
|
2956
|
+
const tokens = readTokens(dir);
|
|
2957
|
+
const expired = Boolean(tokens && tokens.expiresAt <= Date.now());
|
|
2958
|
+
return {
|
|
2959
|
+
installed: state && (0, import_node_fs12.existsSync)(payloadDir(dir)) ? state.version : null,
|
|
2960
|
+
// An expired access token with a refresh token renews on the next use; it is still signed in.
|
|
2961
|
+
signedIn: Boolean(tokens && (!expired || tokens.refreshToken)),
|
|
2962
|
+
expired,
|
|
2963
|
+
schedule: querySchedule(config, { stateDir: dir, ...options.autoupdate })
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
function signInRow(local) {
|
|
2967
|
+
if (local.signedIn) return { surface: "sign-in", state: local.expired ? "signed in \xB7 renews on next use" : "signed in", mark: "ok" };
|
|
2968
|
+
return { surface: "sign-in", state: local.expired ? "expired" : "signed out", mark: "fail" };
|
|
2969
|
+
}
|
|
2970
|
+
function autoUpdateRow({ supported, enabled, cadence, lastRun }) {
|
|
2971
|
+
if (!supported) return { surface: AUTO_UPDATE, state: "unsupported on this platform", mark: "note" };
|
|
2972
|
+
if (!enabled) return { surface: AUTO_UPDATE, state: "off", mark: "note" };
|
|
2973
|
+
return { surface: AUTO_UPDATE, state: `on \xB7 ${cadence ?? "hourly"}`, mark: "ok", ...lastRun ? { note: `Last run: ${lastRun}` } : {} };
|
|
2974
|
+
}
|
|
2975
|
+
function verdictFor(ui, local, updateAvailable, ready) {
|
|
2976
|
+
if (!local.installed) return { state: "fail", headline: "is not installed.", next: `${ui.bin} install` };
|
|
2977
|
+
if (!local.signedIn) return { state: "fail", headline: "is signed out.", next: `${ui.bin} login` };
|
|
2978
|
+
if (updateAvailable) return { state: "note", headline: "has an update available.", next: `${ui.bin} update` };
|
|
2979
|
+
return { state: "ok", headline: "is ready.", next: ready };
|
|
2980
|
+
}
|
|
2981
|
+
function within(fetchImpl, ms) {
|
|
2982
|
+
return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(ms) }));
|
|
2983
|
+
}
|
|
2984
|
+
var STATUS_PROBE_MS = 3e3;
|
|
2985
|
+
async function doStatus(config, dir, options, ui, print, json) {
|
|
2986
|
+
const fetchImpl = within(options.fetchImpl ?? fetch, STATUS_PROBE_MS);
|
|
2987
|
+
const local = localFacts(config, dir, options);
|
|
2988
|
+
const [release, record] = await Promise.all([
|
|
2989
|
+
fetchReleaseVersion(config, fetchImpl),
|
|
2990
|
+
readLauncherRecord(launcherTarget(config), { fetch: fetchImpl, timeoutMs: STATUS_PROBE_MS })
|
|
2991
|
+
]);
|
|
2992
|
+
const launcherNewer = record !== null && compareVersions(record.version, LAUNCHER_VERSION) > 0;
|
|
2993
|
+
const releaseOrder = local.installed && release ? compareVersions(release, local.installed) : null;
|
|
2994
|
+
const verdict = verdictFor(ui, local, launcherNewer || (releaseOrder ?? 0) > 0, `${ui.bin} doctor`);
|
|
2995
|
+
if (json) {
|
|
2996
|
+
print(JSON.stringify({
|
|
2997
|
+
product: config.product,
|
|
2998
|
+
launcher: { version: LAUNCHER_VERSION, available: launcherNewer ? record.version : null },
|
|
2999
|
+
installed: local.installed,
|
|
3000
|
+
release,
|
|
3001
|
+
signIn: local.signedIn ? local.expired ? "renews" : "signed-in" : local.expired ? "expired" : "signed-out",
|
|
3002
|
+
autoupdate: { supported: local.schedule.supported, enabled: local.schedule.enabled, lastRun: local.schedule.lastRun ?? null },
|
|
3003
|
+
verdict: verdict.state,
|
|
3004
|
+
next: verdict.next
|
|
3005
|
+
}, null, 2));
|
|
3006
|
+
return verdict.state === "fail" ? 1 : 0;
|
|
3007
|
+
}
|
|
3008
|
+
const launcherRow = record === null ? { surface: "launcher", state: `${LAUNCHER_VERSION} \xB7 ${SURFACE_WORDS.unknown}`, mark: "note" } : launcherNewer ? { surface: "launcher", state: `${LAUNCHER_VERSION} \u2192 ${record.version} \xB7 ${SURFACE_WORDS.behind}`, mark: "note" } : { surface: "launcher", state: `${LAUNCHER_VERSION} \xB7 ${SURFACE_WORDS.current}`, mark: "ok" };
|
|
3009
|
+
const productRow = !local.installed ? { surface: config.product, state: SURFACE_WORDS.missing, mark: "fail" } : releaseOrder === null ? { surface: config.product, state: `${local.installed} \xB7 ${SURFACE_WORDS.unknown}`, mark: "note" } : releaseOrder > 0 ? { surface: config.product, state: `${local.installed} \u2192 ${release} \xB7 ${SURFACE_WORDS.behind}`, mark: "note" } : releaseOrder < 0 ? { surface: config.product, state: `${local.installed} \xB7 ${SURFACE_WORDS.ahead}`, mark: "note" } : { surface: config.product, state: `${local.installed} \xB7 ${SURFACE_WORDS.current}`, mark: "ok" };
|
|
3010
|
+
ui.show(renderReport(ui.product, {
|
|
3011
|
+
rows: [launcherRow, productRow, signInRow(local), autoUpdateRow(local.schedule)],
|
|
3012
|
+
verdict: { ...verdict, facts: [release ? `The release is ${release}.` : "The release version could not be read."] }
|
|
3013
|
+
}, ui.render));
|
|
3014
|
+
return verdict.state === "fail" ? 1 : 0;
|
|
3015
|
+
}
|
|
3016
|
+
function doDoctor(config, dir, options, ui) {
|
|
3017
|
+
const local = localFacts(config, dir, options);
|
|
3018
|
+
const verdict = verdictFor(ui, local, false, `${ui.bin} status`);
|
|
3019
|
+
ui.show(renderReport(ui.product, {
|
|
3020
|
+
rows: [
|
|
3021
|
+
{ surface: "launcher", state: LAUNCHER_VERSION, mark: "ok" },
|
|
3022
|
+
local.installed ? { surface: config.product, state: local.installed, mark: "ok" } : { surface: config.product, state: SURFACE_WORDS.missing, mark: "fail" },
|
|
3023
|
+
signInRow(local),
|
|
3024
|
+
autoUpdateRow(local.schedule)
|
|
3025
|
+
],
|
|
3026
|
+
// Paths belong in the receipt, never a row.
|
|
3027
|
+
verdict: { ...verdict, facts: [`Folder: ${dir}`, `Release host: ${config.host}`] }
|
|
3028
|
+
}, ui.render));
|
|
3029
|
+
const own = verdict.state === "fail" ? 1 : 0;
|
|
2498
3030
|
const acquired = readState(dir)?.acquired;
|
|
2499
3031
|
const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
|
|
2500
3032
|
const verbs = readPayloadVerbs(dir);
|
|
2501
3033
|
const chains = target !== null && (verbs === "*" || Array.isArray(verbs) && verbs.includes("doctor"));
|
|
2502
|
-
if (!chains) return
|
|
3034
|
+
if (!chains) return own;
|
|
2503
3035
|
const cwd = payloadDir(dir);
|
|
2504
3036
|
const inherited = payloadEnv(dir);
|
|
2505
3037
|
const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
|
|
2506
|
-
unlock();
|
|
2507
3038
|
const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], cwd, env);
|
|
2508
|
-
return result.code ?? (result.ok ? 0 : 1);
|
|
2509
|
-
}
|
|
2510
|
-
function doLauncherDoctor(config, dir, options, print) {
|
|
2511
|
-
const tokens = readTokens(dir);
|
|
2512
|
-
const state = readState(dir);
|
|
2513
|
-
const payloadPresent = (0, import_node_fs11.existsSync)(payloadDir(dir));
|
|
2514
|
-
print(`product: ${config.product}`);
|
|
2515
|
-
print(`host: ${config.host}`);
|
|
2516
|
-
print(`login: ${config.loginKind}`);
|
|
2517
|
-
print(`dir: ${dir}`);
|
|
2518
|
-
if (tokens) {
|
|
2519
|
-
const remaining = Math.max(0, Math.round((tokens.expiresAt - Date.now()) / 1e3));
|
|
2520
|
-
print(`token: present (expires in ${remaining}s)`);
|
|
2521
|
-
} else {
|
|
2522
|
-
print("token: none \u2014 run login first.");
|
|
2523
|
-
}
|
|
2524
|
-
print(`payload: ${state ? state.version : "none"}${payloadPresent ? "" : " (not downloaded)"}`);
|
|
2525
|
-
print(`paths: tokens ${(0, import_node_path6.join)(dir, "tokens.json")}, state ${(0, import_node_path6.join)(dir, "state.json")}, payload ${payloadDir(dir)}`);
|
|
2526
|
-
const schedule = querySchedule(config, options.autoupdate ?? {});
|
|
2527
|
-
print(
|
|
2528
|
-
`auto-update: ${!schedule.supported ? "unsupported on this platform" : schedule.enabled ? `on (${schedule.cadence ?? "hourly"})` : "off"}`
|
|
2529
|
-
);
|
|
3039
|
+
return (result.code ?? (result.ok ? 0 : 1)) || own;
|
|
2530
3040
|
}
|
|
2531
3041
|
var invokedAsMain = typeof process.argv[1] === "string" && (() => {
|
|
2532
3042
|
try {
|
|
@@ -2545,8 +3055,7 @@ if (invokedAsMain) {
|
|
|
2545
3055
|
process.exitCode = code;
|
|
2546
3056
|
},
|
|
2547
3057
|
(error) => {
|
|
2548
|
-
|
|
2549
|
-
`);
|
|
3058
|
+
printScreen(renderNotice(NO_PRODUCT, { state: "fail", message: sentence(error.message) }, { color: false }), "stderr");
|
|
2550
3059
|
process.exitCode = 1;
|
|
2551
3060
|
}
|
|
2552
3061
|
);
|
|
@@ -2557,6 +3066,7 @@ if (invokedAsMain) {
|
|
|
2557
3066
|
defaultRunEntry,
|
|
2558
3067
|
ensureFreshToken,
|
|
2559
3068
|
formatMb,
|
|
3069
|
+
parseArgv,
|
|
2560
3070
|
readPayloadEntry,
|
|
2561
3071
|
readPayloadRun,
|
|
2562
3072
|
readPayloadVerbs,
|