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 +10 -2
- package/dist/cli.js +2 -0
- package/dist/codex-desktop-command.js +8 -0
- package/dist/codex-desktop-setup.js +3 -0
- package/dist/codex-runtime-manager.js +39 -18
- package/dist/codex-sharing.js +153 -0
- package/dist/runtime-manager.js +9 -0
- package/dist/service/service-command.js +9 -0
- package/install/codex-shared-watchdog.sh +16 -0
- package/package.json +5 -5
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
|
-
|
|
8
|
-
|
|
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@${
|
|
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
|
|
54
|
-
const managed = await
|
|
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 ${
|
|
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 =
|
|
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,
|
|
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
|
|
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, `.${
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 ${
|
|
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,
|
|
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
|
|
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
|
+
}
|
package/dist/runtime-manager.js
CHANGED
|
@@ -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.
|
|
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.
|
|
31
|
-
"agentroam-runtime-win32-x64": "0.2.0-preview.
|
|
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.
|
|
35
|
-
"@caoqu/agentroam-tui-win32-x64": "0.2.0-preview.
|
|
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",
|