@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,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;
|