@mutmutco/installer-launcher 1.2.5 → 1.3.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 +1 -0
- package/dist/launcher.js +82 -24
- package/dist/launcher.sea.cjs +76 -18
- package/dist/refresh.js +46 -5
- 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
|
```
|
package/dist/launcher.js
CHANGED
|
@@ -677,9 +677,9 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
|
|
|
677
677
|
|
|
678
678
|
// src/index.ts
|
|
679
679
|
import { spawn as spawn2, spawnSync as spawnSync4 } from "node:child_process";
|
|
680
|
-
import { existsSync as existsSync8, readFileSync as readFileSync8, statSync, mkdtempSync as mkdtempSync2, unlinkSync as unlinkSync2, rmdirSync, mkdirSync as mkdirSync6, renameSync as renameSync5, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
|
|
680
|
+
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 rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
|
|
681
681
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
682
|
-
import { dirname as dirname4, join as join7, resolve as
|
|
682
|
+
import { dirname as dirname4, join as join7, resolve as resolve3 } from "node:path";
|
|
683
683
|
import { isSea } from "node:sea";
|
|
684
684
|
import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
|
|
685
685
|
|
|
@@ -1744,7 +1744,7 @@ function firstLine(text) {
|
|
|
1744
1744
|
|
|
1745
1745
|
// src/login-github.ts
|
|
1746
1746
|
import { spawn, spawnSync as spawnSync3 } from "node:child_process";
|
|
1747
|
-
var realSleep = (ms) => new Promise((
|
|
1747
|
+
var realSleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1748
1748
|
function openBrowser(url) {
|
|
1749
1749
|
if (process.env.LAUNCHER_NO_OPEN === "1") return;
|
|
1750
1750
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
|
|
@@ -1898,7 +1898,7 @@ async function loginGoogle(options) {
|
|
|
1898
1898
|
const server = options.host.replace(/\/+$/, "");
|
|
1899
1899
|
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
1900
1900
|
const listener = createServer();
|
|
1901
|
-
await new Promise((
|
|
1901
|
+
await new Promise((resolve4) => listener.listen(0, "127.0.0.1", resolve4));
|
|
1902
1902
|
const port = listener.address().port;
|
|
1903
1903
|
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
1904
1904
|
try {
|
|
@@ -1925,7 +1925,7 @@ async function loginGoogle(options) {
|
|
|
1925
1925
|
code_challenge: challenge,
|
|
1926
1926
|
code_challenge_method: "S256"
|
|
1927
1927
|
}).toString();
|
|
1928
|
-
const code = await new Promise((
|
|
1928
|
+
const code = await new Promise((resolve4, reject) => {
|
|
1929
1929
|
const timer = setTimeout(() => reject(new Error("sign-in timed out \u2014 run login again.")), timeoutMs);
|
|
1930
1930
|
listener.on("request", (req, res) => {
|
|
1931
1931
|
const url = new URL(req.url ?? "/", redirectUri);
|
|
@@ -1943,7 +1943,7 @@ async function loginGoogle(options) {
|
|
|
1943
1943
|
}
|
|
1944
1944
|
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
1945
|
clearTimeout(timer);
|
|
1946
|
-
|
|
1946
|
+
resolve4(received);
|
|
1947
1947
|
});
|
|
1948
1948
|
const print = options.print ?? ((line) => process.stderr.write(`${line}
|
|
1949
1949
|
`));
|
|
@@ -2041,9 +2041,9 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
|
|
|
2041
2041
|
|
|
2042
2042
|
// src/refresh.ts
|
|
2043
2043
|
import { execFile } from "node:child_process";
|
|
2044
|
-
import { chmodSync, existsSync as existsSync7, lstatSync, readdirSync, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
|
|
2044
|
+
import { accessSync, chmodSync, constants, existsSync as existsSync7, lstatSync, readdirSync, readFileSync as readFileSync7, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync5, statSync, writeFileSync as writeFileSync7 } from "node:fs";
|
|
2045
2045
|
import { homedir } from "node:os";
|
|
2046
|
-
import { basename, dirname as dirname3, join as join6 } from "node:path";
|
|
2046
|
+
import { basename, dirname as dirname3, join as join6, resolve as resolve2 } from "node:path";
|
|
2047
2047
|
var LAUNCHER_RECORD_FILE = "launcher.json";
|
|
2048
2048
|
var RECORD_TIMEOUT_MS = 1e4;
|
|
2049
2049
|
function platformKey(platform, arch) {
|
|
@@ -2102,15 +2102,54 @@ function recordedPath(productDir) {
|
|
|
2102
2102
|
function installedPath(target, productDir, platform) {
|
|
2103
2103
|
return platform === "win32" ? join6(productDir, "bin", `${target.bin}.exe`) : join6(homedir(), ".local", "bin", target.bin);
|
|
2104
2104
|
}
|
|
2105
|
+
function onPath(bin, platform, env) {
|
|
2106
|
+
const win = platform === "win32";
|
|
2107
|
+
const read = (name) => env[name] ?? env[Object.keys(env).find((key) => key.toLowerCase() === name.toLowerCase()) ?? ""];
|
|
2108
|
+
const exts = win ? ["", ...(read("PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)] : [""];
|
|
2109
|
+
const found = [];
|
|
2110
|
+
for (const dir of (read("PATH") ?? "").split(win ? ";" : ":").filter(Boolean)) {
|
|
2111
|
+
for (const ext of exts) {
|
|
2112
|
+
const candidate = join6(dir.replace(/^"|"$/gu, ""), `${bin}${ext}`);
|
|
2113
|
+
try {
|
|
2114
|
+
if (!statSync(candidate).isFile()) continue;
|
|
2115
|
+
if (!win) accessSync(candidate, constants.X_OK);
|
|
2116
|
+
found.push(candidate);
|
|
2117
|
+
} catch {
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
return found;
|
|
2122
|
+
}
|
|
2123
|
+
function findPathShadow(bin, file, options = {}) {
|
|
2124
|
+
const platform = options.platform ?? process.platform;
|
|
2125
|
+
const same = (a, b) => {
|
|
2126
|
+
const real = (path) => {
|
|
2127
|
+
try {
|
|
2128
|
+
return realpathSync3(path);
|
|
2129
|
+
} catch {
|
|
2130
|
+
return resolve2(path);
|
|
2131
|
+
}
|
|
2132
|
+
};
|
|
2133
|
+
const [x, y] = [real(a), real(b)];
|
|
2134
|
+
return platform === "win32" ? x.toLowerCase() === y.toLowerCase() : x === y;
|
|
2135
|
+
};
|
|
2136
|
+
const hits = onPath(bin, platform, options.env ?? process.env);
|
|
2137
|
+
const other = hits.find((hit) => !same(hit, file));
|
|
2138
|
+
if (!other) return null;
|
|
2139
|
+
if (hits.findIndex((hit) => same(hit, file)) === 0) {
|
|
2140
|
+
return { state: "shadowing", other, line: `Warning: ${file} shadows another ${bin} on PATH: ${other}.` };
|
|
2141
|
+
}
|
|
2142
|
+
return { state: "shadowed", other, line: `Warning: ${file} is shadowed by another ${bin} earlier on PATH: ${other}.` };
|
|
2143
|
+
}
|
|
2105
2144
|
function parseLauncherVersion(stdout, bin) {
|
|
2106
2145
|
const text = String(stdout).trim();
|
|
2107
2146
|
if (!text.startsWith(`${bin} `)) return null;
|
|
2108
2147
|
return text.match(/launcher (\d+\.\d+\.\d+[^\s)]*)/u)?.[1] ?? null;
|
|
2109
2148
|
}
|
|
2110
2149
|
function runVersion(path, bin) {
|
|
2111
|
-
return new Promise((
|
|
2150
|
+
return new Promise((resolve4) => {
|
|
2112
2151
|
execFile(path, ["--version"], { timeout: 3e4, windowsHide: true, encoding: "utf8" }, (error, stdout) => {
|
|
2113
|
-
|
|
2152
|
+
resolve4(error ? null : parseLauncherVersion(stdout, bin));
|
|
2114
2153
|
});
|
|
2115
2154
|
});
|
|
2116
2155
|
}
|
|
@@ -2142,7 +2181,8 @@ async function refreshLauncher(target, options = {}) {
|
|
|
2142
2181
|
if (!options.dryRun) removeStale(file);
|
|
2143
2182
|
const from = await (options.readVersion ?? ((path) => runVersion(path, target.bin)))(file);
|
|
2144
2183
|
if (!from) return null;
|
|
2145
|
-
const
|
|
2184
|
+
const shadow = findPathShadow(target.bin, file, { platform, ...options.env ? { env: options.env } : {} });
|
|
2185
|
+
const arm = (verdict, to, detail) => ({ surface: "launcher", from, to, verdict, detail: [detail, shadow?.line].filter(Boolean).join(" "), durationMs: Date.now() - started });
|
|
2146
2186
|
const record = await fetchRecord(target, options);
|
|
2147
2187
|
if (record === "bad-signature") return arm("fail", from, "The launcher record failed its signature check.");
|
|
2148
2188
|
const entry2 = record?.files.find((candidate) => candidate.path === key);
|
|
@@ -2196,7 +2236,7 @@ async function refreshLauncher(target, options = {}) {
|
|
|
2196
2236
|
}
|
|
2197
2237
|
|
|
2198
2238
|
// src/index.ts
|
|
2199
|
-
var LAUNCHER_VERSION = true ? "1.
|
|
2239
|
+
var LAUNCHER_VERSION = true ? "1.3.0" : readVersionFromPackage();
|
|
2200
2240
|
function defaultPrint(message) {
|
|
2201
2241
|
process.stdout.write(`${message}
|
|
2202
2242
|
`);
|
|
@@ -2246,7 +2286,13 @@ function parseArgv(argv2) {
|
|
|
2246
2286
|
parsed.run = { file: argv2[i + 1], args: argv2.slice(i + 2) };
|
|
2247
2287
|
return parsed;
|
|
2248
2288
|
}
|
|
2249
|
-
|
|
2289
|
+
const next = flag(i);
|
|
2290
|
+
if (next < 0) {
|
|
2291
|
+
parsed.unknownFlag ??= argv2[i];
|
|
2292
|
+
parsed.forward = argv2.slice(i);
|
|
2293
|
+
return parsed;
|
|
2294
|
+
}
|
|
2295
|
+
i = next;
|
|
2250
2296
|
}
|
|
2251
2297
|
parsed.command = argv2[i];
|
|
2252
2298
|
if (parsed.command === void 0) return parsed;
|
|
@@ -2302,12 +2348,19 @@ async function run(rawOptions = {}) {
|
|
|
2302
2348
|
const dir = resolveProductDir(config.product, rawOptions.dir ?? parsed.flags.dir);
|
|
2303
2349
|
const ui = screensFor(faceProduct(config), config.binName, render, rawOptions);
|
|
2304
2350
|
if ((parsed.command === "install" || parsed.command === "update") && !parsed.flags.version) recordSelf(dir);
|
|
2351
|
+
const leadingForeign = parsed.forward !== void 0 && parsed.unknownFlag === parsed.forward[0] && !parsed.flags.help && !parsed.flags.version;
|
|
2352
|
+
if (leadingForeign && entryFor(dir).target) {
|
|
2353
|
+
return doForward(config, dir, rawOptions, void 0, parsed.forward, ui, () => void 0);
|
|
2354
|
+
}
|
|
2305
2355
|
if (parsed.unknownFlag) return ui.refuse(`Unknown option: ${parsed.unknownFlag}.`, `${ui.bin} --help`, 2);
|
|
2306
2356
|
if (parsed.flags.version) {
|
|
2307
2357
|
print(renderVersion(ui.bin, readState(dir)?.version ?? "not installed", LAUNCHER_VERSION));
|
|
2308
2358
|
return 0;
|
|
2309
2359
|
}
|
|
2310
2360
|
const command2 = parsed.command;
|
|
2361
|
+
if (!parsed.flags.help && command2 === void 0 && entryFor(dir).target) {
|
|
2362
|
+
return doForward(config, dir, rawOptions, void 0, [], ui, () => void 0);
|
|
2363
|
+
}
|
|
2311
2364
|
if (parsed.flags.help || command2 === void 0) {
|
|
2312
2365
|
ui.show(usage(config, dir, render));
|
|
2313
2366
|
return parsed.flags.help ? 0 : 2;
|
|
@@ -2397,7 +2450,7 @@ async function runFile(file, args, printErr) {
|
|
|
2397
2450
|
printErr("launcher --run needs a file to run.");
|
|
2398
2451
|
return 1;
|
|
2399
2452
|
}
|
|
2400
|
-
const abs =
|
|
2453
|
+
const abs = resolve3(process.cwd(), file);
|
|
2401
2454
|
if (!existsSync8(abs)) {
|
|
2402
2455
|
printErr(`cannot run ${file}: no such file.`);
|
|
2403
2456
|
return 1;
|
|
@@ -2753,7 +2806,7 @@ function payloadFileAsCommand(entry2, payload) {
|
|
|
2753
2806
|
if (NEVER_A_PROGRAM.test(first)) return first;
|
|
2754
2807
|
if (process.platform === "win32") return null;
|
|
2755
2808
|
try {
|
|
2756
|
-
return (
|
|
2809
|
+
return (statSync2(candidate).mode & 73) === 0 ? first : null;
|
|
2757
2810
|
} catch {
|
|
2758
2811
|
return null;
|
|
2759
2812
|
}
|
|
@@ -2828,7 +2881,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2828
2881
|
let progressError;
|
|
2829
2882
|
const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
|
|
2830
2883
|
try {
|
|
2831
|
-
const result = await new Promise((
|
|
2884
|
+
const result = await new Promise((resolve4) => {
|
|
2832
2885
|
const child = spawn2(shell ? [command2, ...args].map(quoteForShell).join(" ") : command2, shell ? [] : args, {
|
|
2833
2886
|
cwd,
|
|
2834
2887
|
shell,
|
|
@@ -2885,8 +2938,8 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2885
2938
|
channel.setEncoding("utf8");
|
|
2886
2939
|
channel.on("data", receive);
|
|
2887
2940
|
}
|
|
2888
|
-
child.on("error", (error) =>
|
|
2889
|
-
child.on("close", (code) =>
|
|
2941
|
+
child.on("error", (error) => resolve4({ ok: false, error: error.message }));
|
|
2942
|
+
child.on("close", (code) => resolve4({
|
|
2890
2943
|
ok: code === 0,
|
|
2891
2944
|
...code !== null ? { code } : {},
|
|
2892
2945
|
...code !== 0 ? { error: `exit code ${code}` } : {}
|
|
@@ -2911,16 +2964,19 @@ function armState(arm) {
|
|
|
2911
2964
|
if (arm.verdict === "skip") return "kept";
|
|
2912
2965
|
return arm.from === arm.to ? "current" : "updated";
|
|
2913
2966
|
}
|
|
2967
|
+
function entryFor(dir) {
|
|
2968
|
+
const acquired = readState(dir)?.acquired;
|
|
2969
|
+
return { acquired, target: acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir) };
|
|
2970
|
+
}
|
|
2914
2971
|
function doForward(config, dir, options, command2, rest, ui, unlock) {
|
|
2915
|
-
if (existsSync8(command2)) {
|
|
2972
|
+
if (command2 !== void 0 && existsSync8(command2)) {
|
|
2916
2973
|
return ui.refuse(`${command2} is a file, not a command.`, `${ui.bin} --run ${command2}`, 2);
|
|
2917
2974
|
}
|
|
2918
|
-
const acquired =
|
|
2919
|
-
const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
|
|
2975
|
+
const { acquired, target } = entryFor(dir);
|
|
2920
2976
|
const verbs = readPayloadVerbs(dir);
|
|
2921
|
-
const declared = verbs === "*" || Array.isArray(verbs) && verbs.includes(command2);
|
|
2977
|
+
const declared = command2 === void 0 || verbs === "*" || Array.isArray(verbs) && verbs.includes(command2);
|
|
2922
2978
|
if (!target || !declared) return ui.refuse(`Unknown command: ${command2}.`, `${ui.bin} --help`, 2);
|
|
2923
|
-
const forwarded = [...resolveEntry(target), command2, ...rest];
|
|
2979
|
+
const forwarded = [...resolveEntry(target), ...command2 === void 0 ? [] : [command2], ...rest];
|
|
2924
2980
|
const cwd = payloadDir(dir);
|
|
2925
2981
|
const inherited = payloadEnv(dir);
|
|
2926
2982
|
const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
|
|
@@ -3019,6 +3075,8 @@ async function doStatus(config, dir, options, ui, print, json) {
|
|
|
3019
3075
|
function doDoctor(config, dir, options, ui) {
|
|
3020
3076
|
const local = localFacts(config, dir, options);
|
|
3021
3077
|
const verdict = verdictFor(ui, local, false, `${ui.bin} status`);
|
|
3078
|
+
const shadowFile = options.shadow?.file ?? (isSea() ? process.execPath : void 0);
|
|
3079
|
+
const shadow = shadowFile ? findPathShadow(config.binName, shadowFile, options.shadow ?? {}) : null;
|
|
3022
3080
|
ui.show(renderReport(ui.product, {
|
|
3023
3081
|
rows: [
|
|
3024
3082
|
{ surface: "launcher", state: LAUNCHER_VERSION, mark: "ok" },
|
|
@@ -3027,7 +3085,7 @@ function doDoctor(config, dir, options, ui) {
|
|
|
3027
3085
|
autoUpdateRow(local.schedule)
|
|
3028
3086
|
],
|
|
3029
3087
|
// Paths belong in the receipt, never a row.
|
|
3030
|
-
verdict: { ...verdict, facts: [`Folder: ${dir}`, `Release host: ${config.host}
|
|
3088
|
+
verdict: { ...verdict, facts: [`Folder: ${dir}`, `Release host: ${config.host}`, ...shadow ? [shadow.line] : []] }
|
|
3031
3089
|
}, ui.render));
|
|
3032
3090
|
const own = verdict.state === "fail" ? 1 : 0;
|
|
3033
3091
|
const acquired = readState(dir)?.acquired;
|
package/dist/launcher.sea.cjs
CHANGED
|
@@ -1777,7 +1777,7 @@ function firstLine(text) {
|
|
|
1777
1777
|
|
|
1778
1778
|
// src/login-github.ts
|
|
1779
1779
|
var import_node_child_process3 = require("node:child_process");
|
|
1780
|
-
var realSleep = (ms) => new Promise((
|
|
1780
|
+
var realSleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1781
1781
|
function openBrowser(url) {
|
|
1782
1782
|
if (process.env.LAUNCHER_NO_OPEN === "1") return;
|
|
1783
1783
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
|
|
@@ -1931,7 +1931,7 @@ async function loginGoogle(options) {
|
|
|
1931
1931
|
const server = options.host.replace(/\/+$/, "");
|
|
1932
1932
|
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
1933
1933
|
const listener = (0, import_node_http.createServer)();
|
|
1934
|
-
await new Promise((
|
|
1934
|
+
await new Promise((resolve4) => listener.listen(0, "127.0.0.1", resolve4));
|
|
1935
1935
|
const port = listener.address().port;
|
|
1936
1936
|
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
1937
1937
|
try {
|
|
@@ -1958,7 +1958,7 @@ async function loginGoogle(options) {
|
|
|
1958
1958
|
code_challenge: challenge,
|
|
1959
1959
|
code_challenge_method: "S256"
|
|
1960
1960
|
}).toString();
|
|
1961
|
-
const code = await new Promise((
|
|
1961
|
+
const code = await new Promise((resolve4, reject) => {
|
|
1962
1962
|
const timer = setTimeout(() => reject(new Error("sign-in timed out \u2014 run login again.")), timeoutMs);
|
|
1963
1963
|
listener.on("request", (req, res) => {
|
|
1964
1964
|
const url = new URL(req.url ?? "/", redirectUri);
|
|
@@ -1976,7 +1976,7 @@ async function loginGoogle(options) {
|
|
|
1976
1976
|
}
|
|
1977
1977
|
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
1978
|
clearTimeout(timer);
|
|
1979
|
-
|
|
1979
|
+
resolve4(received);
|
|
1980
1980
|
});
|
|
1981
1981
|
const print = options.print ?? ((line) => process.stderr.write(`${line}
|
|
1982
1982
|
`));
|
|
@@ -2135,15 +2135,54 @@ function recordedPath(productDir) {
|
|
|
2135
2135
|
function installedPath(target, productDir, platform) {
|
|
2136
2136
|
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
2137
|
}
|
|
2138
|
+
function onPath(bin, platform, env) {
|
|
2139
|
+
const win = platform === "win32";
|
|
2140
|
+
const read = (name) => env[name] ?? env[Object.keys(env).find((key) => key.toLowerCase() === name.toLowerCase()) ?? ""];
|
|
2141
|
+
const exts = win ? ["", ...(read("PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)] : [""];
|
|
2142
|
+
const found = [];
|
|
2143
|
+
for (const dir of (read("PATH") ?? "").split(win ? ";" : ":").filter(Boolean)) {
|
|
2144
|
+
for (const ext of exts) {
|
|
2145
|
+
const candidate = (0, import_node_path6.join)(dir.replace(/^"|"$/gu, ""), `${bin}${ext}`);
|
|
2146
|
+
try {
|
|
2147
|
+
if (!(0, import_node_fs11.statSync)(candidate).isFile()) continue;
|
|
2148
|
+
if (!win) (0, import_node_fs11.accessSync)(candidate, import_node_fs11.constants.X_OK);
|
|
2149
|
+
found.push(candidate);
|
|
2150
|
+
} catch {
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
return found;
|
|
2155
|
+
}
|
|
2156
|
+
function findPathShadow(bin, file, options = {}) {
|
|
2157
|
+
const platform = options.platform ?? process.platform;
|
|
2158
|
+
const same = (a, b) => {
|
|
2159
|
+
const real = (path) => {
|
|
2160
|
+
try {
|
|
2161
|
+
return (0, import_node_fs11.realpathSync)(path);
|
|
2162
|
+
} catch {
|
|
2163
|
+
return (0, import_node_path6.resolve)(path);
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
const [x, y] = [real(a), real(b)];
|
|
2167
|
+
return platform === "win32" ? x.toLowerCase() === y.toLowerCase() : x === y;
|
|
2168
|
+
};
|
|
2169
|
+
const hits = onPath(bin, platform, options.env ?? process.env);
|
|
2170
|
+
const other = hits.find((hit) => !same(hit, file));
|
|
2171
|
+
if (!other) return null;
|
|
2172
|
+
if (hits.findIndex((hit) => same(hit, file)) === 0) {
|
|
2173
|
+
return { state: "shadowing", other, line: `Warning: ${file} shadows another ${bin} on PATH: ${other}.` };
|
|
2174
|
+
}
|
|
2175
|
+
return { state: "shadowed", other, line: `Warning: ${file} is shadowed by another ${bin} earlier on PATH: ${other}.` };
|
|
2176
|
+
}
|
|
2138
2177
|
function parseLauncherVersion(stdout, bin) {
|
|
2139
2178
|
const text = String(stdout).trim();
|
|
2140
2179
|
if (!text.startsWith(`${bin} `)) return null;
|
|
2141
2180
|
return text.match(/launcher (\d+\.\d+\.\d+[^\s)]*)/u)?.[1] ?? null;
|
|
2142
2181
|
}
|
|
2143
2182
|
function runVersion(path, bin) {
|
|
2144
|
-
return new Promise((
|
|
2183
|
+
return new Promise((resolve4) => {
|
|
2145
2184
|
(0, import_node_child_process4.execFile)(path, ["--version"], { timeout: 3e4, windowsHide: true, encoding: "utf8" }, (error, stdout) => {
|
|
2146
|
-
|
|
2185
|
+
resolve4(error ? null : parseLauncherVersion(stdout, bin));
|
|
2147
2186
|
});
|
|
2148
2187
|
});
|
|
2149
2188
|
}
|
|
@@ -2175,7 +2214,8 @@ async function refreshLauncher(target, options = {}) {
|
|
|
2175
2214
|
if (!options.dryRun) removeStale(file);
|
|
2176
2215
|
const from = await (options.readVersion ?? ((path) => runVersion(path, target.bin)))(file);
|
|
2177
2216
|
if (!from) return null;
|
|
2178
|
-
const
|
|
2217
|
+
const shadow = findPathShadow(target.bin, file, { platform, ...options.env ? { env: options.env } : {} });
|
|
2218
|
+
const arm = (verdict, to, detail) => ({ surface: "launcher", from, to, verdict, detail: [detail, shadow?.line].filter(Boolean).join(" "), durationMs: Date.now() - started });
|
|
2179
2219
|
const record = await fetchRecord(target, options);
|
|
2180
2220
|
if (record === "bad-signature") return arm("fail", from, "The launcher record failed its signature check.");
|
|
2181
2221
|
const entry2 = record?.files.find((candidate) => candidate.path === key);
|
|
@@ -2229,7 +2269,7 @@ async function refreshLauncher(target, options = {}) {
|
|
|
2229
2269
|
}
|
|
2230
2270
|
|
|
2231
2271
|
// src/index.ts
|
|
2232
|
-
var LAUNCHER_VERSION = true ? "1.
|
|
2272
|
+
var LAUNCHER_VERSION = true ? "1.3.0" : readVersionFromPackage();
|
|
2233
2273
|
function defaultPrint(message) {
|
|
2234
2274
|
process.stdout.write(`${message}
|
|
2235
2275
|
`);
|
|
@@ -2279,7 +2319,13 @@ function parseArgv(argv2) {
|
|
|
2279
2319
|
parsed.run = { file: argv2[i + 1], args: argv2.slice(i + 2) };
|
|
2280
2320
|
return parsed;
|
|
2281
2321
|
}
|
|
2282
|
-
|
|
2322
|
+
const next = flag(i);
|
|
2323
|
+
if (next < 0) {
|
|
2324
|
+
parsed.unknownFlag ??= argv2[i];
|
|
2325
|
+
parsed.forward = argv2.slice(i);
|
|
2326
|
+
return parsed;
|
|
2327
|
+
}
|
|
2328
|
+
i = next;
|
|
2283
2329
|
}
|
|
2284
2330
|
parsed.command = argv2[i];
|
|
2285
2331
|
if (parsed.command === void 0) return parsed;
|
|
@@ -2335,12 +2381,19 @@ async function run(rawOptions = {}) {
|
|
|
2335
2381
|
const dir = resolveProductDir(config.product, rawOptions.dir ?? parsed.flags.dir);
|
|
2336
2382
|
const ui = screensFor(faceProduct(config), config.binName, render, rawOptions);
|
|
2337
2383
|
if ((parsed.command === "install" || parsed.command === "update") && !parsed.flags.version) recordSelf(dir);
|
|
2384
|
+
const leadingForeign = parsed.forward !== void 0 && parsed.unknownFlag === parsed.forward[0] && !parsed.flags.help && !parsed.flags.version;
|
|
2385
|
+
if (leadingForeign && entryFor(dir).target) {
|
|
2386
|
+
return doForward(config, dir, rawOptions, void 0, parsed.forward, ui, () => void 0);
|
|
2387
|
+
}
|
|
2338
2388
|
if (parsed.unknownFlag) return ui.refuse(`Unknown option: ${parsed.unknownFlag}.`, `${ui.bin} --help`, 2);
|
|
2339
2389
|
if (parsed.flags.version) {
|
|
2340
2390
|
print(renderVersion(ui.bin, readState(dir)?.version ?? "not installed", LAUNCHER_VERSION));
|
|
2341
2391
|
return 0;
|
|
2342
2392
|
}
|
|
2343
2393
|
const command2 = parsed.command;
|
|
2394
|
+
if (!parsed.flags.help && command2 === void 0 && entryFor(dir).target) {
|
|
2395
|
+
return doForward(config, dir, rawOptions, void 0, [], ui, () => void 0);
|
|
2396
|
+
}
|
|
2344
2397
|
if (parsed.flags.help || command2 === void 0) {
|
|
2345
2398
|
ui.show(usage(config, dir, render));
|
|
2346
2399
|
return parsed.flags.help ? 0 : 2;
|
|
@@ -2861,7 +2914,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2861
2914
|
let progressError;
|
|
2862
2915
|
const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
|
|
2863
2916
|
try {
|
|
2864
|
-
const result = await new Promise((
|
|
2917
|
+
const result = await new Promise((resolve4) => {
|
|
2865
2918
|
const child = (0, import_node_child_process5.spawn)(shell ? [command2, ...args].map(quoteForShell).join(" ") : command2, shell ? [] : args, {
|
|
2866
2919
|
cwd,
|
|
2867
2920
|
shell,
|
|
@@ -2918,8 +2971,8 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2918
2971
|
channel.setEncoding("utf8");
|
|
2919
2972
|
channel.on("data", receive);
|
|
2920
2973
|
}
|
|
2921
|
-
child.on("error", (error) =>
|
|
2922
|
-
child.on("close", (code) =>
|
|
2974
|
+
child.on("error", (error) => resolve4({ ok: false, error: error.message }));
|
|
2975
|
+
child.on("close", (code) => resolve4({
|
|
2923
2976
|
ok: code === 0,
|
|
2924
2977
|
...code !== null ? { code } : {},
|
|
2925
2978
|
...code !== 0 ? { error: `exit code ${code}` } : {}
|
|
@@ -2944,16 +2997,19 @@ function armState(arm) {
|
|
|
2944
2997
|
if (arm.verdict === "skip") return "kept";
|
|
2945
2998
|
return arm.from === arm.to ? "current" : "updated";
|
|
2946
2999
|
}
|
|
3000
|
+
function entryFor(dir) {
|
|
3001
|
+
const acquired = readState(dir)?.acquired;
|
|
3002
|
+
return { acquired, target: acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir) };
|
|
3003
|
+
}
|
|
2947
3004
|
function doForward(config, dir, options, command2, rest, ui, unlock) {
|
|
2948
|
-
if ((0, import_node_fs12.existsSync)(command2)) {
|
|
3005
|
+
if (command2 !== void 0 && (0, import_node_fs12.existsSync)(command2)) {
|
|
2949
3006
|
return ui.refuse(`${command2} is a file, not a command.`, `${ui.bin} --run ${command2}`, 2);
|
|
2950
3007
|
}
|
|
2951
|
-
const acquired =
|
|
2952
|
-
const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
|
|
3008
|
+
const { acquired, target } = entryFor(dir);
|
|
2953
3009
|
const verbs = readPayloadVerbs(dir);
|
|
2954
|
-
const declared = verbs === "*" || Array.isArray(verbs) && verbs.includes(command2);
|
|
3010
|
+
const declared = command2 === void 0 || verbs === "*" || Array.isArray(verbs) && verbs.includes(command2);
|
|
2955
3011
|
if (!target || !declared) return ui.refuse(`Unknown command: ${command2}.`, `${ui.bin} --help`, 2);
|
|
2956
|
-
const forwarded = [...resolveEntry(target), command2, ...rest];
|
|
3012
|
+
const forwarded = [...resolveEntry(target), ...command2 === void 0 ? [] : [command2], ...rest];
|
|
2957
3013
|
const cwd = payloadDir(dir);
|
|
2958
3014
|
const inherited = payloadEnv(dir);
|
|
2959
3015
|
const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
|
|
@@ -3052,6 +3108,8 @@ async function doStatus(config, dir, options, ui, print, json) {
|
|
|
3052
3108
|
function doDoctor(config, dir, options, ui) {
|
|
3053
3109
|
const local = localFacts(config, dir, options);
|
|
3054
3110
|
const verdict = verdictFor(ui, local, false, `${ui.bin} status`);
|
|
3111
|
+
const shadowFile = options.shadow?.file ?? ((0, import_node_sea2.isSea)() ? process.execPath : void 0);
|
|
3112
|
+
const shadow = shadowFile ? findPathShadow(config.binName, shadowFile, options.shadow ?? {}) : null;
|
|
3055
3113
|
ui.show(renderReport(ui.product, {
|
|
3056
3114
|
rows: [
|
|
3057
3115
|
{ surface: "launcher", state: LAUNCHER_VERSION, mark: "ok" },
|
|
@@ -3060,7 +3118,7 @@ function doDoctor(config, dir, options, ui) {
|
|
|
3060
3118
|
autoUpdateRow(local.schedule)
|
|
3061
3119
|
],
|
|
3062
3120
|
// Paths belong in the receipt, never a row.
|
|
3063
|
-
verdict: { ...verdict, facts: [`Folder: ${dir}`, `Release host: ${config.host}
|
|
3121
|
+
verdict: { ...verdict, facts: [`Folder: ${dir}`, `Release host: ${config.host}`, ...shadow ? [shadow.line] : []] }
|
|
3064
3122
|
}, ui.render));
|
|
3065
3123
|
const own = verdict.state === "fail" ? 1 : 0;
|
|
3066
3124
|
const acquired = readState(dir)?.acquired;
|
package/dist/refresh.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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, readdirSync, readFileSync as readFileSync2, realpathSync as realpathSync2, renameSync, rmSync, 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";
|
|
@@ -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
|
}
|
|
@@ -275,7 +314,8 @@ async function refreshLauncher(target, options = {}) {
|
|
|
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);
|
|
@@ -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