Skip to content

Commit 145d38c

Browse files
author
catlog22
committed
feat: Implement venv status caching with TTL and clear cache on install/uninstall
1 parent eab957c commit 145d38c

File tree

2 files changed

+121
-20
lines changed

2 files changed

+121
-20
lines changed

ccw/src/core/routes/codexlens-routes.ts

Lines changed: 81 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,15 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
8888
res.end(JSON.stringify({ success: true, indexes: [], totalSize: 0, totalSizeFormatted: '0 B' }));
8989
return true;
9090
}
91-
// Get config for index directory path
92-
const configResult = await executeCodexLens(['config', '--json']);
91+
92+
// Execute all CLI commands in parallel
93+
const [configResult, projectsResult, statusResult] = await Promise.all([
94+
executeCodexLens(['config', '--json']),
95+
executeCodexLens(['projects', 'list', '--json']),
96+
executeCodexLens(['status', '--json'])
97+
]);
98+
9399
let indexDir = '';
94-
95100
if (configResult.success) {
96101
try {
97102
const config = extractJSON(configResult.output);
@@ -104,8 +109,6 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
104109
}
105110
}
106111

107-
// Get project list using 'projects list' command
108-
const projectsResult = await executeCodexLens(['projects', 'list', '--json']);
109112
let indexes: any[] = [];
110113
let totalSize = 0;
111114
let vectorIndexCount = 0;
@@ -115,7 +118,8 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
115118
try {
116119
const projectsData = extractJSON(projectsResult.output);
117120
if (projectsData.success && Array.isArray(projectsData.result)) {
118-
const { statSync, existsSync } = await import('fs');
121+
const { stat, readdir } = await import('fs/promises');
122+
const { existsSync } = await import('fs');
119123
const { basename, join } = await import('path');
120124

121125
for (const project of projectsData.result) {
@@ -136,15 +140,14 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
136140
// Try to get actual index size from index_root
137141
if (project.index_root && existsSync(project.index_root)) {
138142
try {
139-
const { readdirSync } = await import('fs');
140-
const files = readdirSync(project.index_root);
143+
const files = await readdir(project.index_root);
141144
for (const file of files) {
142145
try {
143146
const filePath = join(project.index_root, file);
144-
const stat = statSync(filePath);
145-
projectSize += stat.size;
146-
if (!lastModified || stat.mtime > lastModified) {
147-
lastModified = stat.mtime;
147+
const fileStat = await stat(filePath);
148+
projectSize += fileStat.size;
149+
if (!lastModified || fileStat.mtime > lastModified) {
150+
lastModified = fileStat.mtime;
148151
}
149152
// Check for vector/embedding files
150153
if (file.includes('vector') || file.includes('embedding') ||
@@ -194,8 +197,7 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
194197
}
195198
}
196199

197-
// Also get summary stats from status command
198-
const statusResult = await executeCodexLens(['status', '--json']);
200+
// Parse summary stats from status command (already fetched in parallel)
199201
let statusSummary: any = {};
200202

201203
if (statusResult.success) {
@@ -250,6 +252,71 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
250252
return true;
251253
}
252254

255+
// API: CodexLens Dashboard Init - Aggregated endpoint for page initialization
256+
if (pathname === '/api/codexlens/dashboard-init') {
257+
try {
258+
const venvStatus = await checkVenvStatus();
259+
260+
if (!venvStatus.ready) {
261+
res.writeHead(200, { 'Content-Type': 'application/json' });
262+
res.end(JSON.stringify({
263+
installed: false,
264+
status: venvStatus,
265+
config: { index_dir: '~/.codexlens/indexes', index_count: 0 },
266+
semantic: { available: false }
267+
}));
268+
return true;
269+
}
270+
271+
// Parallel fetch all initialization data
272+
const [configResult, statusResult, semanticStatus] = await Promise.all([
273+
executeCodexLens(['config', '--json']),
274+
executeCodexLens(['status', '--json']),
275+
checkSemanticStatus()
276+
]);
277+
278+
// Parse config
279+
let config = { index_dir: '~/.codexlens/indexes', index_count: 0 };
280+
if (configResult.success) {
281+
try {
282+
const configData = extractJSON(configResult.output);
283+
if (configData.success && configData.result) {
284+
config.index_dir = configData.result.index_dir || configData.result.index_root || config.index_dir;
285+
}
286+
} catch (e) {
287+
console.error('[CodexLens] Failed to parse config for dashboard init:', e.message);
288+
}
289+
}
290+
291+
// Parse status
292+
let statusData: any = {};
293+
if (statusResult.success) {
294+
try {
295+
const status = extractJSON(statusResult.output);
296+
if (status.success && status.result) {
297+
config.index_count = status.result.projects_count || 0;
298+
statusData = status.result;
299+
}
300+
} catch (e) {
301+
console.error('[CodexLens] Failed to parse status for dashboard init:', e.message);
302+
}
303+
}
304+
305+
res.writeHead(200, { 'Content-Type': 'application/json' });
306+
res.end(JSON.stringify({
307+
installed: true,
308+
status: venvStatus,
309+
config,
310+
semantic: semanticStatus,
311+
statusData
312+
}));
313+
} catch (err) {
314+
res.writeHead(500, { 'Content-Type': 'application/json' });
315+
res.end(JSON.stringify({ success: false, error: err.message }));
316+
}
317+
return true;
318+
}
319+
253320
// API: CodexLens Bootstrap (Install)
254321
if (pathname === '/api/codexlens/bootstrap' && req.method === 'POST') {
255322
handlePostRequest(req, res, async () => {

ccw/src/tools/codex-lens.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ const VENV_PYTHON =
3333
let bootstrapChecked = false;
3434
let bootstrapReady = false;
3535

36+
// Venv status cache with TTL
37+
interface VenvStatusCache {
38+
status: ReadyStatus;
39+
timestamp: number;
40+
}
41+
let venvStatusCache: VenvStatusCache | null = null;
42+
const VENV_STATUS_TTL = 5 * 60 * 1000; // 5 minutes TTL
43+
3644
// Track running indexing process for cancellation
3745
let currentIndexingProcess: ReturnType<typeof spawn> | null = null;
3846
let currentIndexingAborted = false;
@@ -116,6 +124,13 @@ interface ProgressInfo {
116124
totalFiles?: number;
117125
}
118126

127+
/**
128+
* Clear venv status cache (call after install/uninstall operations)
129+
*/
130+
function clearVenvStatusCache(): void {
131+
venvStatusCache = null;
132+
}
133+
119134
/**
120135
* Detect available Python 3 executable
121136
* @returns Python executable command
@@ -138,17 +153,27 @@ function getSystemPython(): string {
138153

139154
/**
140155
* Check if CodexLens venv exists and has required packages
156+
* @param force - Force refresh cache (default: false)
141157
* @returns Ready status
142158
*/
143-
async function checkVenvStatus(): Promise<ReadyStatus> {
159+
async function checkVenvStatus(force = false): Promise<ReadyStatus> {
160+
// Use cached result if available and not expired
161+
if (!force && venvStatusCache && (Date.now() - venvStatusCache.timestamp < VENV_STATUS_TTL)) {
162+
return venvStatusCache.status;
163+
}
164+
144165
// Check venv exists
145166
if (!existsSync(CODEXLENS_VENV)) {
146-
return { ready: false, error: 'Venv not found' };
167+
const result = { ready: false, error: 'Venv not found' };
168+
venvStatusCache = { status: result, timestamp: Date.now() };
169+
return result;
147170
}
148171

149172
// Check python executable exists
150173
if (!existsSync(VENV_PYTHON)) {
151-
return { ready: false, error: 'Python executable not found in venv' };
174+
const result = { ready: false, error: 'Python executable not found in venv' };
175+
venvStatusCache = { status: result, timestamp: Date.now() };
176+
return result;
152177
}
153178

154179
// Check codexlens is importable
@@ -169,15 +194,21 @@ async function checkVenvStatus(): Promise<ReadyStatus> {
169194
});
170195

171196
child.on('close', (code) => {
197+
let result: ReadyStatus;
172198
if (code === 0) {
173-
resolve({ ready: true, version: stdout.trim() });
199+
result = { ready: true, version: stdout.trim() };
174200
} else {
175-
resolve({ ready: false, error: `CodexLens not installed: ${stderr}` });
201+
result = { ready: false, error: `CodexLens not installed: ${stderr}` };
176202
}
203+
// Cache the result
204+
venvStatusCache = { status: result, timestamp: Date.now() };
205+
resolve(result);
177206
});
178207

179208
child.on('error', (err) => {
180-
resolve({ ready: false, error: `Failed to check venv: ${err.message}` });
209+
const result = { ready: false, error: `Failed to check venv: ${err.message}` };
210+
venvStatusCache = { status: result, timestamp: Date.now() };
211+
resolve(result);
181212
});
182213
});
183214
}
@@ -581,6 +612,8 @@ async function bootstrapVenv(): Promise<BootstrapResult> {
581612
execSync(`"${pipPath}" install codexlens`, { stdio: 'inherit' });
582613
}
583614

615+
// Clear cache after successful installation
616+
clearVenvStatusCache();
584617
return { success: true };
585618
} catch (err) {
586619
return { success: false, error: `Failed to install codexlens: ${(err as Error).message}` };
@@ -1300,6 +1333,7 @@ async function uninstallCodexLens(): Promise<BootstrapResult> {
13001333
// Reset bootstrap cache
13011334
bootstrapChecked = false;
13021335
bootstrapReady = false;
1336+
clearVenvStatusCache();
13031337

13041338
console.log('[CodexLens] CodexLens uninstalled successfully');
13051339
return { success: true, message: 'CodexLens uninstalled successfully' };

0 commit comments

Comments
 (0)