gitnexus 1.2.0 → 1.2.2

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/index.js CHANGED
@@ -15,7 +15,7 @@ const program = new Command();
15
15
  program
16
16
  .name('gitnexus')
17
17
  .description('GitNexus local CLI and MCP server')
18
- .version('1.1.9');
18
+ .version('1.2.0');
19
19
  program
20
20
  .command('setup')
21
21
  .description('One-time setup: configure MCP for Cursor, Claude Code, OpenCode')
@@ -56,6 +56,7 @@ program
56
56
  .option('--model <model>', 'LLM model name (default: gpt-4o-mini)')
57
57
  .option('--base-url <url>', 'LLM API base URL (default: OpenAI)')
58
58
  .option('--api-key <key>', 'LLM API key (saved to ~/.gitnexus/config.json)')
59
+ .option('--gist', 'Publish wiki as a public GitHub Gist after generation')
59
60
  .action(wikiCommand);
60
61
  program
61
62
  .command('augment <pattern>')
@@ -9,5 +9,6 @@ export interface WikiCommandOptions {
9
9
  model?: string;
10
10
  baseUrl?: string;
11
11
  apiKey?: string;
12
+ gist?: boolean;
12
13
  }
13
14
  export declare const wikiCommand: (inputPath?: string, options?: WikiCommandOptions) => Promise<void>;
package/dist/cli/wiki.js CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import path from 'path';
8
8
  import readline from 'readline';
9
+ import { execSync } from 'child_process';
9
10
  import cliProgress from 'cli-progress';
10
11
  import { getGitRoot, isGitRepo } from '../storage/git.js';
11
12
  import { getStoragePaths, loadMeta, loadCLIConfig, saveCLIConfig } from '../storage/repo-manager.js';
@@ -94,11 +95,18 @@ export const wikiCommand = async (inputPath, options) => {
94
95
  return;
95
96
  }
96
97
  // ── Resolve LLM config (with interactive fallback) ─────────────────
