gitnexus 1.6.6-rc.4 → 1.6.6-rc.6

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.
@@ -58,12 +58,55 @@ const installFatalHandlers = () => {
58
58
  process.exit(1);
59
59
  });
60
60
  };
61
- const HEAP_MB = 8192;
62
- const HEAP_FLAG = `--max-old-space-size=${HEAP_MB}`;
61
+ const HEAP_MB = 16384;
62
+ const TEST_RESPAWN_HEAP_MB = Number(process.env.GITNEXUS_TEST_RESPAWN_HEAP_MB);
63
+ const RESPAWN_HEAP_MB = Number.isFinite(TEST_RESPAWN_HEAP_MB) && TEST_RESPAWN_HEAP_MB > 0
64
+ ? Math.floor(TEST_RESPAWN_HEAP_MB)
65
+ : HEAP_MB;
66
+ const HEAP_FLAG = `--max-old-space-size=${RESPAWN_HEAP_MB}`;
63
67
  /** Increase default stack size (KB) to prevent stack overflow on deep class hierarchies. */
64
68
  const STACK_KB = 4096;
65
69
  const STACK_FLAG = `--stack-size=${STACK_KB}`;
66
- /** Re-exec the process with an 8GB heap and larger stack if we're currently below that. */
70
+ /**
71
+ * Heuristic for "child re-exec likely died from V8 OOM".
72
+ *
73
+ * Platform-independent detection is best-effort: V8/Node usually emit
74
+ * stable heap-exhaustion phrases in stderr/message across Linux/macOS/Windows
75
+ * (for example "JavaScript heap out of memory" or "Reached heap limit"),
76
+ * while some environments only expose status/signal (e.g. 134/SIGABRT).
77
+ * We combine both text signatures and process-exit signatures.
78
+ */
79
+ const childProcessLikelyOom = (err) => {
80
+ if (!err || typeof err !== 'object')
81
+ return false;
82
+ const e = err;
83
+ const hasHeapOomSignature = (v) => {
84
+ const text = (Buffer.isBuffer(v) ? v.toString('utf8') : typeof v === 'string' ? v : '').toLowerCase();
85
+ if (!text)
86
+ return false;
87
+ return (text.includes('javascript heap out of memory') ||
88
+ text.includes('reached heap limit') ||
89
+ text.includes('allocation failed - javascript heap out of memory') ||
90
+ text.includes('fatalprocessoutofmemory'));
91
+ };
92
+ const fields = [e.message, e.stderr, e.stdout];
93
+ if (fields.some((v) => hasHeapOomSignature(v)))
94
+ return true;
95
+ const hasAnyChildOutput = [e.stderr, e.stdout].some((v) => (Buffer.isBuffer(v) && v.length > 0) || (typeof v === 'string' && v.length > 0));
96
+ if (hasAnyChildOutput)
97
+ return false;
98
+ return e.status === 134 || e.signal === 'SIGABRT';
99
+ };
100
+ const forceHeapOOMForTestIfEnabled = () => {
101
+ if (process.env.GITNEXUS_TEST_FORCE_HEAP_OOM !== '1')
102
+ return;
103
+ // Allocate JS strings (not Buffers) so pressure lands on V8 heap itself.
104
+ // Buffers can allocate off-heap, which makes OOM triggering less reliable.
105
+ const chunks = [];
106
+ for (;;)
107
+ chunks.push('x'.repeat(1024 * 1024));
108
+ };
109
+ /** Re-exec the process with a 16GB heap and larger stack if we're currently below that. */
67
110
  function ensureHeap() {
68
111
  const nodeOpts = process.env.NODE_OPTIONS || '';
69
112
  if (nodeOpts.includes('--max-old-space-size'))
@@ -83,6 +126,13 @@ function ensureHeap() {
83
126
  });
84
127
  }
85
128
  catch (e) {
129
+ if (childProcessLikelyOom(e)) {
130
+ cliError(` Analysis likely ran out of memory.\n` +
131
+ ` Retry with a larger heap if your machine allows it:\n` +
132
+ ` NODE_OPTIONS="--max-old-space-size=24576" gitnexus analyze [your-args]\n` +
133
+ ` (Windows: set NODE_OPTIONS=--max-old-space-size=24576 && gitnexus analyze [your-args])\n` +
134
+ ` If this persists, it may be a native crash unrelated to heap size.\n`, { recoveryHint: 'heap-oom-respawn' });
135
+ }
86
136
  process.exitCode = e.status ?? 1;
87
137
  }
88
138
  return true;
