@neotales/exec
v0.0.0-alpha.0
Published
The exec module makes it easy to spawn child_processes across different runtimes and different operating systems.
Maintainers
Readme
@neotales/exec
Overview
The exec module makes it easy to spawn child_processes across different runtimes (NodeJS, Bun, Deno) and different operating systems (Windows, Linux, Mac) and include additional utilities like splatting arguments and looking up executables on the path.

Documentation
Documentation is available on jsr.io
A list of other modules can be found at github.com/neotales/js-std
Installation
# Deno
deno add jsr:@neotales/exec
# npm from jsr
npx jsr add @neotales/exec
# from npmjs.org
npm install @neotales/execQuick Start
import { cmd, exec, which, run } from "@neotales/exec";
// Find an executable on the PATH
const gitPath = await which("git");
console.log(gitPath); // "/usr/bin/git"
// Execute a command and get output
const output = await exec(["git", "status"]);
console.log(output.text());
// Create a command with fluent API
const result = await cmd(["ls", "-la"]).withCwd("/tmp").output();
console.log(result.lines());
// Run command with inherited stdout/stderr (visible in terminal)
await run(["npm", "install"]);API Reference
Classes
| Class | Description |
| --------------------- | ----------------------------------------------------------------------- |
| Command | A command/syscall that spawns a child process |
| ShellCommand | A command that spawns a child process for shells (pwsh, bash, sh, etc.) |
| PathFinder | A registry of executable paths with cross-platform support |
| CommandError | Error thrown when a command fails |
| NotFoundOnPathError | Error thrown when an executable is not found on PATH |
Command Execution Functions
| Function | Description |
| -------------------------- | --------------------------------------------------------- |
| cmd(args, options?) | Creates a new Command instance with piped stdout/stderr |
| exec(args, options?) | Executes a command and returns the output as a Promise |
| execSync(args, options?) | Executes a command synchronously |
| run(args, options?) | Runs a command with inherited stdout/stderr |
| runSync(args, options?) | Runs a command synchronously with inherited stdout/stderr |
| spawn(args, options?) | Spawns a child process and returns the ChildProcess |
import { cmd, exec, execSync, run, spawn } from "@neotales/exec";
// cmd - Create a command with fluent API
const text = await cmd(["echo", "hello"]).text();
console.log(text); // "hello\n"
// exec - Execute and get output directly
const output = await exec(["git", "config", "--list"]);
console.log(output.lines());
// execSync - Synchronous execution
const syncOutput = execSync(["echo", "sync"]);
console.log(syncOutput.code); // 0
// run - Execute with inherited stdout/stderr
await run(["npm", "install"]);
// spawn - Long-running processes
await using process = spawn(["node", "server.js"]);
console.log("Server PID:", process.pid);Path Lookup Functions
| Function | Description |
| ---------------------------------------------- | -------------------------------------------------------- |
| which(fileName, prependPath?, useCache?) | Finds the first executable path asynchronously |
| whichSync(fileName, prependPath?, useCache?) | Finds the first executable path synchronously |
| whichAll(fileName, prependPath?) | Finds every executable path or glob match asynchronously |
| whichAllSync(fileName, prependPath?) | Finds every executable path or glob match synchronously |
import { which, whichAll, whichSync } from "@neotales/exec";
// Async lookup
const gitPath = await which("git");
console.log(gitPath); // "/usr/bin/git" or undefined
// Sync lookup
const nodePath = whichSync("node");
console.log(nodePath); // "/usr/bin/node"
// With additional search paths
const customPath = await which("my-tool", ["/opt/tools/bin"]);
// Disable caching for fresh lookup
const freshPath = await which("deno", undefined, false);
// Find every PATH match, including glob matches
const nodePaths = await whichAll("node*");Logging Functions
| Function | Description |
| ---------------- | ------------------------------------------ |
| setLogger(fn?) | Sets a global logger for command execution |
| getLogger() | Gets the current logger function |
import { setLogger, cmd } from "@neotales/exec";
// Log all executed commands
setLogger((file, args) => {
console.log(`Executing: ${file} ${args?.join(" ") ?? ""}`);
});
await cmd(["git", "status"]).output();
// Output: "Executing: /usr/bin/git status"
// Disable logging
setLogger(undefined);Utility Functions
| Function | Description |
| -------------------------- | ------------------------------------------------- |
| convertCommandArgs(args) | Converts string, array, or object to string array |
import { convertCommandArgs } from "@neotales/exec";
// String with spaces
convertCommandArgs("git commit -m 'hello'");
// ["git", "commit", "-m", "hello"]
// Object (splat)
convertCommandArgs({ verbose: true, count: 5 });
// ["--verbose", "--count", "5"]
// Array (pass-through)
convertCommandArgs(["git", "status"]);
// ["git", "status"]Argument Builder
ArgsBuilder constructs command arguments without shell interpolation.
import { ArgsBuilder } from "@neotales/exec";
const args = new ArgsBuilder({ assign: "=" })
.subcommand("git", "commit")
.flag("verbose")
.option("message", "release notes")
.postArgs("--amend")
.build();
// ["git", "commit", "--verbose", "--message=\"release notes\"", "--", "--amend"]Command Class Methods
| Method | Description |
| -------------------- | -------------------------------------------------- |
| output() | Execute and return Output promise |
| outputSync() | Execute synchronously and return Output |
| spawn() | Spawn child process and return ChildProcess |
| run() | Execute with inherited stdout/stderr |
| runSync() | Execute synchronously with inherited stdout/stderr |
| text() | Get stdout as string |
| lines() | Get stdout as string array |
| json() | Parse stdout as JSON |
| pipe(args) | Pipe output to another command |
| withCwd(path) | Set working directory |
| withEnv(env) | Set environment variables |
| withStdin(mode) | Set stdin mode (inherit/piped/null) |
| withStdout(mode) | Set stdout mode (inherit/piped/null) |
| withStderr(mode) | Set stderr mode (inherit/piped/null) |
| withSignal(signal) | Set AbortSignal for cancellation |
import { cmd, Command } from "@neotales/exec";
// Fluent API
const result = await cmd(["ls", "-la"])
.withCwd("/home/user")
.withEnv({ LANG: "en_US.UTF-8" })
.withStdout("piped")
.output();
// Get output in different formats
const text = await cmd(["cat", "file.txt"]).text();
const lines = await cmd(["ls"]).lines();
const data = await cmd(["echo", '{"key": "value"}']).json();
// Pipe commands together
const piped = await cmd(["echo", "hello world"]).pipe(["grep", "hello"]).pipe("cat").output();
console.log(piped.text()); // "hello world\n"
// Await command directly
const output = await new Command(["echo", "test"]);
console.log(output.text());Output Interface
| Property/Method | Description |
| ------------------------------ | ------------------------------------ |
| code | Exit code of the command |
| success | Boolean indicating if code === 0 |
| signal | Signal that terminated the process |
| stdout | Raw stdout as Uint8Array |
| stderr | Raw stderr as Uint8Array |
| text() | Get stdout as string |
| lines() | Get stdout as string array |
| json() | Parse stdout as JSON |
| errorText() | Get stderr as string |
| errorLines() | Get stderr as string array |
| errorJson() | Parse stderr as JSON |
| validate(fn?, failOnStderr?) | Validate output and throw on failure |
import { exec } from "@neotales/exec";
const output = await exec(["git", "status"]);
console.log(output.code); // 0
console.log(output.success); // true
console.log(output.text()); // "On branch main..."
// Validate and throw if failed
output.validate();
// Custom validation
output.validate((code) => code === 0 || code === 1);
// Fail on stderr content
output.validate(undefined, true);PathFinder Class
| Method | Description |
| -------------------- | ---------------------------------- |
| set(name, options) | Register executable search options |
| get(name) | Get registered options |
| has(name) | Check if name is registered |
| delete(name) | Remove registration |
| clear() | Clear all registrations |
| find(name) | Find options by name |
| findExe(name) | Find executable path (async) |
| findExeSync(name) | Find executable path (sync) |
import { pathFinder } from "@neotales/exec";
// Register custom executable paths
pathFinder.set("my-tool", {
name: "my-tool",
envVariable: "MY_TOOL_PATH",
windows: ["C:\\Program Files\\MyTool\\my-tool.exe"],
linux: ["/opt/my-tool/bin/my-tool"],
darwin: ["/Applications/MyTool.app/Contents/MacOS/my-tool"],
});
// Find the executable
const toolPath = await pathFinder.findExe("my-tool");
console.log(toolPath);Types
| Type | Description |
| --------------------- | ------------------------------------------------ |
| CommandArgs | string \| string[] \| SplatObject |
| Stdio | "inherit" \| "piped" \| "null" |
| Signal | Unix signals like "SIGTERM", "SIGKILL", etc. |
| CommandOptions | Options for command execution |
| ShellCommandOptions | Options for shell command execution |
| Output | Interface for command output |
| ChildProcess | Interface for spawned child process |
| CommandStatus | Status of completed command |
| PathFinderOptions | Options for PathFinder registration |
Error Handling
import { exec, CommandError, NotFoundOnPathError } from "@neotales/exec";
try {
const output = await exec(["my-command"]);
output.validate();
} catch (e) {
if (e instanceof NotFoundOnPathError) {
console.log("Executable not found:", e.exe);
} else if (e instanceof CommandError) {
console.log("Command failed:", e.fileName);
console.log("Exit code:", e.exitCode);
}
}Extending ShellCommand
Create custom shell command classes by extending ShellCommand:
import { ShellCommand, type ShellCommandOptions } from "@neotales/exec";
class BashCommand extends ShellCommand {
constructor(script: string, options?: ShellCommandOptions) {
super("bash", script, options);
}
override get ext(): string {
return ".sh";
}
override getShellArgs(script: string, isFile: boolean): string[] {
return isFile ? [script] : ["-c", script];
}
}
// Use custom shell command
const cmd = new BashCommand("echo 'Hello from bash'");
const output = await cmd.output();
console.log(output.text()); // "Hello from bash\n"