// npm 11.7.0's exec.js and libnpmexec (lib/index.js, run-script.js), behind `npm exec` // and npx: run args[0], or `-c`'s command, with a bin dir in front of PATH. The bin is a // local one (node_modules/.bin from the project up, then /usr/local/bin), else its // package installs into the npx cache, ~/.npm/_npx/, through the kernel's // installer, after npm's warning (no tty) or prompt (a tty) unless --yes. The kernel // answers which packages are missing (src/kernel/npm/exec.ts). Guest code. import { createHash } from "node:crypto"; import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { dirname, resolve } from "node:path"; import process from "node:process"; import type { ExecLookup, ExecLookupRequest, ExecPackage, InstallOutcome, InstallRequest } from "../shared/protocol"; import type { Command } from "./command"; import { EXEC_USAGE, logLines, NpmFailure } from "./output"; import { exitLike, readPackage, runScript, type Pkg } from "./run-script"; /** The npm hooks the runtime installs at Symbol.for("nib.runtime"). */ export interface RuntimeHooks { npm(request: InstallRequest): Promise; npmLookup(request: ExecLookupRequest): Promise; } const GLOBAL_BIN = "/usr/local/bin"; const isFile = (path: string): boolean => { try { return statSync(path).isFile(); } catch { return false; } }; /** file-exists.js localFileExists: the first node_modules/.bin from `dir` up holding `name`. */ function localBin(dir: string, name: string): string | null { for (let p = dir, pp = ""; p !== pp; pp = p, p = dirname(p)) { const bin = resolve(p, "node_modules", ".bin"); if (isFile(resolve(bin, name))) return bin; } return null; } /** get-bin-from-manifest.js: the one bin, or the one named for the package. */ function binOf(pkg: ExecPackage): string { const names = Object.keys(pkg.bin); if (new Set(Object.values(pkg.bin)).size === 1) return names[0]!; const name = pkg.name.replace(/^@[^/]+\//, ""); if (pkg.bin[name]) return name; throw new NpmFailure({ message: "could not determine executable to run" }); } /** The prompt libnpmexec reads on a tty, answered by the first line typed. */ function confirm(addList: string[]): Promise { process.stdout.write(`Need to install the following packages:\n${addList.join("\n")}\nOk to proceed? (y) `); return new Promise((done) => { let typed = ""; const onData = (chunk: unknown): void => { typed += String(chunk); if (!typed.includes("\n")) return; process.stdin.removeListener("data", onData); process.stdin.pause(); const answer = typed.split("\n")[0]!.trim().toLowerCase() || "y"; done(answer.charAt(0) === "y"); }; process.stdin.on("data", onData); process.stdin.resume(); }); } export async function exec(command: Extract, prefix: string, hooks: RuntimeHooks): Promise { const { call, yes } = command; const args = [...command.args]; const packages = [...command.packages]; if (call && args.length) throw new NpmFailure({ code: "EUSAGE", message: `\n${EXEC_USAGE}` }); if (!call && !args.length && !packages.length) { throw new NpmFailure({ code: "ENOTSUP", message: "`npm exec` with nothing to run opens a shell, and this runtime has none" }); } const lookup = async (spec: string, path: string, npx: boolean): Promise> => { const outcome = await hooks.npmLookup({ spec, path, npx }); if (!outcome.ok) throw new NpmFailure(outcome.error); return outcome; }; const binPaths: string[] = []; const runPath = process.cwd(); const run = async (): Promise => { if (args.length) args[0] = `"${args[0]}"`; const script = call || args.shift()!; let real: Pkg = {}; try { real = readPackage(prefix); } catch { // libnpmexec runs without a package.json too. } const pkg = { ...real, scripts: { ...real.scripts, npx: script } }; exitLike(await runScript({ pkg, path: runPath, event: "npx", cmd: script, args, binPaths, banner: false })); }; const swap = args.length > 0 && packages.length === 0; if (swap) { let own: Pkg = {}; try { own = readPackage(prefix); } catch { // No package.json: no bin of its own. } const ownBin = own.bin?.[args[0]!]; if (ownBin) { // npm installs the project itself into the npx cache to link this bin; here it // runs from where it is. args.splice(0, 1, "node", resolve(prefix, ownBin)); return run(); } const local = localBin(prefix, args[0]!); if (local) { binPaths.push(local); return run(); } if (isFile(`${GLOBAL_BIN}/${args[0]}`)) { binPaths.push(GLOBAL_BIN); return run(); } packages.push(args[0]!); } const needInstall: string[] = []; let commandPkg: ExecPackage | undefined; await Promise.all(packages.map(async (spec, i) => { const { found, pkg } = await lookup(spec, prefix, false); if (!found) needInstall.push(spec); if (i === 0) commandPkg = pkg; })); if (swap) args[0] = binOf(commandPkg!); if (needInstall.length) { const hash = createHash("sha512").update([...packages].sort((a, b) => a.localeCompare(b, "en")).join("\n")).digest("hex").slice(0, 16); const installDir = resolve(homedir(), ".npm", "_npx", hash); mkdirSync(installDir, { recursive: true }); const add: string[] = []; for (const spec of needInstall) { const { found, pkg } = await lookup(spec, installDir, true); if (!found) add.push(`${pkg.name}@${pkg.version}`); } if (add.length) { if (!yes) { if (yes === false) throw new NpmFailure({ message: `npx canceled due to missing packages and no YES option: ${JSON.stringify(add)}` }); if (!process.stdin.isTTY) { process.stderr.write(logLines("warn", "exec", `The following package${add.length === 1 ? " was" : "s were"} not found and will be installed: ${add.join(", ")}`)); } else if (!(await confirm(add))) { throw new NpmFailure({ message: "canceled" }); } } const outcome = await hooks.npm({ command: "install", cwd: installDir, add, save: { type: null, exact: false } }); if (!outcome.ok) throw new NpmFailure(outcome.error); for (const w of outcome.report.warnings) process.stderr.write(logLines("warn", "", w)); } binPaths.push(resolve(installDir, "node_modules/.bin")); const pkgPath = resolve(installDir, "package.json"); const pkgJson = JSON.parse(readFileSync(pkgPath, "utf8")) as Record; pkgJson._npx = { packages }; writeFileSync(pkgPath, `${JSON.stringify(pkgJson, null, 2)}\n`); } return run(); }