feat: Implement venv status caching with TTL and clear cache on install/uninstall

This commit is contained in:
catlog22
2025-12-25 17:13:07 +08:00
parent eab957ce00
commit 145d38c9bd
2 changed files with 121 additions and 20 deletions

View File

@@ -88,10 +88,15 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
res.end(JSON.stringify({ success: true, indexes: [], totalSize: 0, totalSizeFormatted: '0 B' })); res.end(JSON.stringify({ success: true, indexes: [], totalSize: 0, totalSizeFormatted: '0 B' }));
return true; return true;
} }
// Get config for index directory path
const configResult = await executeCodexLens(['config', '--json']); // Execute all CLI commands in parallel
const [configResult, projectsResult, statusResult] = await Promise.all([
executeCodexLens(['config', '--json']),
executeCodexLens(['projects', 'list', '--json']),
executeCodexLens(['status', '--json'])
]);
let indexDir = ''; let indexDir = '';
if (configResult.success) { if (configResult.success) {
try { try {
const config = extractJSON(configResult.output); const config = extractJSON(configResult.output);
@@ -104,8 +109,6 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
} }
} }
// Get project list using 'projects list' command
const projectsResult = await executeCodexLens(['projects', 'list', '--json']);
let indexes: any[] = []; let indexes: any[] = [];
let totalSize = 0; let totalSize = 0;
let vectorIndexCount = 0; let vectorIndexCount = 0;
@@ -115,7 +118,8 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
try { try {
const projectsData = extractJSON(projectsResult.output); const projectsData = extractJSON(projectsResult.output);
if (projectsData.success && Array.isArray(projectsData.result)) { if (projectsData.success && Array.isArray(projectsData.result)) {
const { statSync, existsSync } = await import('fs'); const { stat, readdir } = await import('fs/promises');
const { existsSync } = await import('fs');
const { basename, join } = await import('path'); const { basename, join } = await import('path');
for (const project of projectsData.result) { for (const project of projectsData.result) {
@@ -136,15 +140,14 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
// Try to get actual index size from index_root // Try to get actual index size from index_root
if (project.index_root && existsSync(project.index_root)) { if (project.index_root && existsSync(project.index_root)) {
try { try {
const { readdirSync } = await import('fs'); const files = await readdir(project.index_root);
const files = readdirSync(project.index_root);
for (const file of files) { for (const file of files) {
try { try {
const filePath = join(project.index_root, file); const filePath = join(project.index_root, file);
const stat = statSync(filePath); const fileStat = await stat(filePath);
projectSize += stat.size; projectSize += fileStat.size;
if (!lastModified || stat.mtime > lastModified) { if (!lastModified || fileStat.mtime > lastModified) {
lastModified = stat.mtime; lastModified = fileStat.mtime;
} }
// Check for vector/embedding files // Check for vector/embedding files
if (file.includes('vector') || file.includes('embedding') || if (file.includes('vector') || file.includes('embedding') ||
@@ -194,8 +197,7 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
} }
} }
// Also get summary stats from status command // Parse summary stats from status command (already fetched in parallel)
const statusResult = await executeCodexLens(['status', '--json']);
let statusSummary: any = {}; let statusSummary: any = {};
if (statusResult.success) { if (statusResult.success) {
@@ -250,6 +252,71 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
return true; return true;
} }
// API: CodexLens Dashboard Init - Aggregated endpoint for page initialization
if (pathname === '/api/codexlens/dashboard-init') {
try {
const venvStatus = await checkVenvStatus();
if (!venvStatus.ready) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
installed: false,
status: venvStatus,
config: { index_dir: '~/.codexlens/indexes', index_count: 0 },
semantic: { available: false }
}));
return true;
}
// Parallel fetch all initialization data
const [configResult, statusResult, semanticStatus] = await Promise.all([
executeCodexLens(['config', '--json']),
executeCodexLens(['status', '--json']),
checkSemanticStatus()
]);
// Parse config
let config = { index_dir: '~/.codexlens/indexes', index_count: 0 };
if (configResult.success) {
try {
const configData = extractJSON(configResult.output);
if (configData.success && configData.result) {
config.index_dir = configData.result.index_dir || configData.result.index_root || config.index_dir;
}
} catch (e) {
console.error('[CodexLens] Failed to parse config for dashboard init:', e.message);
}
}
// Parse status
let statusData: any = {};
if (statusResult.success) {
try {
const status = extractJSON(statusResult.output);
if (status.success && status.result) {
config.index_count = status.result.projects_count || 0;
statusData = status.result;
}
} catch (e) {
console.error('[CodexLens] Failed to parse status for dashboard init:', e.message);
}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
installed: true,
status: venvStatus,
config,
semantic: semanticStatus,
statusData
}));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: err.message }));
}
return true;
}
// API: CodexLens Bootstrap (Install) // API: CodexLens Bootstrap (Install)
if (pathname === '/api/codexlens/bootstrap' && req.method === 'POST') { if (pathname === '/api/codexlens/bootstrap' && req.method === 'POST') {
handlePostRequest(req, res, async () => { handlePostRequest(req, res, async () => {

View File

@@ -33,6 +33,14 @@ const VENV_PYTHON =
let bootstrapChecked = false; let bootstrapChecked = false;
let bootstrapReady = false; let bootstrapReady = false;
// Venv status cache with TTL
interface VenvStatusCache {
status: ReadyStatus;
timestamp: number;
}
let venvStatusCache: VenvStatusCache | null = null;
const VENV_STATUS_TTL = 5 * 60 * 1000; // 5 minutes TTL
// Track running indexing process for cancellation // Track running indexing process for cancellation
let currentIndexingProcess: ReturnType<typeof spawn> | null = null; let currentIndexingProcess: ReturnType<typeof spawn> | null = null;
let currentIndexingAborted = false; let currentIndexingAborted = false;
@@ -116,6 +124,13 @@ interface ProgressInfo {
totalFiles?: number; totalFiles?: number;
} }
/**
* Clear venv status cache (call after install/uninstall operations)
*/
function clearVenvStatusCache(): void {
venvStatusCache = null;
}
/** /**
* Detect available Python 3 executable * Detect available Python 3 executable
* @returns Python executable command * @returns Python executable command
@@ -138,17 +153,27 @@ function getSystemPython(): string {
/** /**
* Check if CodexLens venv exists and has required packages * Check if CodexLens venv exists and has required packages
* @param force - Force refresh cache (default: false)
* @returns Ready status * @returns Ready status
*/ */
async function checkVenvStatus(): Promise<ReadyStatus> { async function checkVenvStatus(force = false): Promise<ReadyStatus> {
// Use cached result if available and not expired
if (!force && venvStatusCache && (Date.now() - venvStatusCache.timestamp < VENV_STATUS_TTL)) {
return venvStatusCache.status;
}
// Check venv exists // Check venv exists
if (!existsSync(CODEXLENS_VENV)) { if (!existsSync(CODEXLENS_VENV)) {
return { ready: false, error: 'Venv not found' }; const result = { ready: false, error: 'Venv not found' };
venvStatusCache = { status: result, timestamp: Date.now() };
return result;
} }
// Check python executable exists // Check python executable exists
if (!existsSync(VENV_PYTHON)) { if (!existsSync(VENV_PYTHON)) {
return { ready: false, error: 'Python executable not found in venv' }; const result = { ready: false, error: 'Python executable not found in venv' };
venvStatusCache = { status: result, timestamp: Date.now() };
return result;
} }
// Check codexlens is importable // Check codexlens is importable
@@ -169,15 +194,21 @@ async function checkVenvStatus(): Promise<ReadyStatus> {
}); });
child.on('close', (code) => { child.on('close', (code) => {
let result: ReadyStatus;
if (code === 0) { if (code === 0) {
resolve({ ready: true, version: stdout.trim() }); result = { ready: true, version: stdout.trim() };
} else { } else {
resolve({ ready: false, error: `CodexLens not installed: ${stderr}` }); result = { ready: false, error: `CodexLens not installed: ${stderr}` };
} }
// Cache the result
venvStatusCache = { status: result, timestamp: Date.now() };
resolve(result);
}); });
child.on('error', (err) => { child.on('error', (err) => {
resolve({ ready: false, error: `Failed to check venv: ${err.message}` }); const result = { ready: false, error: `Failed to check venv: ${err.message}` };
venvStatusCache = { status: result, timestamp: Date.now() };
resolve(result);
}); });
}); });
} }
@@ -581,6 +612,8 @@ async function bootstrapVenv(): Promise<BootstrapResult> {
execSync(`"${pipPath}" install codexlens`, { stdio: 'inherit' }); execSync(`"${pipPath}" install codexlens`, { stdio: 'inherit' });
} }
// Clear cache after successful installation
clearVenvStatusCache();
return { success: true }; return { success: true };
} catch (err) { } catch (err) {
return { success: false, error: `Failed to install codexlens: ${(err as Error).message}` }; return { success: false, error: `Failed to install codexlens: ${(err as Error).message}` };
@@ -1300,6 +1333,7 @@ async function uninstallCodexLens(): Promise<BootstrapResult> {
// Reset bootstrap cache // Reset bootstrap cache
bootstrapChecked = false; bootstrapChecked = false;
bootstrapReady = false; bootstrapReady = false;
clearVenvStatusCache();
console.log('[CodexLens] CodexLens uninstalled successfully'); console.log('[CodexLens] CodexLens uninstalled successfully');
return { success: true, message: 'CodexLens uninstalled successfully' }; return { success: true, message: 'CodexLens uninstalled successfully' };