@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,569 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import {
5
+ createTempDir,
6
+ cleanupTempDir,
7
+ mockConsole,
8
+ mockProcessCwd,
9
+ mockProcessExit,
10
+ createMockVibeProject
11
+ } from '../../utils/test-helpers.js';
12
+ import lintCommand from './index.js';
13
+
14
+ describe('lint command', () => {
15
+ let tempDir;
16
+ let consoleMock;
17
+ let restoreCwd;
18
+ let exitMock;
19
+
20
+ beforeEach(() => {
21
+ // Create temp directory
22
+ tempDir = createTempDir('lint-test');
23
+
24
+ // Mock console and process
25
+ consoleMock = mockConsole();
26
+ restoreCwd = mockProcessCwd(tempDir);
27
+ exitMock = mockProcessExit();
28
+ });
29
+
30
+ afterEach(() => {
31
+ // Restore mocks
32
+ consoleMock.restore();
33
+ restoreCwd();
34
+ exitMock.restore();
35
+
36
+ // Cleanup temp directory
37
+ cleanupTempDir(tempDir);
38
+ });
39
+
40
+ describe('help functionality', () => {
41
+ it('should show help when --help flag is provided', () => {
42
+ // Act
43
+ expect(() => lintCommand(['--help'])).toThrow('process.exit(0)');
44
+
45
+ // Assert
46
+ expect(exitMock.exitCalls).toContain(0);
47
+ expect(consoleMock.logs.log.some(log =>
48
+ log.includes('vibe lint - Validate ticket documentation formatting')
49
+ )).toBe(true);
50
+ });
51
+
52
+ it('should show help when -h flag is provided', () => {
53
+ // Act
54
+ expect(() => lintCommand(['-h'])).toThrow('process.exit(0)');
55
+
56
+ // Assert
57
+ expect(exitMock.exitCalls).toContain(0);
58
+ expect(consoleMock.logs.log.some(log =>
59
+ log.includes('Usage:')
60
+ )).toBe(true);
61
+ });
62
+ });
63
+
64
+ describe('directory validation', () => {
65
+ it('should exit with error when configuration file does not exist', () => {
66
+ // Act
67
+ expect(() => lintCommand([])).toThrow('process.exit(1)');
68
+
69
+ // Assert
70
+ expect(exitMock.exitCalls).toContain(1);
71
+ expect(consoleMock.logs.error.some(error =>
72
+ error.includes('Configuration file not found')
73
+ )).toBe(true);
74
+ });
75
+
76
+ it('should show message when no ticket files found', () => {
77
+ // Arrange - create empty vibe project
78
+ createMockVibeProject(tempDir, { withTickets: [] });
79
+
80
+ // Act
81
+ expect(() => lintCommand([])).toThrow('process.exit(0)');
82
+
83
+ // Assert
84
+ expect(exitMock.exitCalls).toContain(0);
85
+ expect(consoleMock.logs.log).toContain('📝 No ticket files found to lint.');
86
+ });
87
+ });
88
+
89
+ describe('ticket validation', () => {
90
+ it('should validate properly formatted ticket successfully', () => {
91
+ // Arrange - create mock project with valid ticket
92
+ const validTicket = {
93
+ id: 'TKT-001',
94
+ title: 'Test ticket',
95
+ slug: 'test-ticket',
96
+ status: 'open',
97
+ priority: 'medium',
98
+ created_at: '2025-01-01T12:00:00.000Z',
99
+ updated_at: '2025-01-01T12:00:00.000Z',
100
+ description: `## Description
101
+
102
+ This is a test description with enough content to pass validation.
103
+
104
+ ## Acceptance Criteria
105
+
106
+ This section has sufficient content for validation purposes.
107
+
108
+ ## Code Quality
109
+
110
+ This section contains adequate content for testing validation logic.
111
+
112
+ ## Implementation Notes
113
+
114
+ This section provides enough details for the validation system.
115
+
116
+ ## Design / UX Considerations
117
+
118
+ This section includes sufficient UX considerations for testing.
119
+
120
+ ## Testing & Test Cases
121
+
122
+ This section contains adequate test case information.
123
+
124
+ ## AI Prompt
125
+
126
+ This section provides sufficient AI prompt content for testing.
127
+
128
+ ## Expected AI Output
129
+
130
+ This section contains enough information about expected output.
131
+
132
+ ## AI Workflow
133
+
134
+ This section has adequate workflow information for validation.`
135
+ };
136
+
137
+ createMockVibeProject(tempDir, { withTickets: [validTicket] });
138
+
139
+ // Act
140
+ expect(() => lintCommand([])).toThrow('process.exit(0)');
141
+
142
+ // Assert
143
+ expect(exitMock.exitCalls).toContain(0);
144
+ expect(consoleMock.logs.log.some(log =>
145
+ log.includes('All tickets are properly formatted!')
146
+ )).toBe(true);
147
+ });
148
+
149
+ it('should identify missing frontmatter fields', () => {
150
+ // Arrange - create ticket with missing frontmatter
151
+ const invalidTicket = {
152
+ id: 'TKT-001',
153
+ title: 'Test ticket',
154
+ // Missing slug, status, priority, created_at, updated_at
155
+ description: `## Description
156
+
157
+ This ticket has missing frontmatter fields for testing validation.
158
+
159
+ ## Acceptance Criteria
160
+
161
+ This section has sufficient content for validation purposes.
162
+
163
+ ## Code Quality
164
+
165
+ This section contains adequate content for testing validation logic.
166
+
167
+ ## Implementation Notes
168
+
169
+ This section provides enough details for the validation system.
170
+
171
+ ## Design / UX Considerations
172
+
173
+ This section includes sufficient UX considerations for testing.
174
+
175
+ ## Testing & Test Cases
176
+
177
+ This section contains adequate test case information.
178
+
179
+ ## AI Prompt
180
+
181
+ This section provides sufficient AI prompt content for testing.
182
+
183
+ ## Expected AI Output
184
+
185
+ This section contains enough information about expected output.
186
+
187
+ ## AI Workflow
188
+
189
+ This section has adequate workflow information for validation.`
190
+ };
191
+
192
+ createMockVibeProject(tempDir, { withTickets: [invalidTicket] });
193
+
194
+ // Act
195
+ expect(() => lintCommand([])).toThrow('process.exit(1)');
196
+
197
+ // Assert
198
+ expect(exitMock.exitCalls).toContain(1);
199
+ expect(consoleMock.logs.log.some(log =>
200
+ log.includes('Error: Missing required frontmatter field: slug')
201
+ )).toBe(true);
202
+ });
203
+
204
+ it('should identify missing required sections', () => {
205
+ // Arrange - create ticket with missing sections
206
+ const incompleteTicket = {
207
+ id: 'TKT-001',
208
+ title: 'Test ticket',
209
+ slug: 'test-ticket',
210
+ status: 'open',
211
+ priority: 'medium',
212
+ created_at: '2025-01-01T12:00:00.000Z',
213
+ updated_at: '2025-01-01T12:00:00.000Z',
214
+ description: 'Only description, missing other sections'
215
+ };
216
+
217
+ createMockVibeProject(tempDir, { withTickets: [incompleteTicket] });
218
+
219
+ // Act
220
+ expect(() => lintCommand([])).toThrow('process.exit(1)');
221
+
222
+ // Assert
223
+ expect(exitMock.exitCalls).toContain(1);
224
+ expect(consoleMock.logs.log.some(log =>
225
+ log.includes('Missing required section: ## Code Quality')
226
+ )).toBe(true);
227
+ });
228
+
229
+ it('should validate status values', () => {
230
+ // Arrange - create ticket with invalid status
231
+ const ticketContent = `---
232
+ id: TKT-001
233
+ title: Test ticket
234
+ slug: test-ticket
235
+ status: invalid_status
236
+ priority: medium
237
+ created_at: 2025-01-01T12:00:00.000Z
238
+ updated_at: 2025-01-01T12:00:00.000Z
239
+ ---
240
+
241
+ ## Description
242
+ Test
243
+
244
+ ## Acceptance Criteria
245
+ Test
246
+
247
+ ## Code Quality
248
+ Test
249
+
250
+ ## Implementation Notes
251
+ Test
252
+
253
+ ## Design / UX Considerations
254
+ Test
255
+
256
+ ## Testing & Test Cases
257
+ Test
258
+
259
+ ## AI Prompt
260
+ Test
261
+
262
+ ## Expected AI Output
263
+ Test
264
+
265
+ ## AI Workflow
266
+ Test`;
267
+
268
+ createMockVibeProject(tempDir);
269
+ const ticketPath = path.join(tempDir, '.vibe', 'tickets', 'TKT-001-test.md');
270
+ fs.writeFileSync(ticketPath, ticketContent, 'utf-8');
271
+
272
+ // Act
273
+ expect(() => lintCommand([])).toThrow('process.exit(1)');
274
+
275
+ // Assert
276
+ expect(exitMock.exitCalls).toContain(1);
277
+ expect(consoleMock.logs.log.some(log =>
278
+ log.includes('Invalid status "invalid_status"')
279
+ )).toBe(true);
280
+ });
281
+
282
+ it('should validate priority values', () => {
283
+ // Arrange - create ticket with invalid priority
284
+ const ticketContent = `---
285
+ id: TKT-001
286
+ title: Test ticket
287
+ slug: test-ticket
288
+ status: open
289
+ priority: invalid_priority
290
+ created_at: 2025-01-01T12:00:00.000Z
291
+ updated_at: 2025-01-01T12:00:00.000Z
292
+ ---
293
+
294
+ ## Description
295
+ Test
296
+
297
+ ## Acceptance Criteria
298
+ Test
299
+
300
+ ## Code Quality
301
+ Test
302
+
303
+ ## Implementation Notes
304
+ Test
305
+
306
+ ## Design / UX Considerations
307
+ Test
308
+
309
+ ## Testing & Test Cases
310
+ Test
311
+
312
+ ## AI Prompt
313
+ Test
314
+
315
+ ## Expected AI Output
316
+ Test
317
+
318
+ ## AI Workflow
319
+ Test`;
320
+
321
+ createMockVibeProject(tempDir);
322
+ const ticketPath = path.join(tempDir, '.vibe', 'tickets', 'TKT-001-test.md');
323
+ fs.writeFileSync(ticketPath, ticketContent, 'utf-8');
324
+
325
+ // Act
326
+ expect(() => lintCommand([])).toThrow('process.exit(1)');
327
+
328
+ // Assert
329
+ expect(exitMock.exitCalls).toContain(1);
330
+ expect(consoleMock.logs.log.some(log =>
331
+ log.includes('Invalid priority "invalid_priority"')
332
+ )).toBe(true);
333
+ });
334
+
335
+ it('should validate ID format', () => {
336
+ // Arrange - create ticket with invalid ID format
337
+ const ticketContent = `---
338
+ id: INVALID-ID
339
+ title: Test ticket
340
+ slug: test-ticket
341
+ status: open
342
+ priority: medium
343
+ created_at: 2025-01-01T12:00:00.000Z
344
+ updated_at: 2025-01-01T12:00:00.000Z
345
+ ---
346
+
347
+ ## Description
348
+ Test
349
+
350
+ ## Acceptance Criteria
351
+ Test
352
+
353
+ ## Code Quality
354
+ Test
355
+
356
+ ## Implementation Notes
357
+ Test
358
+
359
+ ## Design / UX Considerations
360
+ Test
361
+
362
+ ## Testing & Test Cases
363
+ Test
364
+
365
+ ## AI Prompt
366
+ Test
367
+
368
+ ## Expected AI Output
369
+ Test
370
+
371
+ ## AI Workflow
372
+ Test`;
373
+
374
+ createMockVibeProject(tempDir);
375
+ const ticketPath = path.join(tempDir, '.vibe', 'tickets', 'TKT-001-test.md');
376
+ fs.writeFileSync(ticketPath, ticketContent, 'utf-8');
377
+
378
+ // Act
379
+ expect(() => lintCommand([])).toThrow('process.exit(1)');
380
+
381
+ // Assert
382
+ expect(exitMock.exitCalls).toContain(1);
383
+ expect(consoleMock.logs.log.some(log =>
384
+ log.includes('Invalid ID format "INVALID-ID"')
385
+ )).toBe(true);
386
+ });
387
+ });
388
+
389
+ describe('specific file validation', () => {
390
+ it('should lint specific file when filename provided', () => {
391
+ // Arrange
392
+ const validTicket = {
393
+ id: 'TKT-001',
394
+ title: 'Test ticket',
395
+ slug: 'test-ticket',
396
+ status: 'open',
397
+ priority: 'medium',
398
+ created_at: '2025-01-01T12:00:00.000Z',
399
+ updated_at: '2025-01-01T12:00:00.000Z',
400
+ description: `## Description
401
+
402
+ This is a test description with enough content to pass validation.
403
+
404
+ ## Acceptance Criteria
405
+
406
+ This section has sufficient content for validation purposes.
407
+
408
+ ## Code Quality
409
+
410
+ This section contains adequate content for testing validation logic.
411
+
412
+ ## Implementation Notes
413
+
414
+ This section provides enough details for the validation system.
415
+
416
+ ## Design / UX Considerations
417
+
418
+ This section includes sufficient UX considerations for testing.
419
+
420
+ ## Testing & Test Cases
421
+
422
+ This section contains adequate test case information.
423
+
424
+ ## AI Prompt
425
+
426
+ This section provides sufficient AI prompt content for testing.
427
+
428
+ ## Expected AI Output
429
+
430
+ This section contains enough information about expected output.
431
+
432
+ ## AI Workflow
433
+
434
+ This section has adequate workflow information for validation.`
435
+ };
436
+
437
+ const mockProject = createMockVibeProject(tempDir, { withTickets: [validTicket] });
438
+ const ticketFile = path.basename(mockProject.ticketPaths[0]);
439
+
440
+ // Act
441
+ expect(() => lintCommand([ticketFile])).toThrow('process.exit(0)');
442
+
443
+ // Assert
444
+ expect(exitMock.exitCalls).toContain(0);
445
+ });
446
+
447
+ it('should exit with error when specific file does not exist', () => {
448
+ // Arrange
449
+ createMockVibeProject(tempDir);
450
+
451
+ // Act
452
+ expect(() => lintCommand(['nonexistent.md'])).toThrow('process.exit(1)');
453
+
454
+ // Assert
455
+ expect(exitMock.exitCalls).toContain(1);
456
+ expect(consoleMock.logs.error.some(error =>
457
+ error.includes('File not found: nonexistent.md')
458
+ )).toBe(true);
459
+ });
460
+ });
461
+
462
+ describe('verbose output', () => {
463
+ it('should show detailed output with --verbose flag', () => {
464
+ // Arrange
465
+ const validTicket = {
466
+ id: 'TKT-001',
467
+ title: 'Test ticket',
468
+ slug: 'test-ticket',
469
+ status: 'open',
470
+ priority: 'medium',
471
+ created_at: '2025-01-01T12:00:00.000Z',
472
+ updated_at: '2025-01-01T12:00:00.000Z',
473
+ description: `## Description
474
+
475
+ This is a test description with enough content to pass validation.
476
+
477
+ ## Acceptance Criteria
478
+
479
+ This section has sufficient content for validation purposes.
480
+
481
+ ## Code Quality
482
+
483
+ This section contains adequate content for testing validation logic.
484
+
485
+ ## Implementation Notes
486
+
487
+ This section provides enough details for the validation system.
488
+
489
+ ## Design / UX Considerations
490
+
491
+ This section includes sufficient UX considerations for testing.
492
+
493
+ ## Testing & Test Cases
494
+
495
+ This section contains adequate test case information.
496
+
497
+ ## AI Prompt
498
+
499
+ This section provides sufficient AI prompt content for testing.
500
+
501
+ ## Expected AI Output
502
+
503
+ This section contains enough information about expected output.
504
+
505
+ ## AI Workflow
506
+
507
+ This section has adequate workflow information for validation.`
508
+ };
509
+
510
+ createMockVibeProject(tempDir, { withTickets: [validTicket] });
511
+
512
+ // Act
513
+ expect(() => lintCommand(['--verbose'])).toThrow('process.exit(0)');
514
+
515
+ // Assert
516
+ expect(exitMock.exitCalls).toContain(0);
517
+ expect(consoleMock.logs.log.some(log =>
518
+ log.includes('✅')
519
+ )).toBe(true);
520
+ });
521
+ });
522
+
523
+ describe('error handling', () => {
524
+ it('should handle malformed YAML frontmatter', () => {
525
+ // Arrange - create file with invalid YAML
526
+ const invalidYamlContent = `---
527
+ id: TKT-001
528
+ title: "Unclosed quote
529
+ status: open
530
+ ---
531
+
532
+ ## Description
533
+ Test`;
534
+
535
+ createMockVibeProject(tempDir);
536
+ const ticketPath = path.join(tempDir, '.vibe', 'tickets', 'TKT-001-test.md');
537
+ fs.writeFileSync(ticketPath, invalidYamlContent, 'utf-8');
538
+
539
+ // Act
540
+ expect(() => lintCommand([])).toThrow('process.exit(1)');
541
+
542
+ // Assert
543
+ expect(exitMock.exitCalls).toContain(1);
544
+ expect(consoleMock.logs.log.some(log =>
545
+ log.includes('Invalid YAML frontmatter')
546
+ )).toBe(true);
547
+ });
548
+
549
+ it('should handle files without frontmatter', () => {
550
+ // Arrange - create file without frontmatter
551
+ const noFrontmatterContent = `# Just a regular markdown file
552
+
553
+ This has no frontmatter.`;
554
+
555
+ createMockVibeProject(tempDir);
556
+ const ticketPath = path.join(tempDir, '.vibe', 'tickets', 'TKT-001-test.md');
557
+ fs.writeFileSync(ticketPath, noFrontmatterContent, 'utf-8');
558
+
559
+ // Act
560
+ expect(() => lintCommand([])).toThrow('process.exit(1)');
561
+
562
+ // Assert
563
+ expect(exitMock.exitCalls).toContain(1);
564
+ expect(consoleMock.logs.log.some(log =>
565
+ log.includes('File must start with YAML frontmatter')
566
+ )).toBe(true);
567
+ });
568
+ });
569
+ });
@@ -0,0 +1,131 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import yaml from 'js-yaml';
4
+ import { getTicketsDir } from '../../utils/index.js';
5
+
6
+ /**
7
+ * List all tickets
8
+ * @param {string[]} args Command arguments
9
+ */
10
+ function listCommand(args) {
11
+ // Parse arguments for filtering
12
+ let statusFilter = null;
13
+
14
+ for (let i = 0; i < args.length; i++) {
15
+ if (args[i].startsWith("--status=")) {
16
+ statusFilter = args[i].split("=")[1];
17
+ }
18
+ }
19
+
20
+ // Get tickets directory
21
+ const ticketDir = getTicketsDir();
22
+
23
+ if (!fs.existsSync(ticketDir)) {
24
+ console.error(`❌ Tickets directory not found: ${ticketDir}`);
25
+ process.exit(1);
26
+ }
27
+
28
+ // Read all markdown files in the tickets directory
29
+ const files = fs.readdirSync(ticketDir).filter(file => file.endsWith(".md"));
30
+
31
+ if (files.length === 0) {
32
+ console.log("No tickets found.");
33
+ process.exit(0);
34
+ }
35
+
36
+ // Parse each file to extract frontmatter
37
+ const tickets = [];
38
+
39
+ for (const file of files) {
40
+ try {
41
+ const filePath = path.join(ticketDir, file);
42
+ const content = fs.readFileSync(filePath, "utf-8");
43
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
44
+
45
+ if (match) {
46
+ const frontmatter = yaml.load(match[1]);
47
+ tickets.push({
48
+ id: frontmatter.id || "Unknown",
49
+ title: frontmatter.title || "Untitled",
50
+ status: frontmatter.status || "unknown",
51
+ priority: frontmatter.priority || "medium",
52
+ file
53
+ });
54
+ }
55
+ } catch (error) {
56
+ console.warn(`⚠️ Could not parse ticket: ${file}`);
57
+ }
58
+ }
59
+
60
+ // Filter tickets if status filter is provided
61
+ const filteredTickets = statusFilter
62
+ ? tickets.filter(ticket => ticket.status === statusFilter)
63
+ : tickets;
64
+
65
+ // Sort tickets by ID
66
+ filteredTickets.sort((a, b) => {
67
+ const idA = parseInt(a.id.replace(/\D/g, "")) || 0;
68
+ const idB = parseInt(b.id.replace(/\D/g, "")) || 0;
69
+ return idA - idB;
70
+ });
71
+
72
+ if (filteredTickets.length === 0) {
73
+ console.log(statusFilter
74
+ ? `No tickets found with status: ${statusFilter}`
75
+ : "No tickets found.");
76
+ process.exit(0);
77
+ }
78
+
79
+ // Display tickets in a formatted table
80
+ console.log("\n✨ VibeKit Tickets ✨\n");
81
+
82
+ // Calculate column widths based on content
83
+ const idWidth = 10; // Fixed width for ID column
84
+ const statusWidth = 15; // Fixed width for status column
85
+ const titleWidth = 50; // Fixed width for title column
86
+
87
+ // Print header
88
+ console.log(
89
+ `${"ID".padEnd(idWidth)}${"|"} ${
90
+ "STATUS".padEnd(statusWidth)
91
+ }${"|"} ${
92
+ "TITLE"
93
+ }`
94
+ );
95
+ console.log(`${'-'.repeat(idWidth)}+${'-'.repeat(statusWidth + 2)}+${'-'.repeat(titleWidth)}`);
96
+
97
+ for (const ticket of filteredTickets) {
98
+ let statusColor = "";
99
+
100
+ // Add color based on status
101
+ switch (ticket.status) {
102
+ case "done":
103
+ statusColor = "\x1b[32m"; // Green
104
+ break;
105
+ case "in_progress":
106
+ statusColor = "\x1b[33m"; // Yellow
107
+ break;
108
+ case "review":
109
+ statusColor = "\x1b[36m"; // Cyan
110
+ break;
111
+ default:
112
+ statusColor = "\x1b[0m"; // Default
113
+ }
114
+
115
+ // Format each row with proper padding
116
+ console.log(
117
+ `${ticket.id.padEnd(idWidth)}${"|"} ${
118
+ statusColor + ticket.status.padEnd(statusWidth - 1) + "\x1b[0m"
119
+ }${"|"} ${
120
+ ticket.title.length > titleWidth - 3
121
+ ? ticket.title.substring(0, titleWidth - 3) + "..."
122
+ : ticket.title
123
+ }`
124
+ );
125
+ }
126
+
127
+ console.log(`${'-'.repeat(idWidth)}+${'-'.repeat(statusWidth + 2)}+${'-'.repeat(titleWidth)}`);
128
+ console.log(`Found ${filteredTickets.length} ticket(s)${statusFilter ? ` with status: ${statusFilter}` : ""}.\n`);
129
+ }
130
+
131
+ export default listCommand;