@zq-silk/yui 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/doctor/doctor.js +2 -0
- package/dist/doctor/ptyProbe.js +74 -0
- package/dist/doctor/ptyProbeChild.js +90 -0
- package/docs/testing/verification-levels.md +10 -0
- package/docs/testing/verification-levels.zh-CN.md +8 -0
- package/i18n/README.zh-CN.md +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ not from terminal windows you juggle or details you have to remember.
|
|
|
43
43
|
## Quick start
|
|
44
44
|
|
|
45
45
|
You need Linux x64 (glibc) or macOS (x64 or Apple Silicon), Git, tmux, and
|
|
46
|
-
Node.js `^20.17.0`, `^22.9.0` or `^
|
|
46
|
+
Node.js `^20.17.0`, `^22.9.0`, `^24.0.0` or `^26.0.0`. On macOS install tmux with
|
|
47
47
|
`brew install tmux`. For the simplest setup, have Codex CLI or Claude Code CLI
|
|
48
48
|
installed and ready to use with your own account. Yui coordinates those
|
|
49
49
|
Agents; it does not supply model access.
|
package/dist/doctor/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { accessSync, constants, existsSync, lstatSync, realpathSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
3
|
import Database from "better-sqlite3";
|
|
4
|
+
import { inspectPty } from "./ptyProbe.js";
|
|
4
5
|
import { configuredAgentToDefinition, resolveAgentEnvironment } from "../agent/agent.js";
|
|
5
6
|
import { operationalAgentEnvironment } from "../agent/launchEnvironment.js";
|
|
6
7
|
import { resolveTmuxBin } from "../config/yuiConfig.js";
|
|
@@ -137,6 +138,7 @@ async function inspectDoctor(env, executor) {
|
|
|
137
138
|
controller,
|
|
138
139
|
...(domain === undefined ? [] : [domain]),
|
|
139
140
|
...toolChecks,
|
|
141
|
+
...inspectPty(),
|
|
140
142
|
...agentChecks,
|
|
141
143
|
...review.checks
|
|
142
144
|
],
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
/** Native loading and spawn happen outside Doctor: a native abort cannot kill the CLI. */
|
|
6
|
+
export function inspectPty(options = {}) {
|
|
7
|
+
const started = Date.now();
|
|
8
|
+
const checks = [];
|
|
9
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
10
|
+
if (![20, 22, 24, 26].includes(major)) {
|
|
11
|
+
checks.push({
|
|
12
|
+
name: "pty Node runtime", status: "unsupported",
|
|
13
|
+
detail: `Node ${process.versions.node} is outside Yui's supported Node 20/22/24/26 range; this is not evidence of a helper permission failure.`
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
let entry;
|
|
17
|
+
let identity;
|
|
18
|
+
try {
|
|
19
|
+
const require = createRequire(options.requireFrom ?? import.meta.url);
|
|
20
|
+
entry = require.resolve("node-pty");
|
|
21
|
+
const manifest = require.resolve("node-pty/package.json");
|
|
22
|
+
const version = JSON.parse(readFileSync(manifest, "utf8")).version;
|
|
23
|
+
identity = `node-pty ${version}; entry=${entry}; package=${manifest}`;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return [...checks, {
|
|
27
|
+
name: "node-pty",
|
|
28
|
+
status: error.code === "MODULE_NOT_FOUND" ? "missing" : "invalid",
|
|
29
|
+
detail: `Dependency resolution failed: ${String(error)}`
|
|
30
|
+
}];
|
|
31
|
+
}
|
|
32
|
+
const timeoutMs = options.timeoutMs ?? 1_500;
|
|
33
|
+
const result = spawnSync(process.execPath, [
|
|
34
|
+
fileURLToPath(new URL("./ptyProbeChild.js", import.meta.url)),
|
|
35
|
+
entry, String(options.probeTimeoutMs ?? 750)
|
|
36
|
+
], {
|
|
37
|
+
// No NODE_OPTIONS, shell startup files, preload hooks or user configuration.
|
|
38
|
+
env: { PATH: "/usr/bin:/bin", LANG: "C" },
|
|
39
|
+
cwd: "/",
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
timeout: timeoutMs,
|
|
42
|
+
killSignal: "SIGKILL",
|
|
43
|
+
maxBuffer: 64 * 1024
|
|
44
|
+
});
|
|
45
|
+
for (const line of (result.stdout ?? "").split("\n").filter(Boolean)) {
|
|
46
|
+
try {
|
|
47
|
+
const check = JSON.parse(line);
|
|
48
|
+
if (["node-pty", "pty helper", "pty spawn"].includes(check.name)
|
|
49
|
+
&& ["ok", "missing", "invalid"].includes(check.status)
|
|
50
|
+
&& typeof check.detail === "string") {
|
|
51
|
+
checks.push({ ...check, detail: `${identity}; ${check.detail}` });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch { /* Native diagnostics on stdout are retained below on failure. */ }
|
|
55
|
+
}
|
|
56
|
+
const completed = checks.some(check => check.name === "pty spawn")
|
|
57
|
+
|| checks.some(check => check.status !== "ok" && check.name !== "pty Node runtime");
|
|
58
|
+
if (result.error || result.signal || result.status !== 0 || !completed) {
|
|
59
|
+
const timedOut = result.error?.code === "ETIMEDOUT";
|
|
60
|
+
// A partial success line is not success if the isolated process failed to finish.
|
|
61
|
+
for (let i = 0; i < checks.length; i += 1) {
|
|
62
|
+
if (checks[i].name === "pty spawn" && checks[i].status === "ok") {
|
|
63
|
+
checks[i] = { ...checks[i], status: "invalid", detail: `${checks[i].detail}; isolated probe did not finish successfully` };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
checks.push({
|
|
67
|
+
name: "pty probe", status: "invalid",
|
|
68
|
+
detail: `${identity}; ${timedOut ? `timeout after ${timeoutMs}ms` : "isolated probe failed"}; exit=${result.status} signal=${result.signal}; ${result.error?.message ?? ""} ${(result.stderr || result.stdout || "").trim()}`
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return checks.map(check => ({
|
|
72
|
+
...check, detail: `${check.detail}; elapsed=${Date.now() - started}ms`
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { accessSync, constants, statSync, writeSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const entry = process.argv[2];
|
|
6
|
+
const timeoutMs = Number(process.argv[3]);
|
|
7
|
+
const report = (check) => writeSync(1, `${JSON.stringify(check)}\n`);
|
|
8
|
+
const errorDetail = (error) => error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
9
|
+
async function probe() {
|
|
10
|
+
let pty;
|
|
11
|
+
try {
|
|
12
|
+
pty = require(entry);
|
|
13
|
+
if (typeof pty.spawn !== "function")
|
|
14
|
+
throw new Error("node-pty does not expose spawn");
|
|
15
|
+
report({ name: "node-pty", status: "ok", detail: "native module loaded" });
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
report({
|
|
19
|
+
name: "node-pty", status: "invalid",
|
|
20
|
+
detail: `native load failed: ${errorDetail(error)}. If installation reported blocked lifecycle scripts, review npm allow-scripts separately; no install files were changed.`
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (process.platform === "darwin") {
|
|
25
|
+
// The pinned node-pty selects build/Release, build/Debug or prebuilds.
|
|
26
|
+
// Inspect the module it actually loaded, not the first helper that exists.
|
|
27
|
+
const native = Object.keys(require.cache).find(path => basename(path) === "pty.node");
|
|
28
|
+
if (!native) {
|
|
29
|
+
report({ name: "pty helper", status: "invalid", detail: "Cannot identify the loaded pty.node; helper path is unverified." });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const helper = join(dirname(native), "spawn-helper");
|
|
33
|
+
let mode = "unknown";
|
|
34
|
+
try {
|
|
35
|
+
mode = (statSync(helper).mode & 0o7777).toString(8).padStart(4, "0");
|
|
36
|
+
accessSync(helper, constants.X_OK);
|
|
37
|
+
report({ name: "pty helper", status: "ok", detail: `helper=${helper}; mode=${mode}; executable by current user` });
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const code = error.code;
|
|
41
|
+
report({
|
|
42
|
+
name: "pty helper", status: code === "ENOENT" ? "missing" : "invalid",
|
|
43
|
+
detail: `helper=${helper}; mode=${mode}; ${code === "EACCES" ? "permission denied for current user" : "helper check failed"}: ${errorDetail(error)}`
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
await new Promise(resolve => {
|
|
49
|
+
const marker = "yui-pty-probe-ok";
|
|
50
|
+
let terminal;
|
|
51
|
+
let output = "";
|
|
52
|
+
let finished = false;
|
|
53
|
+
const finish = (status, detail, kill = false) => {
|
|
54
|
+
if (finished)
|
|
55
|
+
return;
|
|
56
|
+
finished = true;
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
if (kill && terminal) {
|
|
59
|
+
try {
|
|
60
|
+
terminal.kill("SIGKILL");
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
detail += `; cleanup: ${errorDetail(error)}`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
report({ name: "pty spawn", status, detail });
|
|
67
|
+
resolve();
|
|
68
|
+
};
|
|
69
|
+
const timer = setTimeout(() => finish("invalid", `timeout after ${timeoutMs}ms waiting for output and normal exit`, true), timeoutMs);
|
|
70
|
+
try {
|
|
71
|
+
// Absolute, fixed executable; no shell, Node preloads, Agent or account access.
|
|
72
|
+
terminal = pty.spawn("/bin/echo", [marker], {
|
|
73
|
+
name: "xterm", cols: 80, rows: 24, cwd: "/",
|
|
74
|
+
env: { PATH: "/usr/bin:/bin", LANG: "C" }
|
|
75
|
+
});
|
|
76
|
+
terminal.onData(data => { output = (output + data).slice(-4096); });
|
|
77
|
+
terminal.onExit(({ exitCode, signal }) => {
|
|
78
|
+
const ok = exitCode === 0 && !signal && output.replace(/\r/g, "").trim() === marker;
|
|
79
|
+
finish(ok ? "ok" : "invalid", `${ok ? "expected output and normal exit confirmed (PTY dependency only; Web Terminal and Provider protocols unverified)" : "unexpected output or exit"}; exit=${exitCode}; signal=${signal ?? 0}; output=${JSON.stringify(output)}`);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
finish("invalid", `spawn failed: ${errorDetail(error)}`, true);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
await probe();
|
|
88
|
+
// node-pty's native watcher can retain handles; the fixed child has exited, or
|
|
89
|
+
// was explicitly killed on failure. The parent also bounds native hangs/aborts.
|
|
90
|
+
process.exit(0);
|
|
@@ -101,6 +101,8 @@ do not add prose-matching tests or claim model validation from static checks.
|
|
|
101
101
|
`ci.yml` runs core plus one assembled-package normal-path smoke per supported
|
|
102
102
|
platform (Linux x64, Mac Intel and Apple Silicon) on every PR, without another
|
|
103
103
|
lint or broad regression suite.
|
|
104
|
+
Linux runs on Node 24 and 26; release fresh-install smoke covers Node 20, 22,
|
|
105
|
+
24 and 26 on all three platforms.
|
|
104
106
|
`node scripts/smoke-runtime-package.mjs --assembled .release-stage` exercises
|
|
105
107
|
the actual CLI/Controller/Host/SQLite and isolated tmux, replacing only the
|
|
106
108
|
external Provider with a deterministic fixture. It covers setup, durable input
|
|
@@ -118,6 +120,14 @@ freshly installed package through
|
|
|
118
120
|
artifact and provenance boundaries. This validates runtime integration, not
|
|
119
121
|
real-model behavior. Pure contract and safety tests remain in `test/core`;
|
|
120
122
|
production wiring is exercised here rather than only through mocked ports.
|
|
123
|
+
The native dependency check launches a fixed local program through PTY and
|
|
124
|
+
requires its output and normal exit. Installed-package smoke explicitly checks
|
|
125
|
+
default Doctor's PTY results using dependencies resolved from the consumer.
|
|
126
|
+
On macOS it also tests a disposable copy with a non-executable spawn-helper:
|
|
127
|
+
Doctor must report the selected helper path and permissions without repairing it.
|
|
128
|
+
Doctor's isolated PTY probe waits up to 750 ms for output/exit, with a 1.5-second
|
|
129
|
+
outer native-process limit, and reports elapsed time. Existing Agent/Controller
|
|
130
|
+
diagnostics have their own costs; this is not a one-second whole-Doctor guarantee.
|
|
121
131
|
The package smoke also checks unconditional status identity and update-owned
|
|
122
132
|
resource/identity capture through the assembled package. Real lifecycle children
|
|
123
133
|
stop the exact Controller and restore its captured launch identity while their
|
|
@@ -74,6 +74,8 @@ package-start 检查跟随已安装树中的本地 Skill 引用,包括跨 Role
|
|
|
74
74
|
|
|
75
75
|
`ci.yml` 在每个 PR 上构建一次,运行 core 及一个组装包正常链路检查,不重复 lint,
|
|
76
76
|
也不增加宽泛回归套件。
|
|
77
|
+
Linux CI 在 Node 24 和 26 上运行;发布时的全新安装 smoke 覆盖三个平台上的
|
|
78
|
+
Node 20、22、24 和 26。
|
|
77
79
|
`node scripts/smoke-runtime-package.mjs --assembled .release-stage` 经过真实
|
|
78
80
|
CLI/Controller/Host/SQLite 与隔离 tmux,仅用确定性夹具替换外部 Provider。它验证
|
|
79
81
|
setup、输入跨重启持久化及幂等、scratch 激活、原生结果入库、完成后保留会话、
|
|
@@ -85,6 +87,12 @@ setup、输入跨重启持久化及幂等、scratch 激活、原生结果入库
|
|
|
85
87
|
npm bin、依赖、受支持 Node 版本、产物和 provenance 边界。这证明运行时集成,不证明
|
|
86
88
|
真实模型行为。纯契约与安全检查保留在 `test/core`,生产组件组装在这里验证,
|
|
87
89
|
不只依赖模拟端口。
|
|
90
|
+
原生依赖检查必须通过 PTY 启动固定本地程序,确认输出和正常退出。安装包 smoke
|
|
91
|
+
明确断言默认 Doctor 的 PTY 检查成功,并从消费者安装目录解析依赖。macOS 还会
|
|
92
|
+
在可丢弃的依赖副本中移除 spawn-helper 执行权限,验证 Doctor 报出实际 helper
|
|
93
|
+
路径和权限原因且不自动修复。Doctor 的隔离 PTY 探针等待输出/退出最多 750 ms,
|
|
94
|
+
外层原生进程限制为 1.5 秒,并报告耗时;原有 Agent/Controller 检查另有成本,
|
|
95
|
+
这不是整个 Doctor 一秒内返回的保证。
|
|
88
96
|
组装包检查还验证固定身份输出,以及升级侧通过组装包采集资源和精确 Controller 身份,
|
|
89
97
|
并在父进程持有交接锁时,通过真实生命周期子进程停止精确 Controller、恢复其已捕获的
|
|
90
98
|
启动身份。无关调用仍被锁阻止,锁保持由父进程持有,持久输入不变;
|
package/i18n/README.zh-CN.md
CHANGED
|
@@ -41,7 +41,7 @@ npm 包包含 Linux x64、Mac Intel 和 Apple Silicon 的 Yui 预编译程序。
|
|
|
41
41
|
同一份包在三个平台验证后发布,运行时只选择对应平台的程序。
|
|
42
42
|
|
|
43
43
|
需要 Linux x64 / glibc 或 macOS(x64 或 Apple Silicon)、Git、tmux,以及
|
|
44
|
-
Node.js `^20.17.0`、`^22.9.0` 或 `^
|
|
44
|
+
Node.js `^20.17.0`、`^22.9.0`、`^24.0.0` 或 `^26.0.0`。macOS 上可用
|
|
45
45
|
`brew install tmux` 安装 tmux。最简单的方式是先安装 Codex CLI 或 Claude
|
|
46
46
|
Code CLI,并确保它已经可以使用你自己的账号正常工作。Yui 负责协调 Agent,
|
|
47
47
|
不提供模型访问额度。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zq-silk/yui",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
21
|
"engines": {
|
|
22
|
-
"node": "^20.17.0 || ^22.9.0 || ^24.0.0"
|
|
22
|
+
"node": "^20.17.0 || ^22.9.0 || ^24.0.0 || ^26.0.0"
|
|
23
23
|
},
|
|
24
24
|
"os": [
|
|
25
25
|
"linux",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@xterm/addon-fit": "^0.11.0",
|
|
50
50
|
"@xterm/xterm": "^6.0.0",
|
|
51
51
|
"better-sqlite3": "^12.11.1",
|
|
52
|
-
"node-pty": "
|
|
52
|
+
"node-pty": "1.2.0-beta.15",
|
|
53
53
|
"smol-toml": "1.8.0",
|
|
54
54
|
"ws": "^8.21.1"
|
|
55
55
|
}
|