https://github.com/privatenumber/pty-spawn
Tiny pseudo-terminal spawning for humans
https://github.com/privatenumber/pty-spawn
node-pty pty spawn subprocess terminal
Last synced: 12 days ago
JSON representation
Tiny pseudo-terminal spawning for humans
- Host: GitHub
- URL: https://github.com/privatenumber/pty-spawn
- Owner: privatenumber
- License: mit
- Created: 2026-02-12T19:30:27.000Z (4 months ago)
- Default Branch: develop
- Last Pushed: 2026-04-27T11:40:02.000Z (about 2 months ago)
- Last Synced: 2026-05-18T07:26:29.744Z (about 1 month ago)
- Topics: node-pty, pty, spawn, subprocess, terminal
- Language: TypeScript
- Homepage:
- Size: 45.9 KB
- Stars: 6
- Watchers: 0
- Forks: 0
- Open Issues: 11
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# pty-spawn
Spawn a PTY process and `await` it like a promise. Built on [`node-pty`](https://github.com/microsoft/node-pty).
## Install
```sh
npm install pty-spawn
```
## Quick start
```ts
import { spawn, waitFor } from 'pty-spawn'
const subprocess = spawn('node', ['server.js'])
// Wait for specific output
await waitFor(subprocess, output => output.includes('Listening on port 3000'))
// Interact
subprocess.stdin.write('quit\n')
// Await final result
const result = await subprocess
console.log(result.output)
```
## Why?
`node-pty` gives you low-level event wiring. `pty-spawn` wraps it into a single awaitable object:
| Without `pty-spawn` | With `pty-spawn` |
| --- | --- |
| Wire up promise + event cleanup | `await subprocess` |
| Buffer output + poll + handle exit/timeout | `waitFor(subprocess, predicate)` |
| Abort/exit race conditions | Late abort never overrides a clean exit |
| Manual output accumulation loop | `subprocess.output` (live string) |
## API
### `spawn(file, args?, options?)`
Spawns a PTY process. Returns a [`Subprocess`](#subprocess).
```ts
const subprocess = spawn('node', ['server.js'], { timeout: 5000 })
// Without args
const subprocess = spawn('bash', { window: { cols: 120 } })
```
#### Options
All [`node-pty` options](https://github.com/microsoft/node-pty) are supported, plus:
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `window` | `{ cols?, rows? }` | — | PTY window size |
| `timeout` | `number` | `0` (disabled) | Auto-abort after _N_ ms |
| `signal` | `AbortSignal` | — | Abort control |
| `reject` | `boolean` | `true` | Reject on non-zero exit, signal, or abort. When `false`, always resolves |
### Subprocess
A `Promise` with control properties attached.
#### Properties
| Property | Type | Description |
| --- | --- | --- |
| `pid` | `number` | Process ID |
| `output` | `string` | Accumulated output (live — grows as the process writes) |
#### `stdin.write(data)`
Write to the process stdin.
```ts
subprocess.stdin.write('hello\n')
```
#### `kill(signal?, options?)`
Terminate the process and wait for exit.
```ts
await subprocess.kill()
await subprocess.kill('SIGTERM')
await subprocess.kill('SIGTERM', { forceKill: 3000 })
await subprocess.kill({ forceKill: 3000 })
```
`forceKill` escalates to `SIGKILL` after the given milliseconds if the process traps the initial signal. Safe to call after exit.
#### `resize(cols, rows)`
Resize the PTY window. Safe to call after exit.
#### Async iteration
The subprocess itself is `AsyncIterable` — stream output chunks in real time:
```ts
for await (const chunk of subprocess) {
process.stdout.write(chunk)
}
```
#### Async disposal
Supports [`await using`](https://github.com/tc39/proposal-explicit-resource-management) for automatic cleanup:
```ts
{
await using subprocess = spawn('node', ['server.js'])
// killed when scope exits
}
```
### Result
`await subprocess` resolves with:
| Property | Type | Description |
| --- | --- | --- |
| `output` | `string` | All terminal output |
| `exitCode` | `number` | Process exit code |
| `signalName` | `string?` | Signal name if terminated (e.g. `'SIGTERM'`) |
| `file` | `string` | Spawned file path |
| `args` | `string[]` | Arguments passed |
| `durationMs` | `number` | Wall-clock duration in ms |
> [!NOTE]
> PTYs combine stdout and stderr into a single stream. `output` contains everything the process wrote to the terminal.
### SubprocessError
Non-zero exit or signal termination rejects with `SubprocessError`, which extends `Error` and includes all `Result` fields.
```ts
import { spawn, SubprocessError } from 'pty-spawn'
try {
await subprocess
} catch (error) {
if (error instanceof SubprocessError) {
error.exitCode // e.g. 1
error.signalName // e.g. 'SIGTERM'
error.output // captured output
}
}
```
Abort and timeout behavior:
- `timeout` and `signal` abort the process, rejecting with `SubprocessError`
- `error.cause` contains the underlying reason (e.g. `TimeoutError`)
- If the process exits cleanly before the abort fires, success wins the race
### `waitFor(subprocess, predicate, options?)`
Wait for output to satisfy a predicate.
```ts
await waitFor(subprocess, output => output.includes('Ready'))
// With timeout
await waitFor(subprocess, output => output.includes('Ready'), {
signal: AbortSignal.timeout(5000)
})
```
The predicate receives accumulated output (since `waitFor` was called) and can be async. Calls are serialized — a slow predicate won't run concurrently.
| Option | Type | Description |
| --- | --- | --- |
| `signal` | `AbortSignal` | Abort control (use `AbortSignal.timeout()` for timeouts) |
## Inspiration
Thanks to [execa](https://github.com/sindresorhus/execa) and [nano-spawn](https://github.com/sindresorhus/nano-spawn) for the inspiration. They proved that `await spawn(...)` is the right primitive for child processes — pty-spawn brings that model to pseudo-terminals.