Skip to content

文档

使用 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 包,就得到哪个引擎。不知道选哪个?路由表下方的能力差异矩阵会告诉你每个引擎实际提供什么。

运行时安装引擎
Nodenpm install unipty @unipty/backend-node-pty第三方 node-pty 预构建
Nodenpm install unipty @unipty/backend-zigpty第三方 zigpty(Zig 构建、零依赖)
Bunbun add unipty @unipty/backend-bun运行时原生 Bun.Terminal
Denoimport via "npm:@unipty/backend-deno-sigma__pty-ffi"内嵌 @sigma/pty-ffi 动态库

换引擎只是一行改动——换一个 Backend 获取,其余代码完全一致:

engine swap
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 类型被保留,并以只读方式暴露。

construct
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 独立解析。

spawn
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 缓冲中,直到第一个视图挂接。

stream
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 码拒绝整个值——绝不部分接受、绝不静默丢弃、绝无无界队列。

write
if (!pty.write("ls -la\r")) {
  await pty.drain(); // readiness recovery, not a physical flush
}

Resize

resize(cols, rows) 只接受正整数(字符单元格)。像素维度保持 Backend 专属;不能 resize 的 Backend 会显式报告 unsupported。

resize
pty.resize(120, 40);

terminate 与 close 非级联

terminate() 是幂等的同步终止请求。close() 是幂等的同步逻辑关闭:返回前发布 closed、令所有 I/O 面失效、让活跃流正常完成——但它不终止子进程,terminate 也不关闭传输。

lifecycle
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 异常);信号致死保留各引擎自身的报告形状——见能力差异矩阵。

exit
const result = await pty.exited;
console.log(result.exitCode, result.signal); // number | null, string | null

能力与错误码

Backend 扩展搭在不透明能力 token 之上,按对象身份查找——没有字符串注册表,没有回退。操作失败携带稳定错误码:unsupported、closed、backpressure、invalid-argument、active-stream。

capability
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。

dispose
await unipty.dispose();

Backend 获取

获取就绪 Backend

手动导入是一等路径且永远不会消失;AutoResolve 是其上的便利层;纯解析与检查保持无副作用。

手动导入——一等路径

manual
const { createBunBackend } = await import("@unipty/backend-bun");
const backend = await createBunBackend();
const unipty = new UniPty({ backend });

AutoResolve

autoResolveUniPtyBackend 分析当前运行时,先处理你的显式候选(不可用候选发出结构化警告),再回退到从 package.json 依赖推断的候选。回退要求恰好一个兼容结果;多个则产生 ambiguous。选定候选的初始化是终止性的——失败以结构化 backend-initialization 码报告,绝不静默换下一个 Backend 重试。

autoresolve
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 入口模块或工厂。两个阶段都不初始化任何东西。

resolve
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 入口,也不初始化任何东西。

helper CLI
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.ts
manifest
import backendManifest from "./unipty-backends.manifest";

const backend = await autoResolveUniPtyBackend({
  manifest: backendManifest,
  candidates: ["@unipty/backend-node-pty"],
});

官方路由

如实声明的底层实现

每个官方包都在元数据 provenance 中声明底层实现。这些声明都不是支持宣称——只有证据目录能说 verified。

运行时底层实现说明
@unipty/backend-node-ptyNodenode-pty via @lydell/node-pty prebuilds第三方原生插件,随包附带预构建二进制。Node 没有原生 PTY API;本路由如实封装生态标准底层,而不是假装不然。
@unipty/backend-zigptyNodezigpty (Zig-built NAPI prebuilds)第二条 Node 路由,底层为 Zig 实现。写入是文本原生的(字节需要 writeDecode 选项);无预编译的元组会让就绪以 unsupported 失败,而不是静默降级为管道。
@unipty/backend-bunBunBun.TerminalBun 内建终端 API:Linux/macOS 自 Bun 1.3.13 起,Windows 经 ConPTY 自 1.3.14 起。支持是带版本的证据,不是笼统宣称。
@unipty/backend-deno-sigma__pty-ffiDeno@sigma/pty-ffi (Rust portable-pty)仅以 npm 发布的包,构建期内嵌 @sigma/pty-ffi/noinit JavaScript 闭包与目标动态库。Deno 需以 -A 或 --allow-ffi --allow-read --allow-run 运行(terminate() 经 pgrep 发现子进程 pid);没有默认下载或缓存。

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

每条路由的公共契约完全一致;底层引擎并不相同。✓ 开箱即用,⚠ 需要选项或带有已声明的限制,✗ 不提供。

能力node-ptyzigptybundeno-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✗ 不可区分✓ unsupportedzigpty 底层完全吞掉流错误;其余三条会把错误打到流上——读失败绝不会被静默当作干净 EOF。
信号致死观察signal 名signal 名、exitCode 0signal 名、exitCode nullexitCode 1、signal null各引擎报告形状不同;适配器逐字透传,绝不伪造引擎没有报告的值。
底层分发形态平台子包零依赖随包(8 元组)运行时内置内嵌动态库deno 还需要 FFI 权限;zigpty 完全没有安装脚本;node-pty 只装当前平台的二进制。

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

元数据协议

./unipty.metadata,无副作用

每个官方 Backend 包都暴露无副作用的 ./unipty.metadata 子路径。最小 schema 携带包身份、Backend 身份、工厂导出名、Core 协议,以及用于无副作用预过滤的目标声明——仅此而已。

unipty.metadata
{
  "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。