import { existsSync, readFileSync, realpathSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; export interface RuntimePathInput { isPackaged: boolean; resourcesPath: string; appPath: string; platform: NodeJS.Platform; arch?: NodeJS.Architecture; } export interface OpencodeRuntimePaths { runtimeDir: string; binPath: string; packageJsonPath: string; } const OPENCODE_RUNTIME_PACKAGE = 'opencode-ai'; const nativePlatformNames: Partial> = { darwin: 'darwin', linux: 'linux', win32: 'windows', }; const nativeArchNames: Partial> = { x64: 'x64', arm64: 'arm64', arm: 'arm', }; function getDevelopmentNativePackageCandidates( platform: NodeJS.Platform, arch: NodeJS.Architecture, ): string[] { const platformName = nativePlatformNames[platform] ?? platform; const archName = nativeArchNames[arch] ?? arch; const base = `opencode-${platformName}-${archName}`; // The baseline x64 package runs on CPUs with and without AVX2, so prefer // it when available. This mirrors the packaged-runtime bundler while // avoiding opencode-ai's postinstall-generated wrapper in pnpm dev trees. return archName === 'x64' ? [`${base}-baseline`, base] : [base]; } function getNativeOpencodeBinName(platform: NodeJS.Platform): string { return platform === 'win32' ? 'opencode.exe' : 'opencode'; } function resolveNativePackageRoot(runtimeDir: string, packageName: string): string | undefined { try { const resolvedRuntimeDir = realpathSync(runtimeDir); const runtimeRequire = createRequire(join(resolvedRuntimeDir, 'package.json')); return dirname(runtimeRequire.resolve(`${packageName}/package.json`)); } catch { return undefined; } } function resolveDevelopmentNativeBinPath( input: RuntimePathInput, runtimeDir: string, ): string | undefined { const arch = input.arch ?? process.arch; for (const packageName of getDevelopmentNativePackageCandidates(input.platform, arch)) { const binaryName = getNativeOpencodeBinName(input.platform); const hoistedBinPath = join(input.appPath, 'node_modules', packageName, 'bin', binaryName); if (existsSync(hoistedBinPath)) return hoistedBinPath; const nativePackageRoot = resolveNativePackageRoot(runtimeDir, packageName); if (!nativePackageRoot) continue; const virtualStoreBinPath = join(nativePackageRoot, 'bin', binaryName); if (existsSync(virtualStoreBinPath)) return virtualStoreBinPath; } return undefined; } export function getOpencodeBinName( platform: NodeJS.Platform = process.platform, isPackaged = false, ): string { if (isPackaged && platform === 'win32') { return 'opencode.exe'; } return 'opencode'; } export function resolveOpencodeRuntimePaths(input: RuntimePathInput): OpencodeRuntimePaths { const runtimeDir = input.isPackaged ? join(input.resourcesPath, OPENCODE_RUNTIME_PACKAGE) : join(input.appPath, 'node_modules', OPENCODE_RUNTIME_PACKAGE); const developmentNativeBinPath = input.isPackaged ? undefined : resolveDevelopmentNativeBinPath(input, runtimeDir); return { runtimeDir, binPath: developmentNativeBinPath ?? join(runtimeDir, 'bin', getOpencodeBinName(input.platform, input.isPackaged)), packageJsonPath: join(runtimeDir, 'package.json'), }; } export function readOpencodeRuntimeVersion(paths: OpencodeRuntimePaths): string | undefined { if (!existsSync(paths.packageJsonPath)) { return undefined; } const packageJson = JSON.parse(readFileSync(paths.packageJsonPath, 'utf8')) as { version?: unknown; }; return typeof packageJson.version === 'string' ? packageJson.version : undefined; }