refactor: Modularize server into separate concerns

Split monolithic index.ts (~1400 lines) into modular structure:
- config.ts: Server configuration and constants
- db/: Database initialization, migrations, and seeds
- routes/: API handlers by domain (themes, canvas, components, etc.)
- services/: Terminal WebSocket server
- utils/: CORS helpers

Entry point now only coordinates initialization.
This commit is contained in:
2026-02-13 13:01:18 -06:00
parent 9681ce4198
commit 645f51a74e
16 changed files with 1503 additions and 1382 deletions

16
server/db/index.ts Normal file
View File

@@ -0,0 +1,16 @@
import { Database } from 'bun:sqlite'
import { DB_PATH } from '../config'
import { runMigrations } from './migrations'
import { runSeeds } from './seeds'
// Create database instance
export const db = new Database(DB_PATH)
// Initialize database
export function initDatabase() {
runMigrations(db)
runSeeds(db)
console.log('[DB] SQLite initialized:', DB_PATH)
}
export { Database }

107
server/db/migrations.ts Normal file
View File

@@ -0,0 +1,107 @@
import type { Database } from 'bun:sqlite'
export function runMigrations(db: Database) {
// History table
db.run(`
CREATE TABLE IF NOT EXISTS history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
tool_name TEXT NOT NULL,
args TEXT,
result TEXT
)
`)
// Config table
db.run(`
CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT
)
`)
// Vue components table
db.run(`
CREATE TABLE IF NOT EXISTS vue_components (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
template TEXT NOT NULL,
setup TEXT,
style TEXT,
props TEXT,
imports TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`)
// Themes table
db.run(`
CREATE TABLE IF NOT EXISTS themes (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
is_default INTEGER DEFAULT 0,
is_system INTEGER DEFAULT 0,
variables TEXT NOT NULL,
metadata TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`)
// Project canvas table
db.run(`
CREATE TABLE IF NOT EXISTS project_canvas (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
type TEXT NOT NULL DEFAULT 'project',
theme_id TEXT,
config TEXT,
tools TEXT,
is_default INTEGER DEFAULT 0,
is_system INTEGER DEFAULT 0,
show_in_toolbar INTEGER DEFAULT 0,
toolbar_icon TEXT,
toolbar_order INTEGER DEFAULT 99,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`)
// Canvas-components relation table
db.run(`
CREATE TABLE IF NOT EXISTS canvas_components (
id INTEGER PRIMARY KEY AUTOINCREMENT,
canvas_id TEXT NOT NULL,
component_id TEXT NOT NULL,
position INTEGER DEFAULT 0,
props TEXT,
layout TEXT,
is_visible INTEGER DEFAULT 1,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(canvas_id, component_id)
)
`)
// Run column migrations for existing tables
runColumnMigrations(db)
}
function runColumnMigrations(db: Database) {
// Add toolbar columns to project_canvas if missing
const alterStatements = [
'ALTER TABLE project_canvas ADD COLUMN show_in_toolbar INTEGER DEFAULT 0',
'ALTER TABLE project_canvas ADD COLUMN toolbar_icon TEXT',
'ALTER TABLE project_canvas ADD COLUMN toolbar_order INTEGER DEFAULT 99'
]
for (const sql of alterStatements) {
try {
db.run(sql)
} catch {
// Column already exists
}
}
}

134
server/db/seeds.ts Normal file
View File

