diff --git a/backend/src/database/database.ts b/backend/src/database/database.ts index 67301d05..96c22969 100644 --- a/backend/src/database/database.ts +++ b/backend/src/database/database.ts @@ -2,6 +2,7 @@ import sqlite3 from 'sqlite3'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { promisify } from 'util'; +import { mkdirSync, existsSync } from 'fs'; import { Repository, Worktree, ClaudeInstance } from '../types.js'; import { MigrationRunner } from './migration-runner.js'; @@ -14,9 +15,21 @@ export class DatabaseService { private run: (sql: string, params?: any[]) => Promise; private get: (sql: string, params?: any[]) => Promise; private all: (sql: string, params?: any[]) => Promise; + private initializationPromise: Promise; - constructor(dbPath: string = 'bob.db') { - this.db = new sqlite3.Database(dbPath); + constructor(dbPath?: string) { + // Use DB_PATH environment variable if available, otherwise fallback to parameter or default + const actualDbPath = dbPath || process.env.DB_PATH || 'bob.db'; + + // Ensure the directory exists + const dbDir = dirname(actualDbPath); + if (!existsSync(dbDir)) { + console.log(`Creating database directory: ${dbDir}`); + mkdirSync(dbDir, { recursive: true }); + } + + console.log(`Initializing database at: ${actualDbPath}`); + this.db = new sqlite3.Database(actualDbPath); // Custom promisify for run method to properly handle the callback signature this.run = (sql: string, params?: any[]) => { @@ -34,7 +47,13 @@ export class DatabaseService { this.get = promisify(this.db.get.bind(this.db)); this.all = promisify(this.db.all.bind(this.db)); this.migrationRunner = new MigrationRunner(this.db); - this.initialize(); + + // Start initialization but don't block constructor + this.initializationPromise = this.initialize(); + } + + async waitForInitialization(): Promise { + return this.initializationPromise; } private async initialize(): Promise { diff --git a/backend/src/server.ts b/backend/src/server.ts index 8f6f571f..817ca876 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -3,6 +3,8 @@ import cors from 'cors'; import path from 'path'; import { createServer } from 'http'; import { WebSocketServer } from 'ws'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; import { GitService } from './services/git.js'; import { ClaudeService } from './services/claude.js'; import { TerminalService } from './services/terminal.js'; @@ -13,6 +15,9 @@ import { createFilesystemRoutes } from './routes/filesystem.js'; import { createDatabaseRoutes } from './routes/database.js'; import gitRoutes from './routes/git.js'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + const app = express(); const server = createServer(app); const wss = new WebSocketServer({ server }); @@ -22,149 +27,178 @@ const PORT = process.env.PORT || 43829; app.use(cors()); app.use(express.json()); -// Initialize database -const db = new DatabaseService(); -console.log('Database initialized'); - -// Initialize services -const gitService = new GitService(db); -const claudeService = new ClaudeService(gitService, db); -const terminalService = new TerminalService(); - -console.log('Services initialized'); - -app.use('/api/repositories', createRepositoryRoutes(gitService, claudeService)); -app.use('/api/instances', createInstanceRoutes(claudeService, terminalService, gitService)); -app.use('/api/filesystem', createFilesystemRoutes()); -app.use('/api/database', createDatabaseRoutes(db)); +// Global service references for graceful shutdown +let db: DatabaseService; +let gitService: GitService; +let claudeService: ClaudeService; +let terminalService: TerminalService; -// Make services available to git routes -app.locals.gitService = gitService; -app.locals.claudeService = claudeService; -app.locals.databaseService = db; -app.use('/api/git', gitRoutes); - -app.get('/api/health', (req, res) => { - res.json({ status: 'ok', timestamp: new Date().toISOString() }); -}); - -app.get('/api/system-status', async (req, res) => { +// Initialize database and start server +async function startServer() { try { - const { exec } = await import('child_process'); - const { promisify } = await import('util'); - const execAsync = promisify(exec); - - // Check Claude CLI availability - let claudeStatus = 'unknown'; - let claudeVersion = ''; - try { - const { stdout } = await execAsync('claude --version'); - claudeVersion = stdout.trim(); - claudeStatus = 'available'; - } catch (error) { - claudeStatus = 'not_available'; - } - - // Check GitHub CLI availability - let githubStatus = 'unknown'; - let githubVersion = ''; - let githubUser = ''; - try { - const { stdout: versionOut } = await execAsync('gh --version'); - githubVersion = versionOut.split('\n')[0]?.trim() || ''; - githubStatus = 'available'; + console.log('Starting Bob server...'); + + // Initialize database + db = new DatabaseService(); + await db.waitForInitialization(); + console.log('Database initialized'); + + // Initialize services + gitService = new GitService(db); + claudeService = new ClaudeService(gitService, db); + terminalService = new TerminalService(); + + console.log('Services initialized'); + + app.use('/api/repositories', createRepositoryRoutes(gitService, claudeService)); + app.use('/api/instances', createInstanceRoutes(claudeService, terminalService, gitService)); + app.use('/api/filesystem', createFilesystemRoutes()); + app.use('/api/database', createDatabaseRoutes(db)); + + // Make services available to git routes + app.locals.gitService = gitService; + app.locals.claudeService = claudeService; + app.locals.databaseService = db; + app.use('/api/git', gitRoutes); + + app.get('/api/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); + }); + app.get('/api/system-status', async (req, res) => { try { - const { stdout: userOut } = await execAsync('gh api user --jq .login'); - githubUser = userOut.trim(); - } catch (userError) { - githubStatus = 'not_authenticated'; + const { exec } = await import('child_process'); + const { promisify } = await import('util'); + const execAsync = promisify(exec); + + // Check Claude CLI availability + let claudeStatus = 'unknown'; + let claudeVersion = ''; + try { + const { stdout } = await execAsync('claude --version'); + claudeVersion = stdout.trim(); + claudeStatus = 'available'; + } catch (error) { + claudeStatus = 'not_available'; + } + + // Check GitHub CLI availability + let githubStatus = 'unknown'; + let githubVersion = ''; + let githubUser = ''; + try { + const { stdout: versionOut } = await execAsync('gh --version'); + githubVersion = versionOut.split('\n')[0]?.trim() || ''; + githubStatus = 'available'; + + try { + const { stdout: userOut } = await execAsync('gh api user --jq .login'); + githubUser = userOut.trim(); + } catch (userError) { + githubStatus = 'not_authenticated'; + } + } catch (error) { + githubStatus = 'not_available'; + } + + // Get system metrics + const gitService = req.app.locals.gitService; + const claudeService = req.app.locals.claudeService; + + const repositories = gitService.getRepositories(); + // Count only actual worktrees (exclude main working trees) + const totalWorktrees = repositories.reduce((count: number, repo: any) => { + const actualWorktrees = repo.worktrees.filter((worktree: any) => !worktree.isMainWorktree); + return count + actualWorktrees.length; + }, 0); + const instances = claudeService.getInstances(); + const activeInstances = instances.filter((i: any) => i.status === 'running' || i.status === 'starting').length; + + res.json({ + claude: { + status: claudeStatus, + version: claudeVersion + }, + github: { + status: githubStatus, + version: githubVersion, + user: githubUser + }, + metrics: { + repositories: repositories.length, + worktrees: totalWorktrees, + totalInstances: instances.length, + activeInstances: activeInstances + }, + server: { + uptime: process.uptime(), + memory: process.memoryUsage(), + nodeVersion: process.version + } + }); + } catch (error) { + console.error('Error getting system status:', error); + res.status(500).json({ error: 'Failed to get system status' }); } - } catch (error) { - githubStatus = 'not_available'; + }); + + // Serve static files from frontend build (only in production) + const isProduction = process.env.NODE_ENV === 'production'; + if (isProduction) { + const frontendPath = path.join(__dirname, '../../frontend/dist'); + app.use(express.static(frontendPath)); + + // Serve index.html for all non-API routes (SPA routing) + app.get('*', (req, res) => { + res.sendFile(path.join(frontendPath, 'index.html')); + }); + } else { + // In development, just show a message for non-API routes + app.get('*', (req, res) => { + res.json({ + message: 'Bob backend running in development mode', + frontend: 'http://localhost:47285', + api: `http://localhost:${PORT}/api` + }); + }); } - // Get system metrics - const gitService = req.app.locals.gitService; - const claudeService = req.app.locals.claudeService; - - const repositories = gitService.getRepositories(); - // Count only actual worktrees (exclude main working trees) - const totalWorktrees = repositories.reduce((count: number, repo: any) => { - const actualWorktrees = repo.worktrees.filter((worktree: any) => !worktree.isMainWorktree); - return count + actualWorktrees.length; - }, 0); - const instances = claudeService.getInstances(); - const activeInstances = instances.filter((i: any) => i.status === 'running' || i.status === 'starting').length; - - res.json({ - claude: { - status: claudeStatus, - version: claudeVersion - }, - github: { - status: githubStatus, - version: githubVersion, - user: githubUser - }, - metrics: { - repositories: repositories.length, - worktrees: totalWorktrees, - totalInstances: instances.length, - activeInstances: activeInstances - }, - server: { - uptime: process.uptime(), - memory: process.memoryUsage(), - nodeVersion: process.version - } - }); - } catch (error) { - console.error('Error getting system status:', error); - res.status(500).json({ error: 'Failed to get system status' }); - } -}); + wss.on('connection', (ws, req) => { + const url = new URL(req.url!, `http://${req.headers.host}`); + const sessionId = url.searchParams.get('sessionId'); -// Serve static files from frontend build (only in production) -const isProduction = process.env.NODE_ENV === 'production'; -if (isProduction) { - const frontendPath = path.join(__dirname, '../../frontend/dist'); - app.use(express.static(frontendPath)); + if (!sessionId) { + ws.close(1000, 'Session ID required'); + return; + } - // Serve index.html for all non-API routes (SPA routing) - app.get('*', (req, res) => { - res.sendFile(path.join(frontendPath, 'index.html')); - }); -} else { - // In development, just show a message for non-API routes - app.get('*', (req, res) => { - res.json({ - message: 'Bob backend running in development mode', - frontend: 'http://localhost:47285', - api: `http://localhost:${PORT}/api` + console.log(`WebSocket connection for terminal session: ${sessionId}`); + terminalService.attachWebSocket(sessionId, ws); }); - }); -} -wss.on('connection', (ws, req) => { - const url = new URL(req.url!, `http://${req.headers.host}`); - const sessionId = url.searchParams.get('sessionId'); + server.listen(PORT, () => { + console.log(`Bob server running on port ${PORT}`); + console.log(`WebSocket server ready for terminal connections`); + }); - if (!sessionId) { - ws.close(1000, 'Session ID required'); - return; + } catch (error) { + console.error('Failed to start server:', error); + process.exit(1); } - - console.log(`WebSocket connection for terminal session: ${sessionId}`); - terminalService.attachWebSocket(sessionId, ws); -}); +} const gracefulShutdown = async () => { console.log('Shutting down gracefully...'); - await claudeService.cleanup(); - terminalService.cleanup(); - db.close(); + if (claudeService) { + await claudeService.cleanup(); + } + if (terminalService) { + terminalService.cleanup(); + } + if (db) { + db.close(); + } server.close(() => { console.log('Server closed'); @@ -175,9 +209,7 @@ const gracefulShutdown = async () => { process.on('SIGTERM', gracefulShutdown); process.on('SIGINT', gracefulShutdown); -server.listen(PORT, () => { - console.log(`Bob server running on port ${PORT}`); - console.log(`WebSocket server ready for terminal connections`); -}); +// Start the server +startServer(); export { app, server }; \ No newline at end of file