Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 130 additions & 19 deletions docs/runtime/child-process.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,19 @@ const text = await proc.stdout.text();
console.log(text); // "const input = "hello world".repeat(400); ..."
```

| Value | Description |
| ------------------------ | ------------------------------------------------ |
| `null` | **Default.** Provide no input to the subprocess |
| `"pipe"` | Return a `FileSink` for fast incremental writing |
| `"inherit"` | Inherit the `stdin` of the parent process |
| `Bun.file()` | Read from the specified file |
| `TypedArray \| DataView` | Use a binary buffer as input |
| `Response` | Use the response `body` as input |
| `Request` | Use the request `body` as input |
| `ReadableStream` | Use a readable stream as input |
| `Blob` | Use a blob as input |
| `number` | Read from the file with a given file descriptor |
| Value | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------------- |
| `null` | **Default.** Provide no input to the subprocess |
| `"pipe"` | Return a `FileSink` for fast incremental writing |
| `"inherit"` | Inherit the `stdin` of the parent process |
| `"pty"` | Use a pseudo-terminal (PTY). Child sees `process.stdin.isTTY === true`. Falls back to `"pipe"` on Windows. |
| `Bun.file()` | Read from the specified file |
| `TypedArray \| DataView` | Use a binary buffer as input |
| `Response` | Use the response `body` as input |
| `Request` | Use the request `body` as input |
| `ReadableStream` | Use a readable stream as input |
| `Blob` | Use a blob as input |
| `number` | Read from the file with a given file descriptor |

The `"pipe"` option lets incrementally write to the subprocess's input stream from the parent process.

Expand Down Expand Up @@ -105,13 +106,121 @@ console.log(text); // => "1.3.3\n"

Configure the output stream by passing one of the following values to `stdout/stderr`:

| Value | Description |
| ------------ | --------------------------------------------------------------------------------------------------- |
| `"pipe"` | **Default for `stdout`.** Pipe the output to a `ReadableStream` on the returned `Subprocess` object |
| `"inherit"` | **Default for `stderr`.** Inherit from the parent process |
| `"ignore"` | Discard the output |
| `Bun.file()` | Write to the specified file |
| `number` | Write to the file with the given file descriptor |
| Value | Description |
| ------------ | ----------------------------------------------------------------------------------------------------------- |
| `"pipe"` | **Default for `stdout`.** Pipe the output to a `ReadableStream` on the returned `Subprocess` object |
| `"inherit"` | **Default for `stderr`.** Inherit from the parent process |
| `"ignore"` | Discard the output |
| `"pty"` | Use a pseudo-terminal (PTY). Child sees `process.stdout.isTTY === true`. Falls back to `"pipe"` on Windows. |
| `Bun.file()` | Write to the specified file |
| `number` | Write to the file with the given file descriptor |

## Pseudo-terminal (PTY)

Use `"pty"` to spawn a process with a pseudo-terminal, making the child process believe it's running in an interactive terminal. This causes `process.stdout.isTTY` to be `true` in the child, which is useful for:

- Getting colored output from CLI tools that detect TTY
- Running interactive programs that require a terminal
- Testing terminal-dependent behavior

### Basic usage

```ts
const proc = Bun.spawn(["ls", "--color=auto"], {
stdout: "pty",
});

// The child process sees process.stdout.isTTY === true
const output = await proc.stdout.text();
console.log(output); // includes ANSI color codes
```

### Checking isTTY in child process

```ts
const proc = Bun.spawn(["bun", "-e", "console.log('isTTY:', process.stdout.isTTY)"], {
stdout: "pty",
});

const output = await proc.stdout.text();
console.log(output); // "isTTY: true"
```

### Multiple PTY streams

You can use PTY for stdin, stdout, and/or stderr. When multiple streams use PTY, they share the same underlying pseudo-terminal:

```ts
const code = `
console.log("stdout.isTTY:", process.stdout.isTTY);
console.log("stdin.isTTY:", process.stdin.isTTY);
console.log("stderr.isTTY:", process.stderr.isTTY);
`;

const proc = Bun.spawn(["bun", "-e", code], {
stdin: "pty",
stdout: "pty",
stderr: "pty",
});

const output = await proc.stdout.text();
// stdout.isTTY: true
// stdin.isTTY: true
// stderr.isTTY: true
```

### Custom terminal dimensions

Specify terminal width and height using the object syntax:

```ts
const code = `
console.log("columns:", process.stdout.columns);
console.log("rows:", process.stdout.rows);
`;

