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.
package/dist/cli/analyze.js
CHANGED
|
@@ -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
|
-
|
|
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