97
- // If --api-key was passed via CLI, save it immediately
98
- if (options?.apiKey) {
98
+ // Save any CLI overrides immediately
99
+ if (options?.apiKey || options?.model || options?.baseUrl) {
99
100
  const existing = await loadCLIConfig();
100
- await saveCLIConfig({ ...existing, apiKey: options.apiKey });
101
- console.log(' API key saved to ~/.gitnexus/config.json\n');
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');
102
110
  }
103
111
  let llmConfig = await resolveLLMConfig({
104
112
  model: options?.model,
@@ -113,25 +121,52 @@ export const wikiCommand = async (inputPath, options) => {
113
121
  process.exitCode = 1;
114
122
  return;
115
123
  }
116
- console.log(' No API key configured.\n');
117
- console.log(' The wiki command requires an LLM API key (OpenAI-compatible).');
118
- console.log(' You can also set OPENAI_API_KEY or GITNEXUS_API_KEY env var.\n');
119
- const key = await prompt(' Enter your API key: ', true);
124
+ console.log(' No LLM configured. Let\'s set it up.\n');
125
+ console.log(' Supports OpenAI, OpenRouter, or any OpenAI-compatible API.\n');
126
+ // Provider selection
127
+ console.log(' [1] OpenAI (api.openai.com)');
128
+ console.log(' [2] OpenRouter (openrouter.ai)');
129
+ console.log(' [3] Custom endpoint\n');
130
+ const choice = await prompt(' Select provider (1/2/3): ');
131
+ let baseUrl;
132
+ let defaultModel;
133
+ if (choice === '2') {
134
+ baseUrl = 'https://openrouter.ai/api/v1';
135
+ defaultModel = 'openai/gpt-4o-mini';
136
+ }
137
+ else if (choice === '3') {
138
+ baseUrl = await prompt(' Base URL (e.g. http://localhost:11434/v1): ');
139
+ if (!baseUrl) {
140
+ console.log('\n No URL provided. Aborting.\n');
141
+ process.exitCode = 1;
142
+ return;
143
+ }
144
+ defaultModel = 'gpt-4o-mini';
145
+ }
146
+ else {
147
+ baseUrl = 'https://api.openai.com/v1';
148
+ defaultModel = 'gpt-4o-mini';
149
+ }
150
+ // Model
151
+ const modelInput = await prompt(` Model (default: ${defaultModel}): `);
152
+ const model = modelInput || defaultModel;
153
+ // API key
154
+ const key = await prompt(' API key: ', true);
120
155
  if (!key) {
121
156
  console.log('\n No key provided. Aborting.\n');
122
157
  process.exitCode = 1;
123
158
  return;
124
159
  }
125
- const save = await prompt(' Save key to ~/.gitnexus/config.json for future use? (Y/n): ');
160
+ // Save
161
+ const save = await prompt('\n Save config to ~/.gitnexus/config.json? (Y/n): ');
126
162
  if (!save || save.toLowerCase() === 'y' || save.toLowerCase() === 'yes') {
127
- const existing = await loadCLIConfig();
128
- await saveCLIConfig({ ...existing, apiKey: key });
129
- console.log(' Key saved.\n');
163
+ await saveCLIConfig({ apiKey: key, baseUrl, model });
164
+ console.log(' Config saved.\n');
130
165
  }
131
166
  else {
132
- console.log(' Key will be used for this session only.\n');
167
+ console.log(' Config will be used for this session only.\n');
133
168
  }
134
- llmConfig = { ...llmConfig, apiKey: key };
169
+ llmConfig = { ...llmConfig, apiKey: key, baseUrl, model };
135
170
  }
136
171
  // ── Setup progress bar ──────────────────────────────────────────────
137
172
  const bar = new cliProgress.SingleBar({
@@ -160,17 +195,19 @@ export const wikiCommand = async (inputPath, options) => {
160
195
  bar.update(100, { phase: 'Done' });
161
196
  bar.stop();
162
197
  const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
198
+ const wikiDir = path.join(storagePath, 'wiki');
199
+ const viewerPath = path.join(wikiDir, 'index.html');
163
200
  if (result.mode === 'up-to-date' && !options?.force) {
164
201
  console.log('\n Wiki is already up to date.');
165
- console.log(` ${path.join(storagePath, 'wiki')}\n`);
202
+ console.log(` Viewer: ${viewerPath}\n`);
203
+ await maybePublishGist(viewerPath, options?.gist);
166
204
  return;
167
205
  }
168
- const wikiDir = path.join(storagePath, 'wiki');
169
206
  console.log(`\n Wiki generated successfully (${elapsed}s)\n`);
170
207
  console.log(` Mode: ${result.mode}`);
171
208
  console.log(` Pages: ${result.pagesGenerated}`);
172
209
  console.log(` Output: ${wikiDir}`);
173
- console.log(` Viewer: ${path.join(wikiDir, 'index.html')}`);
210
+ console.log(` Viewer: ${viewerPath}`);
174
211
  if (result.failedModules && result.failedModules.length > 0) {
175
212
  console.log(`\n Failed modules (${result.failedModules.length}):`);
176
213
  for (const mod of result.failedModules) {
@@ -179,6 +216,7 @@ export const wikiCommand = async (inputPath, options) => {
179
216
  console.log(' Re-run to retry failed modules (pages will be regenerated).');
180
217
  }
181
218
  console.log('');
219
+ await maybePublishGist(viewerPath, options?.gist);
182
220
  }
183
221
  catch (err) {
184
222
  bar.stop();
@@ -197,3 +235,75 @@ export const wikiCommand = async (inputPath, options) => {
197
235
  process.exitCode = 1;
198
236
  }
199
237
  };
238
+ // ─── Gist Publishing ───────────────────────────────────────────────────
239
+ function hasGhCLI() {
240
+ try {
241
+ execSync('gh --version', { stdio: 'ignore' });
242
+ return true;
243
+ }
244
+ catch {
245
+ return false;
246
+ }
247
+ }
248
+ function publishGist(htmlPath) {
249
+ try {
250
+ const output = execSync(`gh gist create "${htmlPath}" --desc "Repository Wiki — generated by GitNexus" --public`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
251
+ // gh gist create prints the gist URL as the last line
252
+ const lines = output.split('\n');
253
+ const gistUrl = lines.find(l => l.includes('gist.github.com')) || lines[lines.length - 1];
254
+ if (!gistUrl || !gistUrl.includes('gist.github.com'))
255
+ return null;
256
+ // Build a raw viewer URL via gist.githack.com
257
+ // gist URL format: https://gist.github.com/{user}/{id}
258
+ const match = gistUrl.match(/gist\.github\.com\/([^/]+)\/([a-f0-9]+)/);
259
+ let rawUrl = gistUrl;
260
+ if (match) {
261
+ rawUrl = `https://gistcdn.githack.com/${match[1]}/${match[2]}/raw/index.html`;
262
+ }
263
+ return { url: gistUrl.trim(), rawUrl };
264
+ }
265
+ catch {
266
+ return null;
267
+ }
268
+ }
269
+ async function maybePublishGist(htmlPath, gistFlag) {
270
+ if (gistFlag === false)
271
+ return;
272
+ // Check that the HTML file exists
273
+ try {
274
+ const fs = await import('fs/promises');
275
+ await fs.access(htmlPath);
276
+ }
277
+ catch {
278
+ return;
279
+ }
280
+ if (!hasGhCLI()) {
281
+ if (gistFlag) {
282
+ console.log(' GitHub CLI (gh) is not installed. Cannot publish gist.');
283
+ console.log(' Install it: https://cli.github.com\n');
284
+ }
285
+ return;
286
+ }
287
+ let shouldPublish = !!gistFlag;
288
+ if (!shouldPublish && process.stdin.isTTY) {
289
+ const answer = await new Promise((resolve) => {
290
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
291
+ rl.question(' Publish wiki as a GitHub Gist for easy viewing? (Y/n): ', (ans) => {
292
+ rl.close();
293
+ resolve(ans.trim().toLowerCase());
294
+ });
295
+ });
296
+ shouldPublish = !answer || answer === 'y' || answer === 'yes';
297
+ }
298
+ if (!shouldPublish)
299
+ return;
300
+ console.log('\n Publishing to GitHub Gist...');
301
+ const result = publishGist(htmlPath);
302
+ if (result) {
303
+ console.log(` Gist: ${result.url}`);
304
+ console.log(` Viewer: ${result.rawUrl}\n`);
305
+ }
306
+ else {
307
+ console.log(' Failed to publish gist. Make sure `gh auth login` is configured.\n');
308
+ }
309
+ }
@@ -50,6 +50,7 @@ export declare class WikiGenerator {
50
50
  mode: 'full' | 'incremental' | 'up-to-date';
51
51
  failedModules: string[];
52
52
  }>;
53
+ private ensureHTMLViewer;
53
54
  private fullGeneration;
54
55
  private buildModuleTree;
55
56
  /**
@@ -51,6 +51,8 @@ export class WikiGenerator {
51
51
  const forceMode = this.options.force;
52
52
  // Up-to-date check (skip if --force)
53
53
  if (!forceMode && existingMeta && existingMeta.fromCommit === currentCommit) {
54
+ // Still regenerate the HTML viewer in case it's missing
55
+ await this.ensureHTMLViewer();
54
56
  return { pagesGenerated: 0, mode: 'up-to-date', failedModules: [] };
55
57
  }
56
58
  // Force mode: delete snapshot to force full re-grouping
@@ -85,14 +87,21 @@ export class WikiGenerator {
85
87
  finally {
86
88
  await closeWikiDb();
87
89
  }
88
- // Generate self-contained HTML viewer
89
- if (result.pagesGenerated > 0) {
90
- this.onProgress('html', 98, 'Building HTML viewer...');
91
- const repoName = path.basename(this.repoPath);
92
- await generateHTMLViewer(this.wikiDir, repoName);
93
- }
90
+ // Always generate the HTML viewer after wiki content changes
91
+ await this.ensureHTMLViewer();
94
92
  return result;
95
93
  }
94
+ // ─── HTML Viewer ─────────────────────────────────────────────────────
95
+ async ensureHTMLViewer() {
96
+ // Only generate if there are markdown pages to bundle
97
+ const dirEntries = await fs.readdir(this.wikiDir).catch(() => []);
98
+ const hasMd = dirEntries.some(f => f.endsWith('.md'));
99
+ if (!hasMd)
100
+ return;
101
+ this.onProgress('html', 98, 'Building HTML viewer...');
102
+ const repoName = path.basename(this.repoPath);
103
+ await generateHTMLViewer(this.wikiDir, repoName);
104
+ }
96
105
  // ─── Full Generation ────────────────────────────────────────────────
97
106
  async fullGeneration(currentCommit) {
98
107
  let pagesGenerated = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
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",