面向 Node、Bun、Deno 的运行时无关 PTY —— 一套公共契约,可由开发者显式选择的 Backend。
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.code:unsupported、closed、backpressure、invalid-argument、active-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:UniPty、Pty、Backend/Endpoint 接缝、公共错误 |
@unipty/backend |
npm | 获取便利层:resolveUniPtyBackend、inspectUniPtyBackend、autoResolveUniPtyBackend、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/example(pnpm 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 # 包图所有权规则
