agentroam 0.2.0-preview.22 → 0.2.0-preview.24

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.
@@ -7,7 +7,7 @@ import { promisify } from "node:util";
7
7
  import { MINIMUM_NODE_VERSION, compareNodeVersions, isSupportedNodeVersion, parseNodeVersion } from "./runtime-policy.mjs";
8
8
 
9
9
  const execFileAsync = promisify(execFile);
10
- const RELEASE_VERSION = "0.2.0-preview.22";
10
+ const RELEASE_VERSION = "0.2.0-preview.24";
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
 
package/dist/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { printLocalDesktopUrl } from './local-desktop-url.js';
1
2
  import { createRequire } from "node:module";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import { parseArgs } from "./args.js";
@@ -137,6 +138,9 @@ export async function main(argv) {
137
138
  }
138
139
  void runtime.exited.then(() => controller.abort());
139
140
  console.log(`✓ Local server: ${runtime.localUrl}/web`);
141
+ await serviceReporter?.localReady(runtime.localUrl);
142
+ if (!serviceReporter)
143
+ printLocalDesktopUrl(runtime.localUrl);
140
144
  const lanUrl = options.localOnly ? findLanUrl(runtime.port) ?? runtime.localUrl : runtime.localUrl;
141
145
  relay = await selectRelay({
142
146
  cli: options,
@@ -5,6 +5,8 @@ import { delimiter, dirname, isAbsolute, relative, resolve, win32 } from "node:p
5
5
  import { promisify } from "node:util";
6
6
  const execFileAsync = promisify(execFile);
7
7
  export const CODEX_RUNTIME_VERSION = "0.153.0";
8
+ // Managed install pin tracks releases; the compatibility floor for user installs is independent.
9
+ export const CODEX_MINIMUM_VERSION = "0.153.0";
8
10
  const CODEX_PACKAGE = `@openai/codex@${CODEX_RUNTIME_VERSION}`;
9
11
  const NPM_REGISTRY = "https://registry.npmjs.org";
10
12
  export const CODEX_INSTALL_TIMEOUT_MS = 60 * 60_000;
@@ -130,6 +132,19 @@ export function parseCodexVersion(output) {
130
132
  const match = output.match(/(?:^|\s)(\d+\.\d+\.\d+)(?:\s|$)/);
131
133
  return match?.[1] ?? null;
132
134
  }
135
+ // Compare numeric components, so 0.153.10 and 0.154.0 both exceed 0.153.2.
136
+ export function isCodexVersionAtLeast(version, minimum) {
137
+ const actual = version.split(".").map(BigInt);
138
+ const required = minimum.split(".").map(BigInt);
139
+ for (let index = 0; index < required.length; index += 1) {
140
+ if (actual[index] !== required[index])
141
+ return actual[index] > required[index];
142
+ }
143
+ return true;
144
+ }
145
+ function meetsCodexMinimum(version) {
146
+ return isCodexVersionAtLeast(version, CODEX_MINIMUM_VERSION);
147
+ }
133
148
  async function findCompatibleGlobalCodex(pathValue, platform, dependencies) {
134
149
  const names = platform === "win32" ? ["codex.exe", "codex"] : ["codex"];
135
150
  const pathDelimiter = platform === "win32" ? ";" : delimiter;
@@ -257,10 +272,11 @@ async function validateCodex(executable, source, _platform, dependencies) {
257
272
  }
258
273
  const versionResult = await dependencies.run(executable, ["--version"], 5_000);
259
274
  const version = parseCodexVersion(`${versionResult.stdout}\n${versionResult.stderr}`);
260
- if (!version)
261
- throw new Error(`Unable to parse Codex version from ${executable}`);
262
- if (version !== CODEX_RUNTIME_VERSION) {
263
- throw new Error(`Codex ${version} at ${executable} is incompatible; AgentRoam requires ${CODEX_RUNTIME_VERSION}`);
275
+ if (!version) {
276
+ throw new Error(`Unable to parse a supported stable Codex version from ${executable}`);
277
+ }
278
+ if (!meetsCodexMinimum(version)) {
279
+ throw new Error(`Codex ${version} at ${executable} is incompatible; AgentRoam requires >=${CODEX_MINIMUM_VERSION}`);
264
280
  }
265
281
  try {
266
282
  await dependencies.run(executable, ["app-server", "--help"], 5_000);
@@ -2,21 +2,31 @@ import { chmod, mkdtemp, mkdir, readdir, rm, writeFile } from "node:fs/promises"
2
2
  import { tmpdir } from "node:os";
3
3
  import { dirname, resolve, win32 } from "node:path";
4
4
  import { afterEach, describe, expect, it, vi } from "vitest";
5
- import { CODEX_INSTALL_STALE_LOCK_MS, CODEX_INSTALL_TIMEOUT_MS, CODEX_RUNTIME_VERSION, managedCodexBinaryCandidates, parseCodexVersion, resolveCodexRuntime, resolveNpmExecutor, } from "./codex-runtime-manager.js";
5
+ import { CODEX_INSTALL_STALE_LOCK_MS, CODEX_INSTALL_TIMEOUT_MS, CODEX_MINIMUM_VERSION, CODEX_RUNTIME_VERSION, isCodexVersionAtLeast, managedCodexBinaryCandidates, parseCodexVersion, resolveCodexRuntime, resolveNpmExecutor, } from "./codex-runtime-manager.js";
6
6
  const temporaryDirectories = [];
7
7
  afterEach(async () => {
8
8
  await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true })));
9
9
  });
10
10
  describe("managed Codex runtime", () => {
11
11
  it("parses the official version output", () => {
12
- expect(parseCodexVersion("codex-cli 0.153.0")).toBe("0.153.0");
12
+ expect(parseCodexVersion("codex-cli 0.153.5")).toBe("0.153.5");
13
13
  expect(parseCodexVersion("codex-cli next")).toBeNull();
14
14
  });
15
+ it("compares versions numerically instead of lexicographically", () => {
16
+ // Samples stay below the current pin: pins only move forward, so these
17
+ // literals can never collide with the upgrade script's version replacement.
18
+ expect(isCodexVersionAtLeast("0.140.12", "0.140.3")).toBe(true);
19
+ expect(isCodexVersionAtLeast("0.140.3", "0.140.12")).toBe(false);
20
+ expect(isCodexVersionAtLeast(CODEX_MINIMUM_VERSION, CODEX_MINIMUM_VERSION)).toBe(true);
21
+ expect(isCodexVersionAtLeast("0.139.9", CODEX_MINIMUM_VERSION)).toBe(false);
22
+ expect(isCodexVersionAtLeast("0.152.0", "0.140.0")).toBe(true);
23
+ expect(isCodexVersionAtLeast("1.0.0", "0.140.0")).toBe(true);
24
+ });
15
25
  it("uses a compatible explicit absolute executable first", async () => {
16
26
  const root = await temporaryRoot();
17
27
  const executable = await fakeExecutable(resolve(root, "codex"));
18
28
  const run = vi.fn(async (_command, args) => ({
19
- stdout: args[0] === "--version" ? "codex-cli 0.153.0\n" : "app-server help\n",
29
+ stdout: args[0] === "--version" ? `codex-cli ${CODEX_RUNTIME_VERSION}\n` : "app-server help\n",
20
30
  stderr: "",
21
31
  }));
22
32
  await expect(resolveCodexRuntime({
@@ -32,7 +42,29 @@ describe("managed Codex runtime", () => {
32
42
  });
33
43
  expect(run.mock.calls.map((call) => call[1])).toEqual([["--version"], ["app-server", "--help"]]);
34
44
  });
35
- it("rejects an incompatible explicit executable instead of silently replacing it", async () => {
45
+ it("accepts explicit executables at or above the minimum stable version", async () => {
46
+ const outputs = [
47
+ `codex-cli ${CODEX_MINIMUM_VERSION}\n`,
48
+ `codex-cli ${CODEX_RUNTIME_VERSION}\n`,
49
+ ];
50
+ for (const output of outputs) {
51
+ const root = await temporaryRoot();
52
+ const executable = await fakeExecutable(resolve(root, "codex"));
53
+ await expect(resolveCodexRuntime({
54
+ dataDir: resolve(root, "data"),
55
+ target: "darwin-arm64",
56
+ environment: { AGENT_CODEX_BIN: executable, PATH: "" },
57
+ platform: "darwin",
58
+ dependencies: {
59
+ run: async (_command, args) => ({
60
+ stdout: args[0] === "--version" ? output : "app-server help\n",
61
+ stderr: "",
62
+ }),
63
+ },
64
+ })).resolves.toMatchObject({ executable, source: "explicit" });
65
+ }
66
+ });
67
+ it("rejects an explicit executable below the minimum instead of silently replacing it", async () => {
36
68
  const root = await temporaryRoot();
37
69
  const executable = await fakeExecutable(resolve(root, "codex"));
38
70
  await expect(resolveCodexRuntime({
@@ -41,9 +73,22 @@ describe("managed Codex runtime", () => {
41
73
  environment: { AGENT_CODEX_BIN: executable, PATH: "" },
42
74
  platform: "darwin",
43
75
  dependencies: {
44
- run: async () => ({ stdout: "codex-cli 0.154.0\n", stderr: "" }),
76
+ run: async () => ({ stdout: "codex-cli 0.152.9\n", stderr: "" }),
45
77
  },
46
- })).rejects.toThrow("requires 0.153.0");
78
+ })).rejects.toThrow(`requires >=${CODEX_MINIMUM_VERSION}`);
79
+ });
80
+ it("rejects prerelease or otherwise unparseable version output", async () => {
81
+ const root = await temporaryRoot();
82
+ const executable = await fakeExecutable(resolve(root, "codex"));
83
+ await expect(resolveCodexRuntime({
84
+ dataDir: resolve(root, "data"),
85
+ target: "darwin-arm64",
86
+ environment: { AGENT_CODEX_BIN: executable, PATH: "" },
87
+ platform: "darwin",
88
+ dependencies: {
89
+ run: async () => ({ stdout: "codex-cli 0.150.0-alpha.1\n", stderr: "" }),
90
+ },
91
+ })).rejects.toThrow("Unable to parse a supported stable Codex version");
47
92
  });
48
93
  it("skips an incompatible PATH executable and reuses the managed runtime", async () => {
49
94
  const root = await temporaryRoot();
@@ -52,7 +97,7 @@ describe("managed Codex runtime", () => {
52
97
  const managed = await fakeExecutable(managedCodexBinaryCandidates(managedRoot, "darwin-arm64")[0]);
53
98
  const run = vi.fn(async (command, args) => ({
54
99
  stdout: args[0] === "--version"
55
- ? command === global ? "codex-cli 0.152.0\n" : "codex-cli 0.153.0\n"
100
+ ? command === global ? "codex-cli 0.152.0\n" : `codex-cli ${CODEX_RUNTIME_VERSION}\n`
56
101
  : "help\n",
57
102
  stderr: "",
58
103
  }));
@@ -65,6 +110,22 @@ describe("managed Codex runtime", () => {
65
110
  })).resolves.toEqual({ executable: managed, version: CODEX_RUNTIME_VERSION, source: "managed" });
66
111
  expect(run.mock.calls.some((call) => call[1].includes("install"))).toBe(false);
67
112
  });
113
+ it("reuses a qualified PATH executable without installing", async () => {
114
+ const root = await temporaryRoot();
115
+ const global = await fakeExecutable(resolve(root, "bin", "codex"));
116
+ const run = vi.fn(async (_command, args) => ({
117
+ stdout: args[0] === "--version" ? `codex-cli ${CODEX_RUNTIME_VERSION}\n` : "help\n",
118
+ stderr: "",
119
+ }));
120
+ await expect(resolveCodexRuntime({
121
+ dataDir: resolve(root, "data"),
122
+ target: "darwin-arm64",
123
+ environment: { PATH: resolve(root, "bin") },
124
+ platform: "darwin",
125
+ dependencies: { run },
126
+ })).resolves.toEqual({ executable: global, version: CODEX_RUNTIME_VERSION, source: "global" });
127
+ expect(run.mock.calls.some((call) => call[1].includes("install"))).toBe(false);
128
+ });
68
129
  it("installs the locked official package into a versioned runtime directory", async () => {
69
130
  const root = await temporaryRoot();
70
131
  const npmExecPath = await fakeExecutable(resolve(root, "npm-cli.js"));
@@ -80,7 +141,7 @@ describe("managed Codex runtime", () => {
80
141
  return { stdout: "installed", stderr: "" };
81
142
  }
82
143
  return {
83
- stdout: args[0] === "--version" ? "codex-cli 0.153.0\n" : "help\n",
144
+ stdout: args[0] === "--version" ? `codex-cli ${CODEX_RUNTIME_VERSION}\n` : "help\n",
84
145
  stderr: "",
85
146
  };
86
147
  });
@@ -110,7 +171,7 @@ describe("managed Codex runtime", () => {
110
171
  return { stdout: "installed", stderr: "" };
111
172
  }
112
173
  return {
113
- stdout: args[0] === "--version" ? "codex-cli 0.153.0\n" : "help\n",
174
+ stdout: args[0] === "--version" ? `codex-cli ${CODEX_RUNTIME_VERSION}\n` : "help\n",
114
175
  stderr: "",
115
176
  };
116
177
  };
@@ -0,0 +1,8 @@
1
+ export function printLocalDesktopUrl(localUrl, log = console.log, platform = process.platform) {
2
+ log(`远程桌面授权地址:${localUrl.replace(/\/$/, '')}/web`);
3
+ log('请在运行 CLI 的这台电脑上用浏览器打开此地址,配对后点击“远程授权”(盾牌图标)。');
4
+ if (platform === 'win32')
5
+ log('点击“开启共享”。请保持 Windows 已登录且未锁屏;暂不支持 UAC 安全桌面和管理员窗口控制。');
6
+ else
7
+ log('按提示授予“屏幕录制”和“辅助功能”权限,然后点击“开启共享”。');
8
+ }
@@ -2,7 +2,7 @@ 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.22";
5
+ export const AGENTROAM_VERSION = "0.2.0-preview.24";
6
6
  export const PLATFORM_DEPENDENCY_VERSIONS = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).optionalDependencies;
7
7
  export const PLATFORM_PACKAGES = {
8
8
  "darwin-arm64": {
@@ -16,6 +16,9 @@ export class ServiceRuntimeReporter {
16
16
  await removeIfExists(this.paths.urlPath);
17
17
  await this.write({ status: "starting" });
18
18
  }
19
+ async localReady(localUrl) {
20
+ await this.write({ status: "starting", localUrl });
21
+ }
19
22
  async ready(ready) {
20
23
  await this.write({ status: "ready", ...ready });
21
24
  await writePrivateText(this.paths.urlPath, `${ready.accessUrl}\n`);
@@ -13,6 +13,9 @@ describe("ServiceRuntimeReporter", () => {
13
13
  await reporter.starting();
14
14
  expect(await readServiceState(paths)).toMatchObject({ status: "starting", pid: 123, version: "0.2.0-preview.9" });
15
15
  await expect(access(paths.urlPath)).rejects.toMatchObject({ code: "ENOENT" });
16
+ await reporter.localReady("http://127.0.0.1:49157");
17
+ expect(await readServiceState(paths)).toMatchObject({ status: "starting", localUrl: "http://127.0.0.1:49157", pid: 123 });
18
+ await expect(access(paths.urlPath)).rejects.toMatchObject({ code: "ENOENT" });
16
19
  await reporter.ready({
17
20
  localUrl: "http://127.0.0.1:3000",
18
21
  publicUrl: "https://example.trycloudflare.com",
@@ -1,3 +1,4 @@
1
+ import { printLocalDesktopUrl } from '../local-desktop-url.js';
1
2
  import { delimiter, dirname, isAbsolute, resolve, win32 } from "node:path";
2
3
  import { resolveCodexRuntime } from "../codex-runtime-manager.js";
3
4
  import { detectPlatform } from "../platform.js";
@@ -60,13 +61,10 @@ export async function runServiceCommand(options, context) {
60
61
  log(`✓ AgentRoam service installed: ${definition}`);
61
62
  if (state?.status === "ready" && state.version === config.version)
62
63
  await cleanupOldLaunchers(config, log);
64
+ if (state?.localUrl && state.status !== "stopped")
65
+ printLocalDesktopUrl(state.localUrl, log, platform);
63
66
  if (state?.status === "ready" && state.accessUrl) {
64
67
  log(`Open: ${state.accessUrl}`);
65
- if (state.localUrl) {
66
- log(`远程桌面授权地址:${state.localUrl.replace(/\/$/, "")}/web`);
67
- log('请在运行 CLI 的这台电脑上用浏览器打开此地址,配对后点击“远程授权”(盾牌图标)。');
68
- log('按提示授予“屏幕录制”和“辅助功能”权限,然后点击“开启共享”。');
69
- }
70
68
  await (context.pairingPrinter ?? printPairingCode)(options.dataDir, log, { accessUrl: state.accessUrl, qr: options.qr, interactive: stdoutIsTTY });
71
69
  }
72
70
  else
@@ -78,6 +76,8 @@ export async function runServiceCommand(options, context) {
78
76
  log("✓ AgentRoam service started");
79
77
  if (state?.status === "ready" && state.accessUrl)
80
78
  await logAccessUrl(state.accessUrl);
79
+ if (state?.localUrl && state.status !== "stopped")
80
+ printLocalDesktopUrl(state.localUrl, log, platform);
81
81
  return;
82
82
  }
83
83
  case "stop":
@@ -95,6 +95,8 @@ export async function runServiceCommand(options, context) {
95
95
  log(`Version: ${status.config.version}`);
96
96
  log(`Root: ${status.config.roots.join(", ")}`);
97
97
  }
98
+ if (status.running && status.state?.localUrl)
99
+ printLocalDesktopUrl(status.state.localUrl, log, platform);
98
100
  if (status.running && status.state?.status === "ready" && status.state.accessUrl) {
99
101
  await logAccessUrl(status.state.accessUrl);
100
102
  }
@@ -118,6 +120,8 @@ export async function runServiceCommand(options, context) {
118
120
  log("✓ AgentRoam service restarted");
119
121
  if (state?.status === "ready" && state.accessUrl)
120
122
  await logAccessUrl(state.accessUrl);
123
+ if (state?.localUrl && state.status !== "stopped")
124
+ printLocalDesktopUrl(state.localUrl, log, platform);
121
125
  return;
122
126
  }
123
127
  case "uninstall": {
@@ -173,3 +173,16 @@ function options(serviceAction) {
173
173
  dataDir: resolve("data"),
174
174
  };
175
175
  }
176
+ it.each(['install', 'start', 'restart', 'status'])('prints the actual Windows local address for %s before tunnel readiness', async (action) => {
177
+ const state = { status: 'starting', localUrl: 'http://127.0.0.1:49157', pid: 123 };
178
+ const log = vi.fn();
179
+ const control = controller({
180
+ install: vi.fn(async () => ({ state, definition: 'task' })), start: vi.fn(async () => state), restart: vi.fn(async () => state),
181
+ status: vi.fn(async () => ({ installed: true, running: true, state, config: null })),
182
+ });
183
+ await runServiceCommand(options(action), { platform: 'win32', arch: 'x64', nodePath: 'C:\\node.exe', cliPath: 'C:\\agentroam.mjs', version: 'test', nodeVersion: '22.22.0', controller: control, log,
184
+ codexResolver: vi.fn(async () => ({ executable: 'C:\\codex.exe', version: 'test', source: 'global' })) });
185
+ expect(log).toHaveBeenCalledWith('远程桌面授权地址:http://127.0.0.1:49157/web');
186
+ expect(log).toHaveBeenCalledWith(expect.stringContaining('保持 Windows 已登录'));
187
+ expect(log.mock.calls.some(([line]) => line.includes('屏幕录制') || line.includes('辅助功能'))).toBe(false);
188
+ });
@@ -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.22" },
25
+ headers: { "user-agent": "agentroam-readiness/0.2.0-preview.24" },
26
26
  signal: options.signal,
27
27
  });
28
28
  if (response.ok) {
@@ -4,6 +4,7 @@ $ProgressPreference = "SilentlyContinue"
4
4
  $NodeVersion = "22.22.0"
5
5
  $MinimumNodeVersion = "22.22.0"
6
6
  # 默认跟随 npm preview 最新标签;可用 $env:AGENTROAM_VERSION 钉精确版本
7
+ # 例如:$env:AGENTROAM_VERSION = "0.2.0-preview.24"; ./install-agentroam.ps1
7
8
  $AgentRoamVersionRequest = if ($env:AGENTROAM_VERSION) { $env:AGENTROAM_VERSION } else { "preview" }
8
9
  $AgentRoamVersion = "pending-version"
9
10
  $NodeArchive = "node-v22.22.0-win-x64.zip"
@@ -4,7 +4,7 @@ set -eu
4
4
  NODE_VERSION="22.22.0"
5
5
  MINIMUM_NODE_VERSION="22.22.0"
6
6
  # Default to the newest published preview; pin an exact version via
7
- # AGENTROAM_VERSION=0.2.0-preview.22 sh install-agentroam.sh
7
+ # AGENTROAM_VERSION=0.2.0-preview.24 sh install-agentroam.sh
8
8
  AGENTROAM_VERSION_REQUEST="${AGENTROAM_VERSION:-preview}"
9
9
  AGENTROAM_VERSION=""
10
10
  NODE_ARCHIVE="node-v22.22.0-darwin-arm64.tar.xz"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentroam",
3
- "version": "0.2.0-preview.22",
3
+ "version": "0.2.0-preview.24",
4
4
  "description": "Zero-install mobile remote terminal and file console",
5
5
  "type": "module",
6
6
  "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.22",
30
- "agentroam-runtime-win32-x64": "0.2.0-preview.22",
31
- "agentroam-cloudflared-darwin-arm64": "0.2.0-preview.20",
32
- "agentroam-cloudflared-win32-x64": "0.2.0-preview.20",
33
- "agentroam-tui-darwin-arm64": "0.2.0-preview.20",
34
- "@caoqu/agentroam-tui-win32-x64": "0.2.0-preview.20"
29
+ "agentroam-runtime-darwin-arm64": "0.2.0-preview.24",
30
+ "agentroam-runtime-win32-x64": "0.2.0-preview.24",
31
+ "agentroam-cloudflared-darwin-arm64": "0.2.0-preview.24",
32
+ "agentroam-cloudflared-win32-x64": "0.2.0-preview.24",
33
+ "agentroam-tui-darwin-arm64": "0.2.0-preview.24",
34
+ "@caoqu/agentroam-tui-win32-x64": "0.2.0-preview.24"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^22.0.0",