Ctrl K
ComunidadGuías…

lib/cli-launch.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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();
}