@schlenkr/ragents 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/apps/server/src/profile-target.ts +25 -0
- package/apps/server/src/web-build.ts +82 -0
- package/apps/server/tsconfig.json +38 -0
- package/apps/web/column.html +12 -0
- package/apps/web/index.html +12 -0
- package/apps/web/tsconfig.json +41 -0
- package/apps/web/vite.config.ts +140 -0
- package/package.json +5 -1
- package/plugins/ragents.profile-distribution/server/web.ts +3 -27
- package/scripts/agent/agent-cli.ts +9 -24
- package/scripts/package/build-package.ts +18 -5
- package/scripts/package/publish-package.ts +20 -5
- package/scripts/package/ragents.mjs +3 -2
- package/scripts/remote/connect.ts +16 -5
- package/scripts/remote/start.ts +33 -15
package/README.md
CHANGED
|
@@ -22,11 +22,15 @@ npm install -g @schlenkr/ragents
|
|
|
22
22
|
RAGENTS_TOKEN=<token> ragents connect https://ragents.example.com
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
- `start <profile>` - start a profile shipped with the package (`core`, `developer`)
|
|
26
|
-
`
|
|
25
|
+
- `start <profile|path>` - start a profile shipped with the package (`core`, `developer`), your
|
|
26
|
+
own `ragents.config.<name>.ts` anywhere on disk, or a stand that `connect` already fetched,
|
|
27
|
+
without asking the server. The web interface is built for that set of plugins when it is
|
|
28
|
+
missing or was last built for another profile or host version; the result goes to
|
|
29
|
+
`<data dir>/web/<profile>`, never into the package folder.
|
|
27
30
|
|
|
28
31
|
```sh
|
|
29
32
|
ragents start developer
|
|
33
|
+
ragents start ~/projects/mine/ragents.config.mine.ts
|
|
30
34
|
```
|
|
31
35
|
|
|
32
36
|
- `provision [<profile>|--workspace]` - fetch the tools the plugins of that profile need, such as
|
|
@@ -44,7 +48,8 @@ npm install -g @schlenkr/ragents
|
|
|
44
48
|
```
|
|
45
49
|
|
|
46
50
|
- `run <folder> "<task>"` - start the host if none answers, bind a new run to the folder and wait
|
|
47
|
-
until the turn is finished; the last line is always `run: <id>`.
|
|
51
|
+
until the turn is finished; the last line is always `run: <id>`. This host serves no web
|
|
52
|
+
interface - an agent does not need one; use `start` for that.
|
|
48
53
|
|
|
49
54
|
```sh
|
|
50
55
|
ragents run ~/projects/demo "Fix the type error in src/broken.ts"
|
|
@@ -71,7 +76,8 @@ npm install -g @schlenkr/ragents
|
|
|
71
76
|
|
|
72
77
|
## Requirements
|
|
73
78
|
|
|
74
|
-
- Node 22 (>= 22.19.0). No git, no pnpm, no
|
|
79
|
+
- Node 22 (>= 22.19.0). No git, no pnpm, no source checkout; the package builds its own web
|
|
80
|
+
interface per profile with the Vite it brings along.
|
|
75
81
|
- `dotnet` (.NET 10 SDK) for the C# diagnostics through Roslyn and the F# diagnostics through FSAC;
|
|
76
82
|
without it TypeScript diagnostics still work.
|
|
77
83
|
- Model access: either `OPENROUTER_API_KEY` in the environment, or `ragents connect <server-url>`
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { isEnvironmentReference } from "./config-definition.js";
|
|
5
|
+
import { defaultDataDirectory } from "./data-directory.js";
|
|
6
|
+
|
|
7
|
+
export interface ProfileTarget {
|
|
8
|
+
readonly profile: string;
|
|
9
|
+
readonly profileFile: string;
|
|
10
|
+
readonly port: number;
|
|
11
|
+
readonly dataDirectory: string;
|
|
12
|
+
readonly baseUrl: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Port und Datenordner eines Profils, wie der Server sie später selbst bestimmt: Umgebung vor Profildatei vor Vorgabe. */
|
|
16
|
+
export const readProfileTarget = async (profile: string, profileFile: string, environment = process.env): Promise<ProfileTarget> => {
|
|
17
|
+
if (!statSync(profileFile, { throwIfNoEntry: false })?.isFile()) throw new Error(`Die Profildatei fehlt: ${profileFile}`);
|
|
18
|
+
const module = await import(pathToFileURL(profileFile).href) as { config: { host: { PORT?: number; DATA_DIR?: unknown } } };
|
|
19
|
+
const port = Number(environment.PORT || module.config.host.PORT);
|
|
20
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`host.PORT des Profils ${profile} ist keine gültige Portnummer.`);
|
|
21
|
+
const configured = module.config.host.DATA_DIR;
|
|
22
|
+
const fromFile = isEnvironmentReference(configured) ? environment[configured.name] : typeof configured === "string" ? configured : undefined;
|
|
23
|
+
const dataDirectory = path.resolve(environment.DATA_DIR ?? fromFile ?? defaultDataDirectory(profile));
|
|
24
|
+
return { profile, profileFile, port, dataDirectory, baseUrl: `http://localhost:${port}` };
|
|
25
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { statSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
7
|
+
import { readHostVersion } from "./host-version.js";
|
|
8
|
+
|
|
9
|
+
/** Vite kommt im Checkout aus apps/web/node_modules, im Paket aus dessen Abhängigkeiten; beide Male ab apps/web aufgelöst. */
|
|
10
|
+
export const viteEntry = (root: string): string => {
|
|
11
|
+
try {
|
|
12
|
+
const manifest = createRequire(path.join(root, "apps/web/package.json")).resolve("vite/package.json");
|
|
13
|
+
return path.join(path.dirname(manifest), "bin/vite.js");
|
|
14
|
+
} catch (cause) {
|
|
15
|
+
throw new Error(`Der Web-Build findet Vite neben ${path.join(root, "apps/web")} nicht: `
|
|
16
|
+
+ `${cause instanceof Error ? cause.message : String(cause)}`);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const run = (file: string, args: readonly string[], cwd: string, environment: NodeJS.ProcessEnv, log: (line: string) => void): Promise<void> =>
|
|
21
|
+
new Promise((resolve, reject) => {
|
|
22
|
+
const child = spawn(file, args, { cwd, env: environment, stdio: ["ignore", "pipe", "pipe"] });
|
|
23
|
+
for (const stream of [child.stdout, child.stderr]) createInterface({ input: stream! }).on("line", log);
|
|
24
|
+
child.once("error", reject);
|
|
25
|
+
child.once("exit", (code) => code === 0 ? resolve() : reject(new Error(`Der Web-Build endete mit Code ${code}`)));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export interface WebBuild {
|
|
29
|
+
readonly root: string;
|
|
30
|
+
/** Node, das Vite ausführt; in der Erweiterung ist process.execPath Electron und taugt nicht. */
|
|
31
|
+
readonly node?: string;
|
|
32
|
+
readonly outputDirectory: string;
|
|
33
|
+
readonly environment: NodeJS.ProcessEnv;
|
|
34
|
+
readonly log: (line: string) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Baut das Web für den Pluginsatz, den die Umgebung nennt: PRODUCT_PROFILE samt Datei oder PLUGIN_LIST_FILE. */
|
|
38
|
+
export const buildWeb = async (build: WebBuild): Promise<void> => {
|
|
39
|
+
const entry = viteEntry(build.root);
|
|
40
|
+
await rm(build.outputDirectory, { recursive: true, force: true });
|
|
41
|
+
await mkdir(path.dirname(build.outputDirectory), { recursive: true });
|
|
42
|
+
await run(build.node ?? process.execPath, [entry, "build", "--outDir", build.outputDirectory, "--emptyOutDir"], path.join(build.root, "apps/web"), build.environment, build.log);
|
|
43
|
+
if (!statSync(path.join(build.outputDirectory, "index.html"), { throwIfNoEntry: false })?.isFile()) {
|
|
44
|
+
throw new Error(`Der Web-Build hat keine index.html in ${build.outputDirectory} hinterlassen`);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Je Profil ein gebautes Web im Datenordner; der Paketordner bleibt unberührt. */
|
|
49
|
+
export const webBuildDirectory = (dataDirectory: string, profile: string): string => path.join(dataDirectory, "web", profile);
|
|
50
|
+
|
|
51
|
+
export interface WebBuildTarget {
|
|
52
|
+
readonly root: string;
|
|
53
|
+
readonly node?: string;
|
|
54
|
+
readonly profile: string;
|
|
55
|
+
readonly profileFile: string;
|
|
56
|
+
readonly dataDirectory: string;
|
|
57
|
+
readonly environment: NodeJS.ProcessEnv;
|
|
58
|
+
readonly log: (line: string) => void;
|
|
59
|
+
readonly build?: (build: WebBuild) => Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const stampOf = (target: WebBuildTarget): string =>
|
|
63
|
+
JSON.stringify({ root: target.root, profileFile: target.profileFile, hostVersion: readHostVersion(target.root) }, null, 2);
|
|
64
|
+
|
|
65
|
+
/** Baut das Web des Profils, wenn es fehlt oder zuletzt für ein anderes Profil oder eine andere Host-Fassung gebaut wurde. */
|
|
66
|
+
export const ensureWebBuild = async (target: WebBuildTarget): Promise<string> => {
|
|
67
|
+
const directory = webBuildDirectory(target.dataDirectory, target.profile);
|
|
68
|
+
const stampFile = `${directory}.json`;
|
|
69
|
+
const stamp = stampOf(target);
|
|
70
|
+
const built = statSync(path.join(directory, "index.html"), { throwIfNoEntry: false })?.isFile() === true;
|
|
71
|
+
if (built && (await readFile(stampFile, "utf8").catch(() => undefined))?.trim() === stamp) return directory;
|
|
72
|
+
target.log(`== Web des Profils ${target.profile} bauen nach ${directory}`);
|
|
73
|
+
await (target.build ?? buildWeb)({
|
|
74
|
+
root: target.root,
|
|
75
|
+
node: target.node,
|
|
76
|
+
outputDirectory: directory,
|
|
77
|
+
environment: { ...target.environment, PRODUCT_PROFILE: target.profile, PRODUCT_PROFILE_FILE: target.profileFile },
|
|
78
|
+
log: target.log,
|
|
79
|
+
});
|
|
80
|
+
await writeFile(stampFile, `${stamp}\n`);
|
|
81
|
+
return directory;
|
|
82
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2024",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"allowImportingTsExtensions": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"isolatedModules": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"types": [
|
|
11
|
+
"node"
|
|
12
|
+
],
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"lib": [
|
|
15
|
+
"ES2024"
|
|
16
|
+
],
|
|
17
|
+
"paths": {
|
|
18
|
+
"@aicontainer/server/*": [
|
|
19
|
+
"../../apps/server/src/*"
|
|
20
|
+
],
|
|
21
|
+
"@aicontainer/web/*": [
|
|
22
|
+
"../../apps/web/src/*"
|
|
23
|
+
],
|
|
24
|
+
"@aicontainer/plugins/*": [
|
|
25
|
+
"../../plugins/*"
|
|
26
|
+
],
|
|
27
|
+
"@ragents/workflow": [
|
|
28
|
+
"../../plugins/ragents.actor-programs/workflow/index.ts"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"include": [
|
|
33
|
+
"src",
|
|
34
|
+
"../../plugins/*/server",
|
|
35
|
+
"../../plugins/*/contract.ts",
|
|
36
|
+
"../../ragents.config.*.ts"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="de">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>RAgents Panel</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/column.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="de">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>RAgents</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"isolatedModules": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"lib": [
|
|
11
|
+
"ES2024",
|
|
12
|
+
"DOM",
|
|
13
|
+
"DOM.Iterable"
|
|
14
|
+
],
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
"allowImportingTsExtensions": true,
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": [
|
|
19
|
+
"./src/*"
|
|
20
|
+
],
|
|
21
|
+
"@aicontainer/server/*": [
|
|
22
|
+
"../../apps/server/src/*"
|
|
23
|
+
],
|
|
24
|
+
"@aicontainer/web/*": [
|
|
25
|
+
"../../apps/web/src/*"
|
|
26
|
+
],
|
|
27
|
+
"@aicontainer/plugins/*": [
|
|
28
|
+
"../../plugins/*"
|
|
29
|
+
],
|
|
30
|
+
"@ragents/workflow": [
|
|
31
|
+
"../../plugins/ragents.actor-programs/workflow/index.ts"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"include": [
|
|
36
|
+
"src",
|
|
37
|
+
"../../plugins/*/web",
|
|
38
|
+
"../../plugins/ragents.actor-programs/client-ui/index.tsx",
|
|
39
|
+
"../../plugins/*/contract.ts"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
4
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import react from "@vitejs/plugin-react";
|
|
7
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
8
|
+
import { defineConfig, type Plugin } from "vite";
|
|
9
|
+
|
|
10
|
+
const apiTarget = process.env.API_TARGET ?? "http://localhost:4710";
|
|
11
|
+
const repositoryRoot = fileURLToPath(new URL("../../", import.meta.url));
|
|
12
|
+
const hostEntry = path.join(repositoryRoot, "apps/web/src/main.tsx");
|
|
13
|
+
const pluginEntriesModule = "virtual:ragents-plugins";
|
|
14
|
+
|
|
15
|
+
interface BundledPlugin {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly folder: string;
|
|
18
|
+
readonly web: string | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const webEntryOf = (folder: string): string | undefined =>
|
|
22
|
+
["web/index.ts", "web/index.tsx"].map((name) => path.join(folder, name))
|
|
23
|
+
.find((candidate) => statSync(candidate, { throwIfNoEntry: false })?.isFile() === true);
|
|
24
|
+
|
|
25
|
+
const repositoryPlugins = (): BundledPlugin[] => {
|
|
26
|
+
const root = path.join(repositoryRoot, "plugins");
|
|
27
|
+
return readdirSync(root, { withFileTypes: true })
|
|
28
|
+
.filter((entry) => entry.isDirectory())
|
|
29
|
+
.map((entry) => ({ id: entry.name, folder: path.join(root, entry.name), web: webEntryOf(path.join(root, entry.name)) }));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** The profile's plugins as the server resolves them; PLUGIN_LIST_FILE names them directly, otherwise the last stdout line carries the JSON. */
|
|
33
|
+
const profilePlugins = (): BundledPlugin[] => {
|
|
34
|
+
const listFile = process.env.PLUGIN_LIST_FILE;
|
|
35
|
+
if (listFile) return JSON.parse(readFileSync(listFile, "utf8")) as BundledPlugin[];
|
|
36
|
+
if (!process.env.PRODUCT_PROFILE) return [];
|
|
37
|
+
const output = execFileSync("node", ["--import", "tsx", "src/profile/plugin-list.ts"], {
|
|
38
|
+
cwd: path.join(repositoryRoot, "apps/server"),
|
|
39
|
+
encoding: "utf8",
|
|
40
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
41
|
+
});
|
|
42
|
+
return JSON.parse(output.trim().split("\n").at(-1) ?? "[]") as BundledPlugin[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Bundles the repository's plugins and, with a profile, its plugins from anywhere on disk. */
|
|
46
|
+
const bundledPlugins = (): BundledPlugin[] => {
|
|
47
|
+
const plugins = repositoryPlugins();
|
|
48
|
+
for (const plugin of profilePlugins()) {
|
|
49
|
+
const known = plugins.find((candidate) => candidate.id === plugin.id);
|
|
50
|
+
if (known && known.folder !== plugin.folder) {
|
|
51
|
+
throw new Error(`Das Plugin ${plugin.id} liegt unter ${known.folder} und ${plugin.folder}`);
|
|
52
|
+
}
|
|
53
|
+
if (!known) plugins.push(plugin);
|
|
54
|
+
}
|
|
55
|
+
return plugins;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const isBareSpecifier = (specifier: string): boolean =>
|
|
59
|
+
!/^(\.{1,2}\/|\/|\0|[a-z][a-z0-9+.-]*:)/i.test(specifier);
|
|
60
|
+
|
|
61
|
+
const ragentsPlugins = (plugins: readonly BundledPlugin[]): Plugin => {
|
|
62
|
+
const external = plugins.filter((plugin) => !plugin.folder.startsWith(repositoryRoot));
|
|
63
|
+
const withWeb = plugins.filter((plugin) => plugin.web !== undefined);
|
|
64
|
+
return {
|
|
65
|
+
name: "ragents-plugins",
|
|
66
|
+
enforce: "pre",
|
|
67
|
+
config: () => ({
|
|
68
|
+
resolve: {
|
|
69
|
+
alias: {
|
|
70
|
+
"@aicontainer/ragents/src": path.join(repositoryRoot, "packages/ragents/src"),
|
|
71
|
+
"@aicontainer/server": path.join(repositoryRoot, "apps/server/src"),
|
|
72
|
+
"@aicontainer/web": path.join(repositoryRoot, "apps/web/src"),
|
|
73
|
+
"@aicontainer/plugins": path.join(repositoryRoot, "plugins"),
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
server: { fs: { allow: [repositoryRoot, ...external.map((plugin) => plugin.folder)] } },
|
|
77
|
+
}),
|
|
78
|
+
resolveId(source, importer) {
|
|
79
|
+
if (source === pluginEntriesModule) return `\0${pluginEntriesModule}`;
|
|
80
|
+
if (!importer || importer.startsWith(repositoryRoot) || !isBareSpecifier(source)) return null;
|
|
81
|
+
return this.resolve(source, hostEntry, { skipSelf: true });
|
|
82
|
+
},
|
|
83
|
+
load(id) {
|
|
84
|
+
if (id !== `\0${pluginEntriesModule}`) return null;
|
|
85
|
+
const entries = withWeb.map((plugin) => ` ${JSON.stringify(plugin.id)}: () => import(${JSON.stringify(plugin.web)}),`);
|
|
86
|
+
return `export const webPluginEntries = {\n${entries.join("\n")}\n};\n`;
|
|
87
|
+
},
|
|
88
|
+
transform(code, id) {
|
|
89
|
+
if (!id.endsWith("/apps/web/src/ui/tailwind.css") || external.length === 0) return null;
|
|
90
|
+
const sources = external.flatMap((plugin) => [
|
|
91
|
+
`@source ${JSON.stringify(path.join(plugin.folder, "web/**/*"))};`,
|
|
92
|
+
`@source ${JSON.stringify(path.join(plugin.folder, "client-ui/**/*"))};`,
|
|
93
|
+
]);
|
|
94
|
+
return `${code}\n${sources.join("\n")}\n`;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default defineConfig({
|
|
100
|
+
plugins: [ragentsPlugins(bundledPlugins()), react(), tailwindcss(), {
|
|
101
|
+
name: "ragents-help",
|
|
102
|
+
apply: "build",
|
|
103
|
+
buildStart() {
|
|
104
|
+
const script = path.join(repositoryRoot, "build/homepage.sh");
|
|
105
|
+
if (statSync(script, { throwIfNoEntry: false })?.isFile()) execFileSync("bash", [script], { cwd: repositoryRoot, stdio: "inherit" });
|
|
106
|
+
},
|
|
107
|
+
async generateBundle() {
|
|
108
|
+
const directory = path.join(repositoryRoot, "docs/homepage/dist");
|
|
109
|
+
if (!statSync(directory, { throwIfNoEntry: false })?.isDirectory()) return;
|
|
110
|
+
const include = async (relative: string): Promise<void> => {
|
|
111
|
+
for (const entry of await readdir(path.join(directory, relative), { withFileTypes: true })) {
|
|
112
|
+
const name = path.posix.join(relative, entry.name);
|
|
113
|
+
if (entry.isDirectory()) await include(name);
|
|
114
|
+
else this.emitFile({ type: "asset", fileName: `help/${name}`, source: await readFile(path.join(directory, name)) });
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
await include("");
|
|
118
|
+
},
|
|
119
|
+
}],
|
|
120
|
+
build: {
|
|
121
|
+
outDir: path.join(repositoryRoot, "apps/web/dist", process.env.PRODUCT_PROFILE ?? "ohne-profil"),
|
|
122
|
+
rollupOptions: {
|
|
123
|
+
input: {
|
|
124
|
+
main: fileURLToPath(new URL("index.html", import.meta.url)),
|
|
125
|
+
column: fileURLToPath(new URL("column.html", import.meta.url)),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
server: {
|
|
130
|
+
port: 5710,
|
|
131
|
+
strictPort: true,
|
|
132
|
+
proxy: {
|
|
133
|
+
"/api": apiTarget,
|
|
134
|
+
"/chat": apiTarget,
|
|
135
|
+
"/plugin-assets": apiTarget,
|
|
136
|
+
"/ragents": apiTarget,
|
|
137
|
+
"/help": apiTarget,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schlenkr/ragents",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "RAgents-Host: Server, Engine, Plugins und Werkzeuge ohne Quell-Checkout",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -37,9 +37,11 @@
|
|
|
37
37
|
"@silvia-odwyer/photon-node": "0.3.4",
|
|
38
38
|
"@tailwindcss/node": "4.3.3",
|
|
39
39
|
"@tailwindcss/oxide": "4.3.3",
|
|
40
|
+
"@tailwindcss/vite": "4.3.3",
|
|
40
41
|
"@types/node": "24.13.3",
|
|
41
42
|
"@types/react": "19.2.18",
|
|
42
43
|
"@types/react-dom": "19.2.4",
|
|
44
|
+
"@vitejs/plugin-react": "4.7.0",
|
|
43
45
|
"@xyflow/react": "12.11.6",
|
|
44
46
|
"ai": "7.0.99",
|
|
45
47
|
"chalk": "5.6.2",
|
|
@@ -47,6 +49,7 @@
|
|
|
47
49
|
"cn": "0.3.0",
|
|
48
50
|
"cross-spawn": "7.0.6",
|
|
49
51
|
"diff": "8.0.4",
|
|
52
|
+
"dompurify": "3.4.13",
|
|
50
53
|
"elkjs": "0.12.0",
|
|
51
54
|
"esbuild": "0.28.2",
|
|
52
55
|
"glob": "13.0.6",
|
|
@@ -73,6 +76,7 @@
|
|
|
73
76
|
"typescript": "5.9.3",
|
|
74
77
|
"typescript-language-server": "6.0.0",
|
|
75
78
|
"undici": "8.5.0",
|
|
79
|
+
"vite": "6.4.3",
|
|
76
80
|
"vscode-jsonrpc": "9.0.2",
|
|
77
81
|
"yaml": "2.9.0"
|
|
78
82
|
},
|
|
@@ -1,29 +1,10 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { statSync } from "node:fs";
|
|
3
1
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
2
|
import path from "node:path";
|
|
5
|
-
import { createInterface } from "node:readline";
|
|
6
3
|
import { hostRoot } from "@aicontainer/server/host-version.js";
|
|
7
4
|
import { webEntryOf } from "@aicontainer/server/plugin-support/plugins-root.js";
|
|
5
|
+
import { buildWeb } from "@aicontainer/server/web-build.js";
|
|
8
6
|
import type { InspectedClientProfile } from "./archive.js";
|
|
9
7
|
|
|
10
|
-
const viteEntry = (root: string): string => {
|
|
11
|
-
const file = path.join(root, "apps/web/node_modules/vite/bin/vite.js");
|
|
12
|
-
if (!statSync(file, { throwIfNoEntry: false })?.isFile()) {
|
|
13
|
-
throw new Error(`Die Profilverteilung baut das Web des Client-Profils und braucht dafür Vite unter ${file}; `
|
|
14
|
-
+ "ein verteilender Server ist ein Checkout mit pnpm install");
|
|
15
|
-
}
|
|
16
|
-
return file;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const run = (file: string, args: readonly string[], cwd: string, environment: NodeJS.ProcessEnv, log: (line: string) => void): Promise<void> =>
|
|
20
|
-
new Promise((resolve, reject) => {
|
|
21
|
-
const child = spawn(file, args, { cwd, env: environment, stdio: ["ignore", "pipe", "pipe"] });
|
|
22
|
-
for (const stream of [child.stdout, child.stderr]) createInterface({ input: stream! }).on("line", log);
|
|
23
|
-
child.once("error", reject);
|
|
24
|
-
child.once("exit", (code) => code === 0 ? resolve() : reject(new Error(`Der Web-Build des Client-Profils endete mit Code ${code}`)));
|
|
25
|
-
});
|
|
26
|
-
|
|
27
8
|
/** Baut das Web für den Pluginsatz des Client-Profils, damit der Client keinen Build braucht. */
|
|
28
9
|
export const buildClientWeb = async (
|
|
29
10
|
inspected: InspectedClientProfile,
|
|
@@ -31,18 +12,13 @@ export const buildClientWeb = async (
|
|
|
31
12
|
log: (line: string) => void,
|
|
32
13
|
root = hostRoot(),
|
|
33
14
|
): Promise<void> => {
|
|
34
|
-
const entry = viteEntry(root);
|
|
35
|
-
await rm(outputDirectory, { recursive: true, force: true });
|
|
36
|
-
await mkdir(path.dirname(outputDirectory), { recursive: true });
|
|
37
15
|
const listFile = `${outputDirectory}.plugins.json`;
|
|
38
16
|
const list = inspected.plugins.map((plugin) => ({ id: plugin.id, folder: plugin.folder, web: webEntryOf(plugin.folder) }));
|
|
17
|
+
await mkdir(path.dirname(listFile), { recursive: true });
|
|
39
18
|
await writeFile(listFile, `${JSON.stringify(list, null, 2)}\n`);
|
|
40
19
|
const environment: NodeJS.ProcessEnv = { ...process.env, PLUGIN_LIST_FILE: listFile };
|
|
41
20
|
delete environment.PRODUCT_PROFILE;
|
|
42
21
|
delete environment.PRODUCT_PROFILE_FILE;
|
|
43
|
-
await
|
|
22
|
+
await buildWeb({ root, outputDirectory, environment, log });
|
|
44
23
|
await rm(listFile);
|
|
45
|
-
if (!statSync(path.join(outputDirectory, "index.html"), { throwIfNoEntry: false })?.isFile()) {
|
|
46
|
-
throw new Error(`Der Web-Build des Client-Profils hat keine index.html in ${outputDirectory} hinterlassen`);
|
|
47
|
-
}
|
|
48
24
|
};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
// Der Hook löst @aicontainer/* auch für eine Profildatei außerhalb des Hosts auf; im Paket gibt es dort kein node_modules.
|
|
2
|
+
import "../../apps/server/src/host-resolution.ts";
|
|
1
3
|
import { spawn } from "node:child_process";
|
|
2
4
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { closeSync,
|
|
5
|
+
import { closeSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
6
|
import path from "node:path";
|
|
5
|
-
import { fileURLToPath
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
6
8
|
import { coreContracts } from "../../apps/server/src/api/contracts.ts";
|
|
7
9
|
import { profileFileName } from "../../apps/server/src/config-file.ts";
|
|
8
|
-
import { isEnvironmentReference } from "../../apps/server/src/config-definition.ts";
|
|
9
|
-
import { defaultDataDirectory } from "../../apps/server/src/data-directory.ts";
|
|
10
10
|
import { hostRoot } from "../../apps/server/src/host-version.ts";
|
|
11
|
+
import { readProfileTarget, type ProfileTarget } from "../../apps/server/src/profile-target.ts";
|
|
11
12
|
import { RpcClient } from "../../apps/web/src/rpc/client.ts";
|
|
12
13
|
import { WORKSPACE_BINDING_OPTION_ID } from "../../plugins/ragents.workspace/contract.ts";
|
|
13
14
|
import { JournalReader, journalLines, readJournal, RUN_ID_PATTERN, type JournalEvent } from "./journal.ts";
|
|
@@ -33,7 +34,8 @@ export const usage = (): string => `Verwendung: ragents <befehl> [argumente]
|
|
|
33
34
|
Vorgabeprofil ist ${DEFAULT_PROFILE}. Exit-Code: 0 fertig, 2 abgebrochen, 1 fehlgeschlagen oder
|
|
34
35
|
Verbindungsproblem. Die letzte Zeile auf stdout ist "run: <id>". Die Adresse kommt aus
|
|
35
36
|
<Datenordner>/host.json, sonst aus RAGENTS_URL, sonst aus host.PORT des Profils; RAGENTS_TOKEN
|
|
36
|
-
geht als Bearer-Token mit, falls das Profil eine Anmeldung verlangt
|
|
37
|
+
geht als Bearer-Token mit, falls das Profil eine Anmeldung verlangt. Der so gestartete Host baut
|
|
38
|
+
die Oberfläche nicht - ein Agent braucht sie nicht; mit Oberfläche startet ragents start <profil>.`;
|
|
37
39
|
|
|
38
40
|
export type AgentCommand =
|
|
39
41
|
| { readonly kind: "run"; readonly profile: string; readonly folder: string; readonly text: string; readonly entry: string | undefined; readonly json: boolean }
|
|
@@ -123,14 +125,6 @@ export const parseArguments = (argv: readonly string[]): AgentCommand => {
|
|
|
123
125
|
throw new Error(command ? `Unbekannter Befehl: ${command}\n\n${usage()}` : usage());
|
|
124
126
|
};
|
|
125
127
|
|
|
126
|
-
export interface ProfileTarget {
|
|
127
|
-
readonly profile: string;
|
|
128
|
-
readonly profileFile: string;
|
|
129
|
-
readonly port: number;
|
|
130
|
-
readonly dataDirectory: string;
|
|
131
|
-
readonly baseUrl: string;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
128
|
export interface HostRecord {
|
|
135
129
|
readonly profile: string;
|
|
136
130
|
readonly url: string;
|
|
@@ -155,17 +149,8 @@ export const writeHostRecord = (dataDirectory: string, record: HostRecord): void
|
|
|
155
149
|
writeFileSync(hostRecordFile(dataDirectory), `${JSON.stringify(record, null, 2)}\n`);
|
|
156
150
|
};
|
|
157
151
|
|
|
158
|
-
export const loadProfile =
|
|
159
|
-
|
|
160
|
-
if (!existsSync(profileFile)) throw new Error(`Die Profildatei fehlt: ${profileFile}`);
|
|
161
|
-
const module = await import(pathToFileURL(profileFile).href) as { config: { host: { PORT?: number; DATA_DIR?: unknown } } };
|
|
162
|
-
const port = Number(process.env.PORT || module.config.host.PORT);
|
|
163
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`host.PORT des Profils ${profile} ist keine gültige Portnummer.`);
|
|
164
|
-
const configured = module.config.host.DATA_DIR;
|
|
165
|
-
const fromFile = isEnvironmentReference(configured) ? process.env[configured.name] : typeof configured === "string" ? configured : undefined;
|
|
166
|
-
const dataDirectory = path.resolve(process.env.DATA_DIR ?? fromFile ?? defaultDataDirectory(profile));
|
|
167
|
-
return { profile, profileFile, port, dataDirectory, baseUrl: `http://localhost:${port}` };
|
|
168
|
-
};
|
|
152
|
+
export const loadProfile = (profile: string, root = hostRoot()): Promise<ProfileTarget> =>
|
|
153
|
+
readProfileTarget(profile, path.join(root, profileFileName(profile)));
|
|
169
154
|
|
|
170
155
|
/** pnpm und der bin-Befehl führen dieses Skript mit dem Arbeitsverzeichnis apps/server aus; ein relativer Ordner gilt trotzdem ab dem Aufrufer. */
|
|
171
156
|
export const callerDirectory = (): string => process.env.RAGENTS_CWD ?? process.env.INIT_CWD ?? process.cwd();
|
|
@@ -39,12 +39,17 @@ const ROOT_FILE_SOURCES = new Set(PACKAGE_ROOT_FILES.map(([source]) => source));
|
|
|
39
39
|
|
|
40
40
|
const skippedInPackage = (relative: string): boolean => SKIPPED_IN_PACKAGE.test(relative) || ROOT_FILE_SOURCES.has(relative);
|
|
41
41
|
|
|
42
|
-
/** Was der Host zur Laufzeit lädt: Server, Engine, Plugins, Profile und die Skripte hinter den Unterbefehlen. */
|
|
42
|
+
/** Was der Host zur Laufzeit lädt: Server, Engine, Plugins, Profile, den Web-Build und die Skripte hinter den Unterbefehlen. */
|
|
43
43
|
export const packageContents = (root = repositoryRoot): readonly string[] => [
|
|
44
44
|
"apps/server/package.json",
|
|
45
|
+
"apps/server/tsconfig.json",
|
|
45
46
|
"apps/server/src",
|
|
46
47
|
"apps/server/agent-home/settings.json",
|
|
47
48
|
"apps/web/package.json",
|
|
49
|
+
"apps/web/tsconfig.json",
|
|
50
|
+
"apps/web/vite.config.ts",
|
|
51
|
+
"apps/web/index.html",
|
|
52
|
+
"apps/web/column.html",
|
|
48
53
|
"apps/web/src",
|
|
49
54
|
...readdirSync(path.join(root, "packages"), { withFileTypes: true })
|
|
50
55
|
.filter((entry) => entry.isDirectory())
|
|
@@ -58,10 +63,10 @@ export const packageContents = (root = repositoryRoot): readonly string[] => [
|
|
|
58
63
|
"scripts/workspace-client",
|
|
59
64
|
];
|
|
60
65
|
|
|
61
|
-
/** Was der Host ausführt
|
|
62
|
-
const SCANNED = ["apps/server/src", "apps/web
|
|
66
|
+
/** Was der Host ausführt, in eine Mini-App bündelt oder beim Start in sein Web baut. */
|
|
67
|
+
const SCANNED = ["apps/server/src", "apps/web", "packages", "plugins", "scripts"];
|
|
63
68
|
const PACKAGE_NAME_PATTERN = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
64
|
-
const SCAN_SKIPPED = new Set(["
|
|
69
|
+
const SCAN_SKIPPED = new Set(["node_modules", "dist"]);
|
|
65
70
|
const SCANNED_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".mjs", ".js", ".css"]);
|
|
66
71
|
const IMPORT_PATTERN = /\b(?:from|import|require(?:\.resolve)?)\s*\(?\s*["']([^"'\n]+)["']/g;
|
|
67
72
|
|
|
@@ -114,10 +119,18 @@ const packageNameOf = (specifier: string): string | undefined => {
|
|
|
114
119
|
return PACKAGE_NAME_PATTERN.test(name) ? name : undefined;
|
|
115
120
|
};
|
|
116
121
|
|
|
117
|
-
|
|
122
|
+
const declaredWebDependencies = (packageDirectory: string): readonly string[] => {
|
|
123
|
+
const file = path.join(packageDirectory, "apps/web/package.json");
|
|
124
|
+
const declared = (JSON.parse(readFileSync(file, "utf8")) as { dependencies?: Record<string, string> }).dependencies ?? {};
|
|
125
|
+
return Object.keys(declared);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** Die Pakete, die der ausgeführte Teil des Pakets wirklich importiert, plus die zur Laufzeit verlinkten Bibliotheken
|
|
129
|
+
* und die, die das Web des Hosts den web-Hälften fremder Plugins anbietet. */
|
|
118
130
|
export const requiredPackages = (packageDirectory: string): ReadonlyMap<string, string> => {
|
|
119
131
|
const sources = new Map<string, string>();
|
|
120
132
|
for (const name of runtimeLibraries) sources.set(name, "plugins/ragents.actor-programs (verlinkt zur Laufzeit)");
|
|
133
|
+
for (const name of declaredWebDependencies(packageDirectory)) sources.set(name, "apps/web/package.json (für die web-Hälften der Plugins)");
|
|
121
134
|
for (const folder of scanRoots(packageDirectory)) {
|
|
122
135
|
for (const file of scannedFiles(folder)) {
|
|
123
136
|
const content = readFileSync(file, "utf8");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { compareVersions, nextVersion, readVersion, versionLine, writeHostPackageVersion, writeVersion } from "../publish-version.ts";
|
|
4
5
|
import { buildPackage, PACKAGE_FOLDER, PACKAGE_NAME } from "./build-package.ts";
|
|
5
6
|
|
|
6
7
|
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
@@ -8,7 +9,8 @@ const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url))
|
|
|
8
9
|
const usage = `Verwendung: pnpm publish:package [--dry-run]
|
|
9
10
|
Baut das Paket ${PACKAGE_NAME} und veröffentlicht es auf npm. Der Token kommt aus der
|
|
10
11
|
Umgebungsvariable npm_key. --dry-run reicht npm den Probelauf durch und veröffentlicht nichts.
|
|
11
|
-
Die Fassung steht in der package.json der Wurzel
|
|
12
|
+
Die Fassung steht in der package.json der Wurzel; das Skript zählt vor dem Bauen die letzte Stelle
|
|
13
|
+
über die zuletzt veröffentlichte hoch, im Probelauf nur in der Ausgabe.`;
|
|
12
14
|
|
|
13
15
|
const REGISTRY_TOKEN_KEY = "npm_config_//registry.npmjs.org/:_authToken";
|
|
14
16
|
|
|
@@ -52,6 +54,10 @@ export const publishedVersions = (name: string, npm: NpmRunner): readonly string
|
|
|
52
54
|
return typeof parsed === "string" ? [parsed] : parsed;
|
|
53
55
|
};
|
|
54
56
|
|
|
57
|
+
/** Die höchste Fassung, die auf npm liegt; ein dort unbekanntes Paket hat keine. */
|
|
58
|
+
export const latestPublishedVersion = (npm: NpmRunner = runNpm, name = PACKAGE_NAME): string | undefined =>
|
|
59
|
+
[...publishedVersions(name, npm)].sort(compareVersions).at(-1);
|
|
60
|
+
|
|
55
61
|
export interface PublishPlan {
|
|
56
62
|
readonly name: string;
|
|
57
63
|
readonly version: string;
|
|
@@ -59,13 +65,13 @@ export interface PublishPlan {
|
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
/** Was vor einem Publish stimmen muss: das gebaute Manifest und eine Fassung, die es auf npm noch nicht gibt. */
|
|
62
|
-
export const publishPlan = (manifest: Record<string, unknown>, published: readonly string[]): PublishPlan => {
|
|
63
|
-
const { name
|
|
68
|
+
export const publishPlan = (manifest: Record<string, unknown>, published: readonly string[], version: string): PublishPlan => {
|
|
69
|
+
const { name } = manifest as { name?: unknown };
|
|
64
70
|
const access = (manifest.publishConfig as { access?: unknown } | undefined)?.access;
|
|
65
71
|
const hostVersion = (manifest.ragents as { hostVersion?: unknown } | undefined)?.hostVersion;
|
|
66
72
|
if (name !== PACKAGE_NAME) throw new Error(`Das gebaute Paket heißt ${String(name)}, veröffentlicht wird ${PACKAGE_NAME}`);
|
|
67
73
|
if (access !== "public") throw new Error(`${PACKAGE_NAME} hat einen Scope und braucht publishConfig.access "public", nicht ${JSON.stringify(access)}`);
|
|
68
|
-
if (
|
|
74
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9a-z.-]+)?$/.test(version)) {
|
|
69
75
|
throw new Error(`Die Fassung ${JSON.stringify(version)} ist keine Fassung; sie steht als version in der package.json der Wurzel`);
|
|
70
76
|
}
|
|
71
77
|
if (typeof hostVersion !== "string" || !/^[0-9a-f]{40}$/.test(hostVersion)) {
|
|
@@ -113,8 +119,17 @@ const main = async (): Promise<void> => {
|
|
|
113
119
|
if (unknown.length) throw new Error(`Unbekanntes Argument: ${unknown.join(" ")}\n${usage}`);
|
|
114
120
|
const token = process.env.npm_key;
|
|
115
121
|
if (!token) throw new Error(`npm_key fehlt: der npm-Token mit Schreibrecht auf ${PACKAGE_NAME}.\n${usage}`);
|
|
122
|
+
const manifestFile = path.join(repositoryRoot, "package.json");
|
|
123
|
+
const published = publishedVersions(PACKAGE_NAME, runNpm);
|
|
124
|
+
const next = nextVersion(readVersion(manifestFile), published);
|
|
125
|
+
console.log(versionLine(next));
|
|
126
|
+
const extensionManifestFile = path.join(repositoryRoot, "apps", "vscode", "package.json");
|
|
127
|
+
if (!dryRun) {
|
|
128
|
+
writeVersion(manifestFile, next.version);
|
|
129
|
+
writeHostPackageVersion(extensionManifestFile, next.version);
|
|
130
|
+
}
|
|
116
131
|
const built = await buildPackage(path.join(repositoryRoot, "dist", PACKAGE_FOLDER));
|
|
117
|
-
const plan = publishPlan(built.manifest,
|
|
132
|
+
const plan = publishPlan(built.manifest, published, next.version);
|
|
118
133
|
console.log(`== ${plan.name}@${plan.version} aus Host ${plan.hostVersion.slice(0, 12)}, ${built.fileCount} Dateien, ${(built.byteSize / 1024 / 1024).toFixed(1)} MB`);
|
|
119
134
|
publishDirectory({ directory: built.directory, version: plan.version, token, dryRun, npm: runNpm, log: (line) => console.log(line) });
|
|
120
135
|
};
|
|
@@ -20,12 +20,13 @@ export const commands = {
|
|
|
20
20
|
|
|
21
21
|
const usage = `Verwendung: ragents <befehl> [argumente]
|
|
22
22
|
|
|
23
|
-
run <ordner> "<auftrag>" Host starten, Run
|
|
23
|
+
run <ordner> "<auftrag>" Host ohne Oberfläche starten, Run anlegen und auf das Ende warten
|
|
24
24
|
send <run> "<text>" Folgeauftrag im selben Run
|
|
25
25
|
journal <run> [--tools] Verlauf eines Runs lesen
|
|
26
26
|
stop <run> | stop --host laufenden Turn abbrechen oder den gemerkten Host beenden
|
|
27
27
|
connect <server-url> Client-Profil holen und den lokalen Server damit starten
|
|
28
|
-
start <profil>
|
|
28
|
+
start <profil|pfad> ein Profil des Pakets, eine eigene Profildatei oder einen
|
|
29
|
+
geholten Stand starten; baut die Oberfläche bei Bedarf
|
|
29
30
|
provision [<profil>|--workspace] Werkzeuge der Plugins holen
|
|
30
31
|
workspace-client <server-url> diesen Rechner als Arbeitsplatz anmelden
|
|
31
32
|
`;
|
|
@@ -8,6 +8,8 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
import { extract as extractTar } from "tar";
|
|
9
9
|
import { ragentsDataRoot } from "../../apps/server/src/data-directory.ts";
|
|
10
10
|
import { hostRoot, readHostPackage, readHostVersion } from "../../apps/server/src/host-version.ts";
|
|
11
|
+
import { readProfileTarget } from "../../apps/server/src/profile-target.ts";
|
|
12
|
+
import { ensureWebBuild } from "../../apps/server/src/web-build.ts";
|
|
11
13
|
import { RpcClient } from "../../apps/web/src/rpc/client.ts";
|
|
12
14
|
import { profileDistributionContracts, type ClientProfileDescription } from "../../plugins/ragents.profile-distribution/contract.ts";
|
|
13
15
|
|
|
@@ -226,24 +228,33 @@ export interface StartedProfile {
|
|
|
226
228
|
readonly profileFile: string;
|
|
227
229
|
/** Nur bei einem geholten Stand gesetzt; ein Profil des Hosts nimmt die Vorgaben seiner eigenen Konfiguration. */
|
|
228
230
|
readonly dataDirectory?: string;
|
|
231
|
+
/** Nur bei einem geholten Stand gesetzt; sonst baut der Start das Web des Profils selbst. */
|
|
229
232
|
readonly webDirectory?: string;
|
|
230
233
|
readonly port: number | undefined;
|
|
231
234
|
}
|
|
232
235
|
|
|
233
|
-
/** Provisioniert die Werkzeuge des Profils und startet den Server damit. */
|
|
236
|
+
/** Provisioniert die Werkzeuge des Profils, baut sein Web falls nötig und startet den Server damit. */
|
|
234
237
|
export const startProfile = async (started: StartedProfile): Promise<number> => {
|
|
238
|
+
const dataDirectory = started.dataDirectory ?? (await readProfileTarget(started.profile, started.profileFile)).dataDirectory;
|
|
235
239
|
const env: NodeJS.ProcessEnv = {
|
|
236
240
|
...process.env,
|
|
237
241
|
PRODUCT_PROFILE: started.profile,
|
|
238
242
|
PRODUCT_PROFILE_FILE: started.profileFile,
|
|
239
|
-
|
|
240
|
-
...(started.webDirectory ? { WEB_DIST_DIR: started.webDirectory } : {}),
|
|
243
|
+
DATA_DIR: dataDirectory,
|
|
241
244
|
};
|
|
242
245
|
console.log("== Werkzeuge provisionieren");
|
|
243
246
|
const provisioned = await run(hostScript("scripts/provision/run-provision.ts"), env);
|
|
244
247
|
if (provisioned !== 0) throw new Error(`Die Provisionierung endete mit Code ${provisioned}`);
|
|
245
|
-
|
|
246
|
-
|
|
248
|
+
const webDirectory = started.webDirectory ?? await ensureWebBuild({
|
|
249
|
+
root: hostRoot(),
|
|
250
|
+
profile: started.profile,
|
|
251
|
+
profileFile: started.profileFile,
|
|
252
|
+
dataDirectory,
|
|
253
|
+
environment: env,
|
|
254
|
+
log: (line) => console.log(line),
|
|
255
|
+
});
|
|
256
|
+
console.log(`== Server starten (Web ${webDirectory}, Daten ${dataDirectory})`);
|
|
257
|
+
return run(hostScript("apps/server/src/main.ts", started.port !== undefined ? ["--port", String(started.port)] : []), { ...env, WEB_DIST_DIR: webDirectory });
|
|
247
258
|
};
|
|
248
259
|
|
|
249
260
|
/** Startet einen Stand, den connect geholt hat, mit dem Web aus dem Cache. */
|
package/scripts/remote/start.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Der Hook löst @aicontainer/* auch für eine Profildatei außerhalb des Hosts auf; im Paket gibt es dort kein node_modules.
|
|
2
|
+
import "../../apps/server/src/host-resolution.ts";
|
|
1
3
|
import { statSync } from "node:fs";
|
|
2
4
|
import { readFile, readdir } from "node:fs/promises";
|
|
3
5
|
import path from "node:path";
|
|
@@ -5,14 +7,16 @@ import { fileURLToPath } from "node:url";
|
|
|
5
7
|
import { profileFileName } from "../../apps/server/src/config-file.ts";
|
|
6
8
|
import { ragentsDataRoot } from "../../apps/server/src/data-directory.ts";
|
|
7
9
|
import { hostRoot } from "../../apps/server/src/host-version.ts";
|
|
10
|
+
import { callerDirectory } from "../agent/agent-cli.ts";
|
|
8
11
|
import { startCached, startProfile, type CachedProfile } from "./connect.ts";
|
|
9
12
|
|
|
10
|
-
const usage = (): string => `Verwendung: ragents start <profil> [--port <n>]
|
|
11
|
-
Startet ein Profil dieses Hosts (core, developer)
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
const usage = (): string => `Verwendung: ragents start <profil|pfad> [--port <n>]
|
|
14
|
+
Startet ein Profil dieses Hosts (core, developer), eine eigene ragents.config.<profil>.ts an
|
|
15
|
+
beliebiger Stelle oder einen Stand, den ragents connect schon geholt hat, ohne den Server zu
|
|
16
|
+
fragen. Ein eigenes Profil bekommt sein Web beim Start gebaut, ein geholter Stand bringt es aus
|
|
17
|
+
demselben Cache mit. Ohne Argument nennt der Befehl die Profile im Cache.`;
|
|
14
18
|
|
|
15
|
-
export const parseArguments = (argv: readonly string[]): {
|
|
19
|
+
export const parseArguments = (argv: readonly string[]): { selection: string; port: number | undefined } => {
|
|
16
20
|
let profile: string | undefined;
|
|
17
21
|
let port: number | undefined;
|
|
18
22
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -28,7 +32,7 @@ export const parseArguments = (argv: readonly string[]): { profile: string; port
|
|
|
28
32
|
profile = argument;
|
|
29
33
|
}
|
|
30
34
|
if (!profile) throw new Error(`Der Profilname fehlt.\n${usage()}`);
|
|
31
|
-
return { profile, port };
|
|
35
|
+
return { selection: profile, port };
|
|
32
36
|
};
|
|
33
37
|
|
|
34
38
|
const directories = async (folder: string): Promise<readonly string[]> =>
|
|
@@ -60,22 +64,36 @@ export const selectCachedProfile = (cached: readonly CachedProfile[], profile: s
|
|
|
60
64
|
+ "nimm ragents connect <server-url>.");
|
|
61
65
|
};
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
const PROFILE_FILE = /^ragents\.config\.([a-z0-9]+(?:-[a-z0-9]+)*)\.ts$/;
|
|
68
|
+
|
|
69
|
+
export interface LocalProfile {
|
|
70
|
+
readonly profile: string;
|
|
71
|
+
readonly profileFile: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Ein Profil ist ein Name neben dem Host (Checkout wie Paket) oder ein Pfad zu einer ragents.config.<profil>.ts an beliebiger Stelle. */
|
|
75
|
+
export const localProfile = (selection: string, root = hostRoot(), caller = callerDirectory()): LocalProfile | undefined => {
|
|
76
|
+
if (!selection.includes("/") && !selection.endsWith(".ts")) {
|
|
77
|
+
const file = path.join(root, profileFileName(selection));
|
|
78
|
+
return statSync(file, { throwIfNoEntry: false })?.isFile() ? { profile: selection, profileFile: file } : undefined;
|
|
79
|
+
}
|
|
80
|
+
const profileFile = path.resolve(caller, selection);
|
|
81
|
+
const match = PROFILE_FILE.exec(path.basename(profileFile));
|
|
82
|
+
if (!match) throw new Error(`Eine Profildatei heißt ragents.config.<profil>.ts, nicht ${path.basename(profileFile)}`);
|
|
83
|
+
if (!statSync(profileFile, { throwIfNoEntry: false })?.isFile()) throw new Error(`Die Profildatei fehlt: ${profileFile}`);
|
|
84
|
+
return { profile: match[1]!, profileFile };
|
|
67
85
|
};
|
|
68
86
|
|
|
69
87
|
const main = async (): Promise<void> => {
|
|
70
88
|
const arguments_ = parseArguments(process.argv.slice(2));
|
|
71
|
-
const own =
|
|
89
|
+
const own = localProfile(arguments_.selection);
|
|
72
90
|
if (own) {
|
|
73
|
-
console.log(`== Profil ${
|
|
74
|
-
console.log(`== Profildatei ${own}`);
|
|
75
|
-
process.exitCode = await startProfile({ profile:
|
|
91
|
+
console.log(`== Profil ${own.profile}`);
|
|
92
|
+
console.log(`== Profildatei ${own.profileFile}`);
|
|
93
|
+
process.exitCode = await startProfile({ profile: own.profile, profileFile: own.profileFile, port: arguments_.port });
|
|
76
94
|
return;
|
|
77
95
|
}
|
|
78
|
-
const cached = selectCachedProfile(await cachedProfiles(ragentsDataRoot()), arguments_.
|
|
96
|
+
const cached = selectCachedProfile(await cachedProfiles(ragentsDataRoot()), arguments_.selection);
|
|
79
97
|
console.log(`== Profil ${cached.profile} vom Server ${cached.serverUrl}, Stand ${cached.version.slice(0, 12)} (aus dem Cache)`);
|
|
80
98
|
console.log(`== Profildatei ${cached.profileFile}`);
|
|
81
99
|
console.log(`== Daten ${cached.dataDirectory}`);
|