@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.js
CHANGED
|
@@ -296,6 +296,11 @@ function parseProductConfig(text) {
|
|
|
296
296
|
} catch {
|
|
297
297
|
throw new Error("product config host must be an http(s) URL");
|
|
298
298
|
}
|
|
299
|
+
const dl = field(record, "dl").replace(/\/+$/, "");
|
|
300
|
+
if (dl) {
|
|
301
|
+
if (!/^https:\/\//u.test(dl)) throw new Error("product config dl must be an https URL");
|
|
302
|
+
config.dl = dl;
|
|
303
|
+
}
|
|
299
304
|
return config;
|
|
300
305
|
}
|
|
301
306
|
function field(record, key) {
|
|
@@ -672,9 +677,10 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
|
|
|
672
677
|
|
|
673
678
|
// src/index.ts
|
|
674
679
|
import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
|
675
|
-
import { existsSync as
|
|
680
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8, statSync, mkdtempSync as mkdtempSync2, unlinkSync as unlinkSync2, rmdirSync, mkdirSync as mkdirSync6, renameSync as renameSync5, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
|
|
676
681
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
677
|
-
import { dirname as
|
|
682
|
+
import { dirname as dirname4, join as join7, resolve as resolve2 } from "node:path";
|
|
683
|
+
import { isSea } from "node:sea";
|
|
678
684
|
import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
|
|
679
685
|
|
|
680
686
|
// ../face/src/face.ts
|
|
@@ -685,6 +691,7 @@ var PRODUCTS = Object.freeze({
|
|
|
685
691
|
"mm-strategy": Object.freeze({
|
|
686
692
|
name: "MM Strategy",
|
|
687
693
|
installWarm: "Welcome. Let's get you ready.",
|
|
694
|
+
checkWarm: "Here's where things stand.",
|
|
688
695
|
accent: "38;2;249;115;22",
|
|
689
696
|
warm: "Welcome back. Checking for updates...",
|
|
690
697
|
doctor: "mm-strategy doctor"
|
|
@@ -692,6 +699,7 @@ var PRODUCTS = Object.freeze({
|
|
|
692
699
|
"mmi-hub": Object.freeze({
|
|
693
700
|
name: "mmi-hub",
|
|
694
701
|
installWarm: "Welcome. Let's get you ready.",
|
|
702
|
+
checkWarm: "Here's where things stand.",
|
|
695
703
|
accent: "38;2;125;211;252",
|
|
696
704
|
warm: "Welcome back. Checking for updates...",
|
|
697
705
|
doctor: "mmi doctor"
|
|
@@ -699,6 +707,7 @@ var PRODUCTS = Object.freeze({
|
|
|
699
707
|
"jerv-hub": Object.freeze({
|
|
700
708
|
name: "jerv-hub",
|
|
701
709
|
installWarm: "Welcome. Let's get you ready.",
|
|
710
|
+
checkWarm: "Here's where things stand.",
|
|
702
711
|
accent: "38;2;248;113;113",
|
|
703
712
|
warm: "Welcome back. Checking for updates...",
|
|
704
713
|
doctor: "jerv doctor"
|
|
@@ -706,6 +715,7 @@ var PRODUCTS = Object.freeze({
|
|
|
706
715
|
jervcode: Object.freeze({
|
|
707
716
|
name: "JervCode",
|
|
708
717
|
installWarm: "Welcome. Let's get you ready.",
|
|
718
|
+
checkWarm: "Here's where things stand.",
|
|
709
719
|
accent: "38;2;192;132;252",
|
|
710
720
|
warm: "Welcome back. Checking for updates...",
|
|
711
721
|
doctor: "jervcode doctor"
|
|
@@ -821,7 +831,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
821
831
|
const welcome = () => continuesFace || nested ? [] : [
|
|
822
832
|
`${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
|
|
823
833
|
bar(),
|
|
824
|
-
`${bar()} ${operation === "install" ? identity.installWarm : identity.warm}`,
|
|
834
|
+
`${bar()} ${operation === "install" ? identity.installWarm : operation === "check" ? identity.checkWarm : identity.warm}`,
|
|
825
835
|
bar()
|
|
826
836
|
];
|
|
827
837
|
const continues = (phase, kind = "ok") => {
|
|
@@ -851,7 +861,11 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
851
861
|
const content = Math.min(width - 6, Math.max(...body.map(visibleWidth)));
|
|
852
862
|
const rule = paint(identity.accent, GLYPH.rule.repeat(content + 4));
|
|
853
863
|
const frame = (left, right) => `${paint(identity.accent, left)}${rule}${paint(identity.accent, right)}`;
|
|
854
|
-
const
|
|
864
|
+
const headGlyph = { [GLYPH.check]: identity.accent, [GLYPH.cross]: identity.accent, [GLYPH.dot]: PALETTE.muted };
|
|
865
|
+
const bodyRow = (line) => {
|
|
866
|
+
const sgr = headGlyph[line.charAt(0)];
|
|
867
|
+
return sgr ? `${paint(sgr, line.charAt(0))}${line.slice(1)}` : line;
|
|
868
|
+
};
|
|
855
869
|
return [
|
|
856
870
|
frame(GLYPH.boxTop, GLYPH.boxTopEnd),
|
|
857
871
|
...body.map((line) => `${paint(identity.accent, GLYPH.bar)} ${bodyRow(line)}${" ".repeat(Math.max(0, content - visibleWidth(line)))} ${paint(identity.accent, GLYPH.bar)}`),
|
|
@@ -877,6 +891,9 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
877
891
|
// ../face/src/shell.ts
|
|
878
892
|
var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
|
|
879
893
|
|
|
894
|
+
// ../face/src/rows.ts
|
|
895
|
+
var plainText = (text) => String(text).replaceAll("\u2192", "->").replaceAll("\xB7", "-").replaceAll("\u2014", "-").replaceAll("\u2026", "...");
|
|
896
|
+
|
|
880
897
|
// ../face/src/spinner.ts
|
|
881
898
|
import { appendFileSync as appendFileSync2, writeSync as writeSync2 } from "node:fs";
|
|
882
899
|
import { Worker } from "node:worker_threads";
|
|
@@ -1109,22 +1126,44 @@ function validateInstallerProduct(value) {
|
|
|
1109
1126
|
if (doctor !== identity.doctor) return fail("doctor");
|
|
1110
1127
|
if (!Array.isArray(input.surfaces) || input.surfaces.length === 0) return fail("surfaces");
|
|
1111
1128
|
const ids = /* @__PURE__ */ new Set();
|
|
1112
|
-
const surfaces = input.surfaces.map((
|
|
1113
|
-
if (!
|
|
1114
|
-
const
|
|
1115
|
-
const id = text(
|
|
1129
|
+
const surfaces = input.surfaces.map((raw2) => {
|
|
1130
|
+
if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return fail("surface");
|
|
1131
|
+
const source2 = raw2;
|
|
1132
|
+
const id = text(source2.id, "surface.id");
|
|
1116
1133
|
if (ids.has(id)) return fail("duplicate surface.id");
|
|
1117
1134
|
ids.add(id);
|
|
1118
1135
|
const surface = { id };
|
|
1119
1136
|
for (const field2 of ["npm", "bin", "kind", "activation"]) {
|
|
1120
|
-
if (
|
|
1137
|
+
if (source2[field2] !== void 0) surface[field2] = text(source2[field2], `surface.${field2}`);
|
|
1121
1138
|
}
|
|
1122
1139
|
if (Boolean(surface.npm) !== Boolean(surface.bin)) return fail("surface npm/bin pair");
|
|
1123
1140
|
if (surface.kind !== void 0 && !["agent-home", "payload"].includes(surface.kind)) return fail("surface.kind");
|
|
1124
1141
|
if (!surface.npm && !surface.kind) return fail("surface implementation");
|
|
1125
1142
|
return surface;
|
|
1126
1143
|
});
|
|
1127
|
-
return { product, gate, doctor, surfaces };
|
|
1144
|
+
if (input.launcher === void 0) return { product, gate, doctor, surfaces };
|
|
1145
|
+
const raw = input.launcher;
|
|
1146
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail("launcher");
|
|
1147
|
+
const source = raw;
|
|
1148
|
+
const https = (value2, field2) => {
|
|
1149
|
+
const url = text(value2, field2).replace(/\/+$/u, "");
|
|
1150
|
+
let parsed;
|
|
1151
|
+
try {
|
|
1152
|
+
parsed = new URL(url);
|
|
1153
|
+
} catch {
|
|
1154
|
+
return fail(field2);
|
|
1155
|
+
}
|
|
1156
|
+
return parsed.protocol === "https:" && !parsed.username && !parsed.password ? url : fail(field2);
|
|
1157
|
+
};
|
|
1158
|
+
const launcher = {
|
|
1159
|
+
product: text(source.product, "launcher.product"),
|
|
1160
|
+
bin: text(source.bin, "launcher.bin"),
|
|
1161
|
+
key: text(source.key, "launcher.key"),
|
|
1162
|
+
host: https(source.host, "launcher.host")
|
|
1163
|
+
};
|
|
1164
|
+
if (source.dl !== void 0) launcher.dl = https(source.dl, "launcher.dl");
|
|
1165
|
+
if (Buffer.from(launcher.key, "base64").length !== 32) return fail("launcher.key");
|
|
1166
|
+
return { product, gate, doctor, surfaces, launcher };
|
|
1128
1167
|
}
|
|
1129
1168
|
var PHASES = {
|
|
1130
1169
|
preflight: ["Checking prerequisites", "Checked prerequisites"],
|
|
@@ -1149,6 +1188,23 @@ var UPDATE_CHECK_PHASES = /* @__PURE__ */ new Set([
|
|
|
1149
1188
|
"sign-in",
|
|
1150
1189
|
"check"
|
|
1151
1190
|
]);
|
|
1191
|
+
var SURFACE_WORDS = Object.freeze({
|
|
1192
|
+
updated: "updated",
|
|
1193
|
+
current: "already current",
|
|
1194
|
+
failed: "failed",
|
|
1195
|
+
skipped: "skipped",
|
|
1196
|
+
retry: "retrying",
|
|
1197
|
+
pending: "pending",
|
|
1198
|
+
kept: "kept",
|
|
1199
|
+
/** Read-only report states. */
|
|
1200
|
+
behind: "update available",
|
|
1201
|
+
ahead: "ahead of the release",
|
|
1202
|
+
staged: "next launch",
|
|
1203
|
+
unknown: "unknown",
|
|
1204
|
+
missing: "not installed"
|
|
1205
|
+
});
|
|
1206
|
+
var AUTO_UPDATE = "auto-update";
|
|
1207
|
+
var LAUNCHER_SURFACE = "launcher";
|
|
1152
1208
|
function createInstallerRun(value, options = {}) {
|
|
1153
1209
|
const declaration = validateInstallerProduct(value);
|
|
1154
1210
|
const env = options.env ?? process.env;
|
|
@@ -1196,6 +1252,18 @@ function createInstallerRun(value, options = {}) {
|
|
|
1196
1252
|
let chunkColumn = 0;
|
|
1197
1253
|
const trailingCR = /* @__PURE__ */ new Set();
|
|
1198
1254
|
let deferredActivity;
|
|
1255
|
+
let runningPhase;
|
|
1256
|
+
const animateRunning = (title, measure) => {
|
|
1257
|
+
if (face.progress(title, measure)) return;
|
|
1258
|
+
if (activeTitle === title) spinner.say(title, measure);
|
|
1259
|
+
else {
|
|
1260
|
+
activeTitle = title;
|
|
1261
|
+
spinner.start(title, measure);
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
const resumeRunningPhase = () => {
|
|
1265
|
+
if (runningPhase && !finished) animateRunning(runningPhase.title, runningPhase.measure);
|
|
1266
|
+
};
|
|
1199
1267
|
const drawnPhaseCompletions = /* @__PURE__ */ new Set();
|
|
1200
1268
|
const closeChunk = () => {
|
|
1201
1269
|
if (chunkOpen) emit("\n", chunkChannel);
|
|
@@ -1247,14 +1315,11 @@ function createInstallerRun(value, options = {}) {
|
|
|
1247
1315
|
throw new Error("installer run: invalid runningTitle");
|
|
1248
1316
|
}
|
|
1249
1317
|
const runningTitle = facts.runningTitle?.trim() ?? title;
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
else {
|
|
1253
|
-
activeTitle = runningTitle;
|
|
1254
|
-
spinner.start(runningTitle, facts.measure ?? null);
|
|
1255
|
-
}
|
|
1318
|
+
runningPhase = { id: fold ? "check" : id, title: runningTitle, measure: facts.measure ?? null };
|
|
1319
|
+
animateRunning(runningTitle, facts.measure ?? null);
|
|
1256
1320
|
return;
|
|
1257
1321
|
}
|
|
1322
|
+
if (runningPhase && runningPhase.id === (fold ? "check" : id)) runningPhase = void 0;
|
|
1258
1323
|
if (fold && id !== "check") {
|
|
1259
1324
|
if (facts.detail) run2.relay(facts.detail);
|
|
1260
1325
|
return;
|
|
@@ -1267,17 +1332,18 @@ function createInstallerRun(value, options = {}) {
|
|
|
1267
1332
|
},
|
|
1268
1333
|
surface(facts) {
|
|
1269
1334
|
if (finished) throw new Error("installer run already finished");
|
|
1270
|
-
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);
|
|
1271
1336
|
if (!surface) throw new Error("installer run: undeclared surface");
|
|
1272
1337
|
start();
|
|
1273
1338
|
const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} ${tty ? "\u2192" : "->"} ${facts.to}` : ` ${facts.to}` : "";
|
|
1274
|
-
const status =
|
|
1339
|
+
const status = facts.state === "updated" && options.dryRun ? "would update" : SURFACE_WORDS[facts.state];
|
|
1275
1340
|
if (!status) throw new Error("installer run: unknown surface state");
|
|
1276
1341
|
const separator = tty ? "\xB7" : "-";
|
|
1277
1342
|
const activation = facts.state === "updated" && surface.activation ? ` ${separator} ${surface.activation}` : "";
|
|
1278
1343
|
const completed = ["updated", "current", "kept", "skipped"].includes(facts.state);
|
|
1279
1344
|
durable(`${facts.id}${versions} ${separator} ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
|
|
1280
1345
|
if (facts.detail) run2.relay(facts.detail);
|
|
1346
|
+
resumeRunningPhase();
|
|
1281
1347
|
},
|
|
1282
1348
|
milestone({ step, state, ms, measure }) {
|
|
1283
1349
|
start();
|
|
@@ -1296,6 +1362,7 @@ function createInstallerRun(value, options = {}) {
|
|
|
1296
1362
|
if (state !== "fail" && drawnPhaseCompletions.has(step)) return;
|
|
1297
1363
|
durable(step, ms === void 0 ? null : ms / 1e3, state);
|
|
1298
1364
|
if (state === "ok") drawnPhaseCompletions.add(step);
|
|
1365
|
+
resumeRunningPhase();
|
|
1299
1366
|
},
|
|
1300
1367
|
signIn({ url, code }) {
|
|
1301
1368
|
start();
|
|
@@ -1377,8 +1444,9 @@ function createInstallerRun(value, options = {}) {
|
|
|
1377
1444
|
if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
|
|
1378
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}.`;
|
|
1379
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;
|
|
1380
1448
|
const body = [
|
|
1381
|
-
`${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."}`,
|
|
1382
1450
|
changed,
|
|
1383
1451
|
...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
|
|
1384
1452
|
...facts.detail ? [facts.detail] : [],
|
|
@@ -1394,12 +1462,96 @@ function createInstallerRun(value, options = {}) {
|
|
|
1394
1462
|
stop() {
|
|
1395
1463
|
closeChunk();
|
|
1396
1464
|
activeTitle = void 0;
|
|
1465
|
+
runningPhase = void 0;
|
|
1397
1466
|
spinner.stop();
|
|
1398
1467
|
}
|
|
1399
1468
|
};
|
|
1400
1469
|
return run2;
|
|
1401
1470
|
}
|
|
1402
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
|
+
|
|
1403
1555
|
// src/autoupdate.ts
|
|
1404
1556
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1405
1557
|
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
@@ -1430,19 +1582,33 @@ function scheduleName(config) {
|
|
|
1430
1582
|
function scheduleLabel(config) {
|
|
1431
1583
|
return `${config.binName}-autoupdate`.replace(/[^A-Za-z0-9_-]+/g, "-");
|
|
1432
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
|
+
}
|
|
1433
1596
|
function quoteWindows(arg) {
|
|
1434
1597
|
return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
|
|
1435
1598
|
}
|
|
1436
1599
|
function enableSchedule(config, command2, options = {}) {
|
|
1437
1600
|
const platform = schedulePlatform(options.platform);
|
|
1438
|
-
if (!platform) throw new Error(`
|
|
1601
|
+
if (!platform) throw new Error(`Auto-update is not supported on ${options.platform ?? process.platform}.`);
|
|
1439
1602
|
const exec = options.exec ?? defaultExec;
|
|
1440
1603
|
const home = homeOf(options);
|
|
1441
1604
|
if (platform === "win32") {
|
|
1442
|
-
const
|
|
1443
|
-
|
|
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"]);
|
|
1444
1610
|
if (result2.code !== 0) {
|
|
1445
|
-
throw new Error(`
|
|
1611
|
+
throw new Error(`Could not turn auto-update on: ${firstLine(result2.stdout || result2.stderr) || `exit ${result2.code}`}`);
|
|
1446
1612
|
}
|
|
1447
1613
|
return;
|
|
1448
1614
|
}
|
|
@@ -1455,7 +1621,7 @@ function enableSchedule(config, command2, options = {}) {
|
|
|
1455
1621
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
1456
1622
|
const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
|
|
1457
1623
|
if (result2.code !== 0) {
|
|
1458
|
-
throw new Error(`
|
|
1624
|
+
throw new Error(`Could not turn auto-update on: ${firstLine(result2.stderr || result2.stdout) || `exit ${result2.code}`}`);
|
|
1459
1625
|
}
|
|
1460
1626
|
return;
|
|
1461
1627
|
}
|
|
@@ -1466,23 +1632,24 @@ function enableSchedule(config, command2, options = {}) {
|
|
|
1466
1632
|
writeFileSync6(join5(dir, `${label}.timer`), linuxTimer(label));
|
|
1467
1633
|
const reload = exec("systemctl", ["--user", "daemon-reload"]);
|
|
1468
1634
|
if (reload.code !== 0) {
|
|
1469
|
-
throw new Error(`
|
|
1635
|
+
throw new Error(`Could not turn auto-update on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
|
|
1470
1636
|
}
|
|
1471
1637
|
const result = exec("systemctl", ["--user", "enable", "--now", `${label}.timer`]);
|
|
1472
1638
|
if (result.code !== 0) {
|
|
1473
|
-
throw new Error(`
|
|
1639
|
+
throw new Error(`Could not turn auto-update on: ${firstLine(result.stderr || result.stdout) || `exit ${result.code}`}`);
|
|
1474
1640
|
}
|
|
1475
1641
|
}
|
|
1476
1642
|
function disableSchedule(config, options = {}) {
|
|
1477
1643
|
const platform = schedulePlatform(options.platform);
|
|
1478
|
-
if (!platform) throw new Error(`
|
|
1644
|
+
if (!platform) throw new Error(`Auto-update is not supported on ${options.platform ?? process.platform}.`);
|
|
1479
1645
|
const exec = options.exec ?? defaultExec;
|
|
1480
1646
|
const home = homeOf(options);
|
|
1481
1647
|
if (platform === "win32") {
|
|
1482
1648
|
const result = exec("schtasks", ["/Delete", "/TN", scheduleName(config), "/F"]);
|
|
1483
1649
|
if (result.code !== 0 && !/cannot find|does not exist/i.test(`${result.stdout} ${result.stderr}`)) {
|
|
1484
|
-
throw new Error(`
|
|
1650
|
+
throw new Error(`Could not turn auto-update off: ${firstLine(result.stdout || result.stderr) || `exit ${result.code}`}`);
|
|
1485
1651
|
}
|
|
1652
|
+
rmSync4(join5(options.stateDir ?? defaultProductDir(config.product), HIDDEN_SCRIPT), { force: true });
|
|
1486
1653
|
return;
|
|
1487
1654
|
}
|
|
1488
1655
|
if (platform === "darwin") {
|
|
@@ -1575,6 +1742,161 @@ function firstLine(text) {
|
|
|
1575
1742
|
return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
|
|
1576
1743
|
}
|
|
1577
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
|
+
|
|
1578
1900
|
// src/login-github.ts
|
|
1579
1901
|
import { spawn } from "node:child_process";
|
|
1580
1902
|
var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
@@ -1810,7 +2132,7 @@ function page(title, body) {
|
|
|
1810
2132
|
}
|
|
1811
2133
|
|
|
1812
2134
|
// src/index.ts
|
|
1813
|
-
var LAUNCHER_VERSION = true ? "1.
|
|
2135
|
+
var LAUNCHER_VERSION = true ? "1.1.0" : readVersionFromPackage();
|
|
1814
2136
|
function defaultPrint(message) {
|
|
1815
2137
|
process.stdout.write(`${message}
|
|
1816
2138
|
`);
|
|
@@ -1819,79 +2141,192 @@ function defaultPrintErr(message) {
|
|
|
1819
2141
|
process.stderr.write(`${message}
|
|
1820
2142
|
`);
|
|
1821
2143
|
}
|
|
2144
|
+
var LAUNCHER_VERBS = /* @__PURE__ */ new Set(["install", "update", "status", "doctor", "login", "logout", "autoupdate"]);
|
|
2145
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set(["--config", "--dir"]);
|
|
2146
|
+
var SWITCHES = {
|
|
2147
|
+
"--json": "json",
|
|
2148
|
+
"--no-color": "noColor",
|
|
2149
|
+
"--dry-run": "dryRun",
|
|
2150
|
+
"--help": "help",
|
|
2151
|
+
"-h": "help",
|
|
2152
|
+
"--version": "version",
|
|
2153
|
+
"-v": "version"
|
|
2154
|
+
};
|
|
2155
|
+
function parseArgv(argv2) {
|
|
2156
|
+
const parsed = { flags: {}, args: [], rest: [] };
|
|
2157
|
+
const flag = (i2) => {
|
|
2158
|
+
const arg = argv2[i2];
|
|
2159
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
2160
|
+
const value = argv2[i2 + 1];
|
|
2161
|
+
if (value === void 0 || value.startsWith("-")) {
|
|
2162
|
+
parsed.unknownFlag ??= arg;
|
|
2163
|
+
return i2 + 1;
|
|
2164
|
+
}
|
|
2165
|
+
parsed.flags[arg === "--config" ? "config" : "dir"] = value;
|
|
2166
|
+
return i2 + 2;
|
|
2167
|
+
}
|
|
2168
|
+
const name = SWITCHES[arg];
|
|
2169
|
+
if (!name) return -1;
|
|
2170
|
+
parsed.flags[name] = true;
|
|
2171
|
+
return i2 + 1;
|
|
2172
|
+
};
|
|
2173
|
+
const skip = (i2) => {
|
|
2174
|
+
const next = flag(i2);
|
|
2175
|
+
if (next >= 0) return next;
|
|
2176
|
+
parsed.unknownFlag ??= argv2[i2];
|
|
2177
|
+
return i2 + 1;
|
|
2178
|
+
};
|
|
2179
|
+
let i = 0;
|
|
2180
|
+
while (i < argv2.length && argv2[i].startsWith("-")) {
|
|
2181
|
+
if (argv2[i] === "--run") {
|
|
2182
|
+
parsed.run = { file: argv2[i + 1], args: argv2.slice(i + 2) };
|
|
2183
|
+
return parsed;
|
|
2184
|
+
}
|
|
2185
|
+
i = skip(i);
|
|
2186
|
+
}
|
|
2187
|
+
parsed.command = argv2[i];
|
|
2188
|
+
if (parsed.command === void 0) return parsed;
|
|
2189
|
+
if (!LAUNCHER_VERBS.has(parsed.command)) {
|
|
2190
|
+
parsed.rest = argv2.slice(i + 1);
|
|
2191
|
+
return parsed;
|
|
2192
|
+
}
|
|
2193
|
+
for (i += 1; i < argv2.length; ) {
|
|
2194
|
+
if (argv2[i].startsWith("-")) i = skip(i);
|
|
2195
|
+
else parsed.args.push(argv2[i++]);
|
|
2196
|
+
}
|
|
2197
|
+
return parsed;
|
|
2198
|
+
}
|
|
2199
|
+
var NO_PRODUCT = "mmi-hub";
|
|
2200
|
+
function sentence(message) {
|
|
2201
|
+
const text = message.trim().replace(/\s+—\s+/gu, ". ");
|
|
2202
|
+
if (!text) return "Something went wrong.";
|
|
2203
|
+
return `${text[0].toUpperCase()}${text.slice(1)}${/[.!?]$/u.test(text) ? "" : "."}`;
|
|
2204
|
+
}
|
|
2205
|
+
function screensFor(product, bin, render, options) {
|
|
2206
|
+
const show = (lines, stream = "stdout") => {
|
|
2207
|
+
const sink = stream === "stderr" ? options.printErr : options.print;
|
|
2208
|
+
if (sink) for (const line of lines) sink(line);
|
|
2209
|
+
else printScreen(lines, stream);
|
|
2210
|
+
};
|
|
2211
|
+
return {
|
|
2212
|
+
product,
|
|
2213
|
+
bin,
|
|
2214
|
+
render,
|
|
2215
|
+
show,
|
|
2216
|
+
notice: (state, message, next) => show(renderNotice(product, { state, message, ...next ? { next } : {} }, render)),
|
|
2217
|
+
refuse: (message, next, code = 1) => {
|
|
2218
|
+
show(renderNotice(product, { state: "fail", message, ...next ? { next } : {} }, render), "stderr");
|
|
2219
|
+
return code;
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
1822
2223
|
async function run(rawOptions = {}) {
|
|
1823
2224
|
const print = rawOptions.print ?? defaultPrint;
|
|
1824
2225
|
const printErr = rawOptions.printErr ?? defaultPrintErr;
|
|
1825
|
-
const
|
|
1826
|
-
|
|
1827
|
-
|
|
2226
|
+
const parsed = parseArgv(rawOptions.argv ?? process.argv.slice(2));
|
|
2227
|
+
if (parsed.run) return runFile(parsed.run.file, parsed.run.args, printErr);
|
|
2228
|
+
const render = {
|
|
2229
|
+
...rawOptions.tty === void 0 ? {} : { tty: rawOptions.tty },
|
|
2230
|
+
...parsed.flags.noColor ? { color: false } : {}
|
|
2231
|
+
};
|
|
1828
2232
|
let config;
|
|
1829
2233
|
try {
|
|
1830
|
-
config = loadProductConfig({ configPath: rawOptions.configPath ??
|
|
2234
|
+
config = loadProductConfig({ configPath: rawOptions.configPath ?? parsed.flags.config });
|
|
1831
2235
|
} catch (error) {
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
const
|
|
1836
|
-
|
|
1837
|
-
if (
|
|
1838
|
-
|
|
2236
|
+
return screensFor(NO_PRODUCT, "", { ...render, color: false }, rawOptions).refuse(`The launcher cannot start: ${sentence(error.message)}`, void 0, 2);
|
|
2237
|
+
}
|
|
2238
|
+
const dir = resolveProductDir(config.product, rawOptions.dir ?? parsed.flags.dir);
|
|
2239
|
+
const ui = screensFor(faceProduct(config), config.binName, render, rawOptions);
|
|
2240
|
+
if ((parsed.command === "install" || parsed.command === "update") && !parsed.flags.version) recordSelf(dir);
|
|
2241
|
+
if (parsed.unknownFlag) return ui.refuse(`Unknown option: ${parsed.unknownFlag}.`, `${ui.bin} --help`, 2);
|
|
2242
|
+
if (parsed.flags.version) {
|
|
2243
|
+
print(renderVersion(ui.bin, readState(dir)?.version ?? "not installed", LAUNCHER_VERSION));
|
|
1839
2244
|
return 0;
|
|
1840
2245
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
2246
|
+
const command2 = parsed.command;
|
|
2247
|
+
if (parsed.flags.help || command2 === void 0) {
|
|
2248
|
+
ui.show(usage(config, dir, render));
|
|
2249
|
+
return parsed.flags.help ? 0 : 2;
|
|
1844
2250
|
}
|
|
1845
|
-
const
|
|
2251
|
+
const mutating = ["install", "login", "logout"].includes(command2) || command2 === "update" && !parsed.flags.dryRun || command2 === "autoupdate" && ["on", "off"].includes(parsed.args[0] ?? "");
|
|
1846
2252
|
let unlock;
|
|
1847
2253
|
try {
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
2254
|
+
if (mutating) {
|
|
2255
|
+
unlock = lockInstallation(dir);
|
|
2256
|
+
await recoverAcquisition(dir, async (command3, cwd, env) => {
|
|
2257
|
+
const result = (rawOptions.runEntry ?? defaultRunEntry)(command3, cwd, env);
|
|
2258
|
+
return result.ok && (result.code === void 0 || result.code === 0);
|
|
2259
|
+
}, payloadEnv(dir) ?? process.env);
|
|
2260
|
+
}
|
|
1853
2261
|
switch (command2) {
|
|
1854
2262
|
case "login":
|
|
1855
|
-
unlock();
|
|
1856
|
-
await doLogin(config, dir, rawOptions,
|
|
2263
|
+
unlock?.();
|
|
2264
|
+
await doLogin(config, dir, rawOptions, (line) => ui.notice("note", line));
|
|
2265
|
+
ui.notice("ok", `Signed in to ${identityFor(ui.product).name}.`, `${ui.bin} install`);
|
|
1857
2266
|
return 0;
|
|
1858
2267
|
case "logout":
|
|
1859
2268
|
wipeProductDir(dir);
|
|
1860
|
-
|
|
2269
|
+
ui.notice("ok", `Signed out of ${identityFor(ui.product).name} and removed the downloaded payload.`, `${ui.bin} login`);
|
|
1861
2270
|
return 0;
|
|
1862
2271
|
case "install":
|
|
1863
|
-
return await doInstall(config, dir, rawOptions, print);
|
|
2272
|
+
return await doInstall(config, dir, rawOptions, print, parsed.flags);
|
|
1864
2273
|
case "update":
|
|
1865
|
-
return await doUpdate(config, dir, rawOptions, print);
|
|
2274
|
+
return await doUpdate(config, dir, rawOptions, print, parsed.flags);
|
|
2275
|
+
case "status":
|
|
2276
|
+
return await doStatus(config, dir, rawOptions, ui, print, Boolean(parsed.flags.json));
|
|
1866
2277
|
case "doctor":
|
|
1867
|
-
return doDoctor(config, dir, rawOptions,
|
|
2278
|
+
return doDoctor(config, dir, rawOptions, ui);
|
|
1868
2279
|
case "autoupdate":
|
|
1869
|
-
return doAutoupdate(config, rawOptions,
|
|
2280
|
+
return doAutoupdate(config, dir, rawOptions, parsed.args, ui);
|
|
1870
2281
|
default:
|
|
1871
|
-
return doForward(config, dir, rawOptions, command2,
|
|
2282
|
+
return doForward(config, dir, rawOptions, command2, parsed.rest, ui, () => unlock?.());
|
|
1872
2283
|
}
|
|
1873
2284
|
} catch (error) {
|
|
1874
|
-
if (error instanceof NeedsLoginError) {
|
|
1875
|
-
|
|
1876
|
-
|
|
2285
|
+
if (error instanceof NeedsLoginError) return ui.refuse("You are signed out.", `${ui.bin} login`, 3);
|
|
2286
|
+
const message = error.message;
|
|
2287
|
+
if (/another installer is running|lock recovery is already pending/u.test(message)) {
|
|
2288
|
+
return ui.refuse("Another install or update is running.", `${ui.bin} status`);
|
|
1877
2289
|
}
|
|
1878
|
-
|
|
1879
|
-
return 1;
|
|
2290
|
+
return ui.refuse(sentence(message), `${ui.bin} ${command2}`);
|
|
1880
2291
|
} finally {
|
|
1881
2292
|
unlock?.();
|
|
1882
2293
|
}
|
|
1883
2294
|
}
|
|
1884
|
-
function
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
2295
|
+
function recordSelf(dir) {
|
|
2296
|
+
if (!isSea()) return;
|
|
2297
|
+
try {
|
|
2298
|
+
mkdirSync6(dir, { recursive: true });
|
|
2299
|
+
writeFileSync8(join7(dir, LAUNCHER_RECORD_FILE), `${JSON.stringify({ path: process.execPath, version: LAUNCHER_VERSION })}
|
|
2300
|
+
`);
|
|
2301
|
+
} catch {
|
|
2302
|
+
}
|
|
1889
2303
|
}
|
|
1890
|
-
function
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
2304
|
+
function launcherTarget(config) {
|
|
2305
|
+
return { product: config.product, bin: config.binName, key: config.publicKey, host: config.host, ...config.dl ? { dl: config.dl } : {} };
|
|
2306
|
+
}
|
|
2307
|
+
function usage(config, dir, render) {
|
|
2308
|
+
const name = config.product;
|
|
2309
|
+
return renderUsage(faceProduct(config), {
|
|
2310
|
+
bin: config.binName,
|
|
2311
|
+
version: `${readState(dir)?.version ?? "not installed"} (launcher ${LAUNCHER_VERSION})`,
|
|
2312
|
+
summary: `Sign in, install and update ${name}.`,
|
|
2313
|
+
commands: [
|
|
2314
|
+
{ usage: "install", text: `Sign in and install ${name}.` },
|
|
2315
|
+
{ usage: "update [--dry-run]", text: `Update ${name} and this launcher.` },
|
|
2316
|
+
{ usage: "status", text: `Show versions, sign-in and ${AUTO_UPDATE}.` },
|
|
2317
|
+
{ usage: "doctor", text: `Check this machine, then run the ${name} doctor.` },
|
|
2318
|
+
{ usage: "login", text: "Sign in." },
|
|
2319
|
+
{ usage: "logout", text: "Sign out and remove the downloaded payload." },
|
|
2320
|
+
{ usage: "autoupdate on|off|status", text: `Turn hourly ${AUTO_UPDATE} on or off, or show it.` },
|
|
2321
|
+
{ usage: "<verb> ...", text: `Passed to ${name} when it declares it.` }
|
|
2322
|
+
],
|
|
2323
|
+
options: [
|
|
2324
|
+
{ usage: "--json", text: "Print status as JSON." },
|
|
2325
|
+
{ usage: "--no-color", text: "Print without colour." },
|
|
2326
|
+
{ usage: "--config <path>", text: "Use this product config." },
|
|
2327
|
+
{ usage: "--dir <path>", text: "Use this folder for sign-in and downloads." }
|
|
2328
|
+
]
|
|
2329
|
+
}, render);
|
|
1895
2330
|
}
|
|
1896
2331
|
async function runFile(file, args, printErr) {
|
|
1897
2332
|
if (!file) {
|
|
@@ -1899,7 +2334,7 @@ async function runFile(file, args, printErr) {
|
|
|
1899
2334
|
return 1;
|
|
1900
2335
|
}
|
|
1901
2336
|
const abs = resolve2(process.cwd(), file);
|
|
1902
|
-
if (!
|
|
2337
|
+
if (!existsSync8(abs)) {
|
|
1903
2338
|
printErr(`cannot run ${file}: no such file.`);
|
|
1904
2339
|
return 1;
|
|
1905
2340
|
}
|
|
@@ -1955,15 +2390,14 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
|
|
|
1955
2390
|
});
|
|
1956
2391
|
return refreshed.accessToken;
|
|
1957
2392
|
}
|
|
1958
|
-
async function doLogin(config, dir, options,
|
|
2393
|
+
async function doLogin(config, dir, options, prompt, installer) {
|
|
1959
2394
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1960
2395
|
const open = options.open ?? openBrowser;
|
|
1961
2396
|
if (config.loginKind === "github") {
|
|
1962
2397
|
const tokens = await loginGithub(config, {
|
|
1963
2398
|
fetchImpl,
|
|
1964
2399
|
open,
|
|
1965
|
-
|
|
1966
|
-
...installer ? { onDeviceCode: (prompt) => installer.signIn(prompt) } : {}
|
|
2400
|
+
onDeviceCode: (grant) => installer ? installer.signIn(grant) : prompt(`To sign in, open ${grant.url} and enter the code ${grant.code}.`)
|
|
1967
2401
|
});
|
|
1968
2402
|
writeTokens(dir, {
|
|
1969
2403
|
accessToken: tokens.accessToken,
|
|
@@ -1971,7 +2405,12 @@ async function doLogin(config, dir, options, print, installer) {
|
|
|
1971
2405
|
expiresAt: Date.now() + tokens.expiresIn * 1e3
|
|
1972
2406
|
});
|
|
1973
2407
|
} else {
|
|
1974
|
-
const tokens = await loginGoogle({
|
|
2408
|
+
const tokens = await loginGoogle({
|
|
2409
|
+
host: config.host,
|
|
2410
|
+
open,
|
|
2411
|
+
fetchImpl,
|
|
2412
|
+
print: (line) => installer ? installer.relay(line) : prompt(line)
|
|
2413
|
+
});
|
|
1975
2414
|
writeTokens(dir, {
|
|
1976
2415
|
accessToken: tokens.accessToken,
|
|
1977
2416
|
refreshToken: tokens.refreshToken,
|
|
@@ -1979,20 +2418,19 @@ async function doLogin(config, dir, options, print, installer) {
|
|
|
1979
2418
|
clientId: tokens.clientId
|
|
1980
2419
|
});
|
|
1981
2420
|
}
|
|
1982
|
-
if (!installer) print(`signed in to ${config.product}.`);
|
|
1983
2421
|
}
|
|
1984
|
-
async function fetchAccessTokenOrLogin(config, dir, options,
|
|
2422
|
+
async function fetchAccessTokenOrLogin(config, dir, options, installer) {
|
|
1985
2423
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1986
2424
|
const fresh = await ensureFreshToken(config, dir, fetchImpl);
|
|
1987
2425
|
if (fresh) return fresh;
|
|
1988
|
-
await doLogin(config, dir, options,
|
|
2426
|
+
await doLogin(config, dir, options, (line) => installer.relay(line), installer);
|
|
1989
2427
|
const after = readTokens(dir);
|
|
1990
2428
|
if (!after) throw new Error("sign-in did not produce a token");
|
|
1991
2429
|
return after.accessToken;
|
|
1992
2430
|
}
|
|
1993
2431
|
function readPayloadArgv(dir, key) {
|
|
1994
2432
|
try {
|
|
1995
|
-
const parsed = JSON.parse(
|
|
2433
|
+
const parsed = JSON.parse(readFileSync8(join7(payloadDir(dir), "payload.json"), "utf8"));
|
|
1996
2434
|
const entry2 = parsed[key];
|
|
1997
2435
|
if (typeof entry2 === "string") {
|
|
1998
2436
|
const parts = entry2.trim().split(/\s+/).filter(Boolean);
|
|
@@ -2014,7 +2452,7 @@ function readPayloadRun(dir) {
|
|
|
2014
2452
|
}
|
|
2015
2453
|
function readPayloadVerbs(dir) {
|
|
2016
2454
|
try {
|
|
2017
|
-
const parsed = JSON.parse(
|
|
2455
|
+
const parsed = JSON.parse(readFileSync8(join7(payloadDir(dir), "payload.json"), "utf8"));
|
|
2018
2456
|
const verbs = parsed.verbs;
|
|
2019
2457
|
if (verbs === "*") return "*";
|
|
2020
2458
|
if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
|
|
@@ -2031,7 +2469,7 @@ function resolveEntry(entry2) {
|
|
|
2031
2469
|
function needsShell(command2) {
|
|
2032
2470
|
if (process.platform !== "win32") return false;
|
|
2033
2471
|
if (/\.(cmd|bat)$/i.test(command2)) return true;
|
|
2034
|
-
return !
|
|
2472
|
+
return !existsSync8(command2);
|
|
2035
2473
|
}
|
|
2036
2474
|
function quoteForShell(arg) {
|
|
2037
2475
|
return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
|
|
@@ -2097,8 +2535,16 @@ function faceProduct(config) {
|
|
|
2097
2535
|
function formatMb(bytes) {
|
|
2098
2536
|
return (bytes / 1048576).toFixed(1).replace(/\.0$/, "");
|
|
2099
2537
|
}
|
|
2100
|
-
async function installOrUpdate(config, dir, options, print, update) {
|
|
2538
|
+
async function installOrUpdate(config, dir, options, print, update, flags2) {
|
|
2539
|
+
let launcherFailed = false;
|
|
2540
|
+
const code = await installOrUpdateRun(config, dir, options, print, update, flags2, () => {
|
|
2541
|
+
launcherFailed = true;
|
|
2542
|
+
});
|
|
2543
|
+
return launcherFailed ? Math.max(code, 1) : code;
|
|
2544
|
+
}
|
|
2545
|
+
async function installOrUpdateRun(config, dir, options, print, update, flags2, markLauncherFailed) {
|
|
2101
2546
|
const product = faceProduct(config);
|
|
2547
|
+
const dryRun = update && Boolean(flags2.dryRun);
|
|
2102
2548
|
const installer = createInstallerRun({
|
|
2103
2549
|
product,
|
|
2104
2550
|
gate: config.host,
|
|
@@ -2108,17 +2554,55 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2108
2554
|
operation: update ? "update" : "install",
|
|
2109
2555
|
tty: options.tty,
|
|
2110
2556
|
animate: !options.print && Boolean(process.stderr.isTTY),
|
|
2557
|
+
dryRun,
|
|
2558
|
+
...flags2.noColor ? { color: false } : {},
|
|
2111
2559
|
...options.print ? { write: (text) => print(text.replace(/\n$/, "")) } : {}
|
|
2112
2560
|
});
|
|
2113
2561
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2114
2562
|
let version = readState(dir)?.version ?? "unknown";
|
|
2115
2563
|
installer.start();
|
|
2116
2564
|
try {
|
|
2565
|
+
const refresh = options.refresh ?? (isSea() ? refreshLauncher : void 0);
|
|
2566
|
+
const arm = refresh ? await refresh(launcherTarget(config), {
|
|
2567
|
+
lockHeld: true,
|
|
2568
|
+
path: process.execPath,
|
|
2569
|
+
dryRun,
|
|
2570
|
+
productDir: dir,
|
|
2571
|
+
fetch: fetchImpl,
|
|
2572
|
+
readVersion: async () => LAUNCHER_VERSION
|
|
2573
|
+
}) : null;
|
|
2574
|
+
if (arm) {
|
|
2575
|
+
installer.surface({
|
|
2576
|
+
id: LAUNCHER_SURFACE,
|
|
2577
|
+
from: arm.from,
|
|
2578
|
+
to: arm.to,
|
|
2579
|
+
state: armState(arm),
|
|
2580
|
+
...arm.durationMs === void 0 ? {} : { seconds: arm.durationMs / 1e3 },
|
|
2581
|
+
...arm.verdict !== "ok" && arm.detail ? { detail: arm.detail } : {}
|
|
2582
|
+
});
|
|
2583
|
+
if (arm.verdict === "fail") {
|
|
2584
|
+
const finish = installer.finish;
|
|
2585
|
+
installer.finish = (facts) => finish({
|
|
2586
|
+
...facts,
|
|
2587
|
+
operationFailed: true,
|
|
2588
|
+
detail: facts.detail ?? "The launcher could not update itself.",
|
|
2589
|
+
retry: facts.retry ?? `${config.binName} update`
|
|
2590
|
+
});
|
|
2591
|
+
markLauncherFailed();
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2117
2594
|
const current = readState(dir);
|
|
2118
|
-
const payloadPresent =
|
|
2595
|
+
const payloadPresent = existsSync8(payloadDir(dir));
|
|
2119
2596
|
let started = Date.now();
|
|
2120
2597
|
installer.phase("resolve", { state: "running" });
|
|
2121
2598
|
const pointer = await fetchReleaseVersion(config, fetchImpl);
|
|
2599
|
+
if (dryRun) {
|
|
2600
|
+
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
2601
|
+
const installed = payloadPresent ? current?.version : void 0;
|
|
2602
|
+
if (pointer) installer.surface({ id: config.product, from: installed, to: pointer, state: installed === pointer ? "current" : "updated" });
|
|
2603
|
+
installer.finish({ ...pointer ? { version: pointer } : {}, total: 1, updated: pointer && installed !== pointer ? 1 : 0, failed: 0, dryRun: true });
|
|
2604
|
+
return 0;
|
|
2605
|
+
}
|
|
2122
2606
|
if (pointer && current?.version === pointer && payloadPresent) {
|
|
2123
2607
|
version = pointer;
|
|
2124
2608
|
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
@@ -2135,14 +2619,14 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2135
2619
|
}
|
|
2136
2620
|
installer.phase("sign-in", { state: "running" });
|
|
2137
2621
|
started = Date.now();
|
|
2138
|
-
const accessToken = await fetchAccessTokenOrLogin(config, dir, options,
|
|
2622
|
+
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, installer);
|
|
2139
2623
|
installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
|
|
2140
2624
|
installer.phase("resolve", { state: "running" });
|
|
2141
2625
|
started = Date.now();
|
|
2142
2626
|
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
2143
2627
|
version = manifest.version;
|
|
2144
2628
|
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
2145
|
-
const unchanged = current?.version === version &&
|
|
2629
|
+
const unchanged = current?.version === version && existsSync8(payloadDir(dir));
|
|
2146
2630
|
if (unchanged && current.acquired) {
|
|
2147
2631
|
installer.surface({ id: config.product, from: version, to: version, state: "current" });
|
|
2148
2632
|
return await finishLastMile(config, dir, options, installer, version, true, acquisitionRepairCommand(dir, version));
|
|
@@ -2152,14 +2636,14 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2152
2636
|
started = Date.now();
|
|
2153
2637
|
const cached = reusableCandidate(dir, manifest);
|
|
2154
2638
|
const candidate = cached ?? randomUUID4();
|
|
2155
|
-
const candidateDir =
|
|
2639
|
+
const candidateDir = join7(dir, "candidates", candidate);
|
|
2156
2640
|
const onProgress = ({ done, total }) => installer.phase("download", {
|
|
2157
2641
|
state: "running",
|
|
2158
2642
|
measure: total ? `${Math.floor(done / total * 100)}% \xB7 ${formatMb(done)} of ${formatMb(total)} MB` : `${formatMb(done)} MB`,
|
|
2159
2643
|
...total ? { runningTitle: `Downloading ${config.product} ${version} (${formatMb(total)} MB)` } : {}
|
|
2160
2644
|
});
|
|
2161
2645
|
if (!cached) await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl, onProgress, cacheDir: dir });
|
|
2162
|
-
const acquisition =
|
|
2646
|
+
const acquisition = existsSync8(join7(candidateDir, "payload", "payload.json")) ? readAcquisition(join7(candidateDir, "payload")) : null;
|
|
2163
2647
|
if (acquisition) {
|
|
2164
2648
|
rememberCandidate(dir, candidate, manifest);
|
|
2165
2649
|
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
@@ -2182,10 +2666,10 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2182
2666
|
installer.finish(outcome ?? { version, total: 1, updated: 1, failed: 0, installed: true });
|
|
2183
2667
|
return 0;
|
|
2184
2668
|
}
|
|
2185
|
-
const target =
|
|
2669
|
+
const target = join7(dir, "payload");
|
|
2186
2670
|
mkdirSync6(dir, { recursive: true });
|
|
2187
|
-
|
|
2188
|
-
|
|
2671
|
+
rmSync6(target, { recursive: true, force: true });
|
|
2672
|
+
renameSync5(join7(candidateDir, "payload"), target);
|
|
2189
2673
|
writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2190
2674
|
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
2191
2675
|
}
|
|
@@ -2211,11 +2695,11 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
2211
2695
|
installer.stop();
|
|
2212
2696
|
}
|
|
2213
2697
|
}
|
|
2214
|
-
function doInstall(config, dir, options, print) {
|
|
2215
|
-
return installOrUpdate(config, dir, options, print, false);
|
|
2698
|
+
function doInstall(config, dir, options, print, flags2) {
|
|
2699
|
+
return installOrUpdate(config, dir, options, print, false, flags2);
|
|
2216
2700
|
}
|
|
2217
|
-
function doUpdate(config, dir, options, print) {
|
|
2218
|
-
return installOrUpdate(config, dir, options, print, true);
|
|
2701
|
+
function doUpdate(config, dir, options, print, flags2) {
|
|
2702
|
+
return installOrUpdate(config, dir, options, print, true, flags2);
|
|
2219
2703
|
}
|
|
2220
2704
|
function payloadEnv(dir) {
|
|
2221
2705
|
const stored = readTokens(dir);
|
|
@@ -2227,8 +2711,8 @@ function payloadFileAsCommand(entry2, payload) {
|
|
|
2227
2711
|
const first = entry2[0];
|
|
2228
2712
|
if (!first || first === "$self") return null;
|
|
2229
2713
|
if (first.includes("/") || first.includes("\\")) return null;
|
|
2230
|
-
const candidate =
|
|
2231
|
-
if (!
|
|
2714
|
+
const candidate = join7(payload, first);
|
|
2715
|
+
if (!existsSync8(candidate)) return null;
|
|
2232
2716
|
if (NEVER_A_PROGRAM.test(first)) return first;
|
|
2233
2717
|
if (process.platform === "win32") return null;
|
|
2234
2718
|
try {
|
|
@@ -2247,7 +2731,7 @@ async function finishLastMile(config, dir, options, installer, version, unchange
|
|
|
2247
2731
|
updated: unchanged ? 0 : 1,
|
|
2248
2732
|
failed: 0,
|
|
2249
2733
|
installed: true,
|
|
2250
|
-
detail: `next step: run ${
|
|
2734
|
+
detail: `next step: run ${join7(payload, config.binName)} to start ${config.product}.`
|
|
2251
2735
|
});
|
|
2252
2736
|
return 0;
|
|
2253
2737
|
}
|
|
@@ -2293,9 +2777,9 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2293
2777
|
const [command2, ...args] = entry2;
|
|
2294
2778
|
const shell = needsShell(command2);
|
|
2295
2779
|
const progress = !(process.platform === "win32" && shell);
|
|
2296
|
-
const outcomeDir = mkdtempSync2(
|
|
2297
|
-
const outcomeFile =
|
|
2298
|
-
const progressFile =
|
|
2780
|
+
const outcomeDir = mkdtempSync2(join7(tmpdir4(), "mm-installer-outcome-"));
|
|
2781
|
+
const outcomeFile = join7(outcomeDir, "outcome.json");
|
|
2782
|
+
const progressFile = join7(outcomeDir, "progress.jsonl");
|
|
2299
2783
|
const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
|
|
2300
2784
|
delete childEnv.MM_FACE_TRANSCRIPT;
|
|
2301
2785
|
delete childEnv.MM_PROGRESS_FILE;
|
|
@@ -2345,8 +2829,8 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2345
2829
|
const pollProgress = () => {
|
|
2346
2830
|
if (progressError) return;
|
|
2347
2831
|
try {
|
|
2348
|
-
if (!
|
|
2349
|
-
const bytes =
|
|
2832
|
+
if (!existsSync8(progressFile)) return;
|
|
2833
|
+
const bytes = readFileSync8(progressFile);
|
|
2350
2834
|
receive(progressDecoder.write(bytes.subarray(progressOffset)));
|
|
2351
2835
|
progressOffset = bytes.length;
|
|
2352
2836
|
} catch {
|
|
@@ -2379,109 +2863,146 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2379
2863
|
return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
|
|
2380
2864
|
}
|
|
2381
2865
|
} finally {
|
|
2382
|
-
if (
|
|
2383
|
-
if (
|
|
2866
|
+
if (existsSync8(progressFile)) unlinkSync2(progressFile);
|
|
2867
|
+
if (existsSync8(outcomeFile)) unlinkSync2(outcomeFile);
|
|
2384
2868
|
rmdirSync(outcomeDir);
|
|
2385
2869
|
}
|
|
2386
2870
|
}
|
|
2387
|
-
function
|
|
2388
|
-
if (
|
|
2389
|
-
|
|
2390
|
-
|
|
2871
|
+
function armState(arm) {
|
|
2872
|
+
if (arm.verdict === "fail") return "failed";
|
|
2873
|
+
if (arm.verdict === "defer") return "retry";
|
|
2874
|
+
if (arm.verdict === "skip") return "kept";
|
|
2875
|
+
return arm.from === arm.to ? "current" : "updated";
|
|
2876
|
+
}
|
|
2877
|
+
function doForward(config, dir, options, command2, rest, ui, unlock) {
|
|
2878
|
+
if (existsSync8(command2)) {
|
|
2879
|
+
return ui.refuse(`${command2} is a file, not a command.`, `${ui.bin} --run ${command2}`, 2);
|
|
2391
2880
|
}
|
|
2392
2881
|
const acquired = readState(dir)?.acquired;
|
|
2393
2882
|
const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
|
|
2394
2883
|
const verbs = readPayloadVerbs(dir);
|
|
2395
|
-
const declared = verbs === "*" || Array.isArray(verbs) &&
|
|
2396
|
-
if (!target || !declared) {
|
|
2397
|
-
|
|
2398
|
-
printUsage(config, print);
|
|
2399
|
-
return 2;
|
|
2400
|
-
}
|
|
2401
|
-
const forwarded = [...resolveEntry(target), ...argv2];
|
|
2884
|
+
const declared = verbs === "*" || Array.isArray(verbs) && verbs.includes(command2);
|
|
2885
|
+
if (!target || !declared) return ui.refuse(`Unknown command: ${command2}.`, `${ui.bin} --help`, 2);
|
|
2886
|
+
const forwarded = [...resolveEntry(target), command2, ...rest];
|
|
2402
2887
|
const cwd = payloadDir(dir);
|
|
2403
2888
|
const inherited = payloadEnv(dir);
|
|
2404
2889
|
const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
|
|
2405
2890
|
unlock();
|
|
2406
2891
|
const result = (options.runEntry ?? defaultRunEntry)(forwarded, cwd, env);
|
|
2407
2892
|
if (!result.ok && result.code === void 0) {
|
|
2408
|
-
|
|
2409
|
-
return 1;
|
|
2893
|
+
return ui.refuse(`Could not start ${config.product}: ${sentence(result.error ?? `could not run ${forwarded[0]}`)}`, `${ui.bin} doctor`);
|
|
2410
2894
|
}
|
|
2411
2895
|
return result.code ?? 0;
|
|
2412
2896
|
}
|
|
2413
|
-
function doAutoupdate(config, options, args,
|
|
2897
|
+
function doAutoupdate(config, dir, options, args, ui) {
|
|
2414
2898
|
const mode = args[0] ?? "status";
|
|
2415
|
-
const scheduleOptions = options.autoupdate
|
|
2899
|
+
const scheduleOptions = { stateDir: dir, ...options.autoupdate };
|
|
2416
2900
|
const command2 = options.autoupdate?.command ?? [process.execPath, "update"];
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
enableSchedule(config, command2, scheduleOptions);
|
|
2420
|
-
} catch (error) {
|
|
2421
|
-
print(error.message);
|
|
2422
|
-
return 1;
|
|
2423
|
-
}
|
|
2424
|
-
const state2 = querySchedule(config, scheduleOptions);
|
|
2425
|
-
print(`auto-update: on (${state2.cadence ?? "hourly"})`);
|
|
2426
|
-
return 0;
|
|
2427
|
-
}
|
|
2428
|
-
if (mode === "off") {
|
|
2901
|
+
const name = identityFor(ui.product).name;
|
|
2902
|
+
if (mode === "on" || mode === "off") {
|
|
2429
2903
|
try {
|
|
2430
|
-
|
|
2904
|
+
if (mode === "on") enableSchedule(config, command2, scheduleOptions);
|
|
2905
|
+
else disableSchedule(config, scheduleOptions);
|
|
2431
2906
|
} catch (error) {
|
|
2432
|
-
|
|
2433
|
-
return 1;
|
|
2907
|
+
return ui.refuse(sentence(error.message), `${ui.bin} doctor`);
|
|
2434
2908
|
}
|
|
2435
|
-
|
|
2909
|
+
if (mode === "on") ui.notice("ok", `Auto-update is on. ${name} checks for updates every hour.`, `${ui.bin} autoupdate status`);
|
|
2910
|
+
else ui.notice("ok", "Auto-update is off.", `${ui.bin} autoupdate on`);
|
|
2436
2911
|
return 0;
|
|
2437
2912
|
}
|
|
2438
|
-
if (mode !== "status") {
|
|
2439
|
-
print(`unknown autoupdate mode: ${mode} \u2014 use on, off or status.`);
|
|
2440
|
-
return 1;
|
|
2441
|
-
}
|
|
2913
|
+
if (mode !== "status") return ui.refuse(`Unknown autoupdate mode: ${mode}.`, `${ui.bin} autoupdate status`, 2);
|
|
2442
2914
|
const state = querySchedule(config, scheduleOptions);
|
|
2443
|
-
if (!state.supported) {
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
}
|
|
2447
|
-
print(`auto-update: ${state.enabled ? `on (${state.cadence ?? "hourly"})` : "off"}`);
|
|
2448
|
-
if (state.lastRun) print(`last run: ${state.lastRun}`);
|
|
2915
|
+
if (!state.supported) return ui.refuse("Auto-update is not supported on this platform.", `${ui.bin} update`);
|
|
2916
|
+
if (state.enabled) ui.notice("ok", `Auto-update is on and runs every hour.${state.lastRun ? ` Last run: ${state.lastRun}.` : ""}`);
|
|
2917
|
+
else ui.notice("note", "Auto-update is off.", `${ui.bin} autoupdate on`);
|
|
2449
2918
|
return 0;
|
|
2450
2919
|
}
|
|
2451
|
-
function
|
|
2452
|
-
|
|
2920
|
+
function localFacts(config, dir, options) {
|
|
2921
|
+
const state = readState(dir);
|
|
2922
|
+
const tokens = readTokens(dir);
|
|
2923
|
+
const expired = Boolean(tokens && tokens.expiresAt <= Date.now());
|
|
2924
|
+
return {
|
|
2925
|
+
installed: state && existsSync8(payloadDir(dir)) ? state.version : null,
|
|
2926
|
+
// An expired access token with a refresh token renews on the next use; it is still signed in.
|
|
2927
|
+
signedIn: Boolean(tokens && (!expired || tokens.refreshToken)),
|
|
2928
|
+
expired,
|
|
2929
|
+
schedule: querySchedule(config, { stateDir: dir, ...options.autoupdate })
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2932
|
+
function signInRow(local) {
|
|
2933
|
+
if (local.signedIn) return { surface: "sign-in", state: local.expired ? "signed in \xB7 renews on next use" : "signed in", mark: "ok" };
|
|
2934
|
+
return { surface: "sign-in", state: local.expired ? "expired" : "signed out", mark: "fail" };
|
|
2935
|
+
}
|
|
2936
|
+
function autoUpdateRow({ supported, enabled, cadence, lastRun }) {
|
|
2937
|
+
if (!supported) return { surface: AUTO_UPDATE, state: "unsupported on this platform", mark: "note" };
|
|
2938
|
+
if (!enabled) return { surface: AUTO_UPDATE, state: "off", mark: "note" };
|
|
2939
|
+
return { surface: AUTO_UPDATE, state: `on \xB7 ${cadence ?? "hourly"}`, mark: "ok", ...lastRun ? { note: `Last run: ${lastRun}` } : {} };
|
|
2940
|
+
}
|
|
2941
|
+
function verdictFor(ui, local, updateAvailable, ready) {
|
|
2942
|
+
if (!local.installed) return { state: "fail", headline: "is not installed.", next: `${ui.bin} install` };
|
|
2943
|
+
if (!local.signedIn) return { state: "fail", headline: "is signed out.", next: `${ui.bin} login` };
|
|
2944
|
+
if (updateAvailable) return { state: "note", headline: "has an update available.", next: `${ui.bin} update` };
|
|
2945
|
+
return { state: "ok", headline: "is ready.", next: ready };
|
|
2946
|
+
}
|
|
2947
|
+
function within(fetchImpl, ms) {
|
|
2948
|
+
return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(ms) }));
|
|
2949
|
+
}
|
|
2950
|
+
var STATUS_PROBE_MS = 3e3;
|
|
2951
|
+
async function doStatus(config, dir, options, ui, print, json) {
|
|
2952
|
+
const fetchImpl = within(options.fetchImpl ?? fetch, STATUS_PROBE_MS);
|
|
2953
|
+
const local = localFacts(config, dir, options);
|
|
2954
|
+
const [release, record] = await Promise.all([
|
|
2955
|
+
fetchReleaseVersion(config, fetchImpl),
|
|
2956
|
+
readLauncherRecord(launcherTarget(config), { fetch: fetchImpl, timeoutMs: STATUS_PROBE_MS })
|
|
2957
|
+
]);
|
|
2958
|
+
const launcherNewer = record !== null && compareVersions(record.version, LAUNCHER_VERSION) > 0;
|
|
2959
|
+
const releaseOrder = local.installed && release ? compareVersions(release, local.installed) : null;
|
|
2960
|
+
const verdict = verdictFor(ui, local, launcherNewer || (releaseOrder ?? 0) > 0, `${ui.bin} doctor`);
|
|
2961
|
+
if (json) {
|
|
2962
|
+
print(JSON.stringify({
|
|
2963
|
+
product: config.product,
|
|
2964
|
+
launcher: { version: LAUNCHER_VERSION, available: launcherNewer ? record.version : null },
|
|
2965
|
+
installed: local.installed,
|
|
2966
|
+
release,
|
|
2967
|
+
signIn: local.signedIn ? local.expired ? "renews" : "signed-in" : local.expired ? "expired" : "signed-out",
|
|
2968
|
+
autoupdate: { supported: local.schedule.supported, enabled: local.schedule.enabled, lastRun: local.schedule.lastRun ?? null },
|
|
2969
|
+
verdict: verdict.state,
|
|
2970
|
+
next: verdict.next
|
|
2971
|
+
}, null, 2));
|
|
2972
|
+
return verdict.state === "fail" ? 1 : 0;
|
|
2973
|
+
}
|
|
2974
|
+
const launcherRow = record === null ? { surface: "launcher", state: `${LAUNCHER_VERSION} \xB7 ${SURFACE_WORDS.unknown}`, mark: "note" } : launcherNewer ? { surface: "launcher", state: `${LAUNCHER_VERSION} \u2192 ${record.version} \xB7 ${SURFACE_WORDS.behind}`, mark: "note" } : { surface: "launcher", state: `${LAUNCHER_VERSION} \xB7 ${SURFACE_WORDS.current}`, mark: "ok" };
|
|
2975
|
+
const productRow = !local.installed ? { surface: config.product, state: SURFACE_WORDS.missing, mark: "fail" } : releaseOrder === null ? { surface: config.product, state: `${local.installed} \xB7 ${SURFACE_WORDS.unknown}`, mark: "note" } : releaseOrder > 0 ? { surface: config.product, state: `${local.installed} \u2192 ${release} \xB7 ${SURFACE_WORDS.behind}`, mark: "note" } : releaseOrder < 0 ? { surface: config.product, state: `${local.installed} \xB7 ${SURFACE_WORDS.ahead}`, mark: "note" } : { surface: config.product, state: `${local.installed} \xB7 ${SURFACE_WORDS.current}`, mark: "ok" };
|
|
2976
|
+
ui.show(renderReport(ui.product, {
|
|
2977
|
+
rows: [launcherRow, productRow, signInRow(local), autoUpdateRow(local.schedule)],
|
|
2978
|
+
verdict: { ...verdict, facts: [release ? `The release is ${release}.` : "The release version could not be read."] }
|
|
2979
|
+
}, ui.render));
|
|
2980
|
+
return verdict.state === "fail" ? 1 : 0;
|
|
2981
|
+
}
|
|
2982
|
+
function doDoctor(config, dir, options, ui) {
|
|
2983
|
+
const local = localFacts(config, dir, options);
|
|
2984
|
+
const verdict = verdictFor(ui, local, false, `${ui.bin} status`);
|
|
2985
|
+
ui.show(renderReport(ui.product, {
|
|
2986
|
+
rows: [
|
|
2987
|
+
{ surface: "launcher", state: LAUNCHER_VERSION, mark: "ok" },
|
|
2988
|
+
local.installed ? { surface: config.product, state: local.installed, mark: "ok" } : { surface: config.product, state: SURFACE_WORDS.missing, mark: "fail" },
|
|
2989
|
+
signInRow(local),
|
|
2990
|
+
autoUpdateRow(local.schedule)
|
|
2991
|
+
],
|
|
2992
|
+
// Paths belong in the receipt, never a row.
|
|
2993
|
+
verdict: { ...verdict, facts: [`Folder: ${dir}`, `Release host: ${config.host}`] }
|
|
2994
|
+
}, ui.render));
|
|
2995
|
+
const own = verdict.state === "fail" ? 1 : 0;
|
|
2453
2996
|
const acquired = readState(dir)?.acquired;
|
|
2454
2997
|
const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
|
|
2455
2998
|
const verbs = readPayloadVerbs(dir);
|
|
2456
2999
|
const chains = target !== null && (verbs === "*" || Array.isArray(verbs) && verbs.includes("doctor"));
|
|
2457
|
-
if (!chains) return
|
|
3000
|
+
if (!chains) return own;
|
|
2458
3001
|
const cwd = payloadDir(dir);
|
|
2459
3002
|
const inherited = payloadEnv(dir);
|
|
2460
3003
|
const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
|
|
2461
|
-
unlock();
|
|
2462
3004
|
const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], cwd, env);
|
|
2463
|
-
return result.code ?? (result.ok ? 0 : 1);
|
|
2464
|
-
}
|
|
2465
|
-
function doLauncherDoctor(config, dir, options, print) {
|
|
2466
|
-
const tokens = readTokens(dir);
|
|
2467
|
-
const state = readState(dir);
|
|
2468
|
-
const payloadPresent = existsSync7(payloadDir(dir));
|
|
2469
|
-
print(`product: ${config.product}`);
|
|
2470
|
-
print(`host: ${config.host}`);
|
|
2471
|
-
print(`login: ${config.loginKind}`);
|
|
2472
|
-
print(`dir: ${dir}`);
|
|
2473
|
-
if (tokens) {
|
|
2474
|
-
const remaining = Math.max(0, Math.round((tokens.expiresAt - Date.now()) / 1e3));
|
|
2475
|
-
print(`token: present (expires in ${remaining}s)`);
|
|
2476
|
-
} else {
|
|
2477
|
-
print("token: none \u2014 run login first.");
|
|
2478
|
-
}
|
|
2479
|
-
print(`payload: ${state ? state.version : "none"}${payloadPresent ? "" : " (not downloaded)"}`);
|
|
2480
|
-
print(`paths: tokens ${join6(dir, "tokens.json")}, state ${join6(dir, "state.json")}, payload ${payloadDir(dir)}`);
|
|
2481
|
-
const schedule = querySchedule(config, options.autoupdate ?? {});
|
|
2482
|
-
print(
|
|
2483
|
-
`auto-update: ${!schedule.supported ? "unsupported on this platform" : schedule.enabled ? `on (${schedule.cadence ?? "hourly"})` : "off"}`
|
|
2484
|
-
);
|
|
3005
|
+
return (result.code ?? (result.ok ? 0 : 1)) || own;
|
|
2485
3006
|
}
|
|
2486
3007
|
var invokedAsMain = typeof process.argv[1] === "string" && (() => {
|
|
2487
3008
|
try {
|
|
@@ -2500,8 +3021,7 @@ if (invokedAsMain) {
|
|
|
2500
3021
|
process.exitCode = code;
|
|
2501
3022
|
},
|
|
2502
3023
|
(error) => {
|
|
2503
|
-
|
|
2504
|
-
`);
|
|
3024
|
+
printScreen(renderNotice(NO_PRODUCT, { state: "fail", message: sentence(error.message) }, { color: false }), "stderr");
|
|
2505
3025
|
process.exitCode = 1;
|
|
2506
3026
|
}
|
|
2507
3027
|
);
|
|
@@ -2511,6 +3031,7 @@ export {
|
|
|
2511
3031
|
defaultRunEntry,
|
|
2512
3032
|
ensureFreshToken,
|
|
2513
3033
|
formatMb,
|
|
3034
|
+
parseArgv,
|
|
2514
3035
|
readPayloadEntry,
|
|
2515
3036
|
readPayloadRun,
|
|
2516
3037
|
readPayloadVerbs,
|