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