gitnexus 1.6.4-rc.30 → 1.6.4-rc.32

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.
@@ -12,11 +12,47 @@ import { execFileSync } from 'child_process';
12
12
  import v8 from 'v8';
13
13
  import cliProgress from 'cli-progress';
14
14
  import { closeLbug } from '../core/lbug/lbug-adapter.js';
15
- import { getStoragePaths, getGlobalRegistryPath, RegistryNameCollisionError, } from '../storage/repo-manager.js';
15
+ import { getStoragePaths, getGlobalRegistryPath, RegistryNameCollisionError, AnalysisNotFinalizedError, assertAnalysisFinalized, } from '../storage/repo-manager.js';
16
16
  import { getGitRoot, hasGitDir } from '../storage/git.js';
17
17
  import { runFullAnalysis } from '../core/run-analyze.js';
18
18
  import { getMaxFileSizeBannerMessage } from '../core/ingestion/utils/max-file-size.js';
19
19
  import fs from 'fs/promises';
20
+ // Capture stderr.write at module load BEFORE anything (LadybugDB native
21
+ // init, progress bar, console redirection) can monkey-patch it. The
22
+ // fatal handlers below MUST reach the user even when the analyze path
23
+ // has redirected console.* through the progress bar's bar.log() — the
24
+ // previous behaviour silently swallowed stack traces and made #1169
25
+ // indistinguishable from a no-op success on Windows.
26
+ const realStderrWrite = process.stderr.write.bind(process.stderr);
27
+ const writeFatalToStderr = (label, err) => {
28
+ const isErr = err instanceof Error;
29
+ const message = isErr ? err.message : String(err);
30
+ realStderrWrite(`\n ${label}: ${message}\n`);
31
+ if (isErr && err.stack)
32
+ realStderrWrite(`${err.stack}\n`);
33
+ };
34
+ let fatalHandlersInstalled = false;
35
+ /**
36
+ * Install one-shot `unhandledRejection` / `uncaughtException` handlers
37
+ * that surface the failure to the real stderr (bypassing any console
38
+ * redirection installed by the progress bar) and force a non-zero exit
39
+ * code. Without these, an async error escaping {@link analyzeCommand}'s
40
+ * try/catch was reported as exit 0 with no diagnostic — the silent
41
+ * failure mode tracked in #1169.
42
+ */
43
+ const installFatalHandlers = () => {
44
+ if (fatalHandlersInstalled)
45
+ return;
46
+ fatalHandlersInstalled = true;
47
+ process.on('unhandledRejection', (err) => {
48
+ writeFatalToStderr('Analysis failed (unhandled rejection)', err);
49
+ process.exit(1);
50
+ });
51
+ process.on('uncaughtException', (err) => {
52
+ writeFatalToStderr('Analysis failed (uncaught exception)', err);
53
+ process.exit(1);
54
+ });
55
+ };
20
56
  const HEAP_MB = 8192;
21
57
  const HEAP_FLAG = `--max-old-space-size=${HEAP_MB}`;
22
58
  /** Increase default stack size (KB) to prevent stack overflow on deep class hierarchies. */
