@schlenkr/ragents 0.1.16 → 0.1.18
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 +5 -1
- package/apps/web/dist/help/guide-distributed.html +22 -8
- package/apps/web/dist/help/guide-distributed.md +28 -7
- package/apps/web/dist/host-web.json +1 -1
- package/apps/web/src/server-client.ts +165 -0
- package/bundles/ragents.activity/ragents-bundle.json +1 -1
- package/bundles/ragents.actor-programs/ragents-bundle.json +1 -1
- package/bundles/ragents.ask/ragents-bundle.json +1 -1
- package/bundles/ragents.browser/ragents-bundle.json +1 -1
- package/bundles/ragents.documents/ragents-bundle.json +1 -1
- package/bundles/ragents.lsp-fsharp/ragents-bundle.json +1 -1
- package/bundles/ragents.lsp-roslyn/ragents-bundle.json +1 -1
- package/bundles/ragents.lsp-typescript/ragents-bundle.json +1 -1
- package/bundles/ragents.model-relay/ragents-bundle.json +1 -1
- package/bundles/ragents.orchestration/ragents-bundle.json +1 -1
- package/bundles/ragents.overseer/ragents-bundle.json +1 -1
- package/bundles/ragents.processes/ragents-bundle.json +1 -1
- package/bundles/ragents.product/ragents-bundle.json +1 -1
- package/bundles/ragents.profile-distribution/ragents-bundle.json +1 -1
- package/bundles/ragents.reference/ragents-bundle.json +1 -1
- package/bundles/ragents.todo/ragents-bundle.json +1 -1
- package/bundles/ragents.transcript/ragents-bundle.json +1 -1
- package/bundles/ragents.watch/ragents-bundle.json +1 -1
- package/bundles/ragents.workspace/ragents-bundle.json +1 -1
- package/package.json +11 -2
- package/packages/ai/dist/api/openai-compatible-response.d.ts +2 -0
- package/packages/ai/package.json +1 -0
- package/packages/ai/src/api/ai-sdk-transport.ts +4 -2
- package/packages/ai/src/api/openai-compatible-response.ts +60 -0
- package/packages/workspace-executor/src/bundled-tools.ts +49 -0
- package/packages/workspace-executor/src/owner-lifetime.d.mts +18 -0
- package/packages/workspace-executor/src/owner-lifetime.mjs +102 -0
- package/plugins/ragents.workspace/client/transport.ts +9 -11
- package/plugins/ragents.workspace/client/workspace-client.ts +8 -1
- package/scripts/package/ragents.mjs +30 -4
- package/scripts/workspace-client/run-workspace-client.ts +51 -34
- package/scripts/package/build-package.ts +0 -319
- package/scripts/package/publish-package.ts +0 -144
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const WORKSPACE_TOOL_TARGETS = ["win32-x64", "win32-arm64", "darwin-x64", "darwin-arm64", "linux-x64", "linux-arm64"] as const;
|
|
6
|
+
export type WorkspaceToolTarget = typeof WORKSPACE_TOOL_TARGETS[number];
|
|
7
|
+
|
|
8
|
+
export const workspaceToolsPackageName = (target: string): string => `@schlenkr/ragents-tools-${target}`;
|
|
9
|
+
|
|
10
|
+
export interface BundledToolsOptions {
|
|
11
|
+
readonly root: string;
|
|
12
|
+
readonly distribution: "extension" | "package";
|
|
13
|
+
readonly platform?: NodeJS.Platform;
|
|
14
|
+
readonly arch?: string;
|
|
15
|
+
readonly paths?: typeof path;
|
|
16
|
+
readonly exists?: (file: string) => boolean;
|
|
17
|
+
readonly resolvePackage?: (specifier: string) => string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BundledTools {
|
|
21
|
+
readonly bash: string | undefined;
|
|
22
|
+
readonly rg: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const resolveBundledTools = (options: BundledToolsOptions): BundledTools => {
|
|
26
|
+
const platform = options.platform ?? process.platform;
|
|
27
|
+
const target = `${platform}-${options.arch ?? process.arch}`;
|
|
28
|
+
const paths = options.paths ?? path;
|
|
29
|
+
const exists = options.exists ?? existsSync;
|
|
30
|
+
const packageRoot = (): string => {
|
|
31
|
+
if (!WORKSPACE_TOOL_TARGETS.includes(target as WorkspaceToolTarget)) throw new Error(`Unsupported workstation platform: ${target}`);
|
|
32
|
+
const name = workspaceToolsPackageName(target);
|
|
33
|
+
try {
|
|
34
|
+
const resolve = options.resolvePackage ?? createRequire(paths.join(options.root, "package.json")).resolve;
|
|
35
|
+
return paths.dirname(resolve(`${name}/package.json`));
|
|
36
|
+
} catch {
|
|
37
|
+
throw new Error(`The bundled workstation tools ${name} are missing. Reinstall @schlenkr/ragents with optional dependencies enabled.`);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const root = options.distribution === "package" ? packageRoot() : options.root;
|
|
41
|
+
const bash = platform === "win32" ? paths.join(root, "dist", "bash", target, "usr", "bin", "bash.exe") : undefined;
|
|
42
|
+
const rg = paths.join(root, "dist", "rg", target, platform === "win32" ? "rg.exe" : "rg");
|
|
43
|
+
if (options.distribution === "package") {
|
|
44
|
+
for (const file of [rg, bash]) {
|
|
45
|
+
if (file !== undefined && !exists(file)) throw new Error(`Bundled workstation tool is missing: ${file}. Reinstall @schlenkr/ragents with optional dependencies enabled.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { bash, rg: exists(rg) ? rg : undefined };
|
|
49
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EventEmitter } from "node:events";
|
|
2
|
+
import type { Readable } from "node:stream";
|
|
3
|
+
|
|
4
|
+
export interface OwnerLifetimeOptions {
|
|
5
|
+
readonly detached?: boolean;
|
|
6
|
+
readonly parents?: readonly number[];
|
|
7
|
+
readonly onStop: (reason: string) => void;
|
|
8
|
+
readonly stdin?: Pick<Readable, "on" | "off" | "resume" | "pause" | "destroyed" | "readableEnded"> | null;
|
|
9
|
+
readonly signals?: EventEmitter;
|
|
10
|
+
readonly alive?: (pid: number) => boolean;
|
|
11
|
+
readonly intervalMs?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function processAlive(pid: number): boolean;
|
|
15
|
+
export function watchOwnerLifetime(options: OwnerLifetimeOptions): () => void;
|
|
16
|
+
export function processChildren(pid?: number): number[];
|
|
17
|
+
export function terminateProcessTree(pid: number): void;
|
|
18
|
+
export function exitWorkspaceProcess(code: number): never;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
export const processAlive = (pid) => {
|
|
5
|
+
try {
|
|
6
|
+
process.kill(pid, 0);
|
|
7
|
+
return true;
|
|
8
|
+
} catch (error) {
|
|
9
|
+
return error.code === "EPERM";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const watchOwnerLifetime = ({ detached = false, parents = [process.ppid], onStop, stdin = process.stdin, signals = process, alive = processAlive, intervalMs = 1000 }) => {
|
|
14
|
+
const releases = [];
|
|
15
|
+
const signalReleases = [];
|
|
16
|
+
let stopped = false;
|
|
17
|
+
const dispose = () => {
|
|
18
|
+
for (const release of releases.splice(0)) release();
|
|
19
|
+
};
|
|
20
|
+
const stop = (reason) => {
|
|
21
|
+
if (stopped) return;
|
|
22
|
+
stopped = true;
|
|
23
|
+
dispose();
|
|
24
|
+
onStop(reason);
|
|
25
|
+
};
|
|
26
|
+
const listen = (emitter, event, handler, keepDuringStop = false) => {
|
|
27
|
+
emitter.on(event, handler);
|
|
28
|
+
(keepDuringStop ? signalReleases : releases).push(() => emitter.off(event, handler));
|
|
29
|
+
};
|
|
30
|
+
listen(signals, "SIGINT", () => stop("SIGINT"), true);
|
|
31
|
+
listen(signals, "SIGTERM", () => stop("SIGTERM"), true);
|
|
32
|
+
listen(signals, "SIGHUP", () => { if (!detached) stop("SIGHUP"); }, true);
|
|
33
|
+
if (!detached) {
|
|
34
|
+
if (parents.some((pid) => !Number.isSafeInteger(pid) || pid <= 0)) throw new Error("The workspace owner PID must be a positive integer.");
|
|
35
|
+
const check = () => {
|
|
36
|
+
if (parents.some((pid) => !alive(pid))) stop("parent exited");
|
|
37
|
+
};
|
|
38
|
+
const timer = setInterval(check, intervalMs);
|
|
39
|
+
timer.unref();
|
|
40
|
+
releases.push(() => clearInterval(timer));
|
|
41
|
+
listen(signals, "disconnect", () => stop("owner disconnected"));
|
|
42
|
+
if (stdin) {
|
|
43
|
+
listen(stdin, "end", () => stop("stdin closed"));
|
|
44
|
+
listen(stdin, "close", () => stop("stdin closed"));
|
|
45
|
+
listen(stdin, "error", () => stop("stdin failed"));
|
|
46
|
+
stdin.resume();
|
|
47
|
+
releases.push(() => stdin.pause());
|
|
48
|
+
if (stdin.destroyed || stdin.readableEnded) queueMicrotask(() => stop("stdin closed"));
|
|
49
|
+
}
|
|
50
|
+
queueMicrotask(check);
|
|
51
|
+
}
|
|
52
|
+
return () => {
|
|
53
|
+
stopped = true;
|
|
54
|
+
dispose();
|
|
55
|
+
for (const release of signalReleases.splice(0)) release();
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const processChildren = (pid = process.pid) => {
|
|
60
|
+
if (process.platform === "win32") {
|
|
61
|
+
const result = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command",
|
|
62
|
+
`Get-CimInstance Win32_Process -Filter 'ParentProcessId = ${pid}' | ForEach-Object { $_.ProcessId }`], { encoding: "utf8", windowsHide: true, timeout: 5000 });
|
|
63
|
+
if (result.status !== 0) throw new Error("Cannot inspect workspace child processes for shutdown.");
|
|
64
|
+
return result.stdout.trim().split(/\s+/).map(Number).filter((child) => child > 0);
|
|
65
|
+
}
|
|
66
|
+
const rows = process.platform === "linux" ? readdirSync("/proc").filter((entry) => /^\d+$/.test(entry)).flatMap((entry) => {
|
|
67
|
+
try {
|
|
68
|
+
const stat = readFileSync(`/proc/${entry}/stat`, "utf8");
|
|
69
|
+
return [[Number(entry), Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1])]];
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (["ENOENT", "ESRCH", "EACCES"].includes(error.code)) return [];
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}) : (() => {
|
|
75
|
+
const table = spawnSync("ps", ["-axo", "pid=,ppid="], { encoding: "utf8" });
|
|
76
|
+
if (table.status !== 0) throw new Error("Cannot inspect workspace child processes for shutdown.");
|
|
77
|
+
return table.stdout.trim().split("\n").map((line) => line.trim().split(/\s+/).map(Number));
|
|
78
|
+
})();
|
|
79
|
+
const descendants = (parent) => rows.filter((row) => row[1] === parent).flatMap((row) => [...descendants(row[0]), row[0]]);
|
|
80
|
+
return descendants(pid);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const terminateProcessTree = (pid) => {
|
|
84
|
+
if (process.platform === "win32") {
|
|
85
|
+
const killed = spawnSync("taskkill", ["/T", "/F", "/PID", String(pid)], { stdio: "ignore", windowsHide: true, timeout: 5000 });
|
|
86
|
+
if (killed.status !== 0 && processAlive(pid)) throw new Error(`Could not stop workspace child process ${pid}.`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
for (const child of [...processChildren(pid), pid]) {
|
|
90
|
+
try { process.kill(child, "SIGKILL"); } catch (error) { if (error.code !== "ESRCH") throw error; }
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const exitWorkspaceProcess = (code) => {
|
|
95
|
+
for (const pid of processChildren()) {
|
|
96
|
+
if (process.platform === "win32") terminateProcessTree(pid);
|
|
97
|
+
else {
|
|
98
|
+
try { process.kill(pid, "SIGKILL"); } catch (error) { if (error.code !== "ESRCH") throw error; }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
process.exit(code);
|
|
102
|
+
};
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { WorkspaceClientTransport } from "./workspace-client";
|
|
1
|
+
import { ServerClient } from "../../../apps/web/src/server-client";
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
return
|
|
3
|
+
export const workspaceClientTransport = (baseUrl: string, token: string | undefined, environment: NodeJS.ProcessEnv = process.env): ServerClient =>
|
|
4
|
+
new ServerClient(baseUrl, token, fetch, {
|
|
5
|
+
log: (line) => console.error(line),
|
|
6
|
+
credentials: async () => {
|
|
7
|
+
const id = environment.RAGENTS_USER;
|
|
8
|
+
const password = environment.RAGENTS_PASSWORD;
|
|
9
|
+
return id && password ? { id, password } : undefined;
|
|
11
10
|
},
|
|
12
|
-
})
|
|
13
|
-
});
|
|
11
|
+
});
|
|
@@ -65,6 +65,7 @@ export interface WorkspaceClientOptions {
|
|
|
65
65
|
/** What the workstation needs for the server: the message layer. */
|
|
66
66
|
export interface WorkspaceClientTransport {
|
|
67
67
|
readonly rpc: RpcClient;
|
|
68
|
+
readonly signInFailure?: () => string | undefined;
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
type ExecuteInput = OperationInput<typeof workspaceClientContracts.execute>;
|
|
@@ -255,6 +256,11 @@ export class WorkspaceClient {
|
|
|
255
256
|
releases.push(
|
|
256
257
|
rpc.handle(workspaceClientContracts.execute, (input, context) => this.#execute(attachment, input, context)),
|
|
257
258
|
rpc.onConnected(() => void this.#announce()),
|
|
259
|
+
rpc.onStatus((status) => {
|
|
260
|
+
if (status.kind === "unauthorized" && attachment === this.#attachment) {
|
|
261
|
+
this.#set({ kind: "failed", message: this.transport.signInFailure?.() ?? "The server requires a sign-in.", mismatch: false });
|
|
262
|
+
}
|
|
263
|
+
}),
|
|
258
264
|
);
|
|
259
265
|
this.#attachment = attachment;
|
|
260
266
|
return attachment;
|
|
@@ -288,13 +294,14 @@ export class WorkspaceClient {
|
|
|
288
294
|
#connected(): Promise<void> {
|
|
289
295
|
const rpc = this.transport.rpc;
|
|
290
296
|
if (rpc.status.kind === "connected") return Promise.resolve();
|
|
297
|
+
if (rpc.status.kind === "unauthorized") return Promise.reject(new Error(this.transport.signInFailure?.() ?? "The server requires a sign-in."));
|
|
291
298
|
return new Promise((settle, fail) => {
|
|
292
299
|
const release = rpc.onStatus((status) => {
|
|
293
300
|
if (status.kind === "connecting") return;
|
|
294
301
|
release();
|
|
295
302
|
if (status.kind === "connected") settle();
|
|
296
303
|
else if (status.kind === "retrying") fail(new Error(status.message));
|
|
297
|
-
else if (status.kind === "unauthorized") fail(new Error("The server requires a sign-in."));
|
|
304
|
+
else if (status.kind === "unauthorized") fail(new Error(this.transport.signInFailure?.() ?? "The server requires a sign-in."));
|
|
298
305
|
else fail(new Error("The server's event stream is closed."));
|
|
299
306
|
});
|
|
300
307
|
});
|
|
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { terminateProcessTree, watchOwnerLifetime } from "../../packages/workspace-executor/src/owner-lifetime.mjs";
|
|
6
7
|
import { ensureHostLinks } from "./host-links.mjs";
|
|
7
8
|
|
|
8
9
|
const AGENT_CLI = "scripts/agent/agent-cli.ts";
|
|
@@ -55,14 +56,39 @@ const main = () => {
|
|
|
55
56
|
}
|
|
56
57
|
ensureHostLinks(root);
|
|
57
58
|
const passed = script === AGENT_CLI ? [command, ...rest] : rest;
|
|
59
|
+
const workspace = command === "workspace-client";
|
|
60
|
+
const detached = rest.includes("--detached");
|
|
58
61
|
const child = spawn(process.execPath, ["--import", "tsx", path.join(root, script), ...passed], {
|
|
59
62
|
cwd: path.join(root, "apps/server"),
|
|
60
|
-
env: { ...process.env, RAGENTS_CWD: process.cwd() },
|
|
61
|
-
stdio: "inherit",
|
|
63
|
+
env: { ...process.env, RAGENTS_CWD: process.cwd(), ...(workspace ? { RAGENTS_WORKSPACE_OWNER_PID: String(process.ppid) } : {}) },
|
|
64
|
+
stdio: workspace ? ["ignore", "inherit", "inherit", "ipc"] : "inherit",
|
|
65
|
+
detached: workspace && process.platform !== "win32",
|
|
62
66
|
});
|
|
67
|
+
if (workspace) {
|
|
68
|
+
let stopReason;
|
|
69
|
+
let deadline;
|
|
70
|
+
const requestStop = () => {
|
|
71
|
+
if (child.connected && stopReason) child.send({ type: "workspace-stop", reason: stopReason }, () => undefined);
|
|
72
|
+
};
|
|
73
|
+
const release = watchOwnerLifetime({ detached, onStop: (reason) => {
|
|
74
|
+
stopReason = reason;
|
|
75
|
+
requestStop();
|
|
76
|
+
deadline = setTimeout(() => { if (child.pid) terminateProcessTree(child.pid); }, 20_000);
|
|
77
|
+
} });
|
|
78
|
+
child.on("message", (message) => { if (message?.type === "workspace-ready") requestStop(); });
|
|
79
|
+
child.once("exit", () => {
|
|
80
|
+
release();
|
|
81
|
+
clearTimeout(deadline);
|
|
82
|
+
if (process.platform !== "win32" && child.pid) {
|
|
83
|
+
try { process.kill(-child.pid, "SIGKILL"); } catch {}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
63
87
|
const forward = (signal) => () => child.kill(signal);
|
|
64
|
-
|
|
65
|
-
|
|
88
|
+
if (!workspace) {
|
|
89
|
+
process.on("SIGINT", forward("SIGINT"));
|
|
90
|
+
process.on("SIGTERM", forward("SIGTERM"));
|
|
91
|
+
}
|
|
66
92
|
child.once("error", (error) => {
|
|
67
93
|
console.error(`ragents ${command} could not be started: ${error.message}`);
|
|
68
94
|
process.exit(1);
|
|
@@ -3,25 +3,28 @@ import { statSync } from "node:fs";
|
|
|
3
3
|
import { hostname } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { resolveBundledTools } from "../../packages/workspace-executor/src/bundled-tools.ts";
|
|
7
|
+
import { exitWorkspaceProcess, watchOwnerLifetime } from "../../packages/workspace-executor/src/owner-lifetime.mjs";
|
|
8
|
+
import { isCheckout } from "../../apps/server/src/host-web.ts";
|
|
6
9
|
import { hostRoot } from "../../apps/server/src/host-version.ts";
|
|
7
10
|
import { callerDirectory } from "../../apps/server/src/profile-target.ts";
|
|
8
11
|
import { provisionWorkspace } from "../../apps/server/src/profile/provisioning.ts";
|
|
9
12
|
import { workspaceClientTransport } from "../../plugins/ragents.workspace/client/transport.ts";
|
|
10
13
|
import { WorkspaceClient, workstationRunsDirectory } from "../../plugins/ragents.workspace/client/workspace-client.ts";
|
|
11
14
|
|
|
12
|
-
const usage = (): string => `Usage:
|
|
13
|
-
Registers the folders as a workspace
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
script runs until it is ended with Ctrl-C.`;
|
|
15
|
+
const usage = (): string => `Usage: ragents workspace-client <server-url> [folder ...] [--id <id>] [--label <name>] [--detached]
|
|
16
|
+
Registers the folders as a workspace and executes tasks with the same tools as VS Code.
|
|
17
|
+
Without folders the current directory applies. Tools are provisioned before registration.
|
|
18
|
+
The npm package includes ripgrep and, on Windows, Bash. In a checkout run pnpm bundle:rg
|
|
19
|
+
(and pnpm bundle:bash on Windows) first.
|
|
20
|
+
Set RAGENTS_TOKEN for an existing session or RAGENTS_USER and RAGENTS_PASSWORD for automatic
|
|
21
|
+
sign-in and renewal. Credentials belong only in the environment.
|
|
22
|
+
By default closing stdin, the terminal, or the owning process ends the client and its children.
|
|
23
|
+
--detached explicitly opts out of terminal and parent ownership for nohup or a service manager;
|
|
24
|
+
SIGINT and SIGTERM still stop it. It does not daemonize the process.`;
|
|
23
25
|
|
|
24
26
|
export interface WorkspaceClientArguments {
|
|
27
|
+
readonly detached: boolean;
|
|
25
28
|
readonly serverUrl: string;
|
|
26
29
|
readonly folders: readonly string[];
|
|
27
30
|
readonly id: string | undefined;
|
|
@@ -30,11 +33,13 @@ export interface WorkspaceClientArguments {
|
|
|
30
33
|
|
|
31
34
|
export const parseArguments = (argv: readonly string[]): WorkspaceClientArguments => {
|
|
32
35
|
const folders: string[] = [];
|
|
36
|
+
let detached = false;
|
|
33
37
|
let serverUrl: string | undefined;
|
|
34
38
|
let id: string | undefined;
|
|
35
39
|
let label: string | undefined;
|
|
36
40
|
for (let index = 0; index < argv.length; index += 1) {
|
|
37
41
|
const argument = argv[index]!;
|
|
42
|
+
if (argument === "--detached") { detached = true; continue; }
|
|
38
43
|
if (argument === "--id" || argument === "--label") {
|
|
39
44
|
const value = argv[index + 1];
|
|
40
45
|
if (!value || value.startsWith("-")) throw new Error(`${argument} needs a value.\n${usage()}`);
|
|
@@ -49,7 +54,7 @@ export const parseArguments = (argv: readonly string[]): WorkspaceClientArgument
|
|
|
49
54
|
}
|
|
50
55
|
if (!serverUrl) throw new Error(`The server address is missing.\n${usage()}`);
|
|
51
56
|
if (id !== undefined && !/^[A-Za-z0-9_-]{8,64}$/.test(id)) throw new Error("--id needs 8 to 64 characters from A-Z, a-z, 0-9, _ and -.");
|
|
52
|
-
return { serverUrl, folders, id, label };
|
|
57
|
+
return { serverUrl, folders, id, label, detached };
|
|
53
58
|
};
|
|
54
59
|
|
|
55
60
|
/** A stable id per machine and folder set, so the server recognizes the same workspace again. */
|
|
@@ -67,6 +72,27 @@ export const resolvedFolders = (folders: readonly string[], caller = callerDirec
|
|
|
67
72
|
|
|
68
73
|
const main = async (): Promise<void> => {
|
|
69
74
|
const parsed = parseArguments(process.argv.slice(2));
|
|
75
|
+
let stopping: Promise<void> | undefined;
|
|
76
|
+
const stop = (code: number, reason: string): Promise<void> => stopping ??= (async () => {
|
|
77
|
+
console.log(`== Stopping workspace: ${reason}`);
|
|
78
|
+
const force = setTimeout(() => exitWorkspaceProcess(code || 1), 15_000);
|
|
79
|
+
try {
|
|
80
|
+
await client.unregister();
|
|
81
|
+
transport.rpc.close();
|
|
82
|
+
} finally {
|
|
83
|
+
clearTimeout(force);
|
|
84
|
+
exitWorkspaceProcess(code);
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
const releaseOwner = watchOwnerLifetime({
|
|
88
|
+
detached: parsed.detached,
|
|
89
|
+
parents: [process.ppid, ...process.env.RAGENTS_WORKSPACE_OWNER_PID ? [Number(process.env.RAGENTS_WORKSPACE_OWNER_PID)] : []],
|
|
90
|
+
stdin: process.send ? null : process.stdin,
|
|
91
|
+
onStop: (reason) => { void stop(0, reason); },
|
|
92
|
+
});
|
|
93
|
+
process.on("message", (message: unknown) => {
|
|
94
|
+
if (typeof message === "object" && message !== null && (message as { type?: string }).type === "workspace-stop") void stop(0, "owner stopped");
|
|
95
|
+
});
|
|
70
96
|
const folders = resolvedFolders(parsed.folders);
|
|
71
97
|
const host = hostname();
|
|
72
98
|
const identity = {
|
|
@@ -78,46 +104,37 @@ const main = async (): Promise<void> => {
|
|
|
78
104
|
runsDirectory: workstationRunsDirectory(),
|
|
79
105
|
};
|
|
80
106
|
const transport = workspaceClientTransport(parsed.serverUrl, process.env.RAGENTS_TOKEN);
|
|
107
|
+
const root = hostRoot();
|
|
108
|
+
const tools = resolveBundledTools({ root: isCheckout(root) ? path.join(root, "apps/vscode") : root, distribution: isCheckout(root) ? "extension" : "package" });
|
|
81
109
|
const client = new WorkspaceClient(transport, identity, {
|
|
82
110
|
hostRoot,
|
|
83
|
-
|
|
84
|
-
rg: process.env.RAGENTS_RG || undefined,
|
|
111
|
+
...tools,
|
|
85
112
|
onExecuted: ({ runId, operation, durationMs, error }) =>
|
|
86
113
|
console.log(`== ${runId.slice(0, 8)} ${operation} ${durationMs} ms ${error ?? "ok"}`),
|
|
87
114
|
});
|
|
88
115
|
console.log(`== Workspace ${identity.label} (${identity.id})`);
|
|
89
116
|
for (const folder of folders) console.log(`== Folder ${folder}`);
|
|
117
|
+
process.send?.({ type: "workspace-ready" });
|
|
90
118
|
console.log("== Provisioning tools");
|
|
91
119
|
await provisionWorkspace((line) => console.log(line));
|
|
92
120
|
console.log(`== Server ${parsed.serverUrl}`);
|
|
93
|
-
|
|
94
|
-
|
|
121
|
+
const workspace = client;
|
|
122
|
+
workspace.onChange(() => {
|
|
123
|
+
const status = workspace.status;
|
|
95
124
|
console.log(status.kind === "failed" ? `== Registration failed: ${status.message}` : `== State ${status.kind}`);
|
|
125
|
+
if (status.kind === "failed" && !stopping && (status.mismatch || transport.rpc.status.kind === "unauthorized")) void stop(1, "registration or sign-in failed");
|
|
96
126
|
});
|
|
97
|
-
await
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
transport.rpc.close();
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
127
|
+
await workspace.register();
|
|
128
|
+
if (stopping) return;
|
|
129
|
+
if (workspace.status.kind !== "registered") return stop(1, "registration failed");
|
|
103
130
|
console.log("== Registered; Ctrl-C ends the workspace");
|
|
104
|
-
|
|
105
|
-
const stop = () => {
|
|
106
|
-
process.off("SIGINT", stop);
|
|
107
|
-
process.off("SIGTERM", stop);
|
|
108
|
-
finish();
|
|
109
|
-
};
|
|
110
|
-
process.on("SIGINT", stop);
|
|
111
|
-
process.on("SIGTERM", stop);
|
|
112
|
-
});
|
|
113
|
-
await client.unregister();
|
|
114
|
-
transport.rpc.close();
|
|
131
|
+
process.once("exit", releaseOwner);
|
|
115
132
|
};
|
|
116
133
|
|
|
117
134
|
const moduleUrl: string | undefined = import.meta.url;
|
|
118
135
|
if (moduleUrl && process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(moduleUrl)) {
|
|
119
136
|
main().catch((error: unknown) => {
|
|
120
137
|
console.error(error instanceof Error ? error.message : String(error));
|
|
121
|
-
|
|
138
|
+
exitWorkspaceProcess(1);
|
|
122
139
|
});
|
|
123
140
|
}
|