@mutmutco/installer-launcher 1.2.5 → 1.3.1
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 +2 -1
- package/dist/launcher.js +116 -35
- package/dist/launcher.sea.cjs +99 -18
- package/dist/refresh.js +51 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ launcher status [--json] read-only: launcher, payload
|
|
|
21
21
|
launcher doctor launcher report (no network), then the payload's doctor
|
|
22
22
|
launcher autoupdate on|off|status hourly per-user schedule (hidden via wscript on Windows)
|
|
23
23
|
launcher <verb> [args…] forwarded untouched when payload.json declares it
|
|
24
|
+
launcher [-p …|--model …] no verb: starts the product's session; leading flags the launcher does not own pass through
|
|
24
25
|
launcher --run <file> [args…] run a payload ESM file with the launcher's own runtime
|
|
25
26
|
launcher --version `<bin> <payload version> (launcher <version>)`
|
|
26
27
|
```
|
|
@@ -244,7 +245,7 @@ atomically only after convergence succeeds. On failure or interrupted startup it
|
|
|
244
245
|
consumer rollback before allowing another install or forwarding. Failed rollback stays pending.
|
|
245
246
|
A crash after shared commit completes the receipt instead of reverting the committed product.
|
|
246
247
|
Host registration is not atomic: failures can leave host references to retained candidates.
|
|
247
|
-
|
|
248
|
+
After a committed promotion the launcher removes candidate folders `state.json` no longer references (the active one is kept; symlinks and junctions are skipped; a failed delete is logged). A failed activation keeps its candidate.
|
|
248
249
|
An orphan lock-recovery guard fails closed and needs operator diagnosis.
|
|
249
250
|
|
|
250
251
|
For already-shipped old launchers, copy the exact file exported by
|
package/dist/launcher.js
CHANGED
|
@@ -6,7 +6,7 @@ import { randomUUID as randomUUID4 } from "node:crypto";
|
|
|
6
6
|
|
|
7
7
|
// src/acquisition.ts
|
|
8
8
|
import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
|
|
9
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync, realpathSync as realpathSync2 } from "node:fs";
|
|
9
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync, realpathSync as realpathSync2, readdirSync, lstatSync, rmSync as rmSync4 } from "node:fs";
|
|
10
10
|
import { join as join4, resolve, relative as relative2, isAbsolute as isAbsolute2, dirname as dirname2 } from "node:path";
|
|
11
11
|
|
|
12
12
|
// src/runtime.ts
|
|
@@ -617,6 +617,28 @@ async function recoverAcquisition(dir, run2, env) {
|
|
|
617
617
|
else if (existsSync5(statePath(dir))) unlinkSync(statePath(dir));
|
|
618
618
|
unlinkSync(pendingPath(dir));
|
|
619
619
|
}
|
|
620
|
+
function reapCandidates(dir) {
|
|
621
|
+
const keep = readState(dir)?.acquired?.candidate;
|
|
622
|
+
if (!keep) return;
|
|
623
|
+
const base = join4(dir, "candidates");
|
|
624
|
+
let names;
|
|
625
|
+
try {
|
|
626
|
+
names = readdirSync(base);
|
|
627
|
+
} catch {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
for (const name of names) {
|
|
631
|
+
if (name === keep || !/^[a-zA-Z0-9_-]+$/.test(name)) continue;
|
|
632
|
+
try {
|
|
633
|
+
const path = join4(base, name);
|
|
634
|
+
const info = lstatSync(path);
|
|
635
|
+
if (!info.isDirectory() || info.isSymbolicLink()) continue;
|
|
636
|
+
rmSync4(path, { recursive: true, force: true });
|
|
637
|
+
} catch (error) {
|
|
638
|
+
console.error(`launcher: could not remove old candidate ${name}: ${error.message}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
620
642
|
async function installAcquisition(dir, candidate, version, acquisition, options) {
|
|
621
643
|
if (!acquisition.platforms.includes(`${process.platform}-${process.arch}`)) throw new Error("this product does not support this platform");
|
|
622
644
|
const root = candidateRoot(dir, candidate);
|
|
@@ -673,13 +695,14 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
|
|
|
673
695
|
throw error;
|
|
674
696
|
}
|
|
675
697
|
unlinkSync(pendingPath(dir));
|
|
698
|
+
reapCandidates(dir);
|
|
676
699
|
}
|
|
677
700
|
|
|
678
701
|
// src/index.ts
|
|
679
702
|
import { spawn as spawn2, spawnSync as spawnSync4 } from "node:child_process";
|
|
680
|
-
import { existsSync as existsSync8, readFileSync as readFileSync8, statSync, mkdtempSync as mkdtempSync2, unlinkSync as unlinkSync2, rmdirSync, mkdirSync as mkdirSync6, renameSync as renameSync5, rmSync as
|
|
703
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8, statSync as statSync2, mkdtempSync as mkdtempSync2, unlinkSync as unlinkSync2, rmdirSync, mkdirSync as mkdirSync6, renameSync as renameSync5, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
681
704
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
682
|
-
import { dirname as dirname4, join as join7, resolve as
|
|
705
|
+
import { dirname as dirname4, join as join7, resolve as resolve3 } from "node:path";
|
|
683
706
|
import { isSea } from "node:sea";
|
|
684
707
|
import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
|
|
685
708
|
|
|
@@ -1554,7 +1577,7 @@ var rowTitle = (row) => `${row.surface}${row.aspect ? ` ${row.aspect}` : ""}${ro
|
|
|
1554
1577
|
|
|
1555
1578
|
// src/autoupdate.ts
|
|
1556
1579
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1557
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as
|
|
1580
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1558
1581
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
1559
1582
|
import { join as join5 } from "node:path";
|
|
1560
1583
|
function schedulePlatform(override) {
|
|
@@ -1649,20 +1672,20 @@ function disableSchedule(config, options = {}) {
|
|
|
1649
1672
|
if (result.code !== 0 && !/cannot find|does not exist/i.test(`${result.stdout} ${result.stderr}`)) {
|
|
1650
1673
|
throw new Error(`Could not turn auto-update off: ${firstLine(result.stdout || result.stderr) || `exit ${result.code}`}`);
|
|
1651
1674
|
}
|
|
1652
|
-
|
|
1675
|
+
rmSync5(join5(options.stateDir ?? defaultProductDir(config.product), HIDDEN_SCRIPT), { force: true });
|
|
1653
1676
|
return;
|
|
1654
1677
|
}
|
|
1655
1678
|
if (platform === "darwin") {
|
|
1656
1679
|
const label2 = scheduleLabel(config);
|
|
1657
1680
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
1658
|
-
|
|
1681
|
+
rmSync5(join5(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
|
|
1659
1682
|
return;
|
|
1660
1683
|
}
|
|
1661
1684
|
const label = scheduleLabel(config);
|
|
1662
1685
|
const dir = join5(home, ".config", "systemd", "user");
|
|
1663
1686
|
exec("systemctl", ["--user", "disable", "--now", `${label}.timer`]);
|
|
1664
|
-
|
|
1665
|
-
|
|
1687
|
+
rmSync5(join5(dir, `${label}.service`), { force: true });
|
|
1688
|
+
rmSync5(join5(dir, `${label}.timer`), { force: true });
|
|
1666
1689
|
}
|
|
1667
1690
|
function querySchedule(config, options = {}) {
|
|
1668
1691
|
const platform = schedulePlatform(options.platform);
|
|
@@ -1744,7 +1767,7 @@ function firstLine(text) {
|
|
|
1744
1767
|
|
|
1745
1768
|
// src/login-github.ts
|
|
1746
1769
|
import { spawn, spawnSync as spawnSync3 } from "node:child_process";
|
|
1747
|
-
var realSleep = (ms) => new Promise((
|
|
1770
|
+
var realSleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1748
1771
|
function openBrowser(url) {
|
|
1749
1772
|
if (process.env.LAUNCHER_NO_OPEN === "1") return;
|
|
1750
1773
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
|
|
@@ -1898,7 +1921,7 @@ async function loginGoogle(options) {
|
|
|
1898
1921
|
const server = options.host.replace(/\/+$/, "");
|
|
1899
1922
|
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
1900
1923
|
const listener = createServer();
|
|
1901
|
-
await new Promise((
|
|
1924
|
+
await new Promise((resolve4) => listener.listen(0, "127.0.0.1", resolve4));
|
|
1902
1925
|
const port = listener.address().port;
|
|
1903
1926
|
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
1904
1927
|
try {
|
|
@@ -1925,7 +1948,7 @@ async function loginGoogle(options) {
|
|
|
1925
1948
|
code_challenge: challenge,
|
|
1926
1949
|
code_challenge_method: "S256"
|
|
1927
1950
|
}).toString();
|
|
1928
|
-
const code = await new Promise((
|
|
1951
|
+
const code = await new Promise((resolve4, reject) => {
|
|
1929
1952
|
const timer = setTimeout(() => reject(new Error("sign-in timed out \u2014 run login again.")), timeoutMs);
|
|
1930
1953
|
listener.on("request", (req, res) => {
|
|
1931
1954
|
const url = new URL(req.url ?? "/", redirectUri);
|
|
@@ -1943,7 +1966,7 @@ async function loginGoogle(options) {
|
|
|
1943
1966
|
}
|
|
1944
1967
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(page("Signed in.", "You can close this tab and go back to the terminal."));
|
|
1945
1968
|
clearTimeout(timer);
|
|
1946
|
-
|
|
1969
|
+
resolve4(received);
|
|
1947
1970
|
});
|
|
1948
1971
|
const print = options.print ?? ((line) => process.stderr.write(`${line}
|
|
1949
1972
|
`));
|
|
@@ -2041,9 +2064,9 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
|
|
|
2041
2064
|
|
|
2042
2065
|
// src/refresh.ts
|
|
2043
2066
|
import { execFile } from "node:child_process";
|
|
2044
|
-
import { chmodSync, existsSync as existsSync7, lstatSync, readdirSync, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as
|
|
2067
|
+
import { accessSync, chmodSync, constants, existsSync as existsSync7, lstatSync as lstatSync2, readdirSync as readdirSync2, readFileSync as readFileSync7, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync6, statSync, writeFileSync as writeFileSync7 } from "node:fs";
|
|
2045
2068
|
import { homedir } from "node:os";
|
|
2046
|
-
import { basename, dirname as dirname3, join as join6 } from "node:path";
|
|
2069
|
+
import { basename, dirname as dirname3, join as join6, resolve as resolve2 } from "node:path";
|
|
2047
2070
|
var LAUNCHER_RECORD_FILE = "launcher.json";
|
|
2048
2071
|
var RECORD_TIMEOUT_MS = 1e4;
|
|
2049
2072
|
function platformKey(platform, arch) {
|
|
@@ -2102,15 +2125,54 @@ function recordedPath(productDir) {
|
|
|
2102
2125
|
function installedPath(target, productDir, platform) {
|
|
2103
2126
|
return platform === "win32" ? join6(productDir, "bin", `${target.bin}.exe`) : join6(homedir(), ".local", "bin", target.bin);
|
|
2104
2127
|
}
|
|
2128
|
+
function onPath(bin, platform, env) {
|
|
2129
|
+
const win = platform === "win32";
|
|
2130
|
+
const read = (name) => env[name] ?? env[Object.keys(env).find((key) => key.toLowerCase() === name.toLowerCase()) ?? ""];
|
|
2131
|
+
const exts = win ? ["", ...(read("PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)] : [""];
|
|
2132
|
+
const found = [];
|
|
2133
|
+
for (const dir of (read("PATH") ?? "").split(win ? ";" : ":").filter(Boolean)) {
|
|
2134
|
+
for (const ext of exts) {
|
|
2135
|
+
const candidate = join6(dir.replace(/^"|"$/gu, ""), `${bin}${ext}`);
|
|
2136
|
+
try {
|
|
2137
|
+
if (!statSync(candidate).isFile()) continue;
|
|
2138
|
+
if (!win) accessSync(candidate, constants.X_OK);
|
|
2139
|
+
found.push(candidate);
|
|
2140
|
+
} catch {
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
return found;
|
|
2145
|
+
}
|
|
2146
|
+
function findPathShadow(bin, file, options = {}) {
|
|
2147
|
+
const platform = options.platform ?? process.platform;
|
|
2148
|
+
const same = (a, b) => {
|
|
2149
|
+
const real = (path) => {
|
|
2150
|
+
try {
|
|
2151
|
+
return realpathSync3(path);
|
|
2152
|
+
} catch {
|
|
2153
|
+
return resolve2(path);
|
|
2154
|
+
}
|
|
2155
|
+
};
|
|
2156
|
+
const [x, y] = [real(a), real(b)];
|
|
2157
|
+
return platform === "win32" ? x.toLowerCase() === y.toLowerCase() : x === y;
|
|
2158
|
+
};
|
|
2159
|
+
const hits = onPath(bin, platform, options.env ?? process.env);
|
|
2160
|
+
const other = hits.find((hit) => !same(hit, file));
|
|
2161
|
+
if (!other) return null;
|
|
2162
|
+
if (hits.findIndex((hit) => same(hit, file)) === 0) {
|
|
2163
|
+
return { state: "shadowing", other, line: `Warning: ${file} shadows another ${bin} on PATH: ${other}.` };
|
|
2164
|
+
}
|
|
2165
|
+
return { state: "shadowed", other, line: `Warning: ${file} is shadowed by another ${bin} earlier on PATH: ${other}.` };
|
|
2166
|
+
}
|
|
2105
2167
|
function parseLauncherVersion(stdout, bin) {
|
|
2106
2168
|
const text = String(stdout).trim();
|
|
2107
2169
|
if (!text.startsWith(`${bin} `)) return null;
|
|
2108
2170
|
return text.match(/launcher (\d+\.\d+\.\d+[^\s)]*)/u)?.[1] ?? null;
|
|
2109
2171
|
}
|
|
2110
2172
|
function runVersion(path, bin) {
|
|
2111
|
-
return new Promise((
|
|
2173
|
+
return new Promise((resolve4) => {
|
|
2112
2174
|
execFile(path, ["--version"], { timeout: 3e4, windowsHide: true, encoding: "utf8" }, (error, stdout) => {
|
|
2113
|
-
|
|
2175
|
+
resolve4(error ? null : parseLauncherVersion(stdout, bin));
|
|
2114
2176
|
});
|
|
2115
2177
|
});
|
|
2116
2178
|
}
|
|
@@ -2118,14 +2180,14 @@ function removeStale(file) {
|
|
|
2118
2180
|
const prefix = `${basename(file)}.old-`;
|
|
2119
2181
|
let names;
|
|
2120
2182
|
try {
|
|
2121
|
-
names =
|
|
2183
|
+
names = readdirSync2(dirname3(file));
|
|
2122
2184
|
} catch {
|
|
2123
2185
|
return;
|
|
2124
2186
|
}
|
|
2125
2187
|
for (const name of names) {
|
|
2126
2188
|
if (!name.startsWith(prefix)) continue;
|
|
2127
2189
|
try {
|
|
2128
|
-
|
|
2190
|
+
rmSync6(join6(dirname3(file), name), { force: true });
|
|
2129
2191
|
} catch {
|
|
2130
2192
|
}
|
|
2131
2193
|
}
|
|
@@ -2138,11 +2200,12 @@ async function refreshLauncher(target, options = {}) {
|
|
|
2138
2200
|
const productDir = options.productDir ?? defaultProductDir(target.product);
|
|
2139
2201
|
const recorded = recordedPath(productDir);
|
|
2140
2202
|
const file = options.path ?? (recorded && existsSync7(recorded) ? recorded : installedPath(target, productDir, platform));
|
|
2141
|
-
if (!existsSync7(file) ||
|
|
2203
|
+
if (!existsSync7(file) || lstatSync2(file).isSymbolicLink()) return null;
|
|
2142
2204
|
if (!options.dryRun) removeStale(file);
|
|
2143
2205
|
const from = await (options.readVersion ?? ((path) => runVersion(path, target.bin)))(file);
|
|
2144
2206
|
if (!from) return null;
|
|
2145
|
-
const
|
|
2207
|
+
const shadow = findPathShadow(target.bin, file, { platform, ...options.env ? { env: options.env } : {} });
|
|
2208
|
+
const arm = (verdict, to, detail) => ({ surface: "launcher", from, to, verdict, detail: [detail, shadow?.line].filter(Boolean).join(" "), durationMs: Date.now() - started });
|
|
2146
2209
|
const record = await fetchRecord(target, options);
|
|
2147
2210
|
if (record === "bad-signature") return arm("fail", from, "The launcher record failed its signature check.");
|
|
2148
2211
|
const entry2 = record?.files.find((candidate) => candidate.path === key);
|
|
@@ -2186,7 +2249,7 @@ async function refreshLauncher(target, options = {}) {
|
|
|
2186
2249
|
if (moved && !existsSync7(file)) renameSync4(moved, file);
|
|
2187
2250
|
} catch {
|
|
2188
2251
|
}
|
|
2189
|
-
|
|
2252
|
+
rmSync6(next, { force: true });
|
|
2190
2253
|
return arm("fail", from, "The launcher could not be replaced.");
|
|
2191
2254
|
}
|
|
2192
2255
|
return arm("ok", record.version, "");
|
|
@@ -2196,7 +2259,7 @@ async function refreshLauncher(target, options = {}) {
|
|
|
2196
2259
|
}
|
|
2197
2260
|
|
|
2198
2261
|
// src/index.ts
|
|
2199
|
-
var LAUNCHER_VERSION = true ? "1.
|
|
2262
|
+
var LAUNCHER_VERSION = true ? "1.3.1" : readVersionFromPackage();
|
|
2200
2263
|
function defaultPrint(message) {
|
|
2201
2264
|
process.stdout.write(`${message}
|
|
2202
2265
|
`);
|
|
@@ -2246,7 +2309,13 @@ function parseArgv(argv2) {
|
|
|
2246
2309
|
parsed.run = { file: argv2[i + 1], args: argv2.slice(i + 2) };
|
|
2247
2310
|
return parsed;
|
|
2248
2311
|
}
|
|
2249
|
-
|
|
2312
|
+
const next = flag(i);
|
|
2313
|
+
if (next < 0) {
|
|
2314
|
+
parsed.unknownFlag ??= argv2[i];
|
|
2315
|
+
parsed.forward = argv2.slice(i);
|
|
2316
|
+
return parsed;
|
|
2317
|
+
}
|
|
2318
|
+
i = next;
|
|
2250
2319
|
}
|
|
2251
2320
|
parsed.command = argv2[i];
|
|
2252
2321
|
if (parsed.command === void 0) return parsed;
|
|
@@ -2302,12 +2371,19 @@ async function run(rawOptions = {}) {
|
|
|
2302
2371
|
const dir = resolveProductDir(config.product, rawOptions.dir ?? parsed.flags.dir);
|
|
2303
2372
|
const ui = screensFor(faceProduct(config), config.binName, render, rawOptions);
|
|
2304
2373
|
if ((parsed.command === "install" || parsed.command === "update") && !parsed.flags.version) recordSelf(dir);
|
|
2374
|
+
const leadingForeign = parsed.forward !== void 0 && parsed.unknownFlag === parsed.forward[0] && !parsed.flags.help && !parsed.flags.version;
|
|
2375
|
+
if (leadingForeign && entryFor(dir).target) {
|
|
2376
|
+
return doForward(config, dir, rawOptions, void 0, parsed.forward, ui, () => void 0);
|
|
2377
|
+
}
|
|
2305
2378
|
if (parsed.unknownFlag) return ui.refuse(`Unknown option: ${parsed.unknownFlag}.`, `${ui.bin} --help`, 2);
|
|
2306
2379
|
if (parsed.flags.version) {
|
|
2307
2380
|
print(renderVersion(ui.bin, readState(dir)?.version ?? "not installed", LAUNCHER_VERSION));
|
|
2308
2381
|
return 0;
|
|
2309
2382
|
}
|
|
2310
2383
|
const command2 = parsed.command;
|
|
2384
|
+
if (!parsed.flags.help && command2 === void 0 && entryFor(dir).target) {
|
|
2385
|
+
return doForward(config, dir, rawOptions, void 0, [], ui, () => void 0);
|
|
2386
|
+
}
|
|
2311
2387
|
if (parsed.flags.help || command2 === void 0) {
|
|
2312
2388
|
ui.show(usage(config, dir, render));
|
|
2313
2389
|
return parsed.flags.help ? 0 : 2;
|
|
@@ -2397,7 +2473,7 @@ async function runFile(file, args, printErr) {
|
|
|
2397
2473
|
printErr("launcher --run needs a file to run.");
|
|
2398
2474
|
return 1;
|
|
2399
2475
|
}
|
|
2400
|
-
const abs =
|
|
2476
|
+
const abs = resolve3(process.cwd(), file);
|
|
2401
2477
|
if (!existsSync8(abs)) {
|
|
2402
2478
|
printErr(`cannot run ${file}: no such file.`);
|
|
2403
2479
|
return 1;
|
|
@@ -2705,7 +2781,7 @@ async function installOrUpdateRun(config, dir, options, print, update, flags2, m
|
|
|
2705
2781
|
}
|
|
2706
2782
|
const target = join7(dir, "payload");
|
|
2707
2783
|
mkdirSync6(dir, { recursive: true });
|
|
2708
|
-
|
|
2784
|
+
rmSync7(target, { recursive: true, force: true });
|
|
2709
2785
|
renameSync5(join7(candidateDir, "payload"), target);
|
|
2710
2786
|
writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2711
2787
|
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
@@ -2753,7 +2829,7 @@ function payloadFileAsCommand(entry2, payload) {
|
|
|
2753
2829
|
if (NEVER_A_PROGRAM.test(first)) return first;
|
|
2754
2830
|
if (process.platform === "win32") return null;
|
|
2755
2831
|
try {
|
|
2756
|
-
return (
|
|
2832
|
+
return (statSync2(candidate).mode & 73) === 0 ? first : null;
|
|
2757
2833
|
} catch {
|
|
2758
2834
|
return null;
|
|
2759
2835
|
}
|
|
@@ -2828,7 +2904,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2828
2904
|
let progressError;
|
|
2829
2905
|
const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
|
|
2830
2906
|
try {
|
|
2831
|
-
const result = await new Promise((
|
|
2907
|
+
const result = await new Promise((resolve4) => {
|
|
2832
2908
|
const child = spawn2(shell ? [command2, ...args].map(quoteForShell).join(" ") : command2, shell ? [] : args, {
|
|
2833
2909
|
cwd,
|
|
2834
2910
|
shell,
|
|
@@ -2885,8 +2961,8 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2885
2961
|
channel.setEncoding("utf8");
|
|
2886
2962
|
channel.on("data", receive);
|
|
2887
2963
|
}
|
|
2888
|
-
child.on("error", (error) =>
|
|
2889
|
-
child.on("close", (code) =>
|
|
2964
|
+
child.on("error", (error) => resolve4({ ok: false, error: error.message }));
|
|
2965
|
+
child.on("close", (code) => resolve4({
|
|
2890
2966
|
ok: code === 0,
|
|
2891
2967
|
...code !== null ? { code } : {},
|
|
2892
2968
|
...code !== 0 ? { error: `exit code ${code}` } : {}
|
|
@@ -2911,16 +2987,19 @@ function armState(arm) {
|
|
|
2911
2987
|
if (arm.verdict === "skip") return "kept";
|
|
2912
2988
|
return arm.from === arm.to ? "current" : "updated";
|
|
2913
2989
|
}
|
|
2990
|
+
function entryFor(dir) {
|
|
2991
|
+
const acquired = readState(dir)?.acquired;
|
|
2992
|
+
return { acquired, target: acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir) };
|
|
2993
|
+
}
|
|
2914
2994
|
function doForward(config, dir, options, command2, rest, ui, unlock) {
|
|
2915
|
-
if (existsSync8(command2)) {
|
|
2995
|
+
if (command2 !== void 0 && existsSync8(command2)) {
|
|
2916
2996
|
return ui.refuse(`${command2} is a file, not a command.`, `${ui.bin} --run ${command2}`, 2);
|
|
2917
2997
|
}
|
|
2918
|
-
const acquired =
|
|
2919
|
-
const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
|
|
2998
|
+
const { acquired, target } = entryFor(dir);
|
|
2920
2999
|
const verbs = readPayloadVerbs(dir);
|
|
2921
|
-
const declared = verbs === "*" || Array.isArray(verbs) && verbs.includes(command2);
|
|
3000
|
+
const declared = command2 === void 0 || verbs === "*" || Array.isArray(verbs) && verbs.includes(command2);
|
|
2922
3001
|
if (!target || !declared) return ui.refuse(`Unknown command: ${command2}.`, `${ui.bin} --help`, 2);
|
|
2923
|
-
const forwarded = [...resolveEntry(target), command2, ...rest];
|
|
3002
|
+
const forwarded = [...resolveEntry(target), ...command2 === void 0 ? [] : [command2], ...rest];
|
|
2924
3003
|
const cwd = payloadDir(dir);
|
|
2925
3004
|
const inherited = payloadEnv(dir);
|
|
2926
3005
|
const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
|
|
@@ -3019,6 +3098,8 @@ async function doStatus(config, dir, options, ui, print, json) {
|
|
|
3019
3098
|
function doDoctor(config, dir, options, ui) {
|
|
3020
3099
|
const local = localFacts(config, dir, options);
|
|
3021
3100
|
const verdict = verdictFor(ui, local, false, `${ui.bin} status`);
|
|
3101
|
+
const shadowFile = options.shadow?.file ?? (isSea() ? process.execPath : void 0);
|
|
3102
|
+
const shadow = shadowFile ? findPathShadow(config.binName, shadowFile, options.shadow ?? {}) : null;
|
|
3022
3103
|
ui.show(renderReport(ui.product, {
|
|
3023
3104
|
rows: [
|
|
3024
3105
|
{ surface: "launcher", state: LAUNCHER_VERSION, mark: "ok" },
|
|
@@ -3027,7 +3108,7 @@ function doDoctor(config, dir, options, ui) {
|
|
|
3027
3108
|
autoUpdateRow(local.schedule)
|
|
3028
3109
|
],
|
|
3029
3110
|
// Paths belong in the receipt, never a row.
|
|
3030
|
-
verdict: { ...verdict, facts: [`Folder: ${dir}`, `Release host: ${config.host}
|
|
3111
|
+
verdict: { ...verdict, facts: [`Folder: ${dir}`, `Release host: ${config.host}`, ...shadow ? [shadow.line] : []] }
|
|
3031
3112
|
}, ui.render));
|
|
3032
3113
|
const own = verdict.state === "fail" ? 1 : 0;
|
|
3033
3114
|
const acquired = readState(dir)?.acquired;
|
package/dist/launcher.sea.cjs
CHANGED
|
@@ -650,6 +650,28 @@ async function recoverAcquisition(dir, run2, env) {
|
|
|
650
650
|
else if ((0, import_node_fs5.existsSync)(statePath(dir))) (0, import_node_fs5.unlinkSync)(statePath(dir));
|
|
651
651
|
(0, import_node_fs5.unlinkSync)(pendingPath(dir));
|
|
652
652
|
}
|
|
653
|
+
function reapCandidates(dir) {
|
|
654
|
+
const keep = readState(dir)?.acquired?.candidate;
|
|
655
|
+
if (!keep) return;
|
|
656
|
+
const base = (0, import_node_path4.join)(dir, "candidates");
|
|
657
|
+
let names;
|
|
658
|
+
try {
|
|
659
|
+
names = (0, import_node_fs5.readdirSync)(base);
|
|
660
|
+
} catch {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
for (const name of names) {
|
|
664
|
+
if (name === keep || !/^[a-zA-Z0-9_-]+$/.test(name)) continue;
|
|
665
|
+
try {
|
|
666
|
+
const path = (0, import_node_path4.join)(base, name);
|
|
667
|
+
const info = (0, import_node_fs5.lstatSync)(path);
|
|
668
|
+
if (!info.isDirectory() || info.isSymbolicLink()) continue;
|
|
669
|
+
(0, import_node_fs5.rmSync)(path, { recursive: true, force: true });
|
|
670
|
+
} catch (error) {
|
|
671
|
+
console.error(`launcher: could not remove old candidate ${name}: ${error.message}`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
653
675
|
async function installAcquisition(dir, candidate, version, acquisition, options) {
|
|
654
676
|
if (!acquisition.platforms.includes(`${process.platform}-${process.arch}`)) throw new Error("this product does not support this platform");
|
|
655
677
|
const root = candidateRoot(dir, candidate);
|
|
@@ -706,6 +728,7 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
|
|
|
706
728
|
throw error;
|
|
707
729
|
}
|
|
708
730
|
(0, import_node_fs5.unlinkSync)(pendingPath(dir));
|
|
731
|
+
reapCandidates(dir);
|
|
709
732
|
}
|
|
710
733
|
|
|
711
734
|
// src/index.ts
|
|
@@ -1777,7 +1800,7 @@ function firstLine(text) {
|
|
|
1777
1800
|
|
|
1778
1801
|
// src/login-github.ts
|
|
1779
1802
|
var import_node_child_process3 = require("node:child_process");
|
|
1780
|
-
var realSleep = (ms) => new Promise((
|
|
1803
|
+
var realSleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1781
1804
|
function openBrowser(url) {
|
|
1782
1805
|
if (process.env.LAUNCHER_NO_OPEN === "1") return;
|
|
1783
1806
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
|
|
@@ -1931,7 +1954,7 @@ async function loginGoogle(options) {
|
|
|
1931
1954
|
const server = options.host.replace(/\/+$/, "");
|
|
1932
1955
|
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
1933
1956
|
const listener = (0, import_node_http.createServer)();
|
|
1934
|
-
await new Promise((
|
|
1957
|
+
await new Promise((resolve4) => listener.listen(0, "127.0.0.1", resolve4));
|
|
1935
1958
|
const port = listener.address().port;
|
|
1936
1959
|
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
1937
1960
|
try {
|
|
@@ -1958,7 +1981,7 @@ async function loginGoogle(options) {
|
|
|
1958
1981
|
code_challenge: challenge,
|
|
1959
1982
|
code_challenge_method: "S256"
|
|
1960
1983
|
}).toString();
|
|
1961
|
-
const code = await new Promise((
|
|
1984
|
+
const code = await new Promise((resolve4, reject) => {
|
|
1962
1985
|
const timer = setTimeout(() => reject(new Error("sign-in timed out \u2014 run login again.")), timeoutMs);
|
|
1963
1986
|
listener.on("request", (req, res) => {
|
|
1964
1987
|
const url = new URL(req.url ?? "/", redirectUri);
|
|
@@ -1976,7 +1999,7 @@ async function loginGoogle(options) {
|
|
|
1976
1999
|
}
|
|
1977
2000
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(page("Signed in.", "You can close this tab and go back to the terminal."));
|
|
1978
2001
|
clearTimeout(timer);
|
|
1979
|
-
|
|
2002
|
+
resolve4(received);
|
|
1980
2003
|
});
|
|
1981
2004
|
const print = options.print ?? ((line) => process.stderr.write(`${line}
|
|
1982
2005
|
`));
|
|
@@ -2135,15 +2158,54 @@ function recordedPath(productDir) {
|
|
|
2135
2158
|
function installedPath(target, productDir, platform) {
|
|
2136
2159
|
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);
|
|
2137
2160
|
}
|
|
2161
|
+
function onPath(bin, platform, env) {
|
|
2162
|
+
const win = platform === "win32";
|
|
2163
|
+
const read = (name) => env[name] ?? env[Object.keys(env).find((key) => key.toLowerCase() === name.toLowerCase()) ?? ""];
|
|
2164
|
+
const exts = win ? ["", ...(read("PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)] : [""];
|
|
2165
|
+
const found = [];
|
|
2166
|
+
for (const dir of (read("PATH") ?? "").split(win ? ";" : ":").filter(Boolean)) {
|
|
2167
|
+
for (const ext of exts) {
|
|
2168
|
+
const candidate = (0, import_node_path6.join)(dir.replace(/^"|"$/gu, ""), `${bin}${ext}`);
|
|
2169
|
+
try {
|
|
2170
|
+
if (!(0, import_node_fs11.statSync)(candidate).isFile()) continue;
|
|
2171
|
+
if (!win) (0, import_node_fs11.accessSync)(candidate, import_node_fs11.constants.X_OK);
|
|
2172
|
+
found.push(candidate);
|
|
2173
|
+
} catch {
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
return found;
|
|
2178
|
+
}
|
|
2179
|
+
function findPathShadow(bin, file, options = {}) {
|
|
2180
|
+
const platform = options.platform ?? process.platform;
|
|
2181
|
+
const same = (a, b) => {
|
|
2182
|
+
const real = (path) => {
|
|
2183
|
+
try {
|
|
2184
|
+
return (0, import_node_fs11.realpathSync)(path);
|
|
2185
|
+
} catch {
|
|
2186
|
+
return (0, import_node_path6.resolve)(path);
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
2189
|
+
const [x, y] = [real(a), real(b)];
|
|
2190
|
+
return platform === "win32" ? x.toLowerCase() === y.toLowerCase() : x === y;
|
|
2191
|
+
};
|
|
2192
|
+
const hits = onPath(bin, platform, options.env ?? process.env);
|
|
2193
|
+
const other = hits.find((hit) => !same(hit, file));
|
|
2194
|
+
if (!other) return null;
|
|
2195
|
+
if (hits.findIndex((hit) => same(hit, file)) === 0) {
|
|
2196
|
+
return { state: "shadowing", other, line: `Warning: ${file} shadows another ${bin} on PATH: ${other}.` };
|
|
2197
|
+
}
|
|
2198
|
+
return { state: "shadowed", other, line: `Warning: ${file} is shadowed by another ${bin} earlier on PATH: ${other}.` };
|
|
2199
|
+
}
|
|
2138
2200
|
function parseLauncherVersion(stdout, bin) {
|
|
2139
2201
|
const text = String(stdout).trim();
|
|
2140
2202
|
if (!text.startsWith(`${bin} `)) return null;
|
|
2141
2203
|
return text.match(/launcher (\d+\.\d+\.\d+[^\s)]*)/u)?.[1] ?? null;
|
|
2142
2204
|
}
|
|
2143
2205
|
function runVersion(path, bin) {
|
|
2144
|
-
return new Promise((
|
|
2206
|
+
return new Promise((resolve4) => {
|
|
2145
2207
|
(0, import_node_child_process4.execFile)(path, ["--version"], { timeout: 3e4, windowsHide: true, encoding: "utf8" }, (error, stdout) => {
|
|
2146
|
-
|
|
2208
|
+
resolve4(error ? null : parseLauncherVersion(stdout, bin));
|
|
2147
2209
|
});
|
|
2148
2210
|
});
|
|
2149
2211
|
}
|
|
@@ -2175,7 +2237,8 @@ async function refreshLauncher(target, options = {}) {
|
|
|
2175
2237
|
if (!options.dryRun) removeStale(file);
|
|
2176
2238
|
const from = await (options.readVersion ?? ((path) => runVersion(path, target.bin)))(file);
|
|
2177
2239
|
if (!from) return null;
|
|
2178
|
-
const
|
|
2240
|
+
const shadow = findPathShadow(target.bin, file, { platform, ...options.env ? { env: options.env } : {} });
|
|
2241
|
+
const arm = (verdict, to, detail) => ({ surface: "launcher", from, to, verdict, detail: [detail, shadow?.line].filter(Boolean).join(" "), durationMs: Date.now() - started });
|
|
2179
2242
|
const record = await fetchRecord(target, options);
|
|
2180
2243
|
if (record === "bad-signature") return arm("fail", from, "The launcher record failed its signature check.");
|
|
2181
2244
|
const entry2 = record?.files.find((candidate) => candidate.path === key);
|
|
@@ -2229,7 +2292,7 @@ async function refreshLauncher(target, options = {}) {
|
|
|
2229
2292
|
}
|
|
2230
2293
|
|
|
2231
2294
|
// src/index.ts
|
|
2232
|
-
var LAUNCHER_VERSION = true ? "1.
|
|
2295
|
+
var LAUNCHER_VERSION = true ? "1.3.1" : readVersionFromPackage();
|
|
2233
2296
|
function defaultPrint(message) {
|
|
2234
2297
|
process.stdout.write(`${message}
|
|
2235
2298
|
`);
|
|
@@ -2279,7 +2342,13 @@ function parseArgv(argv2) {
|
|
|
2279
2342
|
parsed.run = { file: argv2[i + 1], args: argv2.slice(i + 2) };
|
|
2280
2343
|
return parsed;
|
|
2281
2344
|
}
|
|
2282
|
-
|
|
2345
|
+
const next = flag(i);
|
|
2346
|
+
if (next < 0) {
|
|
2347
|
+
parsed.unknownFlag ??= argv2[i];
|
|
2348
|
+
parsed.forward = argv2.slice(i);
|
|
2349
|
+
return parsed;
|
|
2350
|
+
}
|
|
2351
|
+
i = next;
|
|
2283
2352
|
}
|
|
2284
2353
|
parsed.command = argv2[i];
|
|
2285
2354
|
if (parsed.command === void 0) return parsed;
|
|
@@ -2335,12 +2404,19 @@ async function run(rawOptions = {}) {
|
|
|
2335
2404
|
const dir = resolveProductDir(config.product, rawOptions.dir ?? parsed.flags.dir);
|
|
2336
2405
|
const ui = screensFor(faceProduct(config), config.binName, render, rawOptions);
|
|
2337
2406
|
if ((parsed.command === "install" || parsed.command === "update") && !parsed.flags.version) recordSelf(dir);
|
|
2407
|
+
const leadingForeign = parsed.forward !== void 0 && parsed.unknownFlag === parsed.forward[0] && !parsed.flags.help && !parsed.flags.version;
|
|
2408
|
+
if (leadingForeign && entryFor(dir).target) {
|
|
2409
|
+
return doForward(config, dir, rawOptions, void 0, parsed.forward, ui, () => void 0);
|
|
2410
|
+
}
|
|
2338
2411
|
if (parsed.unknownFlag) return ui.refuse(`Unknown option: ${parsed.unknownFlag}.`, `${ui.bin} --help`, 2);
|
|
2339
2412
|
if (parsed.flags.version) {
|
|
2340
2413
|
print(renderVersion(ui.bin, readState(dir)?.version ?? "not installed", LAUNCHER_VERSION));
|
|
2341
2414
|
return 0;
|
|
2342
2415
|
}
|
|
2343
2416
|
const command2 = parsed.command;
|
|
2417
|
+
if (!parsed.flags.help && command2 === void 0 && entryFor(dir).target) {
|
|
2418
|
+
return doForward(config, dir, rawOptions, void 0, [], ui, () => void 0);
|
|
2419
|
+
}
|
|
2344
2420
|
if (parsed.flags.help || command2 === void 0) {
|
|
2345
2421
|
ui.show(usage(config, dir, render));
|
|
2346
2422
|
return parsed.flags.help ? 0 : 2;
|
|
@@ -2861,7 +2937,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2861
2937
|
let progressError;
|
|
2862
2938
|
const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
|
|
2863
2939
|
try {
|
|
2864
|
-
const result = await new Promise((
|
|
2940
|
+
const result = await new Promise((resolve4) => {
|
|
2865
2941
|
const child = (0, import_node_child_process5.spawn)(shell ? [command2, ...args].map(quoteForShell).join(" ") : command2, shell ? [] : args, {
|
|
2866
2942
|
cwd,
|
|
2867
2943
|
shell,
|
|
@@ -2918,8 +2994,8 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2918
2994
|
channel.setEncoding("utf8");
|
|
2919
2995
|
channel.on("data", receive);
|
|
2920
2996
|
}
|
|
2921
|
-
child.on("error", (error) =>
|
|
2922
|
-
child.on("close", (code) =>
|
|
2997
|
+
child.on("error", (error) => resolve4({ ok: false, error: error.message }));
|
|
2998
|
+
child.on("close", (code) => resolve4({
|
|
2923
2999
|
ok: code === 0,
|
|
2924
3000
|
...code !== null ? { code } : {},
|
|
2925
3001
|
...code !== 0 ? { error: `exit code ${code}` } : {}
|
|
@@ -2944,16 +3020,19 @@ function armState(arm) {
|
|
|
2944
3020
|
if (arm.verdict === "skip") return "kept";
|
|
2945
3021
|
return arm.from === arm.to ? "current" : "updated";
|
|
2946
3022
|
}
|
|
3023
|
+
function entryFor(dir) {
|
|
3024
|
+
const acquired = readState(dir)?.acquired;
|
|
3025
|
+
return { acquired, target: acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir) };
|
|
3026
|
+
}
|
|
2947
3027
|
function doForward(config, dir, options, command2, rest, ui, unlock) {
|
|
2948
|
-
if ((0, import_node_fs12.existsSync)(command2)) {
|
|
3028
|
+
if (command2 !== void 0 && (0, import_node_fs12.existsSync)(command2)) {
|
|
2949
3029
|
return ui.refuse(`${command2} is a file, not a command.`, `${ui.bin} --run ${command2}`, 2);
|
|
2950
3030
|
}
|
|
2951
|
-
const acquired =
|
|
2952
|
-
const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
|
|
3031
|
+
const { acquired, target } = entryFor(dir);
|
|
2953
3032
|
const verbs = readPayloadVerbs(dir);
|
|
2954
|
-
const declared = verbs === "*" || Array.isArray(verbs) && verbs.includes(command2);
|
|
3033
|
+
const declared = command2 === void 0 || verbs === "*" || Array.isArray(verbs) && verbs.includes(command2);
|
|
2955
3034
|
if (!target || !declared) return ui.refuse(`Unknown command: ${command2}.`, `${ui.bin} --help`, 2);
|
|
2956
|
-
const forwarded = [...resolveEntry(target), command2, ...rest];
|
|
3035
|
+
const forwarded = [...resolveEntry(target), ...command2 === void 0 ? [] : [command2], ...rest];
|
|
2957
3036
|
const cwd = payloadDir(dir);
|
|
2958
3037
|
const inherited = payloadEnv(dir);
|
|
2959
3038
|
const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
|
|
@@ -3052,6 +3131,8 @@ async function doStatus(config, dir, options, ui, print, json) {
|
|
|
3052
3131
|
function doDoctor(config, dir, options, ui) {
|
|
3053
3132
|
const local = localFacts(config, dir, options);
|
|
3054
3133
|
const verdict = verdictFor(ui, local, false, `${ui.bin} status`);
|
|
3134
|
+
const shadowFile = options.shadow?.file ?? ((0, import_node_sea2.isSea)() ? process.execPath : void 0);
|
|
3135
|
+
const shadow = shadowFile ? findPathShadow(config.binName, shadowFile, options.shadow ?? {}) : null;
|
|
3055
3136
|
ui.show(renderReport(ui.product, {
|
|
3056
3137
|
rows: [
|
|
3057
3138
|
{ surface: "launcher", state: LAUNCHER_VERSION, mark: "ok" },
|
|
@@ -3060,7 +3141,7 @@ function doDoctor(config, dir, options, ui) {
|
|
|
3060
3141
|
autoUpdateRow(local.schedule)
|
|
3061
3142
|
],
|
|
3062
3143
|
// Paths belong in the receipt, never a row.
|
|
3063
|
-
verdict: { ...verdict, facts: [`Folder: ${dir}`, `Release host: ${config.host}
|
|
3144
|
+
verdict: { ...verdict, facts: [`Folder: ${dir}`, `Release host: ${config.host}`, ...shadow ? [shadow.line] : []] }
|
|
3064
3145
|
}, ui.render));
|
|
3065
3146
|
const own = verdict.state === "fail" ? 1 : 0;
|
|
3066
3147
|
const acquired = readState(dir)?.acquired;
|
package/dist/refresh.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// src/refresh.ts
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
-
import { chmodSync, existsSync as existsSync2, lstatSync, readdirSync, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3
|
+
import { accessSync, chmodSync, constants, existsSync as existsSync2, lstatSync as lstatSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync2, renameSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { basename, dirname as dirname2, join as join3 } from "node:path";
|
|
5
|
+
import { basename, dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
|
|
6
6
|
|
|
7
7
|
// src/acquisition.ts
|
|
8
8
|
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
9
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, realpathSync } from "node:fs";
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, realpathSync, readdirSync, lstatSync, rmSync } from "node:fs";
|
|
10
10
|
import { join as join2, resolve, relative, isAbsolute, dirname } from "node:path";
|
|
11
11
|
|
|
12
12
|
// src/download.ts
|
|
@@ -235,15 +235,54 @@ function recordedPath(productDir) {
|
|
|
235
235
|
function installedPath(target, productDir, platform) {
|
|
236
236
|
return platform === "win32" ? join3(productDir, "bin", `${target.bin}.exe`) : join3(homedir(), ".local", "bin", target.bin);
|
|
237
237
|
}
|
|
238
|
+
function onPath(bin, platform, env) {
|
|
239
|
+
const win = platform === "win32";
|
|
240
|
+
const read = (name) => env[name] ?? env[Object.keys(env).find((key) => key.toLowerCase() === name.toLowerCase()) ?? ""];
|
|
241
|
+
const exts = win ? ["", ...(read("PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)] : [""];
|
|
242
|
+
const found = [];
|
|
243
|
+
for (const dir of (read("PATH") ?? "").split(win ? ";" : ":").filter(Boolean)) {
|
|
244
|
+
for (const ext of exts) {
|
|
245
|
+
const candidate = join3(dir.replace(/^"|"$/gu, ""), `${bin}${ext}`);
|
|
246
|
+
try {
|
|
247
|
+
if (!statSync(candidate).isFile()) continue;
|
|
248
|
+
if (!win) accessSync(candidate, constants.X_OK);
|
|
249
|
+
found.push(candidate);
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return found;
|
|
255
|
+
}
|
|
256
|
+
function findPathShadow(bin, file, options = {}) {
|
|
257
|
+
const platform = options.platform ?? process.platform;
|
|
258
|
+
const same = (a, b) => {
|
|
259
|
+
const real = (path) => {
|
|
260
|
+
try {
|
|
261
|
+
return realpathSync2(path);
|
|
262
|
+
} catch {
|
|
263
|
+
return resolve2(path);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
const [x, y] = [real(a), real(b)];
|
|
267
|
+
return platform === "win32" ? x.toLowerCase() === y.toLowerCase() : x === y;
|
|
268
|
+
};
|
|
269
|
+
const hits = onPath(bin, platform, options.env ?? process.env);
|
|
270
|
+
const other = hits.find((hit) => !same(hit, file));
|
|
271
|
+
if (!other) return null;
|
|
272
|
+
if (hits.findIndex((hit) => same(hit, file)) === 0) {
|
|
273
|
+
return { state: "shadowing", other, line: `Warning: ${file} shadows another ${bin} on PATH: ${other}.` };
|
|
274
|
+
}
|
|
275
|
+
return { state: "shadowed", other, line: `Warning: ${file} is shadowed by another ${bin} earlier on PATH: ${other}.` };
|
|
276
|
+
}
|
|
238
277
|
function parseLauncherVersion(stdout, bin) {
|
|
239
278
|
const text = String(stdout).trim();
|
|
240
279
|
if (!text.startsWith(`${bin} `)) return null;
|
|
241
280
|
return text.match(/launcher (\d+\.\d+\.\d+[^\s)]*)/u)?.[1] ?? null;
|
|
242
281
|
}
|
|
243
282
|
function runVersion(path, bin) {
|
|
244
|
-
return new Promise((
|
|
283
|
+
return new Promise((resolve3) => {
|
|
245
284
|
execFile(path, ["--version"], { timeout: 3e4, windowsHide: true, encoding: "utf8" }, (error, stdout) => {
|
|
246
|
-
|
|
285
|
+
resolve3(error ? null : parseLauncherVersion(stdout, bin));
|
|
247
286
|
});
|
|
248
287
|
});
|
|
249
288
|
}
|
|
@@ -251,14 +290,14 @@ function removeStale(file) {
|
|
|
251
290
|
const prefix = `${basename(file)}.old-`;
|
|
252
291
|
let names;
|
|
253
292
|
try {
|
|
254
|
-
names =
|
|
293
|
+
names = readdirSync2(dirname2(file));
|
|
255
294
|
} catch {
|
|
256
295
|
return;
|
|
257
296
|
}
|
|
258
297
|
for (const name of names) {
|
|
259
298
|
if (!name.startsWith(prefix)) continue;
|
|
260
299
|
try {
|
|
261
|
-
|
|
300
|
+
rmSync2(join3(dirname2(file), name), { force: true });
|
|
262
301
|
} catch {
|
|
263
302
|
}
|
|
264
303
|
}
|
|
@@ -271,11 +310,12 @@ async function refreshLauncher(target, options = {}) {
|
|
|
271
310
|
const productDir = options.productDir ?? defaultProductDir(target.product);
|
|
272
311
|
const recorded = recordedPath(productDir);
|
|
273
312
|
const file = options.path ?? (recorded && existsSync2(recorded) ? recorded : installedPath(target, productDir, platform));
|
|
274
|
-
if (!existsSync2(file) ||
|
|
313
|
+
if (!existsSync2(file) || lstatSync2(file).isSymbolicLink()) return null;
|
|
275
314
|
if (!options.dryRun) removeStale(file);
|
|
276
315
|
const from = await (options.readVersion ?? ((path) => runVersion(path, target.bin)))(file);
|
|
277
316
|
if (!from) return null;
|
|
278
|
-
const
|
|
317
|
+
const shadow = findPathShadow(target.bin, file, { platform, ...options.env ? { env: options.env } : {} });
|
|
318
|
+
const arm = (verdict, to, detail) => ({ surface: "launcher", from, to, verdict, detail: [detail, shadow?.line].filter(Boolean).join(" "), durationMs: Date.now() - started });
|
|
279
319
|
const record = await fetchRecord(target, options);
|
|
280
320
|
if (record === "bad-signature") return arm("fail", from, "The launcher record failed its signature check.");
|
|
281
321
|
const entry = record?.files.find((candidate) => candidate.path === key);
|
|
@@ -319,7 +359,7 @@ async function refreshLauncher(target, options = {}) {
|
|
|
319
359
|
if (moved && !existsSync2(file)) renameSync(moved, file);
|
|
320
360
|
} catch {
|
|
321
361
|
}
|
|
322
|
-
|
|
362
|
+
rmSync2(next, { force: true });
|
|
323
363
|
return arm("fail", from, "The launcher could not be replaced.");
|
|
324
364
|
}
|
|
325
365
|
return arm("ok", record.version, "");
|
|
@@ -330,6 +370,7 @@ async function refreshLauncher(target, options = {}) {
|
|
|
330
370
|
export {
|
|
331
371
|
LAUNCHER_RECORD_FILE,
|
|
332
372
|
compareVersions,
|
|
373
|
+
findPathShadow,
|
|
333
374
|
parseLauncherVersion,
|
|
334
375
|
readLauncherRecord,
|
|
335
376
|
refreshLauncher
|
package/package.json
CHANGED