@neta-art/cohub-cli 7.1.1 → 8.0.0
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 +40 -2
- package/dist/auth.js +38 -5
- package/dist/client.js +5 -2
- package/dist/commands/runtime.d.ts +1 -2
- package/dist/commands/runtime.js +178 -302
- package/dist/commands/sandboxd-binary.d.ts +3 -3
- package/dist/commands/sandboxd-binary.js +16 -12
- package/dist/runtime/archive-store.d.ts +2 -0
- package/dist/runtime/archive-store.js +20 -6
- package/dist/runtime/connection.d.ts +4 -2
- package/dist/runtime/connection.js +80 -23
- package/dist/runtime/diagnostics.d.ts +3 -0
- package/dist/runtime/diagnostics.js +3 -0
- package/dist/runtime/harness.d.ts +3 -0
- package/dist/runtime/harness.js +34 -1
- package/dist/runtime/instance.d.ts +5 -0
- package/dist/runtime/instance.js +159 -0
- package/dist/runtime/launch.d.ts +20 -0
- package/dist/runtime/launch.js +176 -0
- package/dist/runtime/native-codex-hook.d.ts +1 -0
- package/dist/runtime/native-codex-hook.js +28 -0
- package/dist/runtime/native-install.d.ts +21 -0
- package/dist/runtime/native-install.js +130 -0
- package/dist/runtime/native-ipc.d.ts +26 -0
- package/dist/runtime/native-ipc.js +101 -0
- package/dist/runtime/native-pi-extension.d.ts +20 -0
- package/dist/runtime/native-pi-extension.js +47 -0
- package/dist/runtime/native-sync-store.d.ts +97 -0
- package/dist/runtime/native-sync-store.js +365 -0
- package/dist/runtime/native-sync.d.ts +25 -0
- package/dist/runtime/native-sync.js +128 -0
- package/dist/runtime/native-transcript.d.ts +27 -0
- package/dist/runtime/native-transcript.js +281 -0
- package/dist/runtime/presentation.d.ts +21 -0
- package/dist/runtime/presentation.js +76 -0
- package/dist/runtime/session-store.d.ts +2 -0
- package/dist/runtime/session-store.js +40 -5
- package/dist/runtime/space-binding.d.ts +3 -0
- package/dist/runtime/space-binding.js +43 -6
- package/dist/runtime/supervisor.d.ts +16 -0
- package/dist/runtime/supervisor.js +277 -0
- package/dist/runtime/worker.d.ts +1 -0
- package/dist/runtime/worker.js +20 -0
- package/package.json +3 -2
package/dist/commands/runtime.js
CHANGED
|
@@ -1,328 +1,204 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { stat } from "node:fs/promises";
|
|
4
|
-
import { basename, resolve } from "node:path";
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
2
|
import { createInterface } from "node:readline/promises";
|
|
6
|
-
import { isLocalHarness, resolveCohubEnvironment, resolveWebsocketUrl } from "@neta-art/cohub";
|
|
7
|
-
import { requireAccessToken } from "../auth.js";
|
|
8
3
|
import { createClient } from "../client.js";
|
|
9
|
-
import {
|
|
10
|
-
import { currentIdentityKey
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
4
|
+
import { json as outJson, jsonRequested } from "../output.js";
|
|
5
|
+
import { currentIdentityKey } from "../space.js";
|
|
6
|
+
import { resolveRuntimeTarget, runtimeUp, parseRuntimeHarnesses } from "../runtime/launch.js";
|
|
7
|
+
import { canonicalRuntimeRoot, getRuntimeSpaceBinding } from "../runtime/space-binding.js";
|
|
8
|
+
import { installNativeSync } from "../runtime/native-install.js";
|
|
9
|
+
import { listNativeSyncStores } from "../runtime/native-sync-store.js";
|
|
10
|
+
import { requestRuntimeInstance, runtimeInstanceDirectory } from "../runtime/instance.js";
|
|
11
|
+
import { atLeastLevel, diagnosticLevels, formatDiagnostic, printRuntimeSummary } from "../runtime/presentation.js";
|
|
14
12
|
import { RuntimeSessionStore } from "../runtime/session-store.js";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return "error";
|
|
22
|
-
if (level.includes("warn"))
|
|
23
|
-
return "warn";
|
|
24
|
-
return stream === "stderr" ? "error" : "debug";
|
|
25
|
-
}
|
|
26
|
-
function captureSandboxOutput(stream, streamName, diagnostics) {
|
|
27
|
-
if (!stream)
|
|
28
|
-
return;
|
|
29
|
-
let pending = "";
|
|
30
|
-
const consume = (chunk) => {
|
|
31
|
-
pending += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
|
32
|
-
let newline = pending.indexOf("\n");
|
|
33
|
-
while (newline >= 0) {
|
|
34
|
-
const line = pending.slice(0, newline).trim();
|
|
35
|
-
pending = pending.slice(newline + 1);
|
|
36
|
-
if (line)
|
|
37
|
-
recordSandboxOutput(line, streamName, diagnostics);
|
|
38
|
-
newline = pending.indexOf("\n");
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
stream.on("data", consume);
|
|
42
|
-
stream.on("end", () => {
|
|
43
|
-
if (pending.trim())
|
|
44
|
-
recordSandboxOutput(pending.trim(), streamName, diagnostics);
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
function recordSandboxOutput(line, streamName, diagnostics) {
|
|
48
|
-
let parsed = null;
|
|
49
|
-
try {
|
|
50
|
-
const value = JSON.parse(line);
|
|
51
|
-
if (value && typeof value === "object" && !Array.isArray(value))
|
|
52
|
-
parsed = value;
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
// Older or third-party binaries may still emit text logs.
|
|
56
|
-
}
|
|
57
|
-
const level = sandboxOutputLevel(parsed?.level, streamName);
|
|
58
|
-
const message = typeof parsed?.msg === "string" ? parsed.msg : line;
|
|
59
|
-
const data = parsed
|
|
60
|
-
? Object.fromEntries(Object.entries(parsed).filter(([key]) => !["msg", "level", "time"].includes(key)))
|
|
61
|
-
: { message: line };
|
|
62
|
-
diagnostics.log(level, "sandboxd.log", {
|
|
63
|
-
stream: streamName,
|
|
64
|
-
message,
|
|
65
|
-
...(level === "error" ? { error: { message } } : {}),
|
|
66
|
-
...data,
|
|
67
|
-
}, { component: "sandboxd" });
|
|
68
|
-
}
|
|
69
|
-
function printDiagnostic(event) {
|
|
70
|
-
const scope = [event.component, event.event].filter(Boolean).join(".");
|
|
71
|
-
const context = [
|
|
72
|
-
event.connectionId && `connection=${event.connectionId}`,
|
|
73
|
-
event.sessionId && `session=${event.sessionId}`,
|
|
74
|
-
event.turnId && `turn=${event.turnId}`,
|
|
75
|
-
event.traceContext?.requestId && `request=${event.traceContext.requestId}`,
|
|
76
|
-
event.traceContext?.traceId && `trace=${event.traceContext.traceId}`,
|
|
77
|
-
].filter(Boolean).join(" ");
|
|
78
|
-
const data = event.data && Object.keys(event.data).length > 0 ? ` ${JSON.stringify(event.data)}` : "";
|
|
79
|
-
process.stdout.write(`${event.timestamp} ${event.level.toUpperCase().padEnd(5)} ${scope}${context ? ` ${context}` : ""}${data}${event.error ? ` ${JSON.stringify(event.error)}` : ""}\n`);
|
|
80
|
-
}
|
|
81
|
-
export function parseRuntimeHarnesses(values) {
|
|
82
|
-
const names = values.flatMap((value) => value.split(",")).map((name) => name.trim()).filter(Boolean);
|
|
83
|
-
if (names.some((name) => !isLocalHarness(name)))
|
|
84
|
-
throw new Error("Harness must be pi or codex");
|
|
85
|
-
return [...new Set(names.length ? names : ["pi"])];
|
|
86
|
-
}
|
|
13
|
+
import { readRuntimeDiagnosticEvents, RuntimeDiagnosticReader, runtimeDiagnosticsDirectory, serializeDiagnosticError } from "../runtime/diagnostics.js";
|
|
14
|
+
export { resolveLocalSpaceName, parseRuntimeHarnesses } from "../runtime/launch.js";
|
|
15
|
+
const reportFailure = (cause) => {
|
|
16
|
+
process.stderr.write(`Runtime failed / Runtime 失败: ${serializeDiagnosticError(cause).message}\n`);
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
};
|
|
87
19
|
export function registerRuntime(program) {
|
|
88
|
-
const runtime = program.command("runtime").description("Connect a local workspace");
|
|
20
|
+
const runtime = program.command("runtime").description("Connect a local workspace / 连接本地工作区");
|
|
89
21
|
runtime.command("up [dir]")
|
|
90
|
-
.description("Connect local Harnesses and files")
|
|
91
|
-
.option("-s, --space <id>", "Target Space")
|
|
92
|
-
.option("-n, --
|
|
93
|
-
.option("--
|
|
94
|
-
.option("--
|
|
95
|
-
.option("--
|
|
96
|
-
.option("
|
|
97
|
-
.option("--
|
|
22
|
+
.description("Connect local Harnesses and files / 连接本地 Harness 和文件")
|
|
23
|
+
.option("-s, --space <id>", "Target Space / 目标 Space")
|
|
24
|
+
.option("-n, --new", "Create a new Space / 创建新 Space")
|
|
25
|
+
.option("--name <name>", "New Space name / 新 Space 名称")
|
|
26
|
+
.option("-d, --detach", "Run in the background / 后台运行")
|
|
27
|
+
.option("--harness <name>", "Pi or Codex; repeatable / 可重复指定", (value, previous) => [...previous, value], [])
|
|
28
|
+
.option("--pi <path>", "Pi executable / Pi 可执行文件")
|
|
29
|
+
.option("--codex <path>", "Codex executable / Codex 可执行文件")
|
|
30
|
+
.option("-y, --yes", "Accept defaults and local execution / 接受默认选择并授权本地执行")
|
|
31
|
+
.option("--verbose", "Show diagnostic details / 显示诊断详情")
|
|
32
|
+
.option("--json", "JSON output / JSON 输出")
|
|
98
33
|
.action(async (dir, options) => {
|
|
99
|
-
const controller = new AbortController();
|
|
100
|
-
const stop = () => controller.abort();
|
|
101
|
-
process.once("SIGINT", stop);
|
|
102
|
-
process.once("SIGTERM", stop);
|
|
103
34
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
const capabilities = await discoverHarnesses(harnesses, options, root);
|
|
123
|
-
const client = createClient();
|
|
124
|
-
const requested = options.space?.trim() || explicitSpace(program);
|
|
125
|
-
const validateLocalRuntime = async (spaceId) => {
|
|
126
|
-
const sandbox = (await client.space(spaceId).sandbox.get()).sandbox;
|
|
127
|
-
if (sandbox?.provider !== "local")
|
|
128
|
-
throw new Error("Space does not have a local Runtime");
|
|
129
|
-
};
|
|
130
|
-
const { spaceId } = await resolveRuntimeSpace({
|
|
131
|
-
root,
|
|
132
|
-
identityKey: currentIdentityKey(),
|
|
133
|
-
explicitSpaceId: requested,
|
|
134
|
-
createSpace: async () => (await client.spaces.create({
|
|
135
|
-
name: resolveLocalSpaceName(root, options.name),
|
|
136
|
-
config: { sandbox: { provider: "local" } },
|
|
137
|
-
})).space.id,
|
|
138
|
-
validateSpace: validateLocalRuntime,
|
|
139
|
-
});
|
|
140
|
-
const spaceClient = client.space(spaceId);
|
|
141
|
-
const store = new RuntimeSessionStore(spaceId, { projectionSource: spaceClient });
|
|
142
|
-
const runtimeId = randomUUID();
|
|
143
|
-
const diagnostics = new RuntimeDiagnostics({ root: store.root, spaceId, runtimeId });
|
|
144
|
-
store.setDiagnostics(diagnostics);
|
|
145
|
-
diagnostics.log("info", "runtime.cli_started", {
|
|
146
|
-
platform: process.platform,
|
|
147
|
-
arch: process.arch,
|
|
148
|
-
node: process.versions.node,
|
|
149
|
-
harnesses,
|
|
150
|
-
proxyConfigured: ["HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY"].some((key) => Boolean(process.env[key]?.trim())),
|
|
151
|
-
});
|
|
35
|
+
await runtimeUp(program, dir, { ...options, json: jsonRequested(options) });
|
|
36
|
+
}
|
|
37
|
+
catch (cause) {
|
|
38
|
+
reportFailure(cause);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
for (const action of ["attach", "detach"])
|
|
42
|
+
runtime.command(action)
|
|
43
|
+
.description(action === "attach" ? "Sync native Pi / Codex Turns / 同步原生 Pi / Codex 对话" : "Pause native sync; retain all receipts / 暂停原生同步,保留所有回执")
|
|
44
|
+
.option("-s, --space <id>", "Target Space / 目标 Space")
|
|
45
|
+
.option("--harness <name>", "Pi or Codex; repeatable / 可重复指定", (value, previous) => [...previous, value], [])
|
|
46
|
+
.option("--pi <path>", "Pi executable for capability checks / 用于能力检查的 Pi 路径")
|
|
47
|
+
.option("--codex <path>", "Codex executable for capability checks / 用于能力检查的 Codex 路径")
|
|
48
|
+
.option("-y, --yes", "Authorize project conversation and native archive uploads / 授权上传项目对话及原生归档")
|
|
49
|
+
.option("--json", "JSON output / JSON 输出")
|
|
50
|
+
.action(async (options) => {
|
|
152
51
|
try {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
bridge = spawn(binary, ["--local", "--space", spaceId, "--root", root, "--relay", process.env.COHUB_RELAY_URL?.trim() || relay.toString()], {
|
|
180
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
181
|
-
env: {
|
|
182
|
-
...process.env,
|
|
183
|
-
COHUB_RELAY_TOKEN: token,
|
|
184
|
-
COHUB_RUNTIME_ID: runtimeId,
|
|
185
|
-
COHUB_LOG_FORMAT: "json",
|
|
186
|
-
},
|
|
187
|
-
});
|
|
188
|
-
captureSandboxOutput(bridge.stdout, "stdout", diagnostics);
|
|
189
|
-
captureSandboxOutput(bridge.stderr, "stderr", diagnostics);
|
|
190
|
-
bridgeClosed = new Promise((resolveClosed) => bridge?.once("close", () => resolveClosed()));
|
|
191
|
-
bridge.on("error", (cause) => {
|
|
192
|
-
diagnostics.log("error", "sandboxd.process_error", { error: serializeDiagnosticError(cause) }, { component: "sandboxd" });
|
|
193
|
-
console.error(cause);
|
|
194
|
-
controller.abort();
|
|
195
|
-
});
|
|
196
|
-
bridge.once("exit", (code, signal) => {
|
|
197
|
-
diagnostics.log(code === 0 ? "info" : "error", "sandboxd.process_exit", { code, signal }, { component: "sandboxd" });
|
|
198
|
-
controller.abort();
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
if (announced)
|
|
202
|
-
return;
|
|
203
|
-
announced = true;
|
|
204
|
-
const webUrl = `${resolveCohubEnvironment() === "prod" ? "https://cohub.live" : "https://dev.cohub.live"}/spaces/${spaceId}`;
|
|
205
|
-
if (jsonRequested(options))
|
|
206
|
-
outJson({ spaceId, root, harnesses, runtimeId, diagnosticsPath: diagnostics.logPath, url: webUrl });
|
|
207
|
-
else
|
|
208
|
-
console.error(`Runtime connected: ${webUrl} (runtimeId=${runtimeId}, logs=${diagnostics.logPath})`);
|
|
209
|
-
},
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
finally {
|
|
213
|
-
if (bridge) {
|
|
214
|
-
const child = bridge;
|
|
215
|
-
child.kill("SIGTERM");
|
|
216
|
-
const timeout = setTimeout(() => child.kill("SIGKILL"), 3000);
|
|
217
|
-
await bridgeClosed;
|
|
218
|
-
clearTimeout(timeout);
|
|
52
|
+
const spaceId = await resolveRuntimeTarget(program, options.space);
|
|
53
|
+
const identity = currentIdentityKey();
|
|
54
|
+
if (!identity)
|
|
55
|
+
throw new Error("Sign in first / 请先登录");
|
|
56
|
+
const root = await canonicalRuntimeRoot(process.cwd());
|
|
57
|
+
if (action === "attach" && (await getRuntimeSpaceBinding(root, identity))?.spaceId !== spaceId)
|
|
58
|
+
throw new Error("Bind this directory with runtime up --space first / 请先使用 runtime up --space 绑定当前目录");
|
|
59
|
+
const instance = await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId));
|
|
60
|
+
if (action === "attach" && (!instance || instance.root !== root))
|
|
61
|
+
throw new Error("Start this directory's Runtime first: cohub runtime up -d / 请先启动当前目录的 Runtime");
|
|
62
|
+
if (action === "attach" && !instance?.nativeSync)
|
|
63
|
+
throw new Error("Restart the Runtime with this CLI before attaching / 请先使用新版 CLI 重启 Runtime,再接入原生客户端");
|
|
64
|
+
const harnesses = parseRuntimeHarnesses(options.harness.length ? options.harness : instance?.harnesses ?? ["pi", "codex"]);
|
|
65
|
+
if (action === "attach" && harnesses.some((harness) => !instance?.harnesses.includes(harness)))
|
|
66
|
+
throw new Error("Enable these Harnesses with runtime up first / 请先通过 runtime up 启用对应 Harness");
|
|
67
|
+
if (action === "attach" && !options.yes) {
|
|
68
|
+
if (!process.stdin.isTTY)
|
|
69
|
+
throw new Error("Use --yes to authorize native sync / 请使用 --yes 授权原生同步");
|
|
70
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
71
|
+
try {
|
|
72
|
+
const answer = await rl.question(`Install user-level ${harnesses.join(" / ")} integration and upload this project's opened conversations, tool output and raw archives to this Space? History may contain secrets. [y/N]\n安装用户级原生集成,并将当前项目打开的对话、工具输出和原始归档上传至此 Space?历史可能包含敏感信息。[y/N] `);
|
|
73
|
+
if (!/^y(es)?$/i.test(answer.trim()))
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
rl.close();
|
|
219
78
|
}
|
|
220
79
|
}
|
|
80
|
+
const result = await installNativeSync({ root, spaceId, identity, harnesses, disabled: action === "detach", executables: { pi: options.pi, codex: options.codex } });
|
|
81
|
+
if (jsonRequested(options))
|
|
82
|
+
outJson(result);
|
|
83
|
+
else
|
|
84
|
+
process.stdout.write(action === "attach"
|
|
85
|
+
? `Native sync enabled. Reload Pi or restart Codex and review its hook trust prompt. / 原生同步已启用。请重载 Pi 或重启 Codex,并审核 Hook 信任提示。\n${result.configPath}\n`
|
|
86
|
+
: "Native sync paused; all local records retained / 原生同步已暂停,所有本地记录已保留\n");
|
|
221
87
|
}
|
|
222
88
|
catch (cause) {
|
|
223
|
-
|
|
224
|
-
throw cause;
|
|
89
|
+
reportFailure(cause);
|
|
225
90
|
}
|
|
226
|
-
|
|
227
|
-
|
|
91
|
+
});
|
|
92
|
+
runtime.command("status").description("Local and server status / 本地与服务端状态")
|
|
93
|
+
.option("-s, --space <id>", "Target Space / 目标 Space")
|
|
94
|
+
.option("--json", "JSON output / JSON 输出")
|
|
95
|
+
.action(async (options) => {
|
|
96
|
+
try {
|
|
97
|
+
const spaceId = await resolveRuntimeTarget(program, options.space);
|
|
98
|
+
const identity = currentIdentityKey();
|
|
99
|
+
const local = identity ? await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId)) : null;
|
|
100
|
+
const space = createClient().space(spaceId);
|
|
101
|
+
const store = new RuntimeSessionStore(spaceId, { projectionSource: space });
|
|
102
|
+
const [remote, pendingLocalArchives, failedLocalArchives, nativeStores] = await Promise.all([
|
|
103
|
+
space.getRuntime(undefined, { signal: AbortSignal.timeout(5000) }).then((value) => ({ value, error: null })).catch((error) => ({ value: null, error: serializeDiagnosticError(error).message })),
|
|
104
|
+
store.archives.pendingCount(), store.archives.failedCaptureCount(),
|
|
105
|
+
identity ? listNativeSyncStores(store.root, spaceId, identity) : [],
|
|
106
|
+
]);
|
|
107
|
+
const nativeSessions = await Promise.all(nativeStores.map((native) => native.status()));
|
|
108
|
+
const result = { ...remote.value, spaceId, local, remote: remote.value, remoteError: remote.error, diagnosticsPath: runtimeDiagnosticsDirectory(store.root), pendingLocalArchives, failedLocalArchives, nativeSessions };
|
|
109
|
+
if (jsonRequested(options))
|
|
110
|
+
outJson(result);
|
|
111
|
+
else {
|
|
112
|
+
if (local)
|
|
113
|
+
printRuntimeSummary(local);
|
|
114
|
+
else
|
|
115
|
+
process.stdout.write(`Local process / 本地进程 Not running / 未运行\nSpace / 空间 ${spaceId}\nLogs / 日志 ${result.diagnosticsPath}\n`);
|
|
116
|
+
process.stdout.write(`Server / 服务端 ${remote.error ? `Unknown / 未知 — ${remote.error}` : remote.value?.online ? "Harness connected / Harness 已连接" : "Offline / 离线"}\nArchives / 归档 ${pendingLocalArchives} pending / 待同步 · ${failedLocalArchives} failed / 失败\n`);
|
|
117
|
+
if (nativeSessions.length)
|
|
118
|
+
process.stdout.write(`Native chats / 原生对话 ${nativeSessions.length} · ${nativeSessions.reduce((sum, session) => sum + session.pendingTurns, 0)} Turns pending / Turn 待同步 · ${nativeSessions.reduce((sum, session) => sum + session.pendingArchives, 0)} archives pending / 归档待同步\n`);
|
|
228
119
|
}
|
|
229
120
|
}
|
|
230
121
|
catch (cause) {
|
|
231
|
-
|
|
232
|
-
error("Runtime failed", cause instanceof Error ? cause.message : String(cause));
|
|
122
|
+
reportFailure(cause);
|
|
233
123
|
}
|
|
234
|
-
finally {
|
|
235
|
-
process.removeListener("SIGINT", stop);
|
|
236
|
-
process.removeListener("SIGTERM", stop);
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
runtime.command("status").description("Runtime status").option("-s, --space <id>", "Target Space").action(async (options) => {
|
|
240
|
-
const spaceId = options.space?.trim() || await resolveSpace(program);
|
|
241
|
-
const client = createClient();
|
|
242
|
-
const spaceClient = client.space(spaceId);
|
|
243
|
-
const store = new RuntimeSessionStore(spaceId, { projectionSource: spaceClient });
|
|
244
|
-
const [status, pendingLocalArchives, failedLocalArchives] = await Promise.all([
|
|
245
|
-
spaceClient.getRuntime(),
|
|
246
|
-
store.archives.pendingCount(),
|
|
247
|
-
store.archives.failedCaptureCount(),
|
|
248
|
-
]);
|
|
249
|
-
outJson({
|
|
250
|
-
...status,
|
|
251
|
-
diagnosticsPath: runtimeDiagnosticsDirectory(store.root),
|
|
252
|
-
pendingLocalArchives,
|
|
253
|
-
failedLocalArchives,
|
|
254
|
-
});
|
|
255
124
|
});
|
|
256
|
-
runtime.command("
|
|
257
|
-
.
|
|
258
|
-
.option("-
|
|
259
|
-
.option("
|
|
260
|
-
.option("--follow", "Keep watching for new events")
|
|
261
|
-
.option("--json", "Print raw diagnostic events")
|
|
125
|
+
runtime.command("down").description("Stop this local Runtime; retain all data / 停止本地 Runtime,保留所有数据")
|
|
126
|
+
.option("-s, --space <id>", "Target Space / 目标 Space")
|
|
127
|
+
.option("-y, --yes", "Stop even with unconfirmed executions / 确认停止包含未确认执行的 Runtime")
|
|
128
|
+
.option("--json", "JSON output / JSON 输出")
|
|
262
129
|
.action(async (options) => {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
else if (asJson) {
|
|
281
|
-
outJson(fresh);
|
|
282
|
-
}
|
|
283
|
-
else if (fresh.length === 0) {
|
|
284
|
-
process.stdout.write("No Runtime diagnostics / 未找到 Runtime 诊断记录\n");
|
|
285
|
-
}
|
|
286
|
-
else {
|
|
287
|
-
for (const event of fresh)
|
|
288
|
-
printDiagnostic(event);
|
|
130
|
+
try {
|
|
131
|
+
const spaceId = await resolveRuntimeTarget(program, options.space);
|
|
132
|
+
const identity = currentIdentityKey();
|
|
133
|
+
if (!identity)
|
|
134
|
+
throw new Error("Sign in to the Runtime account / 请登录 Runtime 所属账号");
|
|
135
|
+
const directory = runtimeInstanceDirectory(identity, spaceId);
|
|
136
|
+
const local = await requestRuntimeInstance(directory, "stop", Boolean(options.yes));
|
|
137
|
+
const until = Date.now() + 15_000;
|
|
138
|
+
let running = Boolean(local);
|
|
139
|
+
while (running && Date.now() < until) {
|
|
140
|
+
await delay(250);
|
|
141
|
+
try {
|
|
142
|
+
running = Boolean(await requestRuntimeInstance(directory));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
running = true;
|
|
146
|
+
} // An unreachable control socket does not prove the process stopped.
|
|
289
147
|
}
|
|
290
|
-
|
|
148
|
+
if (running)
|
|
149
|
+
throw new Error("Runtime is still stopping; inspect logs / Runtime 仍在停止,请检查日志");
|
|
150
|
+
if (jsonRequested(options))
|
|
151
|
+
outJson({ spaceId, stopped: true });
|
|
152
|
+
else
|
|
153
|
+
process.stdout.write("Runtime stopped; data retained / Runtime 已停止,数据已保留\n");
|
|
154
|
+
}
|
|
155
|
+
catch (cause) {
|
|
156
|
+
reportFailure(cause);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
runtime.command("logs").description("Read local Runtime diagnostics / 查看本地 Runtime 日志")
|
|
160
|
+
.option("-s, --space <id>", "Target Space / 目标 Space")
|
|
161
|
+
.option("-l, --limit <count>", "Number of events / 事件数量", "100")
|
|
162
|
+
.option("--level <level>", "Minimum level: debug, info, warn, error / 最低级别", "info")
|
|
163
|
+
.option("-f, --follow", "Keep watching / 持续查看")
|
|
164
|
+
.option("--json", "Raw diagnostic events / 原始诊断事件")
|
|
165
|
+
.action(async (options) => {
|
|
166
|
+
const controller = new AbortController();
|
|
167
|
+
const stop = () => controller.abort();
|
|
291
168
|
try {
|
|
292
|
-
await
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
};
|
|
319
|
-
timer = setTimeout(() => void poll(), 2_000);
|
|
320
|
-
process.once("SIGINT", stop);
|
|
321
|
-
process.once("SIGTERM", stop);
|
|
322
|
-
});
|
|
169
|
+
const spaceId = await resolveRuntimeTarget(program, options.space);
|
|
170
|
+
const store = new RuntimeSessionStore(spaceId, { projectionSource: createClient().space(spaceId) });
|
|
171
|
+
const limit = Number(options.limit);
|
|
172
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10_000)
|
|
173
|
+
throw new Error("Use a limit from 1 to 10000 / 数量范围为 1 到 10000");
|
|
174
|
+
if (!diagnosticLevels.includes(options.level))
|
|
175
|
+
throw new Error("Use debug, info, warn or error / 请使用有效日志级别");
|
|
176
|
+
const asJson = jsonRequested(options);
|
|
177
|
+
const reader = new RuntimeDiagnosticReader(store.root);
|
|
178
|
+
process.once("SIGINT", stop);
|
|
179
|
+
process.once("SIGTERM", stop);
|
|
180
|
+
do {
|
|
181
|
+
const events = (options.follow ? await reader.read({ limit }) : await readRuntimeDiagnosticEvents(store.root, { limit }))
|
|
182
|
+
.filter((event) => atLeastLevel(event.level, options.level));
|
|
183
|
+
if (asJson && !options.follow)
|
|
184
|
+
outJson(events);
|
|
185
|
+
else
|
|
186
|
+
for (const event of events)
|
|
187
|
+
process.stdout.write(asJson ? `${JSON.stringify(event)}\n` : formatDiagnostic(event, true));
|
|
188
|
+
if (!options.follow) {
|
|
189
|
+
if (!asJson && !events.length)
|
|
190
|
+
process.stdout.write("No matching diagnostics / 未找到匹配日志\n");
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
await delay(1000, undefined, { signal: controller.signal }).catch(() => undefined);
|
|
194
|
+
} while (!controller.signal.aborted);
|
|
323
195
|
}
|
|
324
196
|
catch (cause) {
|
|
325
|
-
|
|
197
|
+
reportFailure(cause);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
process.removeListener("SIGINT", stop);
|
|
201
|
+
process.removeListener("SIGTERM", stop);
|
|
326
202
|
}
|
|
327
203
|
});
|
|
328
204
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export declare const SANDBOXD_VERSION = "
|
|
1
|
+
export declare const SANDBOXD_VERSION = "v2.54.0";
|
|
2
2
|
export declare class SandboxdDownloadError extends Error {
|
|
3
3
|
name: string;
|
|
4
4
|
}
|
|
5
|
-
export declare function validSandboxdArchiveEntries(entries: string[]
|
|
6
|
-
export declare function validateSandboxdArchive(archivePath: string
|
|
5
|
+
export declare function validSandboxdArchiveEntries(entries: string[]): boolean;
|
|
6
|
+
export declare function validateSandboxdArchive(archivePath: string): Promise<string[]>;
|
|
7
7
|
export type EnsureSandboxdOptions = {
|
|
8
8
|
version?: string;
|
|
9
9
|
onStatus?: (message: string) => void;
|
|
@@ -14,7 +14,14 @@ import { Readable } from "node:stream";
|
|
|
14
14
|
// published by .github/workflows/sandbox-binaries-build.yml. Only bump it AFTER
|
|
15
15
|
// that tag's publish-cdn job has succeeded, otherwise `runtime up` 404s on the
|
|
16
16
|
// default download.
|
|
17
|
-
|
|
17
|
+
//
|
|
18
|
+
// v2.54.0 is the first published tag with the private managed Runtime control
|
|
19
|
+
// pipe (`COHUB_RUNTIME_MANAGED` over fd 3), so `runtime up` reads connection
|
|
20
|
+
// state from the daemon instead of polling the API every five seconds. It also
|
|
21
|
+
// carries the optional workspace-search runner download. v2.53.1 already has
|
|
22
|
+
// the native FSEvents backends and the `runtimeId` control frame, and older
|
|
23
|
+
// releases stay usable through the compatibility readiness/restart path.
|
|
24
|
+
export const SANDBOXD_VERSION = "v2.54.0";
|
|
18
25
|
const BINARY_NAME = "cohub-sandboxd";
|
|
19
26
|
// Public CDN prefix hosting the release archives (the repo is private, so the
|
|
20
27
|
// GitHub Release assets are not publicly downloadable). Overridable for staging
|
|
@@ -91,14 +98,11 @@ const fetchText = (url, accept) => withTimeout(`Download of ${url}`, async (sign
|
|
|
91
98
|
throw new SandboxdDownloadError(`Download failed (${response.status}) for ${url}`);
|
|
92
99
|
return (await response.text()).trim();
|
|
93
100
|
});
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const expected = [BINARY_NAME, "LICENSE", "NOTICE"];
|
|
100
|
-
const withNotices = entries.length === expected.length && expected.every((entry) => entries.includes(entry));
|
|
101
|
-
return binaryOnly || withNotices;
|
|
101
|
+
// Every published release ships the binary alongside its LICENSE and NOTICE.
|
|
102
|
+
const EXPECTED_ARCHIVE_ENTRIES = [BINARY_NAME, "LICENSE", "NOTICE"];
|
|
103
|
+
export function validSandboxdArchiveEntries(entries) {
|
|
104
|
+
return (entries.length === EXPECTED_ARCHIVE_ENTRIES.length &&
|
|
105
|
+
EXPECTED_ARCHIVE_ENTRIES.every((entry) => entries.includes(entry)));
|
|
102
106
|
}
|
|
103
107
|
// Reject unexpected paths before extracting the checksum-verified release.
|
|
104
108
|
const listTarGz = (archivePath, verbose = false) => new Promise((res, rej) => {
|
|
@@ -116,9 +120,9 @@ const listTarGz = (archivePath, verbose = false) => new Promise((res, rej) => {
|
|
|
116
120
|
? res(stdout.split("\n").map((line) => line.trim()).filter(Boolean))
|
|
117
121
|
: rej(new SandboxdDownloadError(`tar listing failed: ${stderr.trim() || `exit ${code}`}`)));
|
|
118
122
|
});
|
|
119
|
-
export async function validateSandboxdArchive(archivePath
|
|
123
|
+
export async function validateSandboxdArchive(archivePath) {
|
|
120
124
|
const entries = await listTarGz(archivePath);
|
|
121
|
-
if (!validSandboxdArchiveEntries(entries
|
|
125
|
+
if (!validSandboxdArchiveEntries(entries)) {
|
|
122
126
|
throw new SandboxdDownloadError(`Unexpected sandbox archive contents: ${entries.join(", ") || "(empty)"}`);
|
|
123
127
|
}
|
|
124
128
|
const details = await listTarGz(archivePath, true);
|
|
@@ -185,7 +189,7 @@ const downloadAndVerify = async (version, target) => {
|
|
|
185
189
|
if (actual !== expected) {
|
|
186
190
|
throw new SandboxdDownloadError(`Checksum mismatch for ${name} (expected ${expected}, got ${actual})`);
|
|
187
191
|
}
|
|
188
|
-
const entries = await validateSandboxdArchive(archivePath
|
|
192
|
+
const entries = await validateSandboxdArchive(archivePath);
|
|
189
193
|
await extractTarGz(archivePath, tempDir);
|
|
190
194
|
for (const entry of entries) {
|
|
191
195
|
if (!(await isSafeArchiveFile(join(tempDir, entry)))) {
|
|
@@ -38,6 +38,8 @@ export declare class RuntimeArchiveStore {
|
|
|
38
38
|
harness: "pi" | "codex";
|
|
39
39
|
nativeSessionId: string;
|
|
40
40
|
path: string;
|
|
41
|
+
sizeBytes?: number;
|
|
42
|
+
expectedChecksum?: string;
|
|
41
43
|
}, turnId: string): Promise<HarnessArchive>;
|
|
42
44
|
private capture;
|
|
43
45
|
flush(signal: AbortSignal): Promise<void>;
|