Programmatic API

The jup package exports runMain. Use it to run the jup CLI from JavaScript.

#Run the CLI

Call runMain(argv, options) to run the same code as the jup command. It returns an object with an exit code: { code }.

By default, runMain does not call process.exit() or take control of your process. You can call it from any part of a script, and you can call it more than once.

Pass the command-line arguments as an array. Do not include jup itself. This example runs the pnpm version chosen by the project:

import { runMain } from "jup";

const { code } = await runMain(["pnpm", "install"]);
process.exitCode = code;
// Same as: jup pnpm install

You can include a version in the tool name. This example runs a Node.js version that matches ^22, without using a shim:

import { runMain } from "jup";

const { code } = await runMain(["node@^22", "script.js", "--verbose"]);
process.exitCode = code;
// Same as: jup node@^22 script.js --verbose

You can also run jup commands:

const { code } = await runMain(["info", "--json"]);
// Same as: jup info --json

Arguments, input, output, errors, exit codes, and signals work like they do in the CLI. See the command reference.

By default, jup starts each tool in a child process. It does not change your process.argv or process.execArgv. Apart from the project environment file described below, it does not change process.env. The returned code is the tool's exit code. If signal N kills the tool, code is 128 + N. Your script keeps running.

const install = await runMain(["pnpm", "install"]);
if (install.code !== 0) console.error("install failed");

// This is still your script, so you can run another command.
await runMain(["pnpm", "test"]);

There is one environment detail to know. For each run, jup reads the nearest project environment file. It uses .jup.env, or .corepack.env when there is no .jup.env. It copies allowed values from that file into process.env. Existing environment variables take priority. See Settings for the list of allowed values.

These copied values stay in process.env. In a long-running process, values loaded for the first project may still be present when you run jup in another project.

jup handles bad user input in the same way as the CLI. For example, an invalid specification, an unknown tool, or a version blocked by the project returns { code: 1 }. jup writes the same message to stdout or stderr as the CLI. It throws only when an operation fails or jup has a bug.

#Let the tool take over the process

Set handover: true only when the jup run is the last thing your script needs to do. For a JavaScript tool, jup then runs the tool in the current process instead of starting a child process. The jup command and its shims use this mode. It lets a command such as pnpm run as one process instead of two.

const { code } = await runMain(["pnpm", "install"], { handover: true });
if (code !== 0) process.exitCode = code;

Do not use this option in the middle of a script. The tool takes control of the process. runMain returns 0 before the tool runs, and the tool sets the real exit code. If a signal kills the tool, it also kills your process.

Use this option when you are writing a small CLI wrapper and have no more work to do after the call.

#Other APIs

There are no other public entry points. Version selection, project discovery, specification parsing, and the store are internal. The package does not export any types. TypeScript still checks a call, because the option and result types come with the runMain signature.

You can do everything jup supports through runMain. For project information, jup info --json usually provides what a script needs.

If the CLI cannot do what your script needs, open an issue and describe what you are trying to build. Keeping the public API small makes it easier to add new features without breaking existing scripts.

jup  Pin and run the right package manager or runtime for every project.