agentroam 0.2.0-preview.35 → 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.
- package/dist/args.js +16 -2
- package/dist/cli.js +15 -0
- package/dist/codex-desktop-command.js +56 -0
- package/dist/codex-desktop-idle-guard.js +15 -0
- package/dist/codex-desktop-preflight.js +57 -0
- package/dist/codex-desktop-probe.js +147 -0
- package/dist/codex-desktop-setup.js +119 -0
- package/dist/codex-desktop-status.js +85 -0
- package/dist/service/launcher-cleanup.js +19 -1
- package/dist/service/service-command.js +6 -0
- package/install/install-agentroam.sh +7 -1
- package/install/install-manifest.json +2 -2
- package/install/start-codex-desktop-shared.sh +149 -0
- package/package.json +7 -7
package/dist/args.js
CHANGED
|
@@ -3,7 +3,7 @@ import { resolve } from "node:path";
|
|
|
3
3
|
export function parseArgs(argv) {
|
|
4
4
|
const values = [...argv];
|
|
5
5
|
let command = "start";
|
|
6
|
-
if (values[0] && ["start", "doctor", "version", "service", "unlock-service", "update", "update-worker", "pair", "devices", "revoke", "approvals", "approve", "deny", "lock", "unlock", "audit"].includes(values[0])) {
|
|
6
|
+
if (values[0] && ["start", "doctor", "version", "service", "unlock-service", "update", "update-worker", "pair", "devices", "revoke", "approvals", "approve", "deny", "lock", "unlock", "audit", "codex-desktop"].includes(values[0])) {
|
|
7
7
|
command = values.shift();
|
|
8
8
|
}
|
|
9
9
|
let serviceAction = null;
|
|
@@ -70,13 +70,24 @@ export function parseArgs(argv) {
|
|
|
70
70
|
};
|
|
71
71
|
for (let index = 0; index < values.length && command !== "update-worker"; index++) {
|
|
72
72
|
const arg = values[index];
|
|
73
|
+
if (command === "codex-desktop" && !["--restart", "--dry-run", "--setup", "--data-dir", "--help", "-h"].includes(arg)) {
|
|
74
|
+
throw cliError("codex-desktop accepts only --setup, --restart, --dry-run and --data-dir");
|
|
75
|
+
}
|
|
73
76
|
const next = () => {
|
|
74
77
|
const value = values[++index];
|
|
75
78
|
if (!value)
|
|
76
79
|
throw cliError(`missing value for ${arg}`);
|
|
77
80
|
return value;
|
|
78
81
|
};
|
|
79
|
-
if (arg === "--
|
|
82
|
+
if (arg === "--restart" && command === "codex-desktop")
|
|
83
|
+
options.desktopRestart = true;
|
|
84
|
+
else if (arg === "--dry-run" && command === "codex-desktop")
|
|
85
|
+
options.desktopDryRun = true;
|
|
86
|
+
else if (arg === "--setup" && command === "codex-desktop")
|
|
87
|
+
options.desktopSetup = true;
|
|
88
|
+
else if (arg === "--setup-codex-desktop" && command === "service" && serviceAction === "install")
|
|
89
|
+
options.desktopSetup = true;
|
|
90
|
+
else if (arg === "--url" && command === "pair")
|
|
80
91
|
options.pairingUrl = next();
|
|
81
92
|
else if (arg === "--root")
|
|
82
93
|
options.roots.push(resolve(next()));
|
|
@@ -112,6 +123,9 @@ export function parseArgs(argv) {
|
|
|
112
123
|
}
|
|
113
124
|
if (command === "approve" && !options.approvalPhrase)
|
|
114
125
|
throw cliError("approve requires --phrase matching the phone");
|
|
126
|
+
if (command === "codex-desktop" && options.desktopSetup && (options.desktopRestart || options.desktopDryRun)) {
|
|
127
|
+
throw cliError("--setup cannot be combined with --restart or --dry-run");
|
|
128
|
+
}
|
|
115
129
|
if (command === "update" && updateVersion && !/^\d+\.\d+\.\d+$/.test(updateVersion)) {
|
|
116
130
|
throw cliError("update version must be an exact stable X.Y.Z version");
|
|
117
131
|
}
|
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,8 @@ import { runUpdateWorker } from "./update/update-worker.js";
|
|
|
21
21
|
import { lockInstanceStartup, stopPreviousInstances } from "./instance-takeover.js";
|
|
22
22
|
import { runUnlockServiceCommand } from "./unlock-service.js";
|
|
23
23
|
import { createCliErrorLog } from "./error-log.js";
|
|
24
|
+
import { runCodexDesktopCommand } from "./codex-desktop-command.js";
|
|
25
|
+
import { setupCodexDesktop } from "./codex-desktop-setup.js";
|
|
24
26
|
const VERSION = AGENTROAM_VERSION;
|
|
25
27
|
export async function main(argv) {
|
|
26
28
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
@@ -39,6 +41,10 @@ export async function main(argv) {
|
|
|
39
41
|
agentroam revoke --all 全部退出并取消待用配对码
|
|
40
42
|
agentroam service install|start|stop|restart|status|url|logs|uninstall
|
|
41
43
|
agentroam unlock-service install|uninstall|status 管理 Windows 远程解锁服务(安装时弹出 UAC)
|
|
44
|
+
agentroam codex-desktop [--dry-run] 通过共享后端启动官方 Codex 桌面端(macOS)
|
|
45
|
+
agentroam codex-desktop --restart 退出并重新连接官方桌面端,会中断它的全部运行会话
|
|
46
|
+
agentroam codex-desktop --setup 空闲桌面自动重新连接;有任务或状态未知时询问是否重启
|
|
47
|
+
agentroam service install --setup-codex-desktop 扫码前配置官方桌面共享连接(macOS)
|
|
42
48
|
设备命令支持 --data-dir PATH;配对码有效期 5 分钟,设备授权有效期 30 天。`);
|
|
43
49
|
return;
|
|
44
50
|
}
|
|
@@ -92,6 +98,15 @@ export async function main(argv) {
|
|
|
92
98
|
return;
|
|
93
99
|
}
|
|
94
100
|
const target = detectPlatform();
|
|
101
|
+
if (options.command === "codex-desktop") {
|
|
102
|
+
if (!argv.includes("--data-dir"))
|
|
103
|
+
options.dataDir = await installedDataDir(options.dataDir);
|
|
104
|
+
if (options.desktopSetup)
|
|
105
|
+
await setupCodexDesktop(options);
|
|
106
|
+
else
|
|
107
|
+
await runCodexDesktopCommand(options);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
95
110
|
if (options.command === "unlock-service") {
|
|
96
111
|
if (target !== "windows-amd64")
|
|
97
112
|
throw new Error("远程解锁服务仅支持 Windows 10/11 x64");
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { resolveCodexRuntime } from "./codex-runtime-manager.js";
|
|
5
|
+
import { readServiceConfig, resolveServicePaths } from "./service/service-files.js";
|
|
6
|
+
import { detectPlatform } from "./platform.js";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const scriptPath = fileURLToPath(new URL("../install/start-codex-desktop-shared.sh", import.meta.url));
|
|
9
|
+
export async function runCodexDesktopCommand(options, context = {}) {
|
|
10
|
+
const platform = context.platform ?? process.platform;
|
|
11
|
+
if (platform !== "darwin")
|
|
12
|
+
throw new Error("官方 Codex 桌面共享后端入口目前仅支持 macOS");
|
|
13
|
+
const log = context.log ?? console.log;
|
|
14
|
+
const environment = { ...(context.environment ?? process.env) };
|
|
15
|
+
const service = await (context.readConfig ?? (() => readServiceConfig(resolveServicePaths())))();
|
|
16
|
+
if (service?.dataDir === options.dataDir) {
|
|
17
|
+
if (!environment.AGENT_CODEX_BIN && service.codexPath)
|
|
18
|
+
environment.AGENT_CODEX_BIN = service.codexPath;
|
|
19
|
+
if (!environment.CODEX_HOME && service.codexHome)
|
|
20
|
+
environment.CODEX_HOME = service.codexHome;
|
|
21
|
+
}
|
|
22
|
+
// A dry run reads the installed configuration and never installs a runtime.
|
|
23
|
+
if (!options.desktopDryRun) {
|
|
24
|
+
const codex = await (context.codexResolver ?? resolveCodexRuntime)({
|
|
25
|
+
dataDir: options.dataDir,
|
|
26
|
+
target: detectPlatform(platform, process.arch, process.versions.node),
|
|
27
|
+
environment: { ...environment },
|
|
28
|
+
platform,
|
|
29
|
+
nodeExecutable: process.execPath,
|
|
30
|
+
onProgress: (message) => log(`… ${message}`),
|
|
31
|
+
});
|
|
32
|
+
environment.AGENT_CODEX_BIN = codex.executable;
|
|
33
|
+
}
|
|
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
|
+
}
|
|
39
|
+
if (options.desktopRestart)
|
|
40
|
+
args.push("--restart");
|
|
41
|
+
if (options.desktopDryRun)
|
|
42
|
+
args.push("--dry-run");
|
|
43
|
+
try {
|
|
44
|
+
const { stdout, stderr } = await (context.execute ?? execFileAsync)("/bin/bash", args, {
|
|
45
|
+
env: environment, encoding: "utf8", timeout: 60_000, maxBuffer: 1024 * 1024,
|
|
46
|
+
});
|
|
47
|
+
if (stdout.trim())
|
|
48
|
+
log(stdout.trimEnd());
|
|
49
|
+
if (stderr.trim())
|
|
50
|
+
log(stderr.trimEnd());
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const detail = error;
|
|
54
|
+
throw new Error(detail.stderr?.trim() || detail.message, { cause: error });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { inspectCodexDesktop } from "./codex-desktop-status.js";
|
|
2
|
+
// Invoked only by the packaged launcher, after daemon preflight and immediately
|
|
3
|
+
// before graceful quit. Runtime installation may have taken minutes since setup.
|
|
4
|
+
try {
|
|
5
|
+
const snapshot = await inspectCodexDesktop(process.env);
|
|
6
|
+
if (snapshot.status !== "running" || String(snapshot.pid) !== process.env.AGENTROAM_CODEX_DESKTOP_EXPECTED_PID
|
|
7
|
+
|| snapshot.activity.state !== "idle") {
|
|
8
|
+
console.error("Desktop is no longer confirmed idle; no automatic restart attempted. Run agentroam codex-desktop --setup to choose again.");
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
console.error("Desktop idle check failed; no automatic restart attempted.");
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
}
|
|
9
|
+
/** Read live state only. Never resume, subscribe, interrupt or unload a thread. */
|
|
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) {
|
|
22
|
+
let dispatcher;
|
|
23
|
+
let socket;
|
|
24
|
+
let timer;
|
|
25
|
+
let rejectPending;
|
|
26
|
+
try {
|
|
27
|
+
const address = new URL(endpoint);
|
|
28
|
+
let url = endpoint;
|
|
29
|
+
if (address.protocol === "ws+unix:") {
|
|
30
|
+
const separator = address.pathname.indexOf(":");
|
|
31
|
+
if (address.hostname !== "localhost" || separator < 1)
|
|
32
|
+
throw new Error("invalid local socket");
|
|
33
|
+
dispatcher = new Agent({ connect: { socketPath: decodeURIComponent(address.pathname.slice(0, separator)) } });
|
|
34
|
+
url = `ws://localhost${address.pathname.slice(separator + 1)}`;
|
|
35
|
+
}
|
|
36
|
+
else if (!(["ws:", "wss:"].includes(address.protocol)
|
|
37
|
+
&& ["127.0.0.1", "localhost", "[::1]"].includes(address.hostname))) {
|
|
38
|
+
throw new Error("not a local Desktop endpoint");
|
|
39
|
+
}
|
|
40
|
+
if (address.username || address.password || address.search || address.hash)
|
|
41
|
+
throw new Error("invalid endpoint");
|
|
42
|
+
socket = new WebSocket(url, dispatcher ? { dispatcher } : undefined);
|
|
43
|
+
const connection = socket;
|
|
44
|
+
let nextId = 0;
|
|
45
|
+
let changedToActive = false;
|
|
46
|
+
const pending = new Map();
|
|
47
|
+
const fail = () => {
|
|
48
|
+
for (const item of pending.values())
|
|
49
|
+
item.reject(new Error("Desktop probe disconnected"));
|
|
50
|
+
pending.clear();
|
|
51
|
+
};
|
|
52
|
+
rejectPending = fail;
|
|
53
|
+
const opened = new Promise((resolve, reject) => {
|
|
54
|
+
connection.addEventListener("open", () => resolve(), { once: true });
|
|
55
|
+
connection.addEventListener("error", () => reject(new Error("Desktop probe connection failed")), { once: true });
|
|
56
|
+
connection.addEventListener("close", () => reject(new Error("Desktop probe disconnected")), { once: true });
|
|
57
|
+
});
|
|
58
|
+
connection.addEventListener("error", fail);
|
|
59
|
+
connection.addEventListener("close", fail);
|
|
60
|
+
connection.addEventListener("message", (event) => {
|
|
61
|
+
try {
|
|
62
|
+
const message = JSON.parse(String(event.data));
|
|
63
|
+
if (message.method === "thread/status/changed" && message.params?.status?.type === "active")
|
|
64
|
+
changedToActive = true;
|
|
65
|
+
const item = pending.get(message.id);
|
|
66
|
+
if (!item)
|
|
67
|
+
return;
|
|
68
|
+
pending.delete(message.id);
|
|
69
|
+
if (message.error)
|
|
70
|
+
item.reject(new DesktopProbeRpcError(message.error.code, String(message.error.message ?? "Desktop probe RPC failed")));
|
|
71
|
+
else
|
|
72
|
+
item.resolve(message.result);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
fail();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
const request = (method, params) => new Promise((resolve, reject) => {
|
|
79
|
+
const id = ++nextId;
|
|
80
|
+
pending.set(id, { resolve, reject });
|
|
81
|
+
try {
|
|
82
|
+
connection.send(JSON.stringify({ id, method, params }));
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
pending.delete(id);
|
|
86
|
+
reject(error);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
const run = async () => {
|
|
90
|
+
await opened;
|
|
91
|
+
await request("initialize", { clientInfo: { name: "agentroam_desktop_setup", version: "1" }, capabilities: { experimentalApi: true } });
|
|
92
|
+
connection.send(JSON.stringify({ method: "initialized", params: {} }));
|
|
93
|
+
return operation(request, () => changedToActive);
|
|
94
|
+
};
|
|
95
|
+
return await Promise.race([
|
|
96
|
+
run(),
|
|
97
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("Desktop probe timed out")), timeoutMs); }),
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (timer)
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
rejectPending?.();
|
|
104
|
+
socket?.close();
|
|
105
|
+
await dispatcher?.destroy();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export async function readDesktopActivity(request) {
|
|
109
|
+
const listLoaded = async () => {
|
|
110
|
+
const ids = new Set();
|
|
111
|
+
const cursors = new Set();
|
|
112
|
+
let cursor = null;
|
|
113
|
+
for (let pageIndex = 0; pageIndex < 20; pageIndex++) {
|
|
114
|
+
const page = await request("thread/loaded/list", { cursor, limit: 100 });
|
|
115
|
+
if (!Array.isArray(page?.data) || page.data.some((id) => typeof id !== "string" || !id))
|
|
116
|
+
throw new Error("incomplete loaded thread list");
|
|
117
|
+
for (const id of page.data)
|
|
118
|
+
ids.add(id);
|
|
119
|
+
if (page.nextCursor === null || page.nextCursor === undefined)
|
|
120
|
+
return [...ids].sort();
|
|
121
|
+
if (typeof page.nextCursor !== "string" || !page.nextCursor || cursors.has(page.nextCursor))
|
|
122
|
+
throw new Error("invalid cursor");
|
|
123
|
+
cursors.add(page.nextCursor);
|
|
124
|
+
cursor = page.nextCursor;
|
|
125
|
+
}
|
|
126
|
+
throw new Error("Desktop thread list exceeded probe limit");
|
|
127
|
+
};
|
|
128
|
+
const ids = await listLoaded();
|
|
129
|
+
let activeCount = 0;
|
|
130
|
+
let hasUnknownStatus = false;
|
|
131
|
+
for (const threadId of ids) {
|
|
132
|
+
const result = await request("thread/read", { threadId, includeTurns: false });
|
|
133
|
+
if (result?.thread?.id !== threadId)
|
|
134
|
+
throw new Error("Desktop thread identity changed");
|
|
135
|
+
const status = result.thread.status?.type;
|
|
136
|
+
if (status === "active")
|
|
137
|
+
activeCount++;
|
|
138
|
+
else if (status !== "idle")
|
|
139
|
+
hasUnknownStatus = true;
|
|
140
|
+
}
|
|
141
|
+
if (activeCount)
|
|
142
|
+
return { state: "busy", activeCount };
|
|
143
|
+
if (hasUnknownStatus || JSON.stringify(ids) !== JSON.stringify(await listLoaded())) {
|
|
144
|
+
return { state: "unknown", reason: "桌面任务列表发生变化或包含无法确认的状态" };
|
|
145
|
+
}
|
|
146
|
+
return { state: "idle" };
|
|
147
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { openSync, closeSync } from "node:fs";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { ReadStream, WriteStream } from "node:tty";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { runCodexDesktopCommand } from "./codex-desktop-command.js";
|
|
6
|
+
import { inspectCodexDesktop } from "./codex-desktop-status.js";
|
|
7
|
+
export async function setupCodexDesktop(options, context = {}) {
|
|
8
|
+
if ((context.platform ?? process.platform) !== "darwin")
|
|
9
|
+
return;
|
|
10
|
+
const log = context.log ?? console.log;
|
|
11
|
+
const environment = { ...(context.environment ?? process.env) };
|
|
12
|
+
if (context.service?.dataDir === options.dataDir) {
|
|
13
|
+
if (!environment.CODEX_HOME && context.service.codexHome)
|
|
14
|
+
environment.CODEX_HOME = context.service.codexHome;
|
|
15
|
+
if (!environment.AGENT_CODEX_BIN && context.service.codexPath)
|
|
16
|
+
environment.AGENT_CODEX_BIN = context.service.codexPath;
|
|
17
|
+
}
|
|
18
|
+
const inspect = context.inspect ?? inspectCodexDesktop;
|
|
19
|
+
const deferred = () => log("已保留当前 Codex 桌面端。扫码仍可连接 AgentRoam;原桌面会话暂未共享,可稍后执行 agentroam codex-desktop --setup。");
|
|
20
|
+
try {
|
|
21
|
+
let snapshot = await inspect(environment);
|
|
22
|
+
if (snapshot.status === "not-installed") {
|
|
23
|
+
log("未安装官方 Codex 桌面端,继续生成扫码连接入口。");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const original = snapshot;
|
|
27
|
+
let restartConfirmed = false;
|
|
28
|
+
if (snapshot.status === "running" && snapshot.activity.state === "idle") {
|
|
29
|
+
// Read every loaded thread again immediately before choosing automatic restart.
|
|
30
|
+
snapshot = await inspect(environment);
|
|
31
|
+
if (!sameDesktop(original, snapshot)) {
|
|
32
|
+
log("桌面进程已变化,本次保留桌面。");
|
|
33
|
+
deferred();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (snapshot.status === "running" && snapshot.activity.state !== "idle") {
|
|
38
|
+
const reason = snapshot.activity.state === "busy"
|
|
39
|
+
? `官方 Codex 桌面端当前有 ${snapshot.activity.activeCount} 个运行中的会话。`
|
|
40
|
+
: "无法可靠确认官方 Codex 桌面端的全部任务已结束。";
|
|
41
|
+
log(reason);
|
|
42
|
+
const answer = await (context.confirm ?? confirmDesktopRestart)("现在重启桌面端以共享会话?重启可能中断桌面端的所有运行任务;选择保留则暂不能共享原桌面会话 [y/N]:");
|
|
43
|
+
if (!answer || !/^(?:y|yes)$/i.test(answer.trim())) {
|
|
44
|
+
deferred();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
restartConfirmed = true;
|
|
48
|
+
const checked = await inspect(environment);
|
|
49
|
+
if (!sameDesktop(snapshot, checked)) {
|
|
50
|
+
log("确认后桌面进程已变化,本次保留桌面。");
|
|
51
|
+
deferred();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
snapshot = checked;
|
|
55
|
+
}
|
|
56
|
+
if (snapshot.status !== "running" && snapshot.status !== "stopped")
|
|
57
|
+
return;
|
|
58
|
+
environment.CODEX_DESKTOP_APP = snapshot.appPath;
|
|
59
|
+
environment.AGENTROAM_CODEX_DESKTOP_EXPECTED_PID = snapshot.status === "running" ? String(snapshot.pid) : "0";
|
|
60
|
+
if (snapshot.status === "running" && !restartConfirmed) {
|
|
61
|
+
environment.AGENTROAM_CODEX_DESKTOP_IDLE_GUARD = fileURLToPath(new URL("./codex-desktop-idle-guard.js", import.meta.url));
|
|
62
|
+
environment.AGENTROAM_CODEX_DESKTOP_GUARD_NODE = process.execPath;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
delete environment.AGENTROAM_CODEX_DESKTOP_IDLE_GUARD;
|
|
66
|
+
delete environment.AGENTROAM_CODEX_DESKTOP_GUARD_NODE;
|
|
67
|
+
}
|
|
68
|
+
const launchOptions = { ...options, command: "codex-desktop", desktopSetup: false,
|
|
69
|
+
desktopDryRun: false, desktopRestart: snapshot.status === "running" };
|
|
70
|
+
log(snapshot.status === "stopped" ? "正在启动官方 Codex 桌面端并连接共享后端…" : "正在重新连接官方 Codex 桌面端的共享后端…");
|
|
71
|
+
await (context.launch ?? ((args, env) => runCodexDesktopCommand(args, { environment: env, log })))(launchOptions, environment);
|
|
72
|
+
log("已尝试桌面连接,随后显示扫码入口。AgentRoam 将根据实际连接结果,依次使用桌面、shared、standalone 后端。");
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
log(`桌面共享连接未完成:${error instanceof Error ? error.message : error}`);
|
|
76
|
+
log("AgentRoam 安装继续,扫码可连接;后端按桌面 → shared → standalone 回退。原桌面会话暂未确认共享,可稍后执行 agentroam codex-desktop --setup 重试。");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function sameDesktop(previous, current) {
|
|
80
|
+
return previous.status === "running" && current.status === "running"
|
|
81
|
+
&& previous.pid === current.pid && previous.appPath === current.appPath;
|
|
82
|
+
}
|
|
83
|
+
/** curl | sh owns stdin. Only the controlling terminal can authorize restart. */
|
|
84
|
+
export async function confirmDesktopRestart(question) {
|
|
85
|
+
let inputFd;
|
|
86
|
+
let outputFd;
|
|
87
|
+
let input;
|
|
88
|
+
let output;
|
|
89
|
+
let reader;
|
|
90
|
+
try {
|
|
91
|
+
inputFd = openSync("/dev/tty", "r");
|
|
92
|
+
outputFd = openSync("/dev/tty", "w");
|
|
93
|
+
input = new ReadStream(inputFd);
|
|
94
|
+
output = new WriteStream(outputFd);
|
|
95
|
+
reader = createInterface({ input, output, terminal: true });
|
|
96
|
+
const prompt = reader;
|
|
97
|
+
return await new Promise((resolve) => {
|
|
98
|
+
prompt.once("close", () => resolve(null));
|
|
99
|
+
prompt.once("SIGINT", () => resolve(null));
|
|
100
|
+
input.once("error", () => resolve(null));
|
|
101
|
+
output.once("error", () => resolve(null));
|
|
102
|
+
prompt.question(question, resolve);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
reader?.close();
|
|
110
|
+
if (input)
|
|
111
|
+
input.destroy();
|
|
112
|
+
else if (inputFd !== undefined)
|
|
113
|
+
closeSync(inputFd);
|
|
114
|
+
if (output)
|
|
115
|
+
output.destroy();
|
|
116
|
+
else if (outputFd !== undefined)
|
|
117
|
+
closeSync(outputFd);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { basename, join } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { probeDesktopActivity } from "./codex-desktop-probe.js";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
export async function inspectCodexDesktop(environment, context = {}) {
|
|
9
|
+
const executableExists = context.executableExists ?? (async (path) => {
|
|
10
|
+
try {
|
|
11
|
+
await access(path, constants.X_OK);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
let appPath = environment.CODEX_DESKTOP_APP;
|
|
19
|
+
if (!appPath)
|
|
20
|
+
appPath = await executableExists("/Applications/ChatGPT.app") ? "/Applications/ChatGPT.app" : "/Applications/Codex.app";
|
|
21
|
+
const binary = join(appPath, "Contents/MacOS", basename(appPath, ".app"));
|
|
22
|
+
if (!await executableExists(binary))
|
|
23
|
+
return { status: "not-installed" };
|
|
24
|
+
const execute = context.execute ?? execFileAsync;
|
|
25
|
+
const commandOptions = { encoding: "utf8", timeout: 1_500, maxBuffer: 2 * 1024 * 1024 };
|
|
26
|
+
const { stdout } = await execute("/bin/ps", ["-axo", "pid=,ppid=,args="], commandOptions);
|
|
27
|
+
const processes = String(stdout).split("\n").flatMap((line) => {
|
|
28
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
|
|
29
|
+
return match ? [{ pid: Number(match[1]), parentPid: Number(match[2]), command: match[3] }] : [];
|
|
30
|
+
});
|
|
31
|
+
const desktops = processes.filter(({ command }) => command === binary || command.startsWith(`${binary} `));
|
|
32
|
+
if (!desktops.length)
|
|
33
|
+
return { status: "stopped", appPath };
|
|
34
|
+
const pid = desktops[0].pid;
|
|
35
|
+
const unknown = (reason) => ({ status: "running", appPath, pid, activity: { state: "unknown", reason } });
|
|
36
|
+
if (desktops.length !== 1)
|
|
37
|
+
return unknown("检测到多个官方桌面进程,无法完整确认任务状态");
|
|
38
|
+
const servers = processes.filter((process) => process.parentPid === pid && /^(?:.*\/)?codex\s.*\bapp-server(?:\s|$)/.test(process.command));
|
|
39
|
+
if (servers.length !== 1)
|
|
40
|
+
return unknown("桌面端未提供可完整查询的独立后端");
|
|
41
|
+
const server = servers[0];
|
|
42
|
+
const listenUrl = /(?:^|\s)--listen(?:=|\s+)(unix:\/\/.*?|ws:\/\/\S+)(?=\s+-|$)/.exec(server.command)?.[1];
|
|
43
|
+
if (!listenUrl || /\bapp-server\s+(?:proxy|daemon)\b/.test(server.command)) {
|
|
44
|
+
return unknown("桌面端使用私有后端,无法可靠判断全部会话是否空闲");
|
|
45
|
+
}
|
|
46
|
+
let endpoint = null;
|
|
47
|
+
try {
|
|
48
|
+
if (listenUrl.startsWith("ws://")) {
|
|
49
|
+
const address = new URL(listenUrl);
|
|
50
|
+
if (["0.0.0.0", "[::]"].includes(address.hostname))
|
|
51
|
+
address.hostname = "localhost";
|
|
52
|
+
if (!["localhost", "127.0.0.1", "[::1]"].includes(address.hostname) || address.port === "0")
|
|
53
|
+
throw new Error("not local");
|
|
54
|
+
const result = await execute("/usr/sbin/lsof", ["-nP", "-a", "-p", String(server.pid), "-iTCP", "-sTCP:LISTEN", "-Ftn"], commandOptions);
|
|
55
|
+
const port = address.port || "80";
|
|
56
|
+
if (String(result.stdout).split("\n").some((line) => /^n(?:127\.0\.0\.1|localhost|\[::1\]|\*|0\.0\.0\.0|\[::\]):(\d+)$/.exec(line)?.[1] === port))
|
|
57
|
+
endpoint = address.href;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const result = await execute("/usr/sbin/lsof", ["-nP", "-a", "-p", String(server.pid), "-U", "-Ftn"], commandOptions);
|
|
61
|
+
const requested = listenUrl.slice("unix://".length);
|
|
62
|
+
const paths = [];
|
|
63
|
+
let unix = false;
|
|
64
|
+
for (const line of String(result.stdout).split("\n")) {
|
|
65
|
+
if (line.startsWith("t"))
|
|
66
|
+
unix = line === "tunix";
|
|
67
|
+
if (!unix || !line.startsWith("n/"))
|
|
68
|
+
continue;
|
|
69
|
+
const path = line.slice(1).split(" type=")[0];
|
|
70
|
+
if (path.includes(" -> ") || path.includes(":"))
|
|
71
|
+
continue;
|
|
72
|
+
if (!requested || path === requested)
|
|
73
|
+
paths.push(path);
|
|
74
|
+
}
|
|
75
|
+
if (paths.length === 1)
|
|
76
|
+
endpoint = `ws+unix://localhost${encodeURI(paths[0])}:/rpc`;
|
|
77
|
+
}
|
|
78
|
+
if (!endpoint)
|
|
79
|
+
return unknown("未找到归属桌面端的可连接监听地址");
|
|
80
|
+
return { status: "running", appPath, pid, activity: await (context.probe ?? probeDesktopActivity)(endpoint) };
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return unknown("无法完整查询桌面端的实时任务状态");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -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
|
}
|
|
@@ -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 { setupCodexDesktop } from "../codex-desktop-setup.js";
|
|
11
12
|
export async function runServiceCommand(options, context) {
|
|
12
13
|
const platform = context.platform ?? process.platform;
|
|
13
14
|
const log = context.log ?? console.log;
|
|
@@ -59,10 +60,15 @@ export async function runServiceCommand(options, context) {
|
|
|
59
60
|
};
|
|
60
61
|
const { definition, state } = await controller.install(config);
|
|
61
62
|
log(`✓ AgentRoam service installed: ${definition}`);
|
|
63
|
+
if (platform === "darwin" && !options.desktopSetup)
|
|
64
|
+
log("官方 Codex 桌面共享后端:agentroam codex-desktop --setup;预演:agentroam codex-desktop --dry-run。");
|
|
62
65
|
if (state?.status === "ready" && state.version === config.version)
|
|
63
66
|
await cleanupOldLaunchers(config, log);
|
|
64
67
|
if (state?.localUrl && state.status !== "stopped")
|
|
65
68
|
printLocalDesktopUrl(state.localUrl, log, platform);
|
|
69
|
+
if (options.desktopSetup && platform === "darwin") {
|
|
70
|
+
await (context.desktopSetup ?? setupCodexDesktop)(options, { platform, environment, service: config, log });
|
|
71
|
+
}
|
|
66
72
|
if (state?.status === "ready" && state.accessUrl) {
|
|
67
73
|
log(`Open: ${state.accessUrl}`);
|
|
68
74
|
await (context.pairingPrinter ?? printPairingCode)(options.dataDir, log, { accessUrl: state.accessUrl, qr: options.qr, interactive: stdoutIsTTY });
|
|
@@ -259,7 +259,13 @@ fi
|
|
|
259
259
|
if [ "${AGENTROAM_INSTALL_SKIP_SERVICE:-}" = "1" ]; then
|
|
260
260
|
printf 'AgentRoam service registration skipped for isolated verification.\n'
|
|
261
261
|
else
|
|
262
|
-
"$
|
|
262
|
+
codex_setup_module="$(dirname "$entry")/../dist/codex-desktop-setup.js"
|
|
263
|
+
if [ -f "$codex_setup_module" ]; then
|
|
264
|
+
"$NODE_BIN" "$entry" service install --root "$SERVICE_ROOT" --data-dir "$DATA_DIR" --setup-codex-desktop
|
|
265
|
+
else
|
|
266
|
+
printf '当前发布的 CLI 尚未包含官方 Codex 桌面共享配置;扫码可连接 AgentRoam,原桌面会话暂未共享,请升级 CLI 后重试。\n'
|
|
267
|
+
"$NODE_BIN" "$entry" service install --root "$SERVICE_ROOT" --data-dir "$DATA_DIR"
|
|
268
|
+
fi
|
|
263
269
|
fi
|
|
264
270
|
printf '\nAgentRoam %s installed: %s\n' "$AGENTROAM_VERSION" "$WRAPPER_PATH"
|
|
265
271
|
case ":${PATH:-}:" in
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"installers": {
|
|
4
4
|
"darwin-arm64": {
|
|
5
5
|
"fileName": "install-agentroam.sh",
|
|
6
|
-
"sha256": "
|
|
7
|
-
"size":
|
|
6
|
+
"sha256": "8ae51c123a05aa32f1483baf6d80ff7c5dbe0cc638578402e2a65c5e314f90de",
|
|
7
|
+
"size": 12028
|
|
8
8
|
},
|
|
9
9
|
"windows-amd64": {
|
|
10
10
|
"fileName": "install-agentroam.ps1",
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Launch a stopped official Desktop against the existing shared app-server.
|
|
5
|
+
# Restart is opt-in; a normal start preserves an already running Desktop.
|
|
6
|
+
codex_desktop_dry_run=false
|
|
7
|
+
codex_desktop_restart=false
|
|
8
|
+
for codex_desktop_option in "$@"; do
|
|
9
|
+
case "$codex_desktop_option" in
|
|
10
|
+
--dry-run) codex_desktop_dry_run=true ;;
|
|
11
|
+
--restart) codex_desktop_restart=true ;;
|
|
12
|
+
*) echo "Usage: agentroam codex-desktop [--dry-run] [--restart]" >&2; exit 2 ;;
|
|
13
|
+
esac
|
|
14
|
+
done
|
|
15
|
+
|
|
16
|
+
if [[ "$(uname -s)" != Darwin ]]; then
|
|
17
|
+
echo "This launcher requires macOS." >&2
|
|
18
|
+
exit 2
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
codex_desktop_app="${CODEX_DESKTOP_APP:-}"
|
|
22
|
+
if [[ -z "$codex_desktop_app" ]]; then
|
|
23
|
+
if [[ -d /Applications/ChatGPT.app ]]; then
|
|
24
|
+
codex_desktop_app=/Applications/ChatGPT.app
|
|
25
|
+
else
|
|
26
|
+
codex_desktop_app=/Applications/Codex.app
|
|
27
|
+
fi
|
|
28
|
+
fi
|
|
29
|
+
codex_desktop_name="$(basename "$codex_desktop_app" .app)"
|
|
30
|
+
codex_desktop_binary="$codex_desktop_app/Contents/MacOS/$codex_desktop_name"
|
|
31
|
+
if [[ ! -x "$codex_desktop_binary" ]]; then
|
|
32
|
+
echo "Official Desktop executable not found: $codex_desktop_binary" >&2
|
|
33
|
+
exit 2
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Attempt the connection regardless of Desktop version. A missing marker in
|
|
37
|
+
# app.asar is not proof of incompatibility; the client's actual initialization
|
|
38
|
+
# determines whether Desktop, shared or standalone is selected.
|
|
39
|
+
|
|
40
|
+
codex_shared_root="${CODEX_HOME:-$HOME/.codex}"
|
|
41
|
+
codex_shared_socket="$codex_shared_root/app-server-control/app-server-control.sock"
|
|
42
|
+
if [[ "$codex_shared_socket" != /* || "$codex_shared_socket" == *[[:space:]%:#?]* ]]; then
|
|
43
|
+
echo "Shared socket path must be absolute and contain no whitespace or URL delimiters." >&2
|
|
44
|
+
exit 2
|
|
45
|
+
fi
|
|
46
|
+
codex_shared_url="ws+unix://localhost$codex_shared_socket:/rpc"
|
|
47
|
+
codex_shared_cli="${AGENT_CODEX_BIN:-$HOME/.local/bin/codex}"
|
|
48
|
+
if [[ -n "${AGENT_CODEX_BIN:-}" && ! -x "$codex_shared_cli" ]]; then
|
|
49
|
+
echo "Configured AGENT_CODEX_BIN is not executable: $codex_shared_cli" >&2
|
|
50
|
+
exit 2
|
|
51
|
+
fi
|
|
52
|
+
if [[ -z "${AGENT_CODEX_BIN:-}" && ! -x "$codex_shared_cli" ]]; then
|
|
53
|
+
codex_shared_cli="$(command -v codex || true)"
|
|
54
|
+
fi
|
|
55
|
+
if [[ -z "$codex_shared_cli" ]]; then
|
|
56
|
+
echo "Codex CLI not found. Set AGENT_CODEX_BIN to its executable." >&2
|
|
57
|
+
exit 2
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
codex_desktop_running_pid=""
|
|
61
|
+
check_desktop_pid() {
|
|
62
|
+
codex_desktop_running_pid=""
|
|
63
|
+
local codex_desktop_processes
|
|
64
|
+
codex_desktop_processes="$(/bin/ps -axo pid=,args=)" || return 1
|
|
65
|
+
while read -r codex_desktop_pid codex_desktop_command; do
|
|
66
|
+
if [[ "$codex_desktop_command" == "$codex_desktop_binary" || "$codex_desktop_command" == "$codex_desktop_binary "* ]]; then
|
|
67
|
+
if [[ -n "$codex_desktop_running_pid" ]]; then
|
|
68
|
+
echo "Multiple Desktop processes found; no restart attempted." >&2
|
|
69
|
+
return 1
|
|
70
|
+
fi
|
|
71
|
+
codex_desktop_running_pid="$codex_desktop_pid"
|
|
72
|
+
fi
|
|
73
|
+
done <<< "$codex_desktop_processes"
|
|
74
|
+
if [[ -n "${AGENTROAM_CODEX_DESKTOP_EXPECTED_PID:-}" && "${codex_desktop_running_pid:-0}" != "$AGENTROAM_CODEX_DESKTOP_EXPECTED_PID" ]]; then
|
|
75
|
+
echo "Desktop process changed after setup inspection; no restart attempted." >&2
|
|
76
|
+
return 1
|
|
77
|
+
fi
|
|
78
|
+
}
|
|
79
|
+
check_desktop_pid
|
|
80
|
+
|
|
81
|
+
if [[ "$codex_desktop_dry_run" == true ]]; then
|
|
82
|
+
echo "Desktop: $codex_desktop_app"
|
|
83
|
+
echo "CODEX_APP_SERVER_WS_URL=$codex_shared_url"
|
|
84
|
+
echo "CLI: $codex_shared_cli"
|
|
85
|
+
if [[ -n "$codex_desktop_running_pid" ]]; then
|
|
86
|
+
if [[ "$codex_desktop_restart" == true ]]; then
|
|
87
|
+
echo "Desktop PID $codex_desktop_running_pid would be quit and relaunched; all its running sessions would be interrupted."
|
|
88
|
+
else
|
|
89
|
+
echo "Desktop PID $codex_desktop_running_pid is running; launch will refuse until Desktop has exited."
|
|
90
|
+
fi
|
|
91
|
+
fi
|
|
92
|
+
echo "Dry run: no process started or stopped, no environment persisted."
|
|
93
|
+
exit 0
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
if [[ -n "$codex_desktop_running_pid" && "$codex_desktop_restart" != true ]]; then
|
|
97
|
+
echo "Desktop PID $codex_desktop_running_pid is still running. Exit Desktop after its active sessions finish, then rerun, or explicitly use agentroam codex-desktop --restart (interrupts all Desktop sessions)." >&2
|
|
98
|
+
exit 1
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# Check daemon availability before asking the existing Desktop to quit.
|
|
102
|
+
(cd "$HOME" && "$codex_shared_cli" app-server daemon start)
|
|
103
|
+
if [[ ! -S "$codex_shared_socket" ]]; then
|
|
104
|
+
echo "Shared app-server socket not found: $codex_shared_socket" >&2
|
|
105
|
+
exit 1
|
|
106
|
+
fi
|
|
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"
|
|
115
|
+
|
|
116
|
+
# Recheck after daemon preflight; never quit a different Desktop instance.
|
|
117
|
+
codex_desktop_preflight_pid="$codex_desktop_running_pid"
|
|
118
|
+
check_desktop_pid
|
|
119
|
+
if [[ "$codex_desktop_preflight_pid" != "$codex_desktop_running_pid" ]]; then
|
|
120
|
+
echo "Desktop process changed during daemon preflight; no restart attempted." >&2
|
|
121
|
+
exit 1
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
if [[ -n "$codex_desktop_running_pid" ]]; then
|
|
125
|
+
if [[ -n "${AGENTROAM_CODEX_DESKTOP_IDLE_GUARD:-}" ]]; then
|
|
126
|
+
"${AGENTROAM_CODEX_DESKTOP_GUARD_NODE:?}" "$AGENTROAM_CODEX_DESKTOP_IDLE_GUARD"
|
|
127
|
+
check_desktop_pid
|
|
128
|
+
fi
|
|
129
|
+
echo "Restarting official Desktop; all its running sessions will be interrupted."
|
|
130
|
+
/usr/bin/osascript \
|
|
131
|
+
-e 'on run argv' \
|
|
132
|
+
-e 'tell application (item 1 of argv) to quit' \
|
|
133
|
+
-e 'end run' "$codex_desktop_app"
|
|
134
|
+
for ((codex_desktop_wait=0; codex_desktop_wait<40; codex_desktop_wait++)); do
|
|
135
|
+
if ! /bin/kill -0 "$codex_desktop_running_pid" 2>/dev/null; then break; fi
|
|
136
|
+
sleep 0.25
|
|
137
|
+
done
|
|
138
|
+
if /bin/kill -0 "$codex_desktop_running_pid" 2>/dev/null; then
|
|
139
|
+
echo "Desktop did not exit; no forced kill or second launch was attempted." >&2
|
|
140
|
+
exit 1
|
|
141
|
+
fi
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
# open --env applies only to the newly launched app, avoiding global launchd
|
|
145
|
+
# environment changes and preserving all Desktop settings and saved windows.
|
|
146
|
+
/usr/bin/open -a "$codex_desktop_app" \
|
|
147
|
+
--env "CODEX_APP_SERVER_WS_URL=$codex_shared_url" \
|
|
148
|
+
--env "CODEX_APP_SERVER_FORCE_CLI=0"
|
|
149
|
+
echo "Desktop launch requested against $codex_shared_url."
|
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.37",
|
|
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.
|
|
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.37",
|
|
30
|
+
"agentroam-runtime-win32-x64": "0.2.0-preview.37",
|
|
31
|
+
"agentroam-cloudflared-darwin-arm64": "0.2.0-preview.36",
|
|
32
|
+
"agentroam-cloudflared-win32-x64": "0.2.0-preview.36",
|
|
33
|
+
"agentroam-tui-darwin-arm64": "0.2.0-preview.36",
|
|
34
|
+
"@caoqu/agentroam-tui-win32-x64": "0.2.0-preview.36"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^22.0.0",
|