@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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +368 -0
  3. package/assets/config.yml +35 -0
  4. package/assets/default.md +47 -0
  5. package/assets/instructions/README.md +46 -0
  6. package/assets/instructions/claude.md +83 -0
  7. package/assets/instructions/codex.md +19 -0
  8. package/index.js +106 -0
  9. package/package.json +90 -0
  10. package/src/commands/close/index.js +66 -0
  11. package/src/commands/close/index.test.js +235 -0
  12. package/src/commands/get-started/index.js +138 -0
  13. package/src/commands/get-started/index.test.js +246 -0
  14. package/src/commands/init/index.js +51 -0
  15. package/src/commands/init/index.test.js +159 -0
  16. package/src/commands/link/index.js +395 -0
  17. package/src/commands/link/index.test.js +28 -0
  18. package/src/commands/lint/index.js +657 -0
  19. package/src/commands/lint/index.test.js +569 -0
  20. package/src/commands/list/index.js +131 -0
  21. package/src/commands/list/index.test.js +153 -0
  22. package/src/commands/new/index.js +305 -0
  23. package/src/commands/new/index.test.js +256 -0
  24. package/src/commands/refine/index.js +741 -0
  25. package/src/commands/refine/index.test.js +28 -0
  26. package/src/commands/review/index.js +957 -0
  27. package/src/commands/review/index.test.js +193 -0
  28. package/src/commands/start/index.js +180 -0
  29. package/src/commands/start/index.test.js +88 -0
  30. package/src/commands/unlink/index.js +123 -0
  31. package/src/commands/unlink/index.test.js +22 -0
  32. package/src/utils/arrow-select.js +233 -0
  33. package/src/utils/cli.js +489 -0
  34. package/src/utils/cli.test.js +9 -0
  35. package/src/utils/git.js +146 -0
  36. package/src/utils/git.test.js +330 -0
  37. package/src/utils/index.js +193 -0
  38. package/src/utils/index.test.js +375 -0
  39. package/src/utils/prompts.js +47 -0
  40. package/src/utils/prompts.test.js +165 -0
  41. package/src/utils/test-helpers.js +492 -0
  42. package/src/utils/ticket.js +423 -0
  43. 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;