Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { NotificationService } from './services/notification'
import { ScheduleRunner, ScheduleService } from './services/schedules'
import { migrateGlobalSkills } from './services/skills'
import { getOpenCodeImportStatus, syncOpenCodeImport } from './services/opencode-import'
import { OpenCodeSupervisor } from './services/opencode-supervisor'
import { OpenCodeConfigSchema } from '@opencode-manager/shared/schemas'
import { parse as parseJsonc } from 'jsonc-parser'

Expand Down Expand Up @@ -84,6 +85,7 @@ import { DEFAULT_AGENTS_MD } from './constants'

let ipcServer: IPCServer | undefined
const gitAuthService = new GitAuthService()
let openCodeSupervisor: OpenCodeSupervisor | undefined
async function ensureDefaultConfigExists(): Promise<void> {
const settingsService = new SettingsService(db)
const workspaceConfigPath = getOpenCodeConfigFilePath()
Expand Down Expand Up @@ -208,15 +210,23 @@ try {
const settingsService = new SettingsService(db)
settingsService.initializeLastKnownGoodConfig()

openCodeSupervisor = new OpenCodeSupervisor(opencodeServerManager, settingsService, {
userId: 'default'
})

await migrateGlobalSkills()

ipcServer = await createIPCServer(process.env.STORAGE_PATH || undefined)
await gitAuthService.initialize(ipcServer, db)
logger.info(`Git IPC server running at ${ipcServer.ipcHandlePath}`)

opencodeServerManager.setDatabase(db)
await opencodeServerManager.start()
logger.info(`OpenCode server running on port ${opencodeServerManager.getPort()}`)
const openCodeStatus = await openCodeSupervisor.start()
if (openCodeStatus.healthy) {
logger.info(`OpenCode server running on port ${openCodeStatus.port}`)
} else {
logger.warn(`OpenCode server unavailable after startup recovery: ${openCodeStatus.lastError ?? openCodeStatus.state}`)
}

await syncAdminFromEnv(auth, db)
} catch (error) {
Expand Down Expand Up @@ -251,18 +261,18 @@ void scheduleRunnerInstance.start()

app.route('/api/auth', createAuthRoutes(auth))
app.route('/api/auth-info', createAuthInfoRoutes(auth, db))
app.route('/api/health', createHealthRoutes(db))
app.route('/api/health', createHealthRoutes(db, openCodeSupervisor))

app.route('/api/mcp-oauth-proxy', createMcpOauthProxyRoutes(requireAuth))

const protectedApi = new Hono()
protectedApi.use('/*', requireAuth)

protectedApi.route('/repos', createRepoRoutes(db, gitAuthService, scheduleService))
protectedApi.route('/settings', createSettingsRoutes(db, gitAuthService))
protectedApi.route('/repos', createRepoRoutes(db, gitAuthService, scheduleService, openCodeSupervisor))
protectedApi.route('/settings', createSettingsRoutes(db, gitAuthService, openCodeSupervisor))
protectedApi.route('/files', createFileRoutes())
protectedApi.route('/providers', createProvidersRoutes())
protectedApi.route('/oauth', createOAuthRoutes())
protectedApi.route('/providers', createProvidersRoutes(openCodeSupervisor))
protectedApi.route('/oauth', createOAuthRoutes(openCodeSupervisor))
protectedApi.route('/tts', createTTSRoutes(db))
protectedApi.route('/stt', createSTTRoutes(db))
protectedApi.route('/sse', createSSERoutes())
Expand Down Expand Up @@ -373,9 +383,14 @@ const shutdown = async (signal: string) => {
await ipcServer.dispose()
logger.info('Git IPC server stopped')
}
if (openCodeSupervisor) {
await openCodeSupervisor.stop()
}
scheduleRunnerInstance?.stop()
logger.info('Schedule runner stopped')
await opencodeServerManager.stop()
if (!openCodeSupervisor) {
await opencodeServerManager.stop()
}
logger.info('OpenCode server stopped')
} catch (error) {
logger.error('Error during shutdown:', error)
Expand Down
30 changes: 22 additions & 8 deletions backend/src/routes/health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Hono } from 'hono'
import type { Database } from 'bun:sqlite'
import { readFile } from 'fs/promises'
import { opencodeServerManager } from '../services/opencode-single-server'
import type { OpenCodeSupervisor } from '../services/opencode-supervisor'
import { compareVersions } from '../utils/version-utils'

const GITHUB_REPO_OWNER = 'chriswritescode-dev'
Expand Down Expand Up @@ -66,17 +67,22 @@ const opencodeManagerVersionPromise = (async (): Promise<string | null> => {
}
})()

