Skip to content

面向 Node、Bun、Deno 的运行时中立 PTY——一份公开契约、开发者可自选 Backend、每条支持声明都有证据背书。

面向 Node、Bun、Deno 的运行时无关 PTY —— 一套公共契约,可由开发者显式选择的 Backend。

CI License: MIT

English | 简体中文

Node、Bun、Deno 各自暴露了不同的 PTY 底层实现——安装模型、I/O 表示、生命周期语义、原生部署约束都不同。UniPty 把这些收敛为一套小而诚实的契约:应用显式选择 Backend,所有底层差异都留在 Core 私有的接缝之后。没有隐式 shell 执行,不会静默回退到管道,也不做运行时替换。

安装

Core 加上一个你选定的 Backend——装哪个包,就得到哪个引擎:

运行时 安装 你得到的 Backend 包
Node npm install unipty @unipty/backend-node-pty 第三方 node-pty 预构建
Node npm install unipty @unipty/backend-zigpty 第三方 zigpty(Zig 构建、零依赖)
Bun bun add unipty @unipty/backend-bun 运行时原生 Bun.Terminal
Deno npm:@unipty/backend-deno-sigma__pty-ffi 导入(FFI 路由需 -A 运行) 内嵌 @sigma/pty-ffi 动态库

不知道选哪个引擎?下方「选型」一节的能力差异矩阵会告诉你每个引擎到底给你什么。

快速上手

import { UniPty } from "unipty";
import { createNodePtyBackend } from "@unipty/backend-node-pty";

const backend = await createNodePtyBackend(); // 一次性就绪
const unipty = new UniPty({ backend });

const pty = unipty.spawn(["/bin/sh", "-i"], {
  cwd: process.cwd(),
  terminal: { cols: 120, rows: 40 },
});

for await (const text of pty.stream({ encoding: "utf8" })) {
  process.stdout.write(text);
}
pty.write("echo hello\n"); // 布尔写入就绪——语义见契约速览
pty.resize(80, 24); // 仅字符单元格
pty.terminate(); // 终止请求,绝不级联 close
pty.close(); // 传输关闭,绝不杀死子进程
const { exitCode, signal } = await pty.exited; // 独立观察

换引擎只是一行改动——上面所有代码在每条路由上完全一致: createZigptyBackend()zigpty)、createBunBackend()bun)或 createDenoSigmaPtyFfiBackend()deno)。引擎专属行为(选项、权限、 能力差异)见各包 README。

契约速览

你能调用的一切,以及它的确切承诺:

语义
spawn(argv, options) 同步;argv 是结构化数据;几何按维度独立解析(显式 → COLUMNS/LINES → 宿主 TTY → 80×24)
stream({ encoding }) 每 PTY 一个活跃视图(否则 active-stream);取消仅脱离该视图
write(data) / drain() 布尔就绪;整值接受;类型化饱和
resize(cols, rows) 有限正整数(字符单元格);不支持时显式失败
close() / terminate() 幂等、同步、非级联
exited 可重复 await 的 { exitCode, signal },独立于流完成与 close
错误 稳定 error.codeunsupportedclosedbackpressureinvalid-argumentactive-stream

四个最常被设计出来「防坑」的行为:

  • 结构化启动 —— Bun 风格 spawn(argv, options),argv 为非空参数向量。没有字符串命令重载、没有隐式 shell;元字符只是普通数据。
  • 表示选择的流 —— pty.stream({ encoding: "utf8" | "bytes" })。UTF-8 视图优先使用原生文本,否则增量解码字节;字节视图只产出原生字节——重新编码的文本绝不冒充原始输出。
  • 布尔写入就绪 —— write() 返回 false 的含义是「暂停并等待 drain()」,绝不是「重试」;饱和时以类型化的 backpressure 失败拒绝整个值。绝不部分接受、绝不静默丢弃。
  • 非级联生命周期 —— close() 绝不杀子进程,terminate() 绝不关传输,exited 在两者之后依然有效。unipty.dispose() 立即阻止新 spawn,等待所有存活 PTY 关闭后恰好一次释放 Backend。

选型

官方路由

