mkpr-cli 1.0.2 → 1.0.3
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/package.json +1 -2
- package/src/index.js +678 -398
package/src/index.js
CHANGED
|
@@ -5,41 +5,29 @@ const inquirer = require('inquirer');
|
|
|
5
5
|
const chalk = require('chalk');
|
|
6
6
|
const ora = require('ora');
|
|
7
7
|
const Conf = require('conf');
|
|
8
|
-
const fetch = require('node-fetch');
|
|
9
8
|
const { execSync } = require('child_process');
|
|
9
|
+
const { randomUUID } = require('crypto');
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
|
|
13
13
|
// ============================================
|
|
14
|
-
//
|
|
14
|
+
// FETCH COMPATIBILITY (Node 18+ native or node-fetch@2)
|
|
15
15
|
// ============================================
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
'yarn.lock',
|
|
27
|
-
'pnpm-lock.yaml',
|
|
28
|
-
'bun.lockb',
|
|
29
|
-
'composer.lock',
|
|
30
|
-
'Gemfile.lock',
|
|
31
|
-
'poetry.lock',
|
|
32
|
-
'Cargo.lock',
|
|
33
|
-
'pubspec.lock',
|
|
34
|
-
'packages.lock.json',
|
|
35
|
-
'gradle.lockfile',
|
|
36
|
-
'flake.lock'
|
|
37
|
-
]
|
|
17
|
+
let fetch;
|
|
18
|
+
if (globalThis.fetch) {
|
|
19
|
+
fetch = globalThis.fetch;
|
|
20
|
+
} else {
|
|
21
|
+
try {
|
|
22
|
+
fetch = require('node-fetch');
|
|
23
|
+
} catch {
|
|
24
|
+
console.error(chalk.red('❌ fetch not available. Use Node 18+ or install node-fetch@2'));
|
|
25
|
+
process.exit(1);
|
|
38
26
|
}
|
|
39
|
-
}
|
|
27
|
+
}
|
|
40
28
|
|
|
41
29
|
// ============================================
|
|
42
|
-
//
|
|
30
|
+
// CONSTANTS (Single source of truth)
|
|
43
31
|
// ============================================
|
|
44
32
|
|
|
45
33
|
const DEFAULT_EXCLUDES = [
|
|
@@ -86,10 +74,6 @@ const FIXED_EXCLUDE_PATTERNS = [
|
|
|
86
74
|
'.yarn/install-state.gz'
|
|
87
75
|
];
|
|
88
76
|
|
|
89
|
-
// ============================================
|
|
90
|
-
// PR CHANGE TYPES
|
|
91
|
-
// ============================================
|
|
92
|
-
|
|
93
77
|
const PR_TYPES = [
|
|
94
78
|
'feature', // New feature
|
|
95
79
|
'fix', // Bug fix
|
|
@@ -99,10 +83,101 @@ const PR_TYPES = [
|
|
|
99
83
|
'chore', // Maintenance
|
|
100
84
|
'perf', // Performance improvement
|
|
101
85
|
'style', // Style/formatting changes
|
|
102
|
-
'ci'
|
|
103
|
-
'breaking' // Breaking change
|
|
86
|
+
'ci' // CI/CD
|
|
104
87
|
];
|
|
105
88
|
|
|
89
|
+
const FETCH_TIMEOUT_MS = 180000; // 3 minutes for PR generation (larger context)
|
|
90
|
+
const MAX_DIFF_LENGTH = 8000;
|
|
91
|
+
const MAX_BUFFER_SIZE = 1024 * 1024 * 20; // 20MB for large PRs
|
|
92
|
+
|
|
93
|
+
// ============================================
|
|
94
|
+
// CONFIGURATION
|
|
95
|
+
// ============================================
|
|
96
|
+
|
|
97
|
+
const config = new Conf({
|
|
98
|
+
projectName: 'mkpr',
|
|
99
|
+
defaults: {
|
|
100
|
+
ollamaPort: 11434,
|
|
101
|
+
ollamaModel: 'llama3.2',
|
|
102
|
+
baseBranch: 'main',
|
|
103
|
+
outputDir: '.',
|
|
104
|
+
excludeFiles: [...DEFAULT_EXCLUDES],
|
|
105
|
+
debug: false
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ============================================
|
|
110
|
+
// UTILITY FUNCTIONS
|
|
111
|
+
// ============================================
|
|
112
|
+
|
|
113
|
+
function debugLog(...args) {
|
|
114
|
+
if (config.get('debug')) {
|
|
115
|
+
console.log(chalk.gray('[DEBUG]'), ...args);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatSize(bytes) {
|
|
120
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
121
|
+
if (bytes === 0) return '0 B';
|
|
122
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
123
|
+
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Fetch with timeout using AbortController
|
|
128
|
+
*/
|
|
129
|
+
async function fetchWithTimeout(url, options = {}, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
130
|
+
const controller = new AbortController();
|
|
131
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const response = await fetch(url, {
|
|
135
|
+
...options,
|
|
136
|
+
signal: controller.signal
|
|
137
|
+
});
|
|
138
|
+
return response;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error.name === 'AbortError') {
|
|
141
|
+
throw new Error(`Request timeout after ${timeoutMs / 1000}s. The model may be too slow or Ollama is unresponsive.`);
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
} finally {
|
|
145
|
+
clearTimeout(timeout);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Sanitize branch name for safe filesystem usage
|
|
151
|
+
*/
|
|
152
|
+
function sanitizeBranchName(branchName) {
|
|
153
|
+
return branchName
|
|
154
|
+
.replace(/[\/\\:*?"<>|]/g, '_') // Invalid filesystem chars
|
|
155
|
+
.replace(/\s+/g, '_') // Spaces
|
|
156
|
+
.replace(/^\.+/, '') // Leading dots
|
|
157
|
+
.replace(/\.+$/, '') // Trailing dots
|
|
158
|
+
.replace(/_+/g, '_') // Multiple underscores
|
|
159
|
+
.substring(0, 100); // Max length
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validate branch name to prevent command injection
|
|
164
|
+
*/
|
|
165
|
+
function isValidBranchName(branchName) {
|
|
166
|
+
// Git branch names can't contain: space, ~, ^, :, ?, *, [, \, or start with -
|
|
167
|
+
// Also reject anything that looks like command injection
|
|
168
|
+
const invalidPatterns = [
|
|
169
|
+
/\s/, // spaces
|
|
170
|
+
/[~^:?*\[\]\\]/, // git invalid chars
|
|
171
|
+
/^-/, // starts with dash
|
|
172
|
+
/\.\./, // double dots
|
|
173
|
+
/\/\//, // double slashes
|
|
174
|
+
/[@{}$`|;&]/, // shell dangerous chars
|
|
175
|
+
/^$/ // empty
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
return !invalidPatterns.some(pattern => pattern.test(branchName));
|
|
179
|
+
}
|
|
180
|
+
|
|
106
181
|
// ============================================
|
|
107
182
|
// JSON SCHEMA FOR PR
|
|
108
183
|
// ============================================
|
|
@@ -169,7 +244,6 @@ PR TYPES:
|
|
|
169
244
|
- perf: Performance improvements
|
|
170
245
|
- style: Code style/formatting changes
|
|
171
246
|
- ci: CI/CD configuration changes
|
|
172
|
-
- breaking: Changes that break backward compatibility
|
|
173
247
|
|
|
174
248
|
OUTPUT FORMAT:
|
|
175
249
|
Respond ONLY with a valid JSON object matching this schema:
|
|
@@ -214,18 +288,18 @@ Output: {
|
|
|
214
288
|
|
|
215
289
|
function buildUserPrompt(context) {
|
|
216
290
|
const { currentBranch, baseBranch, diff, commits, changedFiles, stats } = context;
|
|
217
|
-
|
|
291
|
+
|
|
218
292
|
const filesSummary = changedFiles
|
|
219
293
|
.map(f => `${f.status[0].toUpperCase()} ${f.file}`)
|
|
220
294
|
.join('\n');
|
|
221
|
-
|
|
295
|
+
|
|
222
296
|
const commitsSummary = commits
|
|
223
297
|
.slice(0, 20)
|
|
224
298
|
.join('\n');
|
|
225
|
-
|
|
299
|
+
|
|
226
300
|
// Smart diff truncation
|
|
227
|
-
const truncatedDiff = truncateDiffSmart(diff
|
|
228
|
-
|
|
301
|
+
const truncatedDiff = truncateDiffSmart(diff);
|
|
302
|
+
|
|
229
303
|
return `BRANCH INFO:
|
|
230
304
|
Current branch: ${currentBranch}
|
|
231
305
|
Base branch: ${baseBranch}
|
|
@@ -246,37 +320,66 @@ ${truncatedDiff}
|
|
|
246
320
|
Generate a PR description for these changes. Respond with JSON only.`;
|
|
247
321
|
}
|
|
248
322
|
|
|
249
|
-
|
|
250
|
-
|
|
323
|
+
/**
|
|
324
|
+
* Smart diff truncation that preserves file context
|
|
325
|
+
*/
|
|
326
|
+
function truncateDiffSmart(diff) {
|
|
327
|
+
if (diff.length <= MAX_DIFF_LENGTH) {
|
|
251
328
|
return diff;
|
|
252
329
|
}
|
|
253
|
-
|
|
330
|
+
|
|
254
331
|
const lines = diff.split('\n');
|
|
255
|
-
const
|
|
256
|
-
let
|
|
257
|
-
|
|
332
|
+
const chunks = [];
|
|
333
|
+
let currentChunk = { header: '', file: '', lines: [] };
|
|
334
|
+
|
|
258
335
|
for (const line of lines) {
|
|
259
|
-
//
|
|
260
|
-
if (line.startsWith('diff --git')
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
line.startsWith('+') ||
|
|
264
|
-
line.startsWith('-') ||
|
|
265
|
-
line.startsWith('@@')) {
|
|
266
|
-
|
|
267
|
-
if (currentLength + line.length < maxLength) {
|
|
268
|
-
importantLines.push(line);
|
|
269
|
-
currentLength += line.length + 1;
|
|
336
|
+
// New file header
|
|
337
|
+
if (line.startsWith('diff --git')) {
|
|
338
|
+
if (currentChunk.header) {
|
|
339
|
+
chunks.push(currentChunk);
|
|
270
340
|
}
|
|
341
|
+
const match = line.match(/diff --git a\/(.+) b\/(.+)/);
|
|
342
|
+
currentChunk = {
|
|
343
|
+
header: line,
|
|
344
|
+
file: match ? match[2] : '',
|
|
345
|
+
lines: []
|
|
346
|
+
};
|
|
347
|
+
} else if (line.startsWith('+++') || line.startsWith('---') || line.startsWith('@@')) {
|
|
348
|
+
currentChunk.lines.push(line);
|
|
349
|
+
} else if ((line.startsWith('+') || line.startsWith('-')) &&
|
|
350
|
+
!line.startsWith('+++') && !line.startsWith('---')) {
|
|
351
|
+
currentChunk.lines.push(line);
|
|
271
352
|
}
|
|
272
353
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (
|
|
276
|
-
|
|
354
|
+
|
|
355
|
+
// Don't forget last chunk
|
|
356
|
+
if (currentChunk.header) {
|
|
357
|
+
chunks.push(currentChunk);
|
|
277
358
|
}
|
|
278
|
-
|
|
279
|
-
|
|
359
|
+
|
|
360
|
+
// Build truncated diff prioritizing all files with some changes
|
|
361
|
+
const result = [];
|
|
362
|
+
const maxLinesPerFile = Math.max(10, Math.floor(MAX_DIFF_LENGTH / (chunks.length || 1) / 60));
|
|
363
|
+
let totalLength = 0;
|
|
364
|
+
|
|
365
|
+
for (const chunk of chunks) {
|
|
366
|
+
if (totalLength > MAX_DIFF_LENGTH) {
|
|
367
|
+
result.push(`\n[... ${chunks.length - result.length} more files not shown ...]`);
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
result.push(chunk.header);
|
|
372
|
+
const importantLines = chunk.lines.slice(0, maxLinesPerFile);
|
|
373
|
+
result.push(...importantLines);
|
|
374
|
+
|
|
375
|
+
if (chunk.lines.length > importantLines.length) {
|
|
376
|
+
result.push(` ... (${chunk.lines.length - importantLines.length} more lines in ${chunk.file})`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
totalLength = result.join('\n').length;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return result.join('\n');
|
|
280
383
|
}
|
|
281
384
|
|
|
282
385
|
// ============================================
|
|
@@ -286,12 +389,14 @@ function truncateDiffSmart(diff, maxLength) {
|
|
|
286
389
|
async function generatePRDescriptionText(context) {
|
|
287
390
|
const port = config.get('ollamaPort');
|
|
288
391
|
const model = config.get('ollamaModel');
|
|
289
|
-
|
|
392
|
+
|
|
290
393
|
const systemPrompt = buildSystemPrompt();
|
|
291
394
|
const userPrompt = buildUserPrompt(context);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
395
|
+
|
|
396
|
+
debugLog('Sending request to Ollama...');
|
|
397
|
+
debugLog(`Model: ${model}, Port: ${port}`);
|
|
398
|
+
|
|
399
|
+
const response = await fetchWithTimeout(`http://localhost:${port}/api/chat`, {
|
|
295
400
|
method: 'POST',
|
|
296
401
|
headers: { 'Content-Type': 'application/json' },
|
|
297
402
|
body: JSON.stringify({
|
|
@@ -309,46 +414,51 @@ async function generatePRDescriptionText(context) {
|
|
|
309
414
|
}
|
|
310
415
|
})
|
|
311
416
|
});
|
|
312
|
-
|
|
417
|
+
|
|
313
418
|
if (!response.ok) {
|
|
314
419
|
const errorText = await response.text();
|
|
315
|
-
throw new Error(`Ollama error: ${errorText}`);
|
|
420
|
+
throw new Error(`Ollama error (${response.status}): ${errorText}`);
|
|
316
421
|
}
|
|
317
|
-
|
|
422
|
+
|
|
318
423
|
const data = await response.json();
|
|
319
424
|
const rawResponse = data.message?.content || data.response || '';
|
|
320
|
-
|
|
321
|
-
|
|
425
|
+
|
|
426
|
+
debugLog('Raw response:', rawResponse.substring(0, 500) + '...');
|
|
427
|
+
|
|
322
428
|
const prData = parsePRResponse(rawResponse);
|
|
323
|
-
|
|
324
|
-
// Format to Markdown
|
|
325
429
|
return formatPRMarkdown(prData, context);
|
|
326
430
|
}
|
|
327
431
|
|
|
328
432
|
function parsePRResponse(rawResponse) {
|
|
329
433
|
let jsonStr = rawResponse.trim();
|
|
330
|
-
|
|
434
|
+
|
|
331
435
|
// Clean artifacts
|
|
332
436
|
jsonStr = jsonStr.replace(/^```json\s*/i, '');
|
|
333
437
|
jsonStr = jsonStr.replace(/^```\s*/i, '');
|
|
334
438
|
jsonStr = jsonStr.replace(/```\s*$/i, '');
|
|
335
439
|
jsonStr = jsonStr.trim();
|
|
336
|
-
|
|
440
|
+
|
|
337
441
|
// Extract JSON if there's text before/after
|
|
338
442
|
const jsonMatch = jsonStr.match(/\{[\s\S]*\}/);
|
|
339
443
|
if (jsonMatch) {
|
|
340
444
|
jsonStr = jsonMatch[0];
|
|
341
445
|
}
|
|
342
|
-
|
|
446
|
+
|
|
343
447
|
try {
|
|
344
448
|
const parsed = JSON.parse(jsonStr);
|
|
345
|
-
|
|
346
|
-
// Validate required fields
|
|
347
|
-
if (!parsed.title ||
|
|
348
|
-
throw new Error('Missing
|
|
449
|
+
|
|
450
|
+
// Validate required fields with type checking
|
|
451
|
+
if (!parsed.title || typeof parsed.title !== 'string') {
|
|
452
|
+
throw new Error('Missing or invalid "title" field');
|
|
453
|
+
}
|
|
454
|
+
if (!parsed.type || typeof parsed.type !== 'string') {
|
|
455
|
+
throw new Error('Missing or invalid "type" field');
|
|
456
|
+
}
|
|
457
|
+
if (!parsed.summary || typeof parsed.summary !== 'string') {
|
|
458
|
+
throw new Error('Missing or invalid "summary" field');
|
|
349
459
|
}
|
|
350
|
-
|
|
351
|
-
// Validate type
|
|
460
|
+
|
|
461
|
+
// Validate and correct type
|
|
352
462
|
if (!PR_TYPES.includes(parsed.type)) {
|
|
353
463
|
const typeMap = {
|
|
354
464
|
'feat': 'feature',
|
|
@@ -360,31 +470,54 @@ function parsePRResponse(rawResponse) {
|
|
|
360
470
|
'testing': 'test',
|
|
361
471
|
'performance': 'perf',
|
|
362
472
|
'maintenance': 'chore',
|
|
363
|
-
'build': 'chore'
|
|
473
|
+
'build': 'chore',
|
|
474
|
+
'breaking': 'feature' // Breaking is indicated in breaking_changes array
|
|
364
475
|
};
|
|
365
476
|
parsed.type = typeMap[parsed.type.toLowerCase()] || 'chore';
|
|
366
477
|
}
|
|
367
|
-
|
|
368
|
-
// Ensure arrays
|
|
478
|
+
|
|
479
|
+
// Ensure arrays with type checking
|
|
369
480
|
if (!Array.isArray(parsed.changes)) {
|
|
370
|
-
|
|
481
|
+
if (typeof parsed.changes === 'string') {
|
|
482
|
+
parsed.changes = [parsed.changes];
|
|
483
|
+
} else {
|
|
484
|
+
parsed.changes = [];
|
|
485
|
+
}
|
|
371
486
|
}
|
|
487
|
+
parsed.changes = parsed.changes.filter(c => typeof c === 'string' && c.trim());
|
|
488
|
+
|
|
372
489
|
if (!Array.isArray(parsed.breaking_changes)) {
|
|
373
|
-
|
|
490
|
+
if (typeof parsed.breaking_changes === 'string' && parsed.breaking_changes.trim()) {
|
|
491
|
+
parsed.breaking_changes = [parsed.breaking_changes];
|
|
492
|
+
} else {
|
|
493
|
+
parsed.breaking_changes = [];
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
parsed.breaking_changes = parsed.breaking_changes.filter(c => typeof c === 'string' && c.trim());
|
|
497
|
+
|
|
498
|
+
// Clean optional string fields
|
|
499
|
+
if (parsed.testing && typeof parsed.testing !== 'string') {
|
|
500
|
+
parsed.testing = '';
|
|
501
|
+
}
|
|
502
|
+
if (parsed.notes && typeof parsed.notes !== 'string') {
|
|
503
|
+
parsed.notes = '';
|
|
374
504
|
}
|
|
375
|
-
|
|
505
|
+
|
|
506
|
+
// Truncate title if too long
|
|
507
|
+
parsed.title = parsed.title.substring(0, 72);
|
|
508
|
+
|
|
376
509
|
return parsed;
|
|
377
|
-
|
|
510
|
+
|
|
378
511
|
} catch (parseError) {
|
|
379
512
|
console.log(chalk.yellow('\n⚠️ Could not parse JSON, using fallback...'));
|
|
513
|
+
debugLog('Parse error:', parseError.message);
|
|
380
514
|
return extractPRFromText(rawResponse);
|
|
381
515
|
}
|
|
382
516
|
}
|
|
383
517
|
|
|
384
518
|
function extractPRFromText(text) {
|
|
385
|
-
// Fallback for when the model doesn't return valid JSON
|
|
386
519
|
const lines = text.split('\n').filter(l => l.trim());
|
|
387
|
-
|
|
520
|
+
|
|
388
521
|
return {
|
|
389
522
|
title: lines[0]?.substring(0, 72) || 'Update code',
|
|
390
523
|
type: 'chore',
|
|
@@ -400,9 +533,9 @@ function extractPRFromText(text) {
|
|
|
400
533
|
function formatPRMarkdown(prData, context) {
|
|
401
534
|
const { title, type, summary, changes, breaking_changes, testing, notes } = prData;
|
|
402
535
|
const { currentBranch, baseBranch, changedFiles, commits } = context;
|
|
403
|
-
|
|
536
|
+
|
|
404
537
|
let md = `# ${title}\n\n`;
|
|
405
|
-
|
|
538
|
+
|
|
406
539
|
// Type badge
|
|
407
540
|
const typeEmoji = {
|
|
408
541
|
'feature': '✨',
|
|
@@ -413,16 +546,15 @@ function formatPRMarkdown(prData, context) {
|
|
|
413
546
|
'chore': '🔧',
|
|
414
547
|
'perf': '⚡',
|
|
415
548
|
'style': '💄',
|
|
416
|
-
'ci': '👷'
|
|
417
|
-
'breaking': '💥'
|
|
549
|
+
'ci': '👷'
|
|
418
550
|
};
|
|
419
|
-
|
|
551
|
+
|
|
420
552
|
md += `**Type:** ${typeEmoji[type] || '📦'} \`${type}\`\n\n`;
|
|
421
553
|
md += `**Branch:** \`${currentBranch}\` → \`${baseBranch}\`\n\n`;
|
|
422
|
-
|
|
554
|
+
|
|
423
555
|
// Description
|
|
424
556
|
md += `## Description\n\n${summary}\n\n`;
|
|
425
|
-
|
|
557
|
+
|
|
426
558
|
// Changes
|
|
427
559
|
md += `## Changes\n\n`;
|
|
428
560
|
if (changes && changes.length > 0) {
|
|
@@ -433,7 +565,7 @@ function formatPRMarkdown(prData, context) {
|
|
|
433
565
|
md += `- General code update\n`;
|
|
434
566
|
}
|
|
435
567
|
md += '\n';
|
|
436
|
-
|
|
568
|
+
|
|
437
569
|
// Breaking changes
|
|
438
570
|
if (breaking_changes && breaking_changes.length > 0) {
|
|
439
571
|
md += `## ⚠️ Breaking Changes\n\n`;
|
|
@@ -442,38 +574,38 @@ function formatPRMarkdown(prData, context) {
|
|
|
442
574
|
});
|
|
443
575
|
md += '\n';
|
|
444
576
|
}
|
|
445
|
-
|
|
577
|
+
|
|
446
578
|
// Testing
|
|
447
579
|
if (testing) {
|
|
448
580
|
md += `## Testing\n\n${testing}\n\n`;
|
|
449
581
|
}
|
|
450
|
-
|
|
582
|
+
|
|
451
583
|
// Stats
|
|
452
584
|
md += `## Stats\n\n`;
|
|
453
585
|
md += `- **Commits:** ${commits.length}\n`;
|
|
454
586
|
md += `- **Files changed:** ${changedFiles.length}\n`;
|
|
455
|
-
|
|
587
|
+
|
|
456
588
|
const added = changedFiles.filter(f => f.status === 'added').length;
|
|
457
589
|
const modified = changedFiles.filter(f => f.status === 'modified').length;
|
|
458
590
|
const deleted = changedFiles.filter(f => f.status === 'deleted').length;
|
|
459
|
-
|
|
591
|
+
|
|
460
592
|
if (added) md += `- **Files added:** ${added}\n`;
|
|
461
593
|
if (modified) md += `- **Files modified:** ${modified}\n`;
|
|
462
594
|
if (deleted) md += `- **Files deleted:** ${deleted}\n`;
|
|
463
595
|
md += '\n';
|
|
464
|
-
|
|
596
|
+
|
|
465
597
|
// Notes
|
|
466
598
|
if (notes) {
|
|
467
599
|
md += `## Additional Notes\n\n${notes}\n\n`;
|
|
468
600
|
}
|
|
469
|
-
|
|
601
|
+
|
|
470
602
|
// Checklist
|
|
471
603
|
md += `## Checklist\n\n`;
|
|
472
604
|
md += `- [ ] Code follows project standards\n`;
|
|
473
605
|
md += `- [ ] Tests have been added (if applicable)\n`;
|
|
474
606
|
md += `- [ ] Documentation has been updated (if applicable)\n`;
|
|
475
607
|
md += `- [ ] Changes have been tested locally\n`;
|
|
476
|
-
|
|
608
|
+
|
|
477
609
|
return md;
|
|
478
610
|
}
|
|
479
611
|
|
|
@@ -481,60 +613,6 @@ function formatPRMarkdown(prData, context) {
|
|
|
481
613
|
// EXCLUDED FILES MANAGEMENT
|
|
482
614
|
// ============================================
|
|
483
615
|
|
|
484
|
-
function listExcludes() {
|
|
485
|
-
const excludes = config.get('excludeFiles');
|
|
486
|
-
console.log(chalk.cyan('\n🚫 Files excluded from analysis:\n'));
|
|
487
|
-
|
|
488
|
-
if (excludes.length === 0) {
|
|
489
|
-
console.log(chalk.yellow(' (none)'));
|
|
490
|
-
} else {
|
|
491
|
-
excludes.forEach((file, index) => {
|
|
492
|
-
const isDefault = DEFAULT_EXCLUDES.includes(file);
|
|
493
|
-
const tag = isDefault ? chalk.gray(' (default)') : '';
|
|
494
|
-
console.log(chalk.white(` ${index + 1}. ${chalk.yellow(file)}${tag}`));
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
console.log(chalk.cyan('\n📁 Fixed patterns (always excluded):\n'));
|
|
499
|
-
FIXED_EXCLUDE_PATTERNS.forEach(pattern => {
|
|
500
|
-
console.log(chalk.gray(` • ${pattern}`));
|
|
501
|
-
});
|
|
502
|
-
console.log();
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function addExclude(file) {
|
|
506
|
-
const excludes = config.get('excludeFiles');
|
|
507
|
-
|
|
508
|
-
if (excludes.includes(file)) {
|
|
509
|
-
console.log(chalk.yellow(`\n⚠️ "${file}" is already in the exclusion list.\n`));
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
excludes.push(file);
|
|
514
|
-
config.set('excludeFiles', excludes);
|
|
515
|
-
console.log(chalk.green(`\n✅ Added to exclusions: ${chalk.yellow(file)}\n`));
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function removeExclude(file) {
|
|
519
|
-
const excludes = config.get('excludeFiles');
|
|
520
|
-
const index = excludes.indexOf(file);
|
|
521
|
-
|
|
522
|
-
if (index === -1) {
|
|
523
|
-
console.log(chalk.yellow(`\n⚠️ "${file}" is not in the exclusion list.\n`));
|
|
524
|
-
console.log(chalk.white(' Use --list-excludes to see the current list.\n'));
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
excludes.splice(index, 1);
|
|
529
|
-
config.set('excludeFiles', excludes);
|
|
530
|
-
console.log(chalk.green(`\n✅ Removed from exclusions: ${chalk.yellow(file)}\n`));
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
function resetExcludes() {
|
|
534
|
-
config.set('excludeFiles', [...DEFAULT_EXCLUDES]);
|
|
535
|
-
console.log(chalk.green('\n✅ Exclusion list reset to defaults.\n'));
|
|
536
|
-
}
|
|
537
|
-
|
|
538
616
|
function getExcludedFiles() {
|
|
539
617
|
return config.get('excludeFiles');
|
|
540
618
|
}
|
|
@@ -548,29 +626,29 @@ function shouldExcludeFile(filename, excludePatterns) {
|
|
|
548
626
|
return excludePatterns.some(pattern => {
|
|
549
627
|
// Exact pattern
|
|
550
628
|
if (pattern === filename) return true;
|
|
551
|
-
|
|
629
|
+
|
|
552
630
|
// Pattern with wildcard at start (*.min.js)
|
|
553
631
|
if (pattern.startsWith('*')) {
|
|
554
632
|
const suffix = pattern.slice(1);
|
|
555
633
|
if (filename.endsWith(suffix)) return true;
|
|
556
634
|
}
|
|
557
|
-
|
|
635
|
+
|
|
558
636
|
// Pattern with wildcard at end (dist/*)
|
|
559
637
|
if (pattern.endsWith('/*')) {
|
|
560
638
|
const prefix = pattern.slice(0, -2);
|
|
561
639
|
if (filename.startsWith(prefix + '/') || filename === prefix) return true;
|
|
562
640
|
}
|
|
563
|
-
|
|
641
|
+
|
|
564
642
|
// Pattern with wildcard in middle (*.generated.*)
|
|
565
643
|
if (pattern.includes('*')) {
|
|
566
644
|
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
|
|
567
645
|
if (regex.test(filename)) return true;
|
|
568
646
|
}
|
|
569
|
-
|
|
647
|
+
|
|
570
648
|
// Match by filename (without path)
|
|
571
649
|
const basename = filename.split('/').pop();
|
|
572
650
|
if (pattern === basename) return true;
|
|
573
|
-
|
|
651
|
+
|
|
574
652
|
return false;
|
|
575
653
|
});
|
|
576
654
|
}
|
|
@@ -578,315 +656,341 @@ function shouldExcludeFile(filename, excludePatterns) {
|
|
|
578
656
|
function filterDiff(diff, excludePatterns) {
|
|
579
657
|
const lines = diff.split('\n');
|
|
580
658
|
const filteredLines = [];
|
|
581
|
-
let currentFile = null;
|
|
582
659
|
let excludingCurrentFile = false;
|
|
583
|
-
|
|
660
|
+
|
|
584
661
|
for (const line of lines) {
|
|
585
662
|
// Detect start of new file
|
|
586
663
|
if (line.startsWith('diff --git')) {
|
|
587
|
-
// Extract filename: diff --git a/path/file b/path/file
|
|
588
664
|
const match = line.match(/diff --git a\/(.+) b\/(.+)/);
|
|
589
665
|
if (match) {
|
|
590
|
-
currentFile = match[2];
|
|
666
|
+
const currentFile = match[2];
|
|
591
667
|
excludingCurrentFile = shouldExcludeFile(currentFile, excludePatterns);
|
|
668
|
+
debugLog(`File: ${currentFile}, excluded: ${excludingCurrentFile}`);
|
|
592
669
|
}
|
|
593
670
|
}
|
|
594
|
-
|
|
595
|
-
// Only include lines if we're not excluding the current file
|
|
671
|
+
|
|
596
672
|
if (!excludingCurrentFile) {
|
|
597
673
|
filteredLines.push(line);
|
|
598
674
|
}
|
|
599
675
|
}
|
|
600
|
-
|
|
676
|
+
|
|
601
677
|
return filteredLines.join('\n');
|
|
602
678
|
}
|
|
603
679
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
680
|
+
function listExcludes() {
|
|
681
|
+
const excludes = config.get('excludeFiles');
|
|
682
|
+
console.log(chalk.cyan('\n🚫 Files excluded from analysis:\n'));
|
|
607
683
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
684
|
+
if (excludes.length === 0) {
|
|
685
|
+
console.log(chalk.yellow(' (none)'));
|
|
686
|
+
} else {
|
|
687
|
+
excludes.forEach((file, index) => {
|
|
688
|
+
const isDefault = DEFAULT_EXCLUDES.includes(file);
|
|
689
|
+
const tag = isDefault ? chalk.gray(' (default)') : '';
|
|
690
|
+
console.log(chalk.white(` ${index + 1}. ${chalk.yellow(file)}${tag}`));
|
|
691
|
+
});
|
|
613
692
|
}
|
|
693
|
+
|
|
694
|
+
console.log(chalk.cyan('\n📁 Fixed patterns (always excluded):\n'));
|
|
695
|
+
FIXED_EXCLUDE_PATTERNS.forEach(pattern => {
|
|
696
|
+
console.log(chalk.gray(` • ${pattern}`));
|
|
697
|
+
});
|
|
698
|
+
console.log();
|
|
614
699
|
}
|
|
615
700
|
|
|
616
|
-
function
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
execSync(`git rev-parse ${baseBranch}`, { stdio: 'pipe' });
|
|
623
|
-
return baseBranch;
|
|
624
|
-
} catch {
|
|
625
|
-
throw new Error(`Base branch '${baseBranch}' not found. Verify it exists or use --base to specify another.`);
|
|
626
|
-
}
|
|
701
|
+
function addExclude(file) {
|
|
702
|
+
const excludes = config.get('excludeFiles');
|
|
703
|
+
|
|
704
|
+
if (excludes.includes(file)) {
|
|
705
|
+
console.log(chalk.yellow(`\n⚠️ "${file}" is already in the exclusion list.\n`));
|
|
706
|
+
return;
|
|
627
707
|
}
|
|
708
|
+
|
|
709
|
+
excludes.push(file);
|
|
710
|
+
config.set('excludeFiles', excludes);
|
|
711
|
+
console.log(chalk.green(`\n✅ Added to exclusions: ${chalk.yellow(file)}\n`));
|
|
628
712
|
}
|
|
629
713
|
|
|
630
|
-
function
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
714
|
+
function removeExclude(file) {
|
|
715
|
+
const excludes = config.get('excludeFiles');
|
|
716
|
+
const index = excludes.indexOf(file);
|
|
717
|
+
|
|
718
|
+
if (index === -1) {
|
|
719
|
+
console.log(chalk.yellow(`\n⚠️ "${file}" is not in the exclusion list.\n`));
|
|
720
|
+
console.log(chalk.white(' Use --list-excludes to see the current list.\n'));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
excludes.splice(index, 1);
|
|
725
|
+
config.set('excludeFiles', excludes);
|
|
726
|
+
console.log(chalk.green(`\n✅ Removed from exclusions: ${chalk.yellow(file)}\n`));
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function resetExcludes() {
|
|
730
|
+
config.set('excludeFiles', [...DEFAULT_EXCLUDES]);
|
|
731
|
+
console.log(chalk.green('\n✅ Exclusion list reset to defaults.\n'));
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ============================================
|
|
735
|
+
// GIT FUNCTIONS
|
|
736
|
+
// ============================================
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Check if we're inside a valid git repository
|
|
740
|
+
*/
|
|
741
|
+
function isGitRepository() {
|
|
742
|
+
try {
|
|
743
|
+
const result = execSync('git rev-parse --is-inside-work-tree', {
|
|
744
|
+
encoding: 'utf-8',
|
|
745
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
746
|
+
}).trim();
|
|
747
|
+
return result === 'true';
|
|
748
|
+
} catch {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Get current branch name
|
|
755
|
+
*/
|
|
756
|
+
function getCurrentBranch() {
|
|
757
|
+
try {
|
|
758
|
+
return execSync('git rev-parse --abbrev-ref HEAD', {
|
|
759
|
+
encoding: 'utf-8',
|
|
760
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
761
|
+
}).trim();
|
|
762
|
+
} catch (error) {
|
|
763
|
+
debugLog('Error getting current branch:', error.message);
|
|
764
|
+
throw new Error('Could not get current branch.');
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Validate and get remote base branch
|
|
770
|
+
*/
|
|
771
|
+
function getRemoteBaseBranch(baseBranch) {
|
|
772
|
+
// Validate branch name first
|
|
773
|
+
if (!isValidBranchName(baseBranch)) {
|
|
774
|
+
throw new Error(`Invalid branch name: "${baseBranch}"`);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
try {
|
|
778
|
+
// Try origin/branch first
|
|
779
|
+
execSync(`git rev-parse --verify origin/${baseBranch}`, {
|
|
780
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
781
|
+
});
|
|
782
|
+
return `origin/${baseBranch}`;
|
|
783
|
+
} catch {
|
|
784
|
+
try {
|
|
785
|
+
// Try local branch
|
|
786
|
+
execSync(`git rev-parse --verify ${baseBranch}`, {
|
|
787
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
788
|
+
});
|
|
789
|
+
return baseBranch;
|
|
790
|
+
} catch {
|
|
791
|
+
throw new Error(`Base branch '${baseBranch}' not found. Verify it exists or use --base to specify another.`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Get diff between current branch and base
|
|
798
|
+
*/
|
|
799
|
+
function getBranchDiff(baseBranch) {
|
|
800
|
+
if (!isGitRepository()) {
|
|
801
|
+
throw new Error('You are not in a git repository. Run this command from within a git project.');
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
try {
|
|
634
805
|
const currentBranch = getCurrentBranch();
|
|
635
806
|
const remoteBranch = getRemoteBaseBranch(baseBranch);
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
807
|
+
|
|
808
|
+
debugLog(`Current branch: ${currentBranch}`);
|
|
809
|
+
debugLog(`Remote branch: ${remoteBranch}`);
|
|
810
|
+
|
|
811
|
+
// Get diff
|
|
812
|
+
let diff = execSync(`git diff ${remoteBranch}...HEAD --no-color`, {
|
|
641
813
|
encoding: 'utf-8',
|
|
642
|
-
maxBuffer:
|
|
814
|
+
maxBuffer: MAX_BUFFER_SIZE,
|
|
815
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
643
816
|
});
|
|
644
|
-
|
|
817
|
+
|
|
645
818
|
if (!diff.trim()) {
|
|
646
819
|
return null;
|
|
647
820
|
}
|
|
648
|
-
|
|
821
|
+
|
|
649
822
|
// Filter excluded files programmatically
|
|
650
823
|
const excludePatterns = getAllExcludePatterns();
|
|
651
824
|
diff = filterDiff(diff, excludePatterns);
|
|
652
|
-
|
|
825
|
+
|
|
653
826
|
if (!diff.trim()) {
|
|
654
827
|
return null;
|
|
655
828
|
}
|
|
656
|
-
|
|
829
|
+
|
|
657
830
|
return {
|
|
658
831
|
diff,
|
|
659
832
|
currentBranch,
|
|
660
833
|
baseBranch: remoteBranch
|
|
661
834
|
};
|
|
662
|
-
|
|
835
|
+
|
|
663
836
|
} catch (error) {
|
|
664
|
-
|
|
837
|
+
const errorMsg = error.message || error.stderr || String(error);
|
|
838
|
+
|
|
839
|
+
if (errorMsg.includes('not a git repository')) {
|
|
665
840
|
throw new Error('You are not in a git repository.');
|
|
666
841
|
}
|
|
667
|
-
if (
|
|
842
|
+
if (errorMsg.includes('ENOBUFS') || errorMsg.includes('maxBuffer')) {
|
|
668
843
|
throw new Error('The diff is too large. Consider splitting the PR.');
|
|
669
844
|
}
|
|
845
|
+
|
|
846
|
+
debugLog('Error getting branch diff:', errorMsg);
|
|
670
847
|
throw error;
|
|
671
848
|
}
|
|
672
849
|
}
|
|
673
850
|
|
|
851
|
+
/**
|
|
852
|
+
* Get list of commits between branches
|
|
853
|
+
*/
|
|
674
854
|
function getCommitsList(baseBranch) {
|
|
675
855
|
try {
|
|
676
856
|
const remoteBranch = getRemoteBaseBranch(baseBranch);
|
|
677
|
-
const commits = execSync(`git log ${remoteBranch}..HEAD --oneline --no-decorate`, {
|
|
857
|
+
const commits = execSync(`git log ${remoteBranch}..HEAD --oneline --no-decorate`, {
|
|
678
858
|
encoding: 'utf-8',
|
|
679
|
-
maxBuffer:
|
|
859
|
+
maxBuffer: MAX_BUFFER_SIZE,
|
|
860
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
680
861
|
});
|
|
681
862
|
return commits.trim().split('\n').filter(c => c);
|
|
682
|
-
} catch {
|
|
863
|
+
} catch (error) {
|
|
864
|
+
debugLog('Error getting commits:', error.message);
|
|
683
865
|
return [];
|
|
684
866
|
}
|
|
685
867
|
}
|
|
686
868
|
|
|
869
|
+
/**
|
|
870
|
+
* Get list of changed files with status
|
|
871
|
+
*/
|
|
687
872
|
function getChangedFiles(baseBranch) {
|
|
688
873
|
try {
|
|
689
874
|
const remoteBranch = getRemoteBaseBranch(baseBranch);
|
|
690
875
|
const excludePatterns = getAllExcludePatterns();
|
|
691
|
-
|
|
692
|
-
const files = execSync(`git diff ${remoteBranch}...HEAD --name-status`, {
|
|
876
|
+
|
|
877
|
+
const files = execSync(`git diff ${remoteBranch}...HEAD --name-status`, {
|
|
693
878
|
encoding: 'utf-8',
|
|
694
|
-
maxBuffer:
|
|
879
|
+
maxBuffer: MAX_BUFFER_SIZE,
|
|
880
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
695
881
|
});
|
|
696
|
-
|
|
882
|
+
|
|
697
883
|
return files.trim().split('\n').filter(f => f).map(line => {
|
|
698
884
|
const [status, ...fileParts] = line.split('\t');
|
|
699
885
|
const file = fileParts.join('\t');
|
|
700
886
|
const statusMap = { 'A': 'added', 'M': 'modified', 'D': 'deleted', 'R': 'renamed' };
|
|
701
|
-
return {
|
|
702
|
-
status: statusMap[status[0]] || status,
|
|
887
|
+
return {
|
|
888
|
+
status: statusMap[status[0]] || status,
|
|
703
889
|
statusCode: status[0],
|
|
704
890
|
file,
|
|
705
891
|
excluded: shouldExcludeFile(file, excludePatterns)
|
|
706
892
|
};
|
|
707
893
|
});
|
|
708
|
-
} catch {
|
|
894
|
+
} catch (error) {
|
|
895
|
+
debugLog('Error getting changed files:', error.message);
|
|
709
896
|
return [];
|
|
710
897
|
}
|
|
711
898
|
}
|
|
712
899
|
|
|
900
|
+
/**
|
|
901
|
+
* Get diff statistics
|
|
902
|
+
*/
|
|
713
903
|
function getFilesStats(baseBranch) {
|
|
714
904
|
try {
|
|
715
905
|
const remoteBranch = getRemoteBaseBranch(baseBranch);
|
|
716
|
-
const stats = execSync(`git diff ${remoteBranch}...HEAD --stat`, {
|
|
906
|
+
const stats = execSync(`git diff ${remoteBranch}...HEAD --stat`, {
|
|
717
907
|
encoding: 'utf-8',
|
|
718
|
-
maxBuffer:
|
|
908
|
+
maxBuffer: MAX_BUFFER_SIZE,
|
|
909
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
719
910
|
});
|
|
720
911
|
return stats.trim();
|
|
721
|
-
} catch {
|
|
722
|
-
|
|
912
|
+
} catch (error) {
|
|
913
|
+
debugLog('Error getting stats:', error.message);
|
|
914
|
+
return '(stats unavailable)';
|
|
723
915
|
}
|
|
724
916
|
}
|
|
725
917
|
|
|
726
|
-
|
|
727
|
-
|
|
918
|
+
/**
|
|
919
|
+
* Fetch latest from origin
|
|
920
|
+
*/
|
|
921
|
+
function fetchOrigin() {
|
|
922
|
+
try {
|
|
923
|
+
execSync('git fetch origin', {
|
|
924
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
925
|
+
timeout: 30000 // 30 second timeout
|
|
926
|
+
});
|
|
927
|
+
return { success: true };
|
|
928
|
+
} catch (error) {
|
|
929
|
+
const errorMsg = error.message || '';
|
|
930
|
+
debugLog('Fetch error:', errorMsg);
|
|
931
|
+
|
|
932
|
+
// Categorize the error
|
|
933
|
+
if (errorMsg.includes('Could not resolve host') || errorMsg.includes('unable to access')) {
|
|
934
|
+
return { success: false, reason: 'network', message: 'No network connection' };
|
|
935
|
+
}
|
|
936
|
+
if (errorMsg.includes('Authentication failed') || errorMsg.includes('Permission denied')) {
|
|
937
|
+
return { success: false, reason: 'auth', message: 'Authentication failed' };
|
|
938
|
+
}
|
|
939
|
+
if (errorMsg.includes('timeout')) {
|
|
940
|
+
return { success: false, reason: 'timeout', message: 'Connection timeout' };
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return { success: false, reason: 'unknown', message: 'Unknown error' };
|
|
944
|
+
}
|
|
728
945
|
}
|
|
729
946
|
|
|
947
|
+
/**
|
|
948
|
+
* Save PR description to file
|
|
949
|
+
*/
|
|
730
950
|
function savePRDescription(content, branchName, outputDir) {
|
|
731
951
|
const sanitizedName = sanitizeBranchName(branchName);
|
|
732
952
|
const fileName = `${sanitizedName}_pr.md`;
|
|
733
|
-
|
|
734
|
-
// Resolver el directorio de salida respecto al directorio de trabajo actual
|
|
953
|
+
|
|
735
954
|
const resolvedOutputDir = path.resolve(process.cwd(), outputDir);
|
|
736
955
|
const filePath = path.join(resolvedOutputDir, fileName);
|
|
737
|
-
|
|
956
|
+
|
|
738
957
|
if (!fs.existsSync(resolvedOutputDir)) {
|
|
739
958
|
fs.mkdirSync(resolvedOutputDir, { recursive: true });
|
|
740
959
|
}
|
|
741
|
-
|
|
960
|
+
|
|
742
961
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
743
962
|
return filePath;
|
|
744
963
|
}
|
|
745
964
|
|
|
746
965
|
// ============================================
|
|
747
|
-
//
|
|
966
|
+
// OLLAMA FUNCTIONS
|
|
748
967
|
// ============================================
|
|
749
968
|
|
|
750
|
-
const program = new Command();
|
|
751
|
-
|
|
752
|
-
program
|
|
753
|
-
.name('mkpr')
|
|
754
|
-
.description(chalk.cyan('🚀 CLI to generate PR descriptions using Ollama AI'))
|
|
755
|
-
.version('1.0.0');
|
|
756
|
-
|
|
757
|
-
program
|
|
758
|
-
.option('--set-model [model]', 'Set the Ollama model to use (interactive if omitted)')
|
|
759
|
-
.option('--set-port <port>', 'Set the Ollama port')
|
|
760
|
-
.option('--set-base <branch>', 'Set the base branch for comparison (default: main)')
|
|
761
|
-
.option('--set-output <dir>', 'Set output directory for PR files')
|
|
762
|
-
.option('--show-config', 'Show current configuration')
|
|
763
|
-
.option('--list-models', 'List available models in Ollama')
|
|
764
|
-
.option('--add-exclude <file>', 'Add file to exclusion list')
|
|
765
|
-
.option('--remove-exclude <file>', 'Remove file from exclusion list')
|
|
766
|
-
.option('--list-excludes', 'List excluded files')
|
|
767
|
-
.option('--reset-excludes', 'Reset exclusion list to defaults')
|
|
768
|
-
.option('-b, --base <branch>', 'Base branch for this run (not saved)')
|
|
769
|
-
.option('-o, --output <dir>', 'Output directory for this run (not saved)')
|
|
770
|
-
.option('--dry-run', 'Only show description without saving file')
|
|
771
|
-
.action(async (options) => {
|
|
772
|
-
try {
|
|
773
|
-
if (options.showConfig) {
|
|
774
|
-
showConfig();
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
if (options.listModels) {
|
|
779
|
-
await listModels();
|
|
780
|
-
return;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
if (options.listExcludes) {
|
|
784
|
-
listExcludes();
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
if (options.addExclude) {
|
|
789
|
-
addExclude(options.addExclude);
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
if (options.removeExclude) {
|
|
794
|
-
removeExclude(options.removeExclude);
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
if (options.resetExcludes) {
|
|
799
|
-
resetExcludes();
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
if (options.setPort) {
|
|
804
|
-
const port = parseInt(options.setPort);
|
|
805
|
-
if (isNaN(port) || port < 1 || port > 65535) {
|
|
806
|
-
console.log(chalk.red('❌ Invalid port. Must be a number between 1 and 65535.'));
|
|
807
|
-
process.exit(1);
|
|
808
|
-
}
|
|
809
|
-
config.set('ollamaPort', port);
|
|
810
|
-
console.log(chalk.green(`✅ Port set to: ${port}`));
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
if (options.setModel !== undefined) {
|
|
814
|
-
if (options.setModel === true) {
|
|
815
|
-
// --set-model sin valor = modo interactivo
|
|
816
|
-
await changeModelInteractive();
|
|
817
|
-
} else {
|
|
818
|
-
// --set-model <valor> = establecer directamente
|
|
819
|
-
await setModel(options.setModel);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
if (options.setBase) {
|
|
824
|
-
config.set('baseBranch', options.setBase);
|
|
825
|
-
console.log(chalk.green(`✅ Base branch set to: ${options.setBase}`));
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
if (options.setOutput) {
|
|
829
|
-
config.set('outputDir', options.setOutput);
|
|
830
|
-
console.log(chalk.green(`✅ Output directory set to: ${options.setOutput}`));
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
if (options.setPort || options.setModel !== undefined || options.setBase || options.setOutput) {
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
const baseBranch = options.base || config.get('baseBranch');
|
|
838
|
-
const outputDir = options.output || config.get('outputDir');
|
|
839
|
-
const dryRun = options.dryRun || false;
|
|
840
|
-
|
|
841
|
-
await generatePRDescription(baseBranch, outputDir, dryRun);
|
|
842
|
-
|
|
843
|
-
} catch (error) {
|
|
844
|
-
console.error(chalk.red(`❌ Error: ${error.message}`));
|
|
845
|
-
process.exit(1);
|
|
846
|
-
}
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
program.parse();
|
|
850
|
-
|
|
851
|
-
// ============================================
|
|
852
|
-
// CONFIGURATION FUNCTIONS
|
|
853
|
-
// ============================================
|
|
854
|
-
|
|
855
|
-
function showConfig() {
|
|
856
|
-
console.log(chalk.cyan('\n📋 Current configuration:\n'));
|
|
857
|
-
console.log(chalk.white(` Ollama Port: ${chalk.yellow(config.get('ollamaPort'))}`));
|
|
858
|
-
console.log(chalk.white(` Model: ${chalk.yellow(config.get('ollamaModel'))}`));
|
|
859
|
-
console.log(chalk.white(` Base branch: ${chalk.yellow(config.get('baseBranch'))}`));
|
|
860
|
-
console.log(chalk.white(` Output directory: ${chalk.yellow(config.get('outputDir'))}`));
|
|
861
|
-
console.log(chalk.white(` Excluded files: ${chalk.gray(config.get('excludeFiles').length + ' files')}`));
|
|
862
|
-
console.log();
|
|
863
|
-
}
|
|
864
|
-
|
|
865
969
|
async function getAvailableModels() {
|
|
866
970
|
const port = config.get('ollamaPort');
|
|
867
|
-
const response = await
|
|
868
|
-
|
|
971
|
+
const response = await fetchWithTimeout(`http://localhost:${port}/api/tags`, {}, 10000);
|
|
972
|
+
|
|
869
973
|
if (!response.ok) {
|
|
870
974
|
throw new Error(`Could not connect to Ollama on port ${port}`);
|
|
871
975
|
}
|
|
872
|
-
|
|
976
|
+
|
|
873
977
|
const data = await response.json();
|
|
874
978
|
return data.models || [];
|
|
875
979
|
}
|
|
876
980
|
|
|
877
981
|
async function listModels() {
|
|
878
982
|
const spinner = ora('Getting model list...').start();
|
|
879
|
-
|
|
983
|
+
|
|
880
984
|
try {
|
|
881
985
|
const models = await getAvailableModels();
|
|
882
986
|
spinner.stop();
|
|
883
|
-
|
|
987
|
+
|
|
884
988
|
if (models.length === 0) {
|
|
885
989
|
console.log(chalk.yellow('\n⚠️ No models installed in Ollama.'));
|
|
886
990
|
console.log(chalk.white(' Run: ollama pull <model> to download one.\n'));
|
|
887
991
|
return;
|
|
888
992
|
}
|
|
889
|
-
|
|
993
|
+
|
|
890
994
|
console.log(chalk.cyan('\n📦 Available models in Ollama:\n'));
|
|
891
995
|
models.forEach((model, index) => {
|
|
892
996
|
const name = model.name || model.model;
|
|
@@ -895,7 +999,7 @@ async function listModels() {
|
|
|
895
999
|
console.log(chalk.white(` ${index + 1}. ${chalk.yellow(name)} ${chalk.gray(`(${size})`)}${current}`));
|
|
896
1000
|
});
|
|
897
1001
|
console.log();
|
|
898
|
-
|
|
1002
|
+
|
|
899
1003
|
} catch (error) {
|
|
900
1004
|
spinner.fail('Error connecting to Ollama');
|
|
901
1005
|
console.log(chalk.red(`\n❌ ${error.message}`));
|
|
@@ -903,23 +1007,18 @@ async function listModels() {
|
|
|
903
1007
|
}
|
|
904
1008
|
}
|
|
905
1009
|
|
|
906
|
-
function formatSize(bytes) {
|
|
907
|
-
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
908
|
-
if (bytes === 0) return '0 B';
|
|
909
|
-
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
910
|
-
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
1010
|
async function setModel(modelName) {
|
|
914
1011
|
const spinner = ora('Verifying model...').start();
|
|
915
|
-
|
|
1012
|
+
|
|
916
1013
|
try {
|
|
917
1014
|
const models = await getAvailableModels();
|
|
918
1015
|
const modelNames = models.map(m => m.name || m.model);
|
|
919
|
-
|
|
1016
|
+
|
|
920
1017
|
const exactMatch = modelNames.find(name => name === modelName);
|
|
921
|
-
const partialMatch = modelNames.find(name =>
|
|
922
|
-
|
|
1018
|
+
const partialMatch = modelNames.find(name =>
|
|
1019
|
+
name.startsWith(modelName + ':') || name.split(':')[0] === modelName
|
|
1020
|
+
);
|
|
1021
|
+
|
|
923
1022
|
if (exactMatch) {
|
|
924
1023
|
config.set('ollamaModel', exactMatch);
|
|
925
1024
|
spinner.succeed(`Model set to: ${chalk.yellow(exactMatch)}`);
|
|
@@ -936,7 +1035,7 @@ async function setModel(modelName) {
|
|
|
936
1035
|
console.log();
|
|
937
1036
|
process.exit(1);
|
|
938
1037
|
}
|
|
939
|
-
|
|
1038
|
+
|
|
940
1039
|
} catch (error) {
|
|
941
1040
|
spinner.fail('Error verifying model');
|
|
942
1041
|
console.log(chalk.red(`\n❌ ${error.message}`));
|
|
@@ -946,16 +1045,16 @@ async function setModel(modelName) {
|
|
|
946
1045
|
|
|
947
1046
|
async function changeModelInteractive() {
|
|
948
1047
|
const spinner = ora('Getting available models...').start();
|
|
949
|
-
|
|
1048
|
+
|
|
950
1049
|
try {
|
|
951
1050
|
const models = await getAvailableModels();
|
|
952
1051
|
spinner.stop();
|
|
953
|
-
|
|
1052
|
+
|
|
954
1053
|
if (models.length === 0) {
|
|
955
1054
|
console.log(chalk.yellow('\n⚠️ No models installed in Ollama.\n'));
|
|
956
1055
|
return;
|
|
957
1056
|
}
|
|
958
|
-
|
|
1057
|
+
|
|
959
1058
|
const currentModel = config.get('ollamaModel');
|
|
960
1059
|
const choices = models.map(model => {
|
|
961
1060
|
const name = model.name || model.model;
|
|
@@ -967,7 +1066,7 @@ async function changeModelInteractive() {
|
|
|
967
1066
|
short: name
|
|
968
1067
|
};
|
|
969
1068
|
});
|
|
970
|
-
|
|
1069
|
+
|
|
971
1070
|
const { selectedModel } = await inquirer.prompt([
|
|
972
1071
|
{
|
|
973
1072
|
type: 'list',
|
|
@@ -977,10 +1076,10 @@ async function changeModelInteractive() {
|
|
|
977
1076
|
default: currentModel
|
|
978
1077
|
}
|
|
979
1078
|
]);
|
|
980
|
-
|
|
1079
|
+
|
|
981
1080
|
config.set('ollamaModel', selectedModel);
|
|
982
1081
|
console.log(chalk.green(`\n✅ Model changed to: ${chalk.yellow(selectedModel)}`));
|
|
983
|
-
|
|
1082
|
+
|
|
984
1083
|
} catch (error) {
|
|
985
1084
|
spinner.fail('Error getting models');
|
|
986
1085
|
console.log(chalk.red(`\n❌ ${error.message}`));
|
|
@@ -988,55 +1087,76 @@ async function changeModelInteractive() {
|
|
|
988
1087
|
}
|
|
989
1088
|
}
|
|
990
1089
|
|
|
1090
|
+
// ============================================
|
|
1091
|
+
// CONFIGURATION DISPLAY
|
|
1092
|
+
// ============================================
|
|
1093
|
+
|
|
1094
|
+
function showConfig() {
|
|
1095
|
+
console.log(chalk.cyan('\n📋 Current configuration:\n'));
|
|
1096
|
+
console.log(chalk.white(` Ollama Port: ${chalk.yellow(config.get('ollamaPort'))}`));
|
|
1097
|
+
console.log(chalk.white(` Model: ${chalk.yellow(config.get('ollamaModel'))}`));
|
|
1098
|
+
console.log(chalk.white(` Base branch: ${chalk.yellow(config.get('baseBranch'))}`));
|
|
1099
|
+
console.log(chalk.white(` Output directory: ${chalk.yellow(config.get('outputDir'))}`));
|
|
1100
|
+
console.log(chalk.white(` Debug: ${chalk.yellow(config.get('debug') ? 'enabled' : 'disabled')}`));
|
|
1101
|
+
console.log(chalk.white(` Excluded files: ${chalk.gray(config.get('excludeFiles').length + ' files')}`));
|
|
1102
|
+
console.log();
|
|
1103
|
+
}
|
|
1104
|
+
|
|
991
1105
|
// ============================================
|
|
992
1106
|
// MAIN FLOW
|
|
993
1107
|
// ============================================
|
|
994
1108
|
|
|
995
1109
|
async function generatePRDescription(baseBranch, outputDir, dryRun) {
|
|
996
1110
|
console.log(chalk.cyan('\n🔍 Analyzing differences with base branch...\n'));
|
|
997
|
-
|
|
1111
|
+
|
|
998
1112
|
// Fetch to ensure we have the latest version
|
|
999
1113
|
const fetchSpinner = ora('Getting latest changes from origin...').start();
|
|
1000
|
-
|
|
1001
|
-
|
|
1114
|
+
const fetchResult = fetchOrigin();
|
|
1115
|
+
|
|
1116
|
+
if (fetchResult.success) {
|
|
1002
1117
|
fetchSpinner.succeed('Repository updated');
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1118
|
+
} else {
|
|
1119
|
+
if (fetchResult.reason === 'auth') {
|
|
1120
|
+
fetchSpinner.fail(`Could not fetch: ${fetchResult.message}`);
|
|
1121
|
+
console.log(chalk.yellow(' Continuing with local data, but results may be outdated.\n'));
|
|
1122
|
+
} else {
|
|
1123
|
+
fetchSpinner.warn(`Could not fetch (${fetchResult.message}), continuing with local data`);
|
|
1124
|
+
}
|
|
1005
1125
|
}
|
|
1006
|
-
|
|
1126
|
+
|
|
1007
1127
|
const diffData = getBranchDiff(baseBranch);
|
|
1008
|
-
|
|
1128
|
+
|
|
1009
1129
|
if (!diffData) {
|
|
1010
1130
|
console.log(chalk.yellow('⚠️ No differences with base branch.'));
|
|
1011
1131
|
console.log(chalk.white(` Your branch is up to date with ${baseBranch}.\n`));
|
|
1012
1132
|
process.exit(0);
|
|
1013
1133
|
}
|
|
1014
|
-
|
|
1134
|
+
|
|
1015
1135
|
const commits = getCommitsList(baseBranch);
|
|
1016
1136
|
const changedFiles = getChangedFiles(baseBranch);
|
|
1017
1137
|
const stats = getFilesStats(baseBranch);
|
|
1018
|
-
|
|
1138
|
+
|
|
1019
1139
|
// Filter excluded files for display
|
|
1020
1140
|
const includedFiles = changedFiles.filter(f => !f.excluded);
|
|
1021
1141
|
const excludedFiles = changedFiles.filter(f => f.excluded);
|
|
1022
|
-
|
|
1142
|
+
|
|
1023
1143
|
console.log(chalk.white(`📌 Current branch: ${chalk.yellow(diffData.currentBranch)}`));
|
|
1024
1144
|
console.log(chalk.white(`📌 Base branch: ${chalk.yellow(diffData.baseBranch)}`));
|
|
1025
1145
|
console.log(chalk.white(`📝 Commits: ${chalk.yellow(commits.length)}`));
|
|
1026
1146
|
console.log(chalk.white(`📁 Files: ${chalk.yellow(includedFiles.length)} ${excludedFiles.length > 0 ? chalk.gray(`(${excludedFiles.length} excluded)`) : ''}`));
|
|
1027
1147
|
console.log();
|
|
1028
|
-
|
|
1148
|
+
|
|
1029
1149
|
// Show changed files
|
|
1030
1150
|
console.log(chalk.white('📁 Modified files:'));
|
|
1031
1151
|
includedFiles.slice(0, 10).forEach(f => {
|
|
1032
|
-
const statusColor = f.status === 'added' ? chalk.green :
|
|
1033
|
-
|
|
1152
|
+
const statusColor = f.status === 'added' ? chalk.green :
|
|
1153
|
+
f.status === 'deleted' ? chalk.red : chalk.yellow;
|
|
1034
1154
|
console.log(chalk.gray(` ${statusColor(`[${f.statusCode}]`)} ${f.file}`));
|
|
1035
1155
|
});
|
|
1036
1156
|
if (includedFiles.length > 10) {
|
|
1037
1157
|
console.log(chalk.gray(` ... and ${includedFiles.length - 10} more files`));
|
|
1038
1158
|
}
|
|
1039
|
-
|
|
1159
|
+
|
|
1040
1160
|
// Show excluded files
|
|
1041
1161
|
if (excludedFiles.length > 0) {
|
|
1042
1162
|
console.log(chalk.gray(`\n🚫 Excluded from analysis (${excludedFiles.length}):`));
|
|
@@ -1048,7 +1168,7 @@ async function generatePRDescription(baseBranch, outputDir, dryRun) {
|
|
|
1048
1168
|
}
|
|
1049
1169
|
}
|
|
1050
1170
|
console.log();
|
|
1051
|
-
|
|
1171
|
+
|
|
1052
1172
|
const context = {
|
|
1053
1173
|
currentBranch: diffData.currentBranch,
|
|
1054
1174
|
baseBranch: diffData.baseBranch,
|
|
@@ -1057,15 +1177,15 @@ async function generatePRDescription(baseBranch, outputDir, dryRun) {
|
|
|
1057
1177
|
changedFiles: includedFiles,
|
|
1058
1178
|
stats
|
|
1059
1179
|
};
|
|
1060
|
-
|
|
1180
|
+
|
|
1061
1181
|
let continueLoop = true;
|
|
1062
|
-
|
|
1182
|
+
|
|
1063
1183
|
while (continueLoop) {
|
|
1064
1184
|
const spinner = ora({
|
|
1065
1185
|
text: `Generating description with ${chalk.yellow(config.get('ollamaModel'))}...`,
|
|
1066
1186
|
spinner: 'dots'
|
|
1067
1187
|
}).start();
|
|
1068
|
-
|
|
1188
|
+
|
|
1069
1189
|
let prDescription;
|
|
1070
1190
|
try {
|
|
1071
1191
|
prDescription = await generatePRDescriptionText(context);
|
|
@@ -1076,27 +1196,28 @@ async function generatePRDescription(baseBranch, outputDir, dryRun) {
|
|
|
1076
1196
|
console.log(chalk.white(' Verify that Ollama is running and the model is available.\n'));
|
|
1077
1197
|
process.exit(1);
|
|
1078
1198
|
}
|
|
1079
|
-
|
|
1199
|
+
|
|
1080
1200
|
console.log(chalk.cyan('\n📝 Proposed PR description:\n'));
|
|
1081
1201
|
console.log(chalk.gray('─'.repeat(60)));
|
|
1082
1202
|
console.log(prDescription);
|
|
1083
1203
|
console.log(chalk.gray('─'.repeat(60)));
|
|
1084
1204
|
console.log();
|
|
1085
|
-
|
|
1205
|
+
|
|
1086
1206
|
const choices = [
|
|
1087
1207
|
{ name: chalk.green('✅ Accept and save file'), value: 'accept' },
|
|
1088
1208
|
{ name: chalk.yellow('🔄 Generate another description'), value: 'regenerate' },
|
|
1089
1209
|
{ name: chalk.blue('✏️ Edit title manually'), value: 'edit' },
|
|
1210
|
+
{ name: chalk.cyan('📋 Copy to clipboard'), value: 'copy' },
|
|
1090
1211
|
new inquirer.Separator(),
|
|
1091
1212
|
{ name: chalk.magenta('🤖 Change model'), value: 'change-model' },
|
|
1092
1213
|
new inquirer.Separator(),
|
|
1093
1214
|
{ name: chalk.red('❌ Cancel'), value: 'cancel' }
|
|
1094
1215
|
];
|
|
1095
|
-
|
|
1216
|
+
|
|
1096
1217
|
if (dryRun) {
|
|
1097
1218
|
choices[0] = { name: chalk.green('✅ Accept (dry-run, will not save)'), value: 'accept' };
|
|
1098
1219
|
}
|
|
1099
|
-
|
|
1220
|
+
|
|
1100
1221
|
const { action } = await inquirer.prompt([
|
|
1101
1222
|
{
|
|
1102
1223
|
type: 'list',
|
|
@@ -1105,7 +1226,7 @@ async function generatePRDescription(baseBranch, outputDir, dryRun) {
|
|
|
1105
1226
|
choices
|
|
1106
1227
|
}
|
|
1107
1228
|
]);
|
|
1108
|
-
|
|
1229
|
+
|
|
1109
1230
|
switch (action) {
|
|
1110
1231
|
case 'accept':
|
|
1111
1232
|
if (dryRun) {
|
|
@@ -1123,11 +1244,11 @@ async function generatePRDescription(baseBranch, outputDir, dryRun) {
|
|
|
1123
1244
|
}
|
|
1124
1245
|
continueLoop = false;
|
|
1125
1246
|
break;
|
|
1126
|
-
|
|
1247
|
+
|
|
1127
1248
|
case 'regenerate':
|
|
1128
1249
|
console.log(chalk.cyan('\n🔄 Generating new description...\n'));
|
|
1129
1250
|
break;
|
|
1130
|
-
|
|
1251
|
+
|
|
1131
1252
|
case 'edit':
|
|
1132
1253
|
const { editedTitle } = await inquirer.prompt([
|
|
1133
1254
|
{
|
|
@@ -1137,10 +1258,9 @@ async function generatePRDescription(baseBranch, outputDir, dryRun) {
|
|
|
1137
1258
|
default: diffData.currentBranch.replace(/[-_]/g, ' ')
|
|
1138
1259
|
}
|
|
1139
1260
|
]);
|
|
1140
|
-
|
|
1141
|
-
// Replace title in markdown
|
|
1261
|
+
|
|
1142
1262
|
const finalDescription = prDescription.replace(/^# .+$/m, `# ${editedTitle}`);
|
|
1143
|
-
|
|
1263
|
+
|
|
1144
1264
|
if (!dryRun) {
|
|
1145
1265
|
const editSaveSpinner = ora('Saving file...').start();
|
|
1146
1266
|
try {
|
|
@@ -1155,16 +1275,176 @@ async function generatePRDescription(baseBranch, outputDir, dryRun) {
|
|
|
1155
1275
|
}
|
|
1156
1276
|
continueLoop = false;
|
|
1157
1277
|
break;
|
|
1158
|
-
|
|
1278
|
+
|
|
1279
|
+
case 'copy':
|
|
1280
|
+
try {
|
|
1281
|
+
// Try to copy to clipboard using available system commands
|
|
1282
|
+
const copyCommands = [
|
|
1283
|
+
{ cmd: 'pbcopy', platform: 'darwin' },
|
|
1284
|
+
{ cmd: 'xclip -selection clipboard', platform: 'linux' },
|
|
1285
|
+
{ cmd: 'xsel --clipboard --input', platform: 'linux' },
|
|
1286
|
+
{ cmd: 'clip', platform: 'win32' }
|
|
1287
|
+
];
|
|
1288
|
+
|
|
1289
|
+
const platform = process.platform;
|
|
1290
|
+
let copied = false;
|
|
1291
|
+
|
|
1292
|
+
for (const { cmd, platform: p } of copyCommands) {
|
|
1293
|
+
if (p === platform || (p === 'linux' && platform === 'linux')) {
|
|
1294
|
+
try {
|
|
1295
|
+
execSync(cmd, {
|
|
1296
|
+
input: prDescription,
|
|
1297
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
1298
|
+
});
|
|
1299
|
+
copied = true;
|
|
1300
|
+
console.log(chalk.green('\n✅ Copied to clipboard!\n'));
|
|
1301
|
+
break;
|
|
1302
|
+
} catch {
|
|
1303
|
+
continue;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
if (!copied) {
|
|
1309
|
+
console.log(chalk.yellow('\n⚠️ Could not copy to clipboard. Save the file instead.\n'));
|
|
1310
|
+
}
|
|
1311
|
+
} catch {
|
|
1312
|
+
console.log(chalk.yellow('\n⚠️ Clipboard not available on this system.\n'));
|
|
1313
|
+
}
|
|
1314
|
+
break;
|
|
1315
|
+
|
|
1159
1316
|
case 'change-model':
|
|
1160
1317
|
await changeModelInteractive();
|
|
1161
1318
|
console.log(chalk.cyan('\n🔄 Regenerating description with new model...\n'));
|
|
1162
1319
|
break;
|
|
1163
|
-
|
|
1320
|
+
|
|
1164
1321
|
case 'cancel':
|
|
1165
1322
|
console.log(chalk.yellow('\n👋 Operation cancelled.\n'));
|
|
1166
1323
|
continueLoop = false;
|
|
1167
1324
|
break;
|
|
1168
1325
|
}
|
|
1169
1326
|
}
|
|
1170
|
-
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// ============================================
|
|
1330
|
+
// CLI DEFINITION
|
|
1331
|
+
// ============================================
|
|
1332
|
+
|
|
1333
|
+
const program = new Command();
|
|
1334
|
+
|
|
1335
|
+
program
|
|
1336
|
+
.name('mkpr')
|
|
1337
|
+
.description(chalk.cyan('🚀 CLI to generate PR descriptions using Ollama AI'))
|
|
1338
|
+
.version('1.1.0');
|
|
1339
|
+
|
|
1340
|
+
program
|
|
1341
|
+
.option('--set-model [model]', 'Set the Ollama model to use (interactive if omitted)')
|
|
1342
|
+
.option('--set-port <port>', 'Set the Ollama port')
|
|
1343
|
+
.option('--set-base <branch>', 'Set the base branch for comparison (default: main)')
|
|
1344
|
+
.option('--set-output <dir>', 'Set output directory for PR files')
|
|
1345
|
+
.option('--show-config', 'Show current configuration')
|
|
1346
|
+
.option('--list-models', 'List available models in Ollama')
|
|
1347
|
+
.option('--add-exclude <file>', 'Add file to exclusion list')
|
|
1348
|
+
.option('--remove-exclude <file>', 'Remove file from exclusion list')
|
|
1349
|
+
.option('--list-excludes', 'List excluded files')
|
|
1350
|
+
.option('--reset-excludes', 'Reset exclusion list to defaults')
|
|
1351
|
+
.option('-b, --base <branch>', 'Base branch for this run (not saved)')
|
|
1352
|
+
.option('-o, --output <dir>', 'Output directory for this run (not saved)')
|
|
1353
|
+
.option('--dry-run', 'Only show description without saving file')
|
|
1354
|
+
.option('--debug', 'Enable debug mode')
|
|
1355
|
+
.action(async (options) => {
|
|
1356
|
+
try {
|
|
1357
|
+
// Handle debug flag
|
|
1358
|
+
if (options.debug) {
|
|
1359
|
+
config.set('debug', true);
|
|
1360
|
+
console.log(chalk.gray('[DEBUG] Debug mode enabled'));
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
if (options.showConfig) {
|
|
1364
|
+
showConfig();
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (options.listModels) {
|
|
1369
|
+
await listModels();
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
if (options.listExcludes) {
|
|
1374
|
+
listExcludes();
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
if (options.addExclude) {
|
|
1379
|
+
addExclude(options.addExclude);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
if (options.removeExclude) {
|
|
1384
|
+
removeExclude(options.removeExclude);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
if (options.resetExcludes) {
|
|
1389
|
+
resetExcludes();
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
if (options.setPort) {
|
|
1394
|
+
const port = parseInt(options.setPort);
|
|
1395
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
1396
|
+
console.log(chalk.red('❌ Invalid port. Must be a number between 1 and 65535.'));
|
|
1397
|
+
process.exit(1);
|
|
1398
|
+
}
|
|
1399
|
+
config.set('ollamaPort', port);
|
|
1400
|
+
console.log(chalk.green(`✅ Port set to: ${port}`));
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if (options.setModel !== undefined) {
|
|
1404
|
+
if (options.setModel === true) {
|
|
1405
|
+
await changeModelInteractive();
|
|
1406
|
+
} else {
|
|
1407
|
+
await setModel(options.setModel);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
if (options.setBase) {
|
|
1412
|
+
if (!isValidBranchName(options.setBase)) {
|
|
1413
|
+
console.log(chalk.red(`❌ Invalid branch name: "${options.setBase}"`));
|
|
1414
|
+
process.exit(1);
|
|
1415
|
+
}
|
|
1416
|
+
config.set('baseBranch', options.setBase);
|
|
1417
|
+
console.log(chalk.green(`✅ Base branch set to: ${options.setBase}`));
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
if (options.setOutput) {
|
|
1421
|
+
config.set('outputDir', options.setOutput);
|
|
1422
|
+
console.log(chalk.green(`✅ Output directory set to: ${options.setOutput}`));
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
if (options.setPort || options.setModel !== undefined || options.setBase || options.setOutput) {
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// Validate base branch from options
|
|
1430
|
+
const baseBranch = options.base || config.get('baseBranch');
|
|
1431
|
+
if (!isValidBranchName(baseBranch)) {
|
|
1432
|
+
console.log(chalk.red(`❌ Invalid base branch name: "${baseBranch}"`));
|
|
1433
|
+
process.exit(1);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const outputDir = options.output || config.get('outputDir');
|
|
1437
|
+
const dryRun = options.dryRun || false;
|
|
1438
|
+
|
|
1439
|
+
await generatePRDescription(baseBranch, outputDir, dryRun);
|
|
1440
|
+
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
console.error(chalk.red(`❌ Error: ${error.message}`));
|
|
1443
|
+
if (config.get('debug')) {
|
|
1444
|
+
console.error(error.stack);
|
|
1445
|
+
}
|
|
1446
|
+
process.exit(1);
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
program.parse();
|