diff --git a/apps/server/src/providers/opencode-provider.ts b/apps/server/src/providers/opencode-provider.ts index 6dbef31a..42a7045f 100644 --- a/apps/server/src/providers/opencode-provider.ts +++ b/apps/server/src/providers/opencode-provider.ts @@ -180,18 +180,21 @@ export class OpencodeProvider extends CliProvider { npxPackage: 'opencode-ai@latest', commonPaths: { linux: [ + path.join(os.homedir(), '.opencode/bin/opencode'), path.join(os.homedir(), '.npm-global/bin/opencode'), '/usr/local/bin/opencode', '/usr/bin/opencode', path.join(os.homedir(), '.local/bin/opencode'), ], darwin: [ + path.join(os.homedir(), '.opencode/bin/opencode'), path.join(os.homedir(), '.npm-global/bin/opencode'), '/usr/local/bin/opencode', '/opt/homebrew/bin/opencode', path.join(os.homedir(), '.local/bin/opencode'), ], win32: [ + path.join(os.homedir(), '.opencode', 'bin', 'opencode.exe'), path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'opencode.cmd'), path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'opencode'), path.join(process.env.APPDATA || '', 'npm', 'opencode.cmd'), diff --git a/apps/server/src/routes/setup/index.ts b/apps/server/src/routes/setup/index.ts index 3fac6a20..30c2dbc9 100644 --- a/apps/server/src/routes/setup/index.ts +++ b/apps/server/src/routes/setup/index.ts @@ -17,6 +17,7 @@ import { createCursorStatusHandler } from './routes/cursor-status.js'; import { createCodexStatusHandler } from './routes/codex-status.js'; import { createInstallCodexHandler } from './routes/install-codex.js'; import { createAuthCodexHandler } from './routes/auth-codex.js'; +import { createOpencodeStatusHandler } from './routes/opencode-status.js'; import { createGetCursorConfigHandler, createSetCursorDefaultModelHandler, @@ -49,6 +50,9 @@ export function createSetupRoutes(): Router { router.get('/codex-status', createCodexStatusHandler()); router.post('/install-codex', createInstallCodexHandler()); router.post('/auth-codex', createAuthCodexHandler()); + + // OpenCode CLI routes + router.get('/opencode-status', createOpencodeStatusHandler()); router.get('/cursor-config', createGetCursorConfigHandler()); router.post('/cursor-config/default-model', createSetCursorDefaultModelHandler()); router.post('/cursor-config/models', createSetCursorModelsHandler()); diff --git a/apps/server/src/routes/setup/routes/opencode-status.ts b/apps/server/src/routes/setup/routes/opencode-status.ts new file mode 100644 index 00000000..7e8edd5e --- /dev/null +++ b/apps/server/src/routes/setup/routes/opencode-status.ts @@ -0,0 +1,57 @@ +/** + * GET /opencode-status endpoint - Get OpenCode CLI installation and auth status + */ + +import type { Request, Response } from 'express'; +import { OpencodeProvider } from '../../../providers/opencode-provider.js'; +import { getErrorMessage, logError } from '../common.js'; + +/** + * Creates handler for GET /api/setup/opencode-status + * Returns OpenCode CLI installation and authentication status + */ +export function createOpencodeStatusHandler() { + const installCommand = 'curl -fsSL https://opencode.ai/install | bash'; + const loginCommand = 'opencode auth'; + + return async (_req: Request, res: Response): Promise => { + try { + const provider = new OpencodeProvider(); + const status = await provider.detectInstallation(); + + // Derive auth method from authenticated status and API key presence + let authMethod = 'none'; + if (status.authenticated) { + authMethod = status.hasApiKey ? 'api_key_env' : 'cli_authenticated'; + } + + res.json({ + success: true, + installed: status.installed, + version: status.version || null, + path: status.path || null, + auth: { + authenticated: status.authenticated || false, + method: authMethod, + hasApiKey: status.hasApiKey || false, + hasEnvApiKey: !!process.env.ANTHROPIC_API_KEY || !!process.env.OPENAI_API_KEY, + hasOAuthToken: false, // OpenCode doesn't use OAuth + }, + recommendation: status.installed + ? undefined + : 'Install OpenCode CLI to use multi-provider AI models.', + installCommands: { + macos: installCommand, + linux: installCommand, + npm: 'npm install -g opencode-ai', + }, + }); + } catch (error) { + logError(error, 'Get OpenCode status failed'); + res.status(500).json({ + success: false, + error: getErrorMessage(error), + }); + } + }; +} diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index 12172ee9..6853c775 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -1284,6 +1284,32 @@ export class HttpApiClient implements ElectronAPI { error?: string; }> => this.post('/api/setup/verify-codex-auth', { authMethod, apiKey }), + // OpenCode CLI methods + getOpencodeStatus: (): Promise<{ + success: boolean; + status?: string; + installed?: boolean; + method?: string; + version?: string; + path?: string; + recommendation?: string; + installCommands?: { + macos?: string; + linux?: string; + npm?: string; + }; + auth?: { + authenticated: boolean; + method: string; + hasAuthFile?: boolean; + hasOAuthToken?: boolean; + hasApiKey?: boolean; + hasStoredApiKey?: boolean; + hasEnvApiKey?: boolean; + }; + error?: string; + }> => this.get('/api/setup/opencode-status'), + onInstallProgress: (callback: (progress: unknown) => void) => { return this.subscribeToEvent('agent:stream', callback); }, diff --git a/libs/platform/src/system-paths.ts b/libs/platform/src/system-paths.ts index f9f98ae6..a0cbff27 100644 --- a/libs/platform/src/system-paths.ts +++ b/libs/platform/src/system-paths.ts @@ -1034,6 +1034,8 @@ export function getOpenCodeCliPaths(): string[] { const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'); const localAppData = process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'); return [ + // OpenCode's default installation directory + path.join(homeDir, '.opencode', 'bin', 'opencode.exe'), path.join(homeDir, '.local', 'bin', 'opencode.exe'), path.join(appData, 'npm', 'opencode.cmd'), path.join(appData, 'npm', 'opencode'), @@ -1060,6 +1062,8 @@ export function getOpenCodeCliPaths(): string[] { const pnpmHome = process.env.PNPM_HOME || path.join(homeDir, '.local', 'share', 'pnpm'); return [ + // OpenCode's default installation directory + path.join(homeDir, '.opencode', 'bin', 'opencode'), // Standard locations path.join(homeDir, '.local', 'bin', 'opencode'), '/opt/homebrew/bin/opencode',