@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 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 resolve2 } from "node:path";
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((resolve3) => setTimeout(resolve3, ms));
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((resolve3) => listener.listen(0, "127.0.0.1", resolve3));
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((resolve3, reject) => {
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
- resolve3(received);
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((resolve3) => {
2150
+ return new Promise((resolve4) => {
2112
2151
  execFile(path, ["--version"], { timeout: 3e4, windowsHide: true, encoding: "utf8" }, (error, stdout) => {
2113
- resolve3(error ? null : parseLauncherVersion(stdout, bin));
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 arm = (verdict, to, detail) => ({ surface: "launcher", from, to, verdict, detail, durationMs: Date.now() - started });
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.2.5" : readVersionFromPackage();
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
- i = skip(i);
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 = resolve2(process.cwd(), file);
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 (statSync(candidate).mode & 73) === 0 ? first : null;
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((resolve3) => {
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) => resolve3({ ok: false, error: error.message }));
2889
- child.on("close", (code) => resolve3({
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 = readState(dir)?.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;
@@ -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((resolve3) => setTimeout(resolve3, ms));
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((resolve3) => listener.listen(0, "127.0.0.1", resolve3));
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((resolve3, reject) => {
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
- resolve3(received);
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((resolve3) => {
2183
+ return new Promise((resolve4) => {
2145
2184
  (0, import_node_child_process4.execFile)(path, ["--version"], { timeout: 3e4, windowsHide: true, encoding: "utf8" }, (error, stdout) => {
2146
- resolve3(error ? null : parseLauncherVersion(stdout, bin));
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 arm = (verdict, to, detail) => ({ surface: "launcher", from, to, verdict, detail, durationMs: Date.now() - started });
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.2.5" : readVersionFromPackage();
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
- i = skip(i);
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((resolve3) => {
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) => resolve3({ ok: false, error: error.message }));
2922
- child.on("close", (code) => resolve3({
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 = readState(dir)?.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((resolve2) => {
283
+ return new Promise((resolve3) => {
245
284
  execFile(path, ["--version"], { timeout: 3e4, windowsHide: true, encoding: "utf8" }, (error, stdout) => {
246
- resolve2(error ? null : parseLauncherVersion(stdout, bin));
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 arm = (verdict, to, detail) => ({ surface: "launcher", from, to, verdict, detail, durationMs: Date.now() - started });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/installer-launcher",
3
- "version": "1.2.5",
3
+ "version": "1.3.0",
4
4
  "description": "Single-executable (SEA) launcher: sign-in, gated payload fetch, self-update. One copy ships per product.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",