gitnexus 1.6.4-rc.85 → 1.6.4-rc.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/cli/ai-context.js +2 -1
  2. package/dist/cli/analyze.js +48 -31
  3. package/dist/cli/clean.js +4 -3
  4. package/dist/cli/cli-message.d.ts +15 -0
  5. package/dist/cli/cli-message.js +61 -0
  6. package/dist/cli/eval-server.js +37 -12
  7. package/dist/cli/group.js +4 -3
  8. package/dist/cli/mcp.js +13 -5
  9. package/dist/cli/optional-grammars.js +13 -4
  10. package/dist/cli/remove.js +7 -4
  11. package/dist/cli/serve.js +31 -13
  12. package/dist/cli/tool.js +6 -5
  13. package/dist/cli/wiki.js +2 -1
  14. package/dist/config/ignore-service.js +2 -1
  15. package/dist/core/embeddings/embedder.js +9 -8
  16. package/dist/core/embeddings/embedding-pipeline.js +15 -13
  17. package/dist/core/group/bridge-db.js +3 -4
  18. package/dist/core/group/extractors/elixir-workspace-extractor.js +2 -1
  19. package/dist/core/group/extractors/go-workspace-extractor.js +2 -1
  20. package/dist/core/group/extractors/grpc-extractor.js +2 -1
  21. package/dist/core/group/extractors/java-workspace-extractor.js +2 -1
  22. package/dist/core/group/extractors/manifest-extractor.js +2 -1
  23. package/dist/core/group/extractors/node-workspace-extractor.js +2 -1
  24. package/dist/core/group/extractors/python-workspace-extractor.js +2 -1
  25. package/dist/core/group/extractors/rust-workspace-extractor.js +2 -1
  26. package/dist/core/group/service.js +5 -4
  27. package/dist/core/group/sync.js +4 -3
  28. package/dist/core/ingestion/ast-cache.js +2 -1
  29. package/dist/core/ingestion/call-processor.js +3 -2
  30. package/dist/core/ingestion/cluster-enricher.js +3 -2
  31. package/dist/core/ingestion/cobol/cobol-copy-expander.js +4 -20
  32. package/dist/core/ingestion/filesystem-walker.js +3 -2
  33. package/dist/core/ingestion/heritage-processor.js +3 -2
  34. package/dist/core/ingestion/import-processor.js +14 -12
  35. package/dist/core/ingestion/language-config.js +6 -5
  36. package/dist/core/ingestion/method-extractors/generic.js +2 -1
  37. package/dist/core/ingestion/parsing-processor.js +7 -6
  38. package/dist/core/ingestion/pipeline-phases/cobol.js +4 -3
  39. package/dist/core/ingestion/pipeline-phases/communities.js +2 -1
  40. package/dist/core/ingestion/pipeline-phases/cross-file-impl.js +5 -4
  41. package/dist/core/ingestion/pipeline-phases/cross-file.js +3 -2
  42. package/dist/core/ingestion/pipeline-phases/markdown.js +2 -1
  43. package/dist/core/ingestion/pipeline-phases/mro.js +2 -1
  44. package/dist/core/ingestion/pipeline-phases/orm.js +2 -1
  45. package/dist/core/ingestion/pipeline-phases/parse-impl.js +9 -8
  46. package/dist/core/ingestion/pipeline-phases/processes.js +3 -2
  47. package/dist/core/ingestion/pipeline-phases/routes.js +4 -3
  48. package/dist/core/ingestion/pipeline-phases/runner.js +3 -2
  49. package/dist/core/ingestion/pipeline-phases/tools.js +2 -1
  50. package/dist/core/ingestion/process-processor.js +4 -3
  51. package/dist/core/ingestion/scope-extractor-bridge.js +2 -2
  52. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +3 -2
  53. package/dist/core/ingestion/scope-resolution/pipeline/run.js +2 -1
  54. package/dist/core/ingestion/type-env.js +2 -1
  55. package/dist/core/ingestion/utils/max-file-size.js +2 -1
  56. package/dist/core/ingestion/workers/parse-worker.js +5 -4
  57. package/dist/core/ingestion/workers/worker-pool.js +19 -8
  58. package/dist/core/lbug/extension-loader.js +2 -1
  59. package/dist/core/lbug/lbug-adapter.js +5 -4
  60. package/dist/core/logger.d.ts +125 -0
  61. package/dist/core/logger.js +323 -0
  62. package/dist/core/tree-sitter/parser-loader.js +10 -4
  63. package/dist/core/wiki/cursor-client.js +2 -1
  64. package/dist/core/wiki/llm-client.js +2 -9
  65. package/dist/mcp/core/embedder.js +3 -2
  66. package/dist/mcp/local/local-backend.js +17 -16
  67. package/dist/mcp/server.js +7 -1
  68. package/dist/server/api.js +22 -10
  69. package/dist/server/git-clone.js +2 -1
  70. package/dist/server/mcp-http.js +2 -1
  71. package/package.json +3 -1
@@ -8,6 +8,7 @@
8
8
  import fs from 'fs/promises';
9
9
  import path from 'path';
10
10
  import { fileURLToPath } from 'url';
