@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 +1 -0
- package/dist/launcher.js +96 -25
- package/dist/launcher.sea.cjs +90 -19
- 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
|
|
|
@@ -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
|
-
|
|
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((
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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((
|
|
2150
|
+
return new Promise((resolve4) => {
|
|
2099
2151
|
execFile(path, ["--version"], { timeout: 3e4, windowsHide: true, encoding: "utf8" }, (error, stdout) => {
|
|
2100
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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((
|
|
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) =>
|
|
2876
|
-
child.on("close", (code) =>
|
|
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 =
|
|
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;
|
package/dist/launcher.sea.cjs
CHANGED
|
@@ -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
|
-
|
|
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((
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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((
|
|
2183
|
+
return new Promise((resolve4) => {
|
|
2132
2184
|
(0, import_node_child_process4.execFile)(path, ["--version"], { timeout: 3e4, windowsHide: true, encoding: "utf8" }, (error, stdout) => {
|
|
2133
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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((
|
|
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) =>
|
|
2909
|
-
child.on("close", (code) =>
|
|
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 =
|
|
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((
|
|
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