Skip to content
Draft
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
25 changes: 22 additions & 3 deletions backend/src/database/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -14,9 +15,21 @@ export class DatabaseService {
private run: (sql: string, params?: any[]) => Promise<sqlite3.RunResult>;
private get: (sql: string, params?: any[]) => Promise<any>;
private all: (sql: string, params?: any[]) => Promise<any[]>;
private initializationPromise: Promise<void>;

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[]) => {
Expand All @@ -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<void> {
return this.initializationPromise;
}

private async initialize(): Promise<void> {
Expand Down
292 changes: 162 additions & 130 deletions backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 });
Expand All @@ -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');
Expand All @@ -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 };