@mutmutco/installer-launcher 1.2.4 → 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
 
@@ -687,6 +687,9 @@ import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
687
687
  import { appendFileSync, writeSync } from "node:fs";
688
688
 
689
689
  // ../face/src/products.ts
690
+ function closingLine(identity) {
691
+ return identity.open ? `Open it any time: ${identity.open}` : `Check health any time: ${identity.doctor}`;
692
+ }
690
693
  var PRODUCTS = Object.freeze({
691
694
  "mm-strategy": Object.freeze({
692
695
  name: "MM Strategy",
@@ -712,6 +715,16 @@ var PRODUCTS = Object.freeze({
712
715
  warm: "Welcome back. Checking for updates...",
713
716
  doctor: "jerv doctor"
714
717
  }),
718
+ // #7318: the MM-Work desktop app (a signed setup, not a launcher). Accent: owner pick 2026-09-25 (indigo).
719
+ "mm-work": Object.freeze({
720
+ name: "MM-Work",
721
+ installWarm: "Welcome. Let's get you ready.",
722
+ checkWarm: "Here's where things stand.",
723
+ accent: "38;2;129;140;248",
724
+ warm: "Welcome back. Checking for updates...",
725
+ doctor: "",
726
+ open: "Start menu > MM-Work"
727
+ }),
715
728
  jervcode: Object.freeze({
716
729
  name: "JervCode",
717
730
  installWarm: "Welcome. Let's get you ready.",
@@ -880,7 +893,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
880
893
  return receipt([
881
894
  `${GLYPH.check} ${identity.name} is ready.`,
882
895
  fact,
883
- `Check health any time: ${identity.doctor}`
896
+ closingLine(identity)
884
897
  ], options);
885
898
  };
886
899
  const signOff = () => nested ? "" : `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
@@ -1731,7 +1744,7 @@ function firstLine(text) {
1731
1744
 
1732
1745
  // src/login-github.ts
1733
1746
  import { spawn, spawnSync as spawnSync3 } from "node:child_process";
1734
- var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
1747
+ var realSleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
1735
1748
  function openBrowser(url) {
1736
1749
  if (process.env.LAUNCHER_NO_OPEN === "1") return;
1737
1750
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
@@ -1885,7 +1898,7 @@ async function loginGoogle(options) {
1885
1898
  const server = options.host.replace(/\/+$/, "");
1886
1899
  const timeoutMs = options.timeoutMs ?? 5 * 6e4;
1887
1900
  const listener = createServer();
1888
- 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));
1889
1902
  const port = listener.address().port;
1890
1903
  const redirectUri = `http://127.0.0.1:${port}/callback`;
1891
1904
  try {
@@ -1912,7 +1925,7 @@ async function loginGoogle(options) {
1912
1925
  code_challenge: challenge,
1913
1926
  code_challenge_method: "S256"
1914
1927
  }).toString();
1915
- const code = await new Promise((resolve3, reject) => {
1928
+ const code = await new Promise((resolve4, reject) => {
1916
1929
  const timer = setTimeout(() => reject(new Error("sign-in timed out \u2014 run login again.")), timeoutMs);
1917
1930
  listener.on("request", (req, res) => {
1918
1931
  const url = new URL(req.url ?? "/", redirectUri);
@@ -1930,7 +1943,7 @@ async function loginGoogle(options) {
1930
1943
  }
1931
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."));
1932
1945
  clearTimeout(timer);
1933
- resolve3(received);
1946
+ resolve4(received);
1934
1947
  });
1935
1948
  const print = options.print ?? ((line) => process.stderr.write(`${line}
1936
1949
  `));
@@ -2028,9 +2041,9 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
2028
2041
 
2029
2042
  // src/refresh.ts
2030
2043
  import { execFile } from "node:child_process";
2031
- 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";
2032
2045
  import { homedir } from "node:os";
2033
- 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";
2034
2047
  var LAUNCHER_RECORD_FILE = "launcher.json";
2035
2048
  var RECORD_TIMEOUT_MS = 1e4;
2036
2049
  function platformKey(platform, arch) {
@@ -2089,15 +2102,54 @@ function recordedPath(productDir) {
2089
2102
  function installedPath(target, productDir, platform) {
2090
2103
  return platform === "win32" ? join6(productDir, "bin", `${target.bin}.exe`) : join6(homedir(), ".local", "bin", target.bin);
2091
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
+ }
2092
2144
  function parseLauncherVersion(stdout, bin) {
2093
2145
  const text = String(stdout).trim();
2094
2146
  if (!text.startsWith(`${bin} `)) return null;
2095
2147
  return text.match(/launcher (\d+\.\d+\.\d+[^\s)]*)/u)?.[1] ?? null;
2096
2148
  }
2097
2149
  function runVersion(path, bin) {
2098
- return new Promise((resolve3) => {
2150
+ return new Promise((resolve4) => {
2099
2151
  execFile(path, ["--version"], { timeout: 3e4, windowsHide: true, encoding: "utf8" }, (error, stdout) => {
2100
- resolve3(error ? null : parseLauncherVersion(stdout, bin));
2152
+ resolve4(error ? null : parseLauncherVersion(stdout, bin));
2101
2153
  });
2102
2154
  });
2103
2155
  }
@@ -2129,7 +2181,8 @@ async function refreshLauncher(target, options = {}) {
2129
2181
  if (!options.dryRun) removeStale(file);
2130
2182
  const from = await (options.readVersion ?? ((path) => runVersion(path, target.bin)))(file);
2131
2183
  if (!from) return null;
2132
- 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 });
2133
2186
  const record = await fetchRecord(target, options);
2134
2187
  if (record === "bad-signature") return arm("fail", from, "The launcher record failed its signature check.");
2135
2188
  const entry2 = record?.files.find((candidate) => candidate.path === key);
@@ -2183,7 +2236,7 @@ async function refreshLauncher(target, options = {}) {
2183
2236
  }
2184
2237
 
2185
2238
  // src/index.ts
2186
- var LAUNCHER_VERSION = true ? "1.2.4" : readVersionFromPackage();
2239
+ var LAUNCHER_VERSION = true ? "1.3.0" : readVersionFromPackage();
2187
2240
  function defaultPrint(message) {
2188
2241
  process.stdout.write(`${message}
2189
2242
  `);
@@ -2233,7 +2286,13 @@ function parseArgv(argv2) {
2233
2286
  parsed.run = { file: argv2[i + 1], args: argv2.slice(i + 2) };
2234
2287
  return parsed;
2235
2288
  }
2236
- 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;
2237
2296
  }
2238
2297
  parsed.command = argv2[i];
2239
2298
  if (parsed.command === void 0) return parsed;
@@ -2289,12 +2348,19 @@ async function run(rawOptions = {}) {
2289
2348
  const dir = resolveProductDir(config.product, rawOptions.dir ?? parsed.flags.dir);
2290
2349
  const ui = screensFor(faceProduct(config), config.binName, render, rawOptions);
2291
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
+ }
2292
2355
  if (parsed.unknownFlag) return ui.refuse(`Unknown option: ${parsed.unknownFlag}.`, `${ui.bin} --help`, 2);
2293
2356
  if (parsed.flags.version) {
2294
2357
  print(renderVersion(ui.bin, readState(dir)?.version ?? "not installed", LAUNCHER_VERSION));
2295
2358
  return 0;
2296
2359
  }
2297
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
+ }
2298
2364
  if (parsed.flags.help || command2 === void 0) {
2299
2365
  ui.show(usage(config, dir, render));
2300
2366
  return parsed.flags.help ? 0 : 2;
@@ -2384,7 +2450,7 @@ async function runFile(file, args, printErr) {
2384
2450
  printErr("launcher --run needs a file to run.");
2385
2451
  return 1;
2386
2452
  }
2387
- const abs = resolve2(process.cwd(), file);
2453
+ const abs = resolve3(process.cwd(), file);
2388
2454
  if (!existsSync8(abs)) {
2389
2455
  printErr(`cannot run ${file}: no such file.`);
2390
2456
  return 1;
@@ -2740,7 +2806,7 @@ function payloadFileAsCommand(entry2, payload) {
2740
2806
  if (NEVER_A_PROGRAM.test(first)) return first;
2741
2807
  if (process.platform === "win32") return null;
2742
2808
  try {
2743
- return (statSync(candidate).mode & 73) === 0 ? first : null;
2809
+ return (statSync2(candidate).mode & 73) === 0 ? first : null;
2744
2810
  } catch {
2745
2811
  return null;
2746
2812
  }
@@ -2815,7 +2881,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2815
2881
  let progressError;
2816
2882
  const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
2817
2883
  try {
2818
- const result = await new Promise((resolve3) => {
2884
+ const result = await new Promise((resolve4) => {
2819
2885
  const child = spawn2(shell ? [command2, ...args].map(quoteForShell).join(" ") : command2, shell ? [] : args, {
2820
2886
  cwd,
2821
2887
  shell,
@@ -2872,8 +2938,8 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2872
2938
  channel.setEncoding("utf8");
2873
2939
  channel.on("data", receive);
2874
2940
  }
2875
- child.on("error", (error) => resolve3({ ok: false, error: error.message }));
2876
- child.on("close", (code) => resolve3({
2941
+ child.on("error", (error) => resolve4({ ok: false, error: error.message }));
2942
+ child.on("close", (code) => resolve4({
2877
2943
  ok: code === 0,
2878
2944
  ...code !== null ? { code } : {},
2879
2945
  ...code !== 0 ? { error: `exit code ${code}` } : {}
@@ -2898,16 +2964,19 @@ function armState(arm) {
2898
2964
  if (arm.verdict === "skip") return "kept";
2899
2965
  return arm.from === arm.to ? "current" : "updated";
2900
2966
  }
2967
+ function entryFor(dir) {
2968
+ const acquired = readState(dir)?.acquired;
2969
+ return { acquired, target: acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir) };
2970
+ }
2901
2971
  function doForward(config, dir, options, command2, rest, ui, unlock) {
2902
- if (existsSync8(command2)) {
2972
+ if (command2 !== void 0 && existsSync8(command2)) {
2903
2973
  return ui.refuse(`${command2} is a file, not a command.`, `${ui.bin} --run ${command2}`, 2);
2904
2974
  }
2905
- const acquired = readState(dir)?.acquired;
2906
- const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
2975
+ const { acquired, target } = entryFor(dir);
2907
2976
  const verbs = readPayloadVerbs(dir);
2908
- const declared = verbs === "*" || Array.isArray(verbs) && verbs.includes(command2);
2977
+ const declared = command2 === void 0 || verbs === "*" || Array.isArray(verbs) && verbs.includes(command2);
2909
2978
  if (!target || !declared) return ui.refuse(`Unknown command: ${command2}.`, `${ui.bin} --help`, 2);
2910
- const forwarded = [...resolveEntry(target), command2, ...rest];
2979
+ const forwarded = [...resolveEntry(target), ...command2 === void 0 ? [] : [command2], ...rest];
2911
2980
  const cwd = payloadDir(dir);
2912
2981
  const inherited = payloadEnv(dir);
2913
2982
  const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
@@ -3006,6 +3075,8 @@ async function doStatus(config, dir, options, ui, print, json) {
3006
3075
  function doDoctor(config, dir, options, ui) {
3007
3076
  const local = localFacts(config, dir, options);
3008
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;
3009
3080
  ui.show(renderReport(ui.product, {
3010
3081
  rows: [
3011
3082
  { surface: "launcher", state: LAUNCHER_VERSION, mark: "ok" },
@@ -3014,7 +3085,7 @@ function doDoctor(config, dir, options, ui) {
3014
3085
  autoUpdateRow(local.schedule)
3015
3086
  ],
3016
3087
  // Paths belong in the receipt, never a row.
3017
- verdict: { ...verdict, facts: [`Folder: ${dir}`, `Release host: ${config.host}`] }
3088
+ verdict: { ...verdict, facts: [`Folder: ${dir}`, `Release host: ${config.host}`, ...shadow ? [shadow.line] : []] }
3018
3089
  }, ui.render));
3019
3090
  const own = verdict.state === "fail" ? 1 : 0;
3020
3091
  const acquired = readState(dir)?.acquired;
@@ -720,6 +720,9 @@ var import_node_url2 = require("node:url");
720
720
  var import_node_fs6 = require("node:fs");
721
721
 
722
722
  // ../face/src/products.ts
723
+ function closingLine(identity) {
724
+ return identity.open ? `Open it any time: ${identity.open}` : `Check health any time: ${identity.doctor}`;
725
+ }
723
726
  var PRODUCTS = Object.freeze({
724
727
  "mm-strategy": Object.freeze({
725
728
  name: "MM Strategy",
@@ -745,6 +748,16 @@ var PRODUCTS = Object.freeze({
745
748
  warm: "Welcome back. Checking for updates...",
746
749
  doctor: "jerv doctor"
747
750
  }),
751
+ // #7318: the MM-Work desktop app (a signed setup, not a launcher). Accent: owner pick 2026-09-25 (indigo).
752
+ "mm-work": Object.freeze({
753
+ name: "MM-Work",
754
+ installWarm: "Welcome. Let's get you ready.",
755
+ checkWarm: "Here's where things stand.",
756
+ accent: "38;2;129;140;248",
757
+ warm: "Welcome back. Checking for updates...",
758
+ doctor: "",
759
+ open: "Start menu > MM-Work"
760
+ }),
748
761
  jervcode: Object.freeze({
749
762
  name: "JervCode",
750
763
  installWarm: "Welcome. Let's get you ready.",
@@ -913,7 +926,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
913
926
  return receipt([
914
927
  `${GLYPH.check} ${identity.name} is ready.`,
915
928
  fact,
916
- `Check health any time: ${identity.doctor}`
929
+ closingLine(identity)
917
930
  ], options);
918
931
  };
919
932
  const signOff = () => nested ? "" : `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
@@ -1764,7 +1777,7 @@ function firstLine(text) {
1764
1777
 
1765
1778
  // src/login-github.ts
1766
1779
  var import_node_child_process3 = require("node:child_process");
1767
- var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
1780
+ var realSleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
1768
1781
  function openBrowser(url) {
1769
1782
  if (process.env.LAUNCHER_NO_OPEN === "1") return;
1770
1783
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
@@ -1918,7 +1931,7 @@ async function loginGoogle(options) {
1918
1931
  const server = options.host.replace(/\/+$/, "");
1919
1932
  const timeoutMs = options.timeoutMs ?? 5 * 6e4;
1920
1933
  const listener = (0, import_node_http.createServer)();
1921
- 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));
1922
1935
  const port = listener.address().port;
1923
1936
  const redirectUri = `http://127.0.0.1:${port}/callback`;
1924
1937
  try {
@@ -1945,7 +1958,7 @@ async function loginGoogle(options) {
1945
1958
  code_challenge: challenge,
1946
1959
  code_challenge_method: "S256"
1947
1960
  }).toString();
1948
- const code = await new Promise((resolve3, reject) => {
1961
+ const code = await new Promise((resolve4, reject) => {
1949
1962
  const timer = setTimeout(() => reject(new Error("sign-in timed out \u2014 run login again.")), timeoutMs);
1950
1963
  listener.on("request", (req, res) => {
1951
1964
  const url = new URL(req.url ?? "/", redirectUri);
@@ -1963,7 +1976,7 @@ async function loginGoogle(options) {
1963
1976
  }
1964
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."));
1965
1978
  clearTimeout(timer);
1966
- resolve3(received);
1979
+ resolve4(received);
1967
1980
  });
1968
1981
  const print = options.print ?? ((line) => process.stderr.write(`${line}
1969
1982
  `));
@@ -2122,15 +2135,54 @@ function recordedPath(productDir) {
2122
2135
  function installedPath(target, productDir, platform) {
2123
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);
2124
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
+ }
2125
2177
  function parseLauncherVersion(stdout, bin) {
2126
2178
  const text = String(stdout).trim();
2127
2179
  if (!text.startsWith(`${bin} `)) return null;
2128
2180
  return text.match(/launcher (\d+\.\d+\.\d+[^\s)]*)/u)?.[1] ?? null;
2129
2181
  }
2130
2182
  function runVersion(path, bin) {
2131
- return new Promise((resolve3) => {
2183
+ return new Promise((resolve4) => {
2132
2184
  (0, import_node_child_process4.execFile)(path, ["--version"], { timeout: 3e4, windowsHide: true, encoding: "utf8" }, (error, stdout) => {
2133
- resolve3(error ? null : parseLauncherVersion(stdout, bin));
2185
+ resolve4(error ? null : parseLauncherVersion(stdout, bin));
2134
2186
  });
2135
2187
  });
2136
2188
  }
@@ -2162,7 +2214,8 @@ async function refreshLauncher(target, options = {}) {
2162
2214
  if (!options.dryRun) removeStale(file);
2163
2215
  const from = await (options.readVersion ?? ((path) => runVersion(path, target.bin)))(file);
2164
2216
  if (!from) return null;
2165
- 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 });
2166
2219
  const record = await fetchRecord(target, options);
2167
2220
  if (record === "bad-signature") return arm("fail", from, "The launcher record failed its signature check.");
2168
2221
  const entry2 = record?.files.find((candidate) => candidate.path === key);
@@ -2216,7 +2269,7 @@ async function refreshLauncher(target, options = {}) {
2216
2269
  }
2217
2270
 
2218
2271
  // src/index.ts
2219
- var LAUNCHER_VERSION = true ? "1.2.4" : readVersionFromPackage();
2272
+ var LAUNCHER_VERSION = true ? "1.3.0" : readVersionFromPackage();
2220
2273
  function defaultPrint(message) {
2221
2274
  process.stdout.write(`${message}
2222
2275
  `);
@@ -2266,7 +2319,13 @@ function parseArgv(argv2) {
2266
2319
  parsed.run = { file: argv2[i + 1], args: argv2.slice(i + 2) };
2267
2320
  return parsed;
2268
2321
  }
2269
- 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;
2270
2329
  }
2271
2330
  parsed.command = argv2[i];
2272
2331
  if (parsed.command === void 0) return parsed;
@@ -2322,12 +2381,19 @@ async function run(rawOptions = {}) {
2322
2381
  const dir = resolveProductDir(config.product, rawOptions.dir ?? parsed.flags.dir);
2323
2382
  const ui = screensFor(faceProduct(config), config.binName, render, rawOptions);
2324
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
+ }
2325
2388
  if (parsed.unknownFlag) return ui.refuse(`Unknown option: ${parsed.unknownFlag}.`, `${ui.bin} --help`, 2);
2326
2389
  if (parsed.flags.version) {
2327
2390
  print(renderVersion(ui.bin, readState(dir)?.version ?? "not installed", LAUNCHER_VERSION));
2328
2391
  return 0;
2329
2392
  }
2330
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
+ }
2331
2397
  if (parsed.flags.help || command2 === void 0) {
2332
2398
  ui.show(usage(config, dir, render));
2333
2399
  return parsed.flags.help ? 0 : 2;
@@ -2848,7 +2914,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2848
2914
  let progressError;
2849
2915
  const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
2850
2916
  try {
2851
- const result = await new Promise((resolve3) => {
2917
+ const result = await new Promise((resolve4) => {
2852
2918
  const child = (0, import_node_child_process5.spawn)(shell ? [command2, ...args].map(quoteForShell).join(" ") : command2, shell ? [] : args, {
2853
2919
  cwd,
2854
2920
  shell,
@@ -2905,8 +2971,8 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2905
2971
  channel.setEncoding("utf8");
2906
2972
  channel.on("data", receive);
2907
2973
  }
2908
- child.on("error", (error) => resolve3({ ok: false, error: error.message }));
2909
- child.on("close", (code) => resolve3({
2974
+ child.on("error", (error) => resolve4({ ok: false, error: error.message }));
2975
+ child.on("close", (code) => resolve4({
2910
2976
  ok: code === 0,
2911
2977
  ...code !== null ? { code } : {},
2912
2978
  ...code !== 0 ? { error: `exit code ${code}` } : {}
@@ -2931,16 +2997,19 @@ function armState(arm) {
2931
2997
  if (arm.verdict === "skip") return "kept";
2932
2998
  return arm.from === arm.to ? "current" : "updated";
2933
2999
  }
3000
+ function entryFor(dir) {
3001
+ const acquired = readState(dir)?.acquired;
3002
+ return { acquired, target: acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir) };
3003
+ }
2934
3004
  function doForward(config, dir, options, command2, rest, ui, unlock) {
2935
- if ((0, import_node_fs12.existsSync)(command2)) {
3005
+ if (command2 !== void 0 && (0, import_node_fs12.existsSync)(command2)) {
2936
3006
  return ui.refuse(`${command2} is a file, not a command.`, `${ui.bin} --run ${command2}`, 2);
2937
3007
  }
2938
- const acquired = readState(dir)?.acquired;
2939
- const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
3008
+ const { acquired, target } = entryFor(dir);
2940
3009
  const verbs = readPayloadVerbs(dir);
2941
- const declared = verbs === "*" || Array.isArray(verbs) && verbs.includes(command2);
3010
+ const declared = command2 === void 0 || verbs === "*" || Array.isArray(verbs) && verbs.includes(command2);
2942
3011
  if (!target || !declared) return ui.refuse(`Unknown command: ${command2}.`, `${ui.bin} --help`, 2);
2943
- const forwarded = [...resolveEntry(target), command2, ...rest];
3012
+ const forwarded = [...resolveEntry(target), ...command2 === void 0 ? [] : [command2], ...rest];
2944
3013
  const cwd = payloadDir(dir);
2945
3014
  const inherited = payloadEnv(dir);
2946
3015
  const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
@@ -3039,6 +3108,8 @@ async function doStatus(config, dir, options, ui, print, json) {
3039
3108
  function doDoctor(config, dir, options, ui) {
3040
3109
  const local = localFacts(config, dir, options);
3041
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;
3042
3113
  ui.show(renderReport(ui.product, {
3043
3114
  rows: [
3044
3115
  { surface: "launcher", state: LAUNCHER_VERSION, mark: "ok" },
@@ -3047,7 +3118,7 @@ function doDoctor(config, dir, options, ui) {
3047
3118
  autoUpdateRow(local.schedule)
3048
3119
  ],
3049
3120
  // Paths belong in the receipt, never a row.
3050
- verdict: { ...verdict, facts: [`Folder: ${dir}`, `Release host: ${config.host}`] }
3121
+ verdict: { ...verdict, facts: [`Folder: ${dir}`, `Release host: ${config.host}`, ...shadow ? [shadow.line] : []] }
3051
3122
  }, ui.render));
3052
3123
  const own = verdict.state === "fail" ? 1 : 0;
3053
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.4",
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",