@@ -104,6 +154,7 @@ export const shouldGenerateCommunitySkillFiles = (options, pipelineResult) => Bo
104
154
  export const analyzeCommand = async (inputPath, options) => {
105
155
  if (ensureHeap())
106
156
  return;
157
+ forceHeapOOMForTestIfEnabled();
107
158
  // Install fatal handlers immediately after re-exec resolution so any
108
159
  // async error that escapes the try/catch below (#1169) surfaces with
109
160
  // a stack trace and a non-zero exit code instead of a silent exit 0.
package/dist/cli/index.js CHANGED
@@ -108,6 +108,7 @@ program
108
108
  .option('--gist', 'Publish wiki as a public GitHub Gist after generation')
109
109
  .option('-v, --verbose', 'Enable verbose output (show LLM commands and responses)')
110
110
  .option('--review', 'Stop after grouping to review module structure before generating pages')
111
+ .option('--lang <lang>', 'Output language for generated documentation (e.g. english, chinese, spanish, japanese)')
111
112
  .action(createLazyAction(() => import('./wiki.js'), 'wikiCommand'));
112
113
  program
113
114
  .command('augment <pattern>')
@@ -19,5 +19,6 @@ export interface WikiCommandOptions {
19
19
  review?: boolean;
20
20
  timeout?: string;
21
21
  retries?: string;
22
+ lang?: string;
22
23
  }
23
24
  export declare const wikiCommand: (inputPath?: string, options?: WikiCommandOptions) => Promise<void>;
package/dist/cli/wiki.js CHANGED
@@ -364,6 +364,7 @@ export const wikiCommand = async (inputPath, options) => {
364
364
  force: options?.force,
365
365
  concurrency: options?.concurrency ? parseInt(options.concurrency, 10) : undefined,
366
366
  reviewOnly: options?.review,
367
+ lang: options?.lang,
367
368
  };
368
369
  const generator = new WikiGenerator(repoPath, storagePath, lbugPath, llmConfig, wikiOptions, (phase, percent, detail) => {
369
370
  const label = detail || phase;
@@ -16,11 +16,14 @@ export interface WikiOptions {
16
16
  concurrency?: number;
17
17
  /** If true, stop after building module tree for user review */
18
18
  reviewOnly?: boolean;
19
+ /** Output language for generated documentation (e.g. 'english', 'chinese', 'spanish') */
20
+ lang?: string;
19
21
  }
20
22
  export interface WikiMeta {
21
23
  fromCommit: string;
22
24
  generatedAt: string;
23
25
  model: string;
26
+ lang: string;
24
27
  moduleFiles: Record<string, string[]>;
25
28
  moduleTree: ModuleTreeNode[];
26
29
  }
@@ -62,6 +65,16 @@ export declare class WikiGenerator {
62
65
  * Also touches the DB connection periodically to prevent idle timeout.
63
66
  */
64
67
  private streamOpts;
68
+ /**
69
+ * Return the effective lang string: strip control characters, trim, cap at 50 chars,
70
+ * then validate against a character allowlist. Returns '' if the value is absent or invalid.
71
+ * Used for both prompt construction and meta storage/comparison so they are always in sync.
72
+ */
73
+ private effectiveLang;
74
+ /**
75
+ * Append an output-language instruction to a system prompt when --lang is set.
76
+ */
77
+ private buildSystemPrompt;
65
78
  /**
66
79
  * Route LLM call to the appropriate provider (OpenAI-compatible or Cursor CLI).
67
80
  */
@@ -89,6 +89,27 @@ export class WikiGenerator {
89
89
  },
90
90
  };
91
91
  }
92
+ /**
93
+ * Return the effective lang string: strip control characters, trim, cap at 50 chars,
94
+ * then validate against a character allowlist. Returns '' if the value is absent or invalid.
95
+ * Used for both prompt construction and meta storage/comparison so they are always in sync.
96
+ */
97
+ effectiveLang() {
98
+ const lang = (this.options.lang ?? '')
99
+ .replace(/[\x00-\x1F\x7F]/g, '')
100
+ .trim()
101
+ .slice(0, 50);
102
+ return /^[a-zA-Z -]+$/.test(lang) ? lang : '';
103
+ }
104
+ /**
105
+ * Append an output-language instruction to a system prompt when --lang is set.
106
+ */
107
+ buildSystemPrompt(base) {
108
+ const lang = this.effectiveLang();
109
+ if (!lang)
110
+ return base;
111
+ return `${base}\n\nIMPORTANT: Write ALL documentation content in ${lang}. This includes prose, code comments in examples, and diagram labels. Note: page titles (H1 headings) are generated separately and will remain in English.`;
112
+ }
92
113
  /**
93
114
  * Route LLM call to the appropriate provider (OpenAI-compatible or Cursor CLI).
94
115
  */
@@ -112,6 +133,13 @@ export class WikiGenerator {
112
133
  const forceMode = this.options.force;
113
134
  // Up-to-date check (skip if --force)
114
135
  if (!forceMode && existingMeta && existingMeta.fromCommit === currentCommit) {
136
+ const currentLang = this.effectiveLang();
137
+ const metaLang = existingMeta.lang ?? '';
138
+ if (currentLang !== metaLang) {
139
+ const prevDisplay = metaLang || 'english (default)';
140
+ const nextDisplay = currentLang || 'english (default)';
141
+ throw new Error(`Wiki was generated in ${prevDisplay}; use --force to regenerate in ${nextDisplay}.`);
142
+ }
115
143
  // Still regenerate the HTML viewer in case it's missing
116
144
  await this.ensureHTMLViewer();
117
145
  return { pagesGenerated: 0, mode: 'up-to-date', failedModules: [] };
@@ -139,6 +167,13 @@ export class WikiGenerator {
139
167
  let result;
140
168
  try {
141
169
  if (!forceMode && existingMeta && existingMeta.fromCommit) {
170
+ const currentLang = this.effectiveLang();
171
+ const metaLang = existingMeta.lang ?? '';
172
+ if (currentLang !== metaLang) {
173
+ const prevDisplay = metaLang || 'english (default)';
174
+ const nextDisplay = currentLang || 'english (default)';
175
+ throw new Error(`Wiki was generated in ${prevDisplay}; use --force to regenerate in ${nextDisplay}.`);
176
+ }
142
177
  result = await this.incrementalUpdate(existingMeta, currentCommit);
143
178
  }
144
179
  else {
@@ -257,6 +292,7 @@ export class WikiGenerator {
257
292
  fromCommit: currentCommit,
258
293
  generatedAt: new Date().toISOString(),
259
294
  model: this.llmConfig.model,
295
+ lang: this.effectiveLang(),
260
296
  moduleFiles,
261
297
  moduleTree,
262
298
  });
@@ -298,6 +334,9 @@ export class WikiGenerator {
298
334
  FILE_LIST: fileList,
299
335
  DIRECTORY_TREE: dirTree,
300
336
  });
337
+ // Grouping is a structured-data phase (JSON output), not documentation.
338
+ // Do NOT apply buildSystemPrompt here — a language instruction would risk
339
+ // translating module-name keys, breaking slug stability and JSON parsing.
301
340
  const response = await this.invokeLLM(prompt, GROUPING_SYSTEM_PROMPT, this.streamOpts('Grouping files', 15, 13));
302
341
  const grouping = this.parseGroupingResponse(response.content, files);
303
342
  // Convert to tree nodes
@@ -444,8 +483,8 @@ export class WikiGenerator {
444
483
  INCOMING_CALLS: formatCallEdges(interCalls.incoming),
445
484
  PROCESSES: formatProcesses(processes),
446
485
  });
447
- const response = await this.invokeLLM(prompt, MODULE_SYSTEM_PROMPT, this.streamOpts(node.name));
448
- // Write page with front matter
486
+ const response = await this.invokeLLM(prompt, this.buildSystemPrompt(MODULE_SYSTEM_PROMPT), this.streamOpts(node.name));
487
+ // H1 uses the English module name (stable slug source); body is LLM-translated.
449
488
  const pageContent = sanitizeMermaidMarkdown(`# ${node.name}\n\n${response.content}`);
450
489
  await fs.writeFile(path.join(this.wikiDir, `${node.slug}.md`), pageContent, 'utf-8');
451
490
  }
@@ -480,7 +519,7 @@ export class WikiGenerator {
480
519
  CROSS_MODULE_CALLS: formatCallEdges(crossCalls),
481
520
  CROSS_PROCESSES: formatProcesses(processes),
482
521
  });
483
- const response = await this.invokeLLM(prompt, PARENT_SYSTEM_PROMPT, this.streamOpts(node.name));
522
+ const response = await this.invokeLLM(prompt, this.buildSystemPrompt(PARENT_SYSTEM_PROMPT), this.streamOpts(node.name));
484
523
  const pageContent = sanitizeMermaidMarkdown(`# ${node.name}\n\n${response.content}`);
485
524
  await fs.writeFile(path.join(this.wikiDir, `${node.slug}.md`), pageContent, 'utf-8');
486
525
  }
@@ -516,7 +555,7 @@ export class WikiGenerator {
516
555
  MODULE_EDGES: edgesText,
517
556
  TOP_PROCESSES: formatProcesses(topProcesses),
518
557
  });