@@ -0,0 +1,134 @@
import type { Database } from 'bun:sqlite'
const darkTheme = {
id: 'theme-dark',
name: 'Dark',
description: 'Default dark theme',
is_default: 1,
is_system: 1,
variables: JSON.stringify({
colors: {
'bg-primary': '#0f0f14',
'bg-secondary': '#16161d',
'bg-hover': '#1e1e28',
'bg-tertiary': '#252530',
'border-color': '#2a2a3a',
'border-hover': '#3a3a4a'
},
text: {
'text-primary': '#e4e4e7',
'text-secondary': '#a1a1aa',
'text-muted': '#52525b',
'text-inverse': '#0f0f14'
},
accent: {
'accent': '#6366f1',
'accent-hover': '#818cf8',
'accent-muted': 'rgba(99, 102, 241, 0.2)',
'accent-text': '#ffffff'
},
semantic: {
'success': '#22c55e',
'success-bg': 'rgba(34, 197, 94, 0.1)',
'warning': '#eab308',
'warning-bg': 'rgba(234, 179, 8, 0.1)',
'error': '#ef4444',
'error-bg': 'rgba(239, 68, 68, 0.1)',
'info': '#3b82f6',
'info-bg': 'rgba(59, 130, 246, 0.1)'
},
spacing: {
'radius-sm': '4px',
'radius-md': '8px',
'radius-lg': '12px',
'radius-full': '9999px'
},
typography: {
'font-sans': "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
'font-mono': "'JetBrains Mono', 'Fira Code', Consolas, monospace"
},
effects: {
'shadow-sm': '0 1px 2px rgba(0,0,0,0.2)',
'shadow-md': '0 4px 6px rgba(0,0,0,0.3)',
'shadow-lg': '0 10px 15px rgba(0,0,0,0.4)',
'transition-fast': '0.15s ease',
'transition-normal': '0.2s ease'
}
}),
metadata: JSON.stringify({ author: 'system', version: '1.0.0', tags: ['dark', 'default'] })
}
const lightTheme = {
id: 'theme-light',
name: 'Light',
description: 'Clean light theme',
is_default: 0,
is_system: 1,
variables: JSON.stringify({
colors: {
'bg-primary': '#ffffff',
'bg-secondary': '#f4f4f5',
'bg-hover': '#e4e4e7',
'bg-tertiary': '#d4d4d8',
'border-color': '#d4d4d8',
'border-hover': '#a1a1aa'
},
text: {
'text-primary': '#18181b',
'text-secondary': '#52525b',
'text-muted': '#a1a1aa',
'text-inverse': '#ffffff'
},
accent: {
'accent': '#4f46e5',
'accent-hover': '#4338ca',
'accent-muted': 'rgba(79, 70, 229, 0.1)',
'accent-text': '#ffffff'
},
semantic: {
'success': '#16a34a',
'success-bg': 'rgba(22, 163, 74, 0.1)',
'warning': '#ca8a04',
'warning-bg': 'rgba(202, 138, 4, 0.1)',
'error': '#dc2626',
'error-bg': 'rgba(220, 38, 38, 0.1)',
'info': '#2563eb',
'info-bg': 'rgba(37, 99, 235, 0.1)'
},
spacing: {
'radius-sm': '4px',
'radius-md': '8px',
'radius-lg': '12px',
'radius-full': '9999px'
},
typography: {
'font-sans': "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
'font-mono': "'JetBrains Mono', 'Fira Code', Consolas, monospace"
},
effects: {
'shadow-sm': '0 1px 2px rgba(0,0,0,0.05)',
'shadow-md': '0 4px 6px rgba(0,0,0,0.07)',
'shadow-lg': '0 10px 15px rgba(0,0,0,0.1)',
'transition-fast': '0.15s ease',
'transition-normal': '0.2s ease'
}
}),
metadata: JSON.stringify({ author: 'system', version: '1.0.0', tags: ['light'] })
}
export function runSeeds(db: Database) {
// Check if system themes exist
const existingThemes = db.query('SELECT COUNT(*) as count FROM themes WHERE is_system = 1').get() as { count: number }
if (existingThemes.count === 0) {
const stmt = db.prepare(`
INSERT INTO themes (id, name, description, is_default, is_system, variables, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?)
`)
stmt.run(darkTheme.id, darkTheme.name, darkTheme.description, darkTheme.is_default, darkTheme.is_system, darkTheme.variables, darkTheme.metadata)
stmt.run(lightTheme.id, lightTheme.name, lightTheme.description, lightTheme.is_default, lightTheme.is_system, lightTheme.variables, lightTheme.metadata)
console.log('[DB] System themes created')
}
}