From 74bea006fd50df69384b95eabfcf6a809ed1a120 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Wed, 3 Dec 2025 10:44:04 +0000 Subject: [PATCH] feat(spawn): finalize PTY support with docs and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- docs/runtime/child-process.mdx | 94 ++++++++++++++++++++++++++--- src/bun.js/api/bun/spawn/stdio.zig | 22 ++++--- test/js/bun/spawn/spawn-pty.test.ts | 19 ++++-- 3 files changed, 114 insertions(+), 21 deletions(-) diff --git a/docs/runtime/child-process.mdx b/docs/runtime/child-process.mdx index ca2e459d42..54569f2feb 100644 --- a/docs/runtime/child-process.mdx +++ b/docs/runtime/child-process.mdx @@ -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" ``` - - PTY is only supported on macOS and Linux. On Windows, passing `"pty"` will throw an error. - +### 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 diff --git a/src/bun.js/api/bun/spawn/stdio.zig b/src/bun.js/api/bun/spawn/stdio.zig index 9ce9236677..0e8ffbb77c 100644 --- a/src/bun.js/api/bun/spawn/stdio.zig +++ b/src/bun.js/api/bun/spawn/stdio.zig @@ -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 = .{}; diff --git a/test/js/bun/spawn/spawn-pty.test.ts b/test/js/bun/spawn/spawn-pty.test.ts index 31d5e5c657..14649413b9 100644 --- a/test/js/bun/spawn/spawn-pty.test.ts +++ b/test/js/bun/spawn/spawn-pty.test.ts @@ -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"); }); });