feat: Add verbose mode with worker lifecycle monitoring and improved error handling

Adds --verbose flag to init and index commands that shows timestamped progress output instead of animated progress bars. Implements worker timeout protection (10s per file) and periodic worker recycling (every 500 files) to prevent WASM memory crashes from hanging the entire indexing process. Includes detailed logging of worker lifecycle events and memory usage for debugging large repository indexing issues.
This commit is contained in:
Colby McHenry
2026-04-04 22:28:25 -05:00
parent 64d844c938
commit c19c0ca8fd
3 changed files with 223 additions and 46 deletions
+66 -14
View File
@@ -176,6 +176,40 @@ function formatDuration(ms: number): string {
// Shimmer progress renderer (runs in a worker thread for smooth animation)
// Imported at top of file from '../ui/shimmer-progress'
/**
* Create a plain-text progress callback for --verbose mode.
* No animations, no ANSI tricks — just timestamped lines to stdout.
*/
function createVerboseProgress(): (progress: { phase: string; current: number; total: number; currentFile?: string }) => void {
let lastPhase = '';
let lastPct = -1;
const startTime = Date.now();
return (progress) => {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
if (progress.phase !== lastPhase) {
lastPhase = progress.phase;
lastPct = -1;
console.log(`[${elapsed}s] Phase: ${progress.phase}`);
}
if (progress.total > 0) {
const pct = Math.floor((progress.current / progress.total) * 100);
// Log every 5% to keep output manageable
if (pct >= lastPct + 5 || progress.current === progress.total) {
lastPct = pct;
console.log(`[${elapsed}s] ${progress.current}/${progress.total} (${pct}%)${progress.currentFile ? `${progress.currentFile}` : ''}`);
}
} else if (progress.current > 0) {
// Scanning phase (no total yet) — log periodically
if (progress.current % 1000 === 0 || progress.current === 1) {
console.log(`[${elapsed}s] ${formatNumber(progress.current)} files found`);
}
}
};
}
/**
* Print success message
*/
@@ -330,7 +364,8 @@ program
.command('init [path]')
.description('Initialize CodeGraph in a project directory')
.option('-i, --index', 'Run initial indexing after initialization')
.action(async (pathArg: string | undefined, options: { index?: boolean }) => {
.option('-v, --verbose', 'Show detailed worker lifecycle and memory info')
.action(async (pathArg: string | undefined, options: { index?: boolean; verbose?: boolean }) => {
const projectPath = path.resolve(pathArg || process.cwd());
const clack = await importESM('@clack/prompts');
@@ -349,14 +384,22 @@ program
clack.log.success(`Initialized in ${projectPath}`);
if (options.index) {
process.stdout.write(`${colors.dim}${colors.reset}\n`);
const progress = createShimmerProgress();
let result: IndexResult;
const result = await cg.indexAll({
onProgress: progress.onProgress,
});
if (options.verbose) {
result = await cg.indexAll({
onProgress: createVerboseProgress(),
verbose: true,
});
} else {
process.stdout.write(`${colors.dim}${colors.reset}\n`);
const progress = createShimmerProgress();
result = await cg.indexAll({
onProgress: progress.onProgress,
});
await progress.stop();
}
await progress.stop();
printIndexResult(clack, result, projectPath);
} else {
clack.log.info('Run "codegraph index" to index the project');
@@ -423,7 +466,8 @@ program
.description('Index all files in the project')
.option('-f, --force', 'Force full re-index even if already indexed')
.option('-q, --quiet', 'Suppress progress output')
.action(async (pathArg: string | undefined, options: { force?: boolean; quiet?: boolean }) => {
.option('-v, --verbose', 'Show detailed worker lifecycle and memory info')
.action(async (pathArg: string | undefined, options: { force?: boolean; quiet?: boolean; verbose?: boolean }) => {
const projectPath = resolveProjectPath(pathArg);
try {
@@ -453,14 +497,22 @@ program
clack.log.info('Cleared existing index');
}
process.stdout.write(`${colors.dim}${colors.reset}\n`);
const progress = createShimmerProgress();
let result: IndexResult;
const result = await cg.indexAll({
onProgress: progress.onProgress,
});
if (options.verbose) {
result = await cg.indexAll({
onProgress: createVerboseProgress(),
verbose: true,
});
} else {
process.stdout.write(`${colors.dim}${colors.reset}\n`);
const progress = createShimmerProgress();
result = await cg.indexAll({
onProgress: progress.onProgress,
});
await progress.stop();
}
await progress.stop();
printIndexResult(clack, result, projectPath);
if (!result.success) {