@mutmutco/installer-launcher 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -4
- package/dist/launcher.js +696 -175
- package/dist/launcher.sea.cjs +707 -185
- 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;
|
|
@@ -1229,6 +1286,18 @@ function createInstallerRun(value, options = {}) {
|
|
|
1229
1286
|
let chunkColumn = 0;
|
|
1230
1287
|
const trailingCR = /* @__PURE__ */ new Set();
|
|
1231
1288
|
let deferredActivity;
|
|
1289
|
+
let runningPhase;
|
|
1290
|
+
const animateRunning = (title, measure) => {
|
|
1291
|
+
if (face.progress(title, measure)) return;
|
|
1292
|
+
if (activeTitle === title) spinner.say(title, measure);
|
|
1293
|
+
else {
|
|
1294
|
+
activeTitle = title;
|
|
1295
|
+
spinner.start(title, measure);
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
const resumeRunningPhase = () => {
|
|
1299
|
+
if (runningPhase && !finished) animateRunning(runningPhase.title, runningPhase.measure);
|
|
1300
|
+
};
|
|
1232
1301
|
const drawnPhaseCompletions = /* @__PURE__ */ new Set();
|
|
1233
1302
|
const closeChunk = () => {
|
|
1234
1303
|
if (chunkOpen) emit("\n", chunkChannel);
|
|
@@ -1280,14 +1349,11 @@ function createInstallerRun(value, options = {}) {
|
|
|
1280
1349
|
throw new Error("installer run: invalid runningTitle");
|
|
1281
1350
|
}
|
|
1282
1351
|
const runningTitle = facts.runningTitle?.trim() ?? title;
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
else {
|
|
1286
|
-
activeTitle = runningTitle;
|
|
1287
|
-
spinner.start(runningTitle, facts.measure ?? null);
|
|
1288
|
-
}
|
|
1352
|
+
runningPhase = { id: fold ? "check" : id, title: runningTitle, measure: facts.measure ?? null };
|
|
1353
|
+
animateRunning(runningTitle, facts.measure ?? null);
|
|
1289
1354
|
return;
|
|
1290
1355
|
}
|
|
1356
|
+
if (runningPhase && runningPhase.id === (fold ? "check" : id)) runningPhase = void 0;
|
|
1291
1357
|
if (fold && id !== "check") {
|
|
1292
1358
|
if (facts.detail) run2.relay(facts.detail);
|
|
1293
1359
|
return;
|
|
@@ -1300,17 +1366,18 @@ function createInstallerRun(value, options = {}) {
|
|
|
1300
1366
|
},
|
|
1301
1367
|
surface(facts) {
|
|
1302
1368
|
if (finished) throw new Error("installer run already finished");
|
|
1303
|
-
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);
|
|
1304
1370
|
if (!surface) throw new Error("installer run: undeclared surface");
|
|
1305
1371
|
start();
|
|
1306
1372
|
const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} ${tty ? "\u2192" : "->"} ${facts.to}` : ` ${facts.to}` : "";
|
|
1307
|
-
const status =
|
|
1373
|
+
const status = facts.state === "updated" && options.dryRun ? "would update" : SURFACE_WORDS[facts.state];
|
|
1308
1374
|
if (!status) throw new Error("installer run: unknown surface state");
|
|
1309
1375
|
const separator = tty ? "\xB7" : "-";
|
|
1310
1376
|
const activation = facts.state === "updated" && surface.activation ? ` ${separator} ${surface.activation}` : "";
|
|
1311
1377
|
const completed = ["updated", "current", "kept", "skipped"].includes(facts.state);
|
|
1312
1378
|
durable(`${facts.id}${versions} ${separator} ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
|
|
1313
1379
|
if (facts.detail) run2.relay(facts.detail);
|
|
1380
|
+
resumeRunningPhase();
|
|
1314
1381
|
},
|
|
1315
1382
|
milestone({ step, state, ms, measure }) {
|
|
1316
1383
|
start();
|
|
@@ -1329,6 +1396,7 @@ function createInstallerRun(value, options = {}) {
|
|
|
1329
1396
|
if (state !== "fail" && drawnPhaseCompletions.has(step)) return;
|
|
1330
1397
|
durable(step, ms === void 0 ? null : ms / 1e3, state);
|
|
1331
1398
|
if (state === "ok") drawnPhaseCompletions.add(step);
|
|
1399
|
+
resumeRunningPhase();
|
|
1332
1400
|
},
|
|
1333
1401
|
signIn({ url, code }) {
|
|
1334
1402
|
start();
|
|
@@ -1410,8 +1478,9 @@ function createInstallerRun(value, options = {}) {
|
|
|
1410
1478
|
if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
|
|
1411
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}.`;
|
|
1412
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;
|
|
1413
1482
|
const body = [
|
|
1414
|
-
`${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."}`,
|
|
1415
1484
|
changed,
|
|
1416
1485
|
...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
|
|
1417
1486
|
...facts.detail ? [facts.detail] : [],
|
|
@@ -1427,12 +1496,96 @@ function createInstallerRun(value, options = {}) {
|
|
|
1427
1496
|
stop() {
|
|
1428
1497
|
closeChunk();
|
|
1429
1498
|
activeTitle = void 0;
|
|
1499
|
+
runningPhase = void 0;
|
|
1430
1500
|
spinner.stop();
|
|
1431
1501
|
}
|
|
1432
1502
|
};
|
|
1433
1503
|
return run2;
|
|
1434
1504
|
}
|
|
1435
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
|
+
|
|
1436
1589
|
// src/autoupdate.ts
|
|
1437
1590
|
var import_node_child_process3 = require("node:child_process");
|
|
1438
1591
|
var import_node_fs10 = require("node:fs");
|
|
@@ -1463,19 +1616,33 @@ function scheduleName(config) {
|
|
|
1463
1616
|
function scheduleLabel(config) {
|
|
1464
1617
|
return `${config.binName}-autoupdate`.replace(/[^A-Za-z0-9_-]+/g, "-");
|
|
1465
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
|
+
}
|
|
1466
1630
|
function quoteWindows(arg) {
|
|
1467
1631
|
return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
|
|
1468
1632
|
}
|
|
1469
1633
|
function enableSchedule(config, command2, options = {}) {
|
|
1470
1634
|
const platform = schedulePlatform(options.platform);
|
|
1471
|
-
if (!platform) throw new Error(`
|
|
1635
|
+
if (!platform) throw new Error(`Auto-update is not supported on ${options.platform ?? process.platform}.`);
|
|
1472
1636
|
const exec = options.exec ?? defaultExec;
|
|
1473
1637
|
const home = homeOf(options);
|
|
1474
1638
|
if (platform === "win32") {
|
|
1475
|
-
const
|
|
1476
|
-
|
|
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"]);
|
|
1477
1644
|
if (result2.code !== 0) {
|
|
1478
|
-
throw new Error(`
|
|
1645
|
+
throw new Error(`Could not turn auto-update on: ${firstLine(result2.stdout || result2.stderr) || `exit ${result2.code}`}`);
|
|
1479
1646
|
}
|
|
1480
1647
|
return;
|
|
1481
1648
|
}
|
|
@@ -1488,7 +1655,7 @@ function enableSchedule(config, command2, options = {}) {
|
|
|
1488
1655
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
1489
1656
|
const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
|
|
1490
1657
|
if (result2.code !== 0) {
|
|
1491
|
-
throw new Error(`
|
|
1658
|
+
throw new Error(`Could not turn auto-update on: ${firstLine(result2.stderr || result2.stdout) || `exit ${result2.code}`}`);
|
|
1492
1659
|
}
|
|
1493
1660
|
return;
|
|
1494
1661
|
}
|
|
@@ -1499,23 +1666,24 @@ function enableSchedule(config, command2, options = {}) {
|
|
|
1499
1666
|
(0, import_node_fs10.writeFileSync)((0, import_node_path5.join)(dir, `${label}.timer`), linuxTimer(label));
|
|
1500
1667
|
const reload = exec("systemctl", ["--user", "daemon-reload"]);
|
|
1501
1668
|
if (reload.code !== 0) {
|
|
1502
|
-
throw new Error(`
|
|
1669
|
+
throw new Error(`Could not turn auto-update on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
|
|
1503
1670
|
}
|
|
1504
1671
|
const result = exec("systemctl", ["--user", "enable", "--now", `${label}.timer`]);
|
|
1505
1672
|
if (result.code !== 0) {
|
|
1506
|
-
throw new Error(`
|
|
1673
|
+
throw new Error(`Could not turn auto-update on: ${firstLine(result.stderr || result.stdout) || `exit ${result.code}`}`);
|
|
1507
1674
|
}
|
|
1508
1675
|
}
|
|
1509
1676
|
function disableSchedule(config, options = {}) {
|
|
1510
1677
|
const platform = schedulePlatform(options.platform);
|
|
1511
|
-
if (!platform) throw new Error(`
|
|
1678
|
+
if (!platform) throw new Error(`Auto-update is not supported on ${options.platform ?? process.platform}.`);
|
|
1512
1679
|
const exec = options.exec ?? defaultExec;
|
|
1513
1680
|
const home = homeOf(options);
|
|
1514
1681
|
if (platform === "win32") {
|
|
1515
1682
|
const result = exec("schtasks", ["/Delete", "/TN", scheduleName(config), "/F"]);
|
|
1516
1683
|
if (result.code !== 0 && !/cannot find|does not exist/i.test(`${result.stdout} ${result.stderr}`)) {
|
|
1517
|
-
throw new Error(`
|
|
1684
|
+
throw new Error(`Could not turn auto-update off: ${firstLine(result.stdout || result.stderr) || `exit ${result.code}`}`);
|
|
1518
1685
|
}
|
|
1686
|
+
(0, import_node_fs10.rmSync)((0, import_node_path5.join)(options.stateDir ?? defaultProductDir(config.product), HIDDEN_SCRIPT), { force: true });
|
|
1519
1687
|
return;
|
|
1520
1688
|
}
|
|
1521
1689
|
if (platform === "darwin") {
|
|
@@ -1608,15 +1776,170 @@ function firstLine(text) {
|
|
|
1608
1776
|
return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
|
|
1609
1777
|
}
|
|
1610
1778
|
|
|
1611
|
-
// src/
|
|
1779
|
+
// src/refresh.ts
|
|
1612
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");
|
|
1613
1936
|
var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1614
1937
|
function openBrowser(url) {
|
|
1615
1938
|
if (process.env.LAUNCHER_NO_OPEN === "1") return;
|
|
1616
1939
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
|
|
1617
1940
|
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
1618
1941
|
try {
|
|
1619
|
-
const child = (0,
|
|
1942
|
+
const child = (0, import_node_child_process5.spawn)(opener, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
1620
1943
|
child.on("error", () => {
|
|
1621
1944
|
});
|
|
1622
1945
|
child.unref();
|
|
@@ -1843,7 +2166,7 @@ function page(title, body) {
|
|
|
1843
2166
|
}
|
|
1844
2167
|
|
|
1845
2168
|
// src/index.ts
|
|
1846
|
-
var LAUNCHER_VERSION = true ? "1.
|
|
2169
|
+
var LAUNCHER_VERSION = true ? "1.1.0" : readVersionFromPackage();
|
|
1847
2170
|
function defaultPrint(message) {
|
|
1848
2171
|
process.stdout.write(`${message}
|
|
1849
2172
|
`);
|
|
@@ -1852,87 +2175,200 @@ function defaultPrintErr(message) {
|
|
|
1852
2175
|
process.stderr.write(`${message}
|
|
1853
2176
|
`);
|
|
1854
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
|
+
}
|
|
1855
2257
|
async function run(rawOptions = {}) {
|
|
1856
2258
|
const print = rawOptions.print ?? defaultPrint;
|
|
1857
2259
|
const printErr = rawOptions.printErr ?? defaultPrintErr;
|
|
1858
|
-
const
|
|
1859
|
-
|
|
1860
|
-
|
|
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
|
+
};
|
|
1861
2266
|
let config;
|
|
1862
2267
|
try {
|
|
1863
|
-
config = loadProductConfig({ configPath: rawOptions.configPath ??
|
|
2268
|
+
config = loadProductConfig({ configPath: rawOptions.configPath ?? parsed.flags.config });
|
|
1864
2269
|
} catch (error) {
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
const
|
|
1869
|
-
|
|
1870
|
-
if (
|
|
1871
|
-
|
|
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));
|
|
1872
2278
|
return 0;
|
|
1873
2279
|
}
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
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;
|
|
1877
2284
|
}
|
|
1878
|
-
const
|
|
2285
|
+
const mutating = ["install", "login", "logout"].includes(command2) || command2 === "update" && !parsed.flags.dryRun || command2 === "autoupdate" && ["on", "off"].includes(parsed.args[0] ?? "");
|
|
1879
2286
|
let unlock;
|
|
1880
2287
|
try {
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
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
|
+
}
|
|
1886
2295
|
switch (command2) {
|
|
1887
2296
|
case "login":
|
|
1888
|
-
unlock();
|
|
1889
|
-
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`);
|
|
1890
2300
|
return 0;
|
|
1891
2301
|
case "logout":
|
|
1892
2302
|
wipeProductDir(dir);
|
|
1893
|
-
|
|
2303
|
+
ui.notice("ok", `Signed out of ${identityFor(ui.product).name} and removed the downloaded payload.`, `${ui.bin} login`);
|
|
1894
2304
|
return 0;
|
|
1895
2305
|
case "install":
|
|
1896
|
-
return await doInstall(config, dir, rawOptions, print);
|
|
2306
|
+
return await doInstall(config, dir, rawOptions, print, parsed.flags);
|
|
1897
2307
|
case "update":
|
|
1898
|
-
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));
|
|
1899
2311
|
case "doctor":
|
|
1900
|
-
return doDoctor(config, dir, rawOptions,
|
|
2312
|
+
return doDoctor(config, dir, rawOptions, ui);
|
|
1901
2313
|
case "autoupdate":
|
|
1902
|
-
return doAutoupdate(config, rawOptions,
|
|
2314
|
+
return doAutoupdate(config, dir, rawOptions, parsed.args, ui);
|
|
1903
2315
|
default:
|
|
1904
|
-
return doForward(config, dir, rawOptions, command2,
|
|
2316
|
+
return doForward(config, dir, rawOptions, command2, parsed.rest, ui, () => unlock?.());
|
|
1905
2317
|
}
|
|
1906
2318
|
} catch (error) {
|
|
1907
|
-
if (error instanceof NeedsLoginError) {
|
|
1908
|
-
|
|
1909
|
-
|
|
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`);
|
|
1910
2323
|
}
|
|
1911
|
-
|
|
1912
|
-
return 1;
|
|
2324
|
+
return ui.refuse(sentence(message), `${ui.bin} ${command2}`);
|
|
1913
2325
|
} finally {
|
|
1914
2326
|
unlock?.();
|
|
1915
2327
|
}
|
|
1916
2328
|
}
|
|
1917
|
-
function
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
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
|
+
}
|
|
1922
2337
|
}
|
|
1923
|
-
function
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
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);
|
|
1928
2364
|
}
|
|
1929
2365
|
async function runFile(file, args, printErr) {
|
|
1930
2366
|
if (!file) {
|
|
1931
2367
|
printErr("launcher --run needs a file to run.");
|
|
1932
2368
|
return 1;
|
|
1933
2369
|
}
|
|
1934
|
-
const abs = (0,
|
|
1935
|
-
if (!(0,
|
|
2370
|
+
const abs = (0, import_node_path7.resolve)(process.cwd(), file);
|
|
2371
|
+
if (!(0, import_node_fs12.existsSync)(abs)) {
|
|
1936
2372
|
printErr(`cannot run ${file}: no such file.`);
|
|
1937
2373
|
return 1;
|
|
1938
2374
|
}
|
|
@@ -1988,15 +2424,14 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
|
|
|
1988
2424
|
});
|
|
1989
2425
|
return refreshed.accessToken;
|
|
1990
2426
|
}
|
|
1991
|
-
async function doLogin(config, dir, options,
|
|
2427
|
+
async function doLogin(config, dir, options, prompt, installer) {
|
|
1992
2428
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1993
2429
|
const open = options.open ?? openBrowser;
|
|
1994
2430
|
if (config.loginKind === "github") {
|
|
1995
2431
|
const tokens = await loginGithub(config, {
|
|
1996
2432
|
fetchImpl,
|
|
1997
2433
|
open,
|
|
1998
|
-
|
|
1999
|
-
...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}.`)
|
|
2000
2435
|
});
|
|
2001
2436
|
writeTokens(dir, {
|
|
2002
2437
|
accessToken: tokens.accessToken,
|
|
@@ -2004,7 +2439,12 @@ async function doLogin(config, dir, options, print, installer) {
|
|
|
2004
2439
|
expiresAt: Date.now() + tokens.expiresIn * 1e3
|
|
2005
2440
|
});
|
|
2006
2441
|
} else {
|
|
2007
|
-
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
|
+
});
|
|
2008
2448
|
writeTokens(dir, {
|
|
2009
2449
|
accessToken: tokens.accessToken,
|
|
2010
2450
|
refreshToken: tokens.refreshToken,
|
|
@@ -2012,20 +2452,19 @@ async function doLogin(config, dir, options, print, installer) {
|
|
|
2012
2452
|
clientId: tokens.clientId
|
|
2013
2453
|
});
|
|
2014
2454
|
}
|
|
2015
|
-
if (!installer) print(`signed in to ${config.product}.`);
|
|
2016
2455
|
}
|
|
2017
|
-
async function fetchAccessTokenOrLogin(config, dir, options,
|
|
2456
|
+
async function fetchAccessTokenOrLogin(config, dir, options, installer) {
|
|
2018
2457
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2019
2458
|
const fresh = await ensureFreshToken(config, dir, fetchImpl);
|
|
2020
2459
|
if (fresh) return fresh;
|
|
2021
|
-
await doLogin(config, dir, options,
|
|
2460
|
+
await doLogin(config, dir, options, (line) => installer.relay(line), installer);
|
|
2022
2461
|
const after = readTokens(dir);
|
|
2023
2462
|
if (!after) throw new Error("sign-in did not produce a token");
|
|
2024
2463
|
return after.accessToken;
|
|
2025
2464
|
}
|
|
2026
2465
|
function readPayloadArgv(dir, key) {
|
|
2027
2466
|
try {
|
|
2028
|
-
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"));
|
|
2029
2468
|
const entry2 = parsed[key];
|
|
2030
2469
|
if (typeof entry2 === "string") {
|
|
2031
2470
|
const parts = entry2.trim().split(/\s+/).filter(Boolean);
|
|
@@ -2047,7 +2486,7 @@ function readPayloadRun(dir) {
|
|
|
2047
2486
|
}
|
|
2048
2487
|
function readPayloadVerbs(dir) {
|
|
2049
2488
|
try {
|
|
2050
|
-
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"));
|
|
2051
2490
|
const verbs = parsed.verbs;
|
|
2052
2491
|
if (verbs === "*") return "*";
|
|
2053
2492
|
if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
|
|
@@ -2064,7 +2503,7 @@ function resolveEntry(entry2) {
|
|
|
2064
2503
|
function needsShell(command2) {
|
|
2065
2504
|
if (process.platform !== "win32") return false;
|
|
2066
2505
|
if (/\.(cmd|bat)$/i.test(command2)) return true;
|
|
2067
|
-
return !(0,
|
|
2506
|
+
return !(0, import_node_fs12.existsSync)(command2);
|
|
2068
2507
|
}
|
|
2069
2508
|
function quoteForShell(arg) {
|
|
2070
2509
|
return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
|
|
@@ -2096,7 +2535,7 @@ function defaultRunEntry(entry2, cwd, env) {
|
|
|
2096
2535
|
const [command2, ...args] = entry2;
|
|
2097
2536
|
const shell = needsShell(command2);
|
|
2098
2537
|
const commandLine = shell ? [command2, ...args].map(quoteForShell).join(" ") : command2;
|
|
2099
|
-
const spawnEntry = (progress2) => (0,
|
|
2538
|
+
const spawnEntry = (progress2) => (0, import_node_child_process6.spawnSync)(commandLine, shell ? [] : args, {
|
|
2100
2539
|
cwd,
|
|
2101
2540
|
stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
|
|
2102
2541
|
shell,
|
|
@@ -2130,8 +2569,16 @@ function faceProduct(config) {
|
|
|
2130
2569
|
function formatMb(bytes) {
|
|
2131
2570
|
return (bytes / 1048576).toFixed(1).replace(/\.0$/, "");
|
|
2132
2571
|
}
|
|
2133
|
-
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) {
|
|
2134
2580
|
const product = faceProduct(config);
|
|
2581
|
+
const dryRun = update && Boolean(flags2.dryRun);
|
|
2135
2582
|
const installer = createInstallerRun({
|
|
2136
2583
|
product,
|
|
2137
2584
|
gate: config.host,
|
|
@@ -2141,17 +2588,55 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2141
2588
|
operation: update ? "update" : "install",
|
|
2142
2589
|
tty: options.tty,
|
|
2143
2590
|
animate: !options.print && Boolean(process.stderr.isTTY),
|
|
2591
|
+
dryRun,
|
|
2592
|
+
...flags2.noColor ? { color: false } : {},
|
|
2144
2593
|
...options.print ? { write: (text) => print(text.replace(/\n$/, "")) } : {}
|
|
2145
2594
|
});
|
|
2146
2595
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2147
2596
|
let version = readState(dir)?.version ?? "unknown";
|
|
2148
2597
|
installer.start();
|
|
2149
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
|
+
}
|
|
2150
2628
|
const current = readState(dir);
|
|
2151
|
-
const payloadPresent = (0,
|
|
2629
|
+
const payloadPresent = (0, import_node_fs12.existsSync)(payloadDir(dir));
|
|
2152
2630
|
let started = Date.now();
|
|
2153
2631
|
installer.phase("resolve", { state: "running" });
|
|
2154
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
|
+
}
|
|
2155
2640
|
if (pointer && current?.version === pointer && payloadPresent) {
|
|
2156
2641
|
version = pointer;
|
|
2157
2642
|
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
@@ -2168,14 +2653,14 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2168
2653
|
}
|
|
2169
2654
|
installer.phase("sign-in", { state: "running" });
|
|
2170
2655
|
started = Date.now();
|
|
2171
|
-
const accessToken = await fetchAccessTokenOrLogin(config, dir, options,
|
|
2656
|
+
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, installer);
|
|
2172
2657
|
installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
|
|
2173
2658
|
installer.phase("resolve", { state: "running" });
|
|
2174
2659
|
started = Date.now();
|
|
2175
2660
|
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
2176
2661
|
version = manifest.version;
|
|
2177
2662
|
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
2178
|
-
const unchanged = current?.version === version && (0,
|
|
2663
|
+
const unchanged = current?.version === version && (0, import_node_fs12.existsSync)(payloadDir(dir));
|
|
2179
2664
|
if (unchanged && current.acquired) {
|
|
2180
2665
|
installer.surface({ id: config.product, from: version, to: version, state: "current" });
|
|
2181
2666
|
return await finishLastMile(config, dir, options, installer, version, true, acquisitionRepairCommand(dir, version));
|
|
@@ -2185,14 +2670,14 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2185
2670
|
started = Date.now();
|
|
2186
2671
|
const cached = reusableCandidate(dir, manifest);
|
|
2187
2672
|
const candidate = cached ?? (0, import_node_crypto6.randomUUID)();
|
|
2188
|
-
const candidateDir = (0,
|
|
2673
|
+
const candidateDir = (0, import_node_path7.join)(dir, "candidates", candidate);
|
|
2189
2674
|
const onProgress = ({ done, total }) => installer.phase("download", {
|
|
2190
2675
|
state: "running",
|
|
2191
2676
|
measure: total ? `${Math.floor(done / total * 100)}% \xB7 ${formatMb(done)} of ${formatMb(total)} MB` : `${formatMb(done)} MB`,
|
|
2192
2677
|
...total ? { runningTitle: `Downloading ${config.product} ${version} (${formatMb(total)} MB)` } : {}
|
|
2193
2678
|
});
|
|
2194
2679
|
if (!cached) await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl, onProgress, cacheDir: dir });
|
|
2195
|
-
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;
|
|
2196
2681
|
if (acquisition) {
|
|
2197
2682
|
rememberCandidate(dir, candidate, manifest);
|
|
2198
2683
|
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
@@ -2215,10 +2700,10 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2215
2700
|
installer.finish(outcome ?? { version, total: 1, updated: 1, failed: 0, installed: true });
|
|
2216
2701
|
return 0;
|
|
2217
2702
|
}
|
|
2218
|
-
const target = (0,
|
|
2219
|
-
(0,
|
|
2220
|
-
(0,
|
|
2221
|
-
(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);
|
|
2222
2707
|
writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2223
2708
|
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
2224
2709
|
}
|
|
@@ -2244,11 +2729,11 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2244
2729
|
installer.stop();
|
|
2245
2730
|
}
|
|
2246
2731
|
}
|
|
2247
|
-
function doInstall(config, dir, options, print) {
|
|
2248
|
-
return installOrUpdate(config, dir, options, print, false);
|
|
2732
|
+
function doInstall(config, dir, options, print, flags2) {
|
|
2733
|
+
return installOrUpdate(config, dir, options, print, false, flags2);
|
|
2249
2734
|
}
|
|
2250
|
-
function doUpdate(config, dir, options, print) {
|
|
2251
|
-
return installOrUpdate(config, dir, options, print, true);
|
|
2735
|
+
function doUpdate(config, dir, options, print, flags2) {
|
|
2736
|
+
return installOrUpdate(config, dir, options, print, true, flags2);
|
|
2252
2737
|
}
|
|
2253
2738
|
function payloadEnv(dir) {
|
|
2254
2739
|
const stored = readTokens(dir);
|
|
@@ -2260,12 +2745,12 @@ function payloadFileAsCommand(entry2, payload) {
|
|
|
2260
2745
|
const first = entry2[0];
|
|
2261
2746
|
if (!first || first === "$self") return null;
|
|
2262
2747
|
if (first.includes("/") || first.includes("\\")) return null;
|
|
2263
|
-
const candidate = (0,
|
|
2264
|
-
if (!(0,
|
|
2748
|
+
const candidate = (0, import_node_path7.join)(payload, first);
|
|
2749
|
+
if (!(0, import_node_fs12.existsSync)(candidate)) return null;
|
|
2265
2750
|
if (NEVER_A_PROGRAM.test(first)) return first;
|
|
2266
2751
|
if (process.platform === "win32") return null;
|
|
2267
2752
|
try {
|
|
2268
|
-
return ((0,
|
|
2753
|
+
return ((0, import_node_fs12.statSync)(candidate).mode & 73) === 0 ? first : null;
|
|
2269
2754
|
} catch {
|
|
2270
2755
|
return null;
|
|
2271
2756
|
}
|
|
@@ -2280,7 +2765,7 @@ async function finishLastMile(config, dir, options, installer, version, unchange
|
|
|
2280
2765
|
updated: unchanged ? 0 : 1,
|
|
2281
2766
|
failed: 0,
|
|
2282
2767
|
installed: true,
|
|
2283
|
-
detail: `next step: run ${(0,
|
|
2768
|
+
detail: `next step: run ${(0, import_node_path7.join)(payload, config.binName)} to start ${config.product}.`
|
|
2284
2769
|
});
|
|
2285
2770
|
return 0;
|
|
2286
2771
|
}
|
|
@@ -2326,9 +2811,9 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2326
2811
|
const [command2, ...args] = entry2;
|
|
2327
2812
|
const shell = needsShell(command2);
|
|
2328
2813
|
const progress = !(process.platform === "win32" && shell);
|
|
2329
|
-
const outcomeDir = (0,
|
|
2330
|
-
const outcomeFile = (0,
|
|
2331
|
-
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");
|
|
2332
2817
|
const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
|
|
2333
2818
|
delete childEnv.MM_FACE_TRANSCRIPT;
|
|
2334
2819
|
delete childEnv.MM_PROGRESS_FILE;
|
|
@@ -2341,7 +2826,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2341
2826
|
const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
|
|
2342
2827
|
try {
|
|
2343
2828
|
const result = await new Promise((resolve3) => {
|
|
2344
|
-
const child = (0,
|
|
2829
|
+
const child = (0, import_node_child_process6.spawn)(shell ? [command2, ...args].map(quoteForShell).join(" ") : command2, shell ? [] : args, {
|
|
2345
2830
|
cwd,
|
|
2346
2831
|
shell,
|
|
2347
2832
|
windowsHide: true,
|
|
@@ -2378,8 +2863,8 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2378
2863
|
const pollProgress = () => {
|
|
2379
2864
|
if (progressError) return;
|
|
2380
2865
|
try {
|
|
2381
|
-
if (!(0,
|
|
2382
|
-
const bytes = (0,
|
|
2866
|
+
if (!(0, import_node_fs12.existsSync)(progressFile)) return;
|
|
2867
|
+
const bytes = (0, import_node_fs12.readFileSync)(progressFile);
|
|
2383
2868
|
receive(progressDecoder.write(bytes.subarray(progressOffset)));
|
|
2384
2869
|
progressOffset = bytes.length;
|
|
2385
2870
|
} catch {
|
|
@@ -2412,109 +2897,146 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2412
2897
|
return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
|
|
2413
2898
|
}
|
|
2414
2899
|
} finally {
|
|
2415
|
-
if ((0,
|
|
2416
|
-
if ((0,
|
|
2417
|
-
(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);
|
|
2418
2903
|
}
|
|
2419
2904
|
}
|
|
2420
|
-
function
|
|
2421
|
-
if (
|
|
2422
|
-
|
|
2423
|
-
|
|
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);
|
|
2424
2914
|
}
|
|
2425
2915
|
const acquired = readState(dir)?.acquired;
|
|
2426
2916
|
const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
|
|
2427
2917
|
const verbs = readPayloadVerbs(dir);
|
|
2428
|
-
const declared = verbs === "*" || Array.isArray(verbs) &&
|
|
2429
|
-
if (!target || !declared) {
|
|
2430
|
-
|
|
2431
|
-
printUsage(config, print);
|
|
2432
|
-
return 2;
|
|
2433
|
-
}
|
|
2434
|
-
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];
|
|
2435
2921
|
const cwd = payloadDir(dir);
|
|
2436
2922
|
const inherited = payloadEnv(dir);
|
|
2437
2923
|
const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
|
|
2438
2924
|
unlock();
|
|
2439
2925
|
const result = (options.runEntry ?? defaultRunEntry)(forwarded, cwd, env);
|
|
2440
2926
|
if (!result.ok && result.code === void 0) {
|
|
2441
|
-
|
|
2442
|
-
return 1;
|
|
2927
|
+
return ui.refuse(`Could not start ${config.product}: ${sentence(result.error ?? `could not run ${forwarded[0]}`)}`, `${ui.bin} doctor`);
|
|
2443
2928
|
}
|
|
2444
2929
|
return result.code ?? 0;
|
|
2445
2930
|
}
|
|
2446
|
-
function doAutoupdate(config, options, args,
|
|
2931
|
+
function doAutoupdate(config, dir, options, args, ui) {
|
|
2447
2932
|
const mode = args[0] ?? "status";
|
|
2448
|
-
const scheduleOptions = options.autoupdate
|
|
2933
|
+
const scheduleOptions = { stateDir: dir, ...options.autoupdate };
|
|
2449
2934
|
const command2 = options.autoupdate?.command ?? [process.execPath, "update"];
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
enableSchedule(config, command2, scheduleOptions);
|
|
2453
|
-
} catch (error) {
|
|
2454
|
-
print(error.message);
|
|
2455
|
-
return 1;
|
|
2456
|
-
}
|
|
2457
|
-
const state2 = querySchedule(config, scheduleOptions);
|
|
2458
|
-
print(`auto-update: on (${state2.cadence ?? "hourly"})`);
|
|
2459
|
-
return 0;
|
|
2460
|
-
}
|
|
2461
|
-
if (mode === "off") {
|
|
2935
|
+
const name = identityFor(ui.product).name;
|
|
2936
|
+
if (mode === "on" || mode === "off") {
|
|
2462
2937
|
try {
|
|
2463
|
-
|
|
2938
|
+
if (mode === "on") enableSchedule(config, command2, scheduleOptions);
|
|
2939
|
+
else disableSchedule(config, scheduleOptions);
|
|
2464
2940
|
} catch (error) {
|
|
2465
|
-
|
|
2466
|
-
return 1;
|
|
2941
|
+
return ui.refuse(sentence(error.message), `${ui.bin} doctor`);
|
|
2467
2942
|
}
|
|
2468
|
-
|
|
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`);
|
|
2469
2945
|
return 0;
|
|
2470
2946
|
}
|
|
2471
|
-
if (mode !== "status") {
|
|
2472
|
-
print(`unknown autoupdate mode: ${mode} \u2014 use on, off or status.`);
|
|
2473
|
-
return 1;
|
|
2474
|
-
}
|
|
2947
|
+
if (mode !== "status") return ui.refuse(`Unknown autoupdate mode: ${mode}.`, `${ui.bin} autoupdate status`, 2);
|
|
2475
2948
|
const state = querySchedule(config, scheduleOptions);
|
|
2476
|
-
if (!state.supported) {
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
}
|
|
2480
|
-
print(`auto-update: ${state.enabled ? `on (${state.cadence ?? "hourly"})` : "off"}`);
|
|
2481
|
-
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`);
|
|
2482
2952
|
return 0;
|
|
2483
2953
|
}
|
|
2484
|
-
function
|
|
2485
|
-
|
|
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;
|
|
2486
3030
|
const acquired = readState(dir)?.acquired;
|
|
2487
3031
|
const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
|
|
2488
3032
|
const verbs = readPayloadVerbs(dir);
|
|
2489
3033
|
const chains = target !== null && (verbs === "*" || Array.isArray(verbs) && verbs.includes("doctor"));
|
|
2490
|
-
if (!chains) return
|
|
3034
|
+
if (!chains) return own;
|
|
2491
3035
|
const cwd = payloadDir(dir);
|
|
2492
3036
|
const inherited = payloadEnv(dir);
|
|
2493
3037
|
const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
|
|
2494
|
-
unlock();
|
|
2495
3038
|
const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], cwd, env);
|
|
2496
|
-
return result.code ?? (result.ok ? 0 : 1);
|
|
2497
|
-
}
|
|
2498
|
-
function doLauncherDoctor(config, dir, options, print) {
|
|
2499
|
-
const tokens = readTokens(dir);
|
|
2500
|
-
const state = readState(dir);
|
|
2501
|
-
const payloadPresent = (0, import_node_fs11.existsSync)(payloadDir(dir));
|
|
2502
|
-
print(`product: ${config.product}`);
|
|
2503
|
-
print(`host: ${config.host}`);
|
|
2504
|
-
print(`login: ${config.loginKind}`);
|
|
2505
|
-
print(`dir: ${dir}`);
|
|
2506
|
-
if (tokens) {
|
|
2507
|
-
const remaining = Math.max(0, Math.round((tokens.expiresAt - Date.now()) / 1e3));
|
|
2508
|
-
print(`token: present (expires in ${remaining}s)`);
|
|
2509
|
-
} else {
|
|
2510
|
-
print("token: none \u2014 run login first.");
|
|
2511
|
-
}
|
|
2512
|
-
print(`payload: ${state ? state.version : "none"}${payloadPresent ? "" : " (not downloaded)"}`);
|
|
2513
|
-
print(`paths: tokens ${(0, import_node_path6.join)(dir, "tokens.json")}, state ${(0, import_node_path6.join)(dir, "state.json")}, payload ${payloadDir(dir)}`);
|
|
2514
|
-
const schedule = querySchedule(config, options.autoupdate ?? {});
|
|
2515
|
-
print(
|
|
2516
|
-
`auto-update: ${!schedule.supported ? "unsupported on this platform" : schedule.enabled ? `on (${schedule.cadence ?? "hourly"})` : "off"}`
|
|
2517
|
-
);
|
|
3039
|
+
return (result.code ?? (result.ok ? 0 : 1)) || own;
|
|
2518
3040
|
}
|
|
2519
3041
|
var invokedAsMain = typeof process.argv[1] === "string" && (() => {
|
|
2520
3042
|
try {
|
|
@@ -2533,8 +3055,7 @@ if (invokedAsMain) {
|
|
|
2533
3055
|
process.exitCode = code;
|
|
2534
3056
|
},
|
|
2535
3057
|
(error) => {
|
|
2536
|
-
|
|
2537
|
-
`);
|
|
3058
|
+
printScreen(renderNotice(NO_PRODUCT, { state: "fail", message: sentence(error.message) }, { color: false }), "stderr");
|
|
2538
3059
|
process.exitCode = 1;
|
|
2539
3060
|
}
|
|
2540
3061
|
);
|
|
@@ -2545,6 +3066,7 @@ if (invokedAsMain) {
|
|
|
2545
3066
|
defaultRunEntry,
|
|
2546
3067
|
ensureFreshToken,
|
|
2547
3068
|
formatMb,
|
|
3069
|
+
parseArgv,
|
|
2548
3070
|
readPayloadEntry,
|
|
2549
3071
|
readPayloadRun,
|
|
2550
3072
|
readPayloadVerbs,
|