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

250
server/routes/canvas.ts Normal file
View File

@@ -0,0 +1,250 @@
import { db } from '../db'
import { jsonResponse, errorResponse } from '../utils/cors'
function parseCanvas(row: any) {
return {
...row,
is_default: !!row.is_default,
is_system: !!row.is_system,
show_in_toolbar: !!row.show_in_toolbar,
config: row.config ? JSON.parse(row.config) : null,
tools: row.tools ? JSON.parse(row.tools) : []
}
}
export function handleToolbarCanvas() {
const rows = db.query('SELECT * FROM project_canvas WHERE show_in_toolbar = 1 ORDER BY toolbar_order ASC, name ASC').all()
return jsonResponse((rows as any[]).map(parseCanvas))
}
export function handleDefaultCanvas() {
const row = db.query('SELECT * FROM project_canvas WHERE is_default = 1 LIMIT 1').get() as any
if (!row) {
return jsonResponse({ hasDefault: false })
}
return jsonResponse({ hasDefault: true, canvas: parseCanvas(row) })
}
export async function handleCanvas(req: Request) {
if (req.method === 'GET') {
const rows = db.query('SELECT * FROM project_canvas ORDER BY is_system DESC, is_default DESC, name ASC').all()
return jsonResponse((rows as any[]).map(parseCanvas))
}
if (req.method === 'POST') {
const body = await req.json()
const id = body.id || `canvas-${Date.now()}`
const stmt = db.prepare(`
INSERT OR REPLACE INTO project_canvas
(id, name, description, type, theme_id, config, tools, is_default, is_system, show_in_toolbar, toolbar_icon, toolbar_order, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`)
stmt.run(
id,
body.name,
body.description || '',
body.type || 'project',
body.theme_id || null,
JSON.stringify(body.config || {}),
JSON.stringify(body.tools || []),
body.is_default ? 1 : 0,
body.is_system ? 1 : 0,
body.show_in_toolbar ? 1 : 0,
body.toolbar_icon || null,
body.toolbar_order ?? 99
)
return jsonResponse({ success: true, id })
}
return null
}
export async function handleCanvasById(req: Request, id: string, action?: string) {
// POST /api/canvas/:id/clone
if (action === 'clone' && req.method === 'POST') {
const original = db.query('SELECT * FROM project_canvas WHERE id = ?').get(id) as any
if (!original) {
return errorResponse('Canvas not found', 404)
}
const body = await req.json()
const newId = `canvas-${Date.now()}`
const newName = body.name || `${original.name} (copia)`
const stmt = db.prepare(`
INSERT INTO project_canvas
(id, name, description, type, theme_id, config, tools, is_default, is_system)
VALUES (?, ?, ?, 'project', ?, ?, ?, 0, 0)
`)
stmt.run(newId, newName, original.description, original.theme_id, original.config, original.tools)
// Clone canvas components
const components = db.query('SELECT * FROM canvas_components WHERE canvas_id = ?').all(id) as any[]
if (components.length > 0) {
const compStmt = db.prepare(`
INSERT INTO canvas_components (canvas_id, component_id, position, props, layout, is_visible)
VALUES (?, ?, ?, ?, ?, ?)
`)
for (const comp of components) {
compStmt.run(newId, comp.component_id, comp.position, comp.props, comp.layout, comp.is_visible)
}
}
return jsonResponse({ success: true, id: newId })
}
// GET /api/canvas/:id
if (req.method === 'GET' && !action) {
const row = db.query('SELECT * FROM project_canvas WHERE id = ?').get(id) as any
if (!row) {
return errorResponse('Canvas not found', 404)
}
return jsonResponse(parseCanvas(row))
}
// PUT /api/canvas/:id
if (req.method === 'PUT' && !action) {
const canvas = db.query('SELECT * FROM project_canvas WHERE id = ?').get(id) as any
if (!canvas) {
return errorResponse('Canvas not found', 404)
}
const body = await req.json()
const updates: string[] = []
const values: any[] = []
// System canvas can only modify toolbar settings and is_default
if (canvas.is_system) {
if (body.is_default !== undefined) { updates.push('is_default = ?'); values.push(body.is_default ? 1 : 0) }
if (body.show_in_toolbar !== undefined) { updates.push('show_in_toolbar = ?'); values.push(body.show_in_toolbar ? 1 : 0) }
if (body.toolbar_icon !== undefined) { updates.push('toolbar_icon = ?'); values.push(body.toolbar_icon) }
if (body.toolbar_order !== undefined) { updates.push('toolbar_order = ?'); values.push(body.toolbar_order) }
} else {
if (body.name !== undefined) { updates.push('name = ?'); values.push(body.name) }
if (body.description !== undefined) { updates.push('description = ?'); values.push(body.description) }
if (body.theme_id !== undefined) { updates.push('theme_id = ?'); values.push(body.theme_id) }
if (body.config !== undefined) { updates.push('config = ?'); values.push(JSON.stringify(body.config)) }
if (body.tools !== undefined) { updates.push('tools = ?'); values.push(JSON.stringify(body.tools)) }
if (body.is_default !== undefined) { updates.push('is_default = ?'); values.push(body.is_default ? 1 : 0) }
if (body.show_in_toolbar !== undefined) { updates.push('show_in_toolbar = ?'); values.push(body.show_in_toolbar ? 1 : 0) }
if (body.toolbar_icon !== undefined) { updates.push('toolbar_icon = ?'); values.push(body.toolbar_icon) }
if (body.toolbar_order !== undefined) { updates.push('toolbar_order = ?'); values.push(body.toolbar_order) }
}
if (updates.length > 0) {
updates.push('updated_at = CURRENT_TIMESTAMP')
values.push(id)
const sql = `UPDATE project_canvas SET ${updates.join(', ')} WHERE id = ?`
db.run(sql, values)
}
return jsonResponse({ success: true, id })
}
// DELETE /api/canvas/:id
if (req.method === 'DELETE' && !action) {
const canvas = db.query('SELECT is_system FROM project_canvas WHERE id = ?').get(id) as { is_system: number } | null
if (canvas?.is_system) {
return errorResponse('Cannot delete system canvas', 403)
}
db.run('DELETE FROM project_canvas WHERE id = ?', [id])
return jsonResponse({ success: true })
}
return null
}
// Canvas Components API
export async function handleCanvasComponents(req: Request, canvasId: string) {
if (req.method === 'GET') {
const rows = db.query(`
SELECT cc.*, vc.name, vc.template, vc.setup, vc.style, vc.props as component_props, vc.imports
FROM canvas_components cc
JOIN vue_components vc ON cc.component_id = vc.id
WHERE cc.canvas_id = ?
ORDER BY cc.position ASC
`).all(canvasId) as any[]
const components = rows.map(row => ({
id: row.id,
canvasId: row.canvas_id,
componentId: row.component_id,
position: row.position,
props: row.props ? JSON.parse(row.props) : {},
layout: row.layout ? JSON.parse(row.layout) : null,
isVisible: !!row.is_visible,
createdAt: row.created_at,
component: {
id: row.component_id,
name: row.name,
template: row.template,
setup: row.setup,
style: row.style,
props: row.component_props ? JSON.parse(row.component_props) : [],
imports: row.imports ? JSON.parse(row.imports) : []
}
}))
return jsonResponse(components)
}
if (req.method === 'POST') {
const body = await req.json()
// Verify component exists
const component = db.query('SELECT id FROM vue_components WHERE id = ?').get(body.component_id)
if (!component) {
return errorResponse('Component not found', 404)
}
// Get next position
const maxPos = db.query('SELECT MAX(position) as max FROM canvas_components WHERE canvas_id = ?').get(canvasId) as { max: number | null }
const position = body.position ?? ((maxPos?.max ?? -1) + 1)
const stmt = db.prepare(`
INSERT OR REPLACE INTO canvas_components
(canvas_id, component_id, position, props, layout, is_visible)
VALUES (?, ?, ?, ?, ?, ?)
`)
stmt.run(
canvasId,
body.component_id,
position,
JSON.stringify(body.props || {}),
body.layout ? JSON.stringify(body.layout) : null,
body.is_visible !== false ? 1 : 0
)
return jsonResponse({ success: true })
}
return null
}
export async function handleCanvasComponentById(req: Request, canvasId: string, componentId: string) {
if (req.method === 'PUT') {
const body = await req.json()
const updates: string[] = []
const values: any[] = []
if (body.position !== undefined) { updates.push('position = ?'); values.push(body.position) }
if (body.props !== undefined) { updates.push('props = ?'); values.push(JSON.stringify(body.props)) }
if (body.layout !== undefined) { updates.push('layout = ?'); values.push(JSON.stringify(body.layout)) }
if (body.is_visible !== undefined) { updates.push('is_visible = ?'); values.push(body.is_visible ? 1 : 0) }
if (updates.length > 0) {
values.push(canvasId, componentId)
const sql = `UPDATE canvas_components SET ${updates.join(', ')} WHERE canvas_id = ? AND component_id = ?`
db.run(sql, values)
}
return jsonResponse({ success: true })
}
if (req.method === 'DELETE') {
db.run('DELETE FROM canvas_components WHERE canvas_id = ? AND component_id = ?', [canvasId, componentId])
return jsonResponse({ success: true })
}
return null
}

