@neta-art/cohub-cli 7.0.0 → 7.1.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 +17 -9
- package/dist/commands/apps.js +2 -2
- package/dist/commands/desktop.d.ts +9 -0
- package/dist/commands/desktop.js +13 -5
- package/dist/commands/run.d.ts +11 -0
- package/dist/commands/run.js +4 -4
- package/dist/commands/runtime.js +269 -58
- package/dist/commands/sandboxd-binary.d.ts +2 -0
- package/dist/commands/sandboxd-binary.js +37 -16
- package/dist/index.js +4 -2
- package/dist/runtime/archive-store.d.ts +2 -0
- package/dist/runtime/archive-store.js +31 -25
- package/dist/runtime/connection.d.ts +3 -0
- package/dist/runtime/connection.js +251 -42
- package/dist/runtime/diagnostics.d.ts +104 -0
- package/dist/runtime/diagnostics.js +382 -0
- package/dist/runtime/harness.d.ts +3 -2
- package/dist/runtime/harness.js +39 -8
- package/dist/runtime/json-rpc.d.ts +2 -0
- package/dist/runtime/json-rpc.js +12 -1
- package/dist/runtime/native-archive.js +3 -3
- package/dist/runtime/process-group.js +1 -1
- package/dist/runtime/projection-store.d.ts +34 -0
- package/dist/runtime/projection-store.js +103 -0
- package/dist/runtime/session-store.d.ts +28 -5
- package/dist/runtime/session-store.js +272 -67
- package/dist/runtime/space-binding.d.ts +45 -0
- package/dist/runtime/space-binding.js +305 -0
- package/dist/runtime/turn-projection.d.ts +43 -0
- package/dist/runtime/turn-projection.js +127 -0
- package/dist/space.d.ts +11 -3
- package/dist/space.js +18 -6
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -72,33 +72,41 @@ cohub -s <spaceId> spaces invites revoke <code> --yes
|
|
|
72
72
|
cohub -s <spaceId> run -- git status
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
Many space-scoped commands need a target Space
|
|
75
|
+
Many space-scoped commands need a target Space. Without an explicit target, commands
|
|
76
|
+
also use the Space remembered for the current directory before falling back to Home:
|
|
76
77
|
|
|
77
78
|
```bash
|
|
78
79
|
cohub -s <spaceId> spaces prompt "message" --json
|
|
79
80
|
COHUB_SPACE_ID=<spaceId> cohub spaces prompt "message" --json
|
|
80
81
|
```
|
|
81
82
|
|
|
82
|
-
## Local Runtime
|
|
83
|
+
## Local Runtime
|
|
83
84
|
|
|
84
85
|
Connect one local workspace to a Space and select Pi or Codex per turn.
|
|
85
|
-
将一个本地工作区连接到 Space,每轮可选择 Pi 或 Codex。
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
88
|
cohub runtime up ./project --harness pi --harness codex
|
|
89
89
|
cohub runtime up ./project --space <spaceId> --harness codex
|
|
90
|
-
cohub -s <spaceId> spaces prompt "Continue
|
|
90
|
+
cohub -s <spaceId> spaces prompt "Continue" --harness codex
|
|
91
91
|
cohub runtime status --space <spaceId>
|
|
92
|
+
cohub runtime logs --space <spaceId> --follow
|
|
92
93
|
```
|
|
93
94
|
|
|
95
|
+
Runtime diagnostics stay as redacted JSONL under the local Runtime state directory and
|
|
96
|
+
are never uploaded automatically. Use `runtime logs --json` to export a failure report;
|
|
97
|
+
events carry the local `runtimeId` plus server `requestId` / `traceparent` when a turn has
|
|
98
|
+
reached the Agent trace.
|
|
99
|
+
|
|
94
100
|
The Runtime uses WebSockets, native Pi RPC / Codex app-server, and existing cloud
|
|
95
101
|
streaming. Local executables and credentials are required. `sandbox up` has been
|
|
96
102
|
removed. Reconnection and result reconciliation are automatic; no recovery command is needed.
|
|
97
|
-
|
|
103
|
+
When a result genuinely cannot be determined, the affected Chat offers an explicit stop
|
|
104
|
+
confirmation. See [Runtime details](../../docs/local-runtime.md).
|
|
98
105
|
|
|
99
|
-
Runtime
|
|
100
|
-
|
|
101
|
-
|
|
106
|
+
When `--space` is omitted, Runtime remembers a Space for the canonical local directory,
|
|
107
|
+
account, and environment in `~/.config/cohub/runtime-spaces.json`. The first start creates
|
|
108
|
+
and records a local Space; later starts reuse it. An explicit `--space` or `COHUB_SPACE_ID`
|
|
109
|
+
overrides and updates the directory binding.
|
|
102
110
|
|
|
103
111
|
## Chats and prompts
|
|
104
112
|
|
|
@@ -378,7 +386,7 @@ legacy bare targets. A bare target checks the current Space for a file first, th
|
|
|
378
386
|
falls back to the same App references as `apps get`. Showing a preview is
|
|
379
387
|
idempotent: repeating it re-activates the same tab and refreshes any launch state
|
|
380
388
|
carried by the reference. With `--call`, the command waits for the App to announce readiness, invokes the method,
|
|
381
|
-
and waits for the App to complete the same UI command with `client.
|
|
389
|
+
and waits for the App to complete the same UI command with `client.desktop.reportResult()`.
|
|
382
390
|
|
|
383
391
|
App authors decide what is callable by registering handlers inside the App:
|
|
384
392
|
|
package/dist/commands/apps.js
CHANGED
|
@@ -508,7 +508,7 @@ export function registerApps(program) {
|
|
|
508
508
|
.option("--app-scope <scope>", "Scope granted directly to the app runtime (space.view, session.view, file.view, file.edit, taskrun.view, session.prompt.readonly, session.prompt.fullaccess, command.execute)", collectOption, [])
|
|
509
509
|
.option("--viewer-scope <scope>", "Deprecated: viewer grants are no longer gated by the app configuration", collectOption, [])
|
|
510
510
|
.option("--clear-app-scopes", "Clear app runtime scopes")
|
|
511
|
-
.option("--clear-viewer-scopes", "Deprecated: clear legacy scope metadata
|
|
511
|
+
.option("--clear-viewer-scopes", "Deprecated: clear legacy scope metadata")
|
|
512
512
|
.option("--meta <json>", "App metadata as a JSON object")
|
|
513
513
|
.option("--hide-cohub-bar", "Hide the Cohub footer bar on the public app page")
|
|
514
514
|
.option("--show-cohub-bar", "Show the Cohub footer bar on the public app page")
|
|
@@ -728,7 +728,7 @@ export function registerApps(program) {
|
|
|
728
728
|
.description("Grant an app scopes as the current user")
|
|
729
729
|
.requiredOption("--scope <scope>", "Scope to grant (repeatable)", collectOption, [])
|
|
730
730
|
.option("--space <spaceId>", "Target space; defaults to the app's own space")
|
|
731
|
-
.option("--extend", "Add scopes without replacing active grants
|
|
731
|
+
.option("--extend", "Add scopes without replacing active grants")
|
|
732
732
|
.option("--json", "Output as JSON")
|
|
733
733
|
.action(async (appRef, opts) => {
|
|
734
734
|
const client = createClient();
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { resolveOpenSurface } from "@neta-art/cohub";
|
|
2
2
|
import type { Command } from "commander";
|
|
3
3
|
export { resolveOpenSurface };
|
|
4
|
+
/**
|
|
5
|
+
* Optional disambiguation for file:// vs app://. Unlike ordinary
|
|
6
|
+
* Space-scoped commands, an unbound directory must not fall back to Home:
|
|
7
|
+
* the plain target should remain eligible for App resolution.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveOptionalSpaceId(command: Command, options?: {
|
|
10
|
+
cwd?: string;
|
|
11
|
+
bindingsPath?: string;
|
|
12
|
+
}): Promise<string | undefined>;
|
|
4
13
|
export declare function registerDesktop(program: Command): void;
|
|
5
14
|
export declare function registerLegacyUi(program: Command): void;
|
package/dist/commands/desktop.js
CHANGED
|
@@ -2,13 +2,18 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { HttpError, parseAppRef, resolveOpenSurface, DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, DESKTOP_COMMAND_MAX_TIMEOUT_MS, } from "@neta-art/cohub";
|
|
3
3
|
import { createClient } from "../client.js";
|
|
4
4
|
import { error, handleHttp, json as outJson, jsonRequested, ok } from "../output.js";
|
|
5
|
+
import { resolveBoundSpace } from "../space.js";
|
|
5
6
|
import { getAppByRef } from "../app-ref.js";
|
|
6
7
|
export { resolveOpenSurface };
|
|
7
8
|
const FILE_SCHEME = "file://";
|
|
8
9
|
const APP_SCHEME = "app://";
|
|
9
10
|
const LEGACY_WORK_SCHEME = "work://";
|
|
10
|
-
/**
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Optional disambiguation for file:// vs app://. Unlike ordinary
|
|
13
|
+
* Space-scoped commands, an unbound directory must not fall back to Home:
|
|
14
|
+
* the plain target should remain eligible for App resolution.
|
|
15
|
+
*/
|
|
16
|
+
export async function resolveOptionalSpaceId(command, options = {}) {
|
|
12
17
|
let current = command;
|
|
13
18
|
while (current) {
|
|
14
19
|
const opts = current.opts();
|
|
@@ -16,7 +21,10 @@ function optionalSpaceId(command) {
|
|
|
16
21
|
return opts.space.trim();
|
|
17
22
|
current = current.parent ?? null;
|
|
18
23
|
}
|
|
19
|
-
|
|
24
|
+
const fromEnvironment = process.env.COHUB_SPACE_ID?.trim();
|
|
25
|
+
if (fromEnvironment)
|
|
26
|
+
return fromEnvironment;
|
|
27
|
+
return (await resolveBoundSpace(options)) ?? undefined;
|
|
20
28
|
}
|
|
21
29
|
function parseFilePath(value) {
|
|
22
30
|
const path = value.slice(FILE_SCHEME.length).trim();
|
|
@@ -86,7 +94,7 @@ async function resolveOpenTarget(client, command, value) {
|
|
|
86
94
|
return { kind: "file", path: parseFilePath(value) };
|
|
87
95
|
if (hasAppScheme(value))
|
|
88
96
|
return resolveAppTarget(client, value);
|
|
89
|
-
const spaceId =
|
|
97
|
+
const spaceId = await resolveOptionalSpaceId(command);
|
|
90
98
|
if (spaceId) {
|
|
91
99
|
try {
|
|
92
100
|
await client.space(spaceId).files.read(value);
|
|
@@ -190,7 +198,7 @@ const OPEN_NOTES = `
|
|
|
190
198
|
Notes:
|
|
191
199
|
- Use file:// and app:// to make the target explicit; the legacy work://
|
|
192
200
|
scheme is still accepted.
|
|
193
|
-
- A plain target checks the
|
|
201
|
+
- A plain target checks the explicit Space or current directory Runtime binding for a file before resolving an app; it does not fall back to Home.
|
|
194
202
|
- Opening a window is idempotent; repeating it re-activates the same tab.
|
|
195
203
|
- --as picks the surface: window (a preview tab) or overlay (a transparent
|
|
196
204
|
layer above the workspace). Without it, an App published with
|
package/dist/commands/run.d.ts
CHANGED
|
@@ -1,2 +1,13 @@
|
|
|
1
|
+
type RunCliOptions = {
|
|
2
|
+
spaceId: string;
|
|
3
|
+
json: boolean;
|
|
4
|
+
async: boolean;
|
|
5
|
+
command: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function parseRunCliOptions(argv: string[], spaceOptions?: {
|
|
8
|
+
cwd?: string;
|
|
9
|
+
bindingsPath?: string;
|
|
10
|
+
}): Promise<RunCliOptions>;
|
|
1
11
|
export declare function printRunHelp(): void;
|
|
2
12
|
export declare function maybeHandleRunCommand(argv: string[]): Promise<boolean>;
|
|
13
|
+
export {};
|
package/dist/commands/run.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createClient } from "../client.js";
|
|
2
2
|
import { error, handleHttp, json as outJson, spinner } from "../output.js";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveSpaceTarget } from "../space.js";
|
|
4
4
|
const DEFAULT_WAIT_TIMEOUT_MS = (6 * 60 * 60 + 60) * 1000;
|
|
5
5
|
const DEFAULT_POLL_INTERVAL_MS = 1500;
|
|
6
6
|
function shellQuote(value) {
|
|
@@ -44,7 +44,7 @@ function parseSpaceId(tokens) {
|
|
|
44
44
|
}
|
|
45
45
|
return undefined;
|
|
46
46
|
}
|
|
47
|
-
async function parseRunCliOptions(argv) {
|
|
47
|
+
export async function parseRunCliOptions(argv, spaceOptions = {}) {
|
|
48
48
|
const runIndex = topLevelRunIndex(argv);
|
|
49
49
|
if (runIndex < 0)
|
|
50
50
|
return error("Invalid invocation", "Use `cohub run [options] <command>`");
|
|
@@ -104,7 +104,7 @@ async function parseRunCliOptions(argv) {
|
|
|
104
104
|
if (!command) {
|
|
105
105
|
return error("No command", "Pass --command <shell command>, or use `--` followed by the command.");
|
|
106
106
|
}
|
|
107
|
-
const spaceId =
|
|
107
|
+
const spaceId = await resolveSpaceTarget(explicitSpaceId, spaceOptions);
|
|
108
108
|
return { spaceId, json, async, command };
|
|
109
109
|
}
|
|
110
110
|
export function printRunHelp() {
|
|
@@ -125,7 +125,7 @@ Examples:
|
|
|
125
125
|
cohub -s <spaceId> run -- git status -sb
|
|
126
126
|
|
|
127
127
|
Notes:
|
|
128
|
-
- Without -s or COHUB_SPACE_ID, the command
|
|
128
|
+
- Without -s or COHUB_SPACE_ID, the command uses the current directory Runtime binding, then Home.
|
|
129
129
|
- Use --command for commands that contain leading flags, or use -- before the shell command.
|
|
130
130
|
- The command runs in /workspace.
|
|
131
131
|
`);
|
package/dist/commands/runtime.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { stat } from "node:fs/promises";
|
|
3
4
|
import { basename, resolve } from "node:path";
|
|
4
5
|
import { createInterface } from "node:readline/promises";
|
|
@@ -6,45 +7,111 @@ import { isLocalHarness, resolveCohubEnvironment, resolveWebsocketUrl } from "@n
|
|
|
6
7
|
import { requireAccessToken } from "../auth.js";
|
|
7
8
|
import { createClient } from "../client.js";
|
|
8
9
|
import { error, json as outJson, jsonRequested } from "../output.js";
|
|
9
|
-
import { resolveSpace } from "../space.js";
|
|
10
|
+
import { currentIdentityKey, explicitSpace, resolveSpace } from "../space.js";
|
|
11
|
+
import { canonicalRuntimeRoot, resolveRuntimeSpace } from "../runtime/space-binding.js";
|
|
10
12
|
import { discoverHarnesses } from "../runtime/harness.js";
|
|
11
13
|
import { serveRuntime } from "../runtime/connection.js";
|
|
12
14
|
import { RuntimeSessionStore } from "../runtime/session-store.js";
|
|
13
15
|
import { ensureSandboxdBinary } from "./sandboxd-binary.js";
|
|
16
|
+
import { readRuntimeDiagnosticEvents, RuntimeDiagnosticReader, RuntimeDiagnostics, runtimeDiagnosticsDirectory, serializeDiagnosticError, } from "../runtime/diagnostics.js";
|
|
14
17
|
export const resolveLocalSpaceName = (root, name) => name?.trim() || basename(root) || "local-space";
|
|
18
|
+
function sandboxOutputLevel(value, stream) {
|
|
19
|
+
const level = typeof value === "string" ? value.toLowerCase() : "";
|
|
20
|
+
if (level.includes("error"))
|
|
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
|
+
}
|
|
15
81
|
export function parseRuntimeHarnesses(values) {
|
|
16
82
|
const names = values.flatMap((value) => value.split(",")).map((name) => name.trim()).filter(Boolean);
|
|
17
83
|
if (names.some((name) => !isLocalHarness(name)))
|
|
18
|
-
throw new Error("Harness must be pi or codex
|
|
84
|
+
throw new Error("Harness must be pi or codex");
|
|
19
85
|
return [...new Set(names.length ? names : ["pi"])];
|
|
20
86
|
}
|
|
21
87
|
export function registerRuntime(program) {
|
|
22
|
-
const runtime = program.command("runtime").description("Connect a local workspace
|
|
88
|
+
const runtime = program.command("runtime").description("Connect a local workspace");
|
|
23
89
|
runtime.command("up [dir]")
|
|
24
|
-
.description("Connect local Harnesses and files
|
|
25
|
-
.option("-s, --space <id>", "Target Space
|
|
26
|
-
.option("-n, --name <name>", "New Space name
|
|
27
|
-
.option("--harness <name>", "Pi or Codex; repeatable
|
|
28
|
-
.option("--pi <path>", "Pi executable
|
|
29
|
-
.option("--codex <path>", "Codex executable
|
|
30
|
-
.option("-y, --yes", "Accept local execution access
|
|
31
|
-
.option("--json", "JSON output
|
|
90
|
+
.description("Connect local Harnesses and files")
|
|
91
|
+
.option("-s, --space <id>", "Target Space")
|
|
92
|
+
.option("-n, --name <name>", "New Space name")
|
|
93
|
+
.option("--harness <name>", "Pi or Codex; repeatable", (value, previous) => [...previous, value], [])
|
|
94
|
+
.option("--pi <path>", "Pi executable")
|
|
95
|
+
.option("--codex <path>", "Codex executable")
|
|
96
|
+
.option("-y, --yes", "Accept local execution access")
|
|
97
|
+
.option("--json", "JSON output")
|
|
32
98
|
.action(async (dir, options) => {
|
|
33
99
|
const controller = new AbortController();
|
|
34
100
|
const stop = () => controller.abort();
|
|
35
101
|
process.once("SIGINT", stop);
|
|
36
102
|
process.once("SIGTERM", stop);
|
|
37
103
|
try {
|
|
38
|
-
const
|
|
39
|
-
if (!(await stat(
|
|
40
|
-
throw new Error("Workspace is not a directory
|
|
104
|
+
const requestedRoot = resolve(dir ?? process.cwd());
|
|
105
|
+
if (!(await stat(requestedRoot)).isDirectory())
|
|
106
|
+
throw new Error("Workspace is not a directory");
|
|
107
|
+
const root = await canonicalRuntimeRoot(requestedRoot);
|
|
41
108
|
const harnesses = parseRuntimeHarnesses(options.harness);
|
|
42
109
|
if (!options.yes) {
|
|
43
110
|
if (!process.stdin.isTTY)
|
|
44
|
-
throw new Error("Use --yes to authorize local execution
|
|
111
|
+
throw new Error("Use --yes to authorize local execution");
|
|
45
112
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
46
113
|
try {
|
|
47
|
-
const answer = await rl.question(`Connect ${root}? Space collaborators can run commands as your OS user, beyond this folder
|
|
114
|
+
const answer = await rl.question(`Connect ${root}? Space collaborators can run commands as your OS user, beyond this folder. [y/N] `);
|
|
48
115
|
if (!/^y(es)?$/i.test(answer.trim()))
|
|
49
116
|
return;
|
|
50
117
|
}
|
|
@@ -54,64 +121,208 @@ export function registerRuntime(program) {
|
|
|
54
121
|
}
|
|
55
122
|
const capabilities = await discoverHarnesses(harnesses, options, root);
|
|
56
123
|
const client = createClient();
|
|
57
|
-
const requested = options.space?.trim() || program
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
});
|
|
71
152
|
try {
|
|
72
|
-
await
|
|
73
|
-
|
|
74
|
-
token: requireAccessToken, signal: controller.signal, store: new RuntimeSessionStore(spaceId, undefined, client.space(spaceId)),
|
|
75
|
-
onReady: () => {
|
|
76
|
-
if (!bridge) {
|
|
77
|
-
bridge = spawn(binary, ["--local", "--space", spaceId, "--root", root, "--relay", process.env.COHUB_RELAY_URL?.trim() || relay.toString()], { stdio: ["ignore", "inherit", "inherit"], env: { ...process.env, COHUB_RELAY_TOKEN: token } });
|
|
78
|
-
bridgeClosed = new Promise((resolveClosed) => bridge?.once("close", () => resolveClosed()));
|
|
79
|
-
bridge.on("error", (cause) => { console.error(cause); controller.abort(); });
|
|
80
|
-
bridge.once("exit", () => controller.abort());
|
|
81
|
-
}
|
|
82
|
-
const webUrl = `${resolveCohubEnvironment() === "prod" ? "https://cohub.live" : "https://dev.cohub.live"}/spaces/${spaceId}`;
|
|
83
|
-
if (jsonRequested(options))
|
|
84
|
-
outJson({ spaceId, root, harnesses, url: webUrl });
|
|
85
|
-
else
|
|
86
|
-
console.error(`Runtime connected / Runtime 已连接: ${webUrl}`);
|
|
87
|
-
},
|
|
153
|
+
const binary = await ensureSandboxdBinary({
|
|
154
|
+
onStatus: (message) => diagnostics.log("info", "sandboxd.download", { message }, { component: "sandboxd" }),
|
|
88
155
|
});
|
|
156
|
+
const wsBase = resolveWebsocketUrl({ url: process.env.COHUB_WS_URL });
|
|
157
|
+
const url = new URL(wsBase);
|
|
158
|
+
url.pathname = "/runtime/relay";
|
|
159
|
+
const relay = new URL(wsBase);
|
|
160
|
+
relay.pathname = "/sandbox/relay";
|
|
161
|
+
let bridge = null;
|
|
162
|
+
let bridgeClosed = Promise.resolve();
|
|
163
|
+
let announced = false;
|
|
164
|
+
const token = await requireAccessToken();
|
|
165
|
+
try {
|
|
166
|
+
await serveRuntime({
|
|
167
|
+
spaceId,
|
|
168
|
+
cwd: root,
|
|
169
|
+
url: url.toString(),
|
|
170
|
+
capabilities,
|
|
171
|
+
harnesses: options,
|
|
172
|
+
runtimeId,
|
|
173
|
+
diagnostics,
|
|
174
|
+
token: requireAccessToken,
|
|
175
|
+
signal: controller.signal,
|
|
176
|
+
store,
|
|
177
|
+
onReady: () => {
|
|
178
|
+
if (!bridge) {
|
|
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);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (cause) {
|
|
223
|
+
diagnostics.log("error", "runtime.start_failed", { error: serializeDiagnosticError(cause) });
|
|
224
|
+
throw cause;
|
|
89
225
|
}
|
|
90
226
|
finally {
|
|
91
|
-
|
|
92
|
-
const child = bridge;
|
|
93
|
-
child.kill("SIGTERM");
|
|
94
|
-
const timeout = setTimeout(() => child.kill("SIGKILL"), 3000);
|
|
95
|
-
await bridgeClosed;
|
|
96
|
-
clearTimeout(timeout);
|
|
97
|
-
}
|
|
227
|
+
await diagnostics.close().catch((error) => console.error("Runtime diagnostics close failed:", error));
|
|
98
228
|
}
|
|
99
229
|
}
|
|
100
230
|
catch (cause) {
|
|
101
231
|
if (!controller.signal.aborted)
|
|
102
|
-
error("Runtime failed
|
|
232
|
+
error("Runtime failed", cause instanceof Error ? cause.message : String(cause));
|
|
103
233
|
}
|
|
104
234
|
finally {
|
|
105
235
|
process.removeListener("SIGINT", stop);
|
|
106
236
|
process.removeListener("SIGTERM", stop);
|
|
107
237
|
}
|
|
108
238
|
});
|
|
109
|
-
runtime.command("status").description("Runtime status
|
|
239
|
+
runtime.command("status").description("Runtime status").option("-s, --space <id>", "Target Space").action(async (options) => {
|
|
110
240
|
const spaceId = options.space?.trim() || await resolveSpace(program);
|
|
111
|
-
const
|
|
241
|
+
const client = createClient();
|
|
242
|
+
const spaceClient = client.space(spaceId);
|
|
243
|
+
const store = new RuntimeSessionStore(spaceId, { projectionSource: spaceClient });
|
|
112
244
|
const [status, pendingLocalArchives, failedLocalArchives] = await Promise.all([
|
|
113
|
-
|
|
245
|
+
spaceClient.getRuntime(),
|
|
246
|
+
store.archives.pendingCount(),
|
|
247
|
+
store.archives.failedCaptureCount(),
|
|
114
248
|
]);
|
|
115
|
-
outJson({
|
|
249
|
+
outJson({
|
|
250
|
+
...status,
|
|
251
|
+
diagnosticsPath: runtimeDiagnosticsDirectory(store.root),
|
|
252
|
+
pendingLocalArchives,
|
|
253
|
+
failedLocalArchives,
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
runtime.command("logs")
|
|
257
|
+
.description("Read local Runtime diagnostics")
|
|
258
|
+
.option("-s, --space <id>", "Target Space")
|
|
259
|
+
.option("-l, --limit <count>", "Number of events", "100")
|
|
260
|
+
.option("--follow", "Keep watching for new events")
|
|
261
|
+
.option("--json", "Print raw diagnostic events")
|
|
262
|
+
.action(async (options) => {
|
|
263
|
+
const spaceId = options.space?.trim() || await resolveSpace(program);
|
|
264
|
+
const store = new RuntimeSessionStore(spaceId, { projectionSource: createClient().space(spaceId) });
|
|
265
|
+
const limit = Number(options.limit ?? "100");
|
|
266
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10_000)
|
|
267
|
+
return error("Invalid diagnostic limit", "Use an integer between 1 and 10000 / 使用 1 到 10000 之间的整数");
|
|
268
|
+
const asJson = jsonRequested(options);
|
|
269
|
+
const reader = options.follow ? new RuntimeDiagnosticReader(store.root) : null;
|
|
270
|
+
const render = async () => {
|
|
271
|
+
const fresh = options.follow
|
|
272
|
+
? await reader?.read({ limit }) ?? []
|
|
273
|
+
: await readRuntimeDiagnosticEvents(store.root, { limit });
|
|
274
|
+
if (options.follow && fresh.length === 0)
|
|
275
|
+
return;
|
|
276
|
+
if (asJson && options.follow) {
|
|
277
|
+
for (const event of fresh)
|
|
278
|
+
process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
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);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
try {
|
|
292
|
+
await render();
|
|
293
|
+
if (!options.follow)
|
|
294
|
+
return;
|
|
295
|
+
await new Promise((resolve) => {
|
|
296
|
+
let timer = null;
|
|
297
|
+
let stopped = false;
|
|
298
|
+
const stop = () => {
|
|
299
|
+
stopped = true;
|
|
300
|
+
if (timer)
|
|
301
|
+
clearTimeout(timer);
|
|
302
|
+
process.removeListener("SIGINT", stop);
|
|
303
|
+
process.removeListener("SIGTERM", stop);
|
|
304
|
+
resolve();
|
|
305
|
+
};
|
|
306
|
+
const poll = async () => {
|
|
307
|
+
if (stopped)
|
|
308
|
+
return;
|
|
309
|
+
try {
|
|
310
|
+
await render();
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
stop();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (!stopped)
|
|
317
|
+
timer = setTimeout(() => void poll(), 2_000);
|
|
318
|
+
};
|
|
319
|
+
timer = setTimeout(() => void poll(), 2_000);
|
|
320
|
+
process.once("SIGINT", stop);
|
|
321
|
+
process.once("SIGTERM", stop);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
catch (cause) {
|
|
325
|
+
error("Runtime logs failed", cause instanceof Error ? cause.message : String(cause));
|
|
326
|
+
}
|
|
116
327
|
});
|
|
117
328
|
}
|
|
@@ -2,6 +2,8 @@ export declare const SANDBOXD_VERSION = "v1.82.4";
|
|
|
2
2
|
export declare class SandboxdDownloadError extends Error {
|
|
3
3
|
name: string;
|
|
4
4
|
}
|
|
5
|
+
export declare function validSandboxdArchiveEntries(entries: string[], version?: string): boolean;
|
|
6
|
+
export declare function validateSandboxdArchive(archivePath: string, version?: string): Promise<string[]>;
|
|
5
7
|
export type EnsureSandboxdOptions = {
|
|
6
8
|
version?: string;
|
|
7
9
|
onStatus?: (message: string) => void;
|