11
+ import { logger } from '../core/logger.js';
11
12
  // ESM equivalent of __dirname
12
13
  const __filename = fileURLToPath(import.meta.url);
13
14
  const __dirname = path.dirname(__filename);
@@ -235,7 +236,7 @@ Use GitNexus tools to accomplish this task.
235
236
  }
236
237
  catch (err) {
237
238
  // Skip on error, don't fail the whole process
238
- console.warn(`Warning: Could not install skill ${skill.name}:`, err);
239
+ logger.warn({ err }, `Warning: Could not install skill ${skill.name}:`);
239
240
  }
240
241
  }
241
242
  return installedSkills;
@@ -19,6 +19,7 @@ import { getMaxFileSizeBannerMessage } from '../core/ingestion/utils/max-file-si
19
19
  import { warnMissingOptionalGrammars } from './optional-grammars.js';
20
20
  import { glob } from 'glob';
21
21
  import fs from 'fs/promises';
22
+ import { cliError } from './cli-message.js';
22
23
  // Capture stderr.write at module load BEFORE anything (LadybugDB native
23
24
  // init, progress bar, console redirection) can monkey-patch it. The
24
25
  // fatal handlers below MUST reach the user even when the analyze path
@@ -100,7 +101,7 @@ export const analyzeCommand = async (inputPath, options) => {
100
101
  if (options?.workerTimeout) {
101
102
  const workerTimeoutSeconds = Number(options.workerTimeout);
102
103
  if (!Number.isFinite(workerTimeoutSeconds) || workerTimeoutSeconds < 1) {
103
- console.error(' --worker-timeout must be at least 1 second.\n');
104
+ cliError(' --worker-timeout must be at least 1 second.\n');
104
105
  process.exitCode = 1;
105
106
  return;
106
107
  }
@@ -114,7 +115,7 @@ export const analyzeCommand = async (inputPath, options) => {
114
115
  if (typeof options?.embeddings === 'string') {
115
116
  const parsed = Number(options.embeddings);
116
117
  if (!Number.isInteger(parsed) || parsed < 0) {
117
- console.error(` --embeddings expects a non-negative integer (got "${options.embeddings}"). ` +
118
+ cliError(` --embeddings expects a non-negative integer (got "${options.embeddings}"). ` +
118
119
  `Pass 0 to disable the safety cap, or omit the value to keep the default.\n`);
119
120
  process.exitCode = 1;
120
121
  return;
@@ -127,7 +128,7 @@ export const analyzeCommand = async (inputPath, options) => {
127
128
  return true;
128
129
  const parsed = Number(value);
129
130
  if (!Number.isInteger(parsed) || parsed <= 0) {
130
- console.error(` ${optionName} must be a positive integer.\n`);
131
+ cliError(` ${optionName} must be a positive integer.\n`);
131
132
  process.exitCode = 1;
132
133
  return false;
133
134
  }
@@ -142,7 +143,7 @@ export const analyzeCommand = async (inputPath, options) => {
142
143
  if (options?.embeddingDevice) {
143
144
  const allowed = new Set(['auto', 'cpu', 'dml', 'cuda', 'wasm']);
144
145
  if (!allowed.has(options.embeddingDevice)) {
145
- console.error(' --embedding-device must be one of: auto, cpu, dml, cuda, wasm.\n');
146
+ cliError(' --embedding-device must be one of: auto, cpu, dml, cuda, wasm.\n');
146
147
  process.exitCode = 1;
147
148
  return;
148
149
  }
@@ -221,7 +222,9 @@ export const analyzeCommand = async (inputPath, options) => {
221
222
  stopOnComplete: false,
222
223
  }, cliProgress.Presets.shades_grey);
223
224
  bar.start(100, 0, { phase: 'Initializing...' });
224
- // Graceful SIGINT handling
225
+ // Graceful SIGINT handling. Pino's default destination is `sync: false`
226
+ // (buffered) — flush before exit so in-flight records reach stderr.
227
+ // See `gitnexus/src/core/logger.ts:flushLoggerSync`.
225
228
  let aborted = false;
226
229
  const sigintHandler = () => {
227
230
  if (aborted)
@@ -231,12 +234,22 @@ export const analyzeCommand = async (inputPath, options) => {
231
234
  console.log('\n Interrupted — cleaning up...');
232
235
  closeLbug()
233
236
  .catch(() => { })
234
- .finally(() => process.exit(130));
237
+ .finally(async () => {
238
+ const { flushLoggerSync } = await import('../core/logger.js');
239
+ flushLoggerSync();
240
+ process.exit(130);
241
+ });
235
242
  };
236
243
  process.on('SIGINT', sigintHandler);
237
- // Route console output through bar.log() to prevent progress bar corruption
244
+ // Route console output through bar.log() to prevent progress bar corruption.
245
+ // This is a deliberate UI pattern (not a logging concern): analyze runs a
246
+ // long-lived progress bar on stdout; any concurrent console.* write would
247
+ // overwrite the bar mid-render. We capture originals, swap to barLog for
248
+ // the lifetime of the run, and restore on completion/error/SIGINT.
238
249
  const origLog = console.log.bind(console);
250
+ // eslint-disable-next-line no-console -- intentional console-routing for progress bar UX
239
251
  const origWarn = console.warn.bind(console);
252
+ // eslint-disable-next-line no-console -- intentional console-routing for progress bar UX
240
253
  const origError = console.error.bind(console);
241
254
  let barCurrentValue = 0;
242
255
  const barLog = (...args) => {
@@ -245,7 +258,9 @@ export const analyzeCommand = async (inputPath, options) => {
245
258
  bar.update(barCurrentValue);
246
259
  };
247
260
  console.log = barLog;
261
+ // eslint-disable-next-line no-console -- intentional console-routing for progress bar UX
248
262
  console.warn = barLog;
263
+ // eslint-disable-next-line no-console -- intentional console-routing for progress bar UX
249
264
  console.error = barLog;
250
265
  // Track elapsed time per phase
251
266
  let lastPhaseLabel = 'Initializing...';
@@ -301,7 +316,9 @@ export const analyzeCommand = async (inputPath, options) => {
301
316
  clearInterval(elapsedTimer);
302
317
  process.removeListener('SIGINT', sigintHandler);
303
318
  console.log = origLog;
319
+ // eslint-disable-next-line no-console -- restoring after intentional progress-bar routing
304
320
  console.warn = origWarn;
321
+ // eslint-disable-next-line no-console -- restoring after intentional progress-bar routing
305
322
  console.error = origError;
306
323
  bar.stop();
307
324
  console.log(' Already up to date\n');
@@ -357,7 +374,9 @@ export const analyzeCommand = async (inputPath, options) => {
357
374
  clearInterval(elapsedTimer);
358
375
  process.removeListener('SIGINT', sigintHandler);
359
376
  console.log = origLog;
377
+ // eslint-disable-next-line no-console -- restoring after intentional progress-bar routing
360
378
  console.warn = origWarn;
379
+ // eslint-disable-next-line no-console -- restoring after intentional progress-bar routing
361
380
  console.error = origError;
362
381
  bar.update(100, { phase: 'Done' });
363
382
  bar.stop();
@@ -378,19 +397,20 @@ export const analyzeCommand = async (inputPath, options) => {
378
397
  clearInterval(elapsedTimer);
379
398
  process.removeListener('SIGINT', sigintHandler);
380
399
  console.log = origLog;
400
+ // eslint-disable-next-line no-console -- restoring after intentional progress-bar routing
381
401
  console.warn = origWarn;
402
+ // eslint-disable-next-line no-console -- restoring after intentional progress-bar routing
382
403
  console.error = origError;
383
404
  bar.stop();
384
405
  const msg = err.message || String(err);
385
406
  // Registry name-collision from --name (#829) — surface as an
386
407
  // actionable error rather than a generic stack-trace.
387
408
  if (err instanceof RegistryNameCollisionError) {
388
- console.error(`\n Registry name collision:\n`);
389
- console.error(` "${err.registryName}" is already used by "${err.existingPath}".\n`);
390
- console.error(` Options:`);
391
- console.error(` • Pick a different alias: gitnexus analyze --name <alias>`);
392
- console.error(` • Allow the duplicate: gitnexus analyze --allow-duplicate-name (leaves "-r ${err.registryName}" ambiguous)`);
393
- console.error('');
409
+ cliError(`\n Registry name collision:\n` +
410
+ ` "${err.registryName}" is already used by "${err.existingPath}".\n\n` +
411
+ ` Options:\n` +
412
+ ` • Pick a different alias: gitnexus analyze --name <alias>\n` +
413
+ ` • Allow the duplicate: gitnexus analyze --allow-duplicate-name (leaves "-r ${err.registryName}" ambiguous)\n`, { registryName: err.registryName, existingPath: err.existingPath });
394
414
  process.exitCode = 1;
395
415
  return;
396
416
  }
@@ -423,34 +443,31 @@ export const analyzeCommand = async (inputPath, options) => {
423
443
  msg.includes('allocation failed') ||
424
444
  msg.includes('heap out of memory') ||
425
445
  msg.includes('JavaScript heap')) {
426
- console.error(' This error typically occurs on very large repositories.');
427
- console.error(' Suggestions:');
428
- console.error(' 1. Add large vendored/generated directories to .gitnexusignore');
429
- console.error(' 2. Increase Node.js heap: NODE_OPTIONS="--max-old-space-size=16384"');
430
- console.error(' 3. Increase stack size: NODE_OPTIONS="--stack-size=4096"');
431
- console.error('');
446
+ cliError(` This error typically occurs on very large repositories.\n` +
447
+ ` Suggestions:\n` +
448
+ ` 1. Add large vendored/generated directories to .gitnexusignore\n` +
449
+ ` 2. Increase Node.js heap: NODE_OPTIONS="--max-old-space-size=16384"\n` +
450
+ ` 3. Increase stack size: NODE_OPTIONS="--stack-size=4096"\n`, { recoveryHint: 'large-repo' });
432
451
  }
433
452
  else if (msg.includes('ERESOLVE') || msg.includes('Could not resolve dependency')) {
434
453
  // Note: the original arborist "Cannot destructure property 'package' of
435
454
  // 'node.target'" crash happens inside npm *before* gitnexus code runs,
436
455
  // so it can't be caught here. This branch handles dependency-resolution
437
456
  // errors that surface at runtime (e.g. dynamic require failures).
438
- console.error(' This looks like an npm dependency resolution issue.');
439
- console.error(' Suggestions:');
440
- console.error(' 1. Clear the npm cache: npm cache clean --force');
441
- console.error(' 2. Update npm: npm install -g npm@latest');
442
- console.error(' 3. Reinstall gitnexus: npm install -g gitnexus@latest');
443
- console.error(' 4. Or try npx directly: npx gitnexus@latest analyze');
444
- console.error('');
457
+ cliError(` This looks like an npm dependency resolution issue.\n` +
458
+ ` Suggestions:\n` +
459
+ ` 1. Clear the npm cache: npm cache clean --force\n` +
460
+ ` 2. Update npm: npm install -g npm@latest\n` +
461
+ ` 3. Reinstall gitnexus: npm install -g gitnexus@latest\n` +
462
+ ` 4. Or try npx directly: npx gitnexus@latest analyze\n`, { recoveryHint: 'npm-resolution' });
445
463
  }
446
464
  else if (msg.includes('MODULE_NOT_FOUND') ||
447
465
  msg.includes('Cannot find module') ||
448
466
  msg.includes('ERR_MODULE_NOT_FOUND')) {
449
- console.error(' A required module could not be loaded. The installation may be corrupt.');
450
- console.error(' Suggestions:');
451
- console.error(' 1. Reinstall: npm install -g gitnexus@latest');
452
- console.error(' 2. Clear cache: npm cache clean --force && npx gitnexus@latest analyze');
453
- console.error('');
467
+ cliError(` A required module could not be loaded. The installation may be corrupt.\n` +
468
+ ` Suggestions:\n` +
469
+ ` 1. Reinstall: npm install -g gitnexus@latest\n` +
470
+ ` 2. Clear cache: npm cache clean --force && npx gitnexus@latest analyze\n`, { recoveryHint: 'module-not-found' });
454
471
  }
455
472
  process.exitCode = 1;
456
473
  return;
package/dist/cli/clean.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * Also unregisters it from the global registry.
6
6
  */
7
7
  import fs from 'fs/promises';
8
+ import { logger } from '../core/logger.js';
8
9
  import { findRepo, unregisterRepo, listRegisteredRepos, assertSafeStoragePath, UnsafeStoragePathError, } from '../storage/repo-manager.js';
9
10
  export const cleanCommand = async (options) => {
10
11
  // --all flag: clean all indexed repos
@@ -37,7 +38,7 @@ export const cleanCommand = async (options) => {
37
38
  }
38
39
  catch (err) {
39
40
  if (err instanceof UnsafeStoragePathError) {
40
- console.error(`Refusing to clean ${entry.name}: ${err.message}`);
41
+ logger.error(`Refusing to clean ${entry.name}: ${err.message}`);
41
42
  continue;
42
43
  }
43
44
  throw err;
@@ -48,7 +49,7 @@ export const cleanCommand = async (options) => {
48
49
  console.log(`Deleted: ${entry.name} (${entry.storagePath})`);
49
50
  }
50
51
  catch (err) {
51
- console.error(`Failed to delete ${entry.name}:`, err);
52
+ logger.error({ err }, `Failed to delete ${entry.name}:`);
52
53
  }
53
54
  }
54
55
  return;
@@ -73,6 +74,6 @@ export const cleanCommand = async (options) => {
73
74
  console.log(`Deleted: ${repo.storagePath}`);
74
75
  }
75
76
  catch (err) {
76
- console.error('Failed to delete:', err);
77
+ logger.error({ err }, 'Failed to delete:');
77
78
  }
78
79
  };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * User-facing informational message. Use for banners, listening URLs,
3
+ * and any message the user expects to read in plain text.
4
+ */
5
+ export declare function cliInfo(msg: string, fields?: Record<string, unknown>): void;
6
+ /**
7
+ * User-facing warning. Operator-actionable but non-fatal — `cliWarn`
8
+ * indicates the command can still proceed in some form.
9
+ */
10
+ export declare function cliWarn(msg: string, fields?: Record<string, unknown>): void;
11
+ /**
12
+ * User-facing error. Indicates the command cannot proceed; usually
13
+ * paired with a non-zero exit code at the call site.
14
+ */
15
+ export declare function cliError(msg: string, fields?: Record<string, unknown>): void;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * CLI message helpers — for user-facing banners, error guidance, and
3
+ * recovery hints emitted by `gitnexus` subcommands.
4
+ *
5
+ * These functions write **plain text** directly to `process.stderr` AND
6
+ * tee a structured pino record through the singleton `logger`. Plain text
7
+ * preserves the human-readable contract for users running `gitnexus`
8
+ * interactively, redirecting to a file, or piping to `cat`/`grep`. The
9
+ * structured tee keeps log aggregators happy.
10
+ *
11
+ * **Use these for:**
12
+ * - User-facing banners ("Server listening on http://...:N")
13
+ * - Validation errors ("--worker-timeout must be at least 1 second")
14
+ * - Recovery hints ("Suggestions: 1. Clear the npm cache, 2. ...")
15
+ * - One-line user notices ("No indexed repositories found.")
16
+ *
17
+ * **Do NOT use these for:**
18
+ * - Internal diagnostics (worker progress, retry counts, telemetry)
19
+ * — use `logger.info`/`warn`/`error` directly. Internal logs only
20
+ * need structured fields, not double-output to stderr.
21
+ * - High-volume hot paths — every `cliMessage` call writes twice (raw
22
+ * + structured). Acceptable for user-facing messages, wasteful for
23
+ * ingestion pipeline events.
24
+ *
25
+ * Design note: stderr is the right channel even for non-error messages
26
+ * because GitNexus CLI tools (`query`, `cypher`, `impact`) emit JSON
27
+ * data on stdout for piping (`gitnexus query | jq`). User banners on
28
+ * stdout would corrupt that pipeline.
29
+ */
30
+ import { logger } from '../core/logger.js';
31
+ function writeStderr(msg) {
32
+ // Direct write — bypassing `console.*` so it cannot be intercepted by
33
+ // progress-bar redirection (see `cli/analyze.ts:barLog`) or other
34
+ // routing. The structured tee below still goes through the logger so
35
+ // log aggregation works either way.
36
+ process.stderr.write(msg.endsWith('\n') ? msg : msg + '\n');
37
+ }
38
+ /**
39
+ * User-facing informational message. Use for banners, listening URLs,
40
+ * and any message the user expects to read in plain text.
41
+ */
42
+ export function cliInfo(msg, fields) {
43
+ writeStderr(msg);
44
+ logger.info(fields ?? {}, msg);
45
+ }
46
+ /**
47
+ * User-facing warning. Operator-actionable but non-fatal — `cliWarn`
48
+ * indicates the command can still proceed in some form.
49
+ */
50
+ export function cliWarn(msg, fields) {
51
+ writeStderr(msg);
52
+ logger.warn(fields ?? {}, msg);
53
+ }
54
+ /**
55
+ * User-facing error. Indicates the command cannot proceed; usually
56
+ * paired with a non-zero exit code at the call site.
57
+ */
58
+ export function cliError(msg, fields) {
59
+ writeStderr(msg);
60
+ logger.error(fields ?? {}, msg);
61
+ }
@@ -26,6 +26,8 @@
26
26
  import http from 'http';
27
27
  import { writeSync } from 'node:fs';
28
28
  import { LocalBackend } from '../mcp/local/local-backend.js';
29
+ import { logger } from '../core/logger.js';
30
+ import { cliInfo, cliWarn } from './cli-message.js';
29
31
  // ─── Text Formatters ──────────────────────────────────────────────────
30
32
  // Convert structured JSON results into compact, LLM-friendly text.
31
33
  // Design: minimize tokens, maximize actionability.
@@ -274,11 +276,16 @@ export async function evalServerCommand(options) {
274
276
  const backend = new LocalBackend();
275
277
  const ok = await backend.init();
276
278
  if (!ok) {
277
- console.error('GitNexus eval-server: No indexed repositories found. Run: gitnexus analyze');
279
+ // Operator-actionable but the server cannot start; warn-level so log
280
+ // aggregators don't trip error alerts on a configuration miss. Use
281
+ // cliWarn so the diagnostic reaches stderr synchronously before
282
+ // process.exit() — direct logger.warn would be lost to the buffered
283
+ // pino destination on hard exit (skips beforeExit flush).
284
+ cliWarn('GitNexus eval-server: No indexed repositories found. Run: gitnexus analyze');
278
285
  process.exit(1);
279
286
  }
280
287
  const repos = await backend.listRepos();
281
- console.error(`GitNexus eval-server: ${repos.length} repo(s) loaded: ${repos.map((r) => r.name).join(', ')}`);
288
+ logger.info({ repoCount: repos.length, repos: repos.map((r) => r.name) }, 'GitNexus eval-server: repos loaded');
282
289
  let idleTimer = null;
283
290
  function resetIdleTimer() {
284
291
  if (idleTimeoutSec <= 0)
@@ -286,7 +293,7 @@ export async function evalServerCommand(options) {
286
293
  if (idleTimer)
287
294
  clearTimeout(idleTimer);
288
295
  idleTimer = setTimeout(async () => {
289
- console.error('GitNexus eval-server: Idle timeout reached, shutting down');
296
+ logger.info({ idleTimeoutSec }, 'GitNexus eval-server: idle timeout reached, shutting down');
290
297
  await backend.disconnect();
291
298
  process.exit(0);
292
299
  }, idleTimeoutSec * 1000);
@@ -351,16 +358,34 @@ export async function evalServerCommand(options) {
351
358
  }
352
359
  });
353
360
  server.listen(port, '127.0.0.1', () => {
354
- console.error(`GitNexus eval-server: listening on http://127.0.0.1:${port}`);
355
- console.error(` POST /tool/query — search execution flows`);
356
- console.error(` POST /tool/context — 360-degree symbol view`);
357
- console.error(` POST /tool/impact — blast radius analysis`);
358
- console.error(` POST /tool/cypher — raw Cypher query`);
359
- console.error(` GET /health health check`);
360
- console.error(` POST /shutdown graceful shutdown`);
361
+ // Plain-text banner for the human watching stderr; structured record
362
+ // for log aggregation (split into two so the user sees a real banner
363
+ // not `{"level":30,"msg":"...","port":4747,"endpoints":[...]}`).
364
+ const bannerLines = [
365
+ `GitNexus eval-server: listening on http://127.0.0.1:${port}`,
366
+ ` POST /tool/query search execution flows`,
367
+ ` POST /tool/context 360-degree symbol view`,
368
+ ` POST /tool/impact — blast radius analysis`,
369
+ ` POST /tool/cypher — raw Cypher query`,
370
+ ` GET /health — health check`,
371
+ ` POST /shutdown — graceful shutdown`,
372
+ ];
361
373
  if (idleTimeoutSec > 0) {
362
- console.error(` Auto-shutdown after ${idleTimeoutSec}s idle`);
374
+ bannerLines.push(` Auto-shutdown after ${idleTimeoutSec}s idle`);
363
375
  }
376
+ cliInfo(bannerLines.join('\n'), {
377
+ port,
378
+ host: '127.0.0.1',
379
+ idleTimeoutSec: idleTimeoutSec > 0 ? idleTimeoutSec : undefined,
380
+ endpoints: [
381
+ 'POST /tool/query',
382
+ 'POST /tool/context',
383
+ 'POST /tool/impact',
384
+ 'POST /tool/cypher',
385
+ 'GET /health',
386
+ 'POST /shutdown',
387
+ ],
388
+ });
364
389
  try {
365
390
  // Use fd 1 directly — LadybugDB captures process.stdout (#324)
366
391
  writeSync(1, `GITNEXUS_EVAL_SERVER_READY:${port}\n`);
@@ -371,7 +396,7 @@ export async function evalServerCommand(options) {
371
396
  });
372
397
  resetIdleTimer();
373
398
  const shutdown = async () => {
374
- console.error('GitNexus eval-server: shutting down...');
399
+ logger.info('GitNexus eval-server: shutting down...');
375
400
  await backend.disconnect();
376
401
  server.close();
377
402
  process.exit(0);
package/dist/cli/group.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // gitnexus/src/cli/group.ts
2
2
  import { createRequire } from 'node:module';
3
+ import { logger } from '../core/logger.js';
3
4
  const _require = createRequire(import.meta.url);
4
5
  const yaml = _require('js-yaml');
5
6
  export function registerGroupCommands(program) {
@@ -42,7 +43,7 @@ export function registerGroupCommands(program) {
42
43
  const groupDir = getGroupDir(getDefaultGitnexusDir(), groupName);
43
44
  const config = await loadGroupConfig(groupDir);
44
45
  if (!(repoPath in config.repos)) {
45
- console.error(`Repo path "${repoPath}" not found in group "${groupName}"`);
46
+ logger.error(`Repo path "${repoPath}" not found in group "${groupName}"`);
46
47
  process.exitCode = 1;
47
48
  return;
48
49
  }
@@ -202,7 +203,7 @@ export function registerGroupCommands(program) {
202
203
  payload.includeTests = true;
203
204
  const raw = await backend.getGroupService().groupImpact(payload);
204
205
  if (raw && typeof raw === 'object' && 'error' in raw) {
205
- console.error(String(raw.error));
206
+ logger.error(String(raw.error));
206
207
  process.exitCode = 1;
207
208
  return;
208
209
  }
@@ -280,7 +281,7 @@ export function registerGroupCommands(program) {
280
281
  unmatchedOnly: Boolean(opts.unmatched),
281
282
  });
282
283
  if (raw && typeof raw === 'object' && 'error' in raw) {
283
- console.error(String(raw.error));
284
+ logger.error(String(raw.error));
284
285
  process.exitCode = 1;
285
286
  return;
286
287
  }
package/dist/cli/mcp.js CHANGED
@@ -37,11 +37,17 @@ export const mcpCommand = async () => {
37
37
  // startMCPServer (gitnexus/src/mcp/server.ts) so the server's shutdown
38
38
  // path runs cleanly with full stack traces. Registering duplicates here
39
39
  // would only produce noisy double-logging on the same exception.
40
- // Now safe to dynamically import the heavy backend modules. Anything
41
- // they emit to stdout during evaluation will route through the sentinel.
42
- const [{ startMCPServer }, { LocalBackend }] = await Promise.all([
40
+ // Dynamically import heavy backend modules AND the pino logger AFTER
41
+ // the sentinel installs. The logger is dynamic-imported (rather than
42
+ // static) to preserve the leaf-only static-import closure documented at
43
+ // the top of this file — `core/logger.js` itself doesn't write to
44
+ // stdout at module init, but transitive deps (pino, pino-pretty, the
45
+ // worker-thread transport) could in theory, and the import-closure
46
+ // regression test enforces the leaf invariant.
47
+ const [{ startMCPServer }, { LocalBackend }, { logger }] = await Promise.all([
43
48
  import('../mcp/server.js'),
44
49
  import('../mcp/local/local-backend.js'),
50
+ import('../core/logger.js'),
45
51
  ]);
46
52
  // Missing-optional-grammar warnings are intentionally NOT emitted here.
47
53
  // `gitnexus analyze` already warns at index time, filtered by the repo's
@@ -55,10 +61,12 @@ export const mcpCommand = async () => {
55
61
  await backend.init();
56
62
  const repos = await backend.listRepos();
57
63
  if (repos.length === 0) {
58
- console.error('GitNexus: No indexed repos yet. Run `gitnexus analyze` in a git repo — the server will pick it up automatically.');
64
+ // Operator-actionable but the server still starts and serves; warn-level,
65
+ // not error. Tools will discover newly-analyzed repos via lazy refresh.
66
+ logger.warn('GitNexus: No indexed repos yet. Run `gitnexus analyze` in a git repo — the server will pick it up automatically.');
59
67
  }
60
68
  else {
61
- console.error(`GitNexus: MCP server starting with ${repos.length} repo(s): ${repos.map((r) => r.name).join(', ')}`);
69
+ logger.info({ repoCount: repos.length, repos: repos.map((r) => r.name) }, 'GitNexus: MCP server starting');
62
70
  }
63
71
  // Start MCP server (serves all repos, discovers new ones lazily)
64
72
  await startMCPServer(backend);
@@ -12,6 +12,7 @@
12
12
  * is unavailable instead of silently getting a degraded index.
13
13
  */
14
14
  import { createRequire } from 'module';
15
+ import { cliWarn } from './cli-message.js';
15
16
  const _require = createRequire(import.meta.url);
16
17
  const OPTIONAL_GRAMMARS = [
17
18
  { name: 'tree-sitter-dart', pkg: 'tree-sitter-dart', extensions: ['.dart'] },
@@ -47,8 +48,12 @@ export function detectMissingOptionalGrammars() {
47
48
  /could not find|no native build|prebuilds/i.test(msg);
48
49
  if (!looksMissing) {
49
50
  // Present but broken — surface so the user doesn't get a misleading
50
- // "reinstall" recovery message that wouldn't actually help.
51
- console.error(`GitNexus: optional grammar "${g.name}" is installed but failed to load (${msg.slice(0, 200)}). ${g.extensions.join('/')} files will not be parsed.`);
51
+ // "reinstall" recovery message that wouldn't actually help. cliWarn
52
+ // writes plain text to stderr AND tees a structured logger.warn
53
+ // record; the merged repo-wide ESLint pino-migration rule forbids
54
+ // direct `console.error` in CLI code (only `console.log` is allowed
55
+ // there for tool-data stdout output).
56
+ cliWarn(`GitNexus: optional grammar "${g.name}" is installed but failed to load (${msg.slice(0, 200)}). ${g.extensions.join('/')} files will not be parsed.`, { grammar: g.name, extensions: g.extensions, error: msg });
52
57
  }
53
58
  missing.push({ name: g.name, extensions: g.extensions });
54
59
  }
@@ -69,10 +74,14 @@ export function warnMissingOptionalGrammars(opts) {
69
74
  if (missing.length === 0)
70
75
  return;
71
76
  const ctx = opts?.context ? ` [${opts.context}]` : '';
77
+ // Hoist the optional set into a local so the closure below can narrow
78
+ // its type; references to `opts?.relevantExtensions` inside `.some()`
79
+ // lose the outer null-check narrowing and require a non-null assertion.
80
+ const relevantExtensions = opts?.relevantExtensions;
72
81
  for (const g of missing) {
73
- if (opts?.relevantExtensions && !g.extensions.some((e) => opts.relevantExtensions.has(e))) {
82
+ if (relevantExtensions && !g.extensions.some((e) => relevantExtensions.has(e))) {
74
83
  continue;
75
84
  }
76
- console.error(`GitNexus${ctx}: optional grammar "${g.name}" is unavailable — ${g.extensions.join('/')} files will not be parsed. Reinstall without GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (and ensure python3, make, g++) to enable.`);
85
+ cliWarn(`GitNexus${ctx}: optional grammar "${g.name}" is unavailable — ${g.extensions.join('/')} files will not be parsed. Reinstall without GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (and ensure python3, make, g++) to enable.`, { grammar: g.name, extensions: g.extensions, context: opts?.context });
77
86
  }
78
87
  }
@@ -26,6 +26,8 @@
26
26
  * here there is no pipeline, so no conflation.)
27
27
  */
28
28
  import fs from 'fs/promises';
29
+ import { logger } from '../core/logger.js';
30
+ import { cliError } from './cli-message.js';
29
31
  import { readRegistry, resolveRegistryEntry, assertSafeStoragePath, unregisterRepo, RegistryNotFoundError, RegistryAmbiguousTargetError, UnsafeStoragePathError, } from '../storage/repo-manager.js';
30
32
  export const removeCommand = async (target, options) => {
31
33
  // Read the registry snapshot once and pass it to the resolver — this
@@ -41,14 +43,14 @@ export const removeCommand = async (target, options) => {
41
43
  // Idempotent: missing target is a no-op warning, not an error.
42
44
  // The `availableNames` hint comes from the error itself so users
43
45
  // can see what they might have meant.
44
- console.warn(`Nothing to remove: ${err.message}`);
46
+ logger.warn(`Nothing to remove: ${err.message}`);
45
47
  return;
46
48
  }
47
49
  if (err instanceof RegistryAmbiguousTargetError) {
48
50
  // Duplicate aliases are allowed via --allow-duplicate-name (#829);
49
51
  // refuse to guess which one the user meant — surface the full list
50
52
  // and exit non-zero so scripts don't silently pick the wrong repo.
51
- console.error(`Error: ${err.message}`);
53
+ cliError(`Error: ${err.message}`);
52
54
  process.exit(1);
53
55
  }
54
56
  throw err;
@@ -75,7 +77,7 @@ export const removeCommand = async (target, options) => {
75
77
  }
76
78
  catch (err) {
77
79
  if (err instanceof UnsafeStoragePathError) {
78
- console.error(`Error: ${err.message}`);
80
+ cliError(`Error: ${err.message}`);
79
81
  process.exit(1);
80
82
  }
81
83
  throw err;
@@ -93,7 +95,8 @@ export const removeCommand = async (target, options) => {
93
95
  console.log(` Storage: ${entry.storagePath}`);
94
96
  }
95
97
  catch (err) {
96
- console.error(`Failed to remove ${entry.name}:`, err);
98
+ const msg = err instanceof Error ? err.message : String(err);
99
+ cliError(`Failed to remove ${entry.name}: ${msg}`, { err });
97
100
  process.exit(1);
98
101
  }
99
102
  };
package/dist/cli/serve.js CHANGED
@@ -1,15 +1,25 @@
1
1
  import { createServer } from '../server/api.js';
2
- // Catch anything that would cause a silent exit
2
+ import { logger, flushLoggerSync } from '../core/logger.js';
3
+ import { cliError } from './cli-message.js';
4
+ // Catch anything that would cause a silent exit. Pino v10's default
5
+ // destination is `sync: false` (SonicBoom buffered) — call
6
+ // `flushLoggerSync()` between the log and `process.exit(1)` so the crash
7
+ // record is not lost to the unflushed buffer. Worker-thread transports
8
+ // (pino-pretty under TTY) handle their own flush on process exit in v10,
9
+ // so no separate `pino.final` integration is needed (the API was removed
10
+ // in v10 because the transport architecture made it unnecessary).
11
+ //
12
+ // We pass the Error itself in `{ err }` so pino's built-in err serializer
13
+ // captures `type`, `message`, and `stack` as structured fields.
3
14
  process.on('uncaughtException', (err) => {
4
- console.error('\n[gitnexus serve] Uncaught exception:', err.message);
5
- if (process.env.DEBUG)
6
- console.error(err.stack);
15
+ logger.error({ err }, '[gitnexus serve] Uncaught exception');
16
+ flushLoggerSync();
7
17
  process.exit(1);
8
18
  });
9
19
  process.on('unhandledRejection', (reason) => {
10
- console.error('\n[gitnexus serve] Unhandled rejection:', reason?.message || reason);
11
- if (process.env.DEBUG)
12
- console.error(reason?.stack);
20
+ const err = reason instanceof Error ? reason : new Error(String(reason));
21
+ logger.error({ err }, '[gitnexus serve] Unhandled rejection');
22
+ flushLoggerSync();
13
23
  process.exit(1);
14
24
  });
15
25
  export const serveCommand = async (options) => {
@@ -22,16 +32,24 @@ export const serveCommand = async (options) => {
22
32
  await createServer(port, host);
23
33
  }
24
34
  catch (err) {
25
- console.error(`\nFailed to start GitNexus server:\n`);
26
- console.error(` ${err.message || err}\n`);
27
35
  if (err.code === 'EADDRINUSE') {
28
- console.error(` Port ${port} is already in use. Either:`);
29
- console.error(` 1. Stop the other process using port ${port}`);
30
- console.error(` 2. Use a different port: gitnexus serve --port 4748\n`);
36
+ cliError(`\nFailed to start GitNexus server:\n` +
37
+ ` ${err.message || err}\n\n` +
38
+ ` Port ${port} is already in use. Either:\n` +
39
+ ` 1. Stop the other process using port ${port}\n` +
40
+ ` 2. Use a different port: gitnexus serve --port 4748\n`, { code: err.code, port, host });
41
+ }
42
+ else {
43
+ cliError(`\nFailed to start GitNexus server:\n ${err.message || err}\n`, {
44
+ code: err.code,
45
+ port,
46
+ host,
47
+ });
31
48
  }
32
49
  if (err.stack && process.env.DEBUG) {
33
- console.error(err.stack);
50
+ logger.debug({ stack: err.stack }, 'serve start error stack');
34
51
  }
52
+ flushLoggerSync();
35
53
  process.exit(1);
36
54
  }
37
55
  };