Files
2026-07-13 12:38:34 +08:00

330 lines
10 KiB
TypeScript

import { chmod, copyFile, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import * as path from 'node:path';
import { extract as extractTarball } from 'tar';
import { RUN_CODEX_ACP_BINARY_TARGETS } from '../src/services/run-companion-modules';
const GENERATED_ROOT_DIR = path.resolve('./.generated');
const ACP_ADAPTERS_CACHE_DIR = path.join(GENERATED_ROOT_DIR, 'acp-adapters');
const ACP_ADAPTERS_MANIFEST_PATH = path.join(GENERATED_ROOT_DIR, 'acp-adapters.manifest.json');
const REGISTRY_BASE_URL = 'https://registry.npmjs.org';
const USER_AGENT = 'composio-cli-acp-vendor/1';
const ACP_ADAPTERS_PREFIX = 'acp-adapters/';
const ACP_ADAPTERS_CACHE_FORMAT_VERSION = 2;
type PackageJson = {
readonly bin?: Readonly<Record<string, string>>;
readonly optionalDependencies?: Readonly<Record<string, string>>;
readonly version?: string;
};
type RegistryVersionManifest = {
readonly dist?: {
readonly tarball?: string;
};
};
type AcpAdaptersManifest = {
readonly cacheFormatVersion: number;
readonly claude: {
readonly packageName: '@zed-industries/claude-code-acp';
readonly version: string;
readonly agentSdkVersion: string;
};
readonly codex: ReadonlyArray<{
readonly packageName: string;
readonly relativePath: string;
readonly version: string;
}>;
};
const require = createRequire(import.meta.url);
const fileExists = async (filePath: string): Promise<boolean> => Bun.file(filePath).exists();
const readJsonFile = async <T>(filePath: string): Promise<T> =>
JSON.parse(await readFile(filePath, 'utf8')) as T;
const resolvePackageJsonPath = (specifier: string): string =>
require.resolve(`${specifier}/package.json`);
const stripAcpAdaptersPrefix = (relativePath: string): string =>
relativePath.startsWith(ACP_ADAPTERS_PREFIX)
? relativePath.slice(ACP_ADAPTERS_PREFIX.length)
: relativePath;
const readInstalledPackageJson = async (specifier: string): Promise<PackageJson> =>
readJsonFile<PackageJson>(resolvePackageJsonPath(specifier));
const resolveClaudeAdapterEntryPath = async (): Promise<string> => {
const manifest = await readInstalledPackageJson('@zed-industries/claude-code-acp');
const entryPath = manifest.bin?.['claude-code-acp'];
if (!entryPath) {
throw new Error('Missing claude-code-acp bin entry in installed package metadata.');
}
return path.join(
path.dirname(resolvePackageJsonPath('@zed-industries/claude-code-acp')),
entryPath
);
};
const resolveClaudeAgentSdkCliPath = (): string => {
const claudeCodeAcpPkg = require.resolve('@zed-industries/claude-code-acp/package.json');
const nestedRequire = createRequire(claudeCodeAcpPkg);
const agentSdkPkg = nestedRequire.resolve('@anthropic-ai/claude-agent-sdk/package.json');
return path.join(path.dirname(agentSdkPkg), 'cli.js');
};
const resolveClaudeAgentSdkPackageJsonPath = (): string => {
const claudeCodeAcpPkg = require.resolve('@zed-industries/claude-code-acp/package.json');
const nestedRequire = createRequire(claudeCodeAcpPkg);
return nestedRequire.resolve('@anthropic-ai/claude-agent-sdk/package.json');
};
const buildCacheManifest = async (): Promise<AcpAdaptersManifest> => {
const claudePackage = await readInstalledPackageJson('@zed-industries/claude-code-acp');
const agentSdkPackage = await readJsonFile<PackageJson>(resolveClaudeAgentSdkPackageJsonPath());
const codexLauncherPackage = await readInstalledPackageJson('@zed-industries/codex-acp');
const codexTargets = RUN_CODEX_ACP_BINARY_TARGETS.map(target => {
const version = codexLauncherPackage.optionalDependencies?.[target.packageName];
if (!version) {
throw new Error(
`Missing pinned optional dependency version for ${target.packageName} in @zed-industries/codex-acp.`
);
}
return {
packageName: target.packageName,
relativePath: target.relativePath,
version,
};
});
if (!claudePackage.version || !agentSdkPackage.version) {
throw new Error('Missing installed claude ACP package versions.');
}
return {
cacheFormatVersion: ACP_ADAPTERS_CACHE_FORMAT_VERSION,
claude: {
packageName: '@zed-industries/claude-code-acp',
version: claudePackage.version,
agentSdkVersion: agentSdkPackage.version,
},
codex: codexTargets,
};
};
const readExistingManifest = async (): Promise<AcpAdaptersManifest | null> => {
if (!(await fileExists(ACP_ADAPTERS_MANIFEST_PATH))) {
return null;
}
return readJsonFile<AcpAdaptersManifest>(ACP_ADAPTERS_MANIFEST_PATH).catch(() => null);
};
const manifestPathsExist = async (manifest: AcpAdaptersManifest): Promise<boolean> => {
const requiredPaths = [
path.join(ACP_ADAPTERS_CACHE_DIR, 'claude-code-acp.mjs'),
path.join(ACP_ADAPTERS_CACHE_DIR, 'cli.js'),
...manifest.codex.map(target =>
path.join(ACP_ADAPTERS_CACHE_DIR, stripAcpAdaptersPrefix(target.relativePath))
),
];
for (const requiredPath of requiredPaths) {
if (!(await fileExists(requiredPath))) {
return false;
}
}
return true;
};
const fetchRegistryJson = async (
packageName: string,
version: string
): Promise<RegistryVersionManifest> => {
const url = `${REGISTRY_BASE_URL}/${encodeURIComponent(packageName)}/${version}`;
const response = await fetch(url, {
headers: {
accept: 'application/json',
'user-agent': USER_AGENT,
},
});
if (!response.ok) {
throw new Error(
`Failed to fetch registry metadata for ${packageName}@${version}: ${response.status}`
);
}
return (await response.json()) as RegistryVersionManifest;
};
const downloadTarball = async (url: string, targetPath: string): Promise<void> => {
const response = await fetch(url, {
headers: {
'user-agent': USER_AGENT,
},
});
if (!response.ok) {
throw new Error(`Failed to download ${url}: ${response.status}`);
}
await writeFile(targetPath, new Uint8Array(await response.arrayBuffer()));
};
const bundleBunRuntimeFile = async ({
entryPath,
outputPath,
}: {
readonly entryPath: string;
readonly outputPath: string;
}): Promise<void> => {
const result = await Bun.build({
entrypoints: [entryPath],
outdir: path.dirname(outputPath),
naming: path.basename(outputPath),
target: 'bun',
format: 'esm',
packages: 'bundle',
sourcemap: 'none',
});
if (!result.success) {
const details = result.logs
.map(log => log.message)
.filter(message => message.length > 0)
.join('\n');
throw new Error(
details.length > 0
? `Failed to bundle ${path.basename(outputPath)}:\n${details}`
: `Failed to bundle ${path.basename(outputPath)}.`
);
}
await chmod(outputPath, 0o755);
};
const buildClaudeAdapterBundle = async (cacheDir: string): Promise<void> => {
const outputPath = path.join(cacheDir, 'claude-code-acp.mjs');
const entryPath = await resolveClaudeAdapterEntryPath();
await bundleBunRuntimeFile({
entryPath,
outputPath,
});
const cliOutputPath = path.join(cacheDir, 'cli.js');
await bundleBunRuntimeFile({
entryPath: resolveClaudeAgentSdkCliPath(),
outputPath: cliOutputPath,
});
};
const extractCodexBinaryFromTarball = async ({
binaryFileName,
packageName,
relativePath,
stageCacheDir,
stageRootDir,
version,
}: {
readonly binaryFileName: string;
readonly packageName: string;
readonly relativePath: string;
readonly stageCacheDir: string;
readonly stageRootDir: string;
readonly version: string;
}): Promise<void> => {
const metadata = await fetchRegistryJson(packageName, version);
const tarballUrl = metadata.dist?.tarball;
if (!tarballUrl) {
throw new Error(
`Registry metadata for ${packageName}@${version} did not include a tarball URL.`
);
}
const tempArchivePath = path.join(
stageRootDir,
`${packageName.split('/')[1]?.replaceAll('/', '-') ?? 'package'}-${version}.tgz`
);
const tempExtractDir = path.join(
stageRootDir,
`${path.basename(tempArchivePath, '.tgz')}-extract`
);
await downloadTarball(tarballUrl, tempArchivePath);
await mkdir(tempExtractDir, { recursive: true });
await extractTarball({ file: tempArchivePath, cwd: tempExtractDir });
const extractedBinaryPath = path.join(tempExtractDir, 'package', 'bin', binaryFileName);
if (!(await fileExists(extractedBinaryPath))) {
throw new Error(
`Codex ACP tarball for ${packageName}@${version} did not contain ${binaryFileName}.`
);
}
const outputPath = path.join(stageCacheDir, stripAcpAdaptersPrefix(relativePath));
await mkdir(path.dirname(outputPath), { recursive: true });
await copyFile(extractedBinaryPath, outputPath);
await chmod(outputPath, 0o755);
};
const materializeCodexAdapters = async (
manifest: AcpAdaptersManifest,
stageRootDir: string,
stageCacheDir: string
): Promise<void> => {
await Promise.all(
manifest.codex.map(target =>
extractCodexBinaryFromTarball({
binaryFileName:
RUN_CODEX_ACP_BINARY_TARGETS.find(
candidate => candidate.packageName === target.packageName
)?.binaryFileName ?? 'codex-acp',
packageName: target.packageName,
relativePath: target.relativePath,
stageCacheDir,
stageRootDir,
version: target.version,
})
)
);
};
export const materializeAcpAdaptersCache = async (): Promise<string> => {
const manifest = await buildCacheManifest();
const existingManifest = await readExistingManifest();
if (
existingManifest &&
JSON.stringify(existingManifest) === JSON.stringify(manifest) &&
(await manifestPathsExist(manifest))
) {
return ACP_ADAPTERS_CACHE_DIR;
}
await mkdir(GENERATED_ROOT_DIR, { recursive: true });
const stageRootDir = await mkdtemp(path.join(GENERATED_ROOT_DIR, '.acp-adapters-'));
const stageCacheDir = path.join(stageRootDir, 'acp-adapters');
try {
await mkdir(stageCacheDir, { recursive: true });
await buildClaudeAdapterBundle(stageCacheDir);
await materializeCodexAdapters(manifest, stageRootDir, stageCacheDir);
await rm(ACP_ADAPTERS_CACHE_DIR, { force: true, recursive: true });
await rename(stageCacheDir, ACP_ADAPTERS_CACHE_DIR);
await writeFile(ACP_ADAPTERS_MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
} finally {
await rm(stageRootDir, { force: true, recursive: true });
}
return ACP_ADAPTERS_CACHE_DIR;
};