agentroam 0.2.0-preview.36 → 0.2.0-preview.37

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.
@@ -24,7 +24,7 @@ export async function runCodexDesktopCommand(options, context = {}) {
24
24
  const codex = await (context.codexResolver ?? resolveCodexRuntime)({
25
25
  dataDir: options.dataDir,
26
26
  target: detectPlatform(platform, process.arch, process.versions.node),
27
- environment,
27
+ environment: { ...environment },
28
28
  platform,
29
29
  nodeExecutable: process.execPath,
30
30
  onProgress: (message) => log(`… ${message}`),
@@ -32,13 +32,17 @@ export async function runCodexDesktopCommand(options, context = {}) {
32
32
  environment.AGENT_CODEX_BIN = codex.executable;
33
33
  }
34
34
  const args = [scriptPath];
35
+ if (!options.desktopDryRun) {
36
+ environment.AGENTROAM_CODEX_DESKTOP_PREFLIGHT_NODE = process.execPath;
37
+ environment.AGENTROAM_CODEX_DESKTOP_PREFLIGHT = fileURLToPath(new URL("./codex-desktop-preflight.js", import.meta.url));
38
+ }
35
39
  if (options.desktopRestart)
36
40
  args.push("--restart");
37
41
  if (options.desktopDryRun)
38
42
  args.push("--dry-run");
39
43
  try {
40
44
  const { stdout, stderr } = await (context.execute ?? execFileAsync)("/bin/bash", args, {
41
- env: environment, encoding: "utf8", timeout: 30_000, maxBuffer: 1024 * 1024,
45
+ env: environment, encoding: "utf8", timeout: 60_000, maxBuffer: 1024 * 1024,
42
46
  });
43
47
  if (stdout.trim())
44
48
  log(stdout.trimEnd());
@@ -0,0 +1,57 @@
1
+ import { execFile } from "node:child_process";
2
+ import { homedir } from "node:os";
3
+ import { promisify } from "node:util";
4
+ import { pathToFileURL } from "node:url";
5
+ import { DesktopProbeRpcError, readDesktopActivity, withDesktopRpc } from "./codex-desktop-probe.js";
6
+ const execFileAsync = promisify(execFile);
7
+ async function inspectBackend(endpoint) {
8
+ return withDesktopRpc(endpoint, async (request, becameActive) => {
9
+ try {
10
+ await request("config/read", {});
11
+ try {
12
+ await request("account/gatewayOAuth/read", {});
13
+ }
14
+ catch (error) {
15
+ // Older servers may not have the Desktop gateway RPC.
16
+ if (!(error instanceof DesktopProbeRpcError && error.code === -32601))
17
+ throw error;
18
+ }
19
+ return true;
20
+ }
21
+ catch (error) {
22
+ if (!(error instanceof DesktopProbeRpcError) || !/No such file or directory|os error 2/.test(error.message))
23
+ throw error;
24
+ const activity = await readDesktopActivity(request);
25
+ if (activity.state !== "idle" || becameActive())
26
+ throw new Error("共享后端配置失效,但任务忙碌或状态无法确认;已保留桌面和共享后端");
27
+ return false;
28
+ }
29
+ });
30
+ }
31
+ /** Repair only a missing-directory backend after two complete idle snapshots. */
32
+ export async function preflightCodexDesktop(endpoint, executable, context = {}) {
33
+ if (await inspectBackend(endpoint))
34
+ return;
35
+ if (await inspectBackend(endpoint))
36
+ return;
37
+ (context.log ?? console.log)("共享后端的工作目录已失效,已确认全部任务空闲,正在从稳定目录重建…");
38
+ await (context.restart ?? (async () => {
39
+ await execFileAsync(executable, ["app-server", "daemon", "restart"], {
40
+ cwd: homedir(), env: process.env, timeout: 15_000, maxBuffer: 1024 * 1024,
41
+ });
42
+ }))();
43
+ if (!await inspectBackend(endpoint))
44
+ throw new Error("共享后端重建后仍无法读取配置;已保留桌面");
45
+ }
46
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
47
+ const [endpoint, executable] = process.argv.slice(2);
48
+ if (!endpoint || !executable) {
49
+ console.error("Missing shared backend endpoint or Codex executable");
50
+ process.exitCode = 1;
51
+ }
52
+ else
53
+ preflightCodexDesktop(endpoint, executable).catch(error => {
54
+ console.error(`共享后端检查失败:${error instanceof Error ? error.message : error}`);
55
+ process.exitCode = 1;
56
+ });
57
+ }
@@ -1,6 +1,24 @@
1
1
  import { Agent, WebSocket } from "undici";
2
+ export class DesktopProbeRpcError extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.code = code;
7
+ }
8
+ }
2
9
  /** Read live state only. Never resume, subscribe, interrupt or unload a thread. */
3
10
  export async function probeDesktopActivity(endpoint, timeoutMs = 5_000) {
11
+ try {
12
+ return await withDesktopRpc(endpoint, async (request, becameActive) => {
13
+ const activity = await readDesktopActivity(request);
14
+ return becameActive() && activity.state === "idle" ? { state: "busy", activeCount: 1 } : activity;
15
+ }, timeoutMs);
16
+ }
17
+ catch {
18
+ return { state: "unknown", reason: "无法完整查询桌面端的实时任务状态" };
19
+ }
20
+ }
21
+ export async function withDesktopRpc(endpoint, operation, timeoutMs = 5_000) {
4
22
  let dispatcher;
5
23
  let socket;
6
24
  let timer;
@@ -49,7 +67,7 @@ export async function probeDesktopActivity(endpoint, timeoutMs = 5_000) {
49
67
  return;
50
68
  pending.delete(message.id);
51
69
  if (message.error)
52
- item.reject(new Error("Desktop probe RPC failed"));
70
+ item.reject(new DesktopProbeRpcError(message.error.code, String(message.error.message ?? "Desktop probe RPC failed")));
53
71
  else
54
72
  item.resolve(message.result);
55
73
  }
@@ -68,21 +86,17 @@ export async function probeDesktopActivity(endpoint, timeoutMs = 5_000) {
68
86
  reject(error);
69
87
  }
70
88
  });