运行时 底层实现(如实声明)
@unipty/backend-node-pty Node 第三方 node-pty(经 @lydell/node-pty 预构建发行版)
@unipty/backend-zigpty Node 第三方 zigpty——Zig 构建 NAPI 预编译随 tarball 分发(硬性原生门禁,无管道回退)
@unipty/backend-bun Bun 运行时原生 Bun.Terminal(POSIX ≥ 1.3.13,Windows ≥ 1.3.14)
@unipty/backend-deno-sigma__pty-ffi Deno 第三方 @sigma/pty-ffi(Rust portable-pty),整体内嵌为自包含 npm 制品

Node 路由适配的是第三方库——不是 Node 运行时原生 API,文档绝不如此宣称。Deno 只是最后一条路由的运行时元数据,不是其实现身份。

引擎能力差异(各底座实际给到什么)

每条路由的公共契约完全一致——结构化 argv、几何尺寸与 resize、写就绪 + drain + 饱和整值拒绝、非级联 close/terminate、bootstrap 缓冲、公共错误码。但底层引擎并不相同。选路由前请先看这张如实差异表:✓ 开箱即用,⚠ 需要选项或带有已声明的限制,✗ 不提供。

能力 node-pty zigpty bun deno-sigma__pty-ffi 备注
字节写入 pty.write(Uint8Array) ⚠ 需 writeDecode 选项 zigpty 底层 write 仅收字符串;writeDecode: true 安装有状态、分裂安全的解码器(fatal 策略整值拒绝)
原生文本输出(encoding:"utf8" bun 与 deno 双向字节原生;它们的 utf8 视图由 Core 增量解码(无损)
Windows 目标 ✓ ConPTY* ⚠ 可运行,输出为缓冲式† ✓ ≥ 1.3.14* *证据门控(见兼容性目录);†zigpty 引擎自带 Windows 预编译、本路由在该平台照常运行,但底层 pause()/resume() 在 win32 是空操作,输出背压传导不到内核——开启该路由的 outputSpool 选项以"溢写磁盘"为内存封顶
内核级输出背压 ✓(主 socket 暂停) ✓(公开 pause/resume,unix) ✗(传输层无流控) ✗(内部通道 + 轮询泵) node-pty 暂停主 socket;zigpty 走公开 API(exit 后经由接管的读流)——Windows 上该暂停是空操作,改由适配层的 outputSpool(有界内存 + 磁盘溢写)兜底;bun 无传输级流控(底层限制);deno 的 FFI 读端排入内部缓冲
独立传输 EOF 信号 ✓(socket close 事件) ⚠ 真信号 + 静默兜底 ⚠ 回调 + 合成兜底 ✓(读循环 done zigpty 在 exit 时接管主读流(真实 end/close),50ms 静默窗由迟到 chunk 续期兜底;bun 以 Terminal exit 回调为主、exited 合成为兜底
传输读错误可上报 ✓(unsupported ✗ 与干净 EOF 不可区分 ✓(unsupported zigpty 底层完全吞掉流错误;其余三条会把错误打到流上——读失败绝不会被静默当作干净 EOF
信号致死观察 signal 名 signal 名、exitCode: 0 signal 名、exitCode: null exitCode: 1、signal 恒 null 各底层报告形状不同;适配器逐字透传,绝不伪造引擎没有报告的值
底层分发形态 平台子包 零依赖、prebuilds 随包(8 元组) 运行时内置 内嵌动态库 deno 还需要 FFI 权限(-A / --allow-ffi);zigpty 完全没有安装脚本;node-pty 只装当前平台的二进制

所有路由上 exec 失败都是退出观察(绝不是 spawn 异常);各适配器的细节见各自包的 README。

获取 Backend

手动导入是一等路径——Core 永远不需要获取层:

const backend = await createBunBackend(); // 或任意官方工厂
const unipty = new UniPty({ backend });

需要确定性发现时,@unipty/backend 将工作分段:纯解析(不导入)、仅元数据检查(不初始化)、然后是选定候选的初始化——其失败是终止性的且结构化的:

import { autoResolveUniPtyBackend } from "@unipty/backend";

const backend = await autoResolveUniPtyBackend({
  candidates: ["@unipty/backend-node-pty"], // 有序偏好
  from: import.meta.url, // 调用方为根的基址
});

打包部署场景改为提供显式不可变 manifest(defineUniPtyBackendManifest()), 由 unipty-helper-backend manifest --candidate <pkg> --out backend-manifest.ts 生成。完整分段契约见获取层 README

架构(60 秒版)

application code
   │  public contract (spawn / stream / write / resize / lifecycle / exited)
   ▼
UniPty Core ──── owns every observable behaviour: views, conversion,
   │             bootstrap buffering, backpressure, errors, lifecycle state
   ▼
Ready Backend ── one injected, already-ready object per UniPty instance
   │             (native loading / connection / negotiation finished first)
   ▼
real PTY on node-pty / zigpty / Bun.Terminal / @sigma/pty-ffi

读代码前值得知道的设计原则:

  • 底座诚实。 每个适配器都如实记录底层的真实行为(kill-and-close 原语、无界内部缓冲、信号不透明),而不是粉饰;支持声明证据门控——一个元组只有针对已安装制品跑完整公共契约全过才算 verified,发布目录是唯一事实源。
  • 无隐藏策略。 没有隐式 shell、没有静默管道回退、没有第二套插件注册表、没有能力/资产协议。扩展点都是显式的:Backend wrapper 与不透明能力 token(pty.capability(token),按对象身份匹配)。

完整设计叙事见架构设计.md

包一览

npm 说明
unipty npm 公共 Core:UniPtyPty、Backend/Endpoint 接缝、公共错误
@unipty/backend npm 获取便利层:resolveUniPtyBackendinspectUniPtyBackendautoResolveUniPtyBackend、manifest 构造器
@unipty/helper-backend npm 构建期 manifest 生成器(unipty-helper-backend manifest
@unipty/backend-node-pty npm 官方 Node 路由(第三方 node-pty
@unipty/backend-zigpty npm 官方 Node 路由(第三方 zigpty,Zig 构建 NAPI 预编译)
@unipty/backend-bun npm 官方 Bun 路由(运行时原生 Bun.Terminal
@unipty/backend-deno-sigma__pty-ffi npm 官方 Deno 路由(vendored @sigma/pty-ffi,自包含 npm 制品)
@unipty/shell-parser npm 可选生态:基于 unbash 的 argv/shell 解析
@unipty/powershell-parser npm 可选生态:PowerShell 命令解析
@unipty/conformance —(私有) 已安装包一致性装置、证据写出器、发布目录聚合器
@unipty/www —(私有) 静态文档站 → unipty.jixoai.com
@unipty/example —(私有) 本地演示:WebSocket 多 tab 终端,每个 backend 一个运行时

一致性与兼容性证据

每一条支持声明都经过同一接缝:公共一致性套件针对已安装的包制品运行(pack、安装进隔离消费者、只经公共导出驱动)。原生全量通过产出一条正向 Verification Evidence 记录;确定性聚合器校验身份/元组/提交唯一性并产出发布目录,文档站点原样消费它。失败只是 CI 诊断——绝不会变成永久的「不支持」声明。当前发布的逐元组事实见兼容性目录

本地运行:

pnpm --filter @unipty/conformance run conformance --backend node-pty --emit-evidence

文档地图

按意图找去处:

你想…… 去处
深入阅读 API 文档站 · unipty README
选引擎、看其选项与限制 对应路由的 README(从「选型」的路由表进入)
自动发现 Backend 或打包部署 获取层 README · helper README
查每个运行时/平台验证了什么 兼容性目录
本地跑一个活的终端演示 packages/examplepnpm example
理解设计决策 架构设计.md · 能力规格
参与贡献 贡献规范.md
提 Issue / 讨论 GitHub Issues

路线说明:v1 聚焦 PTY;持久化、重连、远程主机属于可替换 Backend 与 wrapper,而不是第二套插件生命周期。

开发

corepack pnpm install
pnpm build && pnpm typecheck && pnpm test
pnpm --filter @unipty/backend-zigpty test   # zigpty 套件(真实 PTY)
pnpm --filter @unipty/backend-bun test      # Bun 套件(需要 Bun)
cd packages/backend-deno-sigma__pty-ffi && deno test -A test/   # Deno 套件
pnpm check:arch                             # 包图所有权规则

许可证

MIT