Add comprehensive tests for schema cleanup migration and search comparison

- Implement tests for migration 005 to verify removal of deprecated fields in the database schema.
- Ensure that new databases are created with a clean schema.
- Validate that keywords are correctly extracted from the normalized file_keywords table.
- Test symbol insertion without deprecated fields and subdir operations without direct_files.
- Create a detailed search comparison test to evaluate vector search vs hybrid search performance.
- Add a script for reindexing projects to extract code relationships and verify GraphAnalyzer functionality.
- Include a test script to check TreeSitter parser availability and relationship extraction from sample files.
This commit is contained in:
catlog22
2025-12-16 19:27:05 +08:00
parent 3da0ef2adb
commit df23975a0b
61 changed files with 13114 additions and 366 deletions

View File

@@ -212,7 +212,7 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
const status = url.searchParams.get('status') || null;
const category = url.searchParams.get('category') as 'user' | 'internal' | 'insight' | null;
const search = url.searchParams.get('search') || null;
const recursive = url.searchParams.get('recursive') === 'true';
const recursive = url.searchParams.get('recursive') !== 'false';
getExecutionHistoryAsync(projectPath, { limit, tool, status, category, search, recursive })
.then(history => {

View File

@@ -23,6 +23,37 @@ export interface RouteContext {
broadcastToClients: (data: unknown) => void;
}
/**
* Strip ANSI color codes from string
* Rich library adds color codes even with --json flag
*/
function stripAnsiCodes(str: string): string {
// ANSI escape code pattern: \x1b[...m or \x1b]...
return str.replace(/\x1b\[[0-9;]*m/g, '')
.replace(/\x1b\][0-9;]*\x07/g, '')
.replace(/\x1b\][^\x07]*\x07/g, '');
}
/**
* Extract JSON from CLI output that may contain logging messages
* CodexLens CLI outputs logs like "INFO ..." before the JSON
* Also strips ANSI color codes that Rich library adds
*/
function extractJSON(output: string): any {
// Strip ANSI color codes first
const cleanOutput = stripAnsiCodes(output);
// Find the first { or [ character (start of JSON)
const jsonStart = cleanOutput.search(/[{\[]/);
if (jsonStart === -1) {
throw new Error('No JSON found in output');
}
// Extract everything from the first { or [ onwards
const jsonString = cleanOutput.substring(jsonStart);
return JSON.parse(jsonString);
}
/**
* Handle CodexLens routes
* @returns true if route was handled, false otherwise
@@ -83,23 +114,45 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
return true;
}
// API: CodexLens Config - GET (Get current configuration)
// API: CodexLens Config - GET (Get current configuration with index count)
if (pathname === '/api/codexlens/config' && req.method === 'GET') {
try {
const result = await executeCodexLens(['config-show', '--json']);
if (result.success) {
// Fetch both config and status to merge index_count
const [configResult, statusResult] = await Promise.all([
executeCodexLens(['config', '--json']),
executeCodexLens(['status', '--json'])
]);
let responseData = { index_dir: '~/.codexlens/indexes', index_count: 0 };
// Parse config (extract JSON from output that may contain log messages)
if (configResult.success) {
try {
const config = JSON.parse(result.output);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(config));
} catch {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ index_dir: '~/.codexlens/indexes', index_count: 0 }));
const config = extractJSON(configResult.output);
if (config.success && config.result) {
responseData.index_dir = config.result.index_root || responseData.index_dir;
}
} catch (e) {
console.error('[CodexLens] Failed to parse config:', e.message);
console.error('[CodexLens] Config output:', configResult.output.substring(0, 200));
}
} else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ index_dir: '~/.codexlens/indexes', index_count: 0 }));
}
// Parse status to get index_count (projects_count)
if (statusResult.success) {
try {
const status = extractJSON(statusResult.output);
if (status.success && status.result) {
responseData.index_count = status.result.projects_count || 0;
}
} catch (e) {
console.error('[CodexLens] Failed to parse status:', e.message);
console.error('[CodexLens] Status output:', statusResult.output.substring(0, 200));
}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(responseData));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: err.message }));
@@ -168,7 +221,7 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
const result = await executeCodexLens(['init', targetPath, '--json'], { cwd: targetPath });
if (result.success) {
try {
const parsed = JSON.parse(result.output);
const parsed = extractJSON(result.output);
return { success: true, result: parsed };
} catch {
return { success: true, output: result.output };
@@ -237,7 +290,7 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
const result = await executeCodexLens(args, { cwd: targetPath, timeout: timeoutMs + 30000 });
if (result.success) {
try {
const parsed = JSON.parse(result.output);
const parsed = extractJSON(result.output);
return { success: true, result: parsed };
} catch {
return { success: true, output: result.output };
@@ -253,10 +306,11 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
}
// API: CodexLens Search (FTS5 text search)
// API: CodexLens Search (FTS5 text search with mode support)
if (pathname === '/api/codexlens/search') {
const query = url.searchParams.get('query') || '';
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
const mode = url.searchParams.get('mode') || 'exact'; // exact, fuzzy, hybrid, vector
const projectPath = url.searchParams.get('path') || initialPath;
if (!query) {
@@ -266,13 +320,13 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
}
try {
const args = ['search', query, '--path', projectPath, '--limit', limit.toString(), '--json'];
const args = ['search', query, '--path', projectPath, '--limit', limit.toString(), '--mode', mode, '--json'];
const result = await executeCodexLens(args, { cwd: projectPath });
if (result.success) {
try {
const parsed = JSON.parse(result.output);
const parsed = extractJSON(result.output);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, ...parsed.result }));
} catch {
@@ -290,10 +344,11 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
return true;
}
// API: CodexLens Search Files Only (return file paths only)
// API: CodexLens Search Files Only (return file paths only, with mode support)
if (pathname === '/api/codexlens/search_files') {
const query = url.searchParams.get('query') || '';
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
const mode = url.searchParams.get('mode') || 'exact'; // exact, fuzzy, hybrid, vector
const projectPath = url.searchParams.get('path') || initialPath;
if (!query) {
@@ -303,13 +358,13 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
}
try {
const args = ['search', query, '--path', projectPath, '--limit', limit.toString(), '--files-only', '--json'];
const args = ['search', query, '--path', projectPath, '--limit', limit.toString(), '--mode', mode, '--files-only', '--json'];
const result = await executeCodexLens(args, { cwd: projectPath });
if (result.success) {
try {
const parsed = JSON.parse(result.output);
const parsed = extractJSON(result.output);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, ...parsed.result }));
} catch {
@@ -327,6 +382,51 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
return true;
}
// API: CodexLens Symbol Search (search for symbols by name)
if (pathname === '/api/codexlens/symbol') {
const query = url.searchParams.get('query') || '';
const file = url.searchParams.get('file');
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
const projectPath = url.searchParams.get('path') || initialPath;
if (!query && !file) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: 'Either query or file parameter is required' }));
return true;
}
try {
let args;
if (file) {
// Get symbols from a specific file
args = ['symbol', '--file', file, '--json'];
} else {
// Search for symbols by name
args = ['symbol', query, '--path', projectPath, '--limit', limit.toString(), '--json'];
}
const result = await executeCodexLens(args, { cwd: projectPath });
if (result.success) {
try {
const parsed = extractJSON(result.output);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, ...parsed.result }));
} catch {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, symbols: [], output: result.output }));
}
} else {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: result.error }));
}
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: err.message }));
}
return true;
}
// API: CodexLens Semantic Search Install (fastembed, ONNX-based, ~200MB)
if (pathname === '/api/codexlens/semantic/install' && req.method === 'POST') {
@@ -350,5 +450,117 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
return true;
}
// API: CodexLens Model List (list available embedding models)
if (pathname === '/api/codexlens/models' && req.method === 'GET') {
try {
const result = await executeCodexLens(['model-list', '--json']);
if (result.success) {
try {
const parsed = extractJSON(result.output);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(parsed));
} catch {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, result: { models: [] }, output: result.output }));
}
} else {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: result.error }));
}
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: err.message }));
}
return true;
}
// API: CodexLens Model Download (download embedding model by profile)
if (pathname === '/api/codexlens/models/download' && req.method === 'POST') {
handlePostRequest(req, res, async (body) => {
const { profile } = body;
if (!profile) {
return { success: false, error: 'profile is required', status: 400 };
}
try {
const result = await executeCodexLens(['model-download', profile, '--json'], { timeout: 600000 }); // 10 min for download
if (result.success) {
try {
const parsed = extractJSON(result.output);
return { success: true, ...parsed };
} catch {
return { success: true, output: result.output };
}
} else {
return { success: false, error: result.error, status: 500 };
}
} catch (err) {
return { success: false, error: err.message, status: 500 };
}
});
return true;
}
// API: CodexLens Model Delete (delete embedding model by profile)
if (pathname === '/api/codexlens/models/delete' && req.method === 'POST') {
handlePostRequest(req, res, async (body) => {
const { profile } = body;
if (!profile) {
return { success: false, error: 'profile is required', status: 400 };
}
try {
const result = await executeCodexLens(['model-delete', profile, '--json']);
if (result.success) {
try {
const parsed = extractJSON(result.output);
return { success: true, ...parsed };
} catch {
return { success: true, output: result.output };
}
} else {
return { success: false, error: result.error, status: 500 };
}
} catch (err) {
return { success: false, error: err.message, status: 500 };
}
});
return true;
}
// API: CodexLens Model Info (get model info by profile)
if (pathname === '/api/codexlens/models/info' && req.method === 'GET') {
const profile = url.searchParams.get('profile');
if (!profile) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: 'profile parameter is required' }));
return true;
}
try {
const result = await executeCodexLens(['model-info', profile, '--json']);
if (result.success) {
try {
const parsed = extractJSON(result.output);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(parsed));
} catch {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: 'Failed to parse response' }));
}
} else {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: result.error }));
}
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: err.message }));
}
return true;
}
return false;
}

View File

@@ -20,9 +20,7 @@ Query all symbols from the CodexLens SQLite database and return them as graph no
"name": "functionName",
"type": "FUNCTION",
"file": "src/file.ts",
"line": 10,
"docstring": "function_type",
"tokenCount": 45
"line": 10
}
]
}
@@ -98,7 +96,7 @@ Maps source code paths to CodexLens index database paths following the storage s
### Database Schema
Queries two main tables:
1. **symbols** - Code symbol definitions
- `id`, `file_id`, `name`, `kind`, `start_line`, `end_line`, `token_count`, `symbol_type`
- `id`, `file_id`, `name`, `kind`, `start_line`, `end_line`
2. **code_relationships** - Inter-symbol dependencies
- `id`, `source_symbol_id`, `target_qualified_name`, `relationship_type`, `source_line`, `target_file`

View File

@@ -5,7 +5,7 @@
import type { IncomingMessage, ServerResponse } from 'http';
import { homedir } from 'os';
import { join, resolve, normalize } from 'path';
import { existsSync } from 'fs';
import { existsSync, readdirSync } from 'fs';
import Database from 'better-sqlite3';
export interface RouteContext {
@@ -63,8 +63,6 @@ interface GraphNode {
type: string;
file: string;
line: number;
docstring?: string;
tokenCount?: number;
}
interface GraphEdge {
@@ -108,6 +106,36 @@ function validateProjectPath(projectPath: string, initialPath: string): string |
return normalized;
}
/**
* Find all _index.db files recursively in a directory
* @param dir Directory to search
* @returns Array of absolute paths to _index.db files
*/
function findAllIndexDbs(dir: string): string[] {
const dbs: string[] = [];
function traverse(currentDir: string): void {
const dbPath = join(currentDir, '_index.db');
if (existsSync(dbPath)) {
dbs.push(dbPath);
}
try {
const entries = readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
traverse(join(currentDir, entry.name));
}
}
} catch {
// Silently skip directories we can't read
}
}
traverse(dir);
return dbs;
}
/**
* Map codex-lens symbol kinds to graph node types
*/
@@ -138,93 +166,117 @@ function mapRelationType(relType: string): string {
}
/**
* Query symbols from codex-lens database
* Query symbols from all codex-lens databases (hierarchical structure)
*/
async function querySymbols(projectPath: string): Promise<GraphNode[]> {
const mapper = new PathMapper();
const dbPath = mapper.sourceToIndexDb(projectPath);
const rootDbPath = mapper.sourceToIndexDb(projectPath);
const indexRoot = rootDbPath.replace(/[\\/]_index\.db$/, '');
if (!existsSync(dbPath)) {
if (!existsSync(indexRoot)) {
return [];
}
try {
const db = Database(dbPath, { readonly: true });
// Find all _index.db files recursively
const dbPaths = findAllIndexDbs(indexRoot);
const rows = db.prepare(`
SELECT
s.id,
s.name,
s.kind,
s.start_line,
s.token_count,
s.symbol_type,
f.path as file
FROM symbols s
JOIN files f ON s.file_id = f.id
ORDER BY f.path, s.start_line
`).all();
db.close();
return rows.map((row: any) => ({
id: `${row.file}:${row.name}:${row.start_line}`,
name: row.name,
type: mapSymbolKind(row.kind),
file: row.file,
line: row.start_line,
docstring: row.symbol_type || undefined,
tokenCount: row.token_count || undefined,
}));
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`[Graph] Failed to query symbols: ${message}`);
if (dbPaths.length === 0) {
return [];
}
const allNodes: GraphNode[] = [];
for (const dbPath of dbPaths) {
try {
const db = Database(dbPath, { readonly: true });
const rows = db.prepare(`
SELECT
s.id,
s.name,
s.kind,
s.start_line,
f.full_path as file
FROM symbols s
JOIN files f ON s.file_id = f.id
ORDER BY f.full_path, s.start_line
`).all();
db.close();
allNodes.push(...rows.map((row: any) => ({
id: `${row.file}:${row.name}:${row.start_line}`,
name: row.name,
type: mapSymbolKind(row.kind),
file: row.file,
line: row.start_line,
})));
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`[Graph] Failed to query symbols from ${dbPath}: ${message}`);
// Continue with other databases even if one fails
}
}
return allNodes;
}
/**
* Query code relationships from codex-lens database
* Query code relationships from all codex-lens databases (hierarchical structure)
*/
async function queryRelationships(projectPath: string): Promise<GraphEdge[]> {
const mapper = new PathMapper();
const dbPath = mapper.sourceToIndexDb(projectPath);
const rootDbPath = mapper.sourceToIndexDb(projectPath);
const indexRoot = rootDbPath.replace(/[\\/]_index\.db$/, '');
if (!existsSync(dbPath)) {
if (!existsSync(indexRoot)) {
return [];
}
try {
const db = Database(dbPath, { readonly: true });
// Find all _index.db files recursively
const dbPaths = findAllIndexDbs(indexRoot);
const rows = db.prepare(`
SELECT
s.name as source_name,
s.start_line as source_line,
f.path as source_file,
r.target_qualified_name,
r.relationship_type,
r.target_file
FROM code_relationships r
JOIN symbols s ON r.source_symbol_id = s.id
JOIN files f ON s.file_id = f.id
ORDER BY f.path, s.start_line
`).all();
db.close();
return rows.map((row: any) => ({
source: `${row.source_file}:${row.source_name}:${row.source_line}`,
target: row.target_qualified_name,
type: mapRelationType(row.relationship_type),
sourceLine: row.source_line,
sourceFile: row.source_file,
}));
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`[Graph] Failed to query relationships: ${message}`);
if (dbPaths.length === 0) {
return [];
}
const allEdges: GraphEdge[] = [];
for (const dbPath of dbPaths) {
try {
const db = Database(dbPath, { readonly: true });
const rows = db.prepare(`
SELECT
s.name as source_name,
s.start_line as source_line,
f.full_path as source_file,
r.target_qualified_name,
r.relationship_type,
r.target_file
FROM code_relationships r
JOIN symbols s ON r.source_symbol_id = s.id
JOIN files f ON s.file_id = f.id
ORDER BY f.full_path, s.start_line
`).all();
db.close();
allEdges.push(...rows.map((row: any) => ({
source: `${row.source_file}:${row.source_name}:${row.source_line}`,
target: row.target_qualified_name,
type: mapRelationType(row.relationship_type),
sourceLine: row.source_line,
sourceFile: row.source_file,
})));
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`[Graph] Failed to query relationships from ${dbPath}: ${message}`);
// Continue with other databases even if one fails
}
}
return allEdges;
}
/**
@@ -292,7 +344,7 @@ async function analyzeImpact(projectPath: string, symbolId: string): Promise<Imp
const rows = db.prepare(`
SELECT DISTINCT
s.name as dependent_name,
f.path as dependent_file,
f.full_path as dependent_file,
s.start_line as dependent_line
FROM code_relationships r
JOIN symbols s ON r.source_symbol_id = s.id
@@ -330,6 +382,8 @@ export async function handleGraphRoutes(ctx: RouteContext): Promise<boolean> {
if (pathname === '/api/graph/nodes') {
const rawPath = url.searchParams.get('path') || initialPath;
const projectPath = validateProjectPath(rawPath, initialPath);
const limitStr = url.searchParams.get('limit') || '1000';
const limit = Math.min(parseInt(limitStr, 10) || 1000, 5000); // Max 5000 nodes
if (!projectPath) {
res.writeHead(400, { 'Content-Type': 'application/json' });
@@ -338,9 +392,15 @@ export async function handleGraphRoutes(ctx: RouteContext): Promise<boolean> {
}
try {
const nodes = await querySymbols(projectPath);
const allNodes = await querySymbols(projectPath);
const nodes = allNodes.slice(0, limit);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ nodes }));
res.end(JSON.stringify({
nodes,
total: allNodes.length,
limit,
hasMore: allNodes.length > limit
}));
} catch (err) {
console.error(`[Graph] Error fetching nodes:`, err);
res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -353,6 +413,8 @@ export async function handleGraphRoutes(ctx: RouteContext): Promise<boolean> {
if (pathname === '/api/graph/edges') {
const rawPath = url.searchParams.get('path') || initialPath;
const projectPath = validateProjectPath(rawPath, initialPath);
const limitStr = url.searchParams.get('limit') || '2000';
const limit = Math.min(parseInt(limitStr, 10) || 2000, 10000); // Max 10000 edges
if (!projectPath) {
res.writeHead(400, { 'Content-Type': 'application/json' });
@@ -361,9 +423,15 @@ export async function handleGraphRoutes(ctx: RouteContext): Promise<boolean> {
}
try {
const edges = await queryRelationships(projectPath);
const allEdges = await queryRelationships(projectPath);
const edges = allEdges.slice(0, limit);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ edges }));
res.end(JSON.stringify({
edges,
total: allEdges.length,
limit,
hasMore: allEdges.length > limit
}));
} catch (err) {
console.error(`[Graph] Error fetching edges:`, err);
res.writeHead(500, { 'Content-Type': 'application/json' });

View File

@@ -1,4 +1,3 @@
// @ts-nocheck
import http from 'http';
import { URL } from 'url';
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, unlinkSync } from 'fs';
@@ -222,7 +221,7 @@ export async function handleMemoryRoutes(ctx: RouteContext): Promise<boolean> {
const projectPath = url.searchParams.get('path') || initialPath;
const limit = parseInt(url.searchParams.get('limit') || '50', 10);
const search = url.searchParams.get('search') || null;
const recursive = url.searchParams.get('recursive') === 'true';
const recursive = url.searchParams.get('recursive') !== 'false';
try {
let prompts;
@@ -230,7 +229,7 @@ export async function handleMemoryRoutes(ctx: RouteContext): Promise<boolean> {
// Recursive mode: aggregate prompts from parent and child projects
if (recursive && !search) {
const { getAggregatedPrompts } = await import('../memory-store.js');
prompts = getAggregatedPrompts(projectPath, limit);
prompts = await getAggregatedPrompts(projectPath, limit);
} else {
// Non-recursive mode or search mode: query only current project
const memoryStore = getMemoryStore(projectPath);
@@ -390,11 +389,11 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
mode: 'analysis',
timeout: 120000,
cd: projectPath,
category: 'insights'
category: 'insight'
});
// Try to parse JSON from response
let insights = { patterns: [], suggestions: [] };
let insights: { patterns: any[]; suggestions: any[] } = { patterns: [], suggestions: [] };
if (result.stdout) {
let outputText = result.stdout;
@@ -515,13 +514,13 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
const projectPath = url.searchParams.get('path') || initialPath;
const filter = url.searchParams.get('filter') || 'all'; // today, week, all
const limit = parseInt(url.searchParams.get('limit') || '10', 10);
const recursive = url.searchParams.get('recursive') === 'true';
const recursive = url.searchParams.get('recursive') !== 'false';
try {
// If requesting aggregated stats, use the aggregated function
if (url.searchParams.has('aggregated') || recursive) {
const { getAggregatedStats } = await import('../memory-store.js');
const aggregatedStats = getAggregatedStats(projectPath);
const aggregatedStats = await getAggregatedStats(projectPath);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({

View File

@@ -0,0 +1,57 @@
// @ts-nocheck
/**
* Status Routes Module
* Aggregated status endpoint for faster dashboard loading
*/
import type { IncomingMessage, ServerResponse } from 'http';
import { getCliToolsStatus } from '../../tools/cli-executor.js';
import { checkVenvStatus, checkSemanticStatus } from '../../tools/codex-lens.js';
export interface RouteContext {
pathname: string;
url: URL;
req: IncomingMessage;
res: ServerResponse;
initialPath: string;
handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise<any>) => void;
broadcastToClients: (data: unknown) => void;
}
/**
* Handle status routes
* @returns true if route was handled, false otherwise
*/
export async function handleStatusRoutes(ctx: RouteContext): Promise<boolean> {
const { pathname, res } = ctx;
// API: Aggregated Status (all statuses in one call)
if (pathname === '/api/status/all') {
try {
// Execute all status checks in parallel
const [cliStatus, codexLensStatus, semanticStatus] = await Promise.all([
getCliToolsStatus(),
checkVenvStatus(),
// Always check semantic status (will return available: false if CodexLens not ready)
checkSemanticStatus().catch(() => ({ available: false, backend: null }))
]);
const response = {
cli: cliStatus,
codexLens: codexLensStatus,
semantic: semanticStatus,
timestamp: new Date().toISOString()
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
return true;
} catch (error) {
console.error('[Status Routes] Error fetching aggregated status:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (error as Error).message }));
return true;
}
}
return false;
}