71
- const operation = async () => {
89
+ const run = async () => {
72
90
  await opened;
73
- await request("initialize", { clientInfo: { name: "agentroam_desktop_setup", version: "1" } });
91
+ await request("initialize", { clientInfo: { name: "agentroam_desktop_setup", version: "1" }, capabilities: { experimentalApi: true } });
74
92
  connection.send(JSON.stringify({ method: "initialized", params: {} }));
75
- const activity = await readDesktopActivity(request);
76
- return changedToActive && activity.state === "idle" ? { state: "busy", activeCount: 1 } : activity;
93
+ return operation(request, () => changedToActive);
77
94
  };
78
95
  return await Promise.race([
79
- operation(),
96
+ run(),
80
97
  new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("Desktop probe timed out")), timeoutMs); }),
81
98
  ]);
82
99
  }
83
- catch {
84
- return { state: "unknown", reason: "无法完整查询桌面端的实时任务状态" };
85
- }
86
100
  finally {
87
101
  if (timer)
88
102
  clearTimeout(timer);
@@ -1,5 +1,18 @@
1
1
  import { lstat, readFile, readdir, realpath, rm } from "node:fs/promises";
2
2
  import { relative, resolve, sep } from "node:path";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ const execFileAsync = promisify(execFile);
6
+ async function processPathsInUse() {
7
+ // Windows has no reliable cwd inventory here: retain old installs instead.
8
+ if (process.platform === "win32")
9
+ throw new Error("Process paths unavailable");
10
+ const { stdout } = await execFileAsync(process.platform === "darwin" ? "/usr/sbin/lsof" : "lsof", ["-nP", "-d", "cwd,txt", "-Fn"], { timeout: 5_000, maxBuffer: 16 * 1024 * 1024 });
11
+ const paths = stdout.split("\n").filter(line => line.startsWith("n/")).map(line => line.slice(1));
12
+ if (!paths.length)
13
+ throw new Error("Incomplete process paths");
14
+ return [process.cwd(), ...paths];
15
+ }
3
16
  function versionParts(version) {
4
17
  const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-preview\.(0|[1-9]\d*))?$/.exec(version);
5
18
  return match ? [Number(match[1]), Number(match[2]), Number(match[3]), match[4] === undefined ? Infinity : Number(match[4])] : null;
@@ -18,7 +31,7 @@ function contains(parent, child) {
18
31
  return path === "" || (!path.startsWith(`..${sep}`) && path !== ".." && !path.startsWith(sep));
19
32
  }
20
33
  /** Remove only recognized managed launcher versions after the replacement service is ready. */
21
- export async function cleanupOldLaunchers(config, log) {
34
+ export async function cleanupOldLaunchers(config, log, context = {}) {
22
35
  try {
23
36
  const dataDir = await realpath(config.dataDir);
24
37
  const launcherDir = resolve(dataDir, "launcher");
@@ -47,6 +60,11 @@ export async function cleanupOldLaunchers(config, log) {
47
60
  const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
48
61
  if (manifest.name !== "agentroam" || manifest.version !== entry.name)
49
62
  continue;
63
+ const usedPaths = await (context.pathsInUse ?? processPathsInUse)();
64
+ if (usedPaths.some(path => contains(candidate, path))) {
65
+ log(`旧版 CLI ${entry.name} 仍被进程使用,已保留。`);
66
+ continue;
67
+ }
50
68
  await rm(candidate, { recursive: true });
51
69
  log(`✓ 已清理旧版 CLI ${entry.name},配置和数据已保留`);
52
70
  }
@@ -99,12 +99,19 @@ if [[ -n "$codex_desktop_running_pid" && "$codex_desktop_restart" != true ]]; th
99
99
  fi
100
100
 
101
101
  # Check daemon availability before asking the existing Desktop to quit.
102
- "$codex_shared_cli" app-server daemon start
102
+ (cd "$HOME" && "$codex_shared_cli" app-server daemon start)
103
103
  if [[ ! -S "$codex_shared_socket" ]]; then
104
104
  echo "Shared app-server socket not found: $codex_shared_socket" >&2
105
105
  exit 1
106
106
  fi
107
107
  "$codex_shared_cli" app-server daemon version >/dev/null
108
+ codex_desktop_preflight_node="${AGENTROAM_CODEX_DESKTOP_PREFLIGHT_NODE:-$(command -v node || true)}"
109
+ codex_desktop_preflight="${AGENTROAM_CODEX_DESKTOP_PREFLIGHT:-$(dirname "$0")/../dist/codex-desktop-preflight.js}"
110
+ if [[ -z "$codex_desktop_preflight_node" || ! -f "$codex_desktop_preflight" ]]; then
111
+ echo "Shared backend configuration check unavailable; Desktop was preserved." >&2
112
+ exit 1
113
+ fi
114
+ "$codex_desktop_preflight_node" "$codex_desktop_preflight" "$codex_shared_url" "$codex_shared_cli"
108
115
 
109
116
  # Recheck after daemon preflight; never quit a different Desktop instance.
110
117
  codex_desktop_preflight_pid="$codex_desktop_running_pid"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentroam",
3
- "version": "0.2.0-preview.36",
3
+ "version": "0.2.0-preview.37",
4
4
  "description": "Zero-install mobile remote terminal and file console",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,8 +26,8 @@
26
26
  "undici": "^6.25.0"
27
27
  },
28
28
  "optionalDependencies": {
29
- "agentroam-runtime-darwin-arm64": "0.2.0-preview.36",
30
- "agentroam-runtime-win32-x64": "0.2.0-preview.36",
29
+ "agentroam-runtime-darwin-arm64": "0.2.0-preview.37",
30
+ "agentroam-runtime-win32-x64": "0.2.0-preview.37",
31
31
  "agentroam-cloudflared-darwin-arm64": "0.2.0-preview.36",
32
32
  "agentroam-cloudflared-win32-x64": "0.2.0-preview.36",
33
33
  "agentroam-tui-darwin-arm64": "0.2.0-preview.36",