View File

@@ -0,0 +1,84 @@
import { db } from '../db'
import { jsonResponse, errorResponse } from '../utils/cors'
export async function handleComponents(req: Request) {
if (req.method === 'GET') {
const rows = db.query('SELECT * FROM vue_components ORDER BY updated_at DESC').all()
return jsonResponse(rows)
}
if (req.method === 'POST') {
const body = await req.json()
const id = body.id || `comp-${Date.now()}`
const stmt = db.prepare(`
INSERT OR REPLACE INTO vue_components
(id, name, template, setup, style, props, imports, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`)
stmt.run(
id,
body.name,
body.template,
body.setup || '',
body.style || '',
JSON.stringify(body.props || []),
JSON.stringify(body.imports || [])
)
return jsonResponse({ success: true, id })
}
if (req.method === 'DELETE') {
db.run('DELETE FROM vue_components')
return jsonResponse({ success: true })
}
return null
}
export async function handleComponentById(req: Request, id: string) {
if (req.method === 'GET') {
const row = db.query('SELECT * FROM vue_components WHERE id = ?').get(id)
if (!row) {
return errorResponse('Component not found', 404)
}
return jsonResponse(row)
}
if (req.method === 'DELETE') {
// Check if component is in use by any canvas
const usage = db.query(`
SELECT pc.id, pc.name
FROM canvas_components cc
JOIN project_canvas pc ON cc.canvas_id = pc.id
WHERE cc.component_id = ?
`).all(id) as { id: string; name: string }[]
if (usage.length > 0) {
return jsonResponse({
error: 'Component in use',
message: `Cannot delete component. It is used by: ${usage.map(u => u.name).join(', ')}`,
usedBy: usage
}, 409)
}
db.run('DELETE FROM vue_components WHERE id = ?', [id])
return jsonResponse({ success: true })
}
return null
}
export function handleComponentUsage(componentId: string) {
const usage = db.query(`
SELECT pc.id, pc.name, pc.type
FROM canvas_components cc
JOIN project_canvas pc ON cc.canvas_id = pc.id
WHERE cc.component_id = ?
`).all(componentId) as { id: string; name: string; type: string }[]
return jsonResponse({
componentId,
usedBy: usage,
canDelete: usage.length === 0
})
}

