@duytransipher/gitnexus 1.4.6-sipher.0

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 (224) hide show
  1. package/LICENSE +73 -0
  2. package/README.md +261 -0
  3. package/dist/cli/ai-context.d.ts +23 -0
  4. package/dist/cli/ai-context.js +265 -0
  5. package/dist/cli/analyze.d.ts +12 -0
  6. package/dist/cli/analyze.js +345 -0
  7. package/dist/cli/augment.d.ts +13 -0
  8. package/dist/cli/augment.js +33 -0
  9. package/dist/cli/clean.d.ts +10 -0
  10. package/dist/cli/clean.js +60 -0
  11. package/dist/cli/eval-server.d.ts +37 -0
  12. package/dist/cli/eval-server.js +389 -0
  13. package/dist/cli/index.d.ts +2 -0
  14. package/dist/cli/index.js +137 -0
  15. package/dist/cli/lazy-action.d.ts +6 -0
  16. package/dist/cli/lazy-action.js +18 -0
  17. package/dist/cli/list.d.ts +6 -0
  18. package/dist/cli/list.js +30 -0
  19. package/dist/cli/mcp.d.ts +8 -0
  20. package/dist/cli/mcp.js +36 -0
  21. package/dist/cli/serve.d.ts +4 -0
  22. package/dist/cli/serve.js +6 -0
  23. package/dist/cli/setup.d.ts +8 -0
  24. package/dist/cli/setup.js +367 -0
  25. package/dist/cli/sipher-patched.d.ts +2 -0
  26. package/dist/cli/sipher-patched.js +77 -0
  27. package/dist/cli/skill-gen.d.ts +26 -0
  28. package/dist/cli/skill-gen.js +549 -0
  29. package/dist/cli/status.d.ts +6 -0
  30. package/dist/cli/status.js +36 -0
  31. package/dist/cli/tool.d.ts +60 -0
  32. package/dist/cli/tool.js +180 -0
  33. package/dist/cli/wiki.d.ts +15 -0
  34. package/dist/cli/wiki.js +365 -0
  35. package/dist/config/ignore-service.d.ts +26 -0
  36. package/dist/config/ignore-service.js +284 -0
  37. package/dist/config/supported-languages.d.ts +15 -0
  38. package/dist/config/supported-languages.js +16 -0
  39. package/dist/core/augmentation/engine.d.ts +26 -0
  40. package/dist/core/augmentation/engine.js +240 -0
  41. package/dist/core/embeddings/embedder.d.ts +60 -0
  42. package/dist/core/embeddings/embedder.js +251 -0
  43. package/dist/core/embeddings/embedding-pipeline.d.ts +51 -0
  44. package/dist/core/embeddings/embedding-pipeline.js +356 -0
  45. package/dist/core/embeddings/index.d.ts +9 -0
  46. package/dist/core/embeddings/index.js +9 -0
  47. package/dist/core/embeddings/text-generator.d.ts +24 -0
  48. package/dist/core/embeddings/text-generator.js +182 -0
  49. package/dist/core/embeddings/types.d.ts +87 -0
  50. package/dist/core/embeddings/types.js +32 -0
  51. package/dist/core/graph/graph.d.ts +2 -0
  52. package/dist/core/graph/graph.js +66 -0
  53. package/dist/core/graph/types.d.ts +66 -0
  54. package/dist/core/graph/types.js +1 -0
  55. package/dist/core/ingestion/ast-cache.d.ts +11 -0
  56. package/dist/core/ingestion/ast-cache.js +35 -0
  57. package/dist/core/ingestion/call-processor.d.ts +23 -0
  58. package/dist/core/ingestion/call-processor.js +793 -0
  59. package/dist/core/ingestion/call-routing.d.ts +68 -0
  60. package/dist/core/ingestion/call-routing.js +129 -0
  61. package/dist/core/ingestion/cluster-enricher.d.ts +38 -0
  62. package/dist/core/ingestion/cluster-enricher.js +170 -0
  63. package/dist/core/ingestion/community-processor.d.ts +39 -0
  64. package/dist/core/ingestion/community-processor.js +312 -0
  65. package/dist/core/ingestion/constants.d.ts +16 -0
  66. package/dist/core/ingestion/constants.js +16 -0
  67. package/dist/core/ingestion/entry-point-scoring.d.ts +40 -0
  68. package/dist/core/ingestion/entry-point-scoring.js +353 -0
  69. package/dist/core/ingestion/export-detection.d.ts +18 -0
  70. package/dist/core/ingestion/export-detection.js +231 -0
  71. package/dist/core/ingestion/filesystem-walker.d.ts +28 -0
  72. package/dist/core/ingestion/filesystem-walker.js +81 -0
  73. package/dist/core/ingestion/framework-detection.d.ts +54 -0
  74. package/dist/core/ingestion/framework-detection.js +411 -0
  75. package/dist/core/ingestion/heritage-processor.d.ts +28 -0
  76. package/dist/core/ingestion/heritage-processor.js +251 -0
  77. package/dist/core/ingestion/import-processor.d.ts +34 -0
  78. package/dist/core/ingestion/import-processor.js +398 -0
  79. package/dist/core/ingestion/language-config.d.ts +46 -0
  80. package/dist/core/ingestion/language-config.js +167 -0
  81. package/dist/core/ingestion/mro-processor.d.ts +45 -0
  82. package/dist/core/ingestion/mro-processor.js +369 -0
  83. package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
  84. package/dist/core/ingestion/named-binding-extraction.js +363 -0
  85. package/dist/core/ingestion/parsing-processor.d.ts +19 -0
  86. package/dist/core/ingestion/parsing-processor.js +315 -0
  87. package/dist/core/ingestion/pipeline.d.ts +6 -0
  88. package/dist/core/ingestion/pipeline.js +401 -0
  89. package/dist/core/ingestion/process-processor.d.ts +51 -0
  90. package/dist/core/ingestion/process-processor.js +315 -0
  91. package/dist/core/ingestion/resolution-context.d.ts +53 -0
  92. package/dist/core/ingestion/resolution-context.js +132 -0
  93. package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
  94. package/dist/core/ingestion/resolvers/csharp.js +109 -0
  95. package/dist/core/ingestion/resolvers/go.d.ts +19 -0
  96. package/dist/core/ingestion/resolvers/go.js +42 -0
  97. package/dist/core/ingestion/resolvers/index.d.ts +18 -0
  98. package/dist/core/ingestion/resolvers/index.js +13 -0
  99. package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
  100. package/dist/core/ingestion/resolvers/jvm.js +87 -0
  101. package/dist/core/ingestion/resolvers/php.d.ts +15 -0
  102. package/dist/core/ingestion/resolvers/php.js +35 -0
  103. package/dist/core/ingestion/resolvers/python.d.ts +19 -0
  104. package/dist/core/ingestion/resolvers/python.js +52 -0
  105. package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
  106. package/dist/core/ingestion/resolvers/ruby.js +15 -0
  107. package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
  108. package/dist/core/ingestion/resolvers/rust.js +73 -0
  109. package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
  110. package/dist/core/ingestion/resolvers/standard.js +123 -0
  111. package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
  112. package/dist/core/ingestion/resolvers/utils.js +122 -0
  113. package/dist/core/ingestion/structure-processor.d.ts +2 -0
  114. package/dist/core/ingestion/structure-processor.js +36 -0
  115. package/dist/core/ingestion/symbol-table.d.ts +63 -0
  116. package/dist/core/ingestion/symbol-table.js +85 -0
  117. package/dist/core/ingestion/tree-sitter-queries.d.ts +15 -0
  118. package/dist/core/ingestion/tree-sitter-queries.js +888 -0
  119. package/dist/core/ingestion/type-env.d.ts +49 -0
  120. package/dist/core/ingestion/type-env.js +613 -0
  121. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
  122. package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
  123. package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
  124. package/dist/core/ingestion/type-extractors/csharp.js +383 -0
  125. package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
  126. package/dist/core/ingestion/type-extractors/go.js +467 -0
  127. package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
  128. package/dist/core/ingestion/type-extractors/index.js +31 -0
  129. package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
  130. package/dist/core/ingestion/type-extractors/jvm.js +681 -0
  131. package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
  132. package/dist/core/ingestion/type-extractors/php.js +549 -0
  133. package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
  134. package/dist/core/ingestion/type-extractors/python.js +455 -0
  135. package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
  136. package/dist/core/ingestion/type-extractors/ruby.js +389 -0
  137. package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
  138. package/dist/core/ingestion/type-extractors/rust.js +456 -0
  139. package/dist/core/ingestion/type-extractors/shared.d.ts +145 -0
  140. package/dist/core/ingestion/type-extractors/shared.js +810 -0
  141. package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
  142. package/dist/core/ingestion/type-extractors/swift.js +137 -0
  143. package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
  144. package/dist/core/ingestion/type-extractors/types.js +1 -0
  145. package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
  146. package/dist/core/ingestion/type-extractors/typescript.js +494 -0
  147. package/dist/core/ingestion/utils.d.ts +138 -0
  148. package/dist/core/ingestion/utils.js +1290 -0
  149. package/dist/core/ingestion/workers/parse-worker.d.ts +122 -0
  150. package/dist/core/ingestion/workers/parse-worker.js +1126 -0
  151. package/dist/core/ingestion/workers/worker-pool.d.ts +16 -0
  152. package/dist/core/ingestion/workers/worker-pool.js +128 -0
  153. package/dist/core/lbug/csv-generator.d.ts +33 -0
  154. package/dist/core/lbug/csv-generator.js +366 -0
  155. package/dist/core/lbug/lbug-adapter.d.ts +103 -0
  156. package/dist/core/lbug/lbug-adapter.js +769 -0
  157. package/dist/core/lbug/schema.d.ts +53 -0
  158. package/dist/core/lbug/schema.js +430 -0
  159. package/dist/core/search/bm25-index.d.ts +23 -0
  160. package/dist/core/search/bm25-index.js +96 -0
  161. package/dist/core/search/hybrid-search.d.ts +49 -0
  162. package/dist/core/search/hybrid-search.js +118 -0
  163. package/dist/core/tree-sitter/parser-loader.d.ts +5 -0
  164. package/dist/core/tree-sitter/parser-loader.js +63 -0
  165. package/dist/core/wiki/generator.d.ts +120 -0
  166. package/dist/core/wiki/generator.js +939 -0
  167. package/dist/core/wiki/graph-queries.d.ts +80 -0
  168. package/dist/core/wiki/graph-queries.js +238 -0
  169. package/dist/core/wiki/html-viewer.d.ts +10 -0
  170. package/dist/core/wiki/html-viewer.js +297 -0
  171. package/dist/core/wiki/llm-client.d.ts +43 -0
  172. package/dist/core/wiki/llm-client.js +186 -0
  173. package/dist/core/wiki/prompts.d.ts +53 -0
  174. package/dist/core/wiki/prompts.js +174 -0
  175. package/dist/lib/utils.d.ts +1 -0
  176. package/dist/lib/utils.js +3 -0
  177. package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
  178. package/dist/mcp/compatible-stdio-transport.js +200 -0
  179. package/dist/mcp/core/embedder.d.ts +27 -0
  180. package/dist/mcp/core/embedder.js +108 -0
  181. package/dist/mcp/core/lbug-adapter.d.ts +57 -0
  182. package/dist/mcp/core/lbug-adapter.js +455 -0
  183. package/dist/mcp/local/local-backend.d.ts +181 -0
  184. package/dist/mcp/local/local-backend.js +1722 -0
  185. package/dist/mcp/resources.d.ts +31 -0
  186. package/dist/mcp/resources.js +411 -0
  187. package/dist/mcp/server.d.ts +23 -0
  188. package/dist/mcp/server.js +296 -0
  189. package/dist/mcp/staleness.d.ts +15 -0
  190. package/dist/mcp/staleness.js +29 -0
  191. package/dist/mcp/tools.d.ts +24 -0
  192. package/dist/mcp/tools.js +292 -0
  193. package/dist/server/api.d.ts +10 -0
  194. package/dist/server/api.js +344 -0
  195. package/dist/server/mcp-http.d.ts +13 -0
  196. package/dist/server/mcp-http.js +100 -0
  197. package/dist/storage/git.d.ts +6 -0
  198. package/dist/storage/git.js +35 -0
  199. package/dist/storage/repo-manager.d.ts +138 -0
  200. package/dist/storage/repo-manager.js +299 -0
  201. package/dist/types/pipeline.d.ts +32 -0
  202. package/dist/types/pipeline.js +18 -0
  203. package/dist/unreal/bridge.d.ts +4 -0
  204. package/dist/unreal/bridge.js +113 -0
  205. package/dist/unreal/config.d.ts +6 -0
  206. package/dist/unreal/config.js +55 -0
  207. package/dist/unreal/types.d.ts +105 -0
  208. package/dist/unreal/types.js +1 -0
  209. package/hooks/claude/gitnexus-hook.cjs +238 -0
  210. package/hooks/claude/pre-tool-use.sh +79 -0
  211. package/hooks/claude/session-start.sh +42 -0
  212. package/package.json +100 -0
  213. package/scripts/ensure-cli-executable.cjs +21 -0
  214. package/scripts/patch-tree-sitter-swift.cjs +74 -0
  215. package/scripts/setup-unreal-gitnexus.ps1 +191 -0
  216. package/skills/gitnexus-cli.md +82 -0
  217. package/skills/gitnexus-debugging.md +89 -0
  218. package/skills/gitnexus-exploring.md +78 -0
  219. package/skills/gitnexus-guide.md +64 -0
  220. package/skills/gitnexus-impact-analysis.md +97 -0
  221. package/skills/gitnexus-pr-review.md +163 -0
  222. package/skills/gitnexus-refactoring.md +121 -0
  223. package/vendor/leiden/index.cjs +355 -0
  224. package/vendor/leiden/utils.cjs +392 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Direct CLI Tool Commands