export function createHealthRoutes(db: Database) {
export function createHealthRoutes(db: Database, openCodeSupervisor?: OpenCodeSupervisor) {
const app = new Hono()

app.get('/', async (c) => {
try {
const opencodeManagerVersion = await opencodeManagerVersionPromise
const dbCheck = db.prepare('SELECT 1').get()
const opencodeHealthy = await opencodeServerManager.checkHealth()
const startupError = opencodeServerManager.getLastStartupError()

const status = startupError && !opencodeHealthy
const lifecycle = openCodeSupervisor
? await openCodeSupervisor.checkNow('api_probe')
: null
const opencodeHealthy = lifecycle?.healthy ?? await opencodeServerManager.checkHealth()
const startupError = lifecycle?.lastError ?? opencodeServerManager.getLastStartupError()

const status = lifecycle?.state === 'recovering'
? 'degraded'
: startupError && !opencodeHealthy
? 'unhealthy'
: (dbCheck && opencodeHealthy ? 'healthy' : 'degraded')

Expand All @@ -92,6 +98,10 @@ export function createHealthRoutes(db: Database) {
opencodeManagerVersion,
}

if (lifecycle) {
response.opencodeLifecycle = lifecycle
}

if (startupError && !opencodeHealthy) {
response.error = startupError
}
Expand All @@ -110,12 +120,16 @@ export function createHealthRoutes(db: Database) {

app.get('/processes', async (c) => {
try {
const opencodeHealthy = await opencodeServerManager.checkHealth()

const lifecycle = openCodeSupervisor
? await openCodeSupervisor.checkNow('api_probe')
: null
const opencodeHealthy = lifecycle?.healthy ?? await opencodeServerManager.checkHealth()

return c.json({
opencode: {
port: opencodeServerManager.getPort(),
healthy: opencodeHealthy
healthy: opencodeHealthy,
lifecycle,
},
timestamp: new Date().toISOString()
})
Expand Down
14 changes: 12 additions & 2 deletions backend/src/routes/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,18 @@ import {
OAuthCallbackRequestSchema
} from '../../../shared/src/schemas/auth'
import { opencodeServerManager } from '../services/opencode-single-server'
import type { OpenCodeSupervisor } from '../services/opencode-supervisor'

export function createOAuthRoutes() {
async function reloadOpenCodeConfig(openCodeSupervisor?: OpenCodeSupervisor): Promise<void> {
if (openCodeSupervisor) {
await openCodeSupervisor.reloadConfig('settings_reload')
return
}

await opencodeServerManager.reloadConfig()
}

export function createOAuthRoutes(openCodeSupervisor?: OpenCodeSupervisor) {
const app = new Hono()

app.post('/:id/oauth/authorize', async (c) => {
Expand Down Expand Up @@ -76,7 +86,7 @@ export function createOAuthRoutes() {
const data = await response.json()

try {
await opencodeServerManager.reloadConfig()
await reloadOpenCodeConfig(openCodeSupervisor)
} catch (reloadError) {
logger.warn(`Failed to reload OpenCode config after OAuth callback for ${providerId}:`, reloadError)
}
Expand Down
16 changes: 13 additions & 3 deletions backend/src/routes/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,18 @@ import { SetCredentialRequestSchema } from '../../../shared/src/schemas/auth'
import { logger } from '../utils/logger'
import { setOpenCodeAuth, deleteOpenCodeAuth } from '../services/proxy'
import { opencodeServerManager } from '../services/opencode-single-server'
import type { OpenCodeSupervisor } from '../services/opencode-supervisor'

export function createProvidersRoutes() {
async function reloadOpenCodeConfig(openCodeSupervisor?: OpenCodeSupervisor): Promise<void> {
if (openCodeSupervisor) {
await openCodeSupervisor.reloadConfig('settings_reload')
return
}

await opencodeServerManager.reloadConfig()
}

export function createProvidersRoutes(openCodeSupervisor?: OpenCodeSupervisor) {
const app = new Hono()
const authService = new AuthService()

Expand Down Expand Up @@ -45,7 +55,7 @@ export function createProvidersRoutes() {
await authService.set(providerId, validated.apiKey)

try {
await opencodeServerManager.reloadConfig()
await reloadOpenCodeConfig(openCodeSupervisor)
} catch (reloadError) {
logger.warn(`Failed to reload OpenCode config after saving credentials for ${providerId}:`, reloadError)
}
Expand All @@ -72,7 +82,7 @@ export function createProvidersRoutes() {
await authService.delete(providerId)

try {
await opencodeServerManager.reloadConfig()
await reloadOpenCodeConfig(openCodeSupervisor)
} catch (reloadError) {
logger.warn(`Failed to reload OpenCode config after deleting credentials for ${providerId}:`, reloadError)
}
Expand Down
23 changes: 19 additions & 4 deletions backend/src/routes/repos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as archiveService from '../services/archive'
import { SettingsService } from '../services/settings'
import { writeFileContent } from '../services/file-operations'
import { opencodeServerManager } from '../services/opencode-single-server'
import type { OpenCodeSupervisor } from '../services/opencode-supervisor'
import { proxyToOpenCodeWithDirectory } from '../services/proxy'
import { logger } from '../utils/logger'
import { getErrorMessage, getStatusCode } from '../utils/error-utils'
Expand All @@ -19,7 +20,22 @@ import { ScheduleService } from '../services/schedules'
import { ensureAssistantMode, getAssistantModeStatus } from '../services/assistant-mode'
import path from 'path'

export function createRepoRoutes(database: Database, gitAuthService: GitAuthService, scheduleService: ScheduleService) {
async function restartOpenCode(openCodeSupervisor?: OpenCodeSupervisor): Promise<void> {
if (openCodeSupervisor) {
await openCodeSupervisor.restart('settings_restart')
return
}

opencodeServerManager.clearStartupError()
await opencodeServerManager.restart()
}

export function createRepoRoutes(
database: Database,
gitAuthService: GitAuthService,
scheduleService: ScheduleService,
openCodeSupervisor?: OpenCodeSupervisor,
) {
const app = new Hono()

app.route('/', createRepoGitRoutes(database, gitAuthService))
Expand Down Expand Up @@ -235,8 +251,7 @@ app.get('/', async (c) => {
logger.info(`Updated OpenCode config: ${openCodeConfigPath}`)

logger.info('Restarting OpenCode server due to workspace config change')
await opencodeServerManager.stop()
await opencodeServerManager.start()
await restartOpenCode(openCodeSupervisor)

const updatedRepo = getRepoById(database, id)
return c.json(updatedRepo)
Expand Down Expand Up @@ -410,7 +425,7 @@ app.get('/', async (c) => {
const status = await ensureAssistantMode(repo, options)
if (status.files.opencodeJson.created) {
opencodeServerManager.clearStartupError()
await opencodeServerManager.restart()
await restartOpenCode(openCodeSupervisor)
}
return c.json(status)
} catch (error: unknown) {
Expand Down
Loading
Loading