import { defineEventHandler, readBody, setResponseHeaders } from 'h3' import { execSync } from 'node:child_process' interface InstallBody { agent: 'claude-code' | 'codex-cli' | 'opencode' | 'copilot' | 'gemini-cli' } interface InstallResult { success: boolean error?: string command?: string docsUrl?: string } const BINARY_MAP: Record = { 'claude-code': 'claude', 'codex-cli': 'codex', 'opencode': 'opencode', 'copilot': 'copilot', 'gemini-cli': 'gemini', } function checkBinary(binary: string): boolean { try { const cmd = process.platform === 'win32' ? `where ${binary} 2>nul` : `which ${binary} 2>/dev/null` return !!execSync(cmd, { encoding: 'utf-8', timeout: 5000 }).trim() } catch { return false } } function hasCommand(cmd: string): boolean { return checkBinary(cmd) } function getInstallInfo(agent: string): { command: string; docsUrl: string } { const isWin = process.platform === 'win32' const isMac = process.platform === 'darwin' switch (agent) { case 'claude-code': return { command: 'npm install -g @anthropic-ai/claude-code', docsUrl: 'https://docs.anthropic.com/en/docs/claude-code', } case 'codex-cli': return { command: 'npm install -g @openai/codex', docsUrl: 'https://github.com/openai/codex', } case 'opencode': return { command: isWin ? 'npm install -g opencode-ai' : 'curl -fsSL https://opencode.ai/install | bash', docsUrl: 'https://opencode.ai', } case 'copilot': return { command: isMac ? 'brew install github/copilot/copilot' : isWin ? 'winget install GitHub.CopilotCLI' : 'See documentation', docsUrl: 'https://docs.github.com/copilot/how-tos/copilot-cli', } case 'gemini-cli': return { command: 'npm install -g @anthropic-ai/gemini-cli', docsUrl: 'https://github.com/anthropics/gemini-cli', } default: return { command: '', docsUrl: '' } } } /** * POST /api/ai/install-agent * Attempts to auto-install a CLI agent. Returns manual instructions on failure. */ export default defineEventHandler(async (event) => { const body = await readBody(event) setResponseHeaders(event, { 'Content-Type': 'application/json' }) if (!body?.agent) { return { success: false, error: 'Missing agent field' } satisfies InstallResult } const binary = BINARY_MAP[body.agent] if (!binary) { return { success: false, error: `Unknown agent: ${body.agent}` } satisfies InstallResult } // Already installed if (checkBinary(binary)) { return { success: true } satisfies InstallResult } const info = getInstallInfo(body.agent) // Try auto-install const result = await tryAutoInstall(body.agent, binary) if (result.success) return result // Return failure with manual instructions return { success: false, error: result.error || 'Auto-install failed', command: info.command, docsUrl: info.docsUrl, } satisfies InstallResult }) async function tryAutoInstall(agent: string, binary: string): Promise { switch (agent) { case 'claude-code': return tryNpmInstall('@anthropic-ai/claude-code', binary) case 'codex-cli': return tryNpmInstall('@openai/codex', binary) case 'opencode': return tryOpenCodeInstall(binary) case 'copilot': return tryCopilotInstall(binary) case 'gemini-cli': return tryNpmInstall('@anthropic-ai/gemini-cli', binary) default: return { success: false, error: 'Unknown agent' } } } async function tryNpmInstall(pkg: string, binary: string): Promise { if (!hasCommand('npm')) { return { success: false, error: 'npm not found. Install Node.js first.' } } try { const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm' execSync(`${npmBin} install -g ${pkg}`, { encoding: 'utf-8', timeout: 180_000, stdio: 'pipe', }) } catch (err) { const stderr = (err as { stderr?: string }).stderr || '' const msg = stderr.includes('EACCES') ? 'Permission denied. Try running with sudo or fix npm permissions.' : err instanceof Error ? err.message : 'npm install failed' return { success: false, error: msg } } return checkBinary(binary) ? { success: true } : { success: false, error: 'Install completed but binary not found in PATH' } } async function tryOpenCodeInstall(binary: string): Promise { const isWin = process.platform === 'win32' const cmd = isWin ? 'npm.cmd install -g opencode-ai' : 'curl -fsSL https://opencode.ai/install | bash' try { execSync(cmd, { encoding: 'utf-8', timeout: 120_000, stdio: 'pipe', }) } catch (err) { const msg = err instanceof Error ? err.message : 'Install failed' return { success: false, error: msg } } return checkBinary(binary) ? { success: true } : { success: false, error: 'Install completed but binary not found in PATH' } } async function tryCopilotInstall(binary: string): Promise { // Try brew on macOS if (process.platform === 'darwin' && hasCommand('brew')) { try { execSync('brew install github/copilot/copilot', { encoding: 'utf-8', timeout: 180_000, stdio: 'pipe', }) if (checkBinary(binary)) return { success: true } } catch { // Fall through to failure } } return { success: false, error: 'Auto-install not available for this platform' } }