275 lines
8.0 KiB
JavaScript
275 lines
8.0 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawn } from "node:child_process";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
function writeJson(payload) {
|
|
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
}
|
|
|
|
async function readStdin() {
|
|
const chunks = [];
|
|
for await (const chunk of process.stdin) {
|
|
chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
}
|
|
return chunks.join("").trim();
|
|
}
|
|
|
|
function normalizePayload(raw) {
|
|
try {
|
|
const parsed = JSON.parse(raw);
|
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
return { ok: false, error: "INVALID_CODEX_DESKTOP_REFRESH_PAYLOAD" };
|
|
}
|
|
return { ok: true, payload: parsed };
|
|
} catch {
|
|
return { ok: false, error: "INVALID_CODEX_DESKTOP_REFRESH_PAYLOAD" };
|
|
}
|
|
}
|
|
|
|
function escapeAppleScriptString(value) {
|
|
return String(value || "")
|
|
.replaceAll("\\", "\\\\")
|
|
.replaceAll('"', '\\"');
|
|
}
|
|
|
|
function parseBoolean(value) {
|
|
return String(value || "").trim().toLowerCase() === "true";
|
|
}
|
|
|
|
export function buildCodexThreadDeepLink(targetThreadRef) {
|
|
return `codex://threads/${encodeURIComponent(targetThreadRef)}`;
|
|
}
|
|
|
|
function normalizeRefreshMode(value) {
|
|
const mode = String(value || "").trim().toLowerCase();
|
|
const supportedModes = new Set(["off", "activate", "reload", "deeplink", "deeplink-reload"]);
|
|
return supportedModes.has(mode) ? mode : null;
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
function runCommand(command, args) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(command, args, {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
|
|
let stderr = "";
|
|
child.stderr.setEncoding("utf8");
|
|
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();
|
|
});
|
|
});
|
|
}
|
|
|
|
async function activateMacApp(appName) {
|
|
const escapedAppName = escapeAppleScriptString(appName);
|
|
try {
|
|
await runCommand("osascript", [
|
|
"-e",
|
|
`tell application "${escapedAppName}" to activate`,
|
|
]);
|
|
return `activated ${appName} by osascript`;
|
|
} catch {
|
|
await runCommand("open", ["-a", appName]);
|
|
return `activated ${appName} by open`;
|
|
}
|
|
}
|
|
|
|
async function reloadMacApp(appName) {
|
|
const activationDetail = await activateMacApp(appName);
|
|
const refreshDetail = await sendMacRefreshShortcut();
|
|
return `${activationDetail}; ${refreshDetail}`;
|
|
}
|
|
|
|
async function sendMacRefreshShortcut() {
|
|
await runCommand("osascript", [
|
|
"-e",
|
|
'tell application "System Events" to key code 15 using command down',
|
|
]);
|
|
return "sent Cmd+R";
|
|
}
|
|
|
|
async function openMacThreadDeepLink(deepLink) {
|
|
await runCommand("open", [deepLink]);
|
|
return `opened ${deepLink}`;
|
|
}
|
|
|
|
async function activateWindowsApp(appName) {
|
|
const escaped = String(appName || "Codex").replaceAll("'", "''");
|
|
await runCommand("powershell.exe", [
|
|
"-NoProfile",
|
|
"-Command",
|
|
`$shell = New-Object -ComObject WScript.Shell; if (-not $shell.AppActivate('${escaped}')) { Start-Process '${escaped}' }`,
|
|
]);
|
|
return `activated ${appName} by powershell`;
|
|
}
|
|
|
|
async function reloadWindowsApp(appName) {
|
|
const escaped = String(appName || "Codex").replaceAll("'", "''");
|
|
await runCommand("powershell.exe", [
|
|
"-NoProfile",
|
|
"-Command",
|
|
`$shell = New-Object -ComObject WScript.Shell; if (-not $shell.AppActivate('${escaped}')) { Start-Process '${escaped}'; Start-Sleep -Milliseconds 300 }; $shell.SendKeys('^r')`,
|
|
]);
|
|
return `activated ${appName} and sent Ctrl+R`;
|
|
}
|
|
|
|
async function openWindowsThreadDeepLink(deepLink) {
|
|
const escapedDeepLink = String(deepLink || "").replaceAll("'", "''");
|
|
await runCommand("powershell.exe", [
|
|
"-NoProfile",
|
|
"-Command",
|
|
`Start-Process '${escapedDeepLink}'`,
|
|
]);
|
|
return `opened ${deepLink}`;
|
|
}
|
|
|
|
async function sendWindowsRefreshShortcut(appName) {
|
|
const escaped = String(appName || "Codex").replaceAll("'", "''");
|
|
await runCommand("powershell.exe", [
|
|
"-NoProfile",
|
|
"-Command",
|
|
`$shell = New-Object -ComObject WScript.Shell; if ($shell.AppActivate('${escaped}')) { $shell.SendKeys('^r') }`,
|
|
]);
|
|
return "sent Ctrl+R";
|
|
}
|
|
|
|
function buildDryRunDetail(refreshMode, appName, deepLink) {
|
|
switch (refreshMode) {
|
|
case "activate":
|
|
return `dry-run: would activate ${appName}`;
|
|
case "reload":
|
|
return `dry-run: would activate ${appName}; would send refresh shortcut`;
|
|
case "deeplink":
|
|
return `dry-run: would open ${deepLink}`;
|
|
case "deeplink-reload":
|
|
return `dry-run: would open ${deepLink}; would send refresh shortcut`;
|
|
default:
|
|
return `dry-run: unsupported mode ${refreshMode}`;
|
|
}
|
|
}
|
|
|
|
export async function executeCodexDesktopRefreshHint(payload, options = {}) {
|
|
const env = options.env || process.env;
|
|
const platform = options.platform || process.platform;
|
|
const targetThreadRef = String(payload?.targetThreadRef || "").trim();
|
|
const appName = String(payload?.appName || env.BOSS_CODEX_DESKTOP_APP_NAME || "Codex").trim() || "Codex";
|
|
const refreshMode = normalizeRefreshMode(
|
|
payload?.refreshMode || env.BOSS_CODEX_DESKTOP_REFRESH_MODE || "deeplink-reload",
|
|
);
|
|
if (payload?.kind !== "codex_desktop_refresh_hint" || !targetThreadRef) {
|
|
return {
|
|
status: "failed",
|
|
targetThreadRef,
|
|
appName,
|
|
error: "INVALID_CODEX_DESKTOP_REFRESH_HINT",
|
|
};
|
|
}
|
|
|
|
if (!refreshMode) {
|
|
return {
|
|
status: "failed",
|
|
targetThreadRef,
|
|
appName,
|
|
error: "INVALID_CODEX_DESKTOP_REFRESH_MODE",
|
|
};
|
|
}
|
|
|
|
if (refreshMode === "off") {
|
|
return {
|
|
status: "skipped",
|
|
targetThreadRef,
|
|
appName,
|
|
detail: "refresh hint disabled by mode",
|
|
};
|
|
}
|
|
|
|
const deepLink = buildCodexThreadDeepLink(targetThreadRef);
|
|
if (parseBoolean(env.BOSS_CODEX_DESKTOP_REFRESH_DRY_RUN)) {
|
|
return {
|
|
status: "completed",
|
|
targetThreadRef,
|
|
appName,
|
|
deepLink,
|
|
detail: buildDryRunDetail(refreshMode, appName, deepLink),
|
|
};
|
|
}
|
|
|
|
let detail = "";
|
|
if (platform === "darwin") {
|
|
if (refreshMode === "activate") {
|
|
detail = await activateMacApp(appName);
|
|
} else if (refreshMode === "reload") {
|
|
detail = await reloadMacApp(appName);
|
|
} else if (refreshMode === "deeplink") {
|
|
detail = await openMacThreadDeepLink(deepLink);
|
|
} else {
|
|
const openDetail = await openMacThreadDeepLink(deepLink);
|
|
await sleep(300);
|
|
const refreshDetail = await sendMacRefreshShortcut();
|
|
detail = `${openDetail}; ${refreshDetail}`;
|
|
}
|
|
} else if (platform === "win32") {
|
|
if (refreshMode === "activate") {
|
|
detail = await activateWindowsApp(appName);
|
|
} else if (refreshMode === "reload") {
|
|
detail = await reloadWindowsApp(appName);
|
|
} else if (refreshMode === "deeplink") {
|
|
detail = await openWindowsThreadDeepLink(deepLink);
|
|
} else {
|
|
const openDetail = await openWindowsThreadDeepLink(deepLink);
|
|
await sleep(300);
|
|
const refreshDetail = await sendWindowsRefreshShortcut(appName);
|
|
detail = `${openDetail}; ${refreshDetail}`;
|
|
}
|
|
} else {
|
|
detail = `platform ${platform} does not support desktop activation`;
|
|
}
|
|
return {
|
|
status: "completed",
|
|
targetThreadRef,
|
|
appName,
|
|
deepLink,
|
|
detail,
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
const normalized = normalizePayload(await readStdin());
|
|
if (!normalized.ok) {
|
|
writeJson({
|
|
status: "failed",
|
|
error: normalized.error,
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
writeJson(await executeCodexDesktopRefreshHint(normalized.payload));
|
|
} catch (error) {
|
|
const payload = normalized.payload;
|
|
writeJson({
|
|
status: "failed",
|
|
targetThreadRef: String(payload?.targetThreadRef || "").trim(),
|
|
appName: String(payload?.appName || process.env.BOSS_CODEX_DESKTOP_APP_NAME || "Codex").trim() || "Codex",
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
await main();
|
|
}
|