agentroam 0.2.0-preview.46 → 0.2.0-preview.48

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/dist/args.js CHANGED
@@ -81,8 +81,8 @@ export function parseArgs(argv) {
81
81
  if (command === "desktop" && !["--data-dir", "--help", "-h"].includes(arg)) {
82
82
  throw cliError("desktop accepts only --data-dir");
83
83
  }
84
- if (command === "codex-desktop" && !["--restart", "--dry-run", "--setup", "--data-dir", "--help", "-h"].includes(arg)) {
85
- throw cliError("codex-desktop accepts only --setup, --restart, --dry-run and --data-dir");
84
+ if (command === "codex-desktop" && !["--restart", "--dry-run", "--setup", "--status", "--repair", "--data-dir", "--help", "-h"].includes(arg)) {
85
+ throw cliError("codex-desktop accepts only --setup, --restart, --dry-run, --status, --repair and --data-dir");
86
86
  }
87
87
  const next = () => {
88
88
  const value = values[++index];
@@ -94,6 +94,10 @@ export function parseArgs(argv) {
94
94
  options.desktopRestart = true;
95
95
  else if (arg === "--dry-run" && command === "codex-desktop")
96
96
  options.desktopDryRun = true;
97
+ else if (arg === "--status" && command === "codex-desktop")
98
+ options.desktopSharingStatus = true;
99
+ else if (arg === "--repair" && command === "codex-desktop")
100
+ options.desktopRepair = true;
97
101
  else if (arg === "--setup" && command === "codex-desktop")
98
102
  options.desktopSetup = true;
99
103
  else if (arg === "--setup-codex-desktop" && command === "service" && serviceAction === "install")
@@ -137,6 +141,10 @@ export function parseArgs(argv) {
137
141
  if (command === "codex-desktop" && options.desktopSetup && (options.desktopRestart || options.desktopDryRun)) {
138
142
  throw cliError("--setup cannot be combined with --restart or --dry-run");
139
143
  }
144
+ if (command === "codex-desktop" && (options.desktopSharingStatus || options.desktopRepair)
145
+ && [options.desktopSharingStatus, options.desktopRepair, options.desktopSetup, options.desktopRestart, options.desktopDryRun].filter(Boolean).length !== 1) {
146
+ throw cliError("--status and --repair cannot be combined with other desktop actions");
147
+ }
140
148
  if (command === "update" && updateVersion && !/^\d+\.\d+\.\d+$/.test(updateVersion)) {
141
149
  throw cliError("update version must be an exact stable X.Y.Z version");
142
150
  }
package/dist/cli.js CHANGED
@@ -48,6 +48,8 @@ export async function main(argv) {
48
48
  agentroam unlock-service install|uninstall|status 管理 Windows 远程解锁服务(安装时弹出 UAC)
49
49
  agentroam codex-desktop [--dry-run] 通过共享后端启动官方 Codex 桌面端(macOS)
50
50
  agentroam codex-desktop --restart 退出并重新连接官方桌面端,会中断它的全部运行会话
51
+ agentroam codex-desktop --status 输出实际共享连接状态(JSON)
52
+ agentroam codex-desktop --repair 修复默认共享和自动恢复,保留运行中的桌面任务
51
53
  agentroam codex-desktop --setup 空闲桌面自动重新连接;有任务或状态未知时询问是否重启
52
54
  agentroam service install --setup-codex-desktop 扫码前配置官方桌面共享连接(macOS)
53
55
  设备命令支持 --data-dir PATH;配对码有效期 5 分钟,设备授权有效期 30 天。`);
@@ -4,6 +4,7 @@ import { promisify } from "node:util";
4
4
  import { resolveCodexRuntime } from "./codex-runtime-manager.js";
5
5
  import { readServiceConfig, resolveServicePaths } from "./service/service-files.js";
6
6
  import { detectPlatform } from "./platform.js";
7
+ import { configureCodexSharing, inspectCodexSharing } from "./codex-sharing.js";
7
8
  const execFileAsync = promisify(execFile);
8
9
  const scriptPath = fileURLToPath(new URL("../install/start-codex-desktop-shared.sh", import.meta.url));
9
10
  export async function runCodexDesktopCommand(options, context = {}) {
@@ -19,6 +20,12 @@ export async function runCodexDesktopCommand(options, context = {}) {
19
20
  if (!environment.CODEX_HOME && service.codexHome)
20
21
  environment.CODEX_HOME = service.codexHome;
21
22
  }
23
+ if (options.desktopSharingStatus || options.desktopRepair) {
24
+ if (options.desktopRepair)
25
+ await (context.configureSharing ?? configureCodexSharing)(options, { platform, environment });
26
+ log(JSON.stringify(await (context.inspectSharing ?? inspectCodexSharing)({ platform, environment })));
27
+ return;
28
+ }
22
29
  // A dry run reads the installed configuration and never installs a runtime.
23
30
  if (!options.desktopDryRun) {
24
31
  const codex = await (context.codexResolver ?? resolveCodexRuntime)({
@@ -30,6 +37,7 @@ export async function runCodexDesktopCommand(options, context = {}) {
30
37
  onProgress: (message) => log(`… ${message}`),
31
38
  });
32
39
  environment.AGENT_CODEX_BIN = codex.executable;
40
+ await (context.configureSharing ?? configureCodexSharing)(options, { platform, environment });
33
41
  }
34
42
  const args = [scriptPath];
35
43
  if (!options.desktopDryRun) {
@@ -4,6 +4,7 @@ import { ReadStream, WriteStream } from "node:tty";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { runCodexDesktopCommand } from "./codex-desktop-command.js";
6
6
  import { inspectCodexDesktop } from "./codex-desktop-status.js";
7
+ import { configureCodexSharing } from "./codex-sharing.js";
7
8
  import { withoutCliProgress } from "../bin/cli-progress.mjs";
8
9
  export async function setupCodexDesktop(options, context = {}) {
9
10
  if ((context.platform ?? process.platform) !== "darwin")
@@ -19,6 +20,8 @@ export async function setupCodexDesktop(options, context = {}) {
19
20
  const inspect = context.inspect ?? inspectCodexDesktop;
20
21
  const deferred = () => log("已保留当前 Codex 桌面端。扫码仍可连接 AgentRoam;原桌面会话暂未共享,可稍后执行 agentroam codex-desktop --setup。");
21
22
  try {
23
+ await (context.configureSharing ?? configureCodexSharing)(options, { environment });
24
+ log("已保存默认共享启动与后台自动恢复配置。");
22
25
  let snapshot = await inspect(environment);
23
26
  if (snapshot.status === "not-installed") {
24
27
  log("未安装官方 Codex 桌面端,继续生成扫码连接入口。");
@@ -1,13 +1,13 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { constants } from "node:fs";
3
- import { access, chmod, mkdir, mkdtemp, open, rename, rm, stat, } from "node:fs/promises";
3
+ import { access, chmod, mkdir, mkdtemp, open, readdir, rename, rm, stat, } from "node:fs/promises";
4
4
  import { delimiter, dirname, isAbsolute, relative, resolve, win32 } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  const execFileAsync = promisify(execFile);
7
- export const CODEX_RUNTIME_VERSION = "0.153.0";
8
- // Managed install pin tracks releases; the compatibility floor for user installs is independent.
7
+ // Fresh installs follow npm's latest stable release; existing installs have an independent floor.
8
+ export const CODEX_RUNTIME_TAG = "latest";
9
9
  export const CODEX_MINIMUM_VERSION = "0.153.0";
10
- const CODEX_PACKAGE = `@openai/codex@${CODEX_RUNTIME_VERSION}`;
10
+ const CODEX_PACKAGE = `@openai/codex@${CODEX_RUNTIME_TAG}`;
11
11
  const NPM_REGISTRY = "https://registry.npmjs.org";
12
12
  export const CODEX_INSTALL_TIMEOUT_MS = 60 * 60_000;
13
13
  const DEFAULT_LOCK_WAIT_MS = 190_000;
@@ -50,17 +50,16 @@ export async function resolveCodexRuntime(options) {
50
50
  const global = await findCompatibleGlobalCodex(environment.PATH, platform, dependencies);
51
51
  if (global)
52
52
  return global;
53
- const managedRoot = resolve(options.dataDir, "runtimes", "codex", CODEX_RUNTIME_VERSION);
54
- const managed = await validateManagedRuntime(managedRoot, options.target, platform, dependencies);
53
+ const managedParent = resolve(options.dataDir, "runtimes", "codex");
54
+ const managed = await findManagedRuntime(managedParent, options.target, platform, dependencies);
55
55
  if (managed)
56
56
  return managed;
57
- options.onProgress?.(`Installing managed Codex ${CODEX_RUNTIME_VERSION}...`);
57
+ options.onProgress?.(`Installing managed Codex from ${CODEX_PACKAGE}...`);
58
58
  return installManagedCodex({
59
59
  ...options,
60
60
  environment,
61
61
  platform,
62
62
  dependencies,
63
- managedRoot,
64
63
  });
65
64
  }
66
65
  export async function resolveNpmExecutor(options = {}) {
@@ -165,19 +164,19 @@ async function findCompatibleGlobalCodex(pathValue, platform, dependencies) {
165
164
  }
166
165
  async function installManagedCodex(options) {
167
166
  const parent = resolve(options.dataDir, "runtimes", "codex");
168
- const lockPath = `${options.managedRoot}.lock`;
167
+ const lockPath = resolve(parent, "install.lock");
169
168
  const lockWaitMs = options.lockWaitMs ?? DEFAULT_LOCK_WAIT_MS;
170
169
  const staleLockMs = options.staleLockMs ?? DEFAULT_STALE_LOCK_MS;
171
170
  await mkdir(parent, { recursive: true });
172
- const lock = await acquireInstallLock(lockPath, options.managedRoot, options.target, options.platform, options.dependencies, lockWaitMs, staleLockMs);
171
+ const lock = await acquireInstallLock(lockPath, parent, options.target, options.platform, options.dependencies, lockWaitMs, staleLockMs);
173
172
  if (lock.completed)
174
173
  return lock.completed;
175
174
  let temporaryRoot = null;
176
175
  try {
177
- const existing = await validateManagedRuntime(options.managedRoot, options.target, options.platform, options.dependencies);
176
+ const existing = await findManagedRuntime(parent, options.target, options.platform, options.dependencies);
178
177
  if (existing)
179
178
  return existing;
180
- temporaryRoot = await mkdtemp(resolve(parent, `.${CODEX_RUNTIME_VERSION}-${process.pid}-`));
179
+ temporaryRoot = await mkdtemp(resolve(parent, `.${CODEX_RUNTIME_TAG}-${process.pid}-`));
181
180
  const npm = await resolveNpmExecutor({
182
181
  environment: options.environment,
183
182
  platform: options.platform,
@@ -189,6 +188,7 @@ async function installManagedCodex(options) {
189
188
  "--ignore-scripts",
190
189
  "--no-audit",
191
190
  "--no-fund",
191
+ "--prefer-online",
192
192
  `--registry=${NPM_REGISTRY}`,
193
193
  "--prefix",
194
194
  temporaryRoot,
@@ -198,18 +198,21 @@ async function installManagedCodex(options) {
198
198
  if (!temporary) {
199
199
  throw new Error(`Codex runtime validation failed after installing ${CODEX_PACKAGE}`);
200
200
  }
201
- await rename(temporaryRoot, options.managedRoot);
201
+ const managedRoot = resolve(parent, temporary.version);
202
+ // An invalid cache for this version must not block a validated replacement.
203
+ await rm(managedRoot, { recursive: true, force: true });
204
+ await rename(temporaryRoot, managedRoot);
202
205
  temporaryRoot = null;
203
- const installed = await validateManagedRuntime(options.managedRoot, options.target, options.platform, options.dependencies);
206
+ const installed = await validateManagedRuntime(managedRoot, options.target, options.platform, options.dependencies);
204
207
  if (!installed)
205
208
  throw new Error("Codex runtime validation failed after activation");
206
209
  return installed;
207
210
  }
208
211
  catch (error) {
209
- const concurrent = await validateManagedRuntime(options.managedRoot, options.target, options.platform, options.dependencies);
212
+ const concurrent = await findManagedRuntime(parent, options.target, options.platform, options.dependencies);
210
213
  if (concurrent)
211
214
  return concurrent;
212
- throw new Error(`Unable to install managed Codex ${CODEX_RUNTIME_VERSION}: ${describeError(error)}`, {
215
+ throw new Error(`Unable to install managed Codex from ${CODEX_PACKAGE}: ${describeError(error)}`, {
213
216
  cause: error,
214
217
  });
215
218
  }
@@ -220,7 +223,7 @@ async function installManagedCodex(options) {
220
223
  await rm(lockPath, { force: true });
221
224
  }
222
225
  }
223
- async function acquireInstallLock(lockPath, managedRoot, target, platform, dependencies, waitMs, staleMs) {
226
+ async function acquireInstallLock(lockPath, managedParent, target, platform, dependencies, waitMs, staleMs) {
224
227
  const deadline = dependencies.now() + waitMs;
225
228
  while (true) {
226
229
  try {
@@ -231,7 +234,7 @@ async function acquireInstallLock(lockPath, managedRoot, target, platform, depen
231
234
  catch (error) {
232
235
  if (error.code !== "EEXIST")
233
236
  throw error;
234
- const completed = await validateManagedRuntime(managedRoot, target, platform, dependencies);
237
+ const completed = await findManagedRuntime(managedParent, target, platform, dependencies);
235
238
  if (completed)
236
239
  return { completed };
237
240
  try {
@@ -251,6 +254,24 @@ async function acquireInstallLock(lockPath, managedRoot, target, platform, depen
251
254
  }
252
255
  }
253
256
  }
257
+ async function findManagedRuntime(parent, target, platform, dependencies) {
258
+ const entries = await readdir(parent, { withFileTypes: true }).catch((error) => {
259
+ if (error.code === "ENOENT")
260
+ return [];
261
+ throw error;
262
+ });
263
+ const versions = entries
264
+ .filter((entry) => entry.isDirectory() && parseCodexVersion(entry.name) === entry.name)
265
+ .map((entry) => entry.name)
266
+ .filter(meetsCodexMinimum)
267
+ .sort((left, right) => left === right ? 0 : isCodexVersionAtLeast(left, right) ? -1 : 1);
268
+ for (const version of versions) {
269
+ const runtime = await validateManagedRuntime(resolve(parent, version), target, platform, dependencies);
270
+ if (runtime?.version === version)
271
+ return runtime;
272
+ }
273
+ return null;
274
+ }
254
275
  async function validateManagedRuntime(runtimeRoot, target, platform, dependencies) {
255
276
  for (const candidate of managedCodexBinaryCandidates(runtimeRoot, target)) {
256
277
  if (!isWithin(runtimeRoot, candidate) || !(await pathAccessible(candidate)))
@@ -0,0 +1,153 @@
1
+ import { execFile } from "node:child_process";
2
+ import { access, chmod, mkdir, readFile, realpath, rename, rm } from "node:fs/promises";
3
+ import { constants } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { dirname, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { isDeepStrictEqual, promisify } from "node:util";
8
+ import { resolveCodexRuntime } from "./codex-runtime-manager.js";
9
+ import { inspectCodexDesktop } from "./codex-desktop-status.js";
10
+ import { withDesktopRpc } from "./codex-desktop-probe.js";
11
+ import { detectPlatform } from "./platform.js";
12
+ import { writePrivateText } from "./service/service-files.js";
13
+ const executeFile = promisify(execFile);
14
+ export const CODEX_SHARING_LABEL = "com.agentroam.codex-desktop-shared-env";
15
+ function locations(context) {
16
+ const home = resolve(context.homeDir ?? homedir());
17
+ const environment = context.environment ?? process.env;
18
+ const codexHome = resolve(environment.CODEX_HOME || resolve(home, ".codex"));
19
+ const socketPath = resolve(codexHome, "app-server-control/app-server-control.sock");
20
+ return { home, environment, codexHome, socketPath,
21
+ endpoint: `ws+unix://localhost${encodeURI(socketPath).replaceAll("#", "%23").replaceAll("?", "%3F").replaceAll(":", "%3A")}:/rpc`,
22
+ root: resolve(home, ".agentroam/codex-desktop"),
23
+ plist: resolve(home, "Library/LaunchAgents", `${CODEX_SHARING_LABEL}.plist`),
24
+ target: `gui/${context.uid ?? process.getuid?.() ?? 0}/${CODEX_SHARING_LABEL}`,
25
+ };
26
+ }
27
+ /** Save defaults and ensure the daemon; never quit Desktop or restart a live backend. */
28
+ export async function configureCodexSharing(options, context = {}) {
29
+ if ((context.platform ?? process.platform) !== "darwin")
30
+ return;
31
+ const paths = locations(context);
32
+ const environment = { ...paths.environment };
33
+ const executable = environment.AGENT_CODEX_BIN || (await resolveCodexRuntime({
34
+ dataDir: options.dataDir, target: detectPlatform(), environment,
35
+ })).executable;
36
+ if (!executable.startsWith("/"))
37
+ throw new Error("Codex 运行文件必须使用绝对路径");
38
+ await access(executable, constants.X_OK);
39
+ const execute = context.execute ?? executeFile;
40
+ const script = resolve(paths.root, "watchdog.sh");
41
+ await writePrivateText(script, await readFile(fileURLToPath(new URL("../install/codex-shared-watchdog.sh", import.meta.url)), "utf8"));
42
+ const args = [script, executable, paths.codexHome, paths.endpoint, environment.PATH || "/usr/bin:/bin:/usr/sbin:/sbin", paths.home];
43
+ const definition = {
44
+ Label: CODEX_SHARING_LABEL, ProgramArguments: ["/bin/bash", ...args],
45
+ RunAtLoad: true, StartInterval: 15, ProcessType: "Background", WorkingDirectory: paths.home,
46
+ StandardOutPath: resolve(paths.root, "watchdog.log"), StandardErrorPath: resolve(paths.root, "watchdog-error.log"),
47
+ };
48
+ const json = `${paths.root}/job.json`;
49
+ const temporary = `${paths.root}/job.plist`;
50
+ const commandOptions = { encoding: "utf8", timeout: 20_000, maxBuffer: 1024 * 1024 };
51
+ // Identical startup calls must not unload/reload an already healthy job.
52
+ let unchanged = false;
53
+ try {
54
+ const current = await execute("/usr/bin/plutil", ["-convert", "json", "-o", "-", paths.plist], commandOptions);
55
+ unchanged = isDeepStrictEqual(JSON.parse(String(current.stdout)), definition);
56
+ }
57
+ catch { /* First install or older definition. */ }
58
+ if (!unchanged) {
59
+ await writePrivateText(json, JSON.stringify(definition));
60
+ try {
61
+ await execute("/usr/bin/plutil", ["-convert", "xml1", "-o", temporary, json], commandOptions);
62
+ await chmod(temporary, 0o600);
63
+ await mkdir(dirname(paths.plist), { recursive: true });
64
+ let loaded = false;
65
+ try {
66
+ await execute("/bin/launchctl", ["print", paths.target], commandOptions);
67
+ loaded = true;
68
+ }
69
+ catch { /* Absent job. */ }
70
+ if (loaded)
71
+ await execute("/bin/launchctl", ["bootout", paths.target], commandOptions);
72
+ await rename(temporary, paths.plist);
73
+ }
74
+ finally {
75
+ await Promise.all([rm(json, { force: true }), rm(temporary, { force: true })]);
76
+ }
77
+ }
78
+ await execute("/bin/launchctl", ["enable", paths.target], commandOptions);
79
+ let loaded = false;
80
+ try {
81
+ await execute("/bin/launchctl", ["print", paths.target], commandOptions);
82
+ loaded = true;
83
+ }
84
+ catch { /* bootstrap below */ }
85
+ if (!loaded)
86
+ await execute("/bin/launchctl", ["bootstrap", paths.target.slice(0, paths.target.lastIndexOf("/")), paths.plist], commandOptions);
87
+ // Make defaults effective now as well as on the next GUI login.
88
+ await execute("/bin/bash", args, { ...commandOptions, env: environment, cwd: paths.home });
89
+ }
90
+ /** Read process/socket evidence; configuration by itself never proves sharing. */
91
+ export async function inspectCodexSharing(context = {}) {
92
+ const result = { state: "failed", configured: false, autoStart: false, daemonReachable: false, message: "共享连接检查失败,请修复后重试。" };
93
+ if ((context.platform ?? process.platform) !== "darwin")
94
+ return { ...result, state: "unsupported", message: "当前系统暂不支持 Codex 桌面共享配置。" };
95
+ const paths = locations(context);
96
+ result.backendEndpoint = paths.endpoint;
97
+ const execute = context.execute ?? executeFile;
98
+ const commandOptions = { encoding: "utf8", timeout: 4_000, maxBuffer: 4 * 1024 * 1024 };
99
+ try {
100
+ const current = await execute("/bin/launchctl", ["getenv", "CODEX_APP_SERVER_WS_URL"], commandOptions).catch(() => ({ stdout: "" }));
101
+ const [force, home] = await Promise.all([
102
+ execute("/bin/launchctl", ["getenv", "CODEX_APP_SERVER_FORCE_CLI"], commandOptions).catch(() => ({ stdout: "" })),
103
+ execute("/bin/launchctl", ["getenv", "CODEX_HOME"], commandOptions).catch(() => ({ stdout: "" })),
104
+ ]);
105
+ result.configured = String(current.stdout).trim() === paths.endpoint && String(force.stdout).trim() !== "1"
106
+ && (String(home.stdout).trim() || resolve(paths.home, ".codex")) === paths.codexHome;
107
+ try {
108
+ const job = await execute("/bin/launchctl", ["print", paths.target], commandOptions);
109
+ const plist = await execute("/usr/bin/plutil", ["-convert", "json", "-o", "-", paths.plist], commandOptions);
110
+ const definition = JSON.parse(String(plist.stdout));
111
+ await access(resolve(paths.root, "watchdog.sh"), constants.R_OK);
112
+ await access(definition.ProgramArguments?.[2] ?? "", constants.X_OK);
113
+ result.autoStart = Boolean(job.stdout) && definition.RunAtLoad === true && definition.StartInterval === 15
114
+ && definition.ProgramArguments?.[0] === "/bin/bash" && definition.ProgramArguments?.[1] === resolve(paths.root, "watchdog.sh")
115
+ && definition.ProgramArguments?.[3] === paths.codexHome && definition.ProgramArguments?.[4] === paths.endpoint;
116
+ }
117
+ catch { /* Missing or outdated watchdog. */ }
118
+ try {
119
+ await (context.probe ?? (async (endpoint) => { await withDesktopRpc(endpoint, async (request) => { await request("config/read", { includeLayers: false }); }, 4_000); }))(paths.endpoint);
120
+ result.daemonReachable = true;
121
+ }
122
+ catch { /* Keep checking private Desktop evidence. */ }
123
+ const desktop = await (context.inspect ?? inspectCodexDesktop)(paths.environment);
124
+ if (desktop.status !== "running")
125
+ return { ...result, state: desktop.status, message: desktop.status === "stopped"
126
+ ? result.configured && result.autoStart && result.daemonReachable ? "Codex 尚未启动;下次打开时使用共享后台。" : "Codex 尚未启动;请先修复共享配置。" : "尚未安装官方 Codex 桌面端。" };
127
+ const { stdout } = await execute("/bin/ps", ["-axo", "pid=,ppid=,args="], commandOptions);
128
+ const processes = String(stdout).split("\n").flatMap(line => {
129
+ const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
130
+ return match ? [{ pid: Number(match[1]), parent: Number(match[2]), command: match[3] }] : [];
131
+ });
132
+ if (processes.some(p => p.parent === desktop.pid && /\bapp-server\b/.test(p.command) && !/\b(?:proxy|daemon)\b/.test(p.command))) {
133
+ return { ...result, state: "independent", message: "Codex 正在使用独立后台。修复后,请在任务结束时退出并重新打开 Codex。" };
134
+ }
135
+ if (!result.daemonReachable)
136
+ return result;
137
+ const socket = await realpath(paths.socketPath);
138
+ const pids = [desktop.pid, ...processes.filter(p => /\bapp-server\b/.test(p.command) && /\bcodex\b/.test(p.command)).map(p => p.pid)];
139
+ const sockets = await execute("/usr/sbin/lsof", ["-nP", "-a", "-p", [...new Set(pids)].join(","), "-U"], commandOptions);
140
+ const rows = String(sockets.stdout).split("\n").flatMap(line => {
141
+ const match = /^\S+\s+(\d+)\s+\S+\s+\S+\s+unix\s+(0x[\da-f]+)\s+\S+\s+(.*)$/i.exec(line);
142
+ return match ? [{ pid: Number(match[1]), device: match[2], name: match[3].trim() }] : [];
143
+ });
144
+ const devices = new Set(rows.filter(row => row.pid !== desktop.pid && row.name === socket).map(row => row.device));
145
+ if (rows.some(row => row.pid === desktop.pid && row.name.startsWith("->") && devices.has(row.name.slice(2)))) {
146
+ return { ...result, state: "shared", message: "Codex 已连接共享后台。" };
147
+ }
148
+ return { ...result, message: "共享后台可用,但尚未确认 Codex 连接。请修复后,在任务结束时重新打开 Codex。" };
149
+ }
150
+ catch {
151
+ return result;
152
+ }
153
+ }
@@ -7,6 +7,7 @@ import { resolveOpenCodeRuntime, } from "./opencode-runtime-manager.js";
7
7
  import { resolvePlatformRuntime } from "./platform-packages.js";
8
8
  import { findAvailablePort } from "./port.js";
9
9
  import { ProcessSupervisor } from "./process-supervisor.js";
10
+ import { configureCodexSharing } from "./codex-sharing.js";
10
11
  import { withCliProgress } from "../bin/cli-progress.mjs";
11
12
  export class RuntimeManager {
12
13
  supervisor;
@@ -24,6 +25,14 @@ export class RuntimeManager {
24
25
  const dataDir = resolve(options.dataDir);
25
26
  await Promise.all(["data", "bin", "cache", "logs"].map((name) => mkdir(resolve(dataDir, name), { recursive: true })));
26
27
  const codexEnvironment = await withCliProgress("正在准备 Codex 组件", () => prepareCodexRuntimeEnvironment(process.env, { dataDir, target }, this.codexResolver, this.report));
28
+ if (process.platform === "darwin" && codexEnvironment.AGENT_CODEX_BIN) {
29
+ try {
30
+ await configureCodexSharing(options, { environment: codexEnvironment });
31
+ }
32
+ catch (error) {
33
+ this.report(`Codex 共享启动配置未完成,可在电脑菜单中修复:${error instanceof Error ? error.message : error}`);
34
+ }
35
+ }
27
36
  const childEnvironment = await withCliProgress("正在准备 OpenCode 组件", () => prepareOpenCodeRuntimeEnvironment(codexEnvironment, { dataDir, target }, this.opencodeResolver, this.report));
28
37
  const { runtimeRoot } = resolvePlatformRuntime(target);
29
38
  await repairNativeRuntimePermissions(runtimeRoot);
@@ -8,6 +8,7 @@ import { MacLaunchAgent } from "./macos-launch-agent.js";
8
8
  import { WindowsTaskService } from "./windows-task-service.js";
9
9
  import { assertSupportedNodeVersion } from "../../bin/runtime-policy.mjs";
10
10
  import { cleanupOldLaunchers } from "./launcher-cleanup.js";
11
+ import { configureCodexSharing } from "../codex-sharing.js";
11
12
  import { setupCodexDesktop } from "../codex-desktop-setup.js";
12
13
  import { authorizeRemoteDesktop } from "../desktop-authorization.js";
13
14
  import { withCliProgress } from "../../bin/cli-progress.mjs";
@@ -60,6 +61,14 @@ export async function runServiceCommand(options, context) {
60
61
  dataDir: options.dataDir,
61
62
  installedAt: now.toISOString(),
62
63
  };
64
+ if (platform === "darwin" && codexPath) {
65
+ try {
66
+ await (context.configureSharing ?? configureCodexSharing)(options, { homeDir: context.homeDir, environment: { ...environment, PATH: environmentPath, AGENT_CODEX_BIN: codexPath } });
67
+ }
68
+ catch (error) {
69
+ log(`Codex 默认共享配置未完成,可在电脑菜单中修复:${error instanceof Error ? error.message : error}`);
70
+ }
71
+ }
63
72
  const { definition, state } = await withCliProgress("正在安装后台服务,等待服务和手机访问连接就绪", () => controller.install(config));
64
73
  log(`✓ AgentRoam service installed: ${definition}`);
65
74
  if (state?.localUrl && state.status !== "stopped")
@@ -0,0 +1,16 @@
1
+ #!/bin/bash
2
+ # Copied to a stable private directory: upgrades must not invalidate this job.
3
+ set -euo pipefail
4
+ codex_bin="$1"
5
+ codex_home="$2"
6
+ shared_url="$3"
7
+ export CODEX_HOME="$codex_home"
8
+ export PATH="$4"
9
+ export CODEX_APP_SERVER_FORCE_CLI=0
10
+ export CODEX_APP_SERVER_WS_URL="$shared_url"
11
+ /bin/launchctl setenv CODEX_APP_SERVER_WS_URL "$shared_url"
12
+ /bin/launchctl setenv CODEX_APP_SERVER_FORCE_CLI 0
13
+ /bin/launchctl setenv CODEX_HOME "$codex_home"
14
+ # daemon start is idempotent; never stop or replace a live daemon.
15
+ cd "$5"
16
+ exec "$codex_bin" app-server daemon start
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentroam",
3
- "version": "0.2.0-preview.46",
3
+ "version": "0.2.0-preview.48",
4
4
  "description": "Zero-install mobile remote terminal and file console",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,12 +27,12 @@
27
27
  "ws": "^8.21.3"
28
28
  },
29
29
  "optionalDependencies": {
30
- "agentroam-runtime-darwin-arm64": "0.2.0-preview.46",
31
- "agentroam-runtime-win32-x64": "0.2.0-preview.46",
30
+ "agentroam-runtime-darwin-arm64": "0.2.0-preview.48",
31
+ "agentroam-runtime-win32-x64": "0.2.0-preview.48",
32
32
  "agentroam-cloudflared-darwin-arm64": "0.2.0-preview.39",
33
33
  "agentroam-cloudflared-win32-x64": "0.2.0-preview.39",
34
- "agentroam-tui-darwin-arm64": "0.2.0-preview.46",
35
- "@caoqu/agentroam-tui-win32-x64": "0.2.0-preview.46"
34
+ "agentroam-tui-darwin-arm64": "0.2.0-preview.47",
35
+ "@caoqu/agentroam-tui-win32-x64": "0.2.0-preview.47"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^22.0.0",