agentroam 0.2.0-preview.16 → 0.2.0-preview.17
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 +16 -15
- package/bin/node-preflight.mjs +15 -22
- package/bin/node-runtime-policy.json +4 -0
- package/bin/runtime-policy.d.mts +5 -0
- package/bin/runtime-policy.mjs +37 -0
- package/dist/cli.js +7 -4
- package/dist/install-script-contract.test.js +31 -6
- package/dist/native-runtime.js +46 -0
- package/dist/native-runtime.test.js +23 -2
- package/dist/node-preflight.test.js +21 -15
- package/dist/opencode-runtime-manager.js +15 -3
- package/dist/opencode-runtime-manager.test.js +33 -1
- package/dist/platform-packages.js +7 -4
- package/dist/platform-packages.test.js +15 -4
- package/dist/platform.js +2 -4
- package/dist/platform.test.js +3 -3
- package/dist/service/service-command.js +2 -3
- package/dist/service/service-command.test.js +20 -0
- package/dist/tunnel/public-readiness.js +1 -1
- package/install/README.md +4 -4
- package/install/install-agentroam.ps1 +14 -8
- package/install/install-agentroam.sh +25 -9
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Zero-install mobile remote terminal and file console. **No desktop app, no VPN**
|
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
|
-
- **Node.js 22** (
|
|
7
|
+
- **Node.js >=22.22.0** (no upper version bound)
|
|
8
8
|
- macOS arm64 or Windows 10/11 x64
|
|
9
9
|
|
|
10
10
|
The release is assembled on macOS arm64. Windows does not need Bun, Python,
|
|
@@ -20,20 +20,20 @@ npx agentroam@preview
|
|
|
20
20
|
Or install from tarball:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
npm install ./agentroam-cloudflared-darwin-arm64-0.2.0-preview.
|
|
24
|
-
./agentroam-runtime-darwin-arm64-0.2.0-preview.
|
|
25
|
-
./agentroam-tui-darwin-arm64-0.2.0-preview.
|
|
26
|
-
./agentroam-0.2.0-preview.
|
|
23
|
+
npm install ./agentroam-cloudflared-darwin-arm64-0.2.0-preview.17.tgz \
|
|
24
|
+
./agentroam-runtime-darwin-arm64-0.2.0-preview.17.tgz \
|
|
25
|
+
./agentroam-tui-darwin-arm64-0.2.0-preview.17.tgz \
|
|
26
|
+
./agentroam-0.2.0-preview.17.tgz
|
|
27
27
|
npx agentroam
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
Windows PowerShell:
|
|
31
31
|
|
|
32
32
|
```powershell
|
|
33
|
-
npm install ./agentroam-cloudflared-win32-x64-0.2.0-preview.
|
|
34
|
-
./agentroam-runtime-win32-x64-0.2.0-preview.
|
|
35
|
-
./caoqu-agentroam-tui-win32-x64-0.2.0-preview.
|
|
36
|
-
./agentroam-0.2.0-preview.
|
|
33
|
+
npm install ./agentroam-cloudflared-win32-x64-0.2.0-preview.17.tgz `
|
|
34
|
+
./agentroam-runtime-win32-x64-0.2.0-preview.17.tgz `
|
|
35
|
+
./caoqu-agentroam-tui-win32-x64-0.2.0-preview.17.tgz `
|
|
36
|
+
./agentroam-0.2.0-preview.17.tgz
|
|
37
37
|
npx agentroam
|
|
38
38
|
```
|
|
39
39
|
|
|
@@ -57,16 +57,17 @@ agent-tui # start the bundled terminal Agent UI
|
|
|
57
57
|
|
|
58
58
|
Run the versioned Shell installer on macOS or PowerShell installer on Windows
|
|
59
59
|
from the project directory AgentRoam should expose. The installer verifies or
|
|
60
|
-
installs Node
|
|
60
|
+
installs a compatible Node and the matching CLI, registers the current-user service, starts
|
|
61
61
|
it, checks readiness, and prints the access URL. Set `AGENTROAM_ROOT` to select a
|
|
62
62
|
different root explicitly. An implicit root equal to the home directory is
|
|
63
63
|
rejected.
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
nvm-
|
|
67
|
-
|
|
65
|
+
The installer reuses the active Node when it meets the minimum version. Otherwise
|
|
66
|
+
it checks nvm-sh on macOS and nvm-windows on Windows and uses the highest
|
|
67
|
+
compatible installed version. The service records that executable without
|
|
68
68
|
changing the user's global or active NVM version. A private Node 22.22.0 is
|
|
69
|
-
downloaded only when no installed Node
|
|
69
|
+
downloaded only when no compatible installed Node is available. Higher Node
|
|
70
|
+
versions are never rejected or downgraded solely because of their major version.
|
|
70
71
|
|
|
71
72
|
The service keeps running after the terminal closes, restarts after failure, and
|
|
72
73
|
starts again when the user logs in. macOS uses a user LaunchAgent; Windows uses a
|
|
@@ -117,7 +118,7 @@ The platform cloudflared binary is installed from npm after size and SHA-256 ver
|
|
|
117
118
|
- Next.js **standalone** web gateway + WebSocket PTY server
|
|
118
119
|
- `@agent/core` bundled runtime
|
|
119
120
|
- Optional platform **cloudflared** package (verified size + SHA-256 before execution)
|
|
120
|
-
- Optional platform **agent-tui** package (Node 22 bundle; Bun is not required)
|
|
121
|
+
- Optional platform **agent-tui** package (Node >=22.22.0 bundle; Bun is not required)
|
|
121
122
|
- **Pairing token** + secure cookies behind HTTPS tunnel
|
|
122
123
|
|
|
123
124
|
Not included: Electron desktop app, SDK, dev `.env`, session caches.
|
package/bin/node-preflight.mjs
CHANGED
|
@@ -4,10 +4,10 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { delimiter, dirname, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
+
import { MINIMUM_NODE_VERSION, compareNodeVersions, isSupportedNodeVersion, parseNodeVersion } from "./runtime-policy.mjs";
|
|
7
8
|
|
|
8
9
|
const execFileAsync = promisify(execFile);
|
|
9
|
-
const
|
|
10
|
-
const RELEASE_VERSION = "0.2.0-preview.16";
|
|
10
|
+
const RELEASE_VERSION = "0.2.0-preview.17";
|
|
11
11
|
const RELEASE_BASE_URL = `https://gitee.com/caoqu/team-agent/releases/download/v${RELEASE_VERSION}`;
|
|
12
12
|
const FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
13
13
|
|
|
@@ -21,10 +21,10 @@ export async function runNodePreflight(options = {}) {
|
|
|
21
21
|
const processHost = options.processHost ?? process;
|
|
22
22
|
const major = Number(nodeVersion.split(".")[0]);
|
|
23
23
|
|
|
24
|
-
if (
|
|
24
|
+
if (isSupportedNodeVersion(nodeVersion)) return { handled: false };
|
|
25
25
|
if (environment.AGENTROAM_MANAGED_NODE) {
|
|
26
26
|
throw cliError(
|
|
27
|
-
`managed Node.js re-entry failed: expected
|
|
27
|
+
`managed Node.js re-entry failed: expected >=${MINIMUM_NODE_VERSION}, got ${nodeVersion} (${environment.AGENTROAM_MANAGED_NODE})`,
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
30
|
if (!Number.isFinite(major) || major < 18) {
|
|
@@ -33,8 +33,8 @@ export async function runNodePreflight(options = {}) {
|
|
|
33
33
|
|
|
34
34
|
const target = detectManagedTarget(platform, arch);
|
|
35
35
|
const dataDir = parsePreflightDataDir(argv, environment, options.homeDir ?? homedir());
|
|
36
|
-
const
|
|
37
|
-
const systemNode = await
|
|
36
|
+
const findSystemNode = options.findSystemNode ?? defaultFindSystemNode;
|
|
37
|
+
const systemNode = await findSystemNode(environment, platform, options.currentExecutable ?? process.execPath);
|
|
38
38
|
let executable = systemNode;
|
|
39
39
|
|
|
40
40
|
if (!executable) {
|
|
@@ -55,7 +55,7 @@ export async function runNodePreflight(options = {}) {
|
|
|
55
55
|
const spawnChild = options.spawnChild ?? defaultSpawnChild;
|
|
56
56
|
const child = spawnChild(executable, [launcherPath, ...argv], {
|
|
57
57
|
stdio: "inherit",
|
|
58
|
-
env: { ...environment, AGENTROAM_MANAGED_NODE:
|
|
58
|
+
env: { ...environment, AGENTROAM_MANAGED_NODE: executable },
|
|
59
59
|
});
|
|
60
60
|
const result = await waitForChild(child, processHost);
|
|
61
61
|
return { handled: true, ...result };
|
|
@@ -77,7 +77,7 @@ function detectManagedTarget(platform, arch) {
|
|
|
77
77
|
throw cliError(`managed Node.js is unsupported on ${platform}-${arch}`);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
async function
|
|
80
|
+
async function defaultFindSystemNode(environment, platform, currentExecutable) {
|
|
81
81
|
const executableName = platform === "win32" ? "node.exe" : "node";
|
|
82
82
|
const pathDelimiter = platform === "win32" ? ";" : delimiter;
|
|
83
83
|
const currentDirectory = dirname(currentExecutable);
|
|
@@ -85,12 +85,12 @@ async function defaultFindSystemNode22(environment, platform, currentExecutable)
|
|
|
85
85
|
if (resolve(directory) === resolve(currentDirectory)) continue;
|
|
86
86
|
const candidate = resolve(directory, executableName);
|
|
87
87
|
const version = await inspectNodeVersion(candidate);
|
|
88
|
-
if (version
|
|
88
|
+
if (isSupportedNodeVersion(version)) return candidate;
|
|
89
89
|
}
|
|
90
|
-
return
|
|
90
|
+
return findNvmNode(environment, platform);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export async function
|
|
93
|
+
export async function findNvmNode(environment = process.env, platform = process.platform) {
|
|
94
94
|
const windows = platform === "win32";
|
|
95
95
|
const homeDir = environment.HOME || environment.USERPROFILE || homedir();
|
|
96
96
|
const roots = windows
|
|
@@ -109,10 +109,10 @@ export async function findNvmNode22(environment = process.env, platform = proces
|
|
|
109
109
|
if (!entry.isDirectory()) continue;
|
|
110
110
|
const candidate = resolve(versionsRoot, entry.name, windows ? "node.exe" : "bin/node");
|
|
111
111
|
const version = await inspectNodeVersion(candidate);
|
|
112
|
-
if (version
|
|
112
|
+
if (isSupportedNodeVersion(version)) candidates.push({ candidate, version });
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
-
candidates.sort((left, right) =>
|
|
115
|
+
candidates.sort((left, right) => compareNodeVersions(right.version, left.version));
|
|
116
116
|
return candidates[0]?.candidate ?? null;
|
|
117
117
|
}
|
|
118
118
|
|
|
@@ -124,20 +124,13 @@ async function inspectNodeVersion(candidate) {
|
|
|
124
124
|
timeout: 5_000,
|
|
125
125
|
windowsHide: true,
|
|
126
126
|
});
|
|
127
|
-
const
|
|
128
|
-
return
|
|
127
|
+
const version = stdout.trim();
|
|
128
|
+
return parseNodeVersion(version) ? version : null;
|
|
129
129
|
} catch {
|
|
130
130
|
return null;
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
function compareVersions(left, right) {
|
|
135
|
-
for (let index = 0; index < 3; index++) {
|
|
136
|
-
if (left[index] !== right[index]) return left[index] - right[index];
|
|
137
|
-
}
|
|
138
|
-
return 0;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
134
|
async function defaultSpawnChild(executable, args, options) {
|
|
142
135
|
const { spawn } = await import("node:child_process");
|
|
143
136
|
return spawn(executable, args, options);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const MINIMUM_NODE_VERSION: string;
|
|
2
|
+
export function parseNodeVersion(version: unknown): number[] | null;
|
|
3
|
+
export function compareNodeVersions(left: string, right: string): number;
|
|
4
|
+
export function isSupportedNodeVersion(version: unknown, minimum?: string): boolean;
|
|
5
|
+
export function assertSupportedNodeVersion(version?: string): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const policy = JSON.parse(readFileSync(new URL("./node-runtime-policy.json", import.meta.url), "utf8"));
|
|
4
|
+
|
|
5
|
+
export const MINIMUM_NODE_VERSION = policy.minimumNodeVersion;
|
|
6
|
+
|
|
7
|
+
export function parseNodeVersion(version) {
|
|
8
|
+
if (typeof version !== "string") return null;
|
|
9
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version);
|
|
10
|
+
if (!match) return null;
|
|
11
|
+
const parts = match.slice(1, 4).map(Number);
|
|
12
|
+
return parts.every(Number.isSafeInteger) ? parts : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function compareNodeVersions(left, right) {
|
|
16
|
+
const leftParts = parseNodeVersion(left);
|
|
17
|
+
const rightParts = parseNodeVersion(right);
|
|
18
|
+
if (!leftParts || !rightParts) throw new Error("invalid Node.js version");
|
|
19
|
+
for (let index = 0; index < 3; index++) {
|
|
20
|
+
if (leftParts[index] !== rightParts[index]) return leftParts[index] - rightParts[index];
|
|
21
|
+
}
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isSupportedNodeVersion(version, minimum = MINIMUM_NODE_VERSION) {
|
|
26
|
+
return parseNodeVersion(version) !== null && compareNodeVersions(version, minimum) >= 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function assertSupportedNodeVersion(version = process.versions.node) {
|
|
30
|
+
if (!isSupportedNodeVersion(version)) {
|
|
31
|
+
throw Object.assign(new Error(`Node.js >=${MINIMUM_NODE_VERSION} required (current ${version})`), { exitCode: 2 });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (policy.schemaVersion !== 1 || !parseNodeVersion(MINIMUM_NODE_VERSION)) {
|
|
36
|
+
throw new Error("invalid AgentRoam Node.js runtime policy");
|
|
37
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { parseArgs } from "./args.js";
|
|
4
4
|
import { ensureCloudflared } from "./cloudflared/installer.js";
|
|
5
5
|
import { resolveCodexRuntime } from "./codex-runtime-manager.js";
|
|
6
|
-
import { repairNativeRuntimePermissions } from "./native-runtime.js";
|
|
6
|
+
import { probeSQLiteRuntime, probeTerminalRuntime, repairNativeRuntimePermissions } from "./native-runtime.js";
|
|
7
7
|
import { findLanUrl } from "./network.js";
|
|
8
8
|
import { createPairingSecret } from "./pairing.js";
|
|
9
9
|
import { detectPlatform } from "./platform.js";
|
|
@@ -138,7 +138,7 @@ export async function main(argv) {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
async function doctor(target, dataDir) {
|
|
141
|
-
console.log(`✓ Node ${process.versions.node}`);
|
|
141
|
+
console.log(`✓ Node ${process.versions.node} · Node-API ${process.versions.napi}`);
|
|
142
142
|
console.log(`✓ Platform ${target}`);
|
|
143
143
|
const { runtimeRoot } = resolvePlatformRuntime(target);
|
|
144
144
|
const runtimeRequire = createRequire(`${runtimeRoot}/package.json`);
|
|
@@ -149,9 +149,12 @@ async function doctor(target, dataDir) {
|
|
|
149
149
|
catch (error) {
|
|
150
150
|
reportDoctorFailure(error);
|
|
151
151
|
}
|
|
152
|
-
for (const module of [
|
|
152
|
+
for (const [module, probe] of [
|
|
153
|
+
["node-pty", probeTerminalRuntime],
|
|
154
|
+
["better-sqlite3", probeSQLiteRuntime],
|
|
155
|
+
]) {
|
|
153
156
|
try {
|
|
154
|
-
runtimeRequire
|
|
157
|
+
await probe(runtimeRequire);
|
|
155
158
|
console.log(`✓ ${module}`);
|
|
156
159
|
}
|
|
157
160
|
catch (error) {
|
|
@@ -4,12 +4,35 @@ import { execFileSync } from "node:child_process";
|
|
|
4
4
|
import { describe, expect, it } from "vitest";
|
|
5
5
|
import { MANAGED_NODE_VERSION, NODE_RUNTIME_ASSETS } from "./node-runtime-manager.js";
|
|
6
6
|
import { AGENTROAM_VERSION } from "./platform-packages.js";
|
|
7
|
+
import { MINIMUM_NODE_VERSION, isSupportedNodeVersion } from "../bin/runtime-policy.mjs";
|
|
7
8
|
const installRoot = resolve(import.meta.dirname, "../install");
|
|
8
9
|
describe("standalone installer contracts", () => {
|
|
10
|
+
it.skipIf(process.platform !== "darwin" || process.arch !== "arm64")("keeps a compatible active Node instead of selecting a different NVM version", async () => {
|
|
11
|
+
const root = await mkdtemp(resolve(process.env.TMPDIR || "/tmp", "agentroam-active-node-"));
|
|
12
|
+
try {
|
|
13
|
+
const homeDir = resolve(root, "home");
|
|
14
|
+
const workspace = resolve(root, "workspace");
|
|
15
|
+
const activeBin = resolve(root, "active bin");
|
|
16
|
+
const nvmRoot = resolve(root, "nvm");
|
|
17
|
+
await Promise.all([mkdir(homeDir), mkdir(workspace), mkdir(activeBin), mkdir(nvmRoot)]);
|
|
18
|
+
const expected = await fakeNode(resolve(activeBin, "node"), "24.13.0");
|
|
19
|
+
await fakeNode(resolve(nvmRoot, "versions/node/v26.0.0/bin/node"), "26.0.0");
|
|
20
|
+
const output = execFileSync("/bin/sh", [resolve(installRoot, "install-agentroam.sh")], {
|
|
21
|
+
cwd: workspace, encoding: "utf8",
|
|
22
|
+
env: { ...process.env, HOME: homeDir, NVM_DIR: nvmRoot, PATH: `${activeBin}:/usr/bin:/bin`,
|
|
23
|
+
AGENTROAM_BOOTSTRAP_TEST: "1", AGENTROAM_BOOTSTRAP_NODE_DISCOVERY_ONLY: "1" },
|
|
24
|
+
});
|
|
25
|
+
expect(output.trim()).toBe(expected);
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
await rm(root, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
9
31
|
it("keeps the macOS installer aligned with the runtime manifest", async () => {
|
|
10
32
|
const scriptPath = resolve(installRoot, "install-agentroam.sh");
|
|
11
33
|
const script = await readFile(scriptPath, "utf8");
|
|
12
34
|
expect(script).toContain(`NODE_VERSION="${MANAGED_NODE_VERSION}"`);
|
|
35
|
+
expect(script).toContain(`MINIMUM_NODE_VERSION="${MINIMUM_NODE_VERSION}"`);
|
|
13
36
|
expect(script).toContain(`AGENTROAM_VERSION="${AGENTROAM_VERSION}"`);
|
|
14
37
|
expect(script).toContain(NODE_RUNTIME_ASSETS["darwin-arm64"].archive);
|
|
15
38
|
expect(script).toContain(NODE_RUNTIME_ASSETS["darwin-arm64"].sha256);
|
|
@@ -20,7 +43,7 @@ describe("standalone installer contracts", () => {
|
|
|
20
43
|
expect(script).toContain('AGENTROAM_INSTALL_SKIP_SERVICE');
|
|
21
44
|
expect(script).toContain('service install --root "$SERVICE_ROOT" --data-dir "$DATA_DIR"');
|
|
22
45
|
expect(script).toContain("refusing to expose the entire home directory implicitly");
|
|
23
|
-
expect(script).toContain("
|
|
46
|
+
expect(script).toContain("find_nvm_node");
|
|
24
47
|
expect(script).toContain('${NVM_DIR:-}');
|
|
25
48
|
expect(script).not.toMatch(/\b(?:brew|sudo|fnm|volta)\b/);
|
|
26
49
|
execFileSync("sh", ["-n", scriptPath]);
|
|
@@ -28,6 +51,7 @@ describe("standalone installer contracts", () => {
|
|
|
28
51
|
it("keeps the Windows installer aligned with the runtime manifest", async () => {
|
|
29
52
|
const script = await readFile(resolve(installRoot, "install-agentroam.ps1"), "utf8");
|
|
30
53
|
expect(script).toContain(`$NodeVersion = "${MANAGED_NODE_VERSION}"`);
|
|
54
|
+
expect(script).toContain(`$MinimumNodeVersion = "${MINIMUM_NODE_VERSION}"`);
|
|
31
55
|
expect(script).toContain(`$AgentRoamVersion = "${AGENTROAM_VERSION}"`);
|
|
32
56
|
expect(script).toContain(NODE_RUNTIME_ASSETS["windows-amd64"].archive);
|
|
33
57
|
expect(script).toContain(NODE_RUNTIME_ASSETS["windows-amd64"].sha256);
|
|
@@ -38,11 +62,11 @@ describe("standalone installer contracts", () => {
|
|
|
38
62
|
expect(script).toContain("$env:AGENTROAM_INSTALL_SKIP_SERVICE");
|
|
39
63
|
expect(script).toContain("service install --root $ServiceRoot --data-dir $DataDir");
|
|
40
64
|
expect(script).toContain("Refusing to expose the entire home directory implicitly");
|
|
41
|
-
expect(script).toContain("Find-
|
|
65
|
+
expect(script).toContain("Find-NvmNode");
|
|
42
66
|
expect(script).toContain("$env:NVM_HOME");
|
|
43
67
|
expect(script).not.toMatch(/\b(?:winget|choco|scoop|Start-Process\s+.*RunAs)\b/i);
|
|
44
68
|
});
|
|
45
|
-
it.skipIf(process.platform !== "darwin" || process.arch !== "arm64")("selects the highest
|
|
69
|
+
it.skipIf(process.platform !== "darwin" || process.arch !== "arm64")("selects the highest compatible NVM Node without changing the active version", async () => {
|
|
46
70
|
const root = await mkdtemp(resolve(process.env.TMPDIR || "/tmp", "agentroam-nvm-discovery-"));
|
|
47
71
|
try {
|
|
48
72
|
const home = resolve(root, "home");
|
|
@@ -52,7 +76,8 @@ describe("standalone installer contracts", () => {
|
|
|
52
76
|
await Promise.all([mkdir(home), mkdir(workspace), mkdir(activeBin), mkdir(nvmRoot)]);
|
|
53
77
|
await fakeNode(resolve(activeBin, "node"), "20.19.0");
|
|
54
78
|
await fakeNode(resolve(nvmRoot, "versions/node/v22.3.0/bin/node"), "22.3.0");
|
|
55
|
-
|
|
79
|
+
await fakeNode(resolve(nvmRoot, "versions/node/v22.22.2/bin/node"), "22.22.2");
|
|
80
|
+
const expected = await fakeNode(resolve(nvmRoot, "versions/node/v25.8.0/bin/node"), "25.8.0");
|
|
56
81
|
await fakeNode(resolve(nvmRoot, "versions/node/v22.99.0/bin/node"), "21.99.0");
|
|
57
82
|
const output = execFileSync("/bin/sh", [resolve(installRoot, "install-agentroam.sh")], {
|
|
58
83
|
cwd: workspace,
|
|
@@ -67,13 +92,13 @@ describe("standalone installer contracts", () => {
|
|
|
67
92
|
},
|
|
68
93
|
});
|
|
69
94
|
expect(output.trim().split("\n").at(-1)).toBe(expected);
|
|
70
|
-
expect(output).toContain("Using Node.js
|
|
95
|
+
expect(output).toContain("Using Node.js v25.8.0 from NVM");
|
|
71
96
|
}
|
|
72
97
|
finally {
|
|
73
98
|
await rm(root, { recursive: true, force: true });
|
|
74
99
|
}
|
|
75
100
|
});
|
|
76
|
-
it.skipIf(process.platform !== "win32" ||
|
|
101
|
+
it.skipIf(process.platform !== "win32" || !isSupportedNodeVersion(process.versions.node))("selects a compatible nvm-windows Node when the active PATH has no Node", async () => {
|
|
77
102
|
const root = await mkdtemp(resolve(process.env.TEMP || process.env.TMP || "C:\\Windows\\Temp", "agentroam-nvm-windows-"));
|
|
78
103
|
try {
|
|
79
104
|
const home = resolve(root, "home");
|
package/dist/native-runtime.js
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
import { chmod } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
+
export function probeSQLiteRuntime(runtimeRequire) {
|
|
4
|
+
const Database = runtimeRequire("better-sqlite3");
|
|
5
|
+
const database = new Database(":memory:");
|
|
6
|
+
try {
|
|
7
|
+
database.exec("CREATE TABLE runtime_probe (value INTEGER NOT NULL)");
|
|
8
|
+
database.prepare("INSERT INTO runtime_probe VALUES (?)").run(7);
|
|
9
|
+
if (database.prepare("SELECT value FROM runtime_probe").get()?.value !== 7) {
|
|
10
|
+
throw new Error("SQLite runtime query failed");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
database.close();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function probeTerminalRuntime(runtimeRequire, platform = process.platform) {
|
|
18
|
+
const pty = runtimeRequire("node-pty");
|
|
19
|
+
const windows = platform === "win32";
|
|
20
|
+
const marker = "agentroam-runtime-probe";
|
|
21
|
+
const terminal = pty.spawn(windows ? (process.env.ComSpec || "cmd.exe") : "/bin/sh", windows
|
|
22
|
+
? ["/d", "/s", "/c", `echo ${marker}`]
|
|
23
|
+
: ["-c", `printf ${marker}`], {
|
|
24
|
+
name: "xterm-256color", cols: 80, rows: 24, cwd: process.cwd(), env: process.env,
|
|
25
|
+
});
|
|
26
|
+
await new Promise((resolveProbe, rejectProbe) => {
|
|
27
|
+
let output = "";
|
|
28
|
+
const timer = setTimeout(() => {
|
|
29
|
+
dataListener.dispose();
|
|
30
|
+
exitListener.dispose();
|
|
31
|
+
try {
|
|
32
|
+
terminal.kill();
|
|
33
|
+
}
|
|
34
|
+
catch { }
|
|
35
|
+
rejectProbe(new Error("PTY runtime probe timed out"));
|
|
36
|
+
}, 5_000);
|
|
37
|
+
const dataListener = terminal.onData((chunk) => { output = (output + chunk).slice(-4096); });
|
|
38
|
+
const exitListener = terminal.onExit(({ exitCode }) => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
dataListener.dispose();
|
|
41
|
+
exitListener.dispose();
|
|
42
|
+
if (exitCode === 0 && output.includes(marker))
|
|
43
|
+
resolveProbe();
|
|
44
|
+
else
|
|
45
|
+
rejectProbe(new Error(`PTY runtime probe failed (exit ${exitCode})`));
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
3
49
|
export async function repairNativeRuntimePermissions(runtimeRoot, platform = process.platform, arch = process.arch) {
|
|
4
50
|
if (platform === "win32")
|
|
5
51
|
return;
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import { mkdtemp, mkdir, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
-
import { repairNativeRuntimePermissions } from "./native-runtime.js";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { probeSQLiteRuntime, probeTerminalRuntime, repairNativeRuntimePermissions } from "./native-runtime.js";
|
|
6
|
+
describe("native runtime health", () => {
|
|
7
|
+
it("detects lazy SQLite binding failures after require succeeds", () => {
|
|
8
|
+
const requireRuntime = (() => class Database {
|
|
9
|
+
constructor() { throw new Error("native binding unavailable"); }
|
|
10
|
+
});
|
|
11
|
+
expect(() => probeSQLiteRuntime(requireRuntime)).toThrow("native binding unavailable");
|
|
12
|
+
});
|
|
13
|
+
it("closes SQLite when its query fails", () => {
|
|
14
|
+
const close = vi.fn();
|
|
15
|
+
const requireRuntime = (() => class Database {
|
|
16
|
+
exec() { throw new Error("database failed"); }
|
|
17
|
+
close = close;
|
|
18
|
+
});
|
|
19
|
+
expect(() => probeSQLiteRuntime(requireRuntime)).toThrow("database failed");
|
|
20
|
+
expect(close).toHaveBeenCalledOnce();
|
|
21
|
+
});
|
|
22
|
+
it("detects PTY spawn failures after require succeeds", async () => {
|
|
23
|
+
const requireRuntime = (() => ({ spawn() { throw new Error("spawn-helper unavailable"); } }));
|
|
24
|
+
await expect(probeTerminalRuntime(requireRuntime)).rejects.toThrow("spawn-helper unavailable");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
6
27
|
describe("repairNativeRuntimePermissions", () => {
|
|
7
28
|
it("restores the node-pty helper execute bits stripped by npm pack", async () => {
|
|
8
29
|
const root = await mkdtemp(resolve(tmpdir(), "agentroam-native-"));
|
|
@@ -4,18 +4,22 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
// @ts-expect-error The preflight intentionally remains plain ESM for Node 18 bootstrap.
|
|
7
|
-
import {
|
|
7
|
+
import { findNvmNode, parsePreflightDataDir, runNodePreflight } from "../bin/node-preflight.mjs";
|
|
8
8
|
const temporaryDirectories = [];
|
|
9
9
|
afterEach(async () => {
|
|
10
10
|
await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true })));
|
|
11
11
|
});
|
|
12
12
|
describe("Node launcher preflight", () => {
|
|
13
|
-
it("passes through
|
|
13
|
+
it.each(["22.22.0", "22.23.0", "24.0.0", "25.8.0", "26.0.0", "100.0.0"])("passes through Node %s without installing or switching", async (nodeVersion) => {
|
|
14
14
|
const ensureNode = vi.fn();
|
|
15
|
-
|
|
15
|
+
const findSystemNode = vi.fn();
|
|
16
|
+
const spawnChild = vi.fn();
|
|
17
|
+
await expect(runNodePreflight({ nodeVersion, ensureNode, findSystemNode, spawnChild, environment: { AGENTROAM_MANAGED_NODE: "/older/node" } })).resolves.toEqual({ handled: false });
|
|
16
18
|
expect(ensureNode).not.toHaveBeenCalled();
|
|
19
|
+
expect(findSystemNode).not.toHaveBeenCalled();
|
|
20
|
+
expect(spawnChild).not.toHaveBeenCalled();
|
|
17
21
|
});
|
|
18
|
-
it.each(["18.20.0", "20.18.0", "
|
|
22
|
+
it.each(["18.20.0", "20.18.0", "22.1.0", "22.21.9"])("re-executes Node %s through the managed runtime", async (nodeVersion) => {
|
|
19
23
|
const child = fakeChild(0, null);
|
|
20
24
|
const ensureNode = vi.fn(async () => ({ executable: "/managed/node", version: "22.22.0" }));
|
|
21
25
|
const spawnChild = vi.fn((_executable, _args, _options) => child);
|
|
@@ -27,7 +31,7 @@ describe("Node launcher preflight", () => {
|
|
|
27
31
|
launcherPath: "/launcher/agentroam.mjs",
|
|
28
32
|
environment: { PATH: "" },
|
|
29
33
|
ensureNode,
|
|
30
|
-
|
|
34
|
+
findSystemNode: async () => null,
|
|
31
35
|
spawnChild,
|
|
32
36
|
processHost: fakeProcessHost(),
|
|
33
37
|
});
|
|
@@ -38,38 +42,40 @@ describe("Node launcher preflight", () => {
|
|
|
38
42
|
}));
|
|
39
43
|
expect(spawnChild).toHaveBeenCalledWith("/managed/node", ["/launcher/agentroam.mjs", "doctor", "--data-dir", "./custom"], expect.objectContaining({
|
|
40
44
|
stdio: "inherit",
|
|
41
|
-
env: expect.objectContaining({ AGENTROAM_MANAGED_NODE: "
|
|
45
|
+
env: expect.objectContaining({ AGENTROAM_MANAGED_NODE: "/managed/node" }),
|
|
42
46
|
}));
|
|
43
47
|
});
|
|
44
|
-
it("reuses another visible system Node
|
|
48
|
+
it("reuses another visible compatible system Node", async () => {
|
|
45
49
|
const ensureNode = vi.fn();
|
|
46
50
|
const spawnChild = vi.fn((_executable, _args, _options) => fakeChild(7, null));
|
|
47
51
|
await expect(runNodePreflight({
|
|
48
52
|
argv: ["version"],
|
|
49
|
-
nodeVersion: "
|
|
53
|
+
nodeVersion: "20.0.0",
|
|
50
54
|
platform: "win32",
|
|
51
55
|
arch: "x64",
|
|
52
56
|
environment: { PATH: "" },
|
|
53
|
-
|
|
57
|
+
findSystemNode: async () => "C:\\Node25\\node.exe",
|
|
54
58
|
ensureNode,
|
|
55
59
|
spawnChild,
|
|
56
60
|
processHost: fakeProcessHost(),
|
|
57
61
|
})).resolves.toMatchObject({ handled: true, exitCode: 7 });
|
|
58
62
|
expect(ensureNode).not.toHaveBeenCalled();
|
|
59
|
-
expect(spawnChild.mock.calls[0][0]).toBe("C:\\
|
|
63
|
+
expect(spawnChild.mock.calls[0][0]).toBe("C:\\Node25\\node.exe");
|
|
60
64
|
});
|
|
61
|
-
it.skipIf(process.platform === "win32")("selects the highest inactive NVM Node
|
|
65
|
+
it.skipIf(process.platform === "win32")("selects the highest compatible inactive NVM Node", async () => {
|
|
62
66
|
const root = await mkdtemp(resolve(tmpdir(), "agentroam-preflight-nvm-"));
|
|
63
67
|
temporaryDirectories.push(root);
|
|
64
68
|
await fakeNodeExecutable(resolve(root, "versions/node/v22.3.0/bin/node"), "22.3.0");
|
|
65
|
-
|
|
69
|
+
await fakeNodeExecutable(resolve(root, "versions/node/v22.22.2/bin/node"), "22.22.2");
|
|
66
70
|
await fakeNodeExecutable(resolve(root, "versions/node/v23.1.0/bin/node"), "23.1.0");
|
|
67
|
-
|
|
71
|
+
const expected = await fakeNodeExecutable(resolve(root, "versions/node/v25.8.0/bin/node"), "25.8.0");
|
|
72
|
+
await fakeNodeExecutable(resolve(root, "versions/node/v26.0.0-rc.1/bin/node"), "26.0.0-rc.1");
|
|
73
|
+
await expect(findNvmNode({ NVM_DIR: root, HOME: resolve(root, "home") }, "darwin"))
|
|
68
74
|
.resolves.toBe(expected);
|
|
69
75
|
});
|
|
70
76
|
it("rejects a failed managed re-entry instead of looping", async () => {
|
|
71
77
|
await expect(runNodePreflight({
|
|
72
|
-
nodeVersion: "
|
|
78
|
+
nodeVersion: "22.21.9",
|
|
73
79
|
environment: { AGENTROAM_MANAGED_NODE: "22.22.0" },
|
|
74
80
|
})).rejects.toMatchObject({ message: expect.stringContaining("re-entry failed"), exitCode: 2 });
|
|
75
81
|
});
|
|
@@ -94,7 +100,7 @@ describe("Node launcher preflight", () => {
|
|
|
94
100
|
platform: "darwin",
|
|
95
101
|
arch: "arm64",
|
|
96
102
|
environment: { PATH: "" },
|
|
97
|
-
|
|
103
|
+
findSystemNode: async () => "/compatible-node",
|
|
98
104
|
spawnChild: () => child,
|
|
99
105
|
processHost,
|
|
100
106
|
});
|
|
@@ -5,7 +5,9 @@ import { delimiter, isAbsolute, relative, resolve, win32 } from "node:path";
|
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { resolveNpmExecutor } from "./codex-runtime-manager.js";
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
|
+
// Default managed installation; compatibility has an independent minimum.
|
|
8
9
|
export const OPENCODE_RUNTIME_VERSION = "1.18.27";
|
|
10
|
+
export const OPENCODE_MINIMUM_VERSION = "1.18.27";
|
|
9
11
|
const OPENCODE_PACKAGE = `opencode-ai@${OPENCODE_RUNTIME_VERSION}`;
|
|
10
12
|
const NPM_REGISTRY = "https://registry.npmjs.org";
|
|
11
13
|
const defaults = {
|
|
@@ -49,7 +51,7 @@ export async function resolveOpenCodeRuntime(options) {
|
|
|
49
51
|
return install({ ...options, environment, platform, dependencies, managedRoot });
|
|
50
52
|
}
|
|
51
53
|
export function parseOpenCodeVersion(output) {
|
|
52
|
-
return output.match(/(?:^|\s)(\d
|
|
54
|
+
return output.match(/(?:^|\s)((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))(?:\s|$)/)?.[1] ?? null;
|
|
53
55
|
}
|
|
54
56
|
export function managedOpenCodeBinaryCandidates(root, platform) {
|
|
55
57
|
return platform === "win32"
|
|
@@ -183,12 +185,22 @@ async function validate(executable, source, _platform, dependencies) {
|
|
|
183
185
|
const version = parseOpenCodeVersion(`${result.stdout}\n${result.stderr}`);
|
|
184
186
|
if (!version)
|
|
185
187
|
throw new Error(`Unable to parse OpenCode version from ${executable}`);
|
|
186
|
-
if (version
|
|
187
|
-
throw new Error(`OpenCode ${version} at ${executable} is incompatible; AgentRoam requires
|
|
188
|
+
if (!meetsOpenCodeMinimum(version)) {
|
|
189
|
+
throw new Error(`OpenCode ${version} at ${executable} is incompatible; AgentRoam requires >=${OPENCODE_MINIMUM_VERSION}`);
|
|
188
190
|
}
|
|
189
191
|
await dependencies.run(executable, ["serve", "--help"], 5_000);
|
|
190
192
|
return { executable, version, source };
|
|
191
193
|
}
|
|
194
|
+
// Compare numeric components, so 1.18.100 and 1.19.0 both exceed 1.18.27.
|
|
195
|
+
function meetsOpenCodeMinimum(version) {
|
|
196
|
+
const actual = version.split(".").map(BigInt);
|
|
197
|
+
const minimum = OPENCODE_MINIMUM_VERSION.split(".").map(BigInt);
|
|
198
|
+
for (let index = 0; index < minimum.length; index += 1) {
|
|
199
|
+
if (actual[index] !== minimum[index])
|
|
200
|
+
return actual[index] > minimum[index];
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
192
204
|
async function accessible(path, executable = false) {
|
|
193
205
|
try {
|
|
194
206
|
await access(path, executable ? constants.X_OK : constants.F_OK);
|
|
@@ -2,7 +2,7 @@ import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
-
import { OPENCODE_RUNTIME_VERSION, managedOpenCodeBinaryCandidates, parseOpenCodeVersion, resolveOpenCodeRuntime, } from "./opencode-runtime-manager.js";
|
|
5
|
+
import { OPENCODE_RUNTIME_VERSION, OPENCODE_MINIMUM_VERSION, managedOpenCodeBinaryCandidates, parseOpenCodeVersion, resolveOpenCodeRuntime, } from "./opencode-runtime-manager.js";
|
|
6
6
|
const roots = [];
|
|
7
7
|
afterEach(async () => {
|
|
8
8
|
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
@@ -31,6 +31,38 @@ describe("resolveOpenCodeRuntime", () => {
|
|
|
31
31
|
})).resolves.toEqual({ executable: binary.path, version: OPENCODE_RUNTIME_VERSION, source: "explicit" });
|
|
32
32
|
expect(run).toHaveBeenCalledWith(binary.path, ["serve", "--help"], 5_000);
|
|
33
33
|
});
|
|
34
|
+
it.each([OPENCODE_MINIMUM_VERSION, "1.18.30", "1.18.100", "1.19.0", "2.0.0"])("reuses PATH version %s without installation", async (version) => {
|
|
35
|
+
const binary = await executable();
|
|
36
|
+
const run = vi.fn(async (_command, args) => ({
|
|
37
|
+
stdout: args[0] === "--version" ? version : "serve help", stderr: "",
|
|
38
|
+
}));
|
|
39
|
+
await expect(resolveOpenCodeRuntime({
|
|
40
|
+
dataDir: binary.root, target: "darwin-arm64", platform: "darwin",
|
|
41
|
+
environment: { PATH: binary.root }, dependencies: { run },
|
|
42
|
+
})).resolves.toEqual({ executable: binary.path, version, source: "global" });
|
|
43
|
+
expect(run.mock.calls.map(([, args]) => args)).toEqual([["--version"], ["serve", "--help"]]);
|
|
44
|
+
});
|
|
45
|
+
it.each(["1.18.26", "1.18.9", "1.17.99", "0.99.99", "dev", "1.19.0-beta.1", "01.19.0"])("rejects unsupported explicit version %s", async (version) => {
|
|
46
|
+
const binary = await executable();
|
|
47
|
+
const run = vi.fn(async () => ({ stdout: version, stderr: "" }));
|
|
48
|
+
await expect(resolveOpenCodeRuntime({
|
|
49
|
+
dataDir: binary.root, target: "darwin-arm64", platform: "darwin",
|
|
50
|
+
environment: { AGENT_OPENCODE_BIN: binary.path, PATH: "" }, dependencies: { run },
|
|
51
|
+
})).rejects.toThrow(/requires >=|Unable to parse OpenCode version/);
|
|
52
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
53
|
+
});
|
|
54
|
+
it("rejects a newer executable without serve support", async () => {
|
|
55
|
+
const binary = await executable();
|
|
56
|
+
await expect(resolveOpenCodeRuntime({
|
|
57
|
+
dataDir: binary.root, target: "darwin-arm64", platform: "darwin",
|
|
58
|
+
environment: { AGENT_OPENCODE_BIN: binary.path, PATH: "" },
|
|
59
|
+
dependencies: { run: async (_command, args) => {
|
|
60
|
+
if (args[0] === "serve")
|
|
61
|
+
throw new Error("serve unavailable");
|
|
62
|
+
return { stdout: "1.18.30", stderr: "" };
|
|
63
|
+
} },
|
|
64
|
+
})).rejects.toThrow("serve unavailable");
|
|
65
|
+
});
|
|
34
66
|
it("rejects relative explicit overrides", async () => {
|
|
35
67
|
await expect(resolveOpenCodeRuntime({
|
|
36
68
|
dataDir: "/data",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
|
-
|
|
4
|
+
import { MINIMUM_NODE_VERSION, assertSupportedNodeVersion } from "../bin/runtime-policy.mjs";
|
|
5
|
+
export const AGENTROAM_VERSION = "0.2.0-preview.17";
|
|
5
6
|
export const PLATFORM_PACKAGES = {
|
|
6
7
|
"darwin-arm64": {
|
|
7
8
|
runtime: "agentroam-runtime-darwin-arm64",
|
|
@@ -37,12 +38,14 @@ export function resolvePlatformRuntime(target, requireFromCli = createRequire(im
|
|
|
37
38
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
38
39
|
if (manifest.packageVersion !== AGENTROAM_VERSION
|
|
39
40
|
|| manifest.target !== target
|
|
40
|
-
|| manifest.
|
|
41
|
-
|| manifest.
|
|
41
|
+
|| manifest.schemaVersion !== 2
|
|
42
|
+
|| manifest.minimumNodeVersion !== MINIMUM_NODE_VERSION
|
|
42
43
|
|| !manifest.nativeFiles
|
|
43
|
-
|| typeof manifest.nativeFiles !== "object"
|
|
44
|
+
|| typeof manifest.nativeFiles !== "object"
|
|
45
|
+
|| Array.isArray(manifest.nativeFiles)) {
|
|
44
46
|
throw new Error(`invalid AgentRoam runtime manifest: ${manifestPath}`);
|
|
45
47
|
}
|
|
48
|
+
assertSupportedNodeVersion();
|
|
46
49
|
return {
|
|
47
50
|
packageName,
|
|
48
51
|
runtimeRoot: dirname(runtimePackageJson),
|
|
@@ -28,8 +28,8 @@ describe("platform packages", () => {
|
|
|
28
28
|
writeFileSync(manifestPath, JSON.stringify({
|
|
29
29
|
packageVersion: AGENTROAM_VERSION,
|
|
30
30
|
target: "windows-amd64",
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
schemaVersion: 2,
|
|
32
|
+
minimumNodeVersion: "22.22.0",
|
|
33
33
|
nativeFiles: { "node_modules/example.node": "a".repeat(64) },
|
|
34
34
|
}));
|
|
35
35
|
const resolved = resolvePlatformRuntime("windows-amd64", fakeRequire({
|
|
@@ -48,8 +48,8 @@ describe("platform packages", () => {
|
|
|
48
48
|
writeFileSync(manifestPath, JSON.stringify({
|
|
49
49
|
packageVersion: "0.0.0",
|
|
50
50
|
target: "darwin-arm64",
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
schemaVersion: 2,
|
|
52
|
+
minimumNodeVersion: "22.22.0",
|
|
53
53
|
nativeFiles: {},
|
|
54
54
|
}));
|
|
55
55
|
expect(() => resolvePlatformRuntime("windows-amd64", fakeRequire({
|
|
@@ -61,6 +61,17 @@ describe("platform packages", () => {
|
|
|
61
61
|
const entry = resolvePlatformTui("windows-amd64", fakeRequire({ "/entry": "C:\\agentroam\\agent-tui.js" }));
|
|
62
62
|
expect(entry).toBe("C:\\agentroam\\agent-tui.js");
|
|
63
63
|
});
|
|
64
|
+
it("rejects legacy ABI manifests even when their release version matches", () => {
|
|
65
|
+
const root = mkdtempSync(resolve(tmpdir(), "agentroam-runtime-legacy-"));
|
|
66
|
+
const manifestPath = resolve(root, "manifest.json");
|
|
67
|
+
writeFileSync(manifestPath, JSON.stringify({
|
|
68
|
+
packageVersion: AGENTROAM_VERSION, target: "darwin-arm64",
|
|
69
|
+
nodeMajor: 22, nodeModuleAbi: 127, nativeFiles: {},
|
|
70
|
+
}));
|
|
71
|
+
expect(() => resolvePlatformRuntime("darwin-arm64", fakeRequire({
|
|
72
|
+
"/manifest.json": manifestPath, "/runtime": resolve(root, "runtime/package.json"),
|
|
73
|
+
}))).toThrow("invalid AgentRoam runtime manifest");
|
|
74
|
+
});
|
|
64
75
|
});
|
|
65
76
|
function fakeRequire(suffixes) {
|
|
66
77
|
return {
|
package/dist/platform.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertSupportedNodeVersion } from "../bin/runtime-policy.mjs";
|
|
1
2
|
export function detectPlatformTarget(platform = process.platform, arch = process.arch) {
|
|
2
3
|
if (platform === "darwin" && arch === "arm64")
|
|
3
4
|
return "darwin-arm64";
|
|
@@ -8,9 +9,6 @@ export function detectPlatformTarget(platform = process.platform, arch = process
|
|
|
8
9
|
throw Object.assign(new Error(`unsupported platform: ${platform}-${arch}`), { exitCode: 2 });
|
|
9
10
|
}
|
|
10
11
|
export function detectPlatform(platform = process.platform, arch = process.arch, nodeVersion = process.versions.node) {
|
|
11
|
-
|
|
12
|
-
if (major !== 22) {
|
|
13
|
-
throw Object.assign(new Error(`Node.js 22 required (current ${nodeVersion})`), { exitCode: 2 });
|
|
14
|
-
}
|
|
12
|
+
assertSupportedNodeVersion(nodeVersion);
|
|
15
13
|
return detectPlatformTarget(platform, arch);
|
|
16
14
|
}
|
package/dist/platform.test.js
CHANGED
|
@@ -2,15 +2,15 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import { detectPlatform, detectPlatformTarget } from "./platform.js";
|
|
3
3
|
describe("detectPlatform", () => {
|
|
4
4
|
it("maps supported targets", () => {
|
|
5
|
-
expect(detectPlatform("darwin", "arm64", "22.
|
|
6
|
-
expect(detectPlatform("win32", "x64", "
|
|
5
|
+
expect(detectPlatform("darwin", "arm64", "22.22.0")).toBe("darwin-arm64");
|
|
6
|
+
expect(detectPlatform("win32", "x64", "25.8.0")).toBe("windows-amd64");
|
|
7
7
|
});
|
|
8
8
|
it("detects a target before Node version validation", () => {
|
|
9
9
|
expect(detectPlatformTarget("darwin", "arm64")).toBe("darwin-arm64");
|
|
10
10
|
expect(detectPlatformTarget("win32", "x64")).toBe("windows-amd64");
|
|
11
11
|
});
|
|
12
12
|
it("rejects unsupported versions", () => {
|
|
13
|
-
expect(() => detectPlatform("darwin", "arm64", "
|
|
13
|
+
expect(() => detectPlatform("darwin", "arm64", "22.21.9")).toThrow("Node.js >=22.22.0");
|
|
14
14
|
});
|
|
15
15
|
it("rejects unsupported platforms", () => {
|
|
16
16
|
expect(() => detectPlatformTarget("linux", "x64")).toThrow("unsupported platform");
|
|
@@ -4,6 +4,7 @@ import { detectPlatform } from "../platform.js";
|
|
|
4
4
|
import { renderQr } from "../qr.js";
|
|
5
5
|
import { MacLaunchAgent } from "./macos-launch-agent.js";
|
|
6
6
|
import { WindowsTaskService } from "./windows-task-service.js";
|
|
7
|
+
import { assertSupportedNodeVersion } from "../../bin/runtime-policy.mjs";
|
|
7
8
|
export async function runServiceCommand(options, context) {
|
|
8
9
|
const platform = context.platform ?? process.platform;
|
|
9
10
|
const log = context.log ?? console.log;
|
|
@@ -19,9 +20,7 @@ export async function runServiceCommand(options, context) {
|
|
|
19
20
|
switch (options.serviceAction) {
|
|
20
21
|
case "install": {
|
|
21
22
|
const nodeVersion = context.nodeVersion ?? process.versions.node;
|
|
22
|
-
|
|
23
|
-
throw cliError(`Node.js 22 is required to install the service (current ${nodeVersion})`);
|
|
24
|
-
}
|
|
23
|
+
assertSupportedNodeVersion(nodeVersion);
|
|
25
24
|
const now = context.now?.() ?? new Date();
|
|
26
25
|
const environment = context.environment ?? process.env;
|
|
27
26
|
const environmentPath = buildServiceEnvironmentPath(context.nodePath, environment.PATH ?? environment.Path, platform);
|
|
@@ -2,6 +2,26 @@ import { resolve } from "node:path";
|
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { buildServiceEnvironmentPath, runServiceCommand } from "./service-command.js";
|
|
4
4
|
describe("runServiceCommand", () => {
|
|
5
|
+
it.each(["22.22.0", "24.0.0", "25.8.0", "26.0.0"])("keeps the selected Node %s when installing a service", async (nodeVersion) => {
|
|
6
|
+
const install = vi.fn(async () => ({ definition: "/test/service.plist" }));
|
|
7
|
+
const nodePath = `/runtimes/node-${nodeVersion}/bin/node`;
|
|
8
|
+
const codexResolver = vi.fn(async () => ({ executable: "/tools/codex", version: "test", source: "global" }));
|
|
9
|
+
await runServiceCommand(options("install"), {
|
|
10
|
+
platform: "darwin", arch: "arm64", nodeVersion, nodePath,
|
|
11
|
+
cliPath: "/agentroam.mjs", version: "test", environment: { PATH: "/usr/bin" },
|
|
12
|
+
codexResolver, controller: controller({ install }), log: vi.fn(),
|
|
13
|
+
});
|
|
14
|
+
expect(install).toHaveBeenCalledWith(expect.objectContaining({ nodePath }));
|
|
15
|
+
expect(codexResolver).toHaveBeenCalledWith(expect.objectContaining({ nodeExecutable: nodePath }));
|
|
16
|
+
});
|
|
17
|
+
it("rejects a below-minimum service runtime before installation", async () => {
|
|
18
|
+
const install = vi.fn();
|
|
19
|
+
await expect(runServiceCommand(options("install"), {
|
|
20
|
+
platform: "darwin", nodePath: "/node", nodeVersion: "22.21.9", cliPath: "/agentroam.mjs",
|
|
21
|
+
version: "test", controller: controller({ install }),
|
|
22
|
+
})).rejects.toThrow("Node.js >=22.22.0");
|
|
23
|
+
expect(install).not.toHaveBeenCalled();
|
|
24
|
+
});
|
|
5
25
|
it("installs the service using absolute runtime context and parsed start options", async () => {
|
|
6
26
|
const install = vi.fn(async (config) => ({
|
|
7
27
|
paths: { plistPath: "/Users/test/Library/LaunchAgents/com.agentroam.service.plist" },
|
|
@@ -22,7 +22,7 @@ export async function waitForPublicReadiness(publicUrl, options = {}) {
|
|
|
22
22
|
try {
|
|
23
23
|
const response = await fetchImpl(endpoint, {
|
|
24
24
|
cache: "no-store",
|
|
25
|
-
headers: { "user-agent": "agentroam-readiness/0.2.0-preview.
|
|
25
|
+
headers: { "user-agent": "agentroam-readiness/0.2.0-preview.17" },
|
|
26
26
|
signal: options.signal,
|
|
27
27
|
});
|
|
28
28
|
if (response.ok) {
|
package/install/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# AgentRoam bootstrap installers
|
|
2
2
|
|
|
3
|
-
These versioned installers support macOS Apple Silicon and Windows x64. They reuse
|
|
3
|
+
These versioned installers support macOS Apple Silicon and Windows x64. They reuse the active Node.js installation when it meets `>=22.22.0`, with no upper version bound. Otherwise they search NVM for the highest compatible version, then fall back to the verified private Node.js `22.22.0` runtime under the AgentRoam data directory. They then register and start the current-user AgentRoam background service. They never modify, replace, or uninstall system Node.js or global power settings.
|
|
4
4
|
|
|
5
5
|
## macOS Apple Silicon
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
curl -fsSL https://gitee.com/caoqu/team-agent/releases/download/v0.2.0-preview.
|
|
8
|
+
curl -fsSL https://gitee.com/caoqu/team-agent/releases/download/v0.2.0-preview.17/install-agentroam.sh | sh
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
The command installs the wrapper at `~/.local/bin/agentroam`. If that directory is not already in `PATH`, the installer prints the required shell setting without editing shell profiles.
|
|
@@ -13,7 +13,7 @@ The command installs the wrapper at `~/.local/bin/agentroam`. If that directory
|
|
|
13
13
|
## Windows x64
|
|
14
14
|
|
|
15
15
|
```powershell
|
|
16
|
-
irm https://gitee.com/caoqu/team-agent/releases/download/v0.2.0-preview.
|
|
16
|
+
irm https://gitee.com/caoqu/team-agent/releases/download/v0.2.0-preview.17/install-agentroam.ps1 | iex
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
The command installs `%USERPROFILE%\.agentroam\bin\agentroam.cmd` and adds only that user directory to the user-level `PATH`. It does not require administrator access or change the machine-level `PATH`.
|
|
@@ -40,7 +40,7 @@ agentroam service uninstall
|
|
|
40
40
|
|
|
41
41
|
## Data and updates
|
|
42
42
|
|
|
43
|
-
Set `AGENTROAM_DATA_DIR` before running either installer to override the default `~/.agentroam` data directory. Node is stored at `<data-dir>/runtimes/node/22.22.0`, and the launcher is stored at `<data-dir>/launcher/0.2.0-preview.
|
|
43
|
+
Set `AGENTROAM_DATA_DIR` before running either installer to override the default `~/.agentroam` data directory. Node is stored at `<data-dir>/runtimes/node/22.22.0`, and the launcher is stored at `<data-dir>/launcher/0.2.0-preview.17`.
|
|
44
44
|
|
|
45
45
|
Each AgentRoam release pins its own Node patch and installer assets. Updates happen only by running a newer versioned installer or installing a newer AgentRoam release; startup never follows a moving Node.js release channel.
|
|
46
46
|
|
|
@@ -2,7 +2,8 @@ $ErrorActionPreference = "Stop"
|
|
|
2
2
|
$ProgressPreference = "SilentlyContinue"
|
|
3
3
|
|
|
4
4
|
$NodeVersion = "22.22.0"
|
|
5
|
-
$
|
|
5
|
+
$MinimumNodeVersion = "22.22.0"
|
|
6
|
+
$AgentRoamVersion = "0.2.0-preview.17"
|
|
6
7
|
$NodeArchive = "node-v22.22.0-win-x64.zip"
|
|
7
8
|
$NodeSha256 = "c97fa376d2becdc8863fcd3ca2dd9a83a9f3468ee7ccf7a6d076ec66a645c77a"
|
|
8
9
|
$NodeUrl = "https://nodejs.org/dist/v22.22.0/$NodeArchive"
|
|
@@ -27,9 +28,14 @@ if (-not $ServiceRootExplicit -and $ServiceRoot -eq $HomeRoot) {
|
|
|
27
28
|
|
|
28
29
|
New-Item -ItemType Directory -Force -Path $NodeParent, $LauncherParent, $WrapperDir | Out-Null
|
|
29
30
|
|
|
30
|
-
function Test-
|
|
31
|
+
function Test-NodeVersion([string]$VersionText) {
|
|
32
|
+
if ($VersionText -notmatch '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$') { return $false }
|
|
33
|
+
try { return [Version]($VersionText.Split('+')[0]) -ge [Version]$MinimumNodeVersion } catch { return $false }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function Test-SupportedNode([string]$NodeBin) {
|
|
31
37
|
if (-not (Test-Path -LiteralPath $NodeBin -PathType Leaf)) { return $false }
|
|
32
|
-
try { return (& $NodeBin -p 'process.versions.node
|
|
38
|
+
try { return Test-NodeVersion ((& $NodeBin -p 'process.versions.node' 2>$null).Trim()) } catch { return $false }
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
function Get-NvmRoots {
|
|
@@ -50,15 +56,15 @@ function Get-NvmRoots {
|
|
|
50
56
|
@($Candidates | Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Container) } | Select-Object -Unique)
|
|
51
57
|
}
|
|
52
58
|
|
|
53
|
-
function Find-
|
|
59
|
+
function Find-NvmNode {
|
|
54
60
|
$VersionMatches = foreach ($NvmRoot in (Get-NvmRoots)) {
|
|
55
61
|
foreach ($VersionDirectory in Get-ChildItem -LiteralPath $NvmRoot -Directory -ErrorAction SilentlyContinue) {
|
|
56
62
|
$Candidate = Join-Path $VersionDirectory.FullName "node.exe"
|
|
57
63
|
if (-not (Test-Path -LiteralPath $Candidate -PathType Leaf)) { continue }
|
|
58
64
|
try {
|
|
59
65
|
$VersionText = (& $Candidate -p 'process.versions.node' 2>$null).Trim()
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
if (Test-NodeVersion $VersionText) {
|
|
67
|
+
$Version = [Version]($VersionText.Split('+')[0])
|
|
62
68
|
[pscustomobject]@{ Path = $Candidate; Version = $Version }
|
|
63
69
|
}
|
|
64
70
|
} catch {}
|
|
@@ -97,10 +103,10 @@ function Enter-InstallLock([string]$Path) {
|
|
|
97
103
|
|
|
98
104
|
$SystemNode = Get-Command node.exe -ErrorAction SilentlyContinue
|
|
99
105
|
if ($env:AGENTROAM_BOOTSTRAP_TEST -eq "1" -and $env:AGENTROAM_FORCE_PRIVATE_NODE -eq "1") { $SystemNode = $null }
|
|
100
|
-
if ($SystemNode -and (Test-
|
|
106
|
+
if ($SystemNode -and (Test-SupportedNode $SystemNode.Source)) {
|
|
101
107
|
$NodeBin = $SystemNode.Source
|
|
102
108
|
} else {
|
|
103
|
-
$NvmNode = if ($env:AGENTROAM_BOOTSTRAP_TEST -eq "1" -and $env:AGENTROAM_FORCE_PRIVATE_NODE -eq "1") { $null } else { Find-
|
|
109
|
+
$NvmNode = if ($env:AGENTROAM_BOOTSTRAP_TEST -eq "1" -and $env:AGENTROAM_FORCE_PRIVATE_NODE -eq "1") { $null } else { Find-NvmNode }
|
|
104
110
|
if ($NvmNode) {
|
|
105
111
|
$NodeBin = $NvmNode
|
|
106
112
|
Write-Host "Using Node.js $(& $NodeBin --version) from NVM: $NodeBin"
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
set -eu
|
|
3
3
|
|
|
4
4
|
NODE_VERSION="22.22.0"
|
|
5
|
-
|
|
5
|
+
MINIMUM_NODE_VERSION="22.22.0"
|
|
6
|
+
AGENTROAM_VERSION="0.2.0-preview.17"
|
|
6
7
|
NODE_ARCHIVE="node-v22.22.0-darwin-arm64.tar.xz"
|
|
7
8
|
NODE_SHA256="2bd596bbfc4a275ceb8721a5954ee97daea5ebe673e96a185ebd732f6fb023ac"
|
|
8
9
|
NODE_URL="https://nodejs.org/dist/v22.22.0/$NODE_ARCHIVE"
|
|
@@ -56,25 +57,40 @@ SERVICE_ROOT="$service_root_physical"
|
|
|
56
57
|
|
|
57
58
|
mkdir -p "$NODE_PARENT" "$LAUNCHER_PARENT" "$WRAPPER_DIR"
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
version_is_supported() {
|
|
61
|
+
awk -v version="$1" -v minimum="$MINIMUM_NODE_VERSION" 'BEGIN {
|
|
62
|
+
if (version !~ /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/) exit 1;
|
|
63
|
+
sub(/\+.*/, "", version);
|
|
64
|
+
split(version, actual, "."); split(minimum, required, ".");
|
|
65
|
+
for (component = 1; component <= 3; component++) {
|
|
66
|
+
if (actual[component] + 0 > required[component] + 0) exit 0;
|
|
67
|
+
if (actual[component] + 0 < required[component] + 0) exit 1;
|
|
68
|
+
}
|
|
69
|
+
exit 0;
|
|
70
|
+
}'
|
|
61
71
|
}
|
|
62
72
|
|
|
63
|
-
|
|
73
|
+
node_is_supported() {
|
|
74
|
+
[ -x "$1" ] && version_is_supported "$("$1" -p 'process.versions.node' 2>/dev/null || true)"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
find_nvm_node() {
|
|
64
78
|
best_bin=""
|
|
79
|
+
best_major=-1
|
|
65
80
|
best_minor=-1
|
|
66
81
|
best_patch=-1
|
|
67
82
|
for nvm_root in "${NVM_DIR:-}" "$HOME/.nvm"; do
|
|
68
83
|
[ -n "$nvm_root" ] || continue
|
|
69
|
-
for candidate in "$nvm_root"/versions/node/
|
|
84
|
+
for candidate in "$nvm_root"/versions/node/v*/bin/node; do
|
|
70
85
|
[ -x "$candidate" ] || continue
|
|
71
86
|
version=$("$candidate" -p 'process.versions.node' 2>/dev/null || true)
|
|
87
|
+
version_is_supported "$version" || continue
|
|
72
88
|
major=$(printf '%s\n' "$version" | awk -F. '{ print $1 + 0 }')
|
|
73
89
|
minor=$(printf '%s\n' "$version" | awk -F. '{ print $2 + 0 }')
|
|
74
90
|
patch=$(printf '%s\n' "$version" | awk -F. '{ sub(/[^0-9].*$/, "", $3); print $3 + 0 }')
|
|
75
|
-
[ "$major" -eq
|
|
76
|
-
if [ "$minor" -gt "$best_minor" ] || { [ "$minor" -eq "$best_minor" ] && [ "$patch" -gt "$best_patch" ]; }; then
|
|
91
|
+
if [ "$major" -gt "$best_major" ] || { [ "$major" -eq "$best_major" ] && { [ "$minor" -gt "$best_minor" ] || { [ "$minor" -eq "$best_minor" ] && [ "$patch" -gt "$best_patch" ]; }; }; }; then
|
|
77
92
|
best_bin="$candidate"
|
|
93
|
+
best_major=$major
|
|
78
94
|
best_minor=$minor
|
|
79
95
|
best_patch=$patch
|
|
80
96
|
fi
|
|
@@ -112,13 +128,13 @@ SYSTEM_NODE=$(command -v node 2>/dev/null || true)
|
|
|
112
128
|
if [ "${AGENTROAM_BOOTSTRAP_TEST:-}" = "1" ] && [ "${AGENTROAM_FORCE_PRIVATE_NODE:-}" = "1" ]; then
|
|
113
129
|
SYSTEM_NODE=""
|
|
114
130
|
fi
|
|
115
|
-
if [ -n "$SYSTEM_NODE" ] &&
|
|
131
|
+
if [ -n "$SYSTEM_NODE" ] && node_is_supported "$SYSTEM_NODE"; then
|
|
116
132
|
NODE_BIN="$SYSTEM_NODE"
|
|
117
133
|
else
|
|
118
134
|
if [ "${AGENTROAM_BOOTSTRAP_TEST:-}" = "1" ] && [ "${AGENTROAM_FORCE_PRIVATE_NODE:-}" = "1" ]; then
|
|
119
135
|
NVM_NODE=""
|
|
120
136
|
else
|
|
121
|
-
NVM_NODE=$(
|
|
137
|
+
NVM_NODE=$(find_nvm_node || true)
|
|
122
138
|
fi
|
|
123
139
|
if [ -n "$NVM_NODE" ]; then
|
|
124
140
|
NODE_BIN="$NVM_NODE"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentroam",
|
|
3
|
-
"version": "0.2.0-preview.
|
|
3
|
+
"version": "0.2.0-preview.17",
|
|
4
4
|
"description": "Zero-install mobile remote terminal and file console",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"agent-tui": "./bin/agent-tui.mjs"
|
|
9
9
|
},
|
|
10
10
|
"engines": {
|
|
11
|
-
"node": ">=22
|
|
11
|
+
"node": ">=22.22.0"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"bin",
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"undici": "^6.25.0"
|
|
27
27
|
},
|
|
28
28
|
"optionalDependencies": {
|
|
29
|
-
"agentroam-runtime-darwin-arm64": "0.2.0-preview.
|
|
30
|
-
"agentroam-runtime-win32-x64": "0.2.0-preview.
|
|
31
|
-
"agentroam-cloudflared-darwin-arm64": "0.2.0-preview.
|
|
32
|
-
"agentroam-cloudflared-win32-x64": "0.2.0-preview.
|
|
33
|
-
"agentroam-tui-darwin-arm64": "0.2.0-preview.
|
|
34
|
-
"@caoqu/agentroam-tui-win32-x64": "0.2.0-preview.
|
|
29
|
+
"agentroam-runtime-darwin-arm64": "0.2.0-preview.17",
|
|
30
|
+
"agentroam-runtime-win32-x64": "0.2.0-preview.17",
|
|
31
|
+
"agentroam-cloudflared-darwin-arm64": "0.2.0-preview.17",
|
|
32
|
+
"agentroam-cloudflared-win32-x64": "0.2.0-preview.17",
|
|
33
|
+
"agentroam-tui-darwin-arm64": "0.2.0-preview.17",
|
|
34
|
+
"@caoqu/agentroam-tui-win32-x64": "0.2.0-preview.17"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^22.0.0",
|