519
- const response = await this.invokeLLM(prompt, OVERVIEW_SYSTEM_PROMPT, this.streamOpts('Generating overview', 88));
558
+ const response = await this.invokeLLM(prompt, this.buildSystemPrompt(OVERVIEW_SYSTEM_PROMPT), this.streamOpts('Generating overview', 88));
520
559
  const pageContent = sanitizeMermaidMarkdown(`# ${path.basename(this.repoPath)} — Wiki\n\n${response.content}`);
521
560
  await fs.writeFile(path.join(this.wikiDir, 'overview.md'), pageContent, 'utf-8');
522
561
  }
@@ -538,6 +577,7 @@ export class WikiGenerator {
538
577
  ...existingMeta,
539
578
  fromCommit: currentCommit,
540
579
  generatedAt: new Date().toISOString(),
580
+ lang: this.effectiveLang(),
541
581
  });
542
582
  return { pagesGenerated: 0, mode: 'incremental', failedModules: [] };
543
583
  }
@@ -627,6 +667,7 @@ export class WikiGenerator {
627
667
  fromCommit: currentCommit,
628
668
  generatedAt: new Date().toISOString(),
629
669
  model: this.llmConfig.model,
670
+ lang: this.effectiveLang(),
630
671
  });
631
672
  this.onProgress('done', 100, 'Incremental update complete');
632
673
  return { pagesGenerated, mode: 'incremental', failedModules: [...this.failedModules] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.4",
3
+ "version": "1.6.6-rc.6",
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",