文档
使用 UniPty
公共契约、如何获取 Backend、每条官方路由到底是什么,以及一个静态文档站在浏览器里能做什么、不能做什么。
Layer 1
Core 拥有公共面
流、bootstrap 缓冲、UTF-8 转换、背压、公共错误码与生命周期状态。一个 UniPty 实例持有一个就绪 Backend,可以创建多个相互独立的 PTY。
Layer 2
Endpoint 是那条接缝
每个 Backend 提供一个 Core 私有 Endpoint:单一有序原生分块源、带 drain 的同步写入、resize、terminate/close,以及独立于传输 EOF、可重复 await 的退出观察。
Layer 3
Backend 就绪先于 Core
工厂或 .ready() 在 new UniPty(options) 之前完成一次性的运行时加载、连接与能力协商。此后 spawn、write、resize、terminate、close 全部保持同步。
安装
Core 加上你选的引擎
装哪个 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 | import via "npm:@unipty/backend-deno-sigma__pty-ffi" | 内嵌 @sigma/pty-ffi 动态库 |
换引擎只是一行改动——换一个 Backend 获取,其余代码完全一致:
import { UniPty } from "unipty";
import { createNodePtyBackend } from "@unipty/backend-node-pty";
import { createZigptyBackend } from "@unipty/backend-zigpty";
import { createBunBackend } from "@unipty/backend-bun";
import { createDenoSigmaPtyFfiBackend } from "@unipty/backend-deno-sigma__pty-ffi";
// Pick the engine by acquiring a different Backend — every line below the
// constructor is identical on all four routes:
const unipty = new UniPty({ backend: await createZigptyBackend() });引擎专属选项(encoding、writeDecode、队列调优、FFI 权限)与行为限制见各包 README,路由表中已附链接。
Core 用法
公共契约
Core 绝不替你加载、命名或解析 Backend。以下每个操作都是运行时无关的,在 Node、Bun、Deno 上完全一致。
以就绪 Backend 构造
工厂(或 .ready())先完成一次性运行时加载;随后 Core 接受就绪实例。具体 Backend 类型被保留,并以只读方式暴露。
import { UniPty } from "unipty";
import { createNodePtyBackend, NodePtyBackend } from "@unipty/backend-node-pty";
const backend: NodePtyBackend = await createNodePtyBackend();
const unipty = new UniPty({ backend });
unipty.backend === backend; // readonly, concrete type preserved以结构化 argv 启动
启动入口是 unipty.spawn(argv, options):argv 非空、首值是可执行文件、没有字符串命令重载。Core 绝不隐式调用 shell。初始几何位于 terminal: { cols, rows }(字符单元格);省略的维度按值 → COLUMNS/LINES → 可信宿主 TTY 探测 → 80 × 24 独立解析。
const pty = unipty.spawn(["/bin/sh", "-i", "-l"], {
terminal: { cols: 120, rows: 40 },
env: { TERM: "xterm-256color" }, // launch context; never overrides geometry
});每 PTY 一条流
stream() 选择表示:Terminal Text(ReadableStream<string>)或 Terminal Bytes(ReadableStream<Uint8Array>)。每 PTY 只有一条活跃流——第二次调用以 active-stream 失败;扇出请用调用方自有的 tee()。取消流只脱离该视图:既不关闭输入,也不终止子进程。启动输出保存在有界 bootstrap 缓冲中,直到第一个视图挂接。
const text = pty.stream({ encoding: "utf8" }); // ReadableStream<string>
const bytes = pty.stream({ encoding: "bytes" }); // only after the first detaches
const reader = text.getReader();
for (;;) {
const { done, value } = await reader.read();
if (done) break;
render(value);
}以布尔就绪写入
write() 接受 string | Uint8Array 并返回布尔。任一返回值都表示整个值被恰好接受一次;false 只是「暂停并 drain」。背压是建议性的,但饱和会以 backpressure 码拒绝整个值——绝不部分接受、绝不静默丢弃、绝无无界队列。
if (!pty.write("ls -la\r")) {
await pty.drain(); // readiness recovery, not a physical flush
}Resize
resize(cols, rows) 只接受正整数(字符单元格)。像素维度保持 Backend 专属;不能 resize 的 Backend 会显式报告 unsupported。
pty.resize(120, 40);terminate 与 close 非级联
terminate() 是幂等的同步终止请求。close() 是幂等的同步逻辑关闭:返回前发布 closed、令所有 I/O 面失效、让活跃流正常完成——但它不终止子进程,terminate 也不关闭传输。
pty.terminate(); // request only; exit is observed independently
pty.close(); // publishes closed; further write/resize/stream() reject with "closed"
console.log(pty.closed);退出是独立观察
exited 是 { exitCode, signal } 的可重复 promise。它独立于传输 EOF、流取消与 close:已建立的退出观察在 close 之后依然有效;signal 记录观察到的终止原因,不是通用的 kill(signal) 词汇。所有路由上 exec 失败都是退出观察(绝不是 spawn 异常);信号致死保留各引擎自身的报告形状——见能力差异矩阵。
const result = await pty.exited;
console.log(result.exitCode, result.signal); // number | null, string | null能力与错误码
Backend 扩展搭在不透明能力 token 之上,按对象身份查找——没有字符串注册表,没有回退。操作失败携带稳定错误码:unsupported、closed、backpressure、invalid-argument、active-stream。
import { defineCapabilityToken } from "unipty";
// Capability tokens are Backend-owned singletons; Core matches object
// identity only (no string registry, no name fallback). No official
// Backend ships a token yet — this is the intended shape when one does:
interface SignalsCapability { kill(signal: string): void }
const signalsCapability = defineCapabilityToken<SignalsCapability>();
const signals = pty.capability(signalsCapability);
if (signals) signals.kill("SIGHUP"); // Backend vocabulary; explicit, never silent释放 Backend 持有者
UniPty.dispose() 阻止新 spawn,保留既有 PTY 为调用方所有,等待它们关闭,然后恰好一次释放共享 Backend 资源。重复调用复用同一 promise。
await unipty.dispose();Backend 获取
获取就绪 Backend
手动导入是一等路径且永远不会消失;AutoResolve 是其上的便利层;纯解析与检查保持无副作用。
手动导入——一等路径
const { createBunBackend } = await import("@unipty/backend-bun");
const backend = await createBunBackend();
const unipty = new UniPty({ backend });AutoResolve
autoResolveUniPtyBackend 分析当前运行时,先处理你的显式候选(不可用候选发出结构化警告),再回退到从 package.json 依赖推断的候选。回退要求恰好一个兼容结果;多个则产生 ambiguous。选定候选的初始化是终止性的——失败以结构化 backend-initialization 码报告,绝不静默换下一个 Backend 重试。
import { autoResolveUniPtyBackend } from "@unipty/backend";
const backend = await autoResolveUniPtyBackend({
candidates: ["@unipty/backend-node-pty", "@unipty/backend-bun"],
from: import.meta.url, // caller-rooted base
onWarning: (warning) => console.warn(warning.code, warning.packageName),
});纯解析与检查
resolveUniPtyBackend 一次解析一个包位置,并要求显式的调用方 from: URL;inspectUniPtyBackend 只导入无副作用的 metadata 子路径——绝不触碰 Backend 入口模块或工厂。两个阶段都不初始化任何东西。
import { resolveUniPtyBackend, inspectUniPtyBackend } from "@unipty/backend";
const report = await resolveUniPtyBackend("@unipty/backend-deno-sigma__pty-ffi", {
from: import.meta.url,
});
if (report.status === "resolved") {
const inspection = await inspectUniPtyBackend(report);
if (inspection.status === "compatible") {
/* metadata-compatible with this Core; still no native initialization */
}
}打包部署:显式 manifest
打包器无法解析运行时包图。用 helper CLI 生成显式构建期 manifest,再让 AutoResolve 从中选择。生成的模块默认导出一个 manifest,静态导入各包的 ./unipty.metadata,并把 Backend 入口 import 留在延迟加载器里——求值 manifest 不导入任何 Backend 入口,也不初始化任何东西。
pnpm unipty-helper-backend manifest \
--candidate @unipty/backend-node-pty \
--candidate @unipty/backend-bun \
--candidate @unipty/backend-deno-sigma__pty-ffi \
--out src/unipty-backends.manifest.tsimport backendManifest from "./unipty-backends.manifest";
const backend = await autoResolveUniPtyBackend({
manifest: backendManifest,
candidates: ["@unipty/backend-node-pty"],
});官方路由
如实声明的底层实现
每个官方包都在元数据 provenance 中声明底层实现。这些声明都不是支持宣称——只有证据目录能说 verified。
| 包 | 运行时 | 底层实现 | 说明 |
|---|---|---|---|
@unipty/backend-node-pty | Node | node-pty via @lydell/node-pty prebuilds | 第三方原生插件,随包附带预构建二进制。Node 没有原生 PTY API;本路由如实封装生态标准底层,而不是假装不然。 |
@unipty/backend-zigpty | Node | zigpty (Zig-built NAPI prebuilds) | 第二条 Node 路由,底层为 Zig 实现。写入是文本原生的(字节需要 writeDecode 选项);无预编译的元组会让就绪以 unsupported 失败,而不是静默降级为管道。 |
@unipty/backend-bun | Bun | Bun.Terminal | Bun 内建终端 API:Linux/macOS 自 Bun 1.3.13 起,Windows 经 ConPTY 自 1.3.14 起。支持是带版本的证据,不是笼统宣称。 |
@unipty/backend-deno-sigma__pty-ffi | Deno | @sigma/pty-ffi (Rust portable-pty) | 仅以 npm 发布的包,构建期内嵌 @sigma/pty-ffi/noinit JavaScript 闭包与目标动态库。Deno 需以 -A 或 --allow-ffi --allow-read --allow-run 运行(terminate() 经 pgrep 发现子进程 pid);没有默认下载或缓存。 |
引擎能力差异(各底座实际给到什么)
每条路由的公共契约完全一致;底层引擎并不相同。✓ 开箱即用,⚠ 需要选项或带有已声明的限制,✗ 不提供。
| 能力 | node-pty | zigpty | bun | deno-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(Windows 上空操作,改由适配层 outputSpool 兜底);bun 无传输级流控;deno 的 FFI 读端排入内部缓冲。 |
| 独立传输 EOF 信号 | ✓(close 事件) | ⚠ 真信号 + 兜底 | ⚠ 回调 + 兜底 | ✓(读循环 done) | zigpty 在 exit 时接管主读流(真实 end/close),50ms 静默窗由迟到 chunk 续期兜底;bun 以 Terminal exit 回调为主、exited 合成为兜底。 |
| 传输读错误可上报 | ✓ unsupported | ✗ 不可区分 | ✓ | ✓ unsupported | zigpty 底层完全吞掉流错误;其余三条会把错误打到流上——读失败绝不会被静默当作干净 EOF。 |
| 信号致死观察 | signal 名 | signal 名、exitCode 0 | signal 名、exitCode null | exitCode 1、signal null | 各引擎报告形状不同;适配器逐字透传,绝不伪造引擎没有报告的值。 |
| 底层分发形态 | 平台子包 | 零依赖随包(8 元组) | 运行时内置 | 内嵌动态库 | deno 还需要 FFI 权限;zigpty 完全没有安装脚本;node-pty 只装当前平台的二进制。 |
所有路由上 exec 失败都是退出观察(绝不是 spawn 异常);各适配器的细节与选项见各包 README。
元数据协议
./unipty.metadata,无副作用
每个官方 Backend 包都暴露无副作用的 ./unipty.metadata 子路径。最小 schema 携带包身份、Backend 身份、工厂导出名、Core 协议,以及用于无副作用预过滤的目标声明——仅此而已。
{
"schema": 1,
"package": { "name": "@unipty/backend-node-pty", "version": "0.2.0" },
"backend": { "id": "node-pty", "factoryExport": "createNodePtyBackend" },
"protocol": { "core": [1] },
"targets": [{ "runtime": "node" }],
"provenance": {
"kind": "third-party",
"substrate": "node-pty (@lydell/node-pty prebuilt distribution)"
}
}目标声明使用规范化的 Node/npm token:os 跟随 process.platform/npm os,arch 跟随 process.arch/npm cpu,libc 是独立的、仅限 Linux 的原生证据轴。可选 provenance 描述实现种类与底层实现;元数据不含成熟度、能力或 verified 支持宣称。
浏览器限制
浏览器标签页里没有 PTY
浏览器不暴露任何伪终端 API,UniPty 也不假装如此。本网站是一个静态文档面。
- 它绝不在浏览器中导入或初始化原生 Backend。
- 它绝不执行本地 PTY 操作;这里没有任何可 spawn 的东西。
- 它的兼容性页面在构建期由一个发布目录制品完全预渲染——浏览器端不做任何证据重算。
为浏览器客户端运行终端,意味着在浏览器之外托管一个 UniPty Backend 并经传输流式转发——v1 刻意把这种安排留给 Backend 拥有者,而不是 Core。