@@ -49,6 +85,10 @@ function ensureHeap() {
49
85
  export const analyzeCommand = async (inputPath, options) => {
50
86
  if (ensureHeap())
51
87
  return;
88
+ // Install fatal handlers immediately after re-exec resolution so any
89
+ // async error that escapes the try/catch below (#1169) surfaces with
90
+ // a stack trace and a non-zero exit code instead of a silent exit 0.
91
+ installFatalHandlers();
52
92
  if (options?.verbose) {
53
93
  process.env.GITNEXUS_VERBOSE = '1';
54
94
  }
@@ -211,6 +251,11 @@ export const analyzeCommand = async (inputPath, options) => {
211
251
  onLog: barLog,
212
252
  });
213
253
  if (result.alreadyUpToDate) {
254
+ // Even the fast path must prove the repo is discoverable. A prior
255
+ // run can write meta.json and then fail before registerRepo(); in
256
+ // that half-finalized state, runFullAnalysis returns alreadyUpToDate
257
+ // on the next invocation unless we check the registry here too.
258
+ await assertAnalysisFinalized(repoPath);
214
259
  clearInterval(elapsedTimer);
215
260
  process.removeListener('SIGINT', sigintHandler);
216
261
  console.log = origLog;
@@ -222,6 +267,14 @@ export const analyzeCommand = async (inputPath, options) => {
222
267
  // runFullAnalysis never opens LadybugDB, so no native handles prevent exit.
223
268
  return;
224
269
  }
270
+ // Post-finalize invariant (#1169): runFullAnalysis nominally writes
271
+ // meta.json and registers the repo, but on Windows it has been
272
+ // observed to return successfully with neither artifact present
273
+ // (banner-only output, exit 0). Verify both before declaring
274
+ // success so the silent-finalize state surfaces with a non-zero
275
+ // exit code and an actionable error instead of being mistaken for
276
+ // a healthy index.
277
+ await assertAnalysisFinalized(repoPath);
225
278
  // Skill generation (CLI-only, uses pipeline result from analysis)
226
279
  if (options?.skills && result.pipelineResult) {
227
280
  updateBar(99, 'Generating skill files...');
@@ -299,7 +352,26 @@ export const analyzeCommand = async (inputPath, options) => {
299
352
  process.exitCode = 1;
300
353
  return;
301
354
  }
302
- console.error(`\n Analysis failed: ${msg}\n`);
355
+ // Finalize invariant failure (#1169) keep the rich actionable
356
+ // message intact and write through realStderrWrite so it can't be
357
+ // erased by a leftover bar refresh on slow terminals.
358
+ if (err instanceof AnalysisNotFinalizedError) {
359
+ writeFatalToStderr('Analysis did not finalize', err);
360
+ realStderrWrite(`\n Diagnostic checklist:\n` +
361
+ ` 1. Re-run "gitnexus analyze" - transient native errors often clear on retry.\n` +
362
+ ` 2. Inspect ${err.storagePath} - a leftover lbug.wal indicates an aborted write.\n` +
363
+ ` 3. If the failure persists, run with NODE_OPTIONS="--max-old-space-size=8192 --trace-exit"\n` +
364
+ ` and attach the trace to the GitNexus issue tracker.\n\n`);
365
+ process.exitCode = 1;
366
+ return;
367
+ }
368
+ // Bypass the redirected console.error and write the full stack to
369
+ // the real stderr captured at module load. The redirected
370
+ // console.error wraps every line with `\\x1b[2K\\r` (ANSI clear-line)
371
+ // and forces a bar.update() afterwards, which on some Windows
372
+ // terminals visually erases the failure message — the canonical
373
+ // shape of the silent-exit symptom in #1169.
374
+ writeFatalToStderr('Analysis failed', err);
303
375
  // Provide helpful guidance for known failure modes
304
376
  if (msg.includes('Maximum call stack size exceeded') ||
305
377
  msg.includes('call stack') ||
@@ -254,6 +254,44 @@ export declare class RegistryAmbiguousTargetError extends Error {
254
254
  readonly kind: "RegistryAmbiguousTargetError";
255
255
  constructor(target: string, matches: RegistryEntry[]);
256
256
  }
257
+ /**
258
+ * Thrown by {@link assertAnalysisFinalized} when a successful `analyze`
259
+ * run did not actually persist `meta.json` or did not register the repo
260
+ * in `~/.gitnexus/registry.json` (#1169).
261
+ *
262
+ * Why this exists: on Windows, `gitnexus analyze` has been observed to
263
+ * exit cleanly (code 0) with `lbug.wal` written but no `meta.json`,
264
+ * leaving the repo invisible to `gitnexus list`/`status` and downstream
265
+ * MCP discovery. The only signal to the user was an empty banner —
266
+ * which is indistinguishable from a no-op early return. This invariant
267
+ * fails loudly with an actionable diagnostic so the silent-finalize bug
268
+ * surfaces with a non-zero exit code and a recoverable error message
269
+ * regardless of the upstream root cause (re-exec churn, native module
270
+ * side effects, antivirus, or future regressions).
271
+ */
272
+ export declare class AnalysisNotFinalizedError extends Error {
273
+ readonly repoPath: string;
274
+ readonly storagePath: string;
275
+ readonly missing: 'meta' | 'registry-entry';
276
+ readonly registryPath: string;
277
+ readonly kind: "AnalysisNotFinalizedError";
278
+ constructor(repoPath: string, storagePath: string, missing: 'meta' | 'registry-entry', registryPath: string);
279
+ }
280
+ /**
281
+ * Verify that a successful `analyze` call actually produced an indexed,
282
+ * registered repo on disk. Two checks, both strictly required:
283
+ *
284
+ * 1. `meta.json` must exist at `<repoPath>/.gitnexus/meta.json`.
285
+ * 2. The global registry (`getGlobalRegistryPath()`) must contain an
286
+ * entry whose canonical path matches `repoPath`.
287
+ *
288
+ * Throws {@link AnalysisNotFinalizedError} on the first failure with the
289
+ * specific missing artifact. Pure read — does not mutate disk state.
290
+ *
291
+ * Callers must skip this assertion on the `alreadyUpToDate` early-return
292
+ * path, where the rebuild was deliberately not run.
293
+ */
294
+ export declare const assertAnalysisFinalized: (repoPath: string) => Promise<void>;
257
295
  /**
258
296
  * Thrown by {@link assertSafeStoragePath} when a registry entry's
259
297
  * `storagePath` does NOT point at the expected `<entry.path>/.gitnexus`
@@ -454,6 +454,76 @@ export class RegistryAmbiguousTargetError extends Error {
454
454
  this.name = 'RegistryAmbiguousTargetError';
455
455
  }
456
456
  }
457
+ /**
458
+ * Thrown by {@link assertAnalysisFinalized} when a successful `analyze`
459
+ * run did not actually persist `meta.json` or did not register the repo
460
+ * in `~/.gitnexus/registry.json` (#1169).
461
+ *
462
+ * Why this exists: on Windows, `gitnexus analyze` has been observed to
463
+ * exit cleanly (code 0) with `lbug.wal` written but no `meta.json`,
464
+ * leaving the repo invisible to `gitnexus list`/`status` and downstream
465
+ * MCP discovery. The only signal to the user was an empty banner —
466
+ * which is indistinguishable from a no-op early return. This invariant
467
+ * fails loudly with an actionable diagnostic so the silent-finalize bug
468
+ * surfaces with a non-zero exit code and a recoverable error message
469
+ * regardless of the upstream root cause (re-exec churn, native module
470
+ * side effects, antivirus, or future regressions).
471
+ */
472
+ export class AnalysisNotFinalizedError extends Error {
473
+ repoPath;
474
+ storagePath;
475
+ missing;
476
+ registryPath;
477
+ kind = 'AnalysisNotFinalizedError';
478
+ constructor(repoPath, storagePath, missing, registryPath) {
479
+ const detail = missing === 'meta'
480
+ ? `meta.json was not written to ${path.join(storagePath, 'meta.json')}`
481
+ : `registry entry for ${repoPath} was not added to ${registryPath}`;
482
+ super(`Analysis did not finalize for ${repoPath}: ${detail}. ` +
483
+ `The on-disk index is incomplete and was not registered. ` +
484
+ `Re-run "gitnexus analyze" — if the problem persists, inspect ` +
485
+ `${storagePath} for a stale lbug.wal that signals an aborted write.`);
486
+ this.repoPath = repoPath;
487
+ this.storagePath = storagePath;
488
+ this.missing = missing;
489
+ this.registryPath = registryPath;
490
+ this.name = 'AnalysisNotFinalizedError';
491
+ }
492
+ }
493
+ /**
494
+ * Verify that a successful `analyze` call actually produced an indexed,
495
+ * registered repo on disk. Two checks, both strictly required:
496
+ *
497
+ * 1. `meta.json` must exist at `<repoPath>/.gitnexus/meta.json`.
498
+ * 2. The global registry (`getGlobalRegistryPath()`) must contain an
499
+ * entry whose canonical path matches `repoPath`.
500
+ *
501
+ * Throws {@link AnalysisNotFinalizedError} on the first failure with the
502
+ * specific missing artifact. Pure read — does not mutate disk state.
503
+ *
504
+ * Callers must skip this assertion on the `alreadyUpToDate` early-return
505
+ * path, where the rebuild was deliberately not run.
506
+ */
507
+ export const assertAnalysisFinalized = async (repoPath) => {
508
+ const resolved = path.resolve(repoPath);
509
+ const { storagePath, metaPath } = getStoragePaths(resolved);
510
+ try {
511
+ await fs.access(metaPath);
512
+ }
513
+ catch {
514
+ throw new AnalysisNotFinalizedError(resolved, storagePath, 'meta', getGlobalRegistryPath());
515
+ }
516
+ const entries = await readRegistry();
517
+ const canonicalInput = canonicalizePath(resolved);
518
+ const isWin = process.platform === 'win32';
519
+ const found = entries.some((e) => {
520
+ const a = canonicalizePath(e.path);
521
+ return isWin ? a.toLowerCase() === canonicalInput.toLowerCase() : a === canonicalInput;
522
+ });
523
+ if (!found) {
524
+ throw new AnalysisNotFinalizedError(resolved, storagePath, 'registry-entry', getGlobalRegistryPath());
525
+ }
526
+ };
457
527
  /**
458
528
  * Thrown by {@link assertSafeStoragePath} when a registry entry's
459
529
  * `storagePath` does NOT point at the expected `<entry.path>/.gitnexus`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.4-rc.30",
3
+ "version": "1.6.4-rc.32",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",