@vibedx/vibekit 0.1.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.
- package/LICENSE +21 -0
- package/README.md +368 -0
- package/assets/config.yml +35 -0
- package/assets/default.md +47 -0
- package/assets/instructions/README.md +46 -0
- package/assets/instructions/claude.md +83 -0
- package/assets/instructions/codex.md +19 -0
- package/index.js +106 -0
- package/package.json +90 -0
- package/src/commands/close/index.js +66 -0
- package/src/commands/close/index.test.js +235 -0
- package/src/commands/get-started/index.js +138 -0
- package/src/commands/get-started/index.test.js +246 -0
- package/src/commands/init/index.js +51 -0
- package/src/commands/init/index.test.js +159 -0
- package/src/commands/link/index.js +395 -0
- package/src/commands/link/index.test.js +28 -0
- package/src/commands/lint/index.js +657 -0
- package/src/commands/lint/index.test.js +569 -0
- package/src/commands/list/index.js +131 -0
- package/src/commands/list/index.test.js +153 -0
- package/src/commands/new/index.js +305 -0
- package/src/commands/new/index.test.js +256 -0
- package/src/commands/refine/index.js +741 -0
- package/src/commands/refine/index.test.js +28 -0
- package/src/commands/review/index.js +957 -0
- package/src/commands/review/index.test.js +193 -0
- package/src/commands/start/index.js +180 -0
- package/src/commands/start/index.test.js +88 -0
- package/src/commands/unlink/index.js +123 -0
- package/src/commands/unlink/index.test.js +22 -0
- package/src/utils/arrow-select.js +233 -0
- package/src/utils/cli.js +489 -0
- package/src/utils/cli.test.js +9 -0
- package/src/utils/git.js +146 -0
- package/src/utils/git.test.js +330 -0
- package/src/utils/index.js +193 -0
- package/src/utils/index.test.js +375 -0
- package/src/utils/prompts.js +47 -0
- package/src/utils/prompts.test.js +165 -0
- package/src/utils/test-helpers.js +492 -0
- package/src/utils/ticket.js +423 -0
- package/src/utils/ticket.test.js +190 -0
|
@@ -0,0 +1,957 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { getTicketsDir, getConfig } from '../../utils/index.js';
|
|
7
|
+
import { isGitRepository, getCurrentBranch } from '../../utils/git.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extract ticket ID from branch name
|
|
11
|
+
* @returns {string|null} Ticket ID or null if not found
|
|
12
|
+
*/
|
|
13
|
+
function extractTicketFromBranch() {
|
|
14
|
+
try {
|
|
15
|
+
const branch = getCurrentBranch();
|
|
16
|
+
if (!branch) return null;
|
|
17
|
+
|
|
18
|
+
const match = branch.match(/TKT-\d{3}/);
|
|
19
|
+
return match ? match[0] : null;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Normalize ticket ID format with input sanitization
|
|
27
|
+
* @param {string} input - Input ticket ID or number
|
|
28
|
+
* @returns {string|null} Normalized ticket ID (TKT-XXX format) or null if invalid
|
|
29
|
+
*/
|
|
30
|
+
function normalizeTicketId(input) {
|
|
31
|
+
// Handle null, undefined, or non-string inputs
|
|
32
|
+
if (!input || typeof input !== 'string') {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Sanitize input: trim whitespace and convert to uppercase
|
|
37
|
+
const sanitized = input.trim().toUpperCase();
|
|
38
|
+
|
|
39
|
+
// Handle empty string after trimming
|
|
40
|
+
if (!sanitized) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Validate maximum length to prevent potential issues
|
|
45
|
+
if (sanitized.length > 20) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If it's just a number, convert to TKT-XXX format
|
|
50
|
+
if (/^\d+$/.test(sanitized)) {
|
|
51
|
+
const num = parseInt(sanitized, 10);
|
|
52
|
+
|
|
53
|
+
// Validate reasonable range (1-999)
|
|
54
|
+
if (num < 1 || num > 999) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const paddedNumber = sanitized.padStart(3, '0');
|
|
59
|
+
return `TKT-${paddedNumber}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// If it's already in TKT-XXX format, validate and return
|
|
63
|
+
if (/^TKT-\d{3}$/.test(sanitized)) {
|
|
64
|
+
const num = parseInt(sanitized.substring(4), 10);
|
|
65
|
+
|
|
66
|
+
// Validate reasonable range (1-999)
|
|
67
|
+
if (num < 1 || num > 999) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return sanitized;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle partial formats like "TKT001" or "TKT-1"
|
|
75
|
+
if (/^TKT\d{1,3}$/.test(sanitized)) {
|
|
76
|
+
const numPart = sanitized.substring(3);
|
|
77
|
+
const num = parseInt(numPart, 10);
|
|
78
|
+
|
|
79
|
+
if (num < 1 || num > 999) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const paddedNumber = numPart.padStart(3, '0');
|
|
84
|
+
return `TKT-${paddedNumber}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (/^TKT-\d{1,2}$/.test(sanitized)) {
|
|
88
|
+
const numPart = sanitized.substring(4);
|
|
89
|
+
const num = parseInt(numPart, 10);
|
|
90
|
+
|
|
91
|
+
if (num < 1 || num > 999) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const paddedNumber = numPart.padStart(3, '0');
|
|
96
|
+
return `TKT-${paddedNumber}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Invalid format
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate ticket ID format and existence
|
|
105
|
+
* @param {string} ticketId - The ticket ID to validate
|
|
106
|
+
* @returns {Object} Validation result with isValid flag and error message
|
|
107
|
+
*/
|
|
108
|
+
function validateTicketId(ticketId) {
|
|
109
|
+
if (!ticketId) {
|
|
110
|
+
return { isValid: false, error: 'Ticket ID is required' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const normalizedId = normalizeTicketId(ticketId);
|
|
114
|
+
if (!normalizedId) {
|
|
115
|
+
return { isValid: false, error: `Invalid ticket ID format: ${ticketId}. Expected format: TKT-XXX or just the number (e.g., TKT-001 or 1)` };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const ticketsDir = getTicketsDir();
|
|
119
|
+
const ticketFiles = fs.readdirSync(ticketsDir)
|
|
120
|
+
.filter(file => file.endsWith('.md') && file.startsWith(normalizedId));
|
|
121
|
+
|
|
122
|
+
if (ticketFiles.length === 0) {
|
|
123
|
+
return { isValid: false, error: `Ticket not found: ${normalizedId}` };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { isValid: true, ticketFile: path.join(ticketsDir, ticketFiles[0]), ticketId: normalizedId };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Parse ticket content and extract requirements
|
|
131
|
+
* @param {string} filePath - Path to the ticket file
|
|
132
|
+
* @returns {Object} Parsed ticket data
|
|
133
|
+
*/
|
|
134
|
+
function parseTicketRequirements(filePath) {
|
|
135
|
+
try {
|
|
136
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
137
|
+
|
|
138
|
+
if (!content.startsWith('---')) {
|
|
139
|
+
throw new Error('Invalid ticket format: missing frontmatter');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const parts = content.split('---');
|
|
143
|
+
if (parts.length < 3) {
|
|
144
|
+
throw new Error('Invalid ticket format: malformed frontmatter');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const frontmatter = yaml.load(parts[1]);
|
|
148
|
+
const ticketContent = parts.slice(2).join('---');
|
|
149
|
+
|
|
150
|
+
// Extract sections
|
|
151
|
+
const sections = {};
|
|
152
|
+
const sectionRegex = /^##\s+(.+?)$([\s\S]*?)(?=^##\s+|$)/gm;
|
|
153
|
+
let match;
|
|
154
|
+
|
|
155
|
+
while ((match = sectionRegex.exec(ticketContent)) !== null) {
|
|
156
|
+
const sectionName = match[1].trim();
|
|
157
|
+
const sectionContent = match[2].trim();
|
|
158
|
+
sections[sectionName] = sectionContent;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
metadata: {
|
|
163
|
+
id: frontmatter.id,
|
|
164
|
+
title: frontmatter.title,
|
|
165
|
+
status: frontmatter.status,
|
|
166
|
+
priority: frontmatter.priority
|
|
167
|
+
},
|
|
168
|
+
sections
|
|
169
|
+
};
|
|
170
|
+
} catch (error) {
|
|
171
|
+
throw new Error(`Failed to parse ticket: ${error.message}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get staged changes from git
|
|
177
|
+
* @returns {string} Staged changes diff
|
|
178
|
+
*/
|
|
179
|
+
function getStagedChanges() {
|
|
180
|
+
try {
|
|
181
|
+
return execSync('git diff --staged', { encoding: 'utf-8' });
|
|
182
|
+
} catch (error) {
|
|
183
|
+
throw new Error(`Failed to get staged changes: ${error.message}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get list of staged files
|
|
189
|
+
* @returns {string[]} Array of staged file paths
|
|
190
|
+
*/
|
|
191
|
+
function getStagedFiles() {
|
|
192
|
+
try {
|
|
193
|
+
const output = execSync('git diff --staged --name-only', { encoding: 'utf-8' });
|
|
194
|
+
return output.trim().split('\n').filter(file => file.length > 0);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Create ticket information section for AI prompt
|
|
202
|
+
* @param {Object} metadata - Ticket metadata
|
|
203
|
+
* @returns {string} Formatted ticket information
|
|
204
|
+
*/
|
|
205
|
+
function createTicketInfoSection(metadata) {
|
|
206
|
+
return `TICKET INFORMATION:
|
|
207
|
+
ID: ${metadata.id}
|
|
208
|
+
Title: ${metadata.title}
|
|
209
|
+
Status: ${metadata.status}
|
|
210
|
+
Priority: ${metadata.priority}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create requirements section for AI prompt
|
|
215
|
+
* @param {Object} sections - Ticket sections
|
|
216
|
+
* @returns {string} Formatted requirements section
|
|
217
|
+
*/
|
|
218
|
+
function createRequirementsSection(sections) {
|
|
219
|
+
return `REQUIREMENTS:
|
|
220
|
+
${sections['Description'] || 'No description provided'}
|
|
221
|
+
|
|
222
|
+
ACCEPTANCE CRITERIA:
|
|
223
|
+
${sections['Acceptance Criteria'] || 'No acceptance criteria provided'}
|
|
224
|
+
|
|
225
|
+
CODE QUALITY REQUIREMENTS:
|
|
226
|
+
${sections['Code Quality'] || 'No code quality requirements specified'}
|
|
227
|
+
|
|
228
|
+
IMPLEMENTATION NOTES:
|
|
229
|
+
${sections['Implementation Notes'] || 'No implementation notes provided'}
|
|
230
|
+
|
|
231
|
+
TESTING REQUIREMENTS:
|
|
232
|
+
${sections['Testing & Test Cases'] || 'No testing requirements specified'}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Create code changes section for AI prompt
|
|
237
|
+
* @param {string[]} stagedFiles - List of staged files
|
|
238
|
+
* @param {string} stagedChanges - Git diff of staged changes
|
|
239
|
+
* @returns {string} Formatted code changes section
|
|
240
|
+
*/
|
|
241
|
+
function createCodeChangesSection(stagedFiles, stagedChanges) {
|
|
242
|
+
return `STAGED FILES:
|
|
243
|
+
${stagedFiles.map(file => `- ${file}`).join('\n')}
|
|
244
|
+
|
|
245
|
+
STAGED CHANGES:
|
|
246
|
+
\`\`\`diff
|
|
247
|
+
${stagedChanges}
|
|
248
|
+
\`\`\``;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Create analysis instructions for AI prompt
|
|
253
|
+
* @returns {string} Formatted analysis instructions
|
|
254
|
+
*/
|
|
255
|
+
function createAnalysisInstructions() {
|
|
256
|
+
return `Please analyze and provide SPECIFIC feedback with:
|
|
257
|
+
1. Overall completion percentage (0-100%)
|
|
258
|
+
2. Detailed breakdown referencing specific functions, files, or line ranges where possible
|
|
259
|
+
3. Code quality assessment with specific examples
|
|
260
|
+
4. Any issues with exact locations (file:function or file:line when possible)
|
|
261
|
+
5. Specific, actionable recommendations
|
|
262
|
+
|
|
263
|
+
BE SPECIFIC: Reference actual function names, file paths, and specific code patterns from the diff.
|
|
264
|
+
For issues and recommendations, provide concrete examples like "In src/commands/review/index.js:validateTicketId(), consider..." or "Lines 45-60 in file.js should..."`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Create JSON response format for AI prompt
|
|
269
|
+
* @returns {string} Formatted JSON schema
|
|
270
|
+
*/
|
|
271
|
+
function createResponseFormat() {
|
|
272
|
+
return `Format your response as JSON:
|
|
273
|
+
{
|
|
274
|
+
"completionPercentage": 85,
|
|
275
|
+
"status": "good|warning|poor",
|
|
276
|
+
"summary": "Brief overall assessment",
|
|
277
|
+
"completed": [
|
|
278
|
+
"Specific completed requirements with file/function references"
|
|
279
|
+
],
|
|
280
|
+
"missing": [
|
|
281
|
+
"Specific missing requirements with suggested locations"
|
|
282
|
+
],
|
|
283
|
+
"issues": [
|
|
284
|
+
"Specific code quality issues with file:function or file:line references"
|
|
285
|
+
],
|
|
286
|
+
"recommendations": [
|
|
287
|
+
"Specific actionable recommendations with exact locations"
|
|
288
|
+
]
|
|
289
|
+
}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Create AI review prompt
|
|
294
|
+
* @param {Object} ticket - Parsed ticket data
|
|
295
|
+
* @param {string} stagedChanges - Git diff of staged changes
|
|
296
|
+
* @param {string[]} stagedFiles - List of staged files
|
|
297
|
+
* @returns {string} AI prompt for review
|
|
298
|
+
*/
|
|
299
|
+
function createReviewPrompt(ticket, stagedChanges, stagedFiles) {
|
|
300
|
+
const sections = [
|
|
301
|
+
'Please review the following code changes against the ticket requirements and provide a detailed, specific analysis.',
|
|
302
|
+
'',
|
|
303
|
+
createTicketInfoSection(ticket.metadata),
|
|
304
|
+
'',
|
|
305
|
+
createRequirementsSection(ticket.sections),
|
|
306
|
+
'',
|
|
307
|
+
createCodeChangesSection(stagedFiles, stagedChanges),
|
|
308
|
+
'',
|
|
309
|
+
createAnalysisInstructions(),
|
|
310
|
+
'',
|
|
311
|
+
createResponseFormat()
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
return sections.join('\n');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Call Claude Code CLI for code review with timeout handling
|
|
319
|
+
* @param {string} prompt - Review prompt
|
|
320
|
+
* @param {number} timeoutMs - Timeout in milliseconds (default: 60000)
|
|
321
|
+
* @returns {Promise<Object>} AI review response
|
|
322
|
+
*/
|
|
323
|
+
async function callClaudeForReview(prompt, timeoutMs = 60000) {
|
|
324
|
+
console.log(chalk.yellow('š¤ Analyzing changes with Claude...'));
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
// Create a timeout promise
|
|
328
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
329
|
+
const timeoutId = setTimeout(() => {
|
|
330
|
+
reject(new Error(`Claude analysis timed out after ${timeoutMs}ms`));
|
|
331
|
+
}, timeoutMs);
|
|
332
|
+
|
|
333
|
+
return timeoutId;
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Create the actual Claude processing promise
|
|
337
|
+
const claudeProcessingPromise = new Promise(async (resolve, reject) => {
|
|
338
|
+
try {
|
|
339
|
+
// Call Claude Code CLI with --print flag for non-interactive output
|
|
340
|
+
const claudeResponse = execSync(
|
|
341
|
+
'claude --print',
|
|
342
|
+
{
|
|
343
|
+
input: prompt,
|
|
344
|
+
encoding: 'utf-8',
|
|
345
|
+
timeout: timeoutMs - 5000, // Leave 5s buffer for cleanup
|
|
346
|
+
maxBuffer: 1024 * 1024 * 5 // 5MB buffer
|
|
347
|
+
}
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
// Parse Claude's response as JSON
|
|
351
|
+
let parsedResponse;
|
|
352
|
+
try {
|
|
353
|
+
// Try to extract JSON from Claude's response
|
|
354
|
+
const jsonMatch = claudeResponse.match(/\{[\s\S]*\}/);
|
|
355
|
+
if (jsonMatch) {
|
|
356
|
+
parsedResponse = JSON.parse(jsonMatch[0]);
|
|
357
|
+
} else {
|
|
358
|
+
// Fallback: create structured response from text
|
|
359
|
+
parsedResponse = parseTextResponse(claudeResponse);
|
|
360
|
+
}
|
|
361
|
+
} catch (parseError) {
|
|
362
|
+
// If JSON parsing fails, create a structured response
|
|
363
|
+
parsedResponse = parseTextResponse(claudeResponse);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Validate response structure
|
|
367
|
+
const validatedResponse = validateClaudeResponse(parsedResponse);
|
|
368
|
+
resolve(validatedResponse);
|
|
369
|
+
|
|
370
|
+
} catch (error) {
|
|
371
|
+
if (error.code === 'ENOENT') {
|
|
372
|
+
reject(new Error('Claude CLI not found. Please ensure Claude Code is installed and accessible.'));
|
|
373
|
+
} else if (error.signal === 'SIGTERM') {
|
|
374
|
+
reject(new Error('Claude analysis was terminated due to timeout'));
|
|
375
|
+
} else {
|
|
376
|
+
reject(new Error(`Claude analysis failed: ${error.message}`));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Race between timeout and Claude processing
|
|
382
|
+
const result = await Promise.race([claudeProcessingPromise, timeoutPromise]);
|
|
383
|
+
return result;
|
|
384
|
+
|
|
385
|
+
} catch (error) {
|
|
386
|
+
// Handle timeout and other errors gracefully
|
|
387
|
+
if (error.message.includes('timed out') || error.message.includes('timeout')) {
|
|
388
|
+
console.error(chalk.yellow('ā ļø Claude analysis timed out. Manual review recommended.'));
|
|
389
|
+
throw new Error('Claude analysis timed out - please review manually or increase timeout');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (error.message.includes('not found')) {
|
|
393
|
+
console.error(chalk.red('ā Claude CLI not available.'));
|
|
394
|
+
throw new Error('Claude CLI not found - please ensure Claude Code is installed');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Re-throw other errors
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Parse Claude's text response into structured format
|
|
404
|
+
* @param {string} textResponse - Raw text response from Claude
|
|
405
|
+
* @returns {Object} Structured response object
|
|
406
|
+
*/
|
|
407
|
+
function parseTextResponse(textResponse) {
|
|
408
|
+
// Extract completion percentage
|
|
409
|
+
const percentageMatch = textResponse.match(/(\d+)%/);
|
|
410
|
+
const completionPercentage = percentageMatch ? parseInt(percentageMatch[1], 10) : 0;
|
|
411
|
+
|
|
412
|
+
// Determine status based on percentage
|
|
413
|
+
let status = 'poor';
|
|
414
|
+
if (completionPercentage >= 80) status = 'good';
|
|
415
|
+
else if (completionPercentage >= 60) status = 'warning';
|
|
416
|
+
|
|
417
|
+
// Extract sections using common patterns
|
|
418
|
+
const extractSection = (sectionName) => {
|
|
419
|
+
const patterns = [
|
|
420
|
+
new RegExp(`${sectionName}:?\\s*([\\s\\S]*?)(?=\\n\\n|\\n[A-Z][^:]*:|$)`, 'i'),
|
|
421
|
+
new RegExp(`${sectionName}\\s*([\\s\\S]*?)(?=\\n\\n|\\n[A-Z][^:]*:|$)`, 'i')
|
|
422
|
+
];
|
|
423
|
+
|
|
424
|
+
for (const pattern of patterns) {
|
|
425
|
+
const match = textResponse.match(pattern);
|
|
426
|
+
if (match) {
|
|
427
|
+
return match[1].trim().split('\n')
|
|
428
|
+
.map(line => line.replace(/^[-ā¢*]\s*/, '').trim())
|
|
429
|
+
.filter(line => line.length > 0);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return [];
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
completionPercentage,
|
|
437
|
+
status,
|
|
438
|
+
summary: textResponse.split('\n')[0] || 'Code review completed',
|
|
439
|
+
completed: extractSection('completed|done|implemented|working'),
|
|
440
|
+
missing: extractSection('missing|incomplete|todo|needed'),
|
|
441
|
+
issues: extractSection('issues|problems|concerns|bugs'),
|
|
442
|
+
recommendations: extractSection('recommendations|suggestions|improvements|consider')
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Validate and normalize Claude's response
|
|
448
|
+
* @param {Object} response - Response object from Claude
|
|
449
|
+
* @returns {Object} Validated response
|
|
450
|
+
*/
|
|
451
|
+
function validateClaudeResponse(response) {
|
|
452
|
+
const validated = {
|
|
453
|
+
completionPercentage: Math.max(0, Math.min(100, response.completionPercentage || 0)),
|
|
454
|
+
status: ['good', 'warning', 'poor'].includes(response.status) ? response.status : 'warning',
|
|
455
|
+
summary: response.summary || 'Code review completed',
|
|
456
|
+
completed: Array.isArray(response.completed) ? response.completed : [],
|
|
457
|
+
missing: Array.isArray(response.missing) ? response.missing : [],
|
|
458
|
+
issues: Array.isArray(response.issues) ? response.issues : [],
|
|
459
|
+
recommendations: Array.isArray(response.recommendations) ? response.recommendations : []
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// Ensure status matches percentage
|
|
463
|
+
if (validated.completionPercentage >= 80 && validated.status === 'poor') {
|
|
464
|
+
validated.status = 'good';
|
|
465
|
+
} else if (validated.completionPercentage >= 60 && validated.status === 'poor') {
|
|
466
|
+
validated.status = 'warning';
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return validated;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Display review results with color coding based on configuration thresholds
|
|
476
|
+
* @param {Object} review - AI review results
|
|
477
|
+
* @param {number} review.completionPercentage - Completion percentage (0-100)
|
|
478
|
+
* @param {string} review.status - Review status (good|warning|poor)
|
|
479
|
+
* @param {string} review.summary - Brief assessment summary
|
|
480
|
+
* @param {string[]} review.completed - List of completed requirements
|
|
481
|
+
* @param {string[]} review.missing - List of missing requirements
|
|
482
|
+
* @param {string[]} review.issues - List of identified issues
|
|
483
|
+
* @param {string[]} review.recommendations - List of recommendations
|
|
484
|
+
* @param {Object} ticket - Ticket metadata
|
|
485
|
+
* @param {Object} ticket.metadata - Ticket metadata object
|
|
486
|
+
* @param {string} ticket.metadata.id - Ticket ID
|
|
487
|
+
* @param {string} ticket.metadata.title - Ticket title
|
|
488
|
+
* @param {Object} config - Configuration object
|
|
489
|
+
* @param {Object} [config.review] - Review configuration
|
|
490
|
+
* @param {number} [config.review.good_threshold=80] - Threshold for green status
|
|
491
|
+
* @param {number} [config.review.warning_threshold=60] - Threshold for yellow status
|
|
492
|
+
*/
|
|
493
|
+
function displayReviewResults(review, ticket, config) {
|
|
494
|
+
const percentage = review.completionPercentage;
|
|
495
|
+
const thresholds = config.review || {};
|
|
496
|
+
const goodThreshold = thresholds.good_threshold || 80;
|
|
497
|
+
const warningThreshold = thresholds.warning_threshold || 60;
|
|
498
|
+
|
|
499
|
+
let percentageColor;
|
|
500
|
+
let statusIcon;
|
|
501
|
+
|
|
502
|
+
if (percentage >= goodThreshold) {
|
|
503
|
+
percentageColor = chalk.green;
|
|
504
|
+
statusIcon = 'ā
';
|
|
505
|
+
} else if (percentage >= warningThreshold) {
|
|
506
|
+
percentageColor = chalk.yellow;
|
|
507
|
+
statusIcon = 'ā ļø';
|
|
508
|
+
} else {
|
|
509
|
+
percentageColor = chalk.red;
|
|
510
|
+
statusIcon = 'ā';
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
console.log(chalk.blue('\nš AI Code Review Results'));
|
|
514
|
+
console.log(chalk.blue('ā'.repeat(50)));
|
|
515
|
+
console.log(`${chalk.bold('Ticket:')} ${ticket.metadata.id} - ${ticket.metadata.title}`);
|
|
516
|
+
console.log(`${chalk.bold('Completion:')} ${statusIcon} ${percentageColor(`${percentage}%`)}`);
|
|
517
|
+
console.log(`${chalk.bold('Summary:')} ${review.summary}`);
|
|
518
|
+
console.log(chalk.blue('ā'.repeat(50)));
|
|
519
|
+
|
|
520
|
+
// Completed items
|
|
521
|
+
if (review.completed.length > 0) {
|
|
522
|
+
console.log(chalk.green('\nā
Completed Requirements:'));
|
|
523
|
+
review.completed.forEach(item => {
|
|
524
|
+
console.log(chalk.green(` ⢠${item}`));
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Missing items
|
|
529
|
+
if (review.missing.length > 0) {
|
|
530
|
+
console.log(chalk.red('\nā Missing/Incomplete:'));
|
|
531
|
+
review.missing.forEach(item => {
|
|
532
|
+
console.log(chalk.red(` ⢠${item}`));
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Issues
|
|
537
|
+
if (review.issues.length > 0) {
|
|
538
|
+
console.log(chalk.yellow('\nā ļø Issues Found:'));
|
|
539
|
+
review.issues.forEach(item => {
|
|
540
|
+
console.log(chalk.yellow(` ⢠${item}`));
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Recommendations
|
|
545
|
+
if (review.recommendations.length > 0) {
|
|
546
|
+
console.log(chalk.blue('\nš” Recommendations:'));
|
|
547
|
+
review.recommendations.forEach(item => {
|
|
548
|
+
console.log(chalk.blue(` ⢠${item}`));
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
console.log(chalk.gray('\nš Use --copy or -c to copy detailed feedback to clipboard'));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Format review results for clipboard in plain text format
|
|
557
|
+
* @param {Object} review - AI review results
|
|
558
|
+
* @param {number} review.completionPercentage - Completion percentage
|
|
559
|
+
* @param {string} review.summary - Review summary
|
|
560
|
+
* @param {string[]} review.completed - Completed items
|
|
561
|
+
* @param {string[]} review.missing - Missing items
|
|
562
|
+
* @param {string[]} review.issues - Issues found
|
|
563
|
+
* @param {string[]} review.recommendations - Recommendations
|
|
564
|
+
* @param {Object} ticket - Ticket metadata
|
|
565
|
+
* @param {Object} ticket.metadata - Ticket metadata object
|
|
566
|
+
* @param {string} ticket.metadata.id - Ticket ID
|
|
567
|
+
* @param {string} ticket.metadata.title - Ticket title
|
|
568
|
+
* @returns {string} Formatted text for clipboard with proper spacing and sections
|
|
569
|
+
* @example
|
|
570
|
+
* const formattedText = formatReviewForClipboard(reviewResult, ticketData);
|
|
571
|
+
* // Returns formatted string ready for clipboard
|
|
572
|
+
*/
|
|
573
|
+
function formatReviewForClipboard(review, ticket) {
|
|
574
|
+
let output = `Code Review Results - ${ticket.metadata.id}\n`;
|
|
575
|
+
output += `${'='.repeat(50)}\n`;
|
|
576
|
+
output += `Ticket: ${ticket.metadata.id} - ${ticket.metadata.title}\n`;
|
|
577
|
+
output += `Completion: ${review.completionPercentage}%\n`;
|
|
578
|
+
output += `Summary: ${review.summary}\n\n`;
|
|
579
|
+
|
|
580
|
+
if (review.completed.length > 0) {
|
|
581
|
+
output += `Completed Requirements:\n`;
|
|
582
|
+
review.completed.forEach(item => {
|
|
583
|
+
output += ` ⢠${item}\n`;
|
|
584
|
+
});
|
|
585
|
+
output += '\n';
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (review.missing.length > 0) {
|
|
589
|
+
output += `Missing/Incomplete:\n`;
|
|
590
|
+
review.missing.forEach(item => {
|
|
591
|
+
output += ` ⢠${item}\n`;
|
|
592
|
+
});
|
|
593
|
+
output += '\n';
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (review.issues.length > 0) {
|
|
597
|
+
output += `Issues Found:\n`;
|
|
598
|
+
review.issues.forEach(item => {
|
|
599
|
+
output += ` ⢠${item}\n`;
|
|
600
|
+
});
|
|
601
|
+
output += '\n';
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (review.recommendations.length > 0) {
|
|
605
|
+
output += `Recommendations:\n`;
|
|
606
|
+
review.recommendations.forEach(item => {
|
|
607
|
+
output += ` ⢠${item}\n`;
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return output;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Copy text to clipboard with platform-specific commands and fallback to cache file
|
|
616
|
+
* @param {string} text - Text to copy to clipboard
|
|
617
|
+
* @param {string} ticketId - Ticket ID for cache file naming
|
|
618
|
+
* @returns {boolean} True if copied to clipboard, false if saved to cache file
|
|
619
|
+
*/
|
|
620
|
+
function copyToClipboard(text, ticketId) {
|
|
621
|
+
const commands = {
|
|
622
|
+
darwin: 'pbcopy',
|
|
623
|
+
win32: 'clip',
|
|
624
|
+
linux: ['xclip -selection clipboard', 'xsel --clipboard --input']
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const platform = process.platform;
|
|
628
|
+
const platformCommands = commands[platform] || commands.linux;
|
|
629
|
+
const commandList = Array.isArray(platformCommands) ? platformCommands : [platformCommands];
|
|
630
|
+
|
|
631
|
+
// Try each clipboard command
|
|
632
|
+
for (const command of commandList) {
|
|
633
|
+
try {
|
|
634
|
+
execSync(command, { input: text, stdio: 'pipe' });
|
|
635
|
+
console.log(chalk.green('\nš Review results copied to clipboard!'));
|
|
636
|
+
return true;
|
|
637
|
+
} catch (error) {
|
|
638
|
+
continue; // Try next command
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Fallback: save to cache file
|
|
643
|
+
const cacheDir = path.join(process.cwd(), `.vibe/.cache/review/logs/${ticketId}`);
|
|
644
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
645
|
+
|
|
646
|
+
const gitignorePath = path.join(process.cwd(), '.vibe/.cache/.gitignore');
|
|
647
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
648
|
+
fs.writeFileSync(gitignorePath, '*\n!.gitignore\n');
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const timestamp = new Date().toISOString().replace(/:/g, '-').split('.')[0];
|
|
652
|
+
const tempFile = path.join(cacheDir, `review-${timestamp}.txt`);
|
|
653
|
+
fs.writeFileSync(tempFile, text);
|
|
654
|
+
|
|
655
|
+
console.log(chalk.yellow('\nš Clipboard not available. Review results saved to:'));
|
|
656
|
+
console.log(chalk.cyan(` ${tempFile}`));
|
|
657
|
+
console.log(chalk.gray(' Copy manually or run: vibe review clean'));
|
|
658
|
+
|
|
659
|
+
// Auto-cleanup after 60 seconds
|
|
660
|
+
setTimeout(() => {
|
|
661
|
+
if (fs.existsSync(tempFile)) {
|
|
662
|
+
fs.unlinkSync(tempFile);
|
|
663
|
+
cleanupEmptyDirs(path.dirname(tempFile));
|
|
664
|
+
}
|
|
665
|
+
}, 60000);
|
|
666
|
+
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Recursively remove empty directories from cache structure
|
|
672
|
+
* @param {string} dirPath - Directory path to clean up
|
|
673
|
+
* @throws {Error} Silently handles and ignores cleanup errors
|
|
674
|
+
* @example
|
|
675
|
+
* cleanupEmptyDirs('.vibe/.cache/review/logs/TKT-001');
|
|
676
|
+
*/
|
|
677
|
+
function cleanupEmptyDirs(dirPath) {
|
|
678
|
+
try {
|
|
679
|
+
if (!fs.existsSync(dirPath)) return;
|
|
680
|
+
|
|
681
|
+
const entries = fs.readdirSync(dirPath);
|
|
682
|
+
if (entries.length === 0) {
|
|
683
|
+
fs.rmdirSync(dirPath);
|
|
684
|
+
// Recursively check parent directory
|
|
685
|
+
const parentDir = path.dirname(dirPath);
|
|
686
|
+
if (parentDir !== dirPath && parentDir.includes('.vibe/.cache')) {
|
|
687
|
+
cleanupEmptyDirs(parentDir);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
} catch (error) {
|
|
691
|
+
// Ignore cleanup errors
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Clean review cache files with optional ticket filtering
|
|
697
|
+
* @param {string[]} args - Command arguments for filtering specific tickets
|
|
698
|
+
* @param {string} [args[0]] - Optional ticket ID to clean specific ticket files
|
|
699
|
+
* @example
|
|
700
|
+
* cleanReviewFiles([]); // Clean all review cache files
|
|
701
|
+
* cleanReviewFiles(['TKT-001']); // Clean only TKT-001 cache files
|
|
702
|
+
* cleanReviewFiles(['11']); // Clean TKT-011 cache files (auto-normalized)
|
|
703
|
+
*/
|
|
704
|
+
function cleanReviewFiles(args) {
|
|
705
|
+
const cacheBasePath = path.join(process.cwd(), '.vibe/.cache/review');
|
|
706
|
+
|
|
707
|
+
if (!fs.existsSync(cacheBasePath)) {
|
|
708
|
+
console.log(chalk.green('ā
No review cache files to clean'));
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Check for specific ticket ID
|
|
713
|
+
let targetPath = cacheBasePath;
|
|
714
|
+
let ticketFilter = null;
|
|
715
|
+
|
|
716
|
+
if (args.length > 0 && args[0] !== 'clean') {
|
|
717
|
+
const ticketId = args[0];
|
|
718
|
+
const normalizedId = normalizeTicketId(ticketId);
|
|
719
|
+
if (normalizedId) {
|
|
720
|
+
targetPath = path.join(cacheBasePath, 'logs', normalizedId);
|
|
721
|
+
ticketFilter = normalizedId;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
try {
|
|
726
|
+
if (!fs.existsSync(targetPath)) {
|
|
727
|
+
if (ticketFilter) {
|
|
728
|
+
console.log(chalk.yellow(`ā ļø No cache files found for ticket ${ticketFilter}`));
|
|
729
|
+
} else {
|
|
730
|
+
console.log(chalk.green('ā
No review cache files to clean'));
|
|
731
|
+
}
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Count files before deletion
|
|
736
|
+
let fileCount = 0;
|
|
737
|
+
function countFiles(dir) {
|
|
738
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
739
|
+
for (const entry of entries) {
|
|
740
|
+
if (entry.isFile()) {
|
|
741
|
+
fileCount++;
|
|
742
|
+
} else if (entry.isDirectory()) {
|
|
743
|
+
countFiles(path.join(dir, entry.name));
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
countFiles(targetPath);
|
|
748
|
+
|
|
749
|
+
// Remove the directory and all contents
|
|
750
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
751
|
+
|
|
752
|
+
// Clean up empty parent directories
|
|
753
|
+
cleanupEmptyDirs(path.dirname(targetPath));
|
|
754
|
+
|
|
755
|
+
if (ticketFilter) {
|
|
756
|
+
console.log(chalk.green(`š§¹ Cleaned ${fileCount} review files for ticket ${ticketFilter}`));
|
|
757
|
+
} else {
|
|
758
|
+
console.log(chalk.green(`š§¹ Cleaned ${fileCount} review cache files`));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
} catch (error) {
|
|
762
|
+
console.error(chalk.red(`ā Failed to clean cache files: ${error.message}`));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Main review command implementation with AI-powered code analysis
|
|
768
|
+
* @param {string[]} args - Command line arguments
|
|
769
|
+
* @param {string} [args[0]] - Subcommand ('clean') or ticket ID
|
|
770
|
+
* @param {string} [args[1]] - Additional arguments (ticket ID for clean, flags)
|
|
771
|
+
* @description Analyzes staged git changes against ticket requirements using AI.
|
|
772
|
+
* Supports auto-detection of ticket ID from branch names, clipboard functionality,
|
|
773
|
+
* configurable acceptance thresholds, and organized cache management.
|
|
774
|
+
*
|
|
775
|
+
* @example
|
|
776
|
+
* // Basic usage
|
|
777
|
+
* await reviewCommand(['TKT-011']); // Review TKT-011
|
|
778
|
+
* await reviewCommand(['11']); // Same as above (auto-normalized)
|
|
779
|
+
* await reviewCommand([]); // Auto-detect from branch
|
|
780
|
+
*
|
|
781
|
+
* // With options
|
|
782
|
+
* await reviewCommand(['TKT-011', '--copy']); // Copy results to clipboard
|
|
783
|
+
* await reviewCommand(['TKT-011', '-c']); // Same as above
|
|
784
|
+
*
|
|
785
|
+
* // Clean commands
|
|
786
|
+
* await reviewCommand(['clean']); // Clean all cache files
|
|
787
|
+
* await reviewCommand(['clean', 'TKT-011']); // Clean specific ticket
|
|
788
|
+
*
|
|
789
|
+
* // Help
|
|
790
|
+
* await reviewCommand(['--help']); // Show help
|
|
791
|
+
*
|
|
792
|
+
* @throws {Error} Exits process with code 1 for validation errors or review failures
|
|
793
|
+
* @throws {Error} Exits process with code 0 for successful reviews above threshold
|
|
794
|
+
*/
|
|
795
|
+
async function reviewCommand(args) {
|
|
796
|
+
// Check for clean subcommand
|
|
797
|
+
if (args[0] === 'clean') {
|
|
798
|
+
cleanReviewFiles(args.slice(1));
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Check for help flag
|
|
803
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
804
|
+
console.log(`
|
|
805
|
+
${chalk.blue('š vibe review')} - AI-powered code review against ticket requirements
|
|
806
|
+
|
|
807
|
+
${chalk.bold('Usage:')}
|
|
808
|
+
vibe review [ticket-id] [options]
|
|
809
|
+
vibe review clean [ticket-id]
|
|
810
|
+
|
|
811
|
+
${chalk.bold('Arguments:')}
|
|
812
|
+
ticket-id Ticket ID to review against (e.g., TKT-001, 1, 11)
|
|
813
|
+
If omitted, extracts ticket ID from current branch name
|
|
814
|
+
|
|
815
|
+
${chalk.bold('Commands:')}
|
|
816
|
+
clean Clean review temp files (all or specific ticket)
|
|
817
|
+
|
|
818
|
+
${chalk.bold('Options:')}
|
|
819
|
+
--copy, -c Copy detailed feedback to clipboard
|
|
820
|
+
--verbose, -v Show detailed debug information
|
|
821
|
+
--help, -h Show this help message
|
|
822
|
+
|
|
823
|
+
${chalk.bold('Examples:')}
|
|
824
|
+
vibe review TKT-011 # Review staged changes against TKT-011
|
|
825
|
+
vibe review 11 # Same as above (auto-formats to TKT-011)
|
|
826
|
+
vibe review # Auto-detect from branch (feature/TKT-011-*)
|
|
827
|
+
vibe review TKT-001 -c # Review and copy results to clipboard
|
|
828
|
+
vibe review TKT-001 -v # Review with verbose debug output
|
|
829
|
+
vibe review clean # Clean all review temp files
|
|
830
|
+
vibe review clean TKT-011 # Clean temp files for specific ticket
|
|
831
|
+
|
|
832
|
+
${chalk.bold('Description:')}
|
|
833
|
+
Uses AI to analyze staged changes against ticket requirements.
|
|
834
|
+
Provides completion percentage, identifies missing items, and gives recommendations.
|
|
835
|
+
|
|
836
|
+
${chalk.bold('Color Coding:')}
|
|
837
|
+
š¢ 80%+ - Good (Green)
|
|
838
|
+
š” 60-79% - Warning (Yellow)
|
|
839
|
+
š“ <60% - Poor (Red)
|
|
840
|
+
`);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Check if we're in a git repository
|
|
845
|
+
if (!isGitRepository()) {
|
|
846
|
+
console.error(chalk.red('ā Not in a git repository. Please run this command from within a git repository.'));
|
|
847
|
+
process.exit(1);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Parse arguments
|
|
851
|
+
let ticketId = args[0];
|
|
852
|
+
const shouldCopy = args.includes('--copy') || args.includes('-c');
|
|
853
|
+
const isVerbose = args.includes('--verbose') || args.includes('-v');
|
|
854
|
+
|
|
855
|
+
// If no ticket ID provided, try to extract from branch
|
|
856
|
+
if (!ticketId) {
|
|
857
|
+
ticketId = extractTicketFromBranch();
|
|
858
|
+
if (!ticketId) {
|
|
859
|
+
console.error(chalk.red('ā Please provide a ticket ID or work on a feature branch'));
|
|
860
|
+
console.error(chalk.gray(' Examples: vibe review TKT-011, vibe review 11, or work on feature/TKT-011-* branch'));
|
|
861
|
+
process.exit(1);
|
|
862
|
+
}
|
|
863
|
+
console.log(chalk.blue(`š Detected ticket from branch: ${ticketId}`));
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Load configuration
|
|
867
|
+
let config;
|
|
868
|
+
try {
|
|
869
|
+
config = getConfig();
|
|
870
|
+
} catch (error) {
|
|
871
|
+
console.error(chalk.red(`ā Failed to load configuration: ${error.message}`));
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Validate ticket ID and existence
|
|
876
|
+
const validation = validateTicketId(ticketId);
|
|
877
|
+
if (!validation.isValid) {
|
|
878
|
+
console.error(chalk.red(`ā ${validation.error}`));
|
|
879
|
+
process.exit(1);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
try {
|
|
883
|
+
// Parse ticket requirements
|
|
884
|
+
const ticket = parseTicketRequirements(validation.ticketFile);
|
|
885
|
+
|
|
886
|
+
// Get staged changes
|
|
887
|
+
const stagedChanges = getStagedChanges();
|
|
888
|
+
if (!stagedChanges.trim()) {
|
|
889
|
+
console.error(chalk.yellow('ā ļø No staged changes found.'));
|
|
890
|
+
console.error(chalk.gray(' Use "git add <files>" to stage changes for review.'));
|
|
891
|
+
console.error(chalk.blue('š” Remember: Only staged changes are reviewed, not all working directory changes.'));
|
|
892
|
+
process.exit(1);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
console.log(chalk.blue('ā¹ļø Reviewing staged changes only. Make sure all changes you want reviewed are staged with "git add".'));
|
|
896
|
+
|
|
897
|
+
const stagedFiles = getStagedFiles();
|
|
898
|
+
|
|
899
|
+
// Create AI prompt
|
|
900
|
+
const prompt = createReviewPrompt(ticket, stagedChanges, stagedFiles);
|
|
901
|
+
|
|
902
|
+
// Debug output in verbose mode
|
|
903
|
+
if (isVerbose) {
|
|
904
|
+
console.log(chalk.gray('\nš Verbose Debug Information:'));
|
|
905
|
+
console.log(chalk.gray('ā'.repeat(40)));
|
|
906
|
+
console.log(chalk.gray(`Ticket ID: ${ticket.metadata.id}`));
|
|
907
|
+
console.log(chalk.gray(`Staged files count: ${stagedFiles.length}`));
|
|
908
|
+
console.log(chalk.gray(`Staged files: ${stagedFiles.join(', ')}`));
|
|
909
|
+
console.log(chalk.gray(`Changes size: ${stagedChanges.length} characters`));
|
|
910
|
+
console.log(chalk.gray(`Config acceptance threshold: ${config.review?.acceptance_threshold || 80}%`));
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Call AI for review
|
|
914
|
+
const review = await callClaudeForReview(prompt);
|
|
915
|
+
|
|
916
|
+
// More debug output in verbose mode
|
|
917
|
+
if (isVerbose) {
|
|
918
|
+
console.log(chalk.gray('\nš¤ AI Response Debug:'));
|
|
919
|
+
console.log(chalk.gray('ā'.repeat(40)));
|
|
920
|
+
console.log(chalk.gray(`Response completion: ${review.completionPercentage}%`));
|
|
921
|
+
console.log(chalk.gray(`Response status: ${review.status}`));
|
|
922
|
+
console.log(chalk.gray(`Completed items: ${review.completed.length}`));
|
|
923
|
+
console.log(chalk.gray(`Missing items: ${review.missing.length}`));
|
|
924
|
+
console.log(chalk.gray(`Issues found: ${review.issues.length}`));
|
|
925
|
+
console.log(chalk.gray(`Recommendations: ${review.recommendations.length}`));
|
|
926
|
+
console.log(chalk.gray('ā'.repeat(40)));
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Display results
|
|
930
|
+
displayReviewResults(review, ticket, config);
|
|
931
|
+
|
|
932
|
+
// Copy to clipboard if requested
|
|
933
|
+
if (shouldCopy) {
|
|
934
|
+
const clipboardText = formatReviewForClipboard(review, ticket);
|
|
935
|
+
copyToClipboard(clipboardText, ticket.metadata.id);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Exit with appropriate code for git hooks
|
|
939
|
+
const reviewConfig = config.review || {};
|
|
940
|
+
const acceptanceThreshold = reviewConfig.acceptance_threshold || 80;
|
|
941
|
+
|
|
942
|
+
if (review.completionPercentage < acceptanceThreshold) {
|
|
943
|
+
console.log(chalk.red(`\nš« Review failed: ${review.completionPercentage}% < ${acceptanceThreshold}% (acceptance threshold)`));
|
|
944
|
+
console.log(chalk.gray(' Configure acceptance_threshold in .vibe/config.yml'));
|
|
945
|
+
process.exit(1); // Fail git hook
|
|
946
|
+
} else {
|
|
947
|
+
console.log(chalk.green(`\nā
Review passed: ${review.completionPercentage}% > ${acceptanceThreshold}% (acceptance threshold)`));
|
|
948
|
+
process.exit(0); // Pass git hook
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
} catch (error) {
|
|
952
|
+
console.error(chalk.red(`ā ${error.message}`));
|
|
953
|
+
process.exit(1);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
export default reviewCommand;
|