diff --git a/backend/src/routes/settings.ts b/backend/src/routes/settings.ts index 046a6413..e88b7082 100644 --- a/backend/src/routes/settings.ts +++ b/backend/src/routes/settings.ts @@ -1,18 +1,32 @@ import { Hono } from 'hono' import { z } from 'zod' +import { execSync } from 'child_process' import type { Database } from 'bun:sqlite' import { SettingsService } from '../services/settings' import { writeFileContent, readFileContent, fileExists } from '../services/file-operations' import { patchOpenCodeConfig, proxyToOpenCodeWithDirectory } from '../services/proxy' import { getOpenCodeConfigFilePath, getAgentsMdPath } from '@opencode-manager/shared/config/env' -import { - UserPreferencesSchema, +import { + UserPreferencesSchema, OpenCodeConfigSchema, } from '../types/settings' import { logger } from '../utils/logger' import { opencodeServerManager } from '../services/opencode-single-server' import { DEFAULT_AGENTS_MD } from '../index' +function compareVersions(v1: string, v2: string): number { + const parts1 = v1.split('.').map(s => Number(s)) + const parts2 = v2.split('.').map(s => Number(s)) + + for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { + const p1 = parts1[i] || 0 + const p2 = parts2[i] || 0 + if (p1 > p2) return 1 + if (p1 < p2) return -1 + } + return 0 +} + const UpdateSettingsSchema = z.object({ preferences: UserPreferencesSchema.partial(), }) @@ -321,6 +335,57 @@ export function createSettingsRoutes(db: Database) { } }) + app.post('/opencode-upgrade', async (c) => { + try { + logger.info('OpenCode upgrade requested') + + const oldVersion = opencodeServerManager.getVersion() + logger.info(`Current OpenCode version: ${oldVersion}`) + + logger.info('Running opencode upgrade...') + const upgradeOutput = execSync('opencode upgrade 2>&1', { encoding: 'utf8' }) + logger.info(`Upgrade output: ${upgradeOutput}`) + + await new Promise(r => setTimeout(r, 2000)) + + const newVersion = opencodeServerManager.getVersion() || await opencodeServerManager.fetchVersion() + + logger.info(`New OpenCode version: ${newVersion}`) + + const upgraded = oldVersion && newVersion && compareVersions(newVersion, oldVersion) > 0 + + if (upgraded) { + logger.info(`OpenCode upgraded from v${oldVersion} to v${newVersion}`) + opencodeServerManager.clearStartupError() + await opencodeServerManager.restart() + logger.info('OpenCode server restarted after upgrade') + + return c.json({ + success: true, + message: `OpenCode upgraded from v${oldVersion} to v${newVersion} and server restarted`, + oldVersion, + newVersion, + upgraded: true + }) + } else { + logger.info('OpenCode is already up to date or version unchanged') + return c.json({ + success: true, + message: 'OpenCode is already up to date', + oldVersion, + newVersion: oldVersion, + upgraded: false + }) + } + } catch (error) { + logger.error('Failed to upgrade OpenCode:', error) + return c.json({ + error: 'Failed to upgrade OpenCode', + details: error instanceof Error ? error.message : 'Unknown error' + }, 500) + } + }) + // Custom Commands routes app.get('/custom-commands', async (c) => { try { diff --git a/frontend/src/api/settings.ts b/frontend/src/api/settings.ts index db24a18d..375ad684 100644 --- a/frontend/src/api/settings.ts +++ b/frontend/src/api/settings.ts @@ -108,6 +108,11 @@ export const settingsApi = { return data }, + upgradeOpenCode: async (): Promise<{ success: boolean; message: string; oldVersion?: string; newVersion?: string; upgraded: boolean }> => { + const { data } = await axios.post(`${API_BASE_URL}/api/settings/opencode-upgrade`) + return data + }, + getAgentsMd: async (): Promise<{ content: string }> => { const { data } = await axios.get(`${API_BASE_URL}/api/settings/agents-md`) return data diff --git a/frontend/src/components/settings/OpenCodeConfigManager.tsx b/frontend/src/components/settings/OpenCodeConfigManager.tsx index a22c9f21..684c23ab 100644 --- a/frontend/src/components/settings/OpenCodeConfigManager.tsx +++ b/frontend/src/components/settings/OpenCodeConfigManager.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useRef } from 'react' -import { Loader2, Plus, Trash2, Edit, Star, StarOff, Download, RotateCcw, FileText } from 'lucide-react' +import { Loader2, Plus, Trash2, Edit, Star, StarOff, Download, RotateCcw, FileText, ArrowUpCircle } from 'lucide-react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Label } from '@/components/ui/label' @@ -88,6 +88,24 @@ export function OpenCodeConfigManager() { }, }) + const upgradeOpenCodeMutation = useMutation({ + mutationFn: async () => { + return await settingsApi.upgradeOpenCode() + }, + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ['health'] }) + queryClient.invalidateQueries({ queryKey: ['opencode', 'agents'] }) + if (data.upgraded) { + showToast.success(`Upgraded to v${data.newVersion} and server restarted`, { id: 'upgrade-opencode' }) + } else { + showToast.success('OpenCode is already up to date', { id: 'upgrade-opencode' }) + } + }, + onError: () => { + showToast.error('Failed to upgrade OpenCode', { id: 'upgrade-opencode' }) + }, + }) + const getRestartErrorMessage = (error: unknown): string => { return error && typeof error === 'object' && 'response' in error ? ((error as { response?: { data?: { details?: string; error?: string } } }).response?.data?.details @@ -310,6 +328,31 @@ export function OpenCodeConfigManager() { )}