27
server/routes/config.ts Normal file
View File

@@ -0,0 +1,27 @@
import { db } from '../db'
import { jsonResponse } from '../utils/cors'
export async function handleConfig(req: Request, url: URL) {
if (req.method === 'GET') {
const key = url.searchParams.get('key')
if (key) {
const row = db.query('SELECT value FROM config WHERE key = ?').get(key) as { value: string } | null
return jsonResponse({ value: row?.value || null })
}
const rows = db.query('SELECT * FROM config').all()
return jsonResponse(rows)
}
if (req.method === 'POST') {
const body = await req.json()
const stmt = db.prepare('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)')
stmt.run(body.key, body.value)
return jsonResponse({ success: true })
}
return null
}
export function handleHealth() {
return jsonResponse({ status: 'ok', timestamp: new Date().toISOString() })
}

119
server/routes/database.ts Normal file
View File

@@ -0,0 +1,119 @@
import { db } from '../db'
import { jsonResponse, errorResponse } from '../utils/cors'
export function handleTables() {
const tables = db.query(`
SELECT name FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
ORDER BY name
`).all() as { name: string }[]
const result = tables.map(t => {
const countResult = db.query(`SELECT COUNT(*) as count FROM "${t.name}"`).get() as { count: number }
return { name: t.name, count: countResult.count }
})
return jsonResponse(result)
}
export async function handleStats() {
// Get database file size
const file = Bun.file('agent-ui.db')
const size = file.size
const sizeStr = size < 1024 ? `${size} B`
: size < 1024 * 1024 ? `${(size / 1024).toFixed(1)} KB`
: `${(size / (1024 * 1024)).toFixed(2)} MB`
// Get tables and counts
const tables = db.query(`
SELECT name FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
`).all() as { name: string }[]
let totalRecords = 0
const breakdown = tables.map(t => {
const countResult = db.query(`SELECT COUNT(*) as count FROM "${t.name}"`).get() as { count: number }
totalRecords += countResult.count
return { name: t.name, count: countResult.count }
})
return jsonResponse({
size: sizeStr,
tables: tables.length,
totalRecords,
breakdown
})
}
export function handleTableSchema(tableName: string) {
// Verify table exists
const tableExists = db.query(`
SELECT name FROM sqlite_master
WHERE type='table' AND name = ?
`).get(tableName)
if (!tableExists) {
return errorResponse('Table not found', 404)
}
const schema = db.query(`PRAGMA table_info("${tableName}")`).all() as any[]
const result = schema.map(col => ({
name: col.name,
type: col.type,
notnull: !!col.notnull,
pk: !!col.pk
}))
return jsonResponse(result)
}
export function handleTableData(tableName: string, url: URL) {
// Verify table exists
const tableExists = db.query(`
SELECT name FROM sqlite_master
WHERE type='table' AND name = ?
`).get(tableName)
if (!tableExists) {
return errorResponse('Table not found', 404)
}
const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 500)
const offset = parseInt(url.searchParams.get('offset') || '0')
const countResult = db.query(`SELECT COUNT(*) as count FROM "${tableName}"`).get() as { count: number }
const rows = db.query(`SELECT * FROM "${tableName}" LIMIT ? OFFSET ?`).all(limit, offset)
return jsonResponse({
total: countResult.count,
limit,
offset,
rows
})
}
export async function handleQuery(req: Request) {
const body = await req.json()
const query = (body.query || '').trim()
// Security: Only allow SELECT statements
const normalizedQuery = query.toLowerCase()
if (!normalizedQuery.startsWith('select')) {
return errorResponse('Only SELECT queries are allowed for security reasons', 403)
}
// Block dangerous keywords
const dangerousKeywords = ['drop', 'delete', 'update', 'insert', 'alter', 'create', 'truncate', 'replace']
for (const keyword of dangerousKeywords) {
if (normalizedQuery.includes(keyword)) {
return errorResponse(`Query contains forbidden keyword: ${keyword.toUpperCase()}`, 403)
}
}
try {
const rows = db.query(query).all()
return jsonResponse({ rows })
} catch (e: any) {
return errorResponse(e.message, 400)
}
}

