336 lines
10 KiB
JavaScript
336 lines
10 KiB
JavaScript
import { spawn } from "node:child_process";
|
|
import path from "node:path";
|
|
|
|
function parseBoolean(value) {
|
|
return String(value || "").trim().toLowerCase() === "true";
|
|
}
|
|
|
|
function parseArgs(value) {
|
|
return String(value || "")
|
|
.trim()
|
|
.split(/\s+/)
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function parseTimeoutMs(value) {
|
|
const parsed = Number.parseInt(String(value || ""), 10);
|
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 3000;
|
|
}
|
|
|
|
function parseNonNegativeInteger(value, fallback) {
|
|
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
function trimToDefined(value) {
|
|
const trimmed = String(value ?? "").trim();
|
|
return trimmed ? trimmed : undefined;
|
|
}
|
|
|
|
function pickConfigValue(config, key, fallback) {
|
|
if (config && config[key] !== undefined && config[key] !== null && `${config[key]}`.trim() !== "") {
|
|
return config[key];
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
function resolveCommandArgs(command, args, cwd) {
|
|
const runtimeName = path.basename(command || "").toLowerCase();
|
|
const scriptRuntimes = new Set([
|
|
"node",
|
|
"node.exe",
|
|
"tsx",
|
|
"tsx.cmd",
|
|
"bun",
|
|
"bun.exe",
|
|
"deno",
|
|
"deno.exe",
|
|
]);
|
|
if (!scriptRuntimes.has(runtimeName) || args.length === 0) {
|
|
return args;
|
|
}
|
|
const [first, ...rest] = args;
|
|
if (!first || first.startsWith("-")) {
|
|
return args;
|
|
}
|
|
return [path.isAbsolute(first) ? first : path.resolve(cwd || process.cwd(), first), ...rest];
|
|
}
|
|
|
|
function parseJsonLine(rawOutput) {
|
|
const lines = String(rawOutput || "")
|
|
.trim()
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
.filter(Boolean);
|
|
return JSON.parse(lines.at(-1) || "");
|
|
}
|
|
|
|
export function getCodexDesktopRefreshBridgeConfig(env = process.env, config = {}) {
|
|
const enabled = parseBoolean(
|
|
pickConfigValue(config, "codexDesktopRefreshEnabled", env.BOSS_CODEX_DESKTOP_REFRESH_ENABLED),
|
|
);
|
|
const command =
|
|
trimToDefined(
|
|
pickConfigValue(config, "codexDesktopRefreshCommand", env.BOSS_CODEX_DESKTOP_REFRESH_COMMAND),
|
|
) || undefined;
|
|
const endpoint =
|
|
trimToDefined(
|
|
pickConfigValue(config, "codexDesktopRefreshEndpoint", env.BOSS_CODEX_DESKTOP_REFRESH_ENDPOINT),
|
|
) || undefined;
|
|
const args = Array.isArray(config?.codexDesktopRefreshArgs)
|
|
? config.codexDesktopRefreshArgs.map((item) => String(item)).filter(Boolean)
|
|
: parseArgs(pickConfigValue(config, "codexDesktopRefreshArgs", env.BOSS_CODEX_DESKTOP_REFRESH_ARGS));
|
|
const cwd =
|
|
trimToDefined(
|
|
pickConfigValue(config, "codexDesktopRefreshWorkdir", env.BOSS_CODEX_DESKTOP_REFRESH_WORKDIR),
|
|
) || undefined;
|
|
const timeoutMs = parseTimeoutMs(
|
|
pickConfigValue(config, "codexDesktopRefreshTimeoutMs", env.BOSS_CODEX_DESKTOP_REFRESH_TIMEOUT_MS),
|
|
);
|
|
const appName =
|
|
trimToDefined(pickConfigValue(config, "codexDesktopRefreshAppName", env.BOSS_CODEX_DESKTOP_APP_NAME)) ||
|
|
"Codex";
|
|
const refreshMode =
|
|
trimToDefined(
|
|
pickConfigValue(config, "codexDesktopRefreshMode", env.BOSS_CODEX_DESKTOP_REFRESH_MODE),
|
|
) || "deeplink-reload";
|
|
const retryCount = parseNonNegativeInteger(
|
|
pickConfigValue(config, "codexDesktopRefreshRetryCount", env.BOSS_CODEX_DESKTOP_REFRESH_RETRY_COUNT),
|
|
2,
|
|
);
|
|
const retryDelayMs = parseNonNegativeInteger(
|
|
pickConfigValue(config, "codexDesktopRefreshRetryDelayMs", env.BOSS_CODEX_DESKTOP_REFRESH_RETRY_DELAY_MS),
|
|
120,
|
|
);
|
|
return {
|
|
enabled,
|
|
endpoint,
|
|
command,
|
|
args,
|
|
cwd,
|
|
timeoutMs,
|
|
appName,
|
|
refreshMode,
|
|
retryCount,
|
|
retryDelayMs,
|
|
};
|
|
}
|
|
|
|
function buildCodexDesktopRefreshPayload(config, mirrorHint) {
|
|
if (!config?.enabled) {
|
|
throw new Error("CODEX_DESKTOP_REFRESH_DISABLED");
|
|
}
|
|
const targetThreadRef = trimToDefined(mirrorHint?.targetThreadRef);
|
|
const sourceMessageId = trimToDefined(mirrorHint?.sourceMessageId);
|
|
if (!targetThreadRef || !sourceMessageId) {
|
|
throw new Error("CODEX_DESKTOP_REFRESH_HINT_REQUIRED");
|
|
}
|
|
|
|
return {
|
|
kind: "codex_desktop_refresh_hint",
|
|
targetThreadRef,
|
|
sourceMessageId,
|
|
rolloutPath: trimToDefined(mirrorHint?.rolloutPath),
|
|
threadTouchStatus: trimToDefined(mirrorHint?.threadTouchStatus),
|
|
appName: trimToDefined(config.appName) || "Codex",
|
|
refreshMode: trimToDefined(config.refreshMode) || "deeplink-reload",
|
|
requestedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
export function buildCodexDesktopRefreshExecution(config, mirrorHint) {
|
|
if (!config?.enabled) {
|
|
throw new Error("CODEX_DESKTOP_REFRESH_DISABLED");
|
|
}
|
|
if (!config?.command) {
|
|
throw new Error("CODEX_DESKTOP_REFRESH_COMMAND_REQUIRED");
|
|
}
|
|
|
|
const cwd = config.cwd || process.cwd();
|
|
return {
|
|
command: config.command,
|
|
args: resolveCommandArgs(config.command, config.args || [], cwd),
|
|
cwd,
|
|
timeoutMs: config.timeoutMs || 3000,
|
|
stdinPayload: buildCodexDesktopRefreshPayload(config, mirrorHint),
|
|
};
|
|
}
|
|
|
|
export function parseCodexDesktopRefreshResult(rawOutput) {
|
|
const parsed = parseJsonLine(rawOutput);
|
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
throw new Error("INVALID_CODEX_DESKTOP_REFRESH_PAYLOAD");
|
|
}
|
|
const attemptCount = parseNonNegativeInteger(parsed.attemptCount, undefined);
|
|
const baseResult = {
|
|
targetThreadRef: trimToDefined(parsed.targetThreadRef),
|
|
appName: trimToDefined(parsed.appName),
|
|
deepLink: trimToDefined(parsed.deepLink),
|
|
attemptCount,
|
|
};
|
|
if (parsed.status === "failed") {
|
|
return {
|
|
status: "failed",
|
|
...baseResult,
|
|
detail: trimToDefined(parsed.error) || "CODEX_DESKTOP_REFRESH_FAILED",
|
|
};
|
|
}
|
|
if (parsed.status !== "completed" && parsed.status !== "skipped") {
|
|
throw new Error("INVALID_CODEX_DESKTOP_REFRESH_PAYLOAD");
|
|
}
|
|
return {
|
|
status: parsed.status,
|
|
...baseResult,
|
|
detail: trimToDefined(parsed.detail),
|
|
};
|
|
}
|
|
|
|
function compactUndefinedFields(result) {
|
|
return Object.fromEntries(Object.entries(result).filter(([, value]) => value !== undefined));
|
|
}
|
|
|
|
function attachBridgeAttemptCount(result, attemptIndex) {
|
|
if (result.attemptCount !== undefined || attemptIndex > 1) {
|
|
return compactUndefinedFields({
|
|
...result,
|
|
attemptCount: result.attemptCount ?? attemptIndex,
|
|
});
|
|
}
|
|
return compactUndefinedFields(result);
|
|
}
|
|
|
|
function runCodexDesktopRefreshExecution(execution) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(execution.command, execution.args, {
|
|
cwd: execution.cwd,
|
|
env: process.env,
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
let timedOut = false;
|
|
const timer = setTimeout(() => {
|
|
timedOut = true;
|
|
child.kill("SIGKILL");
|
|
}, execution.timeoutMs);
|
|
|
|
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", (error) => {
|
|
clearTimeout(timer);
|
|
reject(error);
|
|
});
|
|
child.on("close", (code) => {
|
|
clearTimeout(timer);
|
|
if (timedOut) {
|
|
reject(new Error("CODEX_DESKTOP_REFRESH_TIMEOUT"));
|
|
return;
|
|
}
|
|
if (code !== 0) {
|
|
reject(new Error(stderr.trim() || `codex desktop refresh exit code ${code}`));
|
|
return;
|
|
}
|
|
try {
|
|
resolve(parseCodexDesktopRefreshResult(stdout));
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
child.stdin.write(JSON.stringify(execution.stdinPayload));
|
|
child.stdin.end();
|
|
});
|
|
}
|
|
|
|
async function runCodexDesktopRefreshEndpoint(config, payload) {
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => {
|
|
controller.abort();
|
|
}, config.timeoutMs || 3000);
|
|
try {
|
|
const response = await fetch(config.endpoint, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
signal: controller.signal,
|
|
});
|
|
const body = await response.text();
|
|
if (!response.ok) {
|
|
throw new Error(body.trim() || `codex desktop refresh endpoint status ${response.status}`);
|
|
}
|
|
return parseCodexDesktopRefreshResult(body);
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
async function executeWithRetries(operation, runnerConfig) {
|
|
const maxAttempts = Math.max(1, parseNonNegativeInteger(runnerConfig.retryCount, 2) + 1);
|
|
const retryDelayMs = parseNonNegativeInteger(runnerConfig.retryDelayMs, 120);
|
|
let lastError;
|
|
let lastFailedResult;
|
|
|
|
for (let attemptIndex = 1; attemptIndex <= maxAttempts; attemptIndex += 1) {
|
|
try {
|
|
const result = attachBridgeAttemptCount(await operation(), attemptIndex);
|
|
if (result.status !== "failed") {
|
|
return result;
|
|
}
|
|
lastFailedResult = result;
|
|
} catch (error) {
|
|
lastError = error;
|
|
}
|
|
|
|
if (attemptIndex < maxAttempts) {
|
|
await sleep(retryDelayMs);
|
|
}
|
|
}
|
|
|
|
if (lastFailedResult) {
|
|
return attachBridgeAttemptCount(lastFailedResult, maxAttempts);
|
|
}
|
|
|
|
const message = lastError instanceof Error ? lastError.message : String(lastError || "CODEX_DESKTOP_REFRESH_FAILED");
|
|
throw new Error(`${message}; attempts=${maxAttempts}`);
|
|
}
|
|
|
|
export async function executeCodexDesktopRefreshBridge(mirrorHint, config = {}) {
|
|
const runnerConfig =
|
|
config && Object.prototype.hasOwnProperty.call(config, "enabled")
|
|
? config
|
|
: getCodexDesktopRefreshBridgeConfig(process.env, config);
|
|
if (!runnerConfig.enabled) {
|
|
return {
|
|
status: "skipped",
|
|
reason: "disabled",
|
|
};
|
|
}
|
|
|
|
if (runnerConfig.endpoint) {
|
|
const endpointPayload = buildCodexDesktopRefreshPayload(runnerConfig, mirrorHint);
|
|
try {
|
|
return await executeWithRetries(() => runCodexDesktopRefreshEndpoint(runnerConfig, endpointPayload), runnerConfig);
|
|
} catch (error) {
|
|
if (!runnerConfig.command) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
const execution = buildCodexDesktopRefreshExecution(runnerConfig, mirrorHint);
|
|
return executeWithRetries(() => runCodexDesktopRefreshExecution(execution), runnerConfig);
|
|
}
|