mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:31:06 +00:00
178 lines
5.6 KiB
TypeScript
178 lines
5.6 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import { createRequire } from "node:module";
|
|
import path from "node:path";
|
|
import type { ResolvedAcpxPluginConfig } from "./config.js";
|
|
|
|
const CODEX_ACP_PACKAGE = "@zed-industries/codex-acp";
|
|
const CODEX_ACP_PACKAGE_RANGE = "^0.12.0";
|
|
const CODEX_ACP_BIN = "codex-acp";
|
|
const requireFromHere = createRequire(import.meta.url);
|
|
|
|
type PackageManifest = {
|
|
name?: unknown;
|
|
bin?: unknown;
|
|
};
|
|
|
|
function quoteCommandPart(value: string): string {
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
function resolvePackageBinPath(
|
|
packageJsonPath: string,
|
|
manifest: PackageManifest,
|
|
): string | undefined {
|
|
const { bin } = manifest;
|
|
const relativeBinPath =
|
|
typeof bin === "string"
|
|
? bin
|
|
: bin && typeof bin === "object"
|
|
? (bin as Record<string, unknown>)[CODEX_ACP_BIN]
|
|
: undefined;
|
|
if (typeof relativeBinPath !== "string" || relativeBinPath.trim() === "") {
|
|
return undefined;
|
|
}
|
|
return path.resolve(path.dirname(packageJsonPath), relativeBinPath);
|
|
}
|
|
|
|
async function resolveInstalledCodexAcpBinPath(): Promise<string | undefined> {
|
|
try {
|
|
// Keep OpenClaw's isolated CODEX_HOME wrapper, but launch the plugin-local
|
|
// Codex ACP adapter when runtime-deps staging made it available.
|
|
const packageJsonPath = requireFromHere.resolve(`${CODEX_ACP_PACKAGE}/package.json`);
|
|
const manifest = JSON.parse(await fs.readFile(packageJsonPath, "utf8")) as PackageManifest;
|
|
if (manifest.name !== CODEX_ACP_PACKAGE) {
|
|
return undefined;
|
|
}
|
|
const binPath = resolvePackageBinPath(packageJsonPath, manifest);
|
|
if (!binPath) {
|
|
return undefined;
|
|
}
|
|
await fs.access(binPath);
|
|
return binPath;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function buildCodexAcpWrapperScript(installedBinPath?: string): string {
|
|
return `#!/usr/bin/env node
|
|
import { existsSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { spawn } from "node:child_process";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const codexHome = fileURLToPath(new URL("./codex-home/", import.meta.url));
|
|
const env = {
|
|
...process.env,
|
|
CODEX_HOME: codexHome,
|
|
};
|
|
const configuredArgs = process.argv.slice(2);
|
|
|
|
function resolveNpmCliPath() {
|
|
const candidate = path.resolve(
|
|
path.dirname(process.execPath),
|
|
"..",
|
|
"lib",
|
|
"node_modules",
|
|
"npm",
|
|
"bin",
|
|
"npm-cli.js",
|
|
);
|
|
return existsSync(candidate) ? candidate : undefined;
|
|
}
|
|
|
|
const npmCliPath = resolveNpmCliPath();
|
|
const installedBinPath = ${installedBinPath ? quoteCommandPart(installedBinPath) : "undefined"};
|
|
let defaultCommand;
|
|
let defaultArgs;
|
|
if (installedBinPath) {
|
|
defaultCommand = process.execPath;
|
|
defaultArgs = [installedBinPath];
|
|
} else if (npmCliPath) {
|
|
defaultCommand = process.execPath;
|
|
defaultArgs = [npmCliPath, "exec", "--yes", "--package", "${CODEX_ACP_PACKAGE}@${CODEX_ACP_PACKAGE_RANGE}", "--", "${CODEX_ACP_BIN}"];
|
|
} else {
|
|
defaultCommand = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
defaultArgs = ["--yes", "--package", "${CODEX_ACP_PACKAGE}@${CODEX_ACP_PACKAGE_RANGE}", "--", "${CODEX_ACP_BIN}"];
|
|
}
|
|
const command = configuredArgs[0] ?? defaultCommand;
|
|
const args = configuredArgs.length > 0 ? configuredArgs.slice(1) : defaultArgs;
|
|
|
|
const child = spawn(command, args, {
|
|
env,
|
|
stdio: "inherit",
|
|
windowsHide: true,
|
|
});
|
|
|
|
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
process.once(signal, () => {
|
|
child.kill(signal);
|
|
});
|
|
}
|
|
|
|
child.on("error", (error) => {
|
|
console.error(\`[openclaw] failed to launch isolated Codex ACP wrapper: \${error.message}\`);
|
|
process.exit(1);
|
|
});
|
|
|
|
child.on("exit", (code, signal) => {
|
|
if (code !== null) {
|
|
process.exit(code);
|
|
}
|
|
process.exit(signal ? 1 : 0);
|
|
});
|
|
`;
|
|
}
|
|
|
|
async function prepareIsolatedCodexHome(baseDir: string): Promise<string> {
|
|
const codexHome = path.join(baseDir, "codex-home");
|
|
await fs.mkdir(codexHome, { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(codexHome, "config.toml"),
|
|
"# Generated by OpenClaw for Codex ACP sessions.\n",
|
|
"utf8",
|
|
);
|
|
return codexHome;
|
|
}
|
|
|
|
async function writeCodexAcpWrapper(baseDir: string, installedBinPath?: string): Promise<string> {
|
|
await fs.mkdir(baseDir, { recursive: true });
|
|
const wrapperPath = path.join(baseDir, "codex-acp-wrapper.mjs");
|
|
await fs.writeFile(wrapperPath, buildCodexAcpWrapperScript(installedBinPath), {
|
|
encoding: "utf8",
|
|
});
|
|
await fs.chmod(wrapperPath, 0o755);
|
|
return wrapperPath;
|
|
}
|
|
|
|
function buildCodexAcpWrapperCommand(wrapperPath: string, configuredCommand?: string): string {
|
|
const baseCommand = `${quoteCommandPart(process.execPath)} ${quoteCommandPart(wrapperPath)}`;
|
|
const trimmedConfiguredCommand = configuredCommand?.trim();
|
|
// ACPX stores agent commands as shell-like strings and splits them before spawn.
|
|
return trimmedConfiguredCommand ? `${baseCommand} ${trimmedConfiguredCommand}` : baseCommand;
|
|
}
|
|
|
|
export async function prepareAcpxCodexAuthConfig(params: {
|
|
pluginConfig: ResolvedAcpxPluginConfig;
|
|
stateDir: string;
|
|
logger?: unknown;
|
|
resolveInstalledCodexAcpBinPath?: () => Promise<string | undefined>;
|
|
}): Promise<ResolvedAcpxPluginConfig> {
|
|
void params.logger;
|
|
const codexBaseDir = path.join(params.stateDir, "acpx");
|
|
await prepareIsolatedCodexHome(codexBaseDir);
|
|
const installedBinPath = await (
|
|
params.resolveInstalledCodexAcpBinPath ?? resolveInstalledCodexAcpBinPath
|
|
)();
|
|
const wrapperPath = await writeCodexAcpWrapper(codexBaseDir, installedBinPath);
|
|
const configuredCodexCommand = params.pluginConfig.agents.codex;
|
|
|
|
return {
|
|
...params.pluginConfig,
|
|
agents: {
|
|
...params.pluginConfig.agents,
|
|
codex: buildCodexAcpWrapperCommand(wrapperPath, configuredCodexCommand),
|
|
},
|
|
};
|
|
}
|