diff --git a/eslint.config.mjs b/eslint.config.mjs index 8d1046583..c80b1eb6e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -59,11 +59,14 @@ export default [ }, }, - // CLI/server packages — allow console.log (CLI stdout is contract; HTTP server uses console for request logs) + // CLI/server packages — `console.log` IS the contract (CLI tool data output + // on stdout, e.g. `gitnexus query | jq`; server pretty-printed banners). + // Diagnostic logging (`warn`/`error`/`debug`/`info`) goes through pino like + // the rest of the codebase. { files: ['gitnexus/src/cli/**/*.ts', 'gitnexus/src/server/**/*.ts'], rules: { - 'no-console': 'off', + 'no-console': ['error', { allow: ['log'] }], }, }, diff --git a/gitnexus/src/cli/ai-context.ts b/gitnexus/src/cli/ai-context.ts index 1b8396301..41ba6c73e 100644 --- a/gitnexus/src/cli/ai-context.ts +++ b/gitnexus/src/cli/ai-context.ts @@ -10,6 +10,7 @@ import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import { type GeneratedSkillInfo } from './skill-gen.js'; +import { logger } from '../core/logger.js'; // ESM equivalent of __dirname const __filename = fileURLToPath(import.meta.url); @@ -293,7 +294,7 @@ Use GitNexus tools to accomplish this task. installedSkills.push(skill.name); } catch (err) { // Skip on error, don't fail the whole process - console.warn(`Warning: Could not install skill ${skill.name}:`, err); + logger.warn({ err }, `Warning: Could not install skill ${skill.name}:`); } } diff --git a/gitnexus/src/cli/analyze.ts b/gitnexus/src/cli/analyze.ts index 2c100228d..7d6c6f977 100644 --- a/gitnexus/src/cli/analyze.ts +++ b/gitnexus/src/cli/analyze.ts @@ -24,6 +24,7 @@ import { getGitRoot, hasGitDir } from '../storage/git.js'; import { runFullAnalysis } from '../core/run-analyze.js'; import { getMaxFileSizeBannerMessage } from '../core/ingestion/utils/max-file-size.js'; import fs from 'fs/promises'; +import { logger } from '../core/logger.js'; // Capture stderr.write at module load BEFORE anything (LadybugDB native // init, progress bar, console redirection) can monkey-patch it. The @@ -158,7 +159,7 @@ export const analyzeCommand = async (inputPath?: string, options?: AnalyzeOption if (options?.workerTimeout) { const workerTimeoutSeconds = Number(options.workerTimeout); if (!Number.isFinite(workerTimeoutSeconds) || workerTimeoutSeconds < 1) { - console.error(' --worker-timeout must be at least 1 second.\n'); + logger.error(' --worker-timeout must be at least 1 second.\n'); process.exitCode = 1; return; } @@ -175,7 +176,7 @@ export const analyzeCommand = async (inputPath?: string, options?: AnalyzeOption if (value === undefined) return true; const parsed = Number(value); if (!Number.isInteger(parsed) || parsed <= 0) { - console.error(` ${optionName} must be a positive integer.\n`); + logger.error(` ${optionName} must be a positive integer.\n`); process.exitCode = 1; return false; } @@ -206,7 +207,7 @@ export const analyzeCommand = async (inputPath?: string, options?: AnalyzeOption if (options?.embeddingDevice) { const allowed = new Set(['auto', 'cpu', 'dml', 'cuda', 'wasm']); if (!allowed.has(options.embeddingDevice)) { - console.error(' --embedding-device must be one of: auto, cpu, dml, cuda, wasm.\n'); + logger.error(' --embedding-device must be one of: auto, cpu, dml, cuda, wasm.\n'); process.exitCode = 1; return; } @@ -291,9 +292,15 @@ export const analyzeCommand = async (inputPath?: string, options?: AnalyzeOption }; process.on('SIGINT', sigintHandler); - // Route console output through bar.log() to prevent progress bar corruption + // Route console output through bar.log() to prevent progress bar corruption. + // This is a deliberate UI pattern (not a logging concern): analyze runs a + // long-lived progress bar on stdout; any concurrent console.* write would + // overwrite the bar mid-render. We capture originals, swap to barLog for + // the lifetime of the run, and restore on completion/error/SIGINT. const origLog = console.log.bind(console); + // eslint-disable-next-line no-console -- intentional console-routing for progress bar UX const origWarn = console.warn.bind(console); + // eslint-disable-next-line no-console -- intentional console-routing for progress bar UX const origError = console.error.bind(console); let barCurrentValue = 0; const barLog = (...args: any[]) => { @@ -302,7 +309,9 @@ export const analyzeCommand = async (inputPath?: string, options?: AnalyzeOption bar.update(barCurrentValue); }; console.log = barLog; + // eslint-disable-next-line no-console -- intentional console-routing for progress bar UX console.warn = barLog; + // eslint-disable-next-line no-console -- intentional console-routing for progress bar UX console.error = barLog; // Track elapsed time per phase @@ -367,7 +376,9 @@ export const analyzeCommand = async (inputPath?: string, options?: AnalyzeOption clearInterval(elapsedTimer); process.removeListener('SIGINT', sigintHandler); console.log = origLog; + // eslint-disable-next-line no-console -- restoring after intentional progress-bar routing console.warn = origWarn; + // eslint-disable-next-line no-console -- restoring after intentional progress-bar routing console.error = origError; bar.stop(); console.log(' Already up to date\n'); @@ -440,7 +451,9 @@ export const analyzeCommand = async (inputPath?: string, options?: AnalyzeOption process.removeListener('SIGINT', sigintHandler); console.log = origLog; + // eslint-disable-next-line no-console -- restoring after intentional progress-bar routing console.warn = origWarn; + // eslint-disable-next-line no-console -- restoring after intentional progress-bar routing console.error = origError; bar.update(100, { phase: 'Done' }); @@ -465,7 +478,9 @@ export const analyzeCommand = async (inputPath?: string, options?: AnalyzeOption clearInterval(elapsedTimer); process.removeListener('SIGINT', sigintHandler); console.log = origLog; + // eslint-disable-next-line no-console -- restoring after intentional progress-bar routing console.warn = origWarn; + // eslint-disable-next-line no-console -- restoring after intentional progress-bar routing console.error = origError; bar.stop(); @@ -474,14 +489,14 @@ export const analyzeCommand = async (inputPath?: string, options?: AnalyzeOption // Registry name-collision from --name (#829) — surface as an // actionable error rather than a generic stack-trace. if (err instanceof RegistryNameCollisionError) { - console.error(`\n Registry name collision:\n`); - console.error(` "${err.registryName}" is already used by "${err.existingPath}".\n`); - console.error(` Options:`); - console.error(` • Pick a different alias: gitnexus analyze --name `); - console.error( + logger.error(`\n Registry name collision:\n`); + logger.error(` "${err.registryName}" is already used by "${err.existingPath}".\n`); + logger.error(` Options:`); + logger.error(` • Pick a different alias: gitnexus analyze --name `); + logger.error( ` • Allow the duplicate: gitnexus analyze --allow-duplicate-name (leaves "-r ${err.registryName}" ambiguous)`, ); - console.error(''); + logger.error(''); process.exitCode = 1; return; } @@ -521,34 +536,34 @@ export const analyzeCommand = async (inputPath?: string, options?: AnalyzeOption msg.includes('heap out of memory') || msg.includes('JavaScript heap') ) { - console.error(' This error typically occurs on very large repositories.'); - console.error(' Suggestions:'); - console.error(' 1. Add large vendored/generated directories to .gitnexusignore'); - console.error(' 2. Increase Node.js heap: NODE_OPTIONS="--max-old-space-size=16384"'); - console.error(' 3. Increase stack size: NODE_OPTIONS="--stack-size=4096"'); - console.error(''); + logger.error(' This error typically occurs on very large repositories.'); + logger.error(' Suggestions:'); + logger.error(' 1. Add large vendored/generated directories to .gitnexusignore'); + logger.error(' 2. Increase Node.js heap: NODE_OPTIONS="--max-old-space-size=16384"'); + logger.error(' 3. Increase stack size: NODE_OPTIONS="--stack-size=4096"'); + logger.error(''); } else if (msg.includes('ERESOLVE') || msg.includes('Could not resolve dependency')) { // Note: the original arborist "Cannot destructure property 'package' of // 'node.target'" crash happens inside npm *before* gitnexus code runs, // so it can't be caught here. This branch handles dependency-resolution // errors that surface at runtime (e.g. dynamic require failures). - console.error(' This looks like an npm dependency resolution issue.'); - console.error(' Suggestions:'); - console.error(' 1. Clear the npm cache: npm cache clean --force'); - console.error(' 2. Update npm: npm install -g npm@latest'); - console.error(' 3. Reinstall gitnexus: npm install -g gitnexus@latest'); - console.error(' 4. Or try npx directly: npx gitnexus@latest analyze'); - console.error(''); + logger.error(' This looks like an npm dependency resolution issue.'); + logger.error(' Suggestions:'); + logger.error(' 1. Clear the npm cache: npm cache clean --force'); + logger.error(' 2. Update npm: npm install -g npm@latest'); + logger.error(' 3. Reinstall gitnexus: npm install -g gitnexus@latest'); + logger.error(' 4. Or try npx directly: npx gitnexus@latest analyze'); + logger.error(''); } else if ( msg.includes('MODULE_NOT_FOUND') || msg.includes('Cannot find module') || msg.includes('ERR_MODULE_NOT_FOUND') ) { - console.error(' A required module could not be loaded. The installation may be corrupt.'); - console.error(' Suggestions:'); - console.error(' 1. Reinstall: npm install -g gitnexus@latest'); - console.error(' 2. Clear cache: npm cache clean --force && npx gitnexus@latest analyze'); - console.error(''); + logger.error(' A required module could not be loaded. The installation may be corrupt.'); + logger.error(' Suggestions:'); + logger.error(' 1. Reinstall: npm install -g gitnexus@latest'); + logger.error(' 2. Clear cache: npm cache clean --force && npx gitnexus@latest analyze'); + logger.error(''); } process.exitCode = 1; diff --git a/gitnexus/src/cli/clean.ts b/gitnexus/src/cli/clean.ts index 4681508fb..2bbc32aef 100644 --- a/gitnexus/src/cli/clean.ts +++ b/gitnexus/src/cli/clean.ts @@ -6,6 +6,7 @@ */ import fs from 'fs/promises'; +import { logger } from '../core/logger.js'; import { findRepo, unregisterRepo, @@ -45,7 +46,7 @@ export const cleanCommand = async (options?: { force?: boolean; all?: boolean }) assertSafeStoragePath(entry); } catch (err) { if (err instanceof UnsafeStoragePathError) { - console.error(`Refusing to clean ${entry.name}: ${err.message}`); + logger.error(`Refusing to clean ${entry.name}: ${err.message}`); continue; } throw err; @@ -56,7 +57,7 @@ export const cleanCommand = async (options?: { force?: boolean; all?: boolean }) await unregisterRepo(entry.path); console.log(`Deleted: ${entry.name} (${entry.storagePath})`); } catch (err) { - console.error(`Failed to delete ${entry.name}:`, err); + logger.error({ err }, `Failed to delete ${entry.name}:`); } } return; @@ -85,6 +86,6 @@ export const cleanCommand = async (options?: { force?: boolean; all?: boolean }) await unregisterRepo(repo.repoPath); console.log(`Deleted: ${repo.storagePath}`); } catch (err) { - console.error('Failed to delete:', err); + logger.error({ err }, 'Failed to delete:'); } }; diff --git a/gitnexus/src/cli/eval-server.ts b/gitnexus/src/cli/eval-server.ts index cf9013d63..b3ecd7b20 100644 --- a/gitnexus/src/cli/eval-server.ts +++ b/gitnexus/src/cli/eval-server.ts @@ -27,6 +27,7 @@ import http from 'http'; import { writeSync } from 'node:fs'; import { LocalBackend } from '../mcp/local/local-backend.js'; +import { logger } from '../core/logger.js'; export interface EvalServerOptions { port?: string; @@ -332,12 +333,12 @@ export async function evalServerCommand(options?: EvalServerOptions): Promise r.name).join(', ')}`, ); @@ -347,7 +348,7 @@ export async function evalServerCommand(options?: EvalServerOptions): Promise { - console.error('GitNexus eval-server: Idle timeout reached, shutting down'); + logger.error('GitNexus eval-server: Idle timeout reached, shutting down'); await backend.disconnect(); process.exit(0); }, idleTimeoutSec * 1000); @@ -419,15 +420,15 @@ export async function evalServerCommand(options?: EvalServerOptions): Promise { - console.error(`GitNexus eval-server: listening on http://127.0.0.1:${port}`); - console.error(` POST /tool/query — search execution flows`); - console.error(` POST /tool/context — 360-degree symbol view`); - console.error(` POST /tool/impact — blast radius analysis`); - console.error(` POST /tool/cypher — raw Cypher query`); - console.error(` GET /health — health check`); - console.error(` POST /shutdown — graceful shutdown`); + logger.error(`GitNexus eval-server: listening on http://127.0.0.1:${port}`); + logger.error(` POST /tool/query — search execution flows`); + logger.error(` POST /tool/context — 360-degree symbol view`); + logger.error(` POST /tool/impact — blast radius analysis`); + logger.error(` POST /tool/cypher — raw Cypher query`); + logger.error(` GET /health — health check`); + logger.error(` POST /shutdown — graceful shutdown`); if (idleTimeoutSec > 0) { - console.error(` Auto-shutdown after ${idleTimeoutSec}s idle`); + logger.error(` Auto-shutdown after ${idleTimeoutSec}s idle`); } try { // Use fd 1 directly — LadybugDB captures process.stdout (#324) @@ -440,7 +441,7 @@ export async function evalServerCommand(options?: EvalServerOptions): Promise { - console.error('GitNexus eval-server: shutting down...'); + logger.error('GitNexus eval-server: shutting down...'); await backend.disconnect(); server.close(); process.exit(0); diff --git a/gitnexus/src/cli/group.ts b/gitnexus/src/cli/group.ts index eb0dffc3d..0053099b7 100644 --- a/gitnexus/src/cli/group.ts +++ b/gitnexus/src/cli/group.ts @@ -1,6 +1,7 @@ // gitnexus/src/cli/group.ts import { createRequire } from 'node:module'; import type { Command } from 'commander'; +import { logger } from '../core/logger.js'; const _require = createRequire(import.meta.url); const yaml = _require('js-yaml') as typeof import('js-yaml'); @@ -51,7 +52,7 @@ export function registerGroupCommands(program: Command): void { const groupDir = getGroupDir(getDefaultGitnexusDir(), groupName); const config = await loadGroupConfig(groupDir); if (!(repoPath in config.repos)) { - console.error(`Repo path "${repoPath}" not found in group "${groupName}"`); + logger.error(`Repo path "${repoPath}" not found in group "${groupName}"`); process.exitCode = 1; return; } @@ -239,7 +240,7 @@ export function registerGroupCommands(program: Command): void { const raw = await backend.getGroupService().groupImpact(payload); if (raw && typeof raw === 'object' && 'error' in raw) { - console.error(String((raw as { error: string }).error)); + logger.error(String((raw as { error: string }).error)); process.exitCode = 1; return; } @@ -333,7 +334,7 @@ export function registerGroupCommands(program: Command): void { }); if (raw && typeof raw === 'object' && 'error' in raw) { - console.error(String((raw as { error: string }).error)); + logger.error(String((raw as { error: string }).error)); process.exitCode = 1; return; } diff --git a/gitnexus/src/cli/mcp.ts b/gitnexus/src/cli/mcp.ts index 58c7f10f0..3bc708087 100644 --- a/gitnexus/src/cli/mcp.ts +++ b/gitnexus/src/cli/mcp.ts @@ -8,18 +8,19 @@ import { startMCPServer } from '../mcp/server.js'; import { LocalBackend } from '../mcp/local/local-backend.js'; +import { logger } from '../core/logger.js'; export const mcpCommand = async () => { // Prevent unhandled errors from crashing the MCP server process. // LadybugDB lock conflicts and transient errors should degrade gracefully. process.on('uncaughtException', (err) => { - console.error(`GitNexus MCP: uncaught exception — ${err.message}`); + logger.error(`GitNexus MCP: uncaught exception — ${err.message}`); // Process is in an undefined state after uncaughtException — exit after flushing setTimeout(() => process.exit(1), 100); }); process.on('unhandledRejection', (reason) => { const msg = reason instanceof Error ? reason.message : String(reason); - console.error(`GitNexus MCP: unhandled rejection — ${msg}`); + logger.error(`GitNexus MCP: unhandled rejection — ${msg}`); }); // Initialize multi-repo backend from registry. @@ -30,11 +31,11 @@ export const mcpCommand = async () => { const repos = await backend.listRepos(); if (repos.length === 0) { - console.error( + logger.error( 'GitNexus: No indexed repos yet. Run `gitnexus analyze` in a git repo — the server will pick it up automatically.', ); } else { - console.error( + logger.error( `GitNexus: MCP server starting with ${repos.length} repo(s): ${repos.map((r) => r.name).join(', ')}`, ); } diff --git a/gitnexus/src/cli/remove.ts b/gitnexus/src/cli/remove.ts index 4d2ce0771..cb8a78d78 100644 --- a/gitnexus/src/cli/remove.ts +++ b/gitnexus/src/cli/remove.ts @@ -27,6 +27,7 @@ */ import fs from 'fs/promises'; +import { logger } from '../core/logger.js'; import { readRegistry, resolveRegistryEntry, @@ -51,14 +52,14 @@ export const removeCommand = async (target: string, options?: { force?: boolean // Idempotent: missing target is a no-op warning, not an error. // The `availableNames` hint comes from the error itself so users // can see what they might have meant. - console.warn(`Nothing to remove: ${err.message}`); + logger.warn(`Nothing to remove: ${err.message}`); return; } if (err instanceof RegistryAmbiguousTargetError) { // Duplicate aliases are allowed via --allow-duplicate-name (#829); // refuse to guess which one the user meant — surface the full list // and exit non-zero so scripts don't silently pick the wrong repo. - console.error(`Error: ${err.message}`); + logger.error(`Error: ${err.message}`); process.exit(1); } throw err; @@ -86,7 +87,7 @@ export const removeCommand = async (target: string, options?: { force?: boolean assertSafeStoragePath(entry); } catch (err) { if (err instanceof UnsafeStoragePathError) { - console.error(`Error: ${err.message}`); + logger.error(`Error: ${err.message}`); process.exit(1); } throw err; @@ -104,7 +105,7 @@ export const removeCommand = async (target: string, options?: { force?: boolean console.log(` Path: ${entry.path}`); console.log(` Storage: ${entry.storagePath}`); } catch (err) { - console.error(`Failed to remove ${entry.name}:`, err); + logger.error({ err }, `Failed to remove ${entry.name}:`); process.exit(1); } }; diff --git a/gitnexus/src/cli/serve.ts b/gitnexus/src/cli/serve.ts index 9f0379306..f0230ce16 100644 --- a/gitnexus/src/cli/serve.ts +++ b/gitnexus/src/cli/serve.ts @@ -1,14 +1,15 @@ import { createServer } from '../server/api.js'; +import { logger } from '../core/logger.js'; // Catch anything that would cause a silent exit process.on('uncaughtException', (err) => { - console.error('\n[gitnexus serve] Uncaught exception:', err.message); - if (process.env.DEBUG) console.error(err.stack); + logger.error({ err: err.message }, '\n[gitnexus serve] Uncaught exception:'); + if (process.env.DEBUG) logger.error(err.stack); process.exit(1); }); process.on('unhandledRejection', (reason: any) => { - console.error('\n[gitnexus serve] Unhandled rejection:', reason?.message || reason); - if (process.env.DEBUG) console.error(reason?.stack); + logger.error({ err: reason?.message || reason }, '\n[gitnexus serve] Unhandled rejection:'); + if (process.env.DEBUG) logger.error(reason?.stack); process.exit(1); }); @@ -22,15 +23,15 @@ export const serveCommand = async (options?: { port?: string; host?: string }) = try { await createServer(port, host); } catch (err: any) { - console.error(`\nFailed to start GitNexus server:\n`); - console.error(` ${err.message || err}\n`); + logger.error(`\nFailed to start GitNexus server:\n`); + logger.error(` ${err.message || err}\n`); if (err.code === 'EADDRINUSE') { - console.error(` Port ${port} is already in use. Either:`); - console.error(` 1. Stop the other process using port ${port}`); - console.error(` 2. Use a different port: gitnexus serve --port 4748\n`); + logger.error(` Port ${port} is already in use. Either:`); + logger.error(` 1. Stop the other process using port ${port}`); + logger.error(` 2. Use a different port: gitnexus serve --port 4748\n`); } if (err.stack && process.env.DEBUG) { - console.error(err.stack); + logger.error(err.stack); } process.exit(1); } diff --git a/gitnexus/src/cli/tool.ts b/gitnexus/src/cli/tool.ts index 443f12f4c..20ef61f3f 100644 --- a/gitnexus/src/cli/tool.ts +++ b/gitnexus/src/cli/tool.ts @@ -17,6 +17,7 @@ import { writeSync } from 'node:fs'; import { LocalBackend } from '../mcp/local/local-backend.js'; +import { logger } from '../core/logger.js'; let _backend: LocalBackend | null = null; @@ -25,7 +26,7 @@ async function getBackend(): Promise { _backend = new LocalBackend(); const ok = await _backend.init(); if (!ok) { - console.error('GitNexus: No indexed repositories found. Run: gitnexus analyze'); + logger.error('GitNexus: No indexed repositories found. Run: gitnexus analyze'); process.exit(1); } return _backend; @@ -67,7 +68,7 @@ export async function queryCommand( }, ): Promise { if (!queryText?.trim()) { - console.error('Usage: gitnexus query '); + logger.error('Usage: gitnexus query '); process.exit(1); } @@ -93,7 +94,7 @@ export async function contextCommand( }, ): Promise { if (!name?.trim() && !options?.uid) { - console.error('Usage: gitnexus context [--uid ] [--file ]'); + logger.error('Usage: gitnexus context [--uid ] [--file ]'); process.exit(1); } @@ -118,7 +119,7 @@ export async function impactCommand( }, ): Promise { if (!target?.trim()) { - console.error('Usage: gitnexus impact [--direction upstream|downstream]'); + logger.error('Usage: gitnexus impact [--direction upstream|downstream]'); process.exit(1); } @@ -153,7 +154,7 @@ export async function cypherCommand( }, ): Promise { if (!query?.trim()) { - console.error('Usage: gitnexus cypher '); + logger.error('Usage: gitnexus cypher '); process.exit(1); } diff --git a/gitnexus/src/cli/wiki.ts b/gitnexus/src/cli/wiki.ts index ccd1cae4e..d304deb2a 100644 --- a/gitnexus/src/cli/wiki.ts +++ b/gitnexus/src/cli/wiki.ts @@ -19,6 +19,7 @@ import { import { WikiGenerator, type WikiOptions } from '../core/wiki/generator.js'; import { resolveLLMConfig, type LLMProvider } from '../core/wiki/llm-client.js'; import { detectCursorCLI } from '../core/wiki/cursor-client.js'; +import { logger } from '../core/logger.js'; export interface WikiCommandOptions { force?: boolean; @@ -583,7 +584,7 @@ export const wikiCommand = async (inputPath?: string, options?: WikiCommandOptio } else { console.log(`\n Error: ${err.message}\n`); if (process.env.GITNEXUS_VERBOSE) { - console.error(err); + logger.error(err); } } process.exitCode = 1; diff --git a/gitnexus/src/server/api.ts b/gitnexus/src/server/api.ts index d0c9c5577..baa6406bb 100644 --- a/gitnexus/src/server/api.ts +++ b/gitnexus/src/server/api.ts @@ -26,8 +26,6 @@ import { isWriteQuery } from '../core/lbug/pool-adapter.js'; import { NODE_TABLES, type GraphNode, type GraphRelationship } from 'gitnexus-shared'; import { searchFTSFromLbug } from '../core/search/bm25-index.js'; import { hybridSearch } from '../core/search/hybrid-search.js'; -// Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node -// at server startup — crashes on unsupported Node ABI versions (#89) import { LocalBackend } from '../mcp/local/local-backend.js'; import { mountMCPEndpoints } from './mcp-http.js'; import { fork } from 'child_process'; @@ -35,6 +33,7 @@ import { fileURLToPath, pathToFileURL } from 'url'; import { JobManager } from './analyze-job.js'; import { assertString, escapeRegExp, BadRequestError, createRouteLimiter } from './validation.js'; import { extractRepoName, getCloneDir, cloneOrPull } from './git-clone.js'; +import { logger } from '../core/logger.js'; const _require = createRequire(import.meta.url); const pkg = _require('../../package.json'); @@ -142,7 +141,7 @@ export const resolveWebDistDir = async ( return dir; } catch (err: any) { if (err?.code !== 'ENOENT') { - console.warn(`[serve] could not access web UI dir ${dir}:`, err.message); + logger.warn({ err: err.message }, `[serve] could not access web UI dir ${dir}:`); } } } @@ -1481,7 +1480,7 @@ export const createServer = async (port: number, host: string = '127.0.0.1') => }); }) .catch((err) => { - console.error('backend.init() failed after analyze:', err); + logger.error({ err }, 'backend.init() failed after analyze:'); jobManager.updateJob(job.id, { status: 'failed', error: 'Server failed to reload after analysis. Try again.', @@ -1513,7 +1512,7 @@ export const createServer = async (port: number, host: string = '127.0.0.1') => j.retryCount++; const delay = 1000 * Math.pow(2, j.retryCount - 1); // 1s, 2s const lastErr = stderrChunks.trim().split('\n').pop() || ''; - console.warn( + logger.warn( `Analyze worker crashed (code ${code}), retry ${j.retryCount}/${MAX_WORKER_RETRIES} in ${delay}ms` + (lastErr ? `: ${lastErr}` : ''), ); @@ -1775,7 +1774,7 @@ export const createServer = async (port: number, host: string = '127.0.0.1') => // Global error handler — catch anything the route handlers miss app.use((err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => { - console.error('Unhandled error:', err); + logger.error({ err }, 'Unhandled error:'); res.status(500).json({ error: 'Internal server error' }); }); @@ -1806,14 +1805,14 @@ export const createServer = async (port: number, host: string = '127.0.0.1') => // Catch-all crash guards (mirrors startMCPServer in mcp/server.ts) let shuttingDown = false; process.on('uncaughtException', (err) => { - console.error('GitNexus uncaughtException:', err?.stack || err); + logger.error({ err: err?.stack || err }, 'GitNexus uncaughtException:'); if (!shuttingDown) { shuttingDown = true; shutdown().catch(() => {}); } }); process.on('unhandledRejection', (reason: any) => { - console.error('GitNexus unhandledRejection:', reason?.stack || reason); + logger.error({ detail: reason?.stack || reason }, 'GitNexus unhandledRejection:'); }); }); }; diff --git a/gitnexus/src/server/git-clone.ts b/gitnexus/src/server/git-clone.ts index b1f369dd7..861a16648 100644 --- a/gitnexus/src/server/git-clone.ts +++ b/gitnexus/src/server/git-clone.ts @@ -10,6 +10,7 @@ import path from 'path'; import os from 'os'; import fs from 'fs/promises'; import { isIP } from 'net'; +import { logger } from '../core/logger.js'; /** Root directory for all cloned repositories. Targets must resolve inside this. */ const CLONE_ROOT = path.resolve(path.join(os.homedir(), '.gitnexus', 'repos')); @@ -446,7 +447,7 @@ function runGit(args: string[], cwd?: string): Promise { if (code === 0) resolve(); else { // Log full stderr internally but don't expose it to API callers (SSRF mitigation) - if (stderr.trim()) console.error(`git ${args[0]} stderr: ${stderr.trim()}`); + if (stderr.trim()) logger.error(`git ${args[0]} stderr: ${stderr.trim()}`); reject(new Error(`git ${args[0]} failed (exit code ${code})`)); } }); diff --git a/gitnexus/src/server/mcp-http.ts b/gitnexus/src/server/mcp-http.ts index 0bc086360..67ec94708 100644 --- a/gitnexus/src/server/mcp-http.ts +++ b/gitnexus/src/server/mcp-http.ts @@ -15,6 +15,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { createMCPServer } from '../mcp/server.js'; import type { LocalBackend } from '../mcp/local/local-backend.js'; import { randomUUID } from 'crypto'; +import { logger } from '../core/logger.js'; interface MCPSession { server: Server; @@ -87,7 +88,7 @@ export function mountMCPEndpoints(app: Express, backend: LocalBackend): () => Pr app.all('/api/mcp', (req: Request, res: Response) => { void handleMcpRequest(req, res).catch((err: any) => { - console.error('MCP HTTP request failed:', err); + logger.error({ err }, 'MCP HTTP request failed:'); if (res.headersSent) return; res.status(500).json({ jsonrpc: '2.0', diff --git a/gitnexus/test/unit/analyze-worker-timeout.test.ts b/gitnexus/test/unit/analyze-worker-timeout.test.ts index e23c0a906..3ed3a3d5e 100644 --- a/gitnexus/test/unit/analyze-worker-timeout.test.ts +++ b/gitnexus/test/unit/analyze-worker-timeout.test.ts @@ -39,17 +39,20 @@ describe('analyzeCommand worker timeout validation', () => { it.each(['0', 'abc', '-5', 'Infinity'])( 'rejects invalid --worker-timeout value %s before analysis starts', async (workerTimeout) => { - // CLI code (cli/analyze.ts) is exempt from the pino migration — - // user-facing stdout/stderr is the contract. Spy on console.error. - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + // Import _captureLogger from the SAME module instance analyze.js will + // see — vi.resetModules() in beforeEach invalidates the singleton. + const { _captureLogger } = await import('../../src/core/logger.js'); + const cap = _captureLogger(); const { analyzeCommand } = await import('../../src/cli/analyze.js'); await analyzeCommand(undefined, { workerTimeout }); expect(process.exitCode).toBe(1); - expect(errorSpy).toHaveBeenCalledWith(' --worker-timeout must be at least 1 second.\n'); + expect( + cap.records().some((r) => r.msg === ' --worker-timeout must be at least 1 second.\n'), + ).toBe(true); expect(runFullAnalysisMock).not.toHaveBeenCalled(); - errorSpy.mockRestore(); + cap.restore(); }, ); diff --git a/gitnexus/test/unit/web-ui-serving.test.ts b/gitnexus/test/unit/web-ui-serving.test.ts index d3f085422..bd1c7faaf 100644 --- a/gitnexus/test/unit/web-ui-serving.test.ts +++ b/gitnexus/test/unit/web-ui-serving.test.ts @@ -2,6 +2,7 @@ import path from 'node:path'; import http from 'node:http'; import express from 'express'; import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { _captureLogger } from '../../src/core/logger.js'; const { accessMock } = vi.hoisted(() => ({ accessMock: vi.fn(), @@ -213,7 +214,7 @@ describe('resolveWebDistDir', () => { }); it('warns on non-ENOENT errors but continues', async () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const cap = _captureLogger(); accessMock.mockImplementation(async (p: string) => { if (p.includes('primary')) throw Object.assign(new Error('permission denied'), { code: 'EACCES' }); @@ -222,11 +223,16 @@ describe('resolveWebDistDir', () => { }); const result = await resolveWebDistDir('/primary', '/fallback'); expect(result).toBe('/fallback'); - expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining('could not access web UI dir /primary'), - 'permission denied', - ); - warnSpy.mockRestore(); + expect( + cap + .records() + .some( + (r) => + String(r.msg ?? '').includes('could not access web UI dir /primary') && + r.err === 'permission denied', + ), + ).toBe(true); + cap.restore(); }); it('prefers GITNEXUS_WEB_DIST env var when set', async () => {