Headless Terminal Driver for TUI Testing & Automation
π―π΅ ζ₯ζ¬θͺγγγ₯γ‘γ³γγ―γγ‘γ
Conch is a robust library for programmatically controlling terminal applications. By combining node-pty for process management and @xterm/headless for accurate terminal emulation, Conch enables you to:
- Test TUI Applications: Write integration tests for interactive CLI tools (vim, k9s, inquirer, etc.) with confidence.
- Automate Terminal Tasks: Build bots that can navigate complex terminal interfaces, wait for specific states, and extract information.
Think of it as "Playwright for Terminals".
- Accurate Emulation: Uses
xterm.js(headless) to maintain the exact state of the terminal screen, including cursor position, colors, and alternate buffers. - Flakiness-Free Waits: Built-in utilities like
waitForText,waitForSilence, andwaitForStablehelp you handle asynchronous terminal output reliably without randomsleep(). - Human-like Input: Simulate key presses (
Enter,Esc,Ctrl+C) and typing naturally. - Snapshot Engine: Capture the "visual" state of the terminal at any moment to verify what the user actually sees.
- TUI App Support: Built-in terminal query auto-responder (DA1, DA2, CPR, DECRQM) enables interactive TUI apps like vim, less, nano, and top to render correctly in headless mode.
- Pluggable Backends: Supports Local PTY, Docker, and SSH. Designed for extensibility β tmux and WebSocket-based backends (ttyd, GoTTY) are planned.
LLMs are good at deciding what to do next, but they need a reliable execution substrate for terminals:
- Observation: deterministic screen state via
getSnapshot()(viewport or full scrollback) - Action:
run(),pressAndSnapshot(),typeAndSnapshot() - Wait:
waitForText/waitForStable/waitForSilenceinstead of fragile sleeps - Command boundaries: optional OSC 133 Shell Integration to detect prompt/command completion and exit codes
This lets you implement a robust loop: snapshot β decide β act β wait β snapshot, even for interactive TUI apps.
import { Conch } from "@ushida_yosei/conch"; const conch = await Conch.launch({ backend: { type: "localPty", file: process.platform === "win32" ? "powershell.exe" : "bash", env: process.env }, cols: 100, rows: 30, timeoutMs: 30_000, shellIntegration: { enable: true, strict: false }, // improves run() reliability }); try { // (1) Bring up a TUI await conch.run("htop", { strict: false }); // example; pick your app // (2) Agent loop: observe β decide β act for (let step = 0; step < 20; step++) { const snap = conch.getSnapshot({ range: "viewport" }); const screen = snap.text; // Your LLM/tooling decides the next key(s) from screen state const nextKey = screen.includes("Help") ? "F1" : "ArrowDown"; await conch.pressAndSnapshot(nextKey, { wait: { kind: "change", timeoutMs: 5_000 } }); } } finally { conch.dispose(); }
Install from npm:
npm install @ushida_yosei/conch
# or
pnpm add @ushida_yosei/conchHere is a simple example that spawns a shell, executes a command, and verifies the output.
import { Conch } from '@ushida_yosei/conch'; async function main() { // 1. Launch (backend + spawn + session) const conch = await Conch.launch({ backend: { type: 'localPty', file: 'bash', args: [], env: process.env }, cols: 80, rows: 24, timeoutMs: 30_000, }); // 2. Execute a command conch.execute('echo "Hello Conch"'); // 3. Wait for the output to appear on the virtual screen await conch.waitForText('Hello Conch'); // 4. Inspect the screen state const snapshot = conch.getSnapshot(); console.log('--- Terminal Screen ---'); console.log(snapshot.text); // Cleanup conch.dispose(); } main();
You can run Conch against a Docker container instead of a local PTY.
import { Conch } from "@ushida_yosei/conch"; const conch = await Conch.launch({ backend: { type: "docker", image: "alpine:latest", cmd: ["/bin/sh"], // default autoRemove: true, }, cols: 80, rows: 24, timeoutMs: 30_000, }); try { const r = await conch.run('echo "hello from docker"', { strict: false }); console.log(r.outputText); } finally { conch.dispose(); }
Notes:
- Requires a reachable Docker daemon (Docker Desktop / dockerd).
- In TTY mode, stdout/stderr are combined into a single stream.
- Shell Integration (OSC 133) in Docker usually requires an image with
bashandcmd: ["bash"](default is/bin/sh).
You can run Conch against a remote server via SSH.
import { Conch } from "@ushida_yosei/conch"; import { readFileSync } from "fs"; const conch = await Conch.launch({ backend: { type: "ssh", host: "example.com", username: "user", privateKey: readFileSync("/path/to/key"), // or: password: "secret", // or: agent: process.env.SSH_AUTH_SOCK, }, cols: 80, rows: 24, timeoutMs: 30_000, shellIntegration: { enable: true, strict: false }, }); try { const r = await conch.run('echo "hello from SSH"'); console.log(r.outputText); // "hello from SSH" console.log(r.exitCode); // 0 } finally { conch.dispose(); }
Notes:
- Requires
ssh2as a peer dependency:npm install ssh2 - Supports password, private key (with passphrase), and SSH agent authentication.
- Connection loss is treated as a fatal error (no auto-reconnect). Create a new instance to reconnect.
- Shell Integration (OSC 133) works over SSH when the remote shell is bash.
- Host key verification is disabled by default (automation use case). Pass
hostVerifierfor strict checking.
Conch can drive interactive TUI applications (vim, less, nano, top, tmux) in headless mode. A built-in terminal query auto-responder handles the DA/CPR/DECRQM sequences that these apps send on startup.
const conch = await Conch.launch({ backend: { type: "localPty", file: "bash", env: process.env }, cols: 80, rows: 24, timeoutMs: 30_000, }); // Open vim, type text, save and quit conch.execute('vim --cmd "set t_RV=" /tmp/test.txt'); await conch.waitForText("~", { timeoutMs: 5_000 }); // wait for vim UI conch.press("i"); // insert mode conch.type("Hello from Conch!"); conch.press("Escape"); conch.type(":wq"); conch.press("Enter"); await conch.waitForStable({ durationMs: 500 }); conch.dispose();
| Program | Status | Notes |
|---|---|---|
| vim/vi | β | Use --cmd "set t_RV=" for instant rendering (otherwise ~4s delay due to PTY buffering) |
| less | β | Alternate buffer, search, PageDown all work |
| nano | β | Alternate buffer, text input, Ctrl+X quit all work |
| top | β | Batch mode (-b -n 1) works. Interactive mode works with delay |
| tmux | β | Session create/attach, commands inside tmux, session cleanup |
- Usage Guide (USAGE.md): Detailed examples and best practices.
- API Reference (API.md): Complete API documentation for
Conch, backends (LocalPty/DockerPty), and utilities. - Source Docs (src/README.md): Internal architecture overview.
- Shell Integration (OSC 133): Full A/B/C/D marker support for command boundary detection and exit codes.
- SSH Backend (SshPty): Connect to remote hosts via SSH with password, key, or agent authentication.
- TmuxPty Backend: Connect to tmux sessions with
dispose() = detachsemantics. Combines Conch's xterm.js precision with tmux's session persistence. Human debugging viatmux attach. - Press Modifier Keys: Full support for
Alt+D,Ctrl+Shift+A,Shift+ArrowUp, etc. (~170 lines) - OSC 133 Library Extraction: Extract pure parsing/script logic into
@ushida_yosei/exec-detectorfor reuse outside Conch. - WebSocket Backends:
TtydPty(ttyd),GoTTYPty(GoTTY) for browser-based terminal sharing tools. - CLI (
conch run): One-shot command execution with JSON output. Full xterm.js + OSC 133 support, no tmux required. For session persistence, use tmux directly. - KubernetesPty Backend: Direct connection to Kubernetes pods via the exec API (WebSocket + channel multiplexing).
- Mouse Events: Click, scroll, drag simulation for TUI apps that support mouse input.
- Visual Snapshot: Render terminal screen as SVG/PNG with color and attribute information.
MIT