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 +2 -1
- package/dist/cli/wiki.d.ts +1 -0
- package/dist/cli/wiki.js +127 -17
- package/dist/core/wiki/generator.d.ts +1 -0
- package/dist/core/wiki/generator.js +15 -6
- package/package.json +1 -1
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.
|
|
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>')
|
package/dist/cli/wiki.d.ts
CHANGED
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
|
-
//
|
|
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
|
-
|
|
101
|
-
|
|
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
|
|
117
|
-
console.log('
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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('
|
|
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(` ${
|
|
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: ${
|
|
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
|
+
}
|
|
@@ -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
|
-
//
|
|
89
|
-
|
|
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