androdex 1.1.3

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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025-2026 Emanuele Di Pietro
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # androdex
2
+
3
+ `androdex` is the host-side CLI bridge for the Androdex project.
4
+
5
+ It is published separately from the Android app. The daemon keeps a stable host identity alive on the host machine and lets the Android client control local Codex remotely.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install -g androdex
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```sh
16
+ androdex pair
17
+ androdex up
18
+ androdex daemon status
19
+ androdex reset-pairing
20
+ androdex resume
21
+ androdex watch
22
+ ```
23
+
24
+ ## What it does
25
+
26
+ - keeps a durable relay presence keyed by a stable host id
27
+ - prints a pairing QR code and raw pairing payload on demand
28
+ - activates local `codex app-server` for the current workspace
29
+ - forwards JSON-RPC traffic between the host and the Android client
30
+ - handles git and workspace actions on the host machine
31
+
32
+ ## Commands
33
+
34
+ ### `androdex up`
35
+
36
+ Activates the current workspace in the daemon and launches `codex app-server` locally if needed.
37
+
38
+ ### `androdex pair`
39
+
40
+ Prints a fresh pairing QR and raw pairing payload for the already-running daemon identity.
41
+
42
+ ### `androdex daemon [start|stop|status]`
43
+
44
+ Manages the background daemon that owns the stable host identity and relay presence.
45
+
46
+ ### `androdex reset-pairing`
47
+
48
+ Clears the saved trusted-device state so the next `androdex pair` starts a fresh pairing flow.
49
+
50
+ ### `androdex resume`
51
+
52
+ Reopens the last active thread in the local Codex desktop app if available.
53
+
54
+ ### `androdex watch [threadId]`
55
+
56
+ Tails the rollout log for the selected thread in real time.
57
+
58
+ ## Environment variables
59
+
60
+ `androdex` accepts `ANDRODEX_*` variables.
61
+
62
+ Useful variables:
63
+
64
+ - `ANDRODEX_RELAY`: set the relay URL explicitly
65
+ - `ANDRODEX_CODEX_ENDPOINT`: connect to an existing Codex WebSocket instead of spawning a local runtime
66
+ - `ANDRODEX_REFRESH_ENABLED`: enable the macOS desktop refresh workaround explicitly
67
+ - `ANDRODEX_REFRESH_DEBOUNCE_MS`: adjust refresh debounce timing
68
+ - `ANDRODEX_REFRESH_COMMAND`: override desktop refresh with a custom command
69
+
70
+ ## Source builds
71
+
72
+ If you cloned the repository and want to run the bridge from source:
73
+
74
+ ```sh
75
+ cd androdex-bridge
76
+ npm install
77
+ npm start
78
+ ```
79
+
80
+ ## Project status
81
+
82
+ This package is part of Androdex, a local-first project focused on the host-machine-plus-Android workflow today.
83
+
84
+ Credit for the upstream fork chain remains with [relaydex](https://github.com/Ranats/relaydex) and [Remodex](https://github.com/Emanuele-web04/remodex).
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ // FILE: androdex.js
3
+ // Purpose: Canonical CLI entrypoint for the Androdex npm package.
4
+ // Layer: CLI binary
5
+ // Exports: none
6
+ // Depends on: ./cli
7
+
8
+ require("./cli");
package/bin/cli.js ADDED
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ // FILE: cli.js
3
+ // Purpose: CLI surface for daemon start, workspace activation, pairing, thread resume, and rollout tailing.
4
+ // Layer: CLI binary
5
+ // Exports: none
6
+ // Depends on: ../src
7
+
8
+ const {
9
+ createPairing,
10
+ getDaemonStatus,
11
+ runDaemonProcess,
12
+ startBridge,
13
+ startDaemonCli,
14
+ stopDaemonCli,
15
+ resetBridgePairing,
16
+ openLastActiveThread,
17
+ watchThreadRollout,
18
+ } = require("../src");
19
+ const { printQR } = require("../src/qr");
20
+
21
+ const command = process.argv[2] || "up";
22
+ const CLI_NAME = "androdex";
23
+ const CLI_PREFIX = `[${CLI_NAME}]`;
24
+
25
+ void main().catch((error) => {
26
+ console.error(`${CLI_PREFIX} ${(error && error.message) || "Command failed."}`);
27
+ process.exit(1);
28
+ });
29
+
30
+ async function main() {
31
+ if (command === "__daemon-run") {
32
+ await runDaemonProcess();
33
+ return;
34
+ }
35
+
36
+ if (command === "up") {
37
+ const status = await startBridge();
38
+ console.log(`${CLI_PREFIX} Active workspace: ${status.currentCwd || process.cwd()}`);
39
+ console.log(`${CLI_PREFIX} Relay: ${status.relayStatus}`);
40
+ return;
41
+ }
42
+
43
+ if (command === "pair") {
44
+ const response = await createPairing();
45
+ printQR(response.pairingPayload);
46
+ return;
47
+ }
48
+
49
+ if (command === "daemon") {
50
+ await handleDaemonCommand(process.argv[3] || "status");
51
+ return;
52
+ }
53
+
54
+ if (command === "reset-pairing") {
55
+ await resetBridgePairing();
56
+ console.log(`${CLI_PREFIX} Cleared the saved pairing state. Run \`${CLI_NAME} pair\` to create a fresh pairing QR.`);
57
+ return;
58
+ }
59
+
60
+ if (command === "resume") {
61
+ const state = openLastActiveThread();
62
+ console.log(
63
+ `${CLI_PREFIX} Opened last active thread: ${state.threadId} (${state.source || "unknown"})`
64
+ );
65
+ return;
66
+ }
67
+
68
+ if (command === "watch") {
69
+ watchThreadRollout(process.argv[3] || "");
70
+ return;
71
+ }
72
+
73
+ console.error(`Unknown command: ${command}`);
74
+ console.error("Usage: androdex up | androdex pair | androdex daemon [start|stop|status] | androdex reset-pairing | androdex resume | androdex watch [threadId]");
75
+ process.exit(1);
76
+ }
77
+
78
+ async function handleDaemonCommand(subcommand) {
79
+ if (subcommand === "start") {
80
+ const response = await startDaemonCli();
81
+ printDaemonStatus(response.status);
82
+ return;
83
+ }
84
+
85
+ if (subcommand === "stop") {
86
+ await stopDaemonCli();
87
+ console.log(`${CLI_PREFIX} Daemon stopped.`);
88
+ return;
89
+ }
90
+
91
+ if (subcommand === "status") {
92
+ const response = await getDaemonStatus();
93
+ printDaemonStatus(response.status);
94
+ return;
95
+ }
96
+
97
+ throw new Error(`Unknown daemon subcommand: ${subcommand}`);
98
+ }
99
+
100
+ function printDaemonStatus(status) {
101
+ console.log(`${CLI_PREFIX} Relay: ${status.relayStatus || "unknown"}`);
102
+ console.log(`${CLI_PREFIX} Host ID: ${status.hostId || "unavailable"}`);
103
+ console.log(`${CLI_PREFIX} Workspace: ${status.currentCwd || "none"}`);
104
+ console.log(`${CLI_PREFIX} Workspace active: ${status.workspaceActive ? "yes" : "no"}`);
105
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "androdex",
3
+ "version": "1.1.3",
4
+ "description": "Local bridge between Codex and the Androdex mobile app. Run `androdex up` to start.",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "androdex": "bin/androdex.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "start": "node ./bin/androdex.js up",
17
+ "test": "node --test ./test/*.test.js"
18
+ },
19
+ "keywords": [
20
+ "androdex",
21
+ "codex",
22
+ "bridge",
23
+ "cli",
24
+ "android"
25
+ ],
26
+ "author": "Robert Gordon",
27
+ "license": "ISC",
28
+ "type": "commonjs",
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "dependencies": {
36
+ "qrcode-terminal": "^0.12.0",
37
+ "uuid": "^13.0.0",
38
+ "ws": "^8.19.0"
39
+ }
40
+ }
package/src/bridge.js ADDED
@@ -0,0 +1,363 @@
1
+ // FILE: bridge.js
2
+ // Purpose: Runs Codex locally, bridges relay traffic, and coordinates desktop refreshes for Codex.app.
3
+ // Layer: CLI service
4
+ // Exports: startBridge
5
+ // Depends on: ws, uuid, ./qr, ./codex-desktop-refresher, ./codex-transport
6
+
7
+ const WebSocket = require("ws");
8
+ const { v4: uuidv4 } = require("uuid");
9
+ const {
10
+ CodexDesktopRefresher,
11
+ readBridgeConfig,
12
+ } = require("./codex-desktop-refresher");
13
+ const { createCodexTransport } = require("./codex-transport");
14
+ const { printQR } = require("./qr");
15
+ const { rememberActiveThread } = require("./session-state");
16
+ const { handleGitRequest } = require("./git-handler");
17
+ const { handleWorkspaceRequest } = require("./workspace-handler");
18
+ const { loadOrCreateBridgeDeviceState } = require("./secure-device-state");
19
+ const { createBridgeSecureTransport } = require("./secure-transport");
20
+
21
+ function startBridge() {
22
+ const config = readBridgeConfig();
23
+ if (!config.relayUrl) {
24
+ throw new Error("Set ANDRODEX_RELAY to a reachable relay URL before pairing or starting the bridge.");
25
+ }
26
+ const sessionId = uuidv4();
27
+ const relayBaseUrl = config.relayUrl.replace(/\/+$/, "");
28
+ const relaySessionUrl = `${relayBaseUrl}/${sessionId}`;
29
+ const deviceState = loadOrCreateBridgeDeviceState();
30
+ const desktopRefresher = new CodexDesktopRefresher({
31
+ enabled: config.refreshEnabled,
32
+ debounceMs: config.refreshDebounceMs,
33
+ refreshCommand: config.refreshCommand,
34
+ bundleId: config.codexBundleId,
35
+ appPath: config.codexAppPath,
36
+ });
37
+
38
+ // Keep the local Codex runtime alive across transient relay disconnects.
39
+ let socket = null;
40
+ let isShuttingDown = false;
41
+ let reconnectAttempt = 0;
42
+ let reconnectTimer = null;
43
+ let lastConnectionStatus = null;
44
+ let codexHandshakeState = config.codexEndpoint ? "warm" : "cold";
45
+ const forwardedInitializeRequestIds = new Set();
46
+ const secureTransport = createBridgeSecureTransport({
47
+ sessionId,
48
+ relayUrl: relayBaseUrl,
49
+ deviceState,
50
+ });
51
+
52
+ const codex = createCodexTransport({
53
+ endpoint: config.codexEndpoint,
54
+ env: process.env,
55
+ logPrefix: "[androdex]",
56
+ });
57
+
58
+ codex.onError((error) => {
59
+ if (config.codexEndpoint) {
60
+ console.error(`[androdex] Failed to connect to Codex endpoint: ${config.codexEndpoint}`);
61
+ } else {
62
+ console.error("[androdex] Failed to start `codex app-server`.");
63
+ console.error(`[androdex] Launch command: ${codex.describe()}`);
64
+ console.error("[androdex] Make sure the Codex CLI is installed and that the launcher works on this OS.");
65
+ }
66
+ console.error(error.message);
67
+ process.exit(1);
68
+ });
69
+
70
+ function clearReconnectTimer() {
71
+ if (!reconnectTimer) {
72
+ return;
73
+ }
74
+
75
+ clearTimeout(reconnectTimer);
76
+ reconnectTimer = null;
77
+ }
78
+
79
+ // Keeps npm start output compact by emitting only high-signal connection states.
80
+ function logConnectionStatus(status) {
81
+ if (lastConnectionStatus === status) {
82
+ return;
83
+ }
84
+
85
+ lastConnectionStatus = status;
86
+ console.log(`[androdex] ${status}`);
87
+ }
88
+
89
+ // Retries the relay socket while preserving the active Codex process and session id.
90
+ function scheduleRelayReconnect(closeCode) {
91
+ if (isShuttingDown) {
92
+ return;
93
+ }
94
+
95
+ if (closeCode === 4000 || closeCode === 4001) {
96
+ logConnectionStatus("disconnected");
97
+ shutdown(codex, () => socket, () => {
98
+ isShuttingDown = true;
99
+ clearReconnectTimer();
100
+ });
101
+ return;
102
+ }
103
+
104
+ if (reconnectTimer) {
105
+ return;
106
+ }
107
+
108
+ reconnectAttempt += 1;
109
+ const delayMs = Math.min(1_000 * reconnectAttempt, 5_000);
110
+ logConnectionStatus("connecting");
111
+ reconnectTimer = setTimeout(() => {
112
+ reconnectTimer = null;
113
+ connectRelay();
114
+ }, delayMs);
115
+ }
116
+
117
+ function connectRelay() {
118
+ if (isShuttingDown) {
119
+ return;
120
+ }
121
+
122
+ logConnectionStatus("connecting");
123
+ const nextSocket = new WebSocket(relaySessionUrl, {
124
+ headers: { "x-role": "mac" },
125
+ });
126
+ socket = nextSocket;
127
+
128
+ nextSocket.on("open", () => {
129
+ clearReconnectTimer();
130
+ reconnectAttempt = 0;
131
+ logConnectionStatus("connected");
132
+ secureTransport.bindLiveSendWireMessage((wireMessage) => {
133
+ if (nextSocket.readyState === WebSocket.OPEN) {
134
+ nextSocket.send(wireMessage);
135
+ }
136
+ });
137
+ });
138
+
139
+ nextSocket.on("message", (data) => {
140
+ const message = typeof data === "string" ? data : data.toString("utf8");
141
+ if (secureTransport.handleIncomingWireMessage(message, {
142
+ sendControlMessage(controlMessage) {
143
+ if (nextSocket.readyState === WebSocket.OPEN) {
144
+ nextSocket.send(JSON.stringify(controlMessage));
145
+ }
146
+ },
147
+ onApplicationMessage(plaintextMessage) {
148
+ handleApplicationMessage(plaintextMessage);
149
+ },
150
+ })) {
151
+ return;
152
+ }
153
+ });
154
+
155
+ nextSocket.on("close", (code) => {
156
+ logConnectionStatus("disconnected");
157
+ if (socket === nextSocket) {
158
+ socket = null;
159
+ }
160
+ desktopRefresher.handleTransportReset();
161
+ scheduleRelayReconnect(code);
162
+ });
163
+
164
+ nextSocket.on("error", () => {
165
+ logConnectionStatus("disconnected");
166
+ });
167
+ }
168
+
169
+ printQR(secureTransport.createPairingPayload());
170
+ connectRelay();
171
+
172
+ codex.onMessage((message) => {
173
+ trackCodexHandshakeState(message);
174
+ desktopRefresher.handleOutbound(message);
175
+ rememberThreadFromMessage("codex", message);
176
+ secureTransport.queueOutboundApplicationMessage(message, (wireMessage) => {
177
+ if (socket?.readyState === WebSocket.OPEN) {
178
+ socket.send(wireMessage);
179
+ }
180
+ });
181
+ });
182
+
183
+ codex.onClose(() => {
184
+ logConnectionStatus("disconnected");
185
+ isShuttingDown = true;
186
+ clearReconnectTimer();
187
+ desktopRefresher.handleTransportReset();
188
+ if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) {
189
+ socket.close();
190
+ }
191
+ });
192
+
193
+ process.on("SIGINT", () => shutdown(codex, () => socket, () => {
194
+ isShuttingDown = true;
195
+ clearReconnectTimer();
196
+ }));
197
+ process.on("SIGTERM", () => shutdown(codex, () => socket, () => {
198
+ isShuttingDown = true;
199
+ clearReconnectTimer();
200
+ }));
201
+
202
+ // Routes decrypted app payloads through the same bridge handlers as before.
203
+ function handleApplicationMessage(rawMessage) {
204
+ if (handleBridgeManagedHandshakeMessage(rawMessage)) {
205
+ return;
206
+ }
207
+ if (handleWorkspaceRequest(rawMessage, sendApplicationResponse)) {
208
+ return;
209
+ }
210
+ if (handleGitRequest(rawMessage, sendApplicationResponse)) {
211
+ return;
212
+ }
213
+ desktopRefresher.handleInbound(rawMessage);
214
+ rememberThreadFromMessage("phone", rawMessage);
215
+ codex.send(rawMessage);
216
+ }
217
+
218
+ // Encrypts bridge-generated responses instead of letting the relay see plaintext.
219
+ function sendApplicationResponse(rawMessage) {
220
+ secureTransport.queueOutboundApplicationMessage(rawMessage, (wireMessage) => {
221
+ if (socket?.readyState === WebSocket.OPEN) {
222
+ socket.send(wireMessage);
223
+ }
224
+ });
225
+ }
226
+
227
+ function rememberThreadFromMessage(source, rawMessage) {
228
+ const threadId = extractThreadId(rawMessage);
229
+ if (!threadId) {
230
+ return;
231
+ }
232
+
233
+ rememberActiveThread(threadId, source);
234
+ }
235
+
236
+ // The spawned/shared Codex app-server stays warm across phone reconnects.
237
+ // When iPhone reconnects it sends initialize again, but forwarding that to the
238
+ // already-initialized Codex transport only produces "Already initialized".
239
+ function handleBridgeManagedHandshakeMessage(rawMessage) {
240
+ let parsed = null;
241
+ try {
242
+ parsed = JSON.parse(rawMessage);
243
+ } catch {
244
+ return false;
245
+ }
246
+
247
+ const method = typeof parsed?.method === "string" ? parsed.method.trim() : "";
248
+ if (!method) {
249
+ return false;
250
+ }
251
+
252
+ if (method === "initialize" && parsed.id != null) {
253
+ if (codexHandshakeState !== "warm") {
254
+ forwardedInitializeRequestIds.add(String(parsed.id));
255
+ return false;
256
+ }
257
+
258
+ sendApplicationResponse(JSON.stringify({
259
+ id: parsed.id,
260
+ result: {
261
+ bridgeManaged: true,
262
+ },
263
+ }));
264
+ return true;
265
+ }
266
+
267
+ if (method === "initialized") {
268
+ return codexHandshakeState === "warm";
269
+ }
270
+
271
+ return false;
272
+ }
273
+
274
+ // Learns whether the underlying Codex transport has already completed its own MCP handshake.
275
+ function trackCodexHandshakeState(rawMessage) {
276
+ let parsed = null;
277
+ try {
278
+ parsed = JSON.parse(rawMessage);
279
+ } catch {
280
+ return;
281
+ }
282
+
283
+ const responseId = parsed?.id;
284
+ if (responseId == null) {
285
+ return;
286
+ }
287
+
288
+ const responseKey = String(responseId);
289
+ if (!forwardedInitializeRequestIds.has(responseKey)) {
290
+ return;
291
+ }
292
+
293
+ forwardedInitializeRequestIds.delete(responseKey);
294
+
295
+ if (parsed?.result != null) {
296
+ codexHandshakeState = "warm";
297
+ return;
298
+ }
299
+
300
+ const errorMessage = typeof parsed?.error?.message === "string"
301
+ ? parsed.error.message.toLowerCase()
302
+ : "";
303
+ if (errorMessage.includes("already initialized")) {
304
+ codexHandshakeState = "warm";
305
+ }
306
+ }
307
+ }
308
+
309
+ function shutdown(codex, getSocket, beforeExit = () => {}) {
310
+ beforeExit();
311
+
312
+ const socket = getSocket();
313
+ if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) {
314
+ socket.close();
315
+ }
316
+
317
+ codex.shutdown();
318
+
319
+ setTimeout(() => process.exit(0), 100);
320
+ }
321
+
322
+ function extractThreadId(rawMessage) {
323
+ let parsed = null;
324
+ try {
325
+ parsed = JSON.parse(rawMessage);
326
+ } catch {
327
+ return null;
328
+ }
329
+
330
+ const method = parsed?.method;
331
+ const params = parsed?.params;
332
+
333
+ if (method === "turn/start") {
334
+ return readString(params?.threadId) || readString(params?.thread_id);
335
+ }
336
+
337
+ if (method === "thread/start" || method === "thread/started") {
338
+ return (
339
+ readString(params?.threadId)
340
+ || readString(params?.thread_id)
341
+ || readString(params?.thread?.id)
342
+ || readString(params?.thread?.threadId)
343
+ || readString(params?.thread?.thread_id)
344
+ );
345
+ }
346
+
347
+ if (method === "turn/completed") {
348
+ return (
349
+ readString(params?.threadId)
350
+ || readString(params?.thread_id)
351
+ || readString(params?.turn?.threadId)
352
+ || readString(params?.turn?.thread_id)
353
+ );
354
+ }
355
+
356
+ return null;
357
+ }
358
+
359
+ function readString(value) {
360
+ return typeof value === "string" && value ? value : null;
361
+ }
362
+
363
+ module.exports = { startBridge };
@@ -0,0 +1,93 @@
1
+ // FILE: codex-desktop-launcher.js
2
+ // Purpose: Opens Codex desktop deep links across supported host platforms.
3
+ // Layer: CLI helper
4
+ // Exports: openCodexDesktopTarget, openCodexDesktopTargetSync
5
+ // Depends on: child_process
6
+
7
+ const { execFile, execFileSync } = require("child_process");
8
+
9
+ function openCodexDesktopTarget({
10
+ targetUrl = "",
11
+ bundleId = "com.openai.codex",
12
+ appPath = "/Applications/Codex.app",
13
+ platform = process.platform,
14
+ } = {}) {
15
+ const plan = createDesktopLaunchPlan({ targetUrl, bundleId, appPath, platform });
16
+ return execFilePromise(plan.command, plan.args, plan.options);
17
+ }
18
+
19
+ function openCodexDesktopTargetSync({
20
+ targetUrl = "",
21
+ bundleId = "com.openai.codex",
22
+ appPath = "/Applications/Codex.app",
23
+ platform = process.platform,
24
+ } = {}) {
25
+ const plan = createDesktopLaunchPlan({ targetUrl, bundleId, appPath, platform });
26
+ execFileSync(plan.command, plan.args, plan.options);
27
+ }
28
+
29
+ function createDesktopLaunchPlan({
30
+ targetUrl = "",
31
+ bundleId = "com.openai.codex",
32
+ appPath = "/Applications/Codex.app",
33
+ platform = process.platform,
34
+ } = {}) {
35
+ const safeTargetUrl = typeof targetUrl === "string" ? targetUrl.trim() : "";
36
+
37
+ if (platform === "darwin") {
38
+ if (safeTargetUrl) {
39
+ return {
40
+ command: "open",
41
+ args: ["-b", bundleId, safeTargetUrl],
42
+ options: { stdio: "ignore" },
43
+ };
44
+ }
45
+
46
+ return {
47
+ command: "open",
48
+ args: ["-a", appPath],
49
+ options: { stdio: "ignore" },
50
+ };
51
+ }
52
+
53
+ if (!safeTargetUrl) {
54
+ throw new Error("Codex desktop target URL is required on this platform.");
55
+ }
56
+
57
+ if (platform === "win32") {
58
+ return {
59
+ command: "cmd.exe",
60
+ args: ["/d", "/c", "start", "\"\"", safeTargetUrl],
61
+ options: {
62
+ stdio: "ignore",
63
+ windowsHide: true,
64
+ },
65
+ };
66
+ }
67
+
68
+ return {
69
+ command: "xdg-open",
70
+ args: [safeTargetUrl],
71
+ options: { stdio: "ignore" },
72
+ };
73
+ }
74
+
75
+ function execFilePromise(command, args, options) {
76
+ return new Promise((resolve, reject) => {
77
+ execFile(command, args, options, (error, stdout, stderr) => {
78
+ if (error) {
79
+ error.stdout = stdout;
80
+ error.stderr = stderr;
81
+ reject(error);
82
+ return;
83
+ }
84
+ resolve({ stdout, stderr });
85
+ });
86
+ });
87
+ }
88
+
89
+ module.exports = {
90
+ createDesktopLaunchPlan,
91
+ openCodexDesktopTarget,
92
+ openCodexDesktopTargetSync,
93
+ };