@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,657 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import yaml from 'js-yaml';
4
+ import { getTicketsDir, getConfigPath } from '../../utils/index.js';
5
+
6
+ /**
7
+ * Load configuration from config.yml
8
+ * @returns {Object} Configuration object
9
+ */
10
+ function loadConfig() {
11
+ const configPath = getConfigPath();
12
+
13
+ if (!fs.existsSync(configPath)) {
14
+ console.error(`❌ Configuration file not found: ${configPath}`);
15
+ console.error('Run "vibe init" to initialize a VibeKit project.');
16
+ process.exit(1);
17
+ }
18
+
19
+ try {
20
+ const configContent = fs.readFileSync(configPath, 'utf-8');
21
+ return yaml.load(configContent);
22
+ } catch (error) {
23
+ console.error(`❌ Failed to parse configuration: ${error.message}`);
24
+ process.exit(1);
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Extract required sections from template
30
+ * @param {Object} config - Configuration object
31
+ * @returns {string[]} Array of required section headers
32
+ */
33
+ function getRequiredSections(config) {
34
+ const templatePath = config.tickets?.default_template || '.vibe/.templates/default.md';
35
+ const fullTemplatePath = path.resolve(templatePath);
36
+
37
+ if (!fs.existsSync(fullTemplatePath)) {
38
+ console.error(`❌ Template file not found: ${templatePath}`);
39
+ process.exit(1);
40
+ }
41
+
42
+ try {
43
+ const templateContent = fs.readFileSync(fullTemplatePath, 'utf-8');
44
+
45
+ // Extract section headers from template (lines starting with ##)
46
+ const sections = [];
47
+ const lines = templateContent.split('\n');
48
+
49
+ for (const line of lines) {
50
+ const match = line.match(/^##\s+(.+)/);
51
+ if (match) {
52
+ sections.push(match[1].trim());
53
+ }
54
+ }
55
+
56
+ return sections;
57
+ } catch (error) {
58
+ console.error(`❌ Failed to read template: ${error.message}`);
59
+ process.exit(1);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Get required frontmatter fields from template
65
+ * @param {Object} config - Configuration object
66
+ * @returns {string[]} Array of required frontmatter fields
67
+ */
68
+ function getRequiredFrontmatter(config) {
69
+ const templatePath = config.tickets?.default_template || '.vibe/.templates/default.md';
70
+ const fullTemplatePath = path.resolve(templatePath);
71
+
72
+ if (!fs.existsSync(fullTemplatePath)) {
73
+ return ['id', 'title', 'slug', 'status', 'priority', 'created_at', 'updated_at'];
74
+ }
75
+
76
+ try {
77
+ const templateContent = fs.readFileSync(fullTemplatePath, 'utf-8');
78
+
79
+ // Extract frontmatter from template
80
+ if (!templateContent.startsWith('---')) {
81
+ return ['id', 'title', 'slug', 'status', 'priority', 'created_at', 'updated_at'];
82
+ }
83
+
84
+ const parts = templateContent.split('---');
85
+ if (parts.length < 3) {
86
+ return ['id', 'title', 'slug', 'status', 'priority', 'created_at', 'updated_at'];
87
+ }
88
+
89
+ const frontmatter = yaml.load(parts[1]);
90
+ return Object.keys(frontmatter || {});
91
+ } catch (error) {
92
+ // Fallback to default required fields
93
+ return ['id', 'title', 'slug', 'status', 'priority', 'created_at', 'updated_at'];
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Validate ticket frontmatter
99
+ * @param {Object} frontmatter - Parsed frontmatter object
100
+ * @param {string} filename - Filename for error reporting
101
+ * @param {Object} config - Configuration object
102
+ * @param {string[]} requiredFields - Required frontmatter fields
103
+ * @returns {string[]} Array of validation errors
104
+ */
105
+ function validateFrontmatter(frontmatter, filename, config, requiredFields) {
106
+ const errors = [];
107
+
108
+ // Check required fields
109
+ for (const field of requiredFields) {
110
+ if (!frontmatter[field]) {
111
+ errors.push(`Missing required frontmatter field: ${field}`);
112
+ }
113
+ }
114
+
115
+ // Get valid options from config
116
+ const validStatuses = config.tickets?.status_options || ['open', 'in_progress', 'review', 'done'];
117
+ const validPriorities = config.tickets?.priority_options || ['low', 'medium', 'high', 'urgent'];
118
+
119
+ // Validate status
120
+ if (frontmatter.status && !validStatuses.includes(frontmatter.status)) {
121
+ errors.push(`Invalid status "${frontmatter.status}". Must be one of: ${validStatuses.join(', ')}`);
122
+ }
123
+
124
+ // Validate priority
125
+ if (frontmatter.priority && !validPriorities.includes(frontmatter.priority)) {
126
+ errors.push(`Invalid priority "${frontmatter.priority}". Must be one of: ${validPriorities.join(', ')}`);
127
+ }
128
+
129
+ // Validate ID format
130
+ if (frontmatter.id && !/^TKT-\d{3}$/.test(frontmatter.id)) {
131
+ errors.push(`Invalid ID format "${frontmatter.id}". Must follow pattern: TKT-XXX (e.g., TKT-001)`);
132
+ }
133
+
134
+ // Validate filename matches ID
135
+ if (frontmatter.id && !filename.startsWith(frontmatter.id)) {
136
+ errors.push(`Filename should start with ticket ID "${frontmatter.id}"`);
137
+ }
138
+
139
+ // Validate dates
140
+ if (frontmatter.created_at && isNaN(Date.parse(frontmatter.created_at))) {
141
+ errors.push(`Invalid created_at date format: ${frontmatter.created_at}`);
142
+ }
143
+
144
+ if (frontmatter.updated_at && isNaN(Date.parse(frontmatter.updated_at))) {
145
+ errors.push(`Invalid updated_at date format: ${frontmatter.updated_at}`);
146
+ }
147
+
148
+ return errors;
149
+ }
150
+
151
+ /**
152
+ * Validate ticket content sections
153
+ * @param {string} content - Ticket content (without frontmatter)
154
+ * @param {string[]} requiredSections - Required section headers
155
+ * @returns {Object} Object with errors and missing sections
156
+ */
157
+ function validateSections(content, requiredSections) {
158
+ const errors = [];
159
+ const missingSections = [];
160
+
161
+ for (const section of requiredSections) {
162
+ const sectionRegex = new RegExp(`^##\\s+${section}`, 'm');
163
+ if (!sectionRegex.test(content)) {
164
+ errors.push(`Missing required section: ## ${section}`);
165
+ missingSections.push(section);
166
+ }
167
+ }
168
+
169
+ // Check for empty sections
170
+ const sections = content.split(/^##\s+/m).filter(s => s.trim());
171
+ for (let i = 1; i < sections.length; i++) {
172
+ const sectionContent = sections[i].split(/^##\s+/m)[0].trim();
173
+ const sectionTitle = sectionContent.split('\n')[0];
174
+ const sectionBody = sectionContent.substring(sectionTitle.length).trim();
175
+
176
+ if (!sectionBody || sectionBody.length < 10) {
177
+ errors.push(`Section "## ${sectionTitle}" appears to be empty or too short`);
178
+ }
179
+ }
180
+
181
+ return { errors, missingSections };
182
+ }
183
+
184
+ /**
185
+ * Extract section content from template
186
+ * @param {Object} config - Configuration object
187
+ * @returns {Object} Object mapping section names to their default content
188
+ */
189
+ function getSectionDefaults(config) {
190
+ const templatePath = config.tickets?.default_template || '.vibe/.templates/default.md';
191
+ const fullTemplatePath = path.resolve(templatePath);
192
+
193
+ const sectionDefaults = {};
194
+
195
+ if (!fs.existsSync(fullTemplatePath)) {
196
+ return sectionDefaults;
197
+ }
198
+
199
+ try {
200
+ const templateContent = fs.readFileSync(fullTemplatePath, 'utf-8');
201
+
202
+ // Split by frontmatter to get only content part
203
+ const parts = templateContent.split('---');
204
+ if (parts.length < 3) {
205
+ return sectionDefaults;
206
+ }
207
+
208
+ const contentPart = parts.slice(2).join('---');
209
+
210
+ // Split content by section headers
211
+ const sections = contentPart.split(/^##\s+/m);
212
+
213
+ // First element is content before any section headers (ignore)
214
+ for (let i = 1; i < sections.length; i++) {
215
+ const section = sections[i];
216
+ const lines = section.split('\n');
217
+ const sectionName = lines[0].trim();
218
+ const sectionContent = lines.slice(1).join('\n');
219
+
220
+ sectionDefaults[sectionName] = sectionContent;
221
+ }
222
+ } catch (error) {
223
+ console.error(`❌ Failed to read template for section defaults: ${error.message}`);
224
+ }
225
+
226
+ return sectionDefaults;
227
+ }
228
+
229
+ /**
230
+ * Get frontmatter defaults from template
231
+ * @param {Object} config - Configuration object
232
+ * @returns {Object} Object with default frontmatter values
233
+ */
234
+ function getFrontmatterDefaults(config) {
235
+ const templatePath = config.tickets?.default_template || '.vibe/.templates/default.md';
236
+ const fullTemplatePath = path.resolve(templatePath);
237
+
238
+ const defaults = {};
239
+
240
+ if (!fs.existsSync(fullTemplatePath)) {
241
+ return defaults;
242
+ }
243
+
244
+ try {
245
+ const templateContent = fs.readFileSync(fullTemplatePath, 'utf-8');
246
+
247
+ if (!templateContent.startsWith('---')) {
248
+ return defaults;
249
+ }
250
+
251
+ const parts = templateContent.split('---');
252
+ if (parts.length < 3) {
253
+ return defaults;
254
+ }
255
+
256
+ const frontmatter = yaml.load(parts[1]);
257
+ return frontmatter || {};
258
+ } catch (error) {
259
+ console.error(`❌ Failed to read template for frontmatter defaults: ${error.message}`);
260
+ }
261
+
262
+ return defaults;
263
+ }
264
+
265
+ /**
266
+ * Fix missing frontmatter fields
267
+ * @param {Object} frontmatter - Current frontmatter object
268
+ * @param {string[]} missingFields - Array of missing field names
269
+ * @param {Object} config - Configuration object
270
+ * @param {string} filename - Filename for generating values
271
+ * @returns {Object} Fixed frontmatter object
272
+ */
273
+ function fixMissingFrontmatter(frontmatter, missingFields, config, filename) {
274
+ const fixedFrontmatter = { ...frontmatter };
275
+ const defaults = getFrontmatterDefaults(config);
276
+ const currentDate = new Date().toISOString();
277
+
278
+ for (const field of missingFields) {
279
+ if (field === 'slug') {
280
+ // Generate slug from title or filename
281
+ const title = fixedFrontmatter.title || filename.replace('.md', '').replace(/^TKT-\d+-/, '');
282
+ fixedFrontmatter.slug = fixedFrontmatter.id ?
283
+ `${fixedFrontmatter.id}-${title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')}` :
284
+ title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
285
+ } else if (field === 'created_at' || field === 'updated_at') {
286
+ fixedFrontmatter[field] = currentDate;
287
+ } else if (defaults[field] !== undefined) {
288
+ // Use template default if available
289
+ fixedFrontmatter[field] = defaults[field];
290
+ } else {
291
+ // Provide sensible defaults for common fields
292
+ switch (field) {
293
+ case 'status':
294
+ fixedFrontmatter[field] = config.tickets?.status_options?.[0] || 'open';
295
+ break;
296
+ case 'priority':
297
+ fixedFrontmatter[field] = 'medium';
298
+ break;
299
+ case 'title':
300
+ fixedFrontmatter[field] = filename.replace('.md', '').replace(/^TKT-\d+-/, '').replace(/-/g, ' ');
301
+ break;
302
+ case 'id':
303
+ const match = filename.match(/^(TKT-\d+)/);
304
+ if (match) {
305
+ fixedFrontmatter[field] = match[1];
306
+ }
307
+ break;
308
+ default:
309
+ fixedFrontmatter[field] = '';
310
+ }
311
+ }
312
+ }
313
+
314
+ return fixedFrontmatter;
315
+ }
316
+
317
+ /**
318
+ * Fix missing sections in ticket content
319
+ * @param {string} content - Original ticket content
320
+ * @param {string[]} missingSections - Array of missing section names
321
+ * @param {Object} config - Configuration object
322
+ * @returns {string} Fixed content with added sections
323
+ */
324
+ function fixMissingSections(content, missingSections, config) {
325
+ let fixedContent = content;
326
+
327
+ // Ensure content ends with newline for proper section spacing
328
+ if (fixedContent && !fixedContent.endsWith('\n')) {
329
+ fixedContent += '\n';
330
+ }
331
+
332
+ // Get section defaults from template
333
+ const sectionDefaults = getSectionDefaults(config);
334
+
335
+ // Add missing sections at the end
336
+ for (const section of missingSections) {
337
+ const defaultContent = sectionDefaults[section] || '\nTODO: Add content for this section\n';
338
+ fixedContent += `\n## ${section}${defaultContent}`;
339
+ }
340
+
341
+ return fixedContent;
342
+ }
343
+
344
+ /**
345
+ * Parse and validate a single ticket file
346
+ * @param {string} filePath - Path to the ticket file
347
+ * @param {Object} config - Configuration object
348
+ * @param {string[]} requiredFields - Required frontmatter fields
349
+ * @param {string[]} requiredSections - Required section headers
350
+ * @param {boolean} fixMode - Whether to fix issues automatically
351
+ * @returns {Object} Validation result with errors, warnings, and fixes
352
+ */
353
+ function validateTicketFile(filePath, config, requiredFields, requiredSections, fixMode = false) {
354
+ const filename = path.basename(filePath);
355
+ const result = {
356
+ filename,
357
+ errors: [],
358
+ warnings: [],
359
+ fixed: false,
360
+ missingSections: []
361
+ };
362
+
363
+ try {
364
+ const content = fs.readFileSync(filePath, 'utf-8');
365
+
366
+ // Check for frontmatter
367
+ if (!content.startsWith('---')) {
368
+ result.errors.push('File must start with YAML frontmatter (---)');
369
+ return result;
370
+ }
371
+
372
+ // Split frontmatter and content
373
+ const parts = content.split('---');
374
+ if (parts.length < 3) {
375
+ result.errors.push('Invalid frontmatter format. Must be enclosed in --- delimiters');
376
+ return result;
377
+ }
378
+
379
+ // Parse frontmatter
380
+ let frontmatter;
381
+ try {
382
+ frontmatter = yaml.load(parts[1]);
383
+ } catch (error) {
384
+ result.errors.push(`Invalid YAML frontmatter: ${error.message}`);
385
+ return result;
386
+ }
387
+
388
+ // Validate frontmatter
389
+ const frontmatterErrors = validateFrontmatter(frontmatter, filename, config, requiredFields);
390
+ result.errors.push(...frontmatterErrors);
391
+
392
+ // Validate content sections
393
+ const ticketContent = parts.slice(2).join('---');
394
+ const sectionValidation = validateSections(ticketContent, requiredSections);
395
+ result.errors.push(...sectionValidation.errors);
396
+ result.missingSections = sectionValidation.missingSections;
397
+
398
+ // Identify missing frontmatter fields for fixing
399
+ const missingFrontmatterFields = [];
400
+ for (const field of requiredFields) {
401
+ if (!frontmatter[field]) {
402
+ missingFrontmatterFields.push(field);
403
+ }
404
+ }
405
+
406
+ // Apply fixes if in fix mode
407
+ if (fixMode && (result.missingSections.length > 0 || missingFrontmatterFields.length > 0)) {
408
+ let fixedFrontmatter = frontmatter;
409
+ let fixedContent = ticketContent;
410
+ let fixedCount = 0;
411
+
412
+ // Fix missing frontmatter fields
413
+ if (missingFrontmatterFields.length > 0) {
414
+ fixedFrontmatter = fixMissingFrontmatter(frontmatter, missingFrontmatterFields, config, filename);
415
+ fixedCount += missingFrontmatterFields.length;
416
+ }
417
+
418
+ // Fix missing sections
419
+ if (result.missingSections.length > 0) {
420
+ fixedContent = fixMissingSections(ticketContent, result.missingSections, config);
421
+ fixedCount += result.missingSections.length;
422
+ }
423
+
424
+ // Write the fixed content
425
+ if (fixedCount > 0) {
426
+ const fixedFrontmatterYaml = yaml.dump(fixedFrontmatter, {
427
+ defaultStyle: null,
428
+ quotingType: '"',
429
+ forceQuotes: false
430
+ });
431
+ const newFileContent = '---\n' + fixedFrontmatterYaml + '---' + fixedContent;
432
+
433
+ try {
434
+ fs.writeFileSync(filePath, newFileContent, 'utf-8');
435
+ result.fixed = true;
436
+
437
+ // Remove the errors that were fixed
438
+ result.errors = result.errors.filter(error =>
439
+ !error.startsWith('Missing required section:') &&
440
+ !error.startsWith('Missing required frontmatter field:')
441
+ );
442
+
443
+ // Add success message for fixes
444
+ const messages = [];
445
+ if (missingFrontmatterFields.length > 0) {
446
+ messages.push(`${missingFrontmatterFields.length} missing frontmatter fields`);
447
+ }
448
+ if (result.missingSections.length > 0) {
449
+ messages.push(`${result.missingSections.length} missing sections`);
450
+ }
451
+ result.warnings.push(`Fixed ${messages.join(' and ')}`);
452
+ } catch (error) {
453
+ result.errors.push(`Failed to write fixes: ${error.message}`);
454
+ }
455
+ }
456
+ }
457
+
458
+ // Add warnings for common issues
459
+ if (ticketContent.includes('TODO') || ticketContent.includes('FIXME')) {
460
+ result.warnings.push('Contains TODO or FIXME comments');
461
+ }
462
+
463
+ if (frontmatter.title && frontmatter.title.length > 80) {
464
+ result.warnings.push('Title is longer than 80 characters');
465
+ }
466
+
467
+ } catch (error) {
468
+ result.errors.push(`Failed to read file: ${error.message}`);
469
+ }
470
+
471
+ return result;
472
+ }
473
+
474
+ /**
475
+ * Format validation results for display
476
+ * @param {Object[]} results - Array of validation results
477
+ * @param {boolean} verbose - Show warnings and details
478
+ * @param {boolean} fixMode - Whether fixes were applied
479
+ */
480
+ function displayResults(results, verbose = false, fixMode = false) {
481
+ let totalErrors = 0;
482
+ let totalWarnings = 0;
483
+ let filesWithIssues = 0;
484
+ let filesFixed = 0;
485
+
486
+ console.log('🔍 VibeKit Ticket Linter Results\n');
487
+
488
+ for (const result of results) {
489
+ const hasErrors = result.errors.length > 0;
490
+ const hasWarnings = result.warnings.length > 0;
491
+ const wasFixed = result.fixed;
492
+
493
+ if (wasFixed) {
494
+ filesFixed++;
495
+ console.log(`🔧 ${result.filename} (FIXED)`);
496
+ if (verbose || fixMode) {
497
+ result.warnings.forEach(warning => {
498
+ console.log(` Fixed: ${warning}`);
499
+ });
500
+ }
501
+ }
502
+
503
+ if (hasErrors || hasWarnings) {
504
+ if (!wasFixed) filesWithIssues++;
505
+
506
+ if (hasErrors) {
507
+ if (!wasFixed) console.log(`❌ ${result.filename}`);
508
+ result.errors.forEach(error => {
509
+ console.log(` Error: ${error}`);
510
+ });
511
+ totalErrors += result.errors.length;
512
+ } else if (!wasFixed) {
513
+ console.log(`⚠️ ${result.filename}`);
514
+ }
515
+
516
+ if ((verbose || fixMode) && hasWarnings && !wasFixed) {
517
+ result.warnings.forEach(warning => {
518
+ console.log(` Warning: ${warning}`);
519
+ });
520
+ }
521
+ totalWarnings += result.warnings.length;
522
+ console.log('');
523
+ } else if (verbose && !wasFixed) {
524
+ console.log(`✅ ${result.filename}`);
525
+ }
526
+ }
527
+
528
+ // Summary
529
+ console.log(`\n📊 Summary:`);
530
+ console.log(` Files checked: ${results.length}`);
531
+ console.log(` Files with issues: ${filesWithIssues}`);
532
+ if (filesFixed > 0) {
533
+ console.log(` Files fixed: ${filesFixed}`);
534
+ }
535
+ console.log(` Total errors: ${totalErrors}`);
536
+ console.log(` Total warnings: ${totalWarnings}`);
537
+
538
+ if (totalErrors === 0) {
539
+ if (filesFixed > 0) {
540
+ console.log('\n🎉 All issues have been fixed! Tickets are now properly formatted.');
541
+ } else {
542
+ console.log('\n🎉 All tickets are properly formatted!');
543
+ }
544
+ } else {
545
+ if (fixMode) {
546
+ console.log('\n💡 Some errors could not be automatically fixed. Please review and fix manually.');
547
+ } else {
548
+ console.log('\n💡 Fix the errors above to ensure consistent ticket formatting.');
549
+ console.log('💡 Use --fix flag to automatically fix missing sections.');
550
+ }
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Lint command implementation
556
+ * @param {string[]} args Command arguments
557
+ */
558
+ function lintCommand(args) {
559
+ let verbose = false;
560
+ let fixMode = false;
561
+ let specificFile = null;
562
+
563
+ // Parse arguments first to check for help
564
+ for (let i = 0; i < args.length; i++) {
565
+ if (args[i] === '--verbose' || args[i] === '-v') {
566
+ verbose = true;
567
+ } else if (args[i] === '--fix') {
568
+ fixMode = true;
569
+ } else if (args[i] === '--help' || args[i] === '-h') {
570
+ console.log(`
571
+ 🔍 vibe lint - Validate ticket documentation formatting
572
+
573
+ Usage:
574
+ vibe lint [options] [file]
575
+
576
+ Options:
577
+ --verbose, -v Show detailed output including warnings
578
+ --fix Automatically fix missing sections using template defaults
579
+ --help, -h Show this help message
580
+
581
+ Examples:
582
+ vibe lint # Lint all tickets
583
+ vibe lint --verbose # Show detailed output
584
+ vibe lint --fix # Lint and auto-fix missing sections
585
+ vibe lint TKT-001-example.md # Lint specific file
586
+
587
+ Validation Rules:
588
+ - Required frontmatter fields: id, title, slug, status, priority, created_at, updated_at
589
+ - Required sections: Description, Acceptance Criteria, Code Quality, etc.
590
+ - Valid statuses: defined in config.yml
591
+ - Valid priorities: defined in config.yml
592
+ - ID format: TKT-XXX (e.g., TKT-001)
593
+ `);
594
+ process.exit(0);
595
+ } else if (!args[i].startsWith('--')) {
596
+ specificFile = args[i];
597
+ }
598
+ }
599
+
600
+ // Load configuration after help check
601
+ const config = loadConfig();
602
+ const requiredFields = getRequiredFrontmatter(config);
603
+ const requiredSections = getRequiredSections(config);
604
+ const validStatuses = config.tickets?.status_options || ['open', 'in_progress', 'review', 'done'];
605
+ const validPriorities = config.tickets?.priority_options || ['low', 'medium', 'high', 'urgent'];
606
+
607
+ // Get tickets directory
608
+ const ticketDir = getTicketsDir();
609
+
610
+ if (!fs.existsSync(ticketDir)) {
611
+ console.error(`❌ Tickets directory not found: ${ticketDir}`);
612
+ console.error('Run "vibe init" to initialize a VibeKit project.');
613
+ process.exit(1);
614
+ }
615
+
616
+ let filesToCheck;
617
+
618
+ if (specificFile) {
619
+ // Check specific file
620
+ const filePath = path.isAbsolute(specificFile)
621
+ ? specificFile
622
+ : path.join(ticketDir, specificFile);
623
+
624
+ if (!fs.existsSync(filePath)) {
625
+ console.error(`❌ File not found: ${specificFile}`);
626
+ process.exit(1);
627
+ }
628
+
629
+ filesToCheck = [filePath];
630
+ } else {
631
+ // Check all markdown files
632
+ const files = fs.readdirSync(ticketDir)
633
+ .filter(file => file.endsWith('.md'))
634
+ .map(file => path.join(ticketDir, file));
635
+
636
+ if (files.length === 0) {
637
+ console.log('📝 No ticket files found to lint.');
638
+ process.exit(0);
639
+ }
640
+
641
+ filesToCheck = files;
642
+ }
643
+
644
+ // Validate all files
645
+ const results = filesToCheck.map(filePath =>
646
+ validateTicketFile(filePath, config, requiredFields, requiredSections, fixMode)
647
+ );
648
+
649
+ // Display results
650
+ displayResults(results, verbose, fixMode);
651
+
652
+ // Exit with error code if there are errors
653
+ const hasErrors = results.some(result => result.errors.length > 0);
654
+ process.exit(hasErrors ? 1 : 0);
655
+ }
656
+
657
+ export default lintCommand;