nodejs / nodejs/node

Feature request: PTY support in `child_process.spawn()`

Aperta
#64,019 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

feature request
Lingua principale
JavaScript
Stelle
122k
Fork
37.3k
Merge medio
4g 2h
PR unite (30g)
283

Descrizione

What is the problem this feature will solve?

Please add first-class pseudo-terminal (PTY) support to child_process.spawn()
(and ideally spawnSync()), so child processes see a real TTY on stdout/stderr/stdin
instead of pipes.

Today, the only practical option is third-party native addons such as
node-pty. That works, but it adds
native compilation, Electron/ABI friction, and an extra dependency for a capability
that shells and other runtimes expose natively.

Built-in PTY support would make Node.js a better cross-platform choice for
developer tools, CI runners, test harnesses, and build orchestrators compared
with wrapping PowerShell or Python just to get a TTY.

Problem

When spawn() uses the default stdio: ['pipe', 'pipe', 'pipe'], the child's
stdout/stderr are not TTYs. Many programs behave differently:

  • Block buffering instead of line buffering (bash, make, gcc, pacman, etc.)
  • isatty() / process.stdout.isTTY is false (no ANSI colors, different log format)
  • Interactive prompts break (password prompts, read, pagers)
  • Progress output is delayed until buffers fill or the process exits

Workarounds are incomplete:

  • stdbuf -oL -eL only affects some libc-buffered programs; grandchild processes
    (e.g. make jobs) may still block-buffer.
  • script -q -f allocates a PTY but adds Script started/done noise to logs.
  • Redirecting to a file (>log.txt) inside the shell has the same non-TTY behavior.

Concrete use case

I maintain a cross-platform bootstrap/build runner (Windows + MSYS/Cygwin) written
in TypeScript/Node.js because it is faster and simpler to ship than PowerShell or
Python for this use case.

The runner uses spawn(bash, ['--login', '-c', script]) and tees output to both
the terminal and a log file. Without a PTY:

  1. The terminal shows almost nothing for minutes ("stuck" UX).
  2. The log file updates in large bursts, not line-by-line.
  3. Some toolchain steps change behavior under non-TTY conditions.

We currently wrap commands in stdbuf -oL -eL and manually tee data events.
That helps a little but is fragile and platform-dependent. A PTY would fix the
root cause.

Minimal desired behavior:

import { spawn } from 'node:child_process';
import { createWriteStream } from 'node:fs';

const log = createWriteStream('build.log');
const child = spawn('bash', ['--login', '-c', 'make -j8'], {
  cwd: repoRoot,
  env: process.env,
  stdio: ['pipe', 'pty', 'pty'], // or a dedicated option; see below
});

child.on('data', (chunk) => {
  log.write(chunk);
  process.stdout.write(chunk);
});

The child should believe it has a terminal (isatty(1) === true), use
line-oriented output, and flush promptly.

Why not node-pty?

node-pty is widely used (VS Code, etc.)
and I am grateful it exists, but for application-level build/CI tooling it has
drawbacks:

  • Native addon: requires node-gyp/prebuilds; breaks or needs rebuilds across
    Node/Electron ABI changes.
  • Extra dependency for something that feels like core process/spawn behavior.
  • API divergence from child_process.spawn() (different return type, resize
    events, etc.).

For terminal emulators, node-pty is fine. For "run this shell script and stream
output live to console + log", PTY belongs in core next to spawn.

Proposed API (sketch)

Option A - extend stdio:

spawn(cmd, args, {
  stdio: ['pipe', 'pty', 'pty'],
  pty: {
    cols: process.stdout.columns ?? 80,
    rows: process.stdout.rows ?? 24,
    name: 'xterm-256color',
  },
});

Option B - dedicated flag:

spawn(cmd, args, {
  pty: true,
  stdio: ['pipe', 'pipe', 'pipe'],
});

Requirements:

  • Works on Linux, macOS, Windows 10+ (ConPTY).
  • Document graceful failure on older Windows (throw or fallback to pipes).
  • Optional resize(cols, rows) on the child handle.
  • PTY stream emits data events; stdin remains writable for automation.
  • Document interaction with shell: true, detached, windowsHide.

Platform notes

  • POSIX: forkpty(3) / openpty + session setup (Node already has test helpers in
    test/pseudo-tty/pty_helper.py).
  • Windows: CreatePseudoConsole() (ConPTY). Older Windows may need documented
    unsupported behavior rather than winpty-level emulation in core.

Prior art / related issues

This request is blocked on libuv PTY landing first; once libuv exposes
spawn-with-PTY, please expose it through child_process.

Why this matters for Node.js

Node is already the default for cross-platform CLI tooling (yarn, vite, eslint,
etc.). Live, faithful subprocess output is a basic expectation for:

  • build systems and monorepo orchestrators
  • test runners
  • dev environment bootstrap scripts
  • CI log streaming

Without PTY, authors either accept broken UX, add native deps, or shell out to
PowerShell/Python/bash script hacks. Native PTY would close that gap and reduce
reliance on node-pty for non-terminal-emulator use cases.

What is the feature you are proposing to solve the problem?

Add optional pseudo-terminal (PTY) support to child_process.spawn() (and
spawnSync()) so a spawned child can use a real TTY instead of pipes.

Proposed API (either shape is fine):

spawn(cmd, args, {
stdio: ['pipe', 'pty', 'pty'],
pty: { cols, rows, name: 'xterm-256color' },
});

or:

spawn(cmd, args, { pty: true, stdio: ['pipe', 'pipe', 'pipe'] });

Behavior:

  • Child sees isatty(stdout/stderr) === true (line-buffered output, colors,
    interactive prompts work as in a real terminal).
  • Parent gets a readable/writable PTY stream (emit 'data', write stdin).
  • Optional child.resize(cols, rows) for terminal size changes.
  • Linux/macOS via POSIX PTY; Windows 10+ via ConPTY; document unsupported
    fallback on older Windows.

This should be implemented on top of libuv PTY support (libuv#2640,
libuv PR#4802) and exposed through child_process, similar to how pipe
and inherit stdio modes work today.

What alternatives have you considered?

No response

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Direzione di ricerca

Inizia dal punto di ingresso child_process.spawn() e verifica test/pseudo-tty/pty_helper.py, quindi controlla l’issue e la PR di libuv referenziate, perché la richiesta è bloccata dal supporto PTY di libuv. Per completare il lavoro sarebbe necessaria un’API concordata con un comportamento POSIX e ConPTY multipiattaforma, inclusa la gestione di stream, ridimensionamento, fallback e spawnSync.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
javascript, node.js
Ambito
api, backend
Tipo di issue
Funzionalità
Difficoltà
5/5
Tempo stimato
Più di una settimana
Stato di attività
Tranquilla
Chiarezza
Abbastanza chiara
Idoneità per principianti
32/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.