@schlenkr/ragents 0.1.17 → 0.1.19
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/src/server-client.ts +165 -0
- package/bundles/ragents.workspace/ragents-bundle.json +1 -1
- package/package.json +10 -2
- package/packages/agent/src/core/agent-settings.ts +2 -2
- package/packages/ai/src/api/ai-sdk.ts +15 -2
- package/packages/ai/src/types.ts +2 -1
- 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
|
@@ -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
|
}
|
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
-
import { copyFile, mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
-
import { builtinModules } from "node:module";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { readHostVersion, readPackageVersion } from "../../apps/server/src/host-version.ts";
|
|
8
|
-
import { hostWebDirectory, hostWebProblem, isCheckout } from "../../apps/server/src/host-web.ts";
|
|
9
|
-
import { runtimeLibraries } from "../../apps/server/src/plugin-support/actor-programs/runtime-libraries.ts";
|
|
10
|
-
import { assertBundleRevision, readBundleManifest, sourceRevisionOf } from "../../apps/server/src/profile/bundle-manifest.ts";
|
|
11
|
-
import { builtInPluginFolders } from "../plugin/build-builtin-plugins.ts";
|
|
12
|
-
|
|
13
|
-
export const PACKAGE_NAME = "@schlenkr/ragents";
|
|
14
|
-
/** The folder under dist/; the package name has a scope, the folder name stays plain. */
|
|
15
|
-
export const PACKAGE_FOLDER = "ragents";
|
|
16
|
-
|
|
17
|
-
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
18
|
-
|
|
19
|
-
const SKIPPED_FOLDERS = new Set(["node_modules", "dist", ".git"]);
|
|
20
|
-
const SKIPPED_FILES = new Set([".DS_Store"]);
|
|
21
|
-
|
|
22
|
-
/** The subcommands' tests run in the checkout; in the package only their test material would be visible. */
|
|
23
|
-
const SKIPPED_IN_PACKAGE = /^scripts\/.*\.test\.tsx?$/;
|
|
24
|
-
|
|
25
|
-
const packagePath = (packageRoot: string, file: string): string => path.relative(packageRoot, file).split(path.sep).join("/");
|
|
26
|
-
|
|
27
|
-
/** The repository's neutral profiles; ragents start runs them from the package as from the checkout. */
|
|
28
|
-
export const PACKAGED_PROFILES = ["core", "developer", "showcase"] as const;
|
|
29
|
-
|
|
30
|
-
export const PACKAGE_LICENSE = "PolyForm-Shield-1.0.0";
|
|
31
|
-
export const PACKAGE_AUTHOR = "Ronald Schlenker";
|
|
32
|
-
export const REPOSITORY_URL = "https://github.com/SchlenkR/RAgents";
|
|
33
|
-
export const PACKAGE_KEYWORDS = ["agents", "llm", "actors", "typescript", "csharp", "fsharp", "vscode"] as const;
|
|
34
|
-
|
|
35
|
-
/** License and listing README are at the root of the package, their sources elsewhere in the repository. */
|
|
36
|
-
export const PACKAGE_ROOT_FILES: readonly (readonly [string, string])[] = [
|
|
37
|
-
["LICENSE", "LICENSE"],
|
|
38
|
-
["scripts/package/README.md", "README.md"],
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
const ROOT_FILE_SOURCES = new Set(PACKAGE_ROOT_FILES.map(([source]) => source));
|
|
42
|
-
|
|
43
|
-
const skippedInPackage = (relative: string): boolean => SKIPPED_IN_PACKAGE.test(relative) || ROOT_FILE_SOURCES.has(relative);
|
|
44
|
-
|
|
45
|
-
/** What the host loads at runtime: server, engine, the built web, the built-in bundles, plugin sources for contracts and the build tool, profiles and the scripts behind the subcommands; plus the types from declarationFolders. */
|
|
46
|
-
export const packageContents = (root = repositoryRoot): readonly string[] => [
|
|
47
|
-
"apps/server/package.json",
|
|
48
|
-
"apps/server/tsconfig.json",
|
|
49
|
-
"apps/server/tsconfig.plugin.json",
|
|
50
|
-
"apps/server/src",
|
|
51
|
-
"apps/web/package.json",
|
|
52
|
-
"apps/web/tsconfig.json",
|
|
53
|
-
"apps/web/tsconfig.plugin.json",
|
|
54
|
-
"apps/web/src",
|
|
55
|
-
"apps/web/dist",
|
|
56
|
-
...readdirSync(path.join(root, "packages"), { withFileTypes: true })
|
|
57
|
-
.filter((entry) => entry.isDirectory())
|
|
58
|
-
.flatMap((entry) => [`packages/${entry.name}/package.json`, `packages/${entry.name}/src`]),
|
|
59
|
-
"bundles",
|
|
60
|
-
"plugins",
|
|
61
|
-
...PACKAGED_PROFILES.map((profile) => `ragents.config.${profile}.ts`),
|
|
62
|
-
"scripts/agent",
|
|
63
|
-
"scripts/package",
|
|
64
|
-
"scripts/plugin",
|
|
65
|
-
"scripts/provision",
|
|
66
|
-
"scripts/remote",
|
|
67
|
-
"scripts/workspace-client",
|
|
68
|
-
];
|
|
69
|
-
|
|
70
|
-
/** The forked agent runtime runs from its sources, its types exist only built; the build tool checks third-party plugins against them. */
|
|
71
|
-
export const declarationFolders = (root = repositoryRoot): readonly string[] =>
|
|
72
|
-
readdirSync(path.join(root, "packages"), { withFileTypes: true })
|
|
73
|
-
.filter((entry) => entry.isDirectory())
|
|
74
|
-
.filter((entry) => {
|
|
75
|
-
const manifest = path.join(root, "packages", entry.name, "package.json");
|
|
76
|
-
const types = statSync(manifest, { throwIfNoEntry: false })?.isFile() ? (JSON.parse(readFileSync(manifest, "utf8")) as { types?: unknown }).types : undefined;
|
|
77
|
-
return typeof types === "string" && types.startsWith("./dist/");
|
|
78
|
-
})
|
|
79
|
-
.map((entry) => `packages/${entry.name}/dist`);
|
|
80
|
-
|
|
81
|
-
/** What the host executes, bundles into a mini-app or reads when building a plugin. */
|
|
82
|
-
const SCANNED = ["apps/server/src", "apps/web", "packages", "plugins", "scripts"];
|
|
83
|
-
const PACKAGE_NAME_PATTERN = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
84
|
-
const SCAN_SKIPPED = new Set(["node_modules", "dist"]);
|
|
85
|
-
const SCANNED_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".mjs", ".js", ".css"]);
|
|
86
|
-
const IMPORT_PATTERN = /\b(?:from|import|require(?:\.resolve)?)\s*\(?\s*["']([^"'\n]+)["']/g;
|
|
87
|
-
/** An executor contribution resolves packages from the host at runtime; the package must carry them too. */
|
|
88
|
-
const HOST_PACKAGE_PATTERN = /\bhostPackageFile\([^,()]*,\s*["']([^"'\n]+)["']/g;
|
|
89
|
-
|
|
90
|
-
const files = async (folder: string, skippedFolders: ReadonlySet<string> = SKIPPED_FOLDERS): Promise<readonly string[]> => {
|
|
91
|
-
const found: string[] = [];
|
|
92
|
-
for (const entry of (await readdir(folder, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name, "en"))) {
|
|
93
|
-
if (entry.isDirectory()) {
|
|
94
|
-
if (!skippedFolders.has(entry.name)) found.push(...await files(path.join(folder, entry.name), skippedFolders));
|
|
95
|
-
} else if (entry.isFile() && !SKIPPED_FILES.has(entry.name)) {
|
|
96
|
-
found.push(path.join(folder, entry.name));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return found;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const copyTo = async (source: string, target: string, packageRoot: string, skippedFolders?: ReadonlySet<string>): Promise<readonly string[]> => {
|
|
103
|
-
if (statSync(source).isFile()) {
|
|
104
|
-
if (skippedInPackage(packagePath(packageRoot, target))) return [];
|
|
105
|
-
await mkdir(path.dirname(target), { recursive: true });
|
|
106
|
-
await copyFile(source, target);
|
|
107
|
-
return [target];
|
|
108
|
-
}
|
|
109
|
-
const copied: string[] = [];
|
|
110
|
-
for (const file of await files(source, skippedFolders)) {
|
|
111
|
-
const destination = path.join(target, path.relative(source, file));
|
|
112
|
-
if (skippedInPackage(packagePath(packageRoot, destination))) continue;
|
|
113
|
-
await mkdir(path.dirname(destination), { recursive: true });
|
|
114
|
-
await copyFile(file, destination);
|
|
115
|
-
copied.push(destination);
|
|
116
|
-
}
|
|
117
|
-
return copied;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const scanRoots = (root: string): readonly string[] => SCANNED.map((entry) => path.join(root, entry));
|
|
121
|
-
|
|
122
|
-
const scannedFiles = (folder: string): readonly string[] => {
|
|
123
|
-
if (!statSync(folder, { throwIfNoEntry: false })?.isDirectory()) return [];
|
|
124
|
-
return readdirSync(folder, { withFileTypes: true }).flatMap((entry) => {
|
|
125
|
-
const absolute = path.join(folder, entry.name);
|
|
126
|
-
if (entry.isDirectory()) return SCAN_SKIPPED.has(entry.name) ? [] : scannedFiles(absolute);
|
|
127
|
-
return entry.isFile() && SCANNED_EXTENSIONS.has(path.extname(entry.name)) ? [absolute] : [];
|
|
128
|
-
});
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const packageNameOf = (specifier: string): string | undefined => {
|
|
132
|
-
if (/^(\.{1,2}\/|\/|node:|data:|@ragents\/|virtual:)/.test(specifier) || specifier === "vscode") return undefined;
|
|
133
|
-
const parts = specifier.split("/");
|
|
134
|
-
const name = specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0]!;
|
|
135
|
-
if (builtinModules.includes(name)) return undefined;
|
|
136
|
-
return PACKAGE_NAME_PATTERN.test(name) ? name : undefined;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const declaredWebDependencies = (packageDirectory: string): readonly string[] => {
|
|
140
|
-
const file = path.join(packageDirectory, "apps/web/package.json");
|
|
141
|
-
const declared = (JSON.parse(readFileSync(file, "utf8")) as { dependencies?: Record<string, string> }).dependencies ?? {};
|
|
142
|
-
return Object.keys(declared);
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
/** The packages that the executed part of the package actually imports, plus the libraries linked at runtime
|
|
146
|
-
* and those that the host's web offers to the web halves of third-party plugins. */
|
|
147
|
-
export const requiredPackages = (packageDirectory: string): ReadonlyMap<string, string> => {
|
|
148
|
-
const sources = new Map<string, string>();
|
|
149
|
-
for (const name of runtimeLibraries) sources.set(name, "plugins/ragents.actor-programs (linked at runtime)");
|
|
150
|
-
for (const name of declaredWebDependencies(packageDirectory)) sources.set(name, "apps/web/package.json (for the web halves of the plugins)");
|
|
151
|
-
for (const folder of scanRoots(packageDirectory)) {
|
|
152
|
-
for (const file of scannedFiles(folder)) {
|
|
153
|
-
const content = readFileSync(file, "utf8");
|
|
154
|
-
for (const match of [...content.matchAll(IMPORT_PATTERN), ...content.matchAll(HOST_PACKAGE_PATTERN)]) {
|
|
155
|
-
const name = packageNameOf(match[1]!);
|
|
156
|
-
if (name && !sources.has(name)) sources.set(name, path.relative(packageDirectory, file));
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return new Map([...sources].sort(([left], [right]) => left.localeCompare(right, "en")));
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
const compareVersions = (left: string, right: string): number => {
|
|
164
|
-
const parts = (value: string) => value.split(/[.+-]/).map((part) => Number.isInteger(Number(part)) ? Number(part) : part);
|
|
165
|
-
const [first, second] = [parts(left), parts(right)];
|
|
166
|
-
for (let index = 0; index < Math.max(first.length, second.length); index += 1) {
|
|
167
|
-
const [one, other] = [first[index] ?? 0, second[index] ?? 0];
|
|
168
|
-
if (one === other) continue;
|
|
169
|
-
return typeof one === "number" && typeof other === "number" ? one - other : String(one).localeCompare(String(other), "en");
|
|
170
|
-
}
|
|
171
|
-
return 0;
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const anchors = (root: string): readonly string[] => [
|
|
175
|
-
path.join(root, "package.json"),
|
|
176
|
-
...["apps", "packages"].flatMap((group) => readdirSync(path.join(root, group), { withFileTypes: true })
|
|
177
|
-
.filter((entry) => entry.isDirectory())
|
|
178
|
-
.map((entry) => path.join(root, group, entry.name, "package.json"))
|
|
179
|
-
.filter((file) => statSync(file, { throwIfNoEntry: false })?.isFile() === true)),
|
|
180
|
-
];
|
|
181
|
-
|
|
182
|
-
/** The versions this checkout has installed for a package; the newest goes into the package. */
|
|
183
|
-
export const installedVersions = (name: string, root = repositoryRoot): readonly string[] => {
|
|
184
|
-
const found = new Set<string>();
|
|
185
|
-
for (const anchor of anchors(root)) {
|
|
186
|
-
for (let directory = path.dirname(anchor); ; directory = path.dirname(directory)) {
|
|
187
|
-
const file = path.join(directory, "node_modules", ...name.split("/"), "package.json");
|
|
188
|
-
if (statSync(file, { throwIfNoEntry: false })?.isFile()) {
|
|
189
|
-
found.add((JSON.parse(readFileSync(file, "utf8")) as { version: string }).version);
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
if (path.dirname(directory) === directory) break;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
const versions = [...found].sort(compareVersions);
|
|
196
|
-
if (versions.length === 0) throw new Error(`The package ${name} is not installed in this checkout; without a version it cannot go into the package`);
|
|
197
|
-
return versions;
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
export interface BuiltPackage {
|
|
201
|
-
readonly directory: string;
|
|
202
|
-
readonly manifest: Record<string, unknown>;
|
|
203
|
-
readonly fileCount: number;
|
|
204
|
-
readonly byteSize: number;
|
|
205
|
-
readonly flattened: readonly string[];
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** The package ships the web prebuilt; it must be built and match its sources. */
|
|
209
|
-
const assertHostWeb = (root: string): void => {
|
|
210
|
-
const problem = hostWebProblem(hostWebDirectory(root), root, isCheckout(root));
|
|
211
|
-
if (problem) throw new Error(`${problem}; run pnpm build:web first`);
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
/** Every built-in plugin must exist as a bundle and match its sources, otherwise the package starts no profile. */
|
|
215
|
-
const assertBuiltInBundles = (root: string): void => {
|
|
216
|
-
for (const folder of builtInPluginFolders(path.join(root, "plugins"))) {
|
|
217
|
-
const bundle = path.join(root, "bundles", path.basename(folder));
|
|
218
|
-
if (!statSync(bundle, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`The bundle ${bundle} is missing; run pnpm build:plugins first`);
|
|
219
|
-
if (readBundleManifest(bundle).sourceRevision !== sourceRevisionOf(folder)) throw new Error(`The bundle ${bundle} no longer matches ${folder}; run pnpm build:plugins first`);
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const copyDeclarations = async (root: string, target: string): Promise<readonly string[]> => {
|
|
224
|
-
const copied: string[] = [];
|
|
225
|
-
for (const folder of declarationFolders(root)) {
|
|
226
|
-
if (!statSync(path.join(root, folder), { throwIfNoEntry: false })?.isDirectory()) throw new Error(`${folder} is missing; run pnpm build:agent first`);
|
|
227
|
-
for (const file of (await files(path.join(root, folder))).filter((name) => name.endsWith(".d.ts"))) {
|
|
228
|
-
const destination = path.join(target, path.relative(root, file));
|
|
229
|
-
await mkdir(path.dirname(destination), { recursive: true });
|
|
230
|
-
await copyFile(file, destination);
|
|
231
|
-
copied.push(destination);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return copied;
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
export const buildPackage = async (target: string, root = repositoryRoot): Promise<BuiltPackage> => {
|
|
238
|
-
assertBuiltInBundles(root);
|
|
239
|
-
assertHostWeb(root);
|
|
240
|
-
await rm(target, { recursive: true, force: true });
|
|
241
|
-
await mkdir(target, { recursive: true });
|
|
242
|
-
let fileCount = 0;
|
|
243
|
-
let byteSize = 0;
|
|
244
|
-
for (const entry of packageContents(root)) {
|
|
245
|
-
// A bundle is fully built and goes into the package completely, including an asset folder named dist.
|
|
246
|
-
const copied = await copyTo(path.join(root, entry), path.join(target, entry), target, entry === "bundles" ? new Set() : undefined);
|
|
247
|
-
fileCount += copied.length;
|
|
248
|
-
for (const file of copied) byteSize += statSync(file).size;
|
|
249
|
-
}
|
|
250
|
-
for (const folder of builtInPluginFolders(path.join(root, "plugins"))) {
|
|
251
|
-
const bundle = path.join(target, "bundles", path.basename(folder));
|
|
252
|
-
assertBundleRevision(bundle, readBundleManifest(bundle));
|
|
253
|
-
}
|
|
254
|
-
for (const file of await copyDeclarations(root, target)) {
|
|
255
|
-
fileCount += 1;
|
|
256
|
-
byteSize += statSync(file).size;
|
|
257
|
-
}
|
|
258
|
-
for (const [source, packaged] of PACKAGE_ROOT_FILES) {
|
|
259
|
-
const destination = path.join(target, packaged);
|
|
260
|
-
await mkdir(path.dirname(destination), { recursive: true });
|
|
261
|
-
await copyFile(path.join(root, source), destination);
|
|
262
|
-
fileCount += 1;
|
|
263
|
-
byteSize += statSync(destination).size;
|
|
264
|
-
}
|
|
265
|
-
const flattened: string[] = [];
|
|
266
|
-
const dependencies: Record<string, string> = {};
|
|
267
|
-
for (const [name] of requiredPackages(target)) {
|
|
268
|
-
const versions = installedVersions(name, root);
|
|
269
|
-
if (versions.length > 1) flattened.push(`${name}: ${versions.join(", ")} -> ${versions.at(-1)!}`);
|
|
270
|
-
dependencies[name] = versions.at(-1)!;
|
|
271
|
-
}
|
|
272
|
-
const manifest = {
|
|
273
|
-
name: PACKAGE_NAME,
|
|
274
|
-
version: readPackageVersion(root),
|
|
275
|
-
description: "RAgents host: server, engine, plugins and tools without a source checkout",
|
|
276
|
-
keywords: [...PACKAGE_KEYWORDS],
|
|
277
|
-
homepage: "https://schlenkr.github.io/RAgents/",
|
|
278
|
-
bugs: { url: `${REPOSITORY_URL}/issues` },
|
|
279
|
-
license: PACKAGE_LICENSE,
|
|
280
|
-
author: PACKAGE_AUTHOR,
|
|
281
|
-
repository: { type: "git", url: `git+${REPOSITORY_URL}.git` },
|
|
282
|
-
type: "module",
|
|
283
|
-
bin: { ragents: "scripts/package/ragents.mjs" },
|
|
284
|
-
engines: { node: ">=22.19.0" },
|
|
285
|
-
publishConfig: { access: "public" },
|
|
286
|
-
dependencies,
|
|
287
|
-
ragents: { hostVersion: readHostVersion(root) },
|
|
288
|
-
};
|
|
289
|
-
const manifestFile = path.join(target, "package.json");
|
|
290
|
-
await writeFile(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
291
|
-
fileCount += 1;
|
|
292
|
-
byteSize += statSync(manifestFile).size;
|
|
293
|
-
return { directory: target, manifest, fileCount, byteSize, flattened };
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
const main = async (): Promise<void> => {
|
|
297
|
-
const pack = process.argv.includes("--pack");
|
|
298
|
-
const unknown = process.argv.slice(2).filter((argument) => argument !== "--pack");
|
|
299
|
-
if (unknown.length) throw new Error(`Unknown argument: ${unknown.join(" ")} (allowed: --pack)`);
|
|
300
|
-
const target = path.join(repositoryRoot, "dist", PACKAGE_FOLDER);
|
|
301
|
-
const built = await buildPackage(target);
|
|
302
|
-
console.log(`== Package ${PACKAGE_NAME}@${built.manifest.version as string} in ${built.directory}`);
|
|
303
|
-
console.log(`== Host version ${(built.manifest.ragents as { hostVersion: string }).hostVersion}`);
|
|
304
|
-
console.log(`== ${built.fileCount} files, ${(built.byteSize / 1024 / 1024).toFixed(1)} MB, ${Object.keys(built.manifest.dependencies as object).length} dependencies`);
|
|
305
|
-
for (const [name, version] of Object.entries(built.manifest.dependencies as Record<string, string>)) console.log(` ${name}@${version}`);
|
|
306
|
-
for (const entry of built.flattened) console.log(`== Multiple versions flattened: ${entry}`);
|
|
307
|
-
if (!pack) return;
|
|
308
|
-
const packed = spawnSync("npm", ["pack", "--pack-destination", path.dirname(built.directory)], { cwd: built.directory, encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
|
|
309
|
-
if (packed.status !== 0) throw new Error(`npm pack ended with code ${packed.status}`);
|
|
310
|
-
console.log(`== Archive ${path.join(path.dirname(built.directory), packed.stdout.trim().split("\n").at(-1) ?? "")}`);
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
const moduleUrl: string | undefined = import.meta.url;
|
|
314
|
-
if (moduleUrl && process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(moduleUrl)) {
|
|
315
|
-
main().catch((error: unknown) => {
|
|
316
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
317
|
-
process.exit(1);
|
|
318
|
-
});
|
|
319
|
-
}
|