lib/cli-launch.mjs
Rama main · réplica de lectura del repositorio original.
import fs from "node:fs";
import path from "node:path";
/**
* Prepare a CLI process without routing user-controlled prompts through a shell.
*
* npm installs an extensionless POSIX shim plus `.cmd` and `.ps1` wrappers on
* Windows. `findBin()` can discover the extensionless shim, but CreateProcess
* cannot execute it (`ENOENT`); `.cmd` also requires a shell (`EINVAL`). npm's
* generated PowerShell wrapper names the real JS/native entrypoint immediately
* before `$args`. Resolve that trusted local target and spawn it directly, so
* prompts keep their argument boundaries without `shell: true` or PowerShell's
* lossy `-File` argument conversion.
*
* @param {string} binPath
* @param {string[]} args
* @param {string} [platform]
* @returns {{ command: string, args: string[] }}
* @throws {Error} On Windows, for a .cmd/.bat/.ps1 wrapper it cannot resolve.
*/
export function prepareCliLaunch(binPath, args, platform = process.platform) {
if (platform !== "win32") return { command: binPath, args };
const ext = path.extname(binPath).toLowerCase();
if (ext && ![".cmd", ".bat", ".ps1"].includes(ext)) return { command: binPath, args };
const shimBase = ext ? binPath.slice(0, -ext.length) : binPath;
const ps1Shim = `${shimBase}.ps1`;
// Nothing resolved. An extensionless path is handed back unchanged (spawn()
// may still find a .exe beside it), but a .cmd/.bat/.ps1 needs a shell, which
// is what this avoids: spawned as-is it only fails (EINVAL for .cmd/.bat), so
// refuse it with the reason instead.
const unresolved = () => {
if (!ext) return { command: binPath, args };
throw new Error(
`Cannot launch ${binPath} without a shell: no npm PowerShell shim beside it names the CLI's real .js or .exe. ` +
"Reinstall the CLI with npm, or put its native .exe on PATH.",
);
};
if (!fs.existsSync(ps1Shim)) return unresolved();
let wrapper = "";
try {
wrapper = fs.readFileSync(ps1Shim, "utf8");
} catch {
return unresolved();
}
const targetMatches = wrapper.matchAll(/["']\$basedir[\\/]([^"']+)["']\s+\$args/g);
for (const match of targetMatches) {
const target = path.resolve(path.dirname(ps1Shim), match[1].replace(/[\\/]/g, path.sep));
if (!fs.existsSync(target)) continue;
const targetExt = path.extname(target).toLowerCase();
if ([".js", ".cjs", ".mjs"].includes(targetExt)) {
return { command: process.execPath, args: [target, ...args] };
}
if ([".exe", ".com"].includes(targetExt)) {
return { command: target, args };
}
}
return unresolved();
}