3
+ *
4
+ * Exposes GitNexus tools (query, context, impact, cypher) as direct CLI commands.
5
+ * Bypasses MCP entirely — invokes LocalBackend directly for minimal overhead.
6
+ *
7
+ * Usage:
8
+ * gitnexus query "authentication flow"
9
+ * gitnexus context --name "validateUser"
10
+ * gitnexus impact --target "AuthService" --direction upstream
11
+ * gitnexus cypher "MATCH (n:Function) RETURN n.name LIMIT 10"
12
+ * gitnexus unreal-find-refs "ALyraWeaponSpawner::GetPickupInstigator"
13
+ *
14
+ * Note: Output goes to stdout via fs.writeSync(fd 1), bypassing LadybugDB's
15
+ * native module which captures the Node.js process.stdout stream during init.
16
+ * See the output() function for details (#324).
17
+ */
18
+ import { writeSync } from 'node:fs';
19
+ import { LocalBackend } from '../mcp/local/local-backend.js';
20
+ let _backend = null;
21
+ async function getBackend() {
22
+ if (_backend)
23
+ return _backend;
24
+ _backend = new LocalBackend();
25
+ const ok = await _backend.init();
26
+ if (!ok) {
27
+ console.error('GitNexus: No indexed repositories found. Run: gitnexus analyze');
28
+ process.exit(1);
29
+ }
30
+ return _backend;
31
+ }
32
+ /**
33
+ * Write tool output to stdout using low-level fd write.
34
+ *
35
+ * LadybugDB's native module captures Node.js process.stdout during init,
36
+ * but the underlying OS file descriptor 1 (stdout) remains intact.
37
+ * By using fs.writeSync(1, ...) we bypass the Node.js stream layer
38
+ * and write directly to the real stdout fd (#324).
39
+ *
40
+ * Falls back to stderr if the fd write fails (e.g., broken pipe).
41
+ */
42
+ function output(data) {
43
+ const text = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
44
+ try {
45
+ writeSync(1, text + '\n');
46
+ }
47
+ catch (err) {
48
+ if (err?.code === 'EPIPE') {
49
+ // Consumer closed the pipe (e.g., `gitnexus cypher ... | head -1`)
50
+ // Exit cleanly per Unix convention
51
+ process.exit(0);
52
+ }
53
+ // Fallback: stderr (previous behavior, works on all platforms)
54
+ process.stderr.write(text + '\n');
55
+ }
56
+ }
57
+ export async function queryCommand(queryText, options) {
58
+ if (!queryText?.trim()) {
59
+ console.error('Usage: gitnexus query <search_query>');
60
+ process.exit(1);
61
+ }
62
+ const backend = await getBackend();
63
+ const result = await backend.callTool('query', {
64
+ query: queryText,
65
+ task_context: options?.context,
66
+ goal: options?.goal,
67
+ limit: options?.limit ? parseInt(options.limit) : undefined,
68
+ include_content: options?.content ?? false,
69
+ repo: options?.repo,
70
+ });
71
+ output(result);
72
+ }
73
+ export async function contextCommand(name, options) {
74
+ if (!name?.trim() && !options?.uid) {
75
+ console.error('Usage: gitnexus context <symbol_name> [--uid <uid>] [--file <path>]');
76
+ process.exit(1);
77
+ }
78
+ const backend = await getBackend();
79
+ const result = await backend.callTool('context', {
80
+ name: name || undefined,
81
+ uid: options?.uid,
82
+ file_path: options?.file,
83
+ include_content: options?.content ?? false,
84
+ repo: options?.repo,
85
+ });
86
+ output(result);
87
+ }
88
+ export async function impactCommand(target, options) {
89
+ if (!target?.trim()) {
90
+ console.error('Usage: gitnexus impact <symbol_name> [--direction upstream|downstream]');
91
+ process.exit(1);
92
+ }
93
+ try {
94
+ const backend = await getBackend();
95
+ const result = await backend.callTool('impact', {
96
+ target,
97
+ direction: options?.direction || 'upstream',
98
+ maxDepth: options?.depth ? parseInt(options.depth, 10) : undefined,
99
+ includeTests: options?.includeTests ?? false,
100
+ repo: options?.repo,
101
+ });
102
+ output(result);
103
+ }
104
+ catch (err) {
105
+ // Belt-and-suspenders: catch infrastructure failures (getBackend, callTool transport)
106
+ // The backend's impact() already returns structured errors for graph query failures
107
+ output({
108
+ error: (err instanceof Error ? err.message : String(err)) || 'Impact analysis failed unexpectedly',
109
+ target: { name: target },
110
+ direction: options?.direction || 'upstream',
111
+ suggestion: 'Try reducing --depth or using gitnexus context <symbol> as a fallback',
112
+ });
113
+ process.exit(1);
114
+ }
115
+ }
116
+ export async function cypherCommand(query, options) {
117
+ if (!query?.trim()) {
118
+ console.error('Usage: gitnexus cypher <cypher_query>');
119
+ process.exit(1);
120
+ }
121
+ const backend = await getBackend();
122
+ const result = await backend.callTool('cypher', {
123
+ query,
124
+ repo: options?.repo,
125
+ });
126
+ output(result);
127
+ }
128
+ export async function syncUnrealAssetManifestCommand(options) {
129
+ const backend = await getBackend();
130
+ const result = await backend.callTool('sync_unreal_asset_manifest', {
131
+ repo: options?.repo,
132
+ });
133
+ output(result);
134
+ }
135
+ export async function findNativeBlueprintReferencesCommand(functionName, options) {
136
+ if (!functionName?.trim() && !options?.uid) {
137
+ console.error('Usage: gitnexus unreal-find-refs <Class::Function> [--uid <uid>]');
138
+ process.exit(1);
139
+ }
140
+ const backend = await getBackend();
141
+ const result = await backend.callTool('find_native_blueprint_references', {
142
+ function: functionName || undefined,
143
+ symbol_uid: options?.uid,
144
+ class_name: options?.className,
145
+ file_path: options?.file,
146
+ refresh_manifest: options?.refreshManifest ?? false,
147
+ max_candidates: options?.maxCandidates ? parseInt(options.maxCandidates, 10) : undefined,
148
+ repo: options?.repo,
149
+ });
150
+ output(result);
151
+ }
152
+ export async function expandBlueprintChainCommand(assetPath, chainAnchorId, options) {
153
+ if (!assetPath?.trim() || !chainAnchorId?.trim()) {
154
+ console.error('Usage: gitnexus unreal-expand-chain <asset_path> <chain_anchor_id>');
155
+ process.exit(1);
156
+ }
157
+ const backend = await getBackend();
158
+ const result = await backend.callTool('expand_blueprint_chain', {
159
+ asset_path: assetPath,
160
+ chain_anchor_id: chainAnchorId,
161
+ direction: options?.direction || 'downstream',
162
+ max_depth: options?.depth ? parseInt(options.depth, 10) : undefined,
163
+ repo: options?.repo,
164
+ });
165
+ output(result);
166
+ }
167
+ export async function findBlueprintsDerivedFromNativeClassCommand(className, options) {
168
+ if (!className?.trim()) {
169
+ console.error('Usage: gitnexus unreal-derived-blueprints <class_name>');
170
+ process.exit(1);
171
+ }
172
+ const backend = await getBackend();
173
+ const result = await backend.callTool('find_blueprints_derived_from_native_class', {
174
+ class_name: className,
175
+ refresh_manifest: options?.refreshManifest ?? false,
176
+ max_results: options?.maxResults ? parseInt(options.maxResults, 10) : undefined,
177
+ repo: options?.repo,
178
+ });
179
+ output(result);
180
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Wiki Command
3
+ *
4
+ * Generates repository documentation from the knowledge graph.
5
+ * Usage: gitnexus wiki [path] [options]
6
+ */
7
+ export interface WikiCommandOptions {
8
+ force?: boolean;
9
+ model?: string;
10
+ baseUrl?: string;
11
+ apiKey?: string;
12
+ concurrency?: string;
13
+ gist?: boolean;
14
+ }
15
+ export declare const wikiCommand: (inputPath?: string, options?: WikiCommandOptions) => Promise<void>;
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Wiki Command
3
+ *
4
+ * Generates repository documentation from the knowledge graph.
5
+ * Usage: gitnexus wiki [path] [options]
6
+ */
7
+ import path from 'path';
8
+ import readline from 'readline';
9
+ import { execSync, execFileSync } from 'child_process';
10
+ import cliProgress from 'cli-progress';
11
+ import { getGitRoot, isGitRepo } from '../storage/git.js';
12
+ import { getStoragePaths, loadMeta, loadCLIConfig, saveCLIConfig } from '../storage/repo-manager.js';
13
+ import { WikiGenerator } from '../core/wiki/generator.js';
14
+ import { resolveLLMConfig } from '../core/wiki/llm-client.js';
15
+ /**
16
+ * Prompt the user for input via stdin.
17
+ */
18
+ function prompt(question, hide = false) {
19
+ return new Promise((resolve) => {
20
+ const rl = readline.createInterface({
21
+ input: process.stdin,
22
+ output: process.stdout,
23
+ });
24
+ if (hide && process.stdin.isTTY) {
25
+ // Mask input for API keys
26
+ process.stdout.write(question);
27
+ let input = '';
28
+ process.stdin.setRawMode(true);
29
+ process.stdin.resume();
30
+ process.stdin.setEncoding('utf-8');
31
+ const onData = (char) => {
32
+ if (char === '\n' || char === '\r' || char === '\u0004') {
33
+ process.stdin.setRawMode(false);
34
+ process.stdin.removeListener('data', onData);
35
+ process.stdout.write('\n');
36
+ rl.close();
37
+ resolve(input);
38
+ }
39
+ else if (char === '\u0003') {
40
+ // Ctrl+C
41
+ process.stdin.setRawMode(false);
42
+ rl.close();
43
+ process.exit(1);
44
+ }
45
+ else if (char === '\u007F' || char === '\b') {
46
+ // Backspace
47
+ if (input.length > 0) {
48
+ input = input.slice(0, -1);
49
+ process.stdout.write('\b \b');
50
+ }
51
+ }
52
+ else {
53
+ input += char;
54
+ process.stdout.write('*');
55
+ }
56
+ };
57
+ process.stdin.on('data', onData);
58
+ }
59
+ else {
60
+ rl.question(question, (answer) => {
61
+ rl.close();
62
+ resolve(answer.trim());
63
+ });
64
+ }
65
+ });
66
+ }
67
+ export const wikiCommand = async (inputPath, options) => {
68
+ console.log('\n GitNexus Wiki Generator\n');
69
+ // ── Resolve repo path ───────────────────────────────────────────────
70
+ let repoPath;
71
+ if (inputPath) {
72
+ repoPath = path.resolve(inputPath);
73
+ }
74
+ else {
75
+ const gitRoot = getGitRoot(process.cwd());
76
+ if (!gitRoot) {
77
+ console.log(' Error: Not inside a git repository\n');
78
+ process.exitCode = 1;
79
+ return;
80
+ }
81
+ repoPath = gitRoot;
82
+ }
83
+ if (!isGitRepo(repoPath)) {
84
+ console.log(' Error: Not a git repository\n');
85
+ process.exitCode = 1;
86
+ return;
87
+ }
88
+ // ── Check for existing index ────────────────────────────────────────
89
+ const { storagePath, lbugPath } = getStoragePaths(repoPath);
90
+ const meta = await loadMeta(storagePath);
91
+ if (!meta) {
92
+ console.log(' Error: No GitNexus index found.');
93
+ console.log(' Run `gitnexus analyze` first to index this repository.\n');
94
+ process.exitCode = 1;
95
+ return;
96
+ }
97
+ // ── Resolve LLM config (with interactive fallback) ─────────────────
98
+ // Save any CLI overrides immediately
99
+ if (options?.apiKey || options?.model || options?.baseUrl) {
100
+ const existing = await loadCLIConfig();
101
+ const updates = {};
102
+ if (options.apiKey)
103
+ updates.apiKey = options.apiKey;
104
+ if (options.model)
105
+ updates.model = options.model;
106
+ if (options.baseUrl)
107
+ updates.baseUrl = options.baseUrl;
108
+ await saveCLIConfig({ ...existing, ...updates });
109
+ console.log(' Config saved to ~/.gitnexus/config.json\n');
110
+ }
111
+ const savedConfig = await loadCLIConfig();
112
+ const hasSavedConfig = !!(savedConfig.apiKey && savedConfig.baseUrl);
113
+ const hasCLIOverrides = !!(options?.apiKey || options?.model || options?.baseUrl);
114
+ let llmConfig = await resolveLLMConfig({
115
+ model: options?.model,
116
+ baseUrl: options?.baseUrl,
117
+ apiKey: options?.apiKey,
118
+ });
119
+ // Run interactive setup if no saved config and no CLI flags provided
120
+ // (even if env vars exist — let user explicitly choose their provider)
121
+ if (!hasSavedConfig && !hasCLIOverrides) {
122
+ if (!process.stdin.isTTY) {
123
+ if (!llmConfig.apiKey) {
124
+ console.log(' Error: No LLM API key found.');
125
+ console.log(' Set OPENAI_API_KEY, GITNEXUS_API_KEY, or AI_GATEWAY_API_KEY environment variable,');
126
+ console.log(' or pass --api-key <key>.\n');
127
+ process.exitCode = 1;
128
+ return;
129
+ }
130
+ // Non-interactive with env var — just use it
131
+ }
132
+ else {
133
+ console.log(' No LLM configured. Let\'s set it up.\n');
134
+ console.log(' Supports OpenAI, OpenRouter, or any OpenAI-compatible API.\n');
135
+ // Provider selection
136
+ console.log(' [1] OpenAI (api.openai.com)');
137
+ console.log(' [2] OpenRouter (openrouter.ai)');
138
+ console.log(' [3] Custom endpoint\n');
139
+ const choice = await prompt(' Select provider (1/2/3): ');
140
+ let baseUrl;
141
+ let defaultModel;
142
+ if (choice === '2') {
143
+ baseUrl = 'https://openrouter.ai/api/v1';
144
+ defaultModel = 'minimax/minimax-m2.5';
145
+ }
146
+ else if (choice === '3') {
147
+ baseUrl = await prompt(' Base URL (e.g. http://localhost:11434/v1): ');
148
+ if (!baseUrl) {
149
+ console.log('\n No URL provided. Aborting.\n');
150
+ process.exitCode = 1;
151
+ return;
152
+ }
153
+ defaultModel = 'gpt-4o-mini';
154
+ }
155
+ else {
156
+ baseUrl = 'https://api.openai.com/v1';
157
+ defaultModel = 'gpt-4o-mini';
158
+ }
159
+ // Model
160
+ const modelInput = await prompt(` Model (default: ${defaultModel}): `);
161
+ const model = modelInput || defaultModel;
162
+ // API key — pre-fill hint if env var exists
163
+ const envKey = process.env.GITNEXUS_API_KEY || process.env.OPENAI_API_KEY || process.env.AI_GATEWAY_API_KEY || '';
164
+ let key;
165
+ if (envKey) {
166
+ const masked = envKey.slice(0, 6) + '...' + envKey.slice(-4);
167
+ const useEnv = await prompt(` Use existing env key (${masked})? (Y/n): `);
168
+ if (!useEnv || useEnv.toLowerCase() === 'y' || useEnv.toLowerCase() === 'yes') {
169
+ key = envKey;
170
+ }
171
+ else {
172
+ key = await prompt(' API key: ', true);
173
+ }
174
+ }
175
+ else {
176
+ key = await prompt(' API key: ', true);
177
+ }
178
+ if (!key) {
179
+ console.log('\n No key provided. Aborting.\n');
180
+ process.exitCode = 1;
181
+ return;
182
+ }
183
+ // Save
184
+ await saveCLIConfig({ apiKey: key, baseUrl, model });
185
+ console.log(' Config saved to ~/.gitnexus/config.json\n');
186
+ llmConfig = { ...llmConfig, apiKey: key, baseUrl, model };
187
+ }
188
+ }
189
+ // ── Setup progress bar with elapsed timer ──────────────────────────
190
+ const bar = new cliProgress.SingleBar({
191
+ format: ' {bar} {percentage}% | {phase}',
192
+ barCompleteChar: '\u2588',
193
+ barIncompleteChar: '\u2591',
194
+ hideCursor: true,
195
+ barGlue: '',
196
+ autopadding: true,
197
+ clearOnComplete: false,
198
+ stopOnComplete: false,
199
+ }, cliProgress.Presets.shades_grey);
200
+ bar.start(100, 0, { phase: 'Initializing...' });
201
+ const t0 = Date.now();
202
+ let lastPhase = '';
203
+ let phaseStart = t0;
204
+ // Tick elapsed time every second while stuck on the same phase
205
+ const elapsedTimer = setInterval(() => {
206
+ if (lastPhase) {
207
+ const elapsed = Math.round((Date.now() - phaseStart) / 1000);
208
+ if (elapsed >= 3) {
209
+ bar.update({ phase: `${lastPhase} (${elapsed}s)` });
210
+ }
211
+ }
212
+ }, 1000);
213
+ // ── Run generator ───────────────────────────────────────────────────
214
+ const wikiOptions = {
215
+ force: options?.force,
216
+ model: options?.model,
217
+ baseUrl: options?.baseUrl,
218
+ concurrency: options?.concurrency ? parseInt(options.concurrency, 10) : undefined,
219
+ };
220
+ const generator = new WikiGenerator(repoPath, storagePath, lbugPath, llmConfig, wikiOptions, (phase, percent, detail) => {
221
+ const label = detail || phase;
222
+ if (label !== lastPhase) {
223
+ lastPhase = label;
224
+ phaseStart = Date.now();
225
+ }
226
+ bar.update(percent, { phase: label });
227
+ });
228
+ try {
229
+ const result = await generator.run();
230
+ clearInterval(elapsedTimer);
231
+ bar.update(100, { phase: 'Done' });
232
+ bar.stop();
233
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
234
+ const wikiDir = path.join(storagePath, 'wiki');
235
+ const viewerPath = path.join(wikiDir, 'index.html');
236
+ if (result.mode === 'up-to-date' && !options?.force) {
237
+ console.log('\n Wiki is already up to date.');
238
+ console.log(` Viewer: ${viewerPath}\n`);
239
+ await maybePublishGist(viewerPath, options?.gist);
240
+ return;
241
+ }
242
+ console.log(`\n Wiki generated successfully (${elapsed}s)\n`);
243
+ console.log(` Mode: ${result.mode}`);
244
+ console.log(` Pages: ${result.pagesGenerated}`);
245
+ console.log(` Output: ${wikiDir}`);
246
+ console.log(` Viewer: ${viewerPath}`);
247
+ if (result.failedModules && result.failedModules.length > 0) {
248
+ console.log(`\n Failed modules (${result.failedModules.length}):`);
249
+ for (const mod of result.failedModules) {
250
+ console.log(` - ${mod}`);
251
+ }
252
+ console.log(' Re-run to retry failed modules (pages will be regenerated).');
253
+ }
254
+ console.log('');
255
+ await maybePublishGist(viewerPath, options?.gist);
256
+ }
257
+ catch (err) {
258
+ clearInterval(elapsedTimer);
259
+ bar.stop();
260
+ if (err.message?.includes('No source files')) {
261
+ console.log(`\n ${err.message}\n`);
262
+ }
263
+ else if (err.message?.includes('API key') || err.message?.includes('API error')) {
264
+ console.log(`\n LLM Error: ${err.message}\n`);
265
+ // Offer to reconfigure on auth-related failures
266
+ const isAuthError = err.message?.includes('401') || err.message?.includes('403')
267
+ || err.message?.includes('502') || err.message?.includes('authenticate')
268
+ || err.message?.includes('Unauthorized');
269
+ if (isAuthError && process.stdin.isTTY) {
270
+ const answer = await new Promise((resolve) => {
271
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
272
+ rl.question(' Reconfigure LLM settings? (Y/n): ', (ans) => { rl.close(); resolve(ans.trim().toLowerCase()); });
273
+ });
274
+ if (!answer || answer === 'y' || answer === 'yes') {
275
+ // Clear saved config so next run triggers interactive setup
276
+ await saveCLIConfig({});
277
+ console.log(' Config cleared. Run `gitnexus wiki` again to reconfigure.\n');
278
+ }
279
+ }
280
+ }
281
+ else {
282
+ console.log(`\n Error: ${err.message}\n`);
283
+ if (process.env.DEBUG) {
284
+ console.error(err);
285
+ }
286
+ }
287
+ process.exitCode = 1;
288
+ }
289
+ };
290
+ // ─── Gist Publishing ───────────────────────────────────────────────────
291
+ function hasGhCLI() {
292
+ try {
293
+ execSync('gh --version', { stdio: 'ignore' });
294
+ return true;
295
+ }
296
+ catch {
297
+ return false;
298
+ }
299
+ }
300
+ function publishGist(htmlPath) {
301
+ try {
302
+ const output = execFileSync('gh', [
303
+ 'gist', 'create', htmlPath,
304
+ '--desc', 'Repository Wiki — generated by GitNexus',
305
+ '--public',
306
+ ], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
307
+ // gh gist create prints the gist URL as the last line
308
+ const lines = output.split('\n');
309
+ const gistUrl = lines.find(l => l.includes('gist.github.com')) || lines[lines.length - 1];
310
+ if (!gistUrl || !gistUrl.includes('gist.github.com'))
311
+ return null;
312
+ // Build a raw viewer URL via gist.githack.com
313
+ // gist URL format: https://gist.github.com/{user}/{id}
314
+ const match = gistUrl.match(/gist\.github\.com\/([^/]+)\/([a-f0-9]+)/);
315
+ let rawUrl = gistUrl;
316
+ if (match) {
317
+ rawUrl = `https://gistcdn.githack.com/${match[1]}/${match[2]}/raw/index.html`;
318
+ }
319
+ return { url: gistUrl.trim(), rawUrl };
320
+ }
321
+ catch {
322
+ return null;
323
+ }
324
+ }
325
+ async function maybePublishGist(htmlPath, gistFlag) {
326
+ if (gistFlag === false)
327
+ return;
328
+ // Check that the HTML file exists
329
+ try {
330
+ const fs = await import('fs/promises');
331
+ await fs.access(htmlPath);
332
+ }
333
+ catch {
334
+ return;
335
+ }
336
+ if (!hasGhCLI()) {
337
+ if (gistFlag) {
338
+ console.log(' GitHub CLI (gh) is not installed. Cannot publish gist.');
339
+ console.log(' Install it: https://cli.github.com\n');
340
+ }
341
+ return;
342
+ }
343
+ let shouldPublish = !!gistFlag;
344
+ if (!shouldPublish && process.stdin.isTTY) {
345
+ const answer = await new Promise((resolve) => {
346
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
347
+ rl.question(' Publish wiki as a GitHub Gist for easy viewing? (Y/n): ', (ans) => {
348
+ rl.close();
349
+ resolve(ans.trim().toLowerCase());
350
+ });
351
+ });
352
+ shouldPublish = !answer || answer === 'y' || answer === 'yes';
353
+ }
354
+ if (!shouldPublish)
355
+ return;
356
+ console.log('\n Publishing to GitHub Gist...');
357
+ const result = publishGist(htmlPath);
358
+ if (result) {
359
+ console.log(` Gist: ${result.url}`);
360
+ console.log(` Viewer: ${result.rawUrl}\n`);
361
+ }
362
+ else {
363
+ console.log(' Failed to publish gist. Make sure `gh auth login` is configured.\n');
364
+ }
365
+ }
@@ -0,0 +1,26 @@
1
+ import { type Ignore } from 'ignore';
2
+ import type { Path } from 'path-scurry';
3
+ export declare const shouldIgnorePath: (filePath: string) => boolean;
4
+ /** Check if a directory name is in the hardcoded ignore list */
5
+ export declare const isHardcodedIgnoredDirectory: (name: string) => boolean;
6
+ /**
7
+ * Load .gitignore and .gitnexusignore rules from the repo root.
8
+ * Returns an `ignore` instance with all patterns, or null if no files found.
9
+ */
10
+ export interface IgnoreOptions {
11
+ /** Skip .gitignore parsing, only read .gitnexusignore. Defaults to GITNEXUS_NO_GITIGNORE env var. */
12
+ noGitignore?: boolean;
13
+ }
14
+ export declare const loadIgnoreRules: (repoPath: string, options?: IgnoreOptions) => Promise<Ignore | null>;
15
+ /**
16
+ * Create a glob-compatible ignore filter combining:
17
+ * - .gitignore / .gitnexusignore patterns (via `ignore` package)
18
+ * - Hardcoded DEFAULT_IGNORE_LIST, IGNORED_EXTENSIONS, IGNORED_FILES
19
+ *
20
+ * Returns an IgnoreLike object for glob's `ignore` option,
21
+ * enabling directory-level pruning during traversal.
22
+ */
23
+ export declare const createIgnoreFilter: (repoPath: string, options?: IgnoreOptions) => Promise<{
24
+ ignored(p: Path): boolean;
25
+ childrenIgnored(p: Path): boolean;
26
+ }>;