Standard input/output utilities for JavaScript runtimes.
What is @rcompat/io?#
A cross-runtime module providing access to standard streams (stdin, stdout, stderr) and process utilities like executing commands, spawning processes, and reading user input. Works consistently across Node, Deno, and Bun.
Installation#
npm install @rcompat/iopnpm add @rcompat/ioyarn add @rcompat/iobun add @rcompat/ioUsage#
Standard streams#
import io from "@rcompat/io";
// write to stdout (no newline)
io.stdout.write("Hello, ");
io.stdout.write("World!\n");
// write to stderr
io.stderr.write("Error: something went wrong\n");Check if running in a terminal#
import io from "@rcompat/io";
if (io.isatty()) {
// interactive terminal - show colors, prompts, etc.
console.log("\x1b[32mGreen text!\x1b[0m");
} else {
// piped or redirected - plain output
console.log("Plain text");
}Run a command#
import io from "@rcompat/io";
// capture output and exit status
const result = await io.run("echo hello");
console.log(result.stdout); // "hello\n"
console.log(result.success); // true
// with options
const files = await io.run("ls -la", { cwd: "/tmp", timeout: 5_000 });Spawn a process#
import io from "@rcompat/io";
const child = io.spawn("cat", { cwd: "." });
const { stdin, stdout } = child;
// write to process stdin (Web WritableStream)
const writer = stdin.getWriter();
await writer.write(new TextEncoder().encode("Hello from stdin!"));
await writer.close();
// read from process stdout (Web ReadableStream)
const reader = stdout.getReader();
const { value } = await reader.read();
console.log(new TextDecoder().decode(value)); // "Hello from stdin!"
const status = await child.status;
console.log(status.code); // 0Pass an executable and argument array separately to bypass the shell:
const child = io.spawn(process.execPath, ["--version"], {
stdio: {
stdin: "ignore",
stdout: "pipe",
stderr: "inherit",
},
});Find an executable#
import io from "@rcompat/io";
const node = await io.which("node");
console.log(node); // "/usr/local/bin/node"
const bun = await io.which("bun");
console.log(bun); // "/home/user/.bun/bin/bun"which invokes the platform locator directly without a shell and returns its
first non-empty result.
API Reference#
stdin#
import io from "@rcompat/io";
io.stdin;The standard input stream. A ReadStream for reading user input.
stdout#
import io from "@rcompat/io";
io.stdout.write(data: string | Uint8Array): boolean;The standard output stream. Use write() to output without a newline.
stderr#
import io from "@rcompat/io";
io.stderr.write(data: string | Uint8Array): boolean;The standard error stream. Use for error messages and diagnostics.
isatty#
import io from "@rcompat/io";
io.isatty(): boolean;Returns true if stdout is connected to a terminal (TTY).
run#
import io from "@rcompat/io";
io.run(command: string, options?: RunOptions): Promise<RunResult>;Runs a shell command, captures its output, and returns its exit status. A completed process does not reject solely because its exit code is nonzero. Startup and stream failures still reject.
| Parameter | Type | Description |
|---|---|---|
command |
string |
Shell command to execute |
options |
RunOptions |
Optional execution options |
interface RunResult {
code: number | null;
signal: ProcessSignal | null;
stderr: string;
stdout: string;
success: boolean;
timedOut: boolean;
}spawn#
import io from "@rcompat/io";
io.spawn(command: string, options?: SpawnOptions): ChildProcess;
io.spawn(
executable: string,
args: readonly string[],
options?: SpawnOptions,
): ChildProcess;Spawns a process and immediately returns a handle with Web Streams, lifecycle status, and process control.
The two-argument form treats command as a platform-shell command. Supplying
an argument array selects direct execution: executable is launched without a
shell and every argument is passed verbatim.
type Stdio = "ignore" | "inherit" | "pipe";
interface StdioOptions {
stdin?: Stdio;
stdout?: Stdio;
stderr?: Stdio;
}
interface SpawnOptions {
cwd?: string;
env?: Record<string, string | undefined>;
killSignal?: ProcessSignal; // timeout signal; default: "SIGKILL"
stdio?: Stdio | StdioOptions; // default for each stream: "pipe"
timeout?: number; // milliseconds
}
interface ChildProcess {
pid: number;
stdin: WritableStream<Uint8Array> | null;
stdout: ReadableStream<Uint8Array> | null;
stderr: ReadableStream<Uint8Array> | null;
status: Promise<ExitStatus>;
kill(signal?: ProcessSignal): void;
}
interface ExitStatus {
code: number | null;
signal: ProcessSignal | null;
success: boolean;
timedOut: boolean;
}String commands run through the platform shell. Executable-plus-argv calls do
not invoke a shell. A single stdio string applies to all three streams; the
object form configures them independently and omitted entries default to
"pipe". A stream property is available only when its mode is "pipe" and is
otherwise null.
Nonzero exits resolve status with success: false. A failure to start may be
thrown by spawn() or reject status, depending on when the host runtime
reports it. @rcompat/io does not install signal handlers in the parent
process; applications may await status and forward signals with kill().
On POSIX platforms, spawned commands run in an isolated process group.
kill() and timeout handling signal the entire group so descendant processes
cannot continue working or keep captured output streams open after the command
has been terminated. A run() timeout covers process exit and complete output
capture, not only the lifetime of the wrapper shell.
which#
import io from "@rcompat/io";
io.which(command: string): Promise<string>;Finds the full path to an executable. Cross-platform (uses which on Unix,
where on Windows).
Examples#
Run a command and process output#
import io from "@rcompat/io";
async function getBranch() {
const result = await io.run("git branch --show-current");
return result.success ? result.stdout.trim() : null;
}
const branch = await getBranch();
console.log(`Current branch: ${branch ?? "not a git repo"}`);Stream processing with spawn#
import io from "@rcompat/io";
const child = io.spawn("ping -c 3 localhost", { cwd: "." });
const { stdout } = child;
const reader = stdout.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
process.stdout.write(decoder.decode(value));
}
const status = await child.status;
if (!status.success) console.error(status);Check for required tools#
import io from "@rcompat/io";
async function checkDependencies(tools) {
const missing = [];
for (const tool of tools) {
try {
await io.which(tool);
} catch {
missing.push(tool);
}
}
return missing;
}
const missing = await checkDependencies(["git", "node", "docker"]);
if (missing.length > 0) {
console.error(`Missing tools: ${missing.join(", ")}`);
process.exit(1);
}Conditional formatting#
import io from "@rcompat/io";
function log(message, color = 32) {
if (io.isatty()) {
io.stdout.write(`\x1b[${color}m${message}\x1b[0m\n`);
} else {
io.stdout.write(`${message}\n`);
}
}
log("Success!", 32); // green in terminal, plain when piped
log("Warning!", 33); // yellow in terminal
log("Error!", 31); // red in terminalCross-Runtime Compatibility#
| Runtime | Supported |
|---|---|
| Node.js | ✓ |
| Deno | ✓ |
| Bun | ✓ |
No configuration required — just import and use.
License#
MIT
Contributing#
See CONTRIBUTING.md in the repository root.