130
server/routes/gitea.ts Normal file
View File

@@ -0,0 +1,130 @@
import { jsonResponse, errorResponse } from '../utils/cors'
export async function handleGiteaRepo(req: Request) {
const body = await req.json()
const { giteaUrl, username, password, owner, repo } = body
if (!giteaUrl || !username || !password || !owner || !repo) {
return errorResponse('Missing required fields', 400)
}
try {
const auth = Buffer.from(`${username}:${password}`).toString('base64')
// Get repo info
const repoRes = await fetch(`${giteaUrl}/api/v1/repos/${owner}/${repo}`, {
headers: { 'Authorization': `Basic ${auth}` }
})
if (!repoRes.ok) {
if (repoRes.status === 401) {
return errorResponse('Invalid credentials', 401)
}
if (repoRes.status === 404) {
return errorResponse('Repository not found', 404)
}
throw new Error('Failed to connect to Gitea')
}
const repoData = await repoRes.json()
// Get branches
const branchesRes = await fetch(`${giteaUrl}/api/v1/repos/${owner}/${repo}/branches`, {
headers: { 'Authorization': `Basic ${auth}` }
})
let branches = ['main']
if (branchesRes.ok) {
const branchesData = await branchesRes.json()
branches = branchesData.map((b: any) => b.name)
}
return jsonResponse({
repo: {
name: repoData.name,
description: repoData.description,
default_branch: repoData.default_branch,
stars_count: repoData.stars_count,
forks_count: repoData.forks_count,
owner: { login: repoData.owner?.login || owner }
},
branches
})
} catch (e: any) {
return errorResponse(e.message, 500)
}
}
export async function handleGiteaTree(req: Request) {
const body = await req.json()
const { giteaUrl, username, password, owner, repo, branch, path } = body
try {
const auth = Buffer.from(`${username}:${password}`).toString('base64')
const apiPath = path
? `${giteaUrl}/api/v1/repos/${owner}/${repo}/contents/${path}?ref=${branch}`
: `${giteaUrl}/api/v1/repos/${owner}/${repo}/contents?ref=${branch}`
const res = await fetch(apiPath, {
headers: { 'Authorization': `Basic ${auth}` }
})
if (!res.ok) {
throw new Error('Failed to load tree')
}
const data = await res.json()
const items = Array.isArray(data) ? data : [data]
const tree = items
.map((item: any) => ({
name: item.name,
path: item.path,
type: item.type === 'dir' ? 'dir' : 'file',
children: item.type === 'dir' ? [] : undefined
}))
.sort((a: any, b: any) => {
if (a.type !== b.type) return a.type === 'dir' ? -1 : 1
return a.name.localeCompare(b.name)
})
return jsonResponse({ tree })
} catch (e: any) {
return errorResponse(e.message, 500)
}
}
export async function handleGiteaFile(req: Request) {
const body = await req.json()
const { giteaUrl, username, password, owner, repo, branch, path } = body
try {
const auth = Buffer.from(`${username}:${password}`).toString('base64')
const res = await fetch(
`${giteaUrl}/api/v1/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
{ headers: { 'Authorization': `Basic ${auth}` } }
)
if (!res.ok) {
throw new Error('Failed to load file')
}
const data = await res.json()
// Decode base64 content
let content = ''
if (data.content) {
content = Buffer.from(data.content, 'base64').toString('utf-8')
}
return jsonResponse({
content,
encoding: data.encoding,
size: data.size,
sha: data.sha
})
} catch (e: any) {
return errorResponse(e.message, 500)
}
}

24
server/routes/history.ts Normal file
View File

@@ -0,0 +1,24 @@
import { db } from '../db'
import { jsonResponse } from '../utils/cors'
export async function handleHistory(req: Request, url: URL) {
if (req.method === 'GET') {
const limit = parseInt(url.searchParams.get('limit') || '50')
const rows = db.query('SELECT * FROM history ORDER BY id DESC LIMIT ?').all(limit)
return jsonResponse(rows)
}
if (req.method === 'POST') {
const body = await req.json()
const stmt = db.prepare('INSERT INTO history (tool_name, args, result) VALUES (?, ?, ?)')
stmt.run(body.tool_name, JSON.stringify(body.args), body.result)
return jsonResponse({ success: true })
}
if (req.method === 'DELETE') {
db.run('DELETE FROM history')
return jsonResponse({ success: true })
}
return null
}

172
server/routes/index.ts Normal file
View File

@@ -0,0 +1,172 @@
import { optionsResponse, notFoundResponse } from '../utils/cors'
import { handleHistory } from './history'
import { handleConfig, handleHealth } from './config'
import { handleWebMCPToken } from './webmcp'
import { handleComponents, handleComponentById, handleComponentUsage } from './components'
import { handleThemes, handleActiveTheme, handleDesignTokens, handleThemeById, handleThemeExport } from './themes'
import { handleCanvas, handleCanvasById, handleToolbarCanvas, handleDefaultCanvas, handleCanvasComponents, handleCanvasComponentById } from './canvas'
import { handleGiteaRepo, handleGiteaTree, handleGiteaFile } from './gitea'
import { handleTables, handleStats, handleTableSchema, handleTableData, handleQuery } from './database'
export async function handleRequest(req: Request): Promise<Response> {
const url = new URL(req.url)
const path = url.pathname
// CORS preflight
if (req.method === 'OPTIONS') {
return optionsResponse()
}
// Health
if (path === '/api/health') {
return handleHealth()
}
// History
if (path === '/api/history') {
const res = await handleHistory(req, url)
if (res) return res
}
// Config
if (path === '/api/config') {
const res = await handleConfig(req, url)
if (res) return res
}
// WebMCP Token
if (path === '/api/webmcp-token') {
const res = await handleWebMCPToken(req)
if (res) return res
}
// Components
if (path === '/api/components') {
const res = await handleComponents(req)
if (res) return res
}
// Component usage
const componentUsageMatch = path.match(/^\/api\/components\/([^/]+)\/usage$/)
if (componentUsageMatch && req.method === 'GET') {
return handleComponentUsage(componentUsageMatch[1])
}
// Component by ID
if (path.startsWith('/api/components/') && !path.includes('/usage')) {
const id = path.split('/').pop()!
const res = await handleComponentById(req, id)
if (res) return res
}
// Themes
if (path === '/api/themes') {
const res = await handleThemes(req)
if (res) return res
}
if (path === '/api/themes/active') {
return handleActiveTheme()
}
if (path === '/api/design-tokens') {
return handleDesignTokens()
}
// Theme export
if (path.startsWith('/api/themes/export/')) {
const id = path.split('/').pop()!
if (req.method === 'GET') {
return handleThemeExport(id)
}
}
// Theme by ID
if (path.startsWith('/api/themes/') && !path.includes('/active') && !path.includes('/export')) {
const pathParts = path.split('/')
const id = pathParts[3]
const action = pathParts[4]
const res = await handleThemeById(req, id, action)
if (res) return res
}
// Canvas toolbar
if (path === '/api/canvas/toolbar') {
return handleToolbarCanvas()
}
// Canvas default
if (path === '/api/canvas/default') {
return handleDefaultCanvas()
}
// Canvas list/create
if (path === '/api/canvas') {
const res = await handleCanvas(req)
if (res) return res
}
// Canvas components
const canvasComponentsMatch = path.match(/^\/api\/canvas\/([^/]+)\/components\/?$/)
if (canvasComponentsMatch) {
const res = await handleCanvasComponents(req, canvasComponentsMatch[1])
if (res) return res
}
// Canvas component by ID
const canvasComponentMatch = path.match(/^\/api\/canvas\/([^/]+)\/components\/([^/]+)$/)
if (canvasComponentMatch) {
const res = await handleCanvasComponentById(req, canvasComponentMatch[1], canvasComponentMatch[2])
if (res) return res
}
// Canvas by ID
if (path.startsWith('/api/canvas/') && !path.includes('/components')) {
const pathParts = path.split('/')
const id = pathParts[3]
const action = pathParts[4]
const res = await handleCanvasById(req, id, action)
if (res) return res
}
// Gitea
if (path === '/api/gitea/repo' && req.method === 'POST') {
return handleGiteaRepo(req)
}
if (path === '/api/gitea/tree' && req.method === 'POST') {
return handleGiteaTree(req)
}
if (path === '/api/gitea/file' && req.method === 'POST') {
return handleGiteaFile(req)
}
// Database Explorer
if (path === '/api/database/tables') {
return handleTables()
}
if (path === '/api/database/stats') {
return handleStats()
}
// Table schema
const tableSchemaMatch = path.match(/^\/api\/database\/tables\/([^/]+)\/schema$/)
if (tableSchemaMatch && req.method === 'GET') {
return handleTableSchema(decodeURIComponent(tableSchemaMatch[1]))
}
// Table data
const tableDataMatch = path.match(/^\/api\/database\/tables\/([^/]+)\/data$/)
if (tableDataMatch && req.method === 'GET') {
return handleTableData(decodeURIComponent(tableDataMatch[1]), url)
}
// Database query
if (path === '/api/database/query' && req.method === 'POST') {
return handleQuery(req)
}
return notFoundResponse()
}

157
server/routes/themes.ts Normal file
View File

@@ -0,0 +1,157 @@
import { db } from '../db'
import { jsonResponse, errorResponse, corsHeaders } from '../utils/cors'
function parseTheme(row: any) {
return {
...row,
is_default: !!row.is_default,
is_system: !!row.is_system,
variables: JSON.parse(row.variables),
metadata: row.metadata ? JSON.parse(row.metadata) : null
}
}
export async function handleThemes(req: Request) {
if (req.method === 'GET') {
const rows = db.query('SELECT * FROM themes ORDER BY is_system DESC, is_default DESC, name ASC').all()
const themes = (rows as any[]).map(parseTheme)
return jsonResponse(themes)
}
if (req.method === 'POST') {
const body = await req.json()
const id = body.id || `theme-${Date.now()}`
const stmt = db.prepare(`
INSERT OR REPLACE INTO themes
(id, name, description, is_default, is_system, variables, metadata, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`)
stmt.run(
id,
body.name,
body.description || '',
body.is_default ? 1 : 0,
body.is_system ? 1 : 0,
JSON.stringify(body.variables),
JSON.stringify(body.metadata || {})
)
return jsonResponse({ success: true, id })
}
return null
}
export function handleActiveTheme() {
const row = db.query('SELECT * FROM themes WHERE is_default = 1 LIMIT 1').get() as any
if (!row) {
return errorResponse('No active theme', 404)
}
return jsonResponse(parseTheme(row))
}
export function handleDesignTokens() {
const row = db.query('SELECT variables FROM themes WHERE is_default = 1 LIMIT 1').get() as { variables: string } | null
const tokens = row ? JSON.parse(row.variables) : {}
return jsonResponse({
version: '1.0.0',
description: 'Design tokens for Agent UI components. Use these CSS variables for consistent styling.',
usage: 'Use var(--token-name) in CSS, e.g., var(--bg-primary)',
tokens,
guidelines: {
backgrounds: 'Use bg-primary for main areas, bg-secondary for cards/panels, bg-tertiary for nested elements',
text: 'Use text-primary for headings, text-secondary for body, text-muted for hints',
accent: 'Use accent for interactive elements, accent-hover for hover states, accent-muted for backgrounds',
semantic: 'Use success/warning/error/info for status indicators with their -bg variants for backgrounds',
spacing: 'Use radius-sm (4px) for small elements, radius-md (8px) for cards, radius-lg (12px) for modals',
effects: 'Use transition-fast for micro-interactions, shadow-md for elevated elements'
},
examples: {
button: 'background: var(--accent); color: var(--accent-text); border-radius: var(--radius-md); transition: var(--transition-fast);',
card: 'background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm);',
input: 'background: var(--bg-primary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: var(--radius-md);',
badge: 'background: var(--accent-muted); color: var(--accent); padding: 0.25rem 0.5rem; border-radius: var(--radius-full);'
}
})
}
export async function handleThemeById(req: Request, id: string, action?: string) {
// POST /api/themes/:id/default - Set as default
if (action === 'default' && req.method === 'POST') {
db.run('UPDATE themes SET is_default = 0')
db.run('UPDATE themes SET is_default = 1 WHERE id = ?', [id])
return jsonResponse({ success: true })
}
// PUT /api/themes/:id - Update theme
if (req.method === 'PUT' && !action) {
const theme = db.query('SELECT * FROM themes WHERE id = ?').get(id) as any
if (!theme) {
return errorResponse('Theme not found', 404)
}
const body = await req.json()
const updates: string[] = []
const values: any[] = []
if (body.name !== undefined) { updates.push('name = ?'); values.push(body.name) }
if (body.description !== undefined) { updates.push('description = ?'); values.push(body.description) }
if (body.variables !== undefined) { updates.push('variables = ?'); values.push(JSON.stringify(body.variables)) }
if (body.metadata !== undefined) { updates.push('metadata = ?'); values.push(JSON.stringify(body.metadata)) }
if (updates.length > 0) {
updates.push('updated_at = CURRENT_TIMESTAMP')
values.push(id)
const sql = `UPDATE themes SET ${updates.join(', ')} WHERE id = ?`
db.run(sql, values)
}
return jsonResponse({ success: true, id })
}
// GET /api/themes/:id - Get theme
if (req.method === 'GET' && !action) {
const row = db.query('SELECT * FROM themes WHERE id = ?').get(id) as any
if (!row) {
return errorResponse('Theme not found', 404)
}
return jsonResponse(parseTheme(row))
}
// DELETE /api/themes/:id - Delete theme
if (req.method === 'DELETE' && !action) {
const theme = db.query('SELECT is_system FROM themes WHERE id = ?').get(id) as { is_system: number } | null
if (theme?.is_system) {
return errorResponse('Cannot delete system theme', 403)
}
db.run('DELETE FROM themes WHERE id = ?', [id])
return jsonResponse({ success: true })
}
return null
}
export function handleThemeExport(id: string) {
const row = db.query('SELECT * FROM themes WHERE id = ?').get(id) as any
if (!row) {
return errorResponse('Theme not found', 404)
}
const exportData = {
name: row.name,
description: row.description,
variables: JSON.parse(row.variables),
metadata: {
...(row.metadata ? JSON.parse(row.metadata) : {}),
exported_at: new Date().toISOString()
}
}
return new Response(JSON.stringify(exportData, null, 2), {
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename="${row.name.toLowerCase().replace(/\s+/g, '-')}-theme.json"`
}
})
}

42
server/routes/webmcp.ts Normal file
View File

@@ -0,0 +1,42 @@
import { jsonResponse, errorResponse } from '../utils/cors'
// WebMCP token storage (in-memory)
let pendingWebMCPToken: { token: string; createdAt: Date } | null = null
export async function handleWebMCPToken(req: Request) {
if (req.method === 'GET') {
if (pendingWebMCPToken) {
// Check if token is not expired (5 minutes)
const age = Date.now() - pendingWebMCPToken.createdAt.getTime()
if (age < 5 * 60 * 1000) {
return jsonResponse({
token: pendingWebMCPToken.token,
createdAt: pendingWebMCPToken.createdAt.toISOString()
})
}
// Token expired
pendingWebMCPToken = null
}
return jsonResponse({ token: null })
}
if (req.method === 'POST') {
const body = await req.json()
if (body.token) {
pendingWebMCPToken = {
token: body.token,
createdAt: new Date()
}
console.log('[WebMCP] Token received and stored')
return jsonResponse({ success: true })
}
return errorResponse('Token required', 400)
}
if (req.method === 'DELETE') {
pendingWebMCPToken = null
return jsonResponse({ success: true })
}
return null
}