feat(spawn): finalize PTY support with docs and tests

- Add comprehensive PTY documentation with examples:
  - Basic usage
  - Multiple PTY streams (stdin/stdout/stderr)
  - Custom terminal dimensions (width/height)
  - Colored output from git, grep, etc.
  - Platform support table
  - Limitations section

- Make PTY fall back to pipe on Windows (instead of error)
- Add spawnSync error for PTY (not supported)
- Add tests for spawnSync PTY error

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2025-12-03 10:44:04 +00:00
parent 5d775066b2
commit 74bea006fd
3 changed files with 114 additions and 21 deletions

View File

@@ -117,7 +117,13 @@ Configure the output stream by passing one of the following values to `stdout/st
## Pseudo-terminal (PTY)
Use `"pty"` to run a process in a pseudo-terminal, making it think it's running in an interactive terminal. This is useful for programs that change their behavior based on whether they're running in a TTY (e.g., colored output, interactive prompts).
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"], {
@@ -126,24 +132,94 @@ const proc = Bun.spawn(["ls", "--color=auto"], {
// The child process sees process.stdout.isTTY === true
const output = await proc.stdout.text();
console.log(output); // colored output
console.log(output); // includes ANSI color codes
```
You can use PTY for stdin, stdout, and stderr. When multiple streams use PTY, they share the same underlying terminal:
### Checking isTTY in child process
```ts
const proc = Bun.spawn(["node", "-e", "console.log(process.stdout.isTTY, process.stdin.isTTY)"], {
stdin: "pty",
const proc = Bun.spawn({
cmd: ["bun", "-e", "console.log('isTTY:', process.stdout.isTTY)"],
stdout: "pty",
});
const output = await proc.stdout.text();
console.log(output); // "true true"
console.log(output); // "isTTY: true"
```
<Note>
PTY is only supported on macOS and Linux. On Windows, passing `"pty"` will throw an error.
</Note>
### 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 proc = Bun.spawn({
cmd: ["bun", "-e", `
console.log("stdout.isTTY:", process.stdout.isTTY);
console.log("stdin.isTTY:", process.stdin.isTTY);
console.log("stderr.isTTY:", process.stderr.isTTY);
`],
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 proc = Bun.spawn({
cmd: ["bun", "-e", `
console.log("columns:", process.stdout.columns);
console.log("rows:", process.stdout.rows);
`],
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

View File

@@ -43,7 +43,6 @@ pub const Stdio = union(enum) {
stdin_used_as_out,
out_used_as_stdin,
blob_used_as_out,
pty_not_supported,
uv_pipe: bun.sys.E,
pub fn toStr(this: *const @This()) []const u8 {
@@ -51,7 +50,6 @@ pub const Stdio = union(enum) {
.stdin_used_as_out => "Stdin cannot be used for stdout or stderr",
.out_used_as_stdin => "Stdout and stderr cannot be used for stdin",
.blob_used_as_out => "Blobs are immutable, and cannot be used for stdout/stderr",
.pty_not_supported => "PTY is not supported on Windows",
.uv_pipe => @panic("TODO"),
};
}
@@ -257,7 +255,7 @@ pub const Stdio = union(enum) {
.path => |pathlike| .{ .path = pathlike.slice() },
.inherit => .{ .inherit = {} },
.ignore => .{ .ignore = {} },
.pty => return .{ .err = .pty_not_supported },
.pty => .{ .buffer = bun.handleOom(bun.default_allocator.create(uv.Pipe)) }, // PTY falls back to pipe on Windows
.memfd => @panic("This should never happen"),
},
@@ -361,10 +359,15 @@ pub const Stdio = union(enum) {
} else if (str.eqlComptime("ipc")) {
out_stdio.* = Stdio{ .ipc = {} };
} else if (str.eqlComptime("pty")) {
if (comptime Environment.isWindows) {
return globalThis.throwInvalidArguments("PTY is not supported on Windows", .{});
if (is_sync) {
return globalThis.throwInvalidArguments("PTY is not supported with spawnSync", .{});
}
// On Windows, PTY falls back to pipe (no real PTY support)
if (comptime Environment.isWindows) {
out_stdio.* = Stdio{ .pipe = {} };
} else {
out_stdio.* = Stdio{ .pty = .{} };
}
out_stdio.* = Stdio{ .pty = .{} };
} else {
return globalThis.throwInvalidArguments("stdio must be an array of 'inherit', 'pipe', 'ignore', 'pty', Bun.file(pathOrFd), number, or null", .{});
}
@@ -461,8 +464,13 @@ pub const Stdio = union(enum) {
if (type_val.isString()) {
const type_str = try type_val.getZigString(globalThis);
if (type_str.eqlComptime("pty")) {
if (is_sync) {
return globalThis.throwInvalidArguments("PTY is not supported with spawnSync", .{});
}
// On Windows, PTY falls back to pipe (no real PTY support)
if (comptime Environment.isWindows) {
return globalThis.throwInvalidArguments("PTY is not supported on Windows", .{});
out_stdio.* = Stdio{ .pipe = {} };
return;
}
var pty_opts: PtyOptions = .{};

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isWindows } from "harness";
import { bunEnv, bunExe } from "harness";
describe("Bun.spawn with PTY", () => {
test("stdout: 'pty' makes process.stdout.isTTY true", async () => {
@@ -146,13 +146,22 @@ describe("Bun.spawn with PTY", () => {
});
});
describe.if(isWindows)("Bun.spawn PTY on Windows", () => {
test("throws error when PTY is used on Windows", () => {
describe("Bun.spawnSync with PTY", () => {
test("throws error when PTY is used with spawnSync", () => {
expect(() => {
Bun.spawn({
Bun.spawnSync({
cmd: ["echo", "test"],
stdout: "pty",
});
}).toThrow("PTY is not supported on Windows");
}).toThrow("PTY is not supported with spawnSync");
});
test("throws error when PTY object syntax is used with spawnSync", () => {
expect(() => {
Bun.spawnSync({
cmd: ["echo", "test"],
stdout: { type: "pty", width: 80, height: 24 },
});
}).toThrow("PTY is not supported with spawnSync");
});
});