@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,741 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { resolveTicketId, parseTicket, updateTicket } from '../../utils/ticket.js';
|
|
5
|
+
import { select, spinner, input, logger } from '../../utils/cli.js';
|
|
6
|
+
import { arrowSelect } from '../../utils/arrow-select.js';
|
|
7
|
+
|
|
8
|
+
// Configuration constants
|
|
9
|
+
const CLAUDE_SDK_TIMEOUT = 30000;
|
|
10
|
+
const ENHANCEMENT_MODEL = 'claude-3-5-sonnet-latest';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load VibeKit configuration
|
|
14
|
+
* @returns {Object} Configuration object
|
|
15
|
+
* @throws {Error} If configuration cannot be loaded
|
|
16
|
+
*/
|
|
17
|
+
function loadConfig() {
|
|
18
|
+
const configPath = path.join(process.cwd(), '.vibe', 'config.yml');
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(configPath)) {
|
|
21
|
+
throw new Error('No .vibe/config.yml found. Run "vibe init" first.');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
26
|
+
const config = yaml.load(configContent);
|
|
27
|
+
|
|
28
|
+
if (!config) {
|
|
29
|
+
throw new Error('Configuration file is empty or invalid');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return config;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw new Error(`Error reading config.yml: ${error.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if Claude Code SDK is available
|
|
40
|
+
* @returns {Promise<boolean>} True if SDK is available
|
|
41
|
+
*/
|
|
42
|
+
async function checkClaudeCodeSDK() {
|
|
43
|
+
try {
|
|
44
|
+
const { spawn } = await import('child_process');
|
|
45
|
+
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
const child = spawn('claude', ['--version'], {
|
|
48
|
+
stdio: 'pipe',
|
|
49
|
+
timeout: 5000
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
child.on('close', (code) => {
|
|
53
|
+
resolve(code === 0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
child.on('error', () => {
|
|
57
|
+
resolve(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Timeout fallback
|
|
61
|
+
const timeout = setTimeout(() => {
|
|
62
|
+
try {
|
|
63
|
+
child.kill('SIGTERM');
|
|
64
|
+
} catch (killError) {
|
|
65
|
+
// Ignore kill errors
|
|
66
|
+
}
|
|
67
|
+
resolve(false);
|
|
68
|
+
}, 5000);
|
|
69
|
+
|
|
70
|
+
child.on('exit', () => {
|
|
71
|
+
clearTimeout(timeout);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if AI is configured and Claude Code SDK is available
|
|
81
|
+
* @returns {Promise<Object>} AI configuration status
|
|
82
|
+
* @throws {Error} If configuration check fails
|
|
83
|
+
*/
|
|
84
|
+
async function checkAiConfiguration() {
|
|
85
|
+
try {
|
|
86
|
+
const config = loadConfig();
|
|
87
|
+
|
|
88
|
+
// Check if AI is enabled in config
|
|
89
|
+
if (!config.ai || !config.ai.enabled || config.ai.provider === 'none') {
|
|
90
|
+
return {
|
|
91
|
+
configured: false,
|
|
92
|
+
needsSetup: false,
|
|
93
|
+
reason: 'AI is not enabled in configuration'
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check for Claude Code SDK availability
|
|
98
|
+
const sdkAvailable = await checkClaudeCodeSDK();
|
|
99
|
+
if (sdkAvailable) {
|
|
100
|
+
return { configured: true };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// SDK not available - needs installation
|
|
104
|
+
return {
|
|
105
|
+
configured: false,
|
|
106
|
+
needsSetup: true,
|
|
107
|
+
reason: 'Claude Code SDK not found'
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
throw new Error(`Failed to check AI configuration: ${error.message}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Show Claude Code SDK installation information
|
|
116
|
+
* @returns {void}
|
|
117
|
+
*/
|
|
118
|
+
function showClaudeCodeInstallation() {
|
|
119
|
+
logger.error('Claude Code SDK not found.');
|
|
120
|
+
logger.info('VibeKit refine requires Claude Code SDK to enhance tickets.');
|
|
121
|
+
console.log('\nTo install Claude Code SDK, run:');
|
|
122
|
+
console.log(' npm install -g @anthropic-ai/claude-code');
|
|
123
|
+
console.log('\nOr visit: https://docs.anthropic.com/en/docs/claude-code');
|
|
124
|
+
logger.tip('After installation, run this command again.');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extract all sections from ticket content
|
|
131
|
+
* @param {string[]} contentLines - Array of content lines
|
|
132
|
+
* @returns {Array} Array of section objects
|
|
133
|
+
*/
|
|
134
|
+
function extractTicketSections(contentLines) {
|
|
135
|
+
if (!Array.isArray(contentLines)) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const sections = [];
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < contentLines.length; i++) {
|
|
142
|
+
const line = contentLines[i];
|
|
143
|
+
|
|
144
|
+
if (typeof line === 'string' && line.trim().startsWith('## ')) {
|
|
145
|
+
const sectionName = line.substring(3).trim();
|
|
146
|
+
|
|
147
|
+
if (sectionName) {
|
|
148
|
+
sections.push({
|
|
149
|
+
name: sectionName,
|
|
150
|
+
header: line.trim(),
|
|
151
|
+
index: i
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return sections;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Create enhancement prompt for Claude with dynamic sections
|
|
162
|
+
* @param {Object} ticketData - Parsed ticket data
|
|
163
|
+
* @param {string} refinementGoals - Specific refinement goals
|
|
164
|
+
* @returns {string} Generated prompt for AI enhancement
|
|
165
|
+
* @throws {Error} If ticket data is invalid
|
|
166
|
+
*/
|
|
167
|
+
function createEnhancementPrompt(ticketData, refinementGoals = '') {
|
|
168
|
+
if (!ticketData || !ticketData.contentLines || !ticketData.metadata) {
|
|
169
|
+
throw new Error('Invalid ticket data provided');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const ticketContent = ticketData.contentLines.join('\n');
|
|
173
|
+
const sections = extractTicketSections(ticketData.contentLines);
|
|
174
|
+
const ticketTitle = ticketData.metadata.title || 'Untitled Ticket';
|
|
175
|
+
|
|
176
|
+
const goalsText = typeof refinementGoals === 'string' && refinementGoals.trim() ?
|
|
177
|
+
`\n\nSpecific refinement goals: ${refinementGoals.trim()}` : '';
|
|
178
|
+
|
|
179
|
+
// Create dynamic JSON structure based on detected sections
|
|
180
|
+
const jsonStructure = {};
|
|
181
|
+
|
|
182
|
+
sections.forEach(section => {
|
|
183
|
+
const key = section.name.toLowerCase()
|
|
184
|
+
.replace(/\s+/g, '_')
|
|
185
|
+
.replace(/[^a-z0-9_]/g, '')
|
|
186
|
+
.substring(0, 50); // Limit key length
|
|
187
|
+
|
|
188
|
+
if (key) {
|
|
189
|
+
jsonStructure[key] = `enhanced ${section.name.toLowerCase()} content here`;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Always include slug for filename and title for metadata
|
|
194
|
+
jsonStructure.slug = 'short-kebab-case-description-only';
|
|
195
|
+
jsonStructure.title = 'Clear, descriptive title based on enhanced content';
|
|
196
|
+
|
|
197
|
+
const jsonExample = JSON.stringify(jsonStructure, null, 2);
|
|
198
|
+
|
|
199
|
+
return `You are a senior software engineer reviewing and enhancing a development ticket.
|
|
200
|
+
|
|
201
|
+
Ticket Title: ${ticketTitle}
|
|
202
|
+
Current Content:
|
|
203
|
+
${ticketContent}${goalsText}
|
|
204
|
+
|
|
205
|
+
IMPORTANT INSTRUCTIONS:
|
|
206
|
+
1. You must respond with ONLY valid JSON - no markdown, explanations, or code blocks
|
|
207
|
+
2. Enhance the existing sections found in the ticket
|
|
208
|
+
3. Keep responses concise, practical, and actionable
|
|
209
|
+
4. Generate a descriptive slug (without ticket ID prefix) and clear title
|
|
210
|
+
5. Focus on technical clarity and implementation details
|
|
211
|
+
6. Make the title descriptive and professional
|
|
212
|
+
7. Use developer-friendly formatting:
|
|
213
|
+
- File paths: \`src/components/Button.jsx\`
|
|
214
|
+
- Commands: \`npm install\`, \`vibe new\`, \`git commit\`
|
|
215
|
+
- Functions: \`handleClick()\`, \`useState()\`
|
|
216
|
+
- Code snippets in backticks for inline code
|
|
217
|
+
8. Keep "Testing & Test Cases" sections brief (2-4 key test points max)
|
|
218
|
+
9. Use "Implementation Notes" for technical details instead of "Notes"
|
|
219
|
+
10. Be specific about technical requirements and file locations
|
|
220
|
+
|
|
221
|
+
Expected JSON format:
|
|
222
|
+
${jsonExample}
|
|
223
|
+
|
|
224
|
+
Response must be valid JSON only.`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Execute Claude Code SDK command safely
|
|
229
|
+
* @param {string} prompt - The prompt to send to Claude
|
|
230
|
+
* @returns {Promise<string>} Claude's response
|
|
231
|
+
* @throws {Error} If execution fails
|
|
232
|
+
*/
|
|
233
|
+
async function executeClaudeCommand(prompt) {
|
|
234
|
+
if (typeof prompt !== 'string' || !prompt.trim()) {
|
|
235
|
+
throw new Error('Prompt must be a non-empty string');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const { writeFileSync, unlinkSync } = await import('fs');
|
|
239
|
+
const { exec } = await import('child_process');
|
|
240
|
+
const { tmpdir } = await import('os');
|
|
241
|
+
const { join } = await import('path');
|
|
242
|
+
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
const tempFile = join(tmpdir(), `vibe-prompt-${Date.now()}-${Math.random().toString(36).substring(7)}.txt`);
|
|
245
|
+
let childProcess = null;
|
|
246
|
+
|
|
247
|
+
// Cleanup function
|
|
248
|
+
const cleanup = () => {
|
|
249
|
+
try {
|
|
250
|
+
if (fs.existsSync(tempFile)) {
|
|
251
|
+
unlinkSync(tempFile);
|
|
252
|
+
}
|
|
253
|
+
} catch (cleanupError) {
|
|
254
|
+
// Ignore cleanup errors
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (childProcess) {
|
|
258
|
+
try {
|
|
259
|
+
childProcess.kill('SIGTERM');
|
|
260
|
+
} catch (killError) {
|
|
261
|
+
// Ignore kill errors
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
// Write prompt to temporary file
|
|
268
|
+
writeFileSync(tempFile, prompt, 'utf8');
|
|
269
|
+
} catch (writeError) {
|
|
270
|
+
reject(new Error(`Failed to write prompt file: ${writeError.message}`));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const command = `cat "${tempFile}" | claude --print --output-format json --model ${ENHANCEMENT_MODEL}`;
|
|
275
|
+
|
|
276
|
+
childProcess = exec(command, {
|
|
277
|
+
timeout: CLAUDE_SDK_TIMEOUT,
|
|
278
|
+
maxBuffer: 2 * 1024 * 1024, // 2MB buffer
|
|
279
|
+
killSignal: 'SIGTERM'
|
|
280
|
+
}, (error, stdout, stderr) => {
|
|
281
|
+
cleanup();
|
|
282
|
+
|
|
283
|
+
if (error) {
|
|
284
|
+
// Handle specific error types
|
|
285
|
+
if (error.code === 'ENOENT') {
|
|
286
|
+
reject(new Error('Claude Code SDK not found. Please install it first.'));
|
|
287
|
+
} else if (error.code === 'EACCES') {
|
|
288
|
+
reject(new Error('Permission denied accessing Claude Code SDK.'));
|
|
289
|
+
} else if (error.signal === 'SIGTERM') {
|
|
290
|
+
reject(new Error('Claude SDK operation timed out.'));
|
|
291
|
+
} else {
|
|
292
|
+
reject(new Error(`Claude Code SDK failed: ${error.message}`));
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Check for stderr output
|
|
298
|
+
if (stderr && stderr.trim()) {
|
|
299
|
+
console.warn(`ā ļø Claude SDK warning: ${stderr.trim()}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Validate stdout
|
|
303
|
+
if (!stdout || stdout.trim() === '') {
|
|
304
|
+
reject(new Error('Claude SDK returned empty response'));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
// Try to parse as JSON first
|
|
310
|
+
const sdkResponse = JSON.parse(stdout.trim());
|
|
311
|
+
const result = sdkResponse.result || sdkResponse.content || sdkResponse;
|
|
312
|
+
|
|
313
|
+
if (typeof result === 'string') {
|
|
314
|
+
resolve(result);
|
|
315
|
+
} else {
|
|
316
|
+
resolve(JSON.stringify(result));
|
|
317
|
+
}
|
|
318
|
+
} catch (parseError) {
|
|
319
|
+
// If JSON parsing fails, try to extract JSON from response
|
|
320
|
+
const jsonMatch = stdout.match(/{[\s\S]*}/);
|
|
321
|
+
if (jsonMatch) {
|
|
322
|
+
try {
|
|
323
|
+
JSON.parse(jsonMatch[0]); // Validate JSON
|
|
324
|
+
resolve(jsonMatch[0]);
|
|
325
|
+
} catch (secondParseError) {
|
|
326
|
+
reject(new Error(`Failed to parse Claude SDK response as JSON: ${secondParseError.message}`));
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
reject(new Error('Claude SDK response is not valid JSON'));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Handle process errors
|
|
335
|
+
childProcess.on('error', (error) => {
|
|
336
|
+
cleanup();
|
|
337
|
+
reject(new Error(`Failed to run Claude Code SDK: ${error.message}`));
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Set up timeout handler
|
|
341
|
+
setTimeout(() => {
|
|
342
|
+
if (childProcess && !childProcess.killed) {
|
|
343
|
+
cleanup();
|
|
344
|
+
reject(new Error('Claude SDK operation timed out'));
|
|
345
|
+
}
|
|
346
|
+
}, CLAUDE_SDK_TIMEOUT + 1000);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Enhance ticket using Claude Code SDK
|
|
352
|
+
* @param {Object} ticketData - Parsed ticket data
|
|
353
|
+
* @param {string} refinementGoals - Specific enhancement goals
|
|
354
|
+
* @returns {Promise<string>} Enhanced content from Claude
|
|
355
|
+
* @throws {Error} If enhancement fails
|
|
356
|
+
*/
|
|
357
|
+
async function enhanceTicketWithSDK(ticketData, refinementGoals = '') {
|
|
358
|
+
try {
|
|
359
|
+
const prompt = createEnhancementPrompt(ticketData, refinementGoals);
|
|
360
|
+
return await executeClaudeCommand(prompt);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
throw new Error(`Ticket enhancement failed: ${error.message}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Parse AI response from Claude Code SDK with dynamic sections
|
|
369
|
+
* @param {string} aiResponse - Raw AI response
|
|
370
|
+
* @param {Object} ticketData - Original ticket data
|
|
371
|
+
* @returns {Object} Parsed enhancement data
|
|
372
|
+
* @throws {Error} If response cannot be parsed
|
|
373
|
+
*/
|
|
374
|
+
function parseAiResponse(aiResponse, ticketData) {
|
|
375
|
+
if (typeof aiResponse !== 'string') {
|
|
376
|
+
throw new Error('AI response must be a string');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!ticketData || !ticketData.contentLines) {
|
|
380
|
+
throw new Error('Invalid ticket data provided');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let jsonResponse;
|
|
384
|
+
try {
|
|
385
|
+
jsonResponse = JSON.parse(aiResponse.trim());
|
|
386
|
+
} catch (parseError) {
|
|
387
|
+
throw new Error(`Failed to parse AI response as JSON: ${parseError.message}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (!jsonResponse || typeof jsonResponse !== 'object') {
|
|
391
|
+
throw new Error('AI response is not a valid object');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const sections = extractTicketSections(ticketData.contentLines);
|
|
395
|
+
|
|
396
|
+
const result = {
|
|
397
|
+
slug: jsonResponse.slug || 'enhanced-ticket',
|
|
398
|
+
title: jsonResponse.title || null
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// Validate slug
|
|
402
|
+
if (typeof result.slug !== 'string' || !result.slug.trim()) {
|
|
403
|
+
result.slug = 'enhanced-ticket';
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Validate and clean title
|
|
407
|
+
if (result.title && typeof result.title === 'string' && result.title.trim()) {
|
|
408
|
+
result.title = result.title.trim();
|
|
409
|
+
} else {
|
|
410
|
+
result.title = null; // Don't update title if invalid or empty
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Map JSON keys back to section headers
|
|
414
|
+
sections.forEach(section => {
|
|
415
|
+
const key = section.name.toLowerCase()
|
|
416
|
+
.replace(/\s+/g, '_')
|
|
417
|
+
.replace(/[^a-z0-9_]/g, '')
|
|
418
|
+
.substring(0, 50);
|
|
419
|
+
|
|
420
|
+
if (key && jsonResponse[key]) {
|
|
421
|
+
const content = jsonResponse[key];
|
|
422
|
+
if (typeof content === 'string' && content.trim()) {
|
|
423
|
+
result[section.name] = content.trim();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Ensure we have at least some enhanced content
|
|
429
|
+
const hasEnhancedContent = Object.keys(result).some(key =>
|
|
430
|
+
key !== 'slug' && key !== 'title' && result[key]
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
if (!hasEnhancedContent) {
|
|
434
|
+
throw new Error('AI response contains no enhanced content for existing sections');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Validate command arguments
|
|
444
|
+
* @param {Array} args - Command arguments
|
|
445
|
+
* @throws {Error} If arguments are invalid
|
|
446
|
+
*/
|
|
447
|
+
function validateArguments(args) {
|
|
448
|
+
if (!Array.isArray(args) || args.length === 0 || !args[0]) {
|
|
449
|
+
throw new Error('Please provide a ticket ID. Usage: vibe refine <ticket-id>\n Examples: vibe refine 8, vibe refine TKT-008, vibe refine 008');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Get refinement goals based on command context
|
|
455
|
+
* @param {boolean} fromNewCommand - Whether called from new command
|
|
456
|
+
* @returns {Promise<string>} Refinement goals
|
|
457
|
+
*/
|
|
458
|
+
async function getRefinementGoals(fromNewCommand) {
|
|
459
|
+
if (fromNewCommand) {
|
|
460
|
+
return 'technical details, code quality, general enhancement, testing - keep it technical and respect existing code boundaries';
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
logger.step('What specific improvements would you like to focus on?');
|
|
464
|
+
return await input('Enter your refinement goals', {
|
|
465
|
+
defaultValue: 'general enhancement'
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Handle interactive refinement options
|
|
471
|
+
* @returns {Promise<string>} User's choice
|
|
472
|
+
*/
|
|
473
|
+
async function handleRefinementOptions() {
|
|
474
|
+
console.log('\nš§ Refinement Options');
|
|
475
|
+
|
|
476
|
+
let choice;
|
|
477
|
+
try {
|
|
478
|
+
choice = await arrowSelect('Choose an action', [
|
|
479
|
+
{ name: 'Apply refinements to ticket', value: '1' },
|
|
480
|
+
{ name: 'Ask for changes/improvements', value: '2' },
|
|
481
|
+
{ name: 'View diff in terminal', value: '3' },
|
|
482
|
+
{ name: 'Cancel and exit', value: '4' }
|
|
483
|
+
], '1');
|
|
484
|
+
} catch (arrowError) {
|
|
485
|
+
console.log('Arrow select failed, falling back to numbered selection...');
|
|
486
|
+
choice = await select('Choose an action', [
|
|
487
|
+
{ name: 'Apply refinements to ticket', value: '1' },
|
|
488
|
+
{ name: 'Ask for changes/improvements', value: '2' },
|
|
489
|
+
{ name: 'View diff in terminal', value: '3' },
|
|
490
|
+
{ name: 'Cancel and exit', value: '4' }
|
|
491
|
+
], '1');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return choice;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Apply refinements to ticket
|
|
499
|
+
* @param {Object} sections - Enhanced sections
|
|
500
|
+
* @param {Object} ticketInfo - Ticket info
|
|
501
|
+
* @param {Object} ticketData - Original ticket data
|
|
502
|
+
* @returns {Promise<void>}
|
|
503
|
+
*/
|
|
504
|
+
async function applyRefinements(sections, ticketInfo, ticketData) {
|
|
505
|
+
const updates = {
|
|
506
|
+
slug: sections.slug
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// Add title if provided
|
|
510
|
+
if (sections.title) {
|
|
511
|
+
updates.title = sections.title;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Add all dynamic sections to updates
|
|
515
|
+
Object.keys(sections).forEach(sectionName => {
|
|
516
|
+
if (sectionName !== 'slug' && sectionName !== 'title') {
|
|
517
|
+
updates[sectionName] = sections[sectionName];
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const result = updateTicket(ticketInfo.path, ticketData, updates);
|
|
522
|
+
if (result.success) {
|
|
523
|
+
logger.success(`Ticket ${ticketInfo.id} has been refined and updated!`);
|
|
524
|
+
logger.info('Review the updated ticket and make any additional adjustments as needed.');
|
|
525
|
+
if (result.message) {
|
|
526
|
+
logger.info(result.message);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Force immediate cleanup and exit
|
|
530
|
+
process.stdin.removeAllListeners();
|
|
531
|
+
if (process.stdin.isTTY) {
|
|
532
|
+
process.stdin.setRawMode(false);
|
|
533
|
+
}
|
|
534
|
+
process.stdin.pause();
|
|
535
|
+
|
|
536
|
+
// Force exit after brief cleanup
|
|
537
|
+
setTimeout(() => {
|
|
538
|
+
process.exit(0);
|
|
539
|
+
}, 100);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Request followup changes
|
|
545
|
+
* @param {Object} currentSections - Current enhanced sections
|
|
546
|
+
* @param {Object} ticketData - Original ticket data
|
|
547
|
+
* @returns {Promise<Object>} Updated sections
|
|
548
|
+
*/
|
|
549
|
+
async function requestFollowupChanges(currentSections, ticketData) {
|
|
550
|
+
logger.step('What changes would you like?');
|
|
551
|
+
const followupRequest = await input('Enter your changes', { required: true });
|
|
552
|
+
|
|
553
|
+
if (!followupRequest.trim()) {
|
|
554
|
+
return currentSections;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const loadingSpinner = spinner('š Processing your request...');
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
// Create a followup prompt with dynamic sections
|
|
561
|
+
const sectionsList = Object.keys(currentSections)
|
|
562
|
+
.filter(key => key !== 'slug' && currentSections[key])
|
|
563
|
+
.map(key => `PREVIOUS ${key.toUpperCase()}:\n${currentSections[key] || 'Not provided'}`)
|
|
564
|
+
.join('\n\n');
|
|
565
|
+
|
|
566
|
+
const followupPrompt = `Previous enhancement for ticket "${(ticketData.metadata && ticketData.metadata.title) || 'Unknown'}":
|
|
567
|
+
|
|
568
|
+
${sectionsList}
|
|
569
|
+
|
|
570
|
+
USER REQUEST: ${followupRequest}
|
|
571
|
+
|
|
572
|
+
Please provide updated enhancements based on the user's request. Return ONLY a JSON object with the same section keys as provided above.`;
|
|
573
|
+
|
|
574
|
+
const refinedResponse = await enhanceTicketWithSDK({
|
|
575
|
+
contentLines: [followupPrompt],
|
|
576
|
+
metadata: ticketData.metadata || {}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
loadingSpinner.succeed('Suggestions updated!');
|
|
580
|
+
return parseAiResponse(refinedResponse, ticketData);
|
|
581
|
+
|
|
582
|
+
} catch (error) {
|
|
583
|
+
loadingSpinner.fail('Failed to refine suggestions');
|
|
584
|
+
|
|
585
|
+
if (error.message.includes('Claude Code SDK failed')) {
|
|
586
|
+
logger.error('AI enhancement service is unavailable. Please try again later.');
|
|
587
|
+
logger.tip('You can continue with the current refinements or cancel.');
|
|
588
|
+
} else {
|
|
589
|
+
logger.error('Failed to process your request. Please try again with different input.');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return currentSections;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* View refinement diff
|
|
598
|
+
* @param {Object} sections - Enhanced sections
|
|
599
|
+
* @returns {Promise<void>}
|
|
600
|
+
*/
|
|
601
|
+
async function viewDiff(sections) {
|
|
602
|
+
// Clear terminal for clean diff view
|
|
603
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
604
|
+
|
|
605
|
+
console.log('ā'.repeat(80));
|
|
606
|
+
console.log('š TICKET REFINEMENT DIFF');
|
|
607
|
+
console.log('ā'.repeat(80));
|
|
608
|
+
|
|
609
|
+
// Show title if updated
|
|
610
|
+
if (sections.title) {
|
|
611
|
+
console.log(`\nš¹ Title (refined):`);
|
|
612
|
+
console.log('ā'.repeat(40));
|
|
613
|
+
console.log(sections.title);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Show all dynamic sections that were enhanced
|
|
617
|
+
Object.keys(sections).forEach(sectionName => {
|
|
618
|
+
if (sectionName !== 'slug' && sectionName !== 'title' && sections[sectionName]) {
|
|
619
|
+
console.log(`\nš¹ ${sectionName} (refined):`);
|
|
620
|
+
console.log('ā'.repeat(40));
|
|
621
|
+
console.log(sections[sectionName]);
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
console.log('\n' + 'ā'.repeat(80));
|
|
626
|
+
await input('Press Enter to continue', { defaultValue: '' });
|
|
627
|
+
|
|
628
|
+
// Clear terminal again after viewing diff
|
|
629
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Main refine command implementation
|
|
634
|
+
* @param {Array} args - Command arguments
|
|
635
|
+
* @param {Object} options - Command options
|
|
636
|
+
* @returns {Promise<void>}
|
|
637
|
+
*/
|
|
638
|
+
async function refineCommand(args, options = {}) {
|
|
639
|
+
const { fromNewCommand = false } = options;
|
|
640
|
+
|
|
641
|
+
try {
|
|
642
|
+
// Validate arguments
|
|
643
|
+
validateArguments(args);
|
|
644
|
+
const ticketInput = args[0];
|
|
645
|
+
|
|
646
|
+
logger.step(`Analyzing ticket ${ticketInput}...`);
|
|
647
|
+
|
|
648
|
+
// Check AI configuration
|
|
649
|
+
const aiStatus = await checkAiConfiguration();
|
|
650
|
+
if (!aiStatus.configured) {
|
|
651
|
+
if (aiStatus.needsSetup) {
|
|
652
|
+
showClaudeCodeInstallation();
|
|
653
|
+
} else {
|
|
654
|
+
logger.error('AI is not enabled in config. Run "vibe link" first.');
|
|
655
|
+
}
|
|
656
|
+
throw new Error(aiStatus.reason || 'AI configuration check failed');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Resolve ticket ID
|
|
660
|
+
const ticketInfo = resolveTicketId(ticketInput);
|
|
661
|
+
if (!ticketInfo) {
|
|
662
|
+
throw new Error(`Ticket ${ticketInput} not found. Use "vibe list" to see available tickets.`);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Parse ticket
|
|
666
|
+
const ticketData = parseTicket(ticketInfo.path);
|
|
667
|
+
logger.info(`Found: ${ticketInfo.id} - ${ticketData.metadata.title}`);
|
|
668
|
+
|
|
669
|
+
// Get refinement goals
|
|
670
|
+
const refinementGoals = await getRefinementGoals(fromNewCommand);
|
|
671
|
+
if (fromNewCommand) {
|
|
672
|
+
logger.step('Applying technical refinement with focus on code quality and testing...');
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Start enhancement process
|
|
676
|
+
const loadingSpinner = spinner('š¤ Loading context...');
|
|
677
|
+
|
|
678
|
+
let aiResponse;
|
|
679
|
+
try {
|
|
680
|
+
loadingSpinner.update('š§ Analyzing ticket content...');
|
|
681
|
+
await new Promise(resolve => setTimeout(resolve, 800)); // Brief pause for UX
|
|
682
|
+
|
|
683
|
+
loadingSpinner.update('⨠Generating enhancements...');
|
|
684
|
+
aiResponse = await enhanceTicketWithSDK(ticketData, refinementGoals);
|
|
685
|
+
loadingSpinner.succeed('Completed triaging and refinement!');
|
|
686
|
+
|
|
687
|
+
} catch (error) {
|
|
688
|
+
loadingSpinner.fail('Enhancement failed');
|
|
689
|
+
|
|
690
|
+
if (error.message.includes('Claude Code SDK failed')) {
|
|
691
|
+
logger.error('AI enhancement service is unavailable.');
|
|
692
|
+
logger.info('This could be due to:');
|
|
693
|
+
console.log(' ⢠Claude Code SDK not installed or configured');
|
|
694
|
+
console.log(' ⢠Network connectivity issues');
|
|
695
|
+
console.log(' ⢠API rate limits or authentication problems');
|
|
696
|
+
logger.tip('You can still view and edit the ticket manually.');
|
|
697
|
+
} else {
|
|
698
|
+
logger.error('Failed to enhance ticket.');
|
|
699
|
+
logger.tip('Please check your Claude Code SDK installation and try again.');
|
|
700
|
+
}
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Parse AI response
|
|
705
|
+
const sections = parseAiResponse(aiResponse, ticketData);
|
|
706
|
+
|
|
707
|
+
// Interactive enhancement mode
|
|
708
|
+
let currentSections = sections;
|
|
709
|
+
|
|
710
|
+
while (true) {
|
|
711
|
+
const choice = await handleRefinementOptions();
|
|
712
|
+
|
|
713
|
+
if (choice === '1') {
|
|
714
|
+
await applyRefinements(currentSections, ticketInfo, ticketData);
|
|
715
|
+
return; // Clean exit after successful update
|
|
716
|
+
|
|
717
|
+
} else if (choice === '2') {
|
|
718
|
+
currentSections = await requestFollowupChanges(currentSections, ticketData);
|
|
719
|
+
|
|
720
|
+
} else if (choice === '3') {
|
|
721
|
+
await viewDiff(currentSections);
|
|
722
|
+
|
|
723
|
+
} else if (choice === '4') {
|
|
724
|
+
logger.info('Refinement cancelled. Ticket remains unchanged.');
|
|
725
|
+
return; // Clean exit when cancelled
|
|
726
|
+
|
|
727
|
+
} else {
|
|
728
|
+
logger.warning('Invalid choice. Please select 1-4.');
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
} catch (error) {
|
|
733
|
+
logger.error(`Refinement failed: ${error.message}`);
|
|
734
|
+
|
|
735
|
+
if (fromNewCommand) {
|
|
736
|
+
logger.tip('You can manually enhance the ticket later with: vibe refine ' + ticketInfo.id.replace('TKT-', ''));
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export default refineCommand;
|