const proc = Bun.spawn(["bun", "-e", code], {
stdout: {
type: "pty",
width: 120, // columns
height: 40, // rows
},
});

const output = await proc.stdout.text();
// columns: 120
// rows: 40
```

### Getting colored output from git, grep, etc.

Many CLI tools detect whether they're running in a TTY and only emit colors when they are:

```ts
// Without PTY - no colors
const noColor = Bun.spawn(["git", "status"], { stdout: "pipe" });
console.log(await noColor.stdout.text()); // plain text

// With PTY - colors enabled
const withColor = Bun.spawn(["git", "status"], { stdout: "pty" });
console.log(await withColor.stdout.text()); // includes ANSI color codes
```

### Platform support

| Platform | PTY Support |
| -------- | -------------------------------------------- |
| macOS | ✅ Full support |
| Linux | ✅ Full support |
| Windows | ⚠️ Falls back to `"pipe"` (no TTY semantics) |

On Windows, `"pty"` silently falls back to `"pipe"` behavior. The child process will see `process.stdout.isTTY` as `undefined`. This allows cross-platform code to work without errors, though TTY-dependent features won't work on Windows.

### Limitations

- **Not supported with `spawnSync`**: PTY requires asynchronous I/O. Using `"pty"` with `Bun.spawnSync()` will throw an error.
- **Line endings**: PTY converts `\n` to `\r\n` on output (standard terminal behavior).
- **No dynamic resize**: Terminal dimensions are set at spawn time and cannot be changed after.

## Exit handling

Expand Down Expand Up @@ -413,6 +522,7 @@ namespace SpawnOptions {
| "pipe"
| "inherit"
| "ignore"
| "pty" // use a pseudo-terminal (macOS/Linux only)
| null // equivalent to "ignore"
| undefined // to use default
| BunFile
Expand All @@ -423,6 +533,7 @@ namespace SpawnOptions {
| "pipe"
| "inherit"
| "ignore"
| "pty" // use a pseudo-terminal (macOS/Linux only)
| null // equivalent to "ignore"
| undefined // to use default
| BunFile
Expand Down
39 changes: 20 additions & 19 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1740,9 +1740,9 @@ declare module "bun" {
* @default "esm"
*/
format?: /**
* ECMAScript Module format
*/
| "esm"
* ECMAScript Module format
*/
| "esm"
/**
* CommonJS format
* **Experimental**
Expand Down Expand Up @@ -3316,10 +3316,10 @@ declare module "bun" {
function color(
input: ColorInput,
outputFormat?: /**
* True color ANSI color string, for use in terminals
* @example \x1b[38;2;100;200;200m
*/
| "ansi"
* True color ANSI color string, for use in terminals
* @example \x1b[38;2;100;200;200m
*/
| "ansi"
| "ansi-16"
| "ansi-16m"
/**
Expand Down Expand Up @@ -5335,6 +5335,7 @@ declare module "bun" {
| "pipe"
| "inherit"
| "ignore"
| "pty"
| null // equivalent to "ignore"
| undefined // to use default
| BunFile
Expand All @@ -5348,6 +5349,7 @@ declare module "bun" {
| "pipe"
| "inherit"
| "ignore"
| "pty"
| null // equivalent to "ignore"
| undefined // to use default
| BunFile
Expand Down Expand Up @@ -5405,14 +5407,16 @@ declare module "bun" {
* - `"ignore"`, `null`, `undefined`: The process will have no standard input (default)
* - `"pipe"`: The process will have a new {@link FileSink} for standard input
* - `"inherit"`: The process will inherit the standard input of the current process
* - `"pty"`: The process will use a pseudo-terminal (PTY). The child will see `process.stdin.isTTY === true`. Falls back to `"pipe"` on Windows. Not supported with `spawnSync`.
* - `ArrayBufferView`, `Blob`, `Bun.file()`, `Response`, `Request`: The process will read from buffer/stream.
* - `number`: The process will read from the file descriptor
*
* For stdout and stdin you may pass:
* For stdout and stderr you may pass:
*
* - `"pipe"`, `undefined`: The process will have a {@link ReadableStream} for standard output/error
* - `"ignore"`, `null`: The process will have no standard output/error
* - `"inherit"`: The process will inherit the standard output/error of the current process
* - `"pty"`: The process will use a pseudo-terminal (PTY). The child will see `process.stdout.isTTY === true` / `process.stderr.isTTY === true`. Falls back to `"pipe"` on Windows. Not supported with `spawnSync`.
* - `ArrayBufferView`: The process write to the preallocated buffer. Not implemented.
* - `number`: The process will write to the file descriptor
*
Expand All @@ -5427,6 +5431,7 @@ declare module "bun" {
* - `"ignore"`, `null`, `undefined`: The process will have no standard input
* - `"pipe"`: The process will have a new {@link FileSink} for standard input
* - `"inherit"`: The process will inherit the standard input of the current process
* - `"pty"`: The process will use a pseudo-terminal (PTY). The child will see `process.stdin.isTTY === true`. Falls back to `"pipe"` on Windows. Not supported with `spawnSync`.
* - `ArrayBufferView`, `Blob`: The process will read from the buffer
* - `number`: The process will read from the file descriptor
*
Expand All @@ -5439,6 +5444,7 @@ declare module "bun" {
* - `"pipe"`, `undefined`: The process will have a {@link ReadableStream} for standard output/error
* - `"ignore"`, `null`: The process will have no standard output/error
* - `"inherit"`: The process will inherit the standard output/error of the current process
* - `"pty"`: The process will use a pseudo-terminal (PTY). The child will see `process.stdout.isTTY === true`. Falls back to `"pipe"` on Windows. Not supported with `spawnSync`.
* - `ArrayBufferView`: The process write to the preallocated buffer. Not implemented.
* - `number`: The process will write to the file descriptor
*
Expand All @@ -5451,6 +5457,7 @@ declare module "bun" {
* - `"pipe"`, `undefined`: The process will have a {@link ReadableStream} for standard output/error
* - `"ignore"`, `null`: The process will have no standard output/error
* - `"inherit"`: The process will inherit the standard output/error of the current process
* - `"pty"`: The process will use a pseudo-terminal (PTY). The child will see `process.stderr.isTTY === true`. Falls back to `"pipe"` on Windows. Not supported with `spawnSync`.
* - `ArrayBufferView`: The process write to the preallocated buffer. Not implemented.
* - `number`: The process will write to the file descriptor
*
Expand Down Expand Up @@ -5650,17 +5657,11 @@ declare module "bun" {
maxBuffer?: number;
}

interface SpawnSyncOptions<In extends Writable, Out extends Readable, Err extends Readable> extends BaseOptions<
In,
Out,
Err
> {}

interface SpawnOptions<In extends Writable, Out extends Readable, Err extends Readable> extends BaseOptions<
In,
Out,
Err
> {
interface SpawnSyncOptions<In extends Writable, Out extends Readable, Err extends Readable>
extends BaseOptions<In, Out, Err> {}

interface SpawnOptions<In extends Writable, Out extends Readable, Err extends Readable>
extends BaseOptions<In, Out, Err> {
/**
* If true, stdout and stderr pipes will not automatically start reading
* data. Reading will only begin when you access the `stdout` or `stderr`
Expand Down
12 changes: 11 additions & 1 deletion src/bun.js/api/bun/js_bun_spawn_bindings.zig
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,16 @@ pub fn spawnMaybeSync(
.stdout_maxbuf = subprocess.stdout_maxbuf,
};

if (comptime Environment.isPosix) {
log("After subprocess init: stdout state={s}, stdin FD={?d}, stdout FD={?d}", .{
@tagName(subprocess.stdout),
if (spawned.stdin) |fd| fd.native() else null,
if (spawned.stdout) |fd| fd.native() else null,
});
} else {
log("After subprocess init: stdout state={s}", .{@tagName(subprocess.stdout)});
}

subprocess.process.setExitHandler(subprocess);

promise_for_stream.ensureStillAlive();
Expand Down Expand Up @@ -997,7 +1007,7 @@ pub fn appendEnvpFromJS(globalThis: *jsc.JSGlobalObject, object: *jsc.JSObject,
}
}

const log = Output.scoped(.Subprocess, .hidden);
const log = Output.scoped(.spawn_bindings, .hidden);
extern "C" const BUN_DEFAULT_PATH_FOR_SPAWN: [*:0]const u8;

const IPC = @import("../../ipc.zig");
Expand Down
Loading
Loading