170 lines
5.4 KiB
JavaScript
170 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawn } from "node:child_process";
|
|
import { access, open } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
const DEFAULT_CODEX_APP_PATH = "/Applications/Codex.app";
|
|
const DEFAULT_BRIDGE_EVENTS_URL = "http://127.0.0.1:4318/api/v1/codex-desktop/events";
|
|
|
|
function runCommand(command, args) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(command, args, {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.setEncoding("utf8");
|
|
child.stderr.setEncoding("utf8");
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk;
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk;
|
|
});
|
|
child.on("error", reject);
|
|
child.on("close", (code) => {
|
|
if (code !== 0) {
|
|
reject(new Error(stderr.trim() || `${command} exit code ${code}`));
|
|
return;
|
|
}
|
|
resolve(stdout);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function pathExists(filePath) {
|
|
try {
|
|
await access(filePath);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function readPlistAsJson(plistPath) {
|
|
const output = await runCommand("plutil", ["-convert", "json", "-o", "-", plistPath]);
|
|
return JSON.parse(output);
|
|
}
|
|
|
|
function extractUrlSchemes(info) {
|
|
const urlTypes = Array.isArray(info?.CFBundleURLTypes) ? info.CFBundleURLTypes : [];
|
|
return [
|
|
...new Set(
|
|
urlTypes.flatMap((item) =>
|
|
Array.isArray(item?.CFBundleURLSchemes)
|
|
? item.CFBundleURLSchemes.map((scheme) => String(scheme || "").trim()).filter(Boolean)
|
|
: [],
|
|
),
|
|
),
|
|
];
|
|
}
|
|
|
|
async function fileContains(filePath, needle) {
|
|
if (!(await pathExists(filePath))) {
|
|
return false;
|
|
}
|
|
const needleBuffer = Buffer.from(needle);
|
|
const handle = await open(filePath, "r");
|
|
const chunkSize = 1024 * 1024;
|
|
const overlapSize = Math.max(needleBuffer.length - 1, 0);
|
|
let carry = Buffer.alloc(0);
|
|
try {
|
|
const buffer = Buffer.alloc(chunkSize);
|
|
let position = 0;
|
|
while (true) {
|
|
const { bytesRead } = await handle.read(buffer, 0, chunkSize, position);
|
|
if (bytesRead === 0) {
|
|
return false;
|
|
}
|
|
const chunk = Buffer.concat([carry, buffer.subarray(0, bytesRead)]);
|
|
if (chunk.includes(needleBuffer)) {
|
|
return true;
|
|
}
|
|
carry = overlapSize > 0 ? chunk.subarray(Math.max(0, chunk.length - overlapSize)) : Buffer.alloc(0);
|
|
position += bytesRead;
|
|
}
|
|
} finally {
|
|
await handle.close();
|
|
}
|
|
}
|
|
|
|
export async function detectCodexDesktopIntegration(options = {}) {
|
|
const appPath =
|
|
String(options.appPath || process.env.BOSS_CODEX_DESKTOP_APP_PATH || DEFAULT_CODEX_APP_PATH).trim() ||
|
|
DEFAULT_CODEX_APP_PATH;
|
|
const bridgeEventsUrl =
|
|
String(options.bridgeEventsUrl || process.env.BOSS_CODEX_DESKTOP_EVENTS_URL || DEFAULT_BRIDGE_EVENTS_URL).trim() ||
|
|
DEFAULT_BRIDGE_EVENTS_URL;
|
|
const infoPlistPath = path.join(appPath, "Contents", "Info.plist");
|
|
const resourcesDir = path.join(appPath, "Contents", "Resources");
|
|
const appAsarPath = path.join(resourcesDir, "app.asar");
|
|
|
|
const appExists = await pathExists(appPath);
|
|
if (!appExists || !(await pathExists(infoPlistPath))) {
|
|
return {
|
|
ok: false,
|
|
app: {
|
|
path: appPath,
|
|
found: false,
|
|
},
|
|
capabilities: {},
|
|
reason: "CODEX_DESKTOP_APP_NOT_FOUND",
|
|
};
|
|
}
|
|
|
|
const info = await readPlistAsJson(infoPlistPath);
|
|
const urlSchemes = extractUrlSchemes(info);
|
|
const hasCodexScheme = urlSchemes.includes("codex");
|
|
const hasThreadDeepLinkResource = await fileContains(appAsarPath, "codex://threads/");
|
|
const hasRouteThreadResource = await fileContains(appAsarPath, "hotkey-window/thread");
|
|
const threadDeepLinkSupported = hasCodexScheme && (hasThreadDeepLinkResource || hasRouteThreadResource);
|
|
|
|
return {
|
|
ok: true,
|
|
app: {
|
|
path: appPath,
|
|
found: true,
|
|
bundleIdentifier: info.CFBundleIdentifier,
|
|
shortVersion: info.CFBundleShortVersionString,
|
|
version: info.CFBundleVersion,
|
|
urlSchemes,
|
|
},
|
|
capabilities: {
|
|
threadDeepLink: {
|
|
supported: threadDeepLinkSupported,
|
|
template: threadDeepLinkSupported ? "codex://threads/{threadId}" : undefined,
|
|
evidence: threadDeepLinkSupported
|
|
? "CFBundleURLSchemes contains codex and app resources contain codex://threads/"
|
|
: "thread deep link route not detected",
|
|
},
|
|
desktopBridgeSse: {
|
|
supported: true,
|
|
url: bridgeEventsUrl,
|
|
evidence: "Boss Codex Desktop Bridge exposes local SSE metadata stream",
|
|
},
|
|
inAppSubscription: {
|
|
supported: false,
|
|
evidence: "No stable public Codex Desktop plugin or IPC subscription API detected by this probe",
|
|
},
|
|
packagePatch: {
|
|
supported: false,
|
|
evidence: "Boss policy does not patch or modify the signed Codex Desktop application bundle",
|
|
},
|
|
},
|
|
recommendedPath: threadDeepLinkSupported
|
|
? "Use Boss rollout mirror + codex://threads/{threadId} + local bridge SSE; only move to in-app subscription if Codex Desktop exposes a stable plugin/IPC API."
|
|
: "Keep Boss rollout mirror and local bridge SSE; avoid Desktop package patching until a stable supported integration exists.",
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
const result = await detectCodexDesktopIntegration();
|
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
await main();
|
|
}
|