@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,492 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname } from 'path';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ /**
10
+ * Create a temporary test directory
11
+ * @param {string} testName - Name for the test directory
12
+ * @returns {string} Path to the temporary directory
13
+ */
14
+ export function createTempDir(testName = 'test') {
15
+ const tempDir = path.join(__dirname, '../../__temp__', testName, Date.now().toString());
16
+ fs.mkdirSync(tempDir, { recursive: true });
17
+ return tempDir;
18
+ }
19
+
20
+ /**
21
+ * Clean up temporary test directory
22
+ * @param {string} tempDir - Path to the temporary directory to remove
23
+ */
24
+ export function cleanupTempDir(tempDir) {
25
+ if (fs.existsSync(tempDir)) {
26
+ fs.rmSync(tempDir, { recursive: true, force: true });
27
+
28
+ // Also clean up empty parent directories
29
+ let parentDir = path.dirname(tempDir);
30
+ const tempRoot = path.join(__dirname, '../../__temp__');
31
+
32
+ // Keep going up the directory tree until we reach __temp__ root or find a non-empty directory
33
+ while (parentDir !== tempRoot && parentDir !== path.dirname(parentDir)) {
34
+ try {
35
+ if (fs.existsSync(parentDir)) {
36
+ const entries = fs.readdirSync(parentDir);
37
+ if (entries.length === 0) {
38
+ fs.rmdirSync(parentDir);
39
+ parentDir = path.dirname(parentDir);
40
+ } else {
41
+ break; // Directory not empty, stop cleaning
42
+ }
43
+ } else {
44
+ break; // Directory doesn't exist, stop
45
+ }
46
+ } catch (error) {
47
+ break; // Permission error or other issue, stop cleaning
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Clean up all temporary test directories
55
+ */
56
+ export function cleanupAllTempDirs() {
57
+ const tempRoot = path.join(__dirname, '../../__temp__');
58
+ if (fs.existsSync(tempRoot)) {
59
+ fs.rmSync(tempRoot, { recursive: true, force: true });
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Create a mock .vibe project structure
65
+ * @param {string} baseDir - Base directory for the mock project
66
+ * @param {Object} options - Configuration options
67
+ * @returns {Object} Paths to created files and directories
68
+ */
69
+ export function createMockVibeProject(baseDir, options = {}) {
70
+ const {
71
+ withConfig = true,
72
+ withTemplate = true,
73
+ withTickets = [],
74
+ configData = null,
75
+ templateData = null
76
+ } = options;
77
+
78
+ const vibeDir = path.join(baseDir, '.vibe');
79
+ const ticketsDir = path.join(vibeDir, 'tickets');
80
+ const templatesDir = path.join(vibeDir, '.templates');
81
+
82
+ // Create directories
83
+ fs.mkdirSync(vibeDir, { recursive: true });
84
+ fs.mkdirSync(ticketsDir, { recursive: true });
85
+ fs.mkdirSync(templatesDir, { recursive: true });
86
+
87
+ const paths = {
88
+ vibeDir,
89
+ ticketsDir,
90
+ templatesDir
91
+ };
92
+
93
+ // Create config.yml
94
+ if (withConfig) {
95
+ const defaultConfig = {
96
+ project: {
97
+ name: 'Test Project',
98
+ version: '1.0.0'
99
+ },
100
+ tickets: {
101
+ path: '.vibe/tickets',
102
+ priority_options: ['low', 'medium', 'high', 'urgent'],
103
+ status_options: ['open', 'in_progress', 'review', 'done'],
104
+ slug: {
105
+ max_length: 30,
106
+ word_limit: 5
107
+ }
108
+ },
109
+ git: {
110
+ branch_prefix: 'feature/'
111
+ },
112
+ ai: {
113
+ enabled: false,
114
+ provider: 'none'
115
+ }
116
+ };
117
+
118
+ const configPath = path.join(vibeDir, 'config.yml');
119
+ const configContent = configData || `project:
120
+ name: Test Project
121
+ version: 1.0.0
122
+
123
+ tickets:
124
+ path: .vibe/tickets
125
+ priority_options:
126
+ - low
127
+ - medium
128
+ - high
129
+ - urgent
130
+ status_options:
131
+ - open
132
+ - in_progress
133
+ - review
134
+ - done
135
+ slug:
136
+ max_length: 30
137
+ word_limit: 5
138
+
139
+ git:
140
+ branch_prefix: feature/
141
+
142
+ ai:
143
+ enabled: false
144
+ provider: none
145
+ `;
146
+
147
+ fs.writeFileSync(configPath, configContent, 'utf-8');
148
+ paths.configPath = configPath;
149
+ }
150
+
151
+ // Create default.md template
152
+ if (withTemplate) {
153
+ const defaultTemplate = templateData || `---
154
+ id: TKT-{id}
155
+ title: {title}
156
+ slug: {slug}
157
+ status: open
158
+ priority: medium
159
+ created_at: {date}
160
+ updated_at: {date}
161
+ ---
162
+
163
+ ## Description
164
+
165
+ Brief description of the task or issue.
166
+
167
+ ## Acceptance Criteria
168
+
169
+ - [ ] Define what needs to be done
170
+ - [ ] Add specific requirements
171
+ - [ ] Include testing criteria
172
+
173
+ ## Code Quality
174
+
175
+ - Follow existing code patterns
176
+ - Write clear, readable code
177
+ - Include appropriate error handling
178
+ - Add JSDoc comments where needed
179
+
180
+ ## Implementation Notes
181
+
182
+ Add any technical details, considerations, or constraints.
183
+
184
+ ## Design / UX Considerations
185
+
186
+ Any design or user experience requirements.
187
+
188
+ ## Testing & Test Cases
189
+
190
+ Outline the testing approach and specific test cases.
191
+
192
+ ## AI Prompt
193
+
194
+ Brief description for AI assistance.
195
+
196
+ ## Expected AI Output
197
+
198
+ What the AI should deliver or accomplish.
199
+
200
+ ## AI Workflow [Claude Code, Codex]
201
+
202
+ <!-- NOTE (Do not remove) -->
203
+ Always use \`vibe start\` to start working on this ticket and \`vibe close\` to close this ticket when done. Read .vibe/.context/aiworkflow directory for following vibekit cli workflow and follow the instructions to work on the tickets.
204
+ `;
205
+
206
+ const templatePath = path.join(templatesDir, 'default.md');
207
+ fs.writeFileSync(templatePath, defaultTemplate, 'utf-8');
208
+ paths.templatePath = templatePath;
209
+ }
210
+
211
+ // Create mock tickets
212
+ withTickets.forEach((ticket, index) => {
213
+ const ticketId = ticket.id || `TKT-${String(index + 1).padStart(3, '0')}`;
214
+ const filename = `${ticketId}-${ticket.slug || ticket.title?.toLowerCase().replace(/[^a-z0-9]+/g, '-') || 'test-ticket'}.md`;
215
+ const ticketPath = path.join(ticketsDir, filename);
216
+
217
+ // Build frontmatter with only provided fields
218
+ const frontmatterLines = [];
219
+ if (ticket.id !== undefined) frontmatterLines.push(`id: ${ticket.id}`);
220
+ if (ticket.title !== undefined) frontmatterLines.push(`title: ${ticket.title}`);
221
+ if (ticket.slug !== undefined) frontmatterLines.push(`slug: ${ticket.slug}`);
222
+ if (ticket.status !== undefined) frontmatterLines.push(`status: ${ticket.status}`);
223
+ if (ticket.priority !== undefined) frontmatterLines.push(`priority: ${ticket.priority}`);
224
+ if (ticket.created_at !== undefined) frontmatterLines.push(`created_at: ${ticket.created_at}`);
225
+ if (ticket.updated_at !== undefined) frontmatterLines.push(`updated_at: ${ticket.updated_at}`);
226
+
227
+ const frontmatter = frontmatterLines.length > 0 ? frontmatterLines.join('\n') : '';
228
+
229
+ const ticketContent = `---
230
+ ${frontmatter}
231
+ ---
232
+
233
+ ${ticket.description || 'Test ticket description'}
234
+ `;
235
+
236
+ fs.writeFileSync(ticketPath, ticketContent, 'utf-8');
237
+ if (!paths.ticketPaths) paths.ticketPaths = [];
238
+ paths.ticketPaths.push(ticketPath);
239
+ });
240
+
241
+ return paths;
242
+ }
243
+
244
+ /**
245
+ * Create a mock git repository structure
246
+ * @param {string} baseDir - Base directory for the mock repository
247
+ * @returns {Object} Information about the created git repository
248
+ */
249
+ export function createMockGitRepo(baseDir) {
250
+ const gitDir = path.join(baseDir, '.git');
251
+ const headFile = path.join(gitDir, 'HEAD');
252
+ const configFile = path.join(gitDir, 'config');
253
+
254
+ fs.mkdirSync(gitDir, { recursive: true });
255
+ fs.writeFileSync(headFile, 'ref: refs/heads/main\n', 'utf-8');
256
+ fs.writeFileSync(configFile, `[core]
257
+ \trepositoryformatversion = 0
258
+ \tfilemode = true
259
+ \tbare = false
260
+ \tlogallrefupdates = true
261
+ [branch "main"]
262
+ \tremote = origin
263
+ \tmerge = refs/heads/main
264
+ `, 'utf-8');
265
+
266
+ return {
267
+ gitDir,
268
+ headFile,
269
+ configFile,
270
+ currentBranch: 'main'
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Mock console methods for testing
276
+ * @returns {Object} Mock console methods and restore function
277
+ */
278
+ export function mockConsole() {
279
+ const originalConsole = {
280
+ log: console.log,
281
+ error: console.error,
282
+ warn: console.warn,
283
+ info: console.info
284
+ };
285
+
286
+ const logs = {
287
+ log: [],
288
+ error: [],
289
+ warn: [],
290
+ info: []
291
+ };
292
+
293
+ console.log = (...args) => logs.log.push(args.join(' '));
294
+ console.error = (...args) => logs.error.push(args.join(' '));
295
+ console.warn = (...args) => logs.warn.push(args.join(' '));
296
+ console.info = (...args) => logs.info.push(args.join(' '));
297
+
298
+ return {
299
+ logs,
300
+ restore: () => {
301
+ console.log = originalConsole.log;
302
+ console.error = originalConsole.error;
303
+ console.warn = originalConsole.warn;
304
+ console.info = originalConsole.info;
305
+ }
306
+ };
307
+ }
308
+
309
+ /**
310
+ * Mock process.cwd() to return a specific directory
311
+ * @param {string} mockDir - Directory to return as current working directory
312
+ * @returns {Function} Restore function to reset process.cwd
313
+ */
314
+ export function mockProcessCwd(mockDir) {
315
+ const originalCwd = process.cwd;
316
+ process.cwd = () => mockDir;
317
+
318
+ return () => {
319
+ process.cwd = originalCwd;
320
+ };
321
+ }
322
+
323
+ /**
324
+ * Mock process.exit() to prevent actual exit during tests
325
+ * @returns {Object} Mock exit function and restore function
326
+ */
327
+ export function mockProcessExit() {
328
+ const originalExit = process.exit;
329
+ const exitCalls = [];
330
+
331
+ process.exit = (code) => {
332
+ exitCalls.push(code);
333
+ throw new Error(`process.exit(${code})`);
334
+ };
335
+
336
+ return {
337
+ exitCalls,
338
+ restore: () => {
339
+ process.exit = originalExit;
340
+ }
341
+ };
342
+ }
343
+
344
+ /**
345
+ * Create a test fixture for file system operations
346
+ * @param {Object} fileStructure - Object representing file structure
347
+ * @param {string} baseDir - Base directory for the fixture
348
+ */
349
+ export function createFileFixture(fileStructure, baseDir) {
350
+ Object.entries(fileStructure).forEach(([filePath, content]) => {
351
+ const fullPath = path.join(baseDir, filePath);
352
+ const dir = path.dirname(fullPath);
353
+
354
+ if (!fs.existsSync(dir)) {
355
+ fs.mkdirSync(dir, { recursive: true });
356
+ }
357
+
358
+ if (typeof content === 'string') {
359
+ fs.writeFileSync(fullPath, content, 'utf-8');
360
+ } else if (content === null) {
361
+ // Create directory
362
+ fs.mkdirSync(fullPath, { recursive: true });
363
+ }
364
+ });
365
+ }
366
+
367
+ /**
368
+ * Assert that a file exists and has expected content
369
+ * @param {string} filePath - Path to the file
370
+ * @param {string|RegExp} expectedContent - Expected content or pattern
371
+ */
372
+ export function assertFileContent(filePath, expectedContent) {
373
+ if (!fs.existsSync(filePath)) {
374
+ throw new Error(`File does not exist: ${filePath}`);
375
+ }
376
+
377
+ const content = fs.readFileSync(filePath, 'utf-8');
378
+
379
+ if (typeof expectedContent === 'string') {
380
+ if (!content.includes(expectedContent)) {
381
+ throw new Error(`File content does not include expected text. Got: ${content}`);
382
+ }
383
+ } else if (expectedContent instanceof RegExp) {
384
+ if (!expectedContent.test(content)) {
385
+ throw new Error(`File content does not match expected pattern. Got: ${content}`);
386
+ }
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Wait for a specified amount of time (for async operations)
392
+ * @param {number} ms - Milliseconds to wait
393
+ * @returns {Promise} Promise that resolves after the wait
394
+ */
395
+ export function wait(ms) {
396
+ return new Promise(resolve => setTimeout(resolve, ms));
397
+ }
398
+
399
+ /**
400
+ * Create a spy function for testing
401
+ * @param {Function} originalFn - Original function to spy on
402
+ * @returns {Object} Spy function with call tracking
403
+ */
404
+ export function createSpy(originalFn = () => {}) {
405
+ const calls = [];
406
+
407
+ const spy = (...args) => {
408
+ calls.push(args);
409
+ return originalFn(...args);
410
+ };
411
+
412
+ spy.calls = calls;
413
+ spy.calledWith = (...expectedArgs) => {
414
+ return calls.some(callArgs =>
415
+ callArgs.length === expectedArgs.length &&
416
+ callArgs.every((arg, index) => arg === expectedArgs[index])
417
+ );
418
+ };
419
+ spy.callCount = () => calls.length;
420
+ spy.lastCall = () => calls[calls.length - 1];
421
+
422
+ return spy;
423
+ }
424
+
425
+ /**
426
+ * Setup mock assets directory structure for init command testing
427
+ * @param {string} tempDir - Base temporary directory
428
+ * @returns {string} Path to the mock assets directory
429
+ */
430
+ export function setupMockAssets(tempDir) {
431
+ // Create the expected assets path structure that init command will look for
432
+ const mockAssetsPath = path.join(tempDir, 'src', 'commands', 'init', '..', '..', '..', 'assets');
433
+ const assetsDir = path.resolve(mockAssetsPath);
434
+
435
+ fs.mkdirSync(assetsDir, { recursive: true });
436
+
437
+ // Create mock config.yml
438
+ const configContent = `project:
439
+ name: Test Project
440
+ version: 1.0.0
441
+
442
+ tickets:
443
+ path: .vibe/tickets
444
+ priority_options:
445
+ - low
446
+ - medium
447
+ - high
448
+ - urgent
449
+ status_options:
450
+ - open
451
+ - in_progress
452
+ - review
453
+ - done
454
+ slug:
455
+ max_length: 30
456
+ word_limit: 5
457
+
458
+ git:
459
+ branch_prefix: feature/
460
+
461
+ ai:
462
+ enabled: false
463
+ provider: none
464
+ `;
465
+
466
+ // Create mock default.md template
467
+ const templateContent = `---
468
+ id: TKT-{id}
469
+ title: {title}
470
+ slug: {slug}
471
+ status: open
472
+ priority: medium
473
+ created_at: {date}
474
+ updated_at: {date}
475
+ ---
476
+
477
+ ## Description
478
+
479
+ Brief description of the task or issue.
480
+
481
+ ## Acceptance Criteria
482
+
483
+ - [ ] Define what needs to be done
484
+ - [ ] Add specific requirements
485
+ - [ ] Include testing criteria
486
+ `;
487
+
488
+ fs.writeFileSync(path.join(assetsDir, 'config.yml'), configContent, 'utf-8');
489
+ fs.writeFileSync(path.join(assetsDir, 'default.md'), templateContent, 'utf-8');
490
+
491
+ return assetsDir;
492
+ }