@nclamvn/vibecode-cli 2.0.0 → 2.2.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/.vibecode/learning/fixes.json +1 -0
- package/.vibecode/learning/preferences.json +1 -0
- package/README.md +310 -49
- package/SESSION_NOTES.md +154 -0
- package/bin/vibecode.js +235 -2
- package/package.json +5 -2
- package/src/agent/decomposition.js +476 -0
- package/src/agent/index.js +391 -0
- package/src/agent/memory.js +542 -0
- package/src/agent/orchestrator.js +917 -0
- package/src/agent/self-healing.js +516 -0
- package/src/commands/agent.js +349 -0
- package/src/commands/ask.js +230 -0
- package/src/commands/assist.js +413 -0
- package/src/commands/build.js +345 -4
- package/src/commands/debug.js +565 -0
- package/src/commands/docs.js +167 -0
- package/src/commands/git.js +1024 -0
- package/src/commands/go.js +635 -0
- package/src/commands/learn.js +294 -0
- package/src/commands/migrate.js +341 -0
- package/src/commands/plan.js +8 -2
- package/src/commands/refactor.js +205 -0
- package/src/commands/review.js +126 -1
- package/src/commands/security.js +229 -0
- package/src/commands/shell.js +486 -0
- package/src/commands/templates.js +397 -0
- package/src/commands/test.js +194 -0
- package/src/commands/undo.js +281 -0
- package/src/commands/watch.js +556 -0
- package/src/commands/wizard.js +322 -0
- package/src/config/constants.js +5 -1
- package/src/config/templates.js +146 -15
- package/src/core/backup.js +325 -0
- package/src/core/error-analyzer.js +237 -0
- package/src/core/fix-generator.js +195 -0
- package/src/core/iteration.js +226 -0
- package/src/core/learning.js +295 -0
- package/src/core/session.js +18 -2
- package/src/core/test-runner.js +281 -0
- package/src/debug/analyzer.js +329 -0
- package/src/debug/evidence.js +228 -0
- package/src/debug/fixer.js +348 -0
- package/src/debug/image-analyzer.js +304 -0
- package/src/debug/index.js +378 -0
- package/src/debug/verifier.js +346 -0
- package/src/index.js +102 -0
- package/src/providers/claude-code.js +12 -7
- package/src/templates/index.js +724 -0
- package/src/ui/__tests__/error-translator.test.js +390 -0
- package/src/ui/dashboard.js +364 -0
- package/src/ui/error-translator.js +775 -0
- package/src/utils/image.js +222 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Templates Command
|
|
3
|
+
// Browse and use project templates
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
import {
|
|
9
|
+
TEMPLATES,
|
|
10
|
+
getCategories,
|
|
11
|
+
getTemplatesByCategory,
|
|
12
|
+
searchTemplates,
|
|
13
|
+
getTemplate,
|
|
14
|
+
getCategoryIcon
|
|
15
|
+
} from '../templates/index.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Templates command entry point
|
|
19
|
+
*/
|
|
20
|
+
export async function templatesCommand(options = {}) {
|
|
21
|
+
// Show template info
|
|
22
|
+
if (options.info) {
|
|
23
|
+
return showTemplateInfo(options.info);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Preview template
|
|
27
|
+
if (options.preview) {
|
|
28
|
+
return previewTemplate(options.preview);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Search templates
|
|
32
|
+
if (options.search) {
|
|
33
|
+
return searchAndShow(options.search);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Default: List all templates
|
|
37
|
+
return listTemplates(options);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* List all templates grouped by category
|
|
42
|
+
*/
|
|
43
|
+
async function listTemplates(options) {
|
|
44
|
+
const categories = getCategories();
|
|
45
|
+
const totalTemplates = Object.keys(TEMPLATES).length;
|
|
46
|
+
|
|
47
|
+
console.log(chalk.cyan(`
|
|
48
|
+
╭────────────────────────────────────────────────────────────────────╮
|
|
49
|
+
│ 📦 VIBECODE TEMPLATE GALLERY │
|
|
50
|
+
│ │
|
|
51
|
+
│ ${String(totalTemplates).padEnd(2)} professional templates ready to use │
|
|
52
|
+
╰────────────────────────────────────────────────────────────────────╯
|
|
53
|
+
`));
|
|
54
|
+
|
|
55
|
+
// Group by category
|
|
56
|
+
for (const category of categories) {
|
|
57
|
+
const templates = getTemplatesByCategory(category.id);
|
|
58
|
+
const icon = getCategoryIcon(category.id);
|
|
59
|
+
|
|
60
|
+
console.log(chalk.white.bold(`\n ${icon} ${category.name.toUpperCase()} (${category.count})`));
|
|
61
|
+
console.log(chalk.gray(' ' + '─'.repeat(60)));
|
|
62
|
+
|
|
63
|
+
for (const template of templates) {
|
|
64
|
+
const id = chalk.green(template.id.padEnd(22));
|
|
65
|
+
const desc = chalk.gray(truncate(template.description, 40));
|
|
66
|
+
console.log(` ${id} ${desc}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(chalk.gray(`
|
|
71
|
+
─────────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
Usage:
|
|
74
|
+
${chalk.cyan('vibecode templates --info <id>')} View template details
|
|
75
|
+
${chalk.cyan('vibecode templates --search <query>')} Search templates
|
|
76
|
+
${chalk.cyan('vibecode go --template <id>')} Use a template
|
|
77
|
+
${chalk.cyan('vibecode go -t <id> --name "X"')} With customization
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
${chalk.cyan('vibecode go --template landing-saas')}
|
|
81
|
+
${chalk.cyan('vibecode go -t dashboard-admin --name "My Dashboard"')}
|
|
82
|
+
`));
|
|
83
|
+
|
|
84
|
+
// Interactive mode unless quiet
|
|
85
|
+
if (!options.quiet) {
|
|
86
|
+
await interactiveMode();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Interactive template browser
|
|
92
|
+
*/
|
|
93
|
+
async function interactiveMode() {
|
|
94
|
+
const { action } = await inquirer.prompt([{
|
|
95
|
+
type: 'list',
|
|
96
|
+
name: 'action',
|
|
97
|
+
message: 'What would you like to do?',
|
|
98
|
+
choices: [
|
|
99
|
+
{ name: '📋 View template details', value: 'info' },
|
|
100
|
+
{ name: '🚀 Use a template', value: 'use' },
|
|
101
|
+
{ name: '🔍 Search templates', value: 'search' },
|
|
102
|
+
{ name: '👋 Exit', value: 'exit' }
|
|
103
|
+
]
|
|
104
|
+
}]);
|
|
105
|
+
|
|
106
|
+
if (action === 'exit') {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (action === 'info') {
|
|
111
|
+
const { templateId } = await inquirer.prompt([{
|
|
112
|
+
type: 'list',
|
|
113
|
+
name: 'templateId',
|
|
114
|
+
message: 'Select template to view:',
|
|
115
|
+
choices: Object.values(TEMPLATES).map(t => ({
|
|
116
|
+
name: `${getCategoryIcon(t.category)} ${t.name} - ${truncate(t.description, 35)}`,
|
|
117
|
+
value: t.id
|
|
118
|
+
})),
|
|
119
|
+
pageSize: 15
|
|
120
|
+
}]);
|
|
121
|
+
return showTemplateInfo(templateId);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (action === 'use') {
|
|
125
|
+
const { templateId } = await inquirer.prompt([{
|
|
126
|
+
type: 'list',
|
|
127
|
+
name: 'templateId',
|
|
128
|
+
message: 'Select template to use:',
|
|
129
|
+
choices: Object.values(TEMPLATES).map(t => ({
|
|
130
|
+
name: `${getCategoryIcon(t.category)} ${t.name}`,
|
|
131
|
+
value: t.id
|
|
132
|
+
})),
|
|
133
|
+
pageSize: 15
|
|
134
|
+
}]);
|
|
135
|
+
|
|
136
|
+
const template = getTemplate(templateId);
|
|
137
|
+
|
|
138
|
+
// Ask for customization if template has variables
|
|
139
|
+
let customOptions = {};
|
|
140
|
+
if (Object.keys(template.variables).length > 0) {
|
|
141
|
+
const { customize } = await inquirer.prompt([{
|
|
142
|
+
type: 'confirm',
|
|
143
|
+
name: 'customize',
|
|
144
|
+
message: 'Customize template options?',
|
|
145
|
+
default: false
|
|
146
|
+
}]);
|
|
147
|
+
|
|
148
|
+
if (customize) {
|
|
149
|
+
customOptions = await promptForVariables(template);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.log(chalk.cyan(`
|
|
154
|
+
─────────────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
Run this command to create your project:
|
|
157
|
+
|
|
158
|
+
${chalk.white.bold(`vibecode go --template ${templateId}${formatCustomOptions(customOptions)}`)}
|
|
159
|
+
|
|
160
|
+
`));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (action === 'search') {
|
|
164
|
+
const { query } = await inquirer.prompt([{
|
|
165
|
+
type: 'input',
|
|
166
|
+
name: 'query',
|
|
167
|
+
message: 'Search:'
|
|
168
|
+
}]);
|
|
169
|
+
|
|
170
|
+
const results = searchTemplates(query);
|
|
171
|
+
if (results.length === 0) {
|
|
172
|
+
console.log(chalk.yellow('\n No templates found matching your search.\n'));
|
|
173
|
+
} else {
|
|
174
|
+
console.log(chalk.green(`\n Found ${results.length} template(s):\n`));
|
|
175
|
+
for (const t of results) {
|
|
176
|
+
const icon = getCategoryIcon(t.category);
|
|
177
|
+
console.log(` ${icon} ${chalk.green(t.id.padEnd(22))} ${chalk.gray(truncate(t.description, 40))}`);
|
|
178
|
+
}
|
|
179
|
+
console.log('');
|
|
180
|
+
|
|
181
|
+
// Offer to view details
|
|
182
|
+
const { viewDetails } = await inquirer.prompt([{
|
|
183
|
+
type: 'confirm',
|
|
184
|
+
name: 'viewDetails',
|
|
185
|
+
message: 'View template details?',
|
|
186
|
+
default: true
|
|
187
|
+
}]);
|
|
188
|
+
|
|
189
|
+
if (viewDetails && results.length > 0) {
|
|
190
|
+
const { templateId } = await inquirer.prompt([{
|
|
191
|
+
type: 'list',
|
|
192
|
+
name: 'templateId',
|
|
193
|
+
message: 'Select template:',
|
|
194
|
+
choices: results.map(t => ({ name: t.name, value: t.id }))
|
|
195
|
+
}]);
|
|
196
|
+
return showTemplateInfo(templateId);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Show detailed template information
|
|
204
|
+
*/
|
|
205
|
+
async function showTemplateInfo(templateId) {
|
|
206
|
+
const template = getTemplate(templateId);
|
|
207
|
+
|
|
208
|
+
if (!template) {
|
|
209
|
+
console.log(chalk.red(`\n ❌ Template "${templateId}" not found.\n`));
|
|
210
|
+
console.log(chalk.gray(` Run ${chalk.cyan('vibecode templates')} to see available templates.\n`));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const icon = getCategoryIcon(template.category);
|
|
215
|
+
|
|
216
|
+
console.log(chalk.cyan(`
|
|
217
|
+
╭────────────────────────────────────────────────────────────────────╮
|
|
218
|
+
│ ${icon} ${template.name.padEnd(56)}│
|
|
219
|
+
╰────────────────────────────────────────────────────────────────────╯
|
|
220
|
+
`));
|
|
221
|
+
|
|
222
|
+
console.log(chalk.white(` ${template.description}\n`));
|
|
223
|
+
|
|
224
|
+
console.log(chalk.gray(' Category: ') + chalk.white(template.category));
|
|
225
|
+
console.log(chalk.gray(' Tags: ') + chalk.cyan(template.tags.join(', ')));
|
|
226
|
+
console.log(chalk.gray(' Stack: ') + chalk.yellow(template.stack.join(', ')));
|
|
227
|
+
|
|
228
|
+
console.log(chalk.gray('\n Features:'));
|
|
229
|
+
for (const feature of template.features) {
|
|
230
|
+
console.log(chalk.green(` ✓ ${feature}`));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Show customization options
|
|
234
|
+
if (Object.keys(template.variables).length > 0) {
|
|
235
|
+
console.log(chalk.gray('\n Customization Options:'));
|
|
236
|
+
for (const [key, config] of Object.entries(template.variables)) {
|
|
237
|
+
const typeInfo = config.type === 'select'
|
|
238
|
+
? `[${config.options.join('|')}]`
|
|
239
|
+
: `(${config.type})`;
|
|
240
|
+
console.log(chalk.yellow(` --${key}`) + chalk.gray(` ${typeInfo}`));
|
|
241
|
+
console.log(chalk.gray(` ${config.description}`));
|
|
242
|
+
console.log(chalk.gray(` Default: "${config.default}"`));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
console.log(chalk.gray(`
|
|
247
|
+
─────────────────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
Use this template:
|
|
250
|
+
${chalk.cyan(`vibecode go --template ${templateId}`)}
|
|
251
|
+
|
|
252
|
+
With customization:
|
|
253
|
+
${chalk.cyan(`vibecode go -t ${templateId} --name "MyProject"`)}
|
|
254
|
+
`));
|
|
255
|
+
|
|
256
|
+
// Prompt to use
|
|
257
|
+
const { useNow } = await inquirer.prompt([{
|
|
258
|
+
type: 'confirm',
|
|
259
|
+
name: 'useNow',
|
|
260
|
+
message: 'Use this template now?',
|
|
261
|
+
default: false
|
|
262
|
+
}]);
|
|
263
|
+
|
|
264
|
+
if (useNow) {
|
|
265
|
+
// Ask for customization
|
|
266
|
+
let customOptions = {};
|
|
267
|
+
if (Object.keys(template.variables).length > 0) {
|
|
268
|
+
customOptions = await promptForVariables(template);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Import and call go command
|
|
272
|
+
const { goCommand } = await import('./go.js');
|
|
273
|
+
await goCommand('', { template: templateId, ...customOptions });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Prompt user for template variables
|
|
279
|
+
*/
|
|
280
|
+
async function promptForVariables(template) {
|
|
281
|
+
const questions = [];
|
|
282
|
+
|
|
283
|
+
for (const [key, config] of Object.entries(template.variables)) {
|
|
284
|
+
if (config.type === 'select') {
|
|
285
|
+
questions.push({
|
|
286
|
+
type: 'list',
|
|
287
|
+
name: key,
|
|
288
|
+
message: config.description,
|
|
289
|
+
choices: config.options,
|
|
290
|
+
default: config.default
|
|
291
|
+
});
|
|
292
|
+
} else if (config.type === 'boolean') {
|
|
293
|
+
questions.push({
|
|
294
|
+
type: 'confirm',
|
|
295
|
+
name: key,
|
|
296
|
+
message: config.description,
|
|
297
|
+
default: config.default
|
|
298
|
+
});
|
|
299
|
+
} else if (config.type === 'number') {
|
|
300
|
+
questions.push({
|
|
301
|
+
type: 'number',
|
|
302
|
+
name: key,
|
|
303
|
+
message: config.description,
|
|
304
|
+
default: config.default
|
|
305
|
+
});
|
|
306
|
+
} else {
|
|
307
|
+
questions.push({
|
|
308
|
+
type: 'input',
|
|
309
|
+
name: key,
|
|
310
|
+
message: config.description,
|
|
311
|
+
default: config.default
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return inquirer.prompt(questions);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Search and display results
|
|
321
|
+
*/
|
|
322
|
+
async function searchAndShow(query) {
|
|
323
|
+
const results = searchTemplates(query);
|
|
324
|
+
|
|
325
|
+
if (results.length === 0) {
|
|
326
|
+
console.log(chalk.yellow(`\n No templates found for "${query}".\n`));
|
|
327
|
+
console.log(chalk.gray(` Try: landing, dashboard, ecommerce, blog, portfolio, app\n`));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
console.log(chalk.green(`\n Found ${results.length} template(s) matching "${query}":\n`));
|
|
332
|
+
|
|
333
|
+
for (const t of results) {
|
|
334
|
+
const icon = getCategoryIcon(t.category);
|
|
335
|
+
console.log(` ${icon} ${chalk.green(t.id.padEnd(22))} ${chalk.gray(truncate(t.description, 40))}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
console.log(chalk.gray(`
|
|
339
|
+
─────────────────────────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
View details:
|
|
342
|
+
${chalk.cyan(`vibecode templates --info ${results[0].id}`)}
|
|
343
|
+
|
|
344
|
+
Use template:
|
|
345
|
+
${chalk.cyan(`vibecode go --template ${results[0].id}`)}
|
|
346
|
+
`));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Preview template (open in browser)
|
|
351
|
+
*/
|
|
352
|
+
async function previewTemplate(templateId) {
|
|
353
|
+
const template = getTemplate(templateId);
|
|
354
|
+
|
|
355
|
+
if (!template) {
|
|
356
|
+
console.log(chalk.red(`\n ❌ Template "${templateId}" not found.\n`));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
console.log(chalk.cyan(`\n 🖼️ Preview: ${template.name}`));
|
|
361
|
+
console.log(chalk.gray(` URL: ${template.preview}\n`));
|
|
362
|
+
|
|
363
|
+
if (template.preview.startsWith('http')) {
|
|
364
|
+
try {
|
|
365
|
+
const open = (await import('open')).default;
|
|
366
|
+
await open(template.preview);
|
|
367
|
+
console.log(chalk.green(' ✓ Opened preview in browser.\n'));
|
|
368
|
+
} catch (e) {
|
|
369
|
+
console.log(chalk.yellow(' ⚠ Could not open browser. Visit URL manually.\n'));
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
console.log(chalk.gray(' (Preview not available yet)\n'));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Truncate string to max length
|
|
378
|
+
*/
|
|
379
|
+
function truncate(str, maxLen) {
|
|
380
|
+
if (str.length <= maxLen) return str;
|
|
381
|
+
return str.substring(0, maxLen - 3) + '...';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Format custom options for display
|
|
386
|
+
*/
|
|
387
|
+
function formatCustomOptions(options) {
|
|
388
|
+
const parts = [];
|
|
389
|
+
for (const [key, value] of Object.entries(options)) {
|
|
390
|
+
if (typeof value === 'string') {
|
|
391
|
+
parts.push(`--${key} "${value}"`);
|
|
392
|
+
} else {
|
|
393
|
+
parts.push(`--${key} ${value}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return parts.length > 0 ? ' ' + parts.join(' ') : '';
|
|
397
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Test Command
|
|
3
|
+
// Phase K2: AI Test Generation
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import inquirer from 'inquirer';
|
|
11
|
+
|
|
12
|
+
export async function testCommand(targetPath, options = {}) {
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
|
|
15
|
+
// Generate tests
|
|
16
|
+
if (options.generate) {
|
|
17
|
+
return generateTests(cwd, targetPath, options);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Run tests (pass through to npm test)
|
|
21
|
+
if (options.run) {
|
|
22
|
+
const { exec } = await import('child_process');
|
|
23
|
+
const child = exec('npm test', { cwd });
|
|
24
|
+
child.stdout?.pipe(process.stdout);
|
|
25
|
+
child.stderr?.pipe(process.stderr);
|
|
26
|
+
return new Promise(resolve => child.on('close', resolve));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Coverage
|
|
30
|
+
if (options.coverage) {
|
|
31
|
+
const { exec } = await import('child_process');
|
|
32
|
+
const child = exec('npm run test:coverage || npm test -- --coverage', { cwd });
|
|
33
|
+
child.stdout?.pipe(process.stdout);
|
|
34
|
+
child.stderr?.pipe(process.stderr);
|
|
35
|
+
return new Promise(resolve => child.on('close', resolve));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Default: show menu
|
|
39
|
+
const { action } = await inquirer.prompt([{
|
|
40
|
+
type: 'list',
|
|
41
|
+
name: 'action',
|
|
42
|
+
message: 'Test options:',
|
|
43
|
+
choices: [
|
|
44
|
+
{ name: '🧪 Run tests (npm test)', value: 'run' },
|
|
45
|
+
{ name: '✨ Generate tests for file/folder', value: 'generate' },
|
|
46
|
+
{ name: '📊 Show coverage', value: 'coverage' },
|
|
47
|
+
{ name: '👋 Exit', value: 'exit' }
|
|
48
|
+
]
|
|
49
|
+
}]);
|
|
50
|
+
|
|
51
|
+
if (action === 'run') {
|
|
52
|
+
return testCommand(null, { run: true });
|
|
53
|
+
}
|
|
54
|
+
if (action === 'generate') {
|
|
55
|
+
const { target } = await inquirer.prompt([{
|
|
56
|
+
type: 'input',
|
|
57
|
+
name: 'target',
|
|
58
|
+
message: 'Path to generate tests for:',
|
|
59
|
+
default: 'src/'
|
|
60
|
+
}]);
|
|
61
|
+
return generateTests(cwd, target, options);
|
|
62
|
+
}
|
|
63
|
+
if (action === 'coverage') {
|
|
64
|
+
return testCommand(null, { coverage: true });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function generateTests(cwd, targetPath, options) {
|
|
69
|
+
console.log(chalk.cyan(`
|
|
70
|
+
╭────────────────────────────────────────────────────────────────────╮
|
|
71
|
+
│ 🧪 TEST GENERATION │
|
|
72
|
+
│ │
|
|
73
|
+
│ Target: ${(targetPath || 'src/').padEnd(52)}│
|
|
74
|
+
│ │
|
|
75
|
+
╰────────────────────────────────────────────────────────────────────╯
|
|
76
|
+
`));
|
|
77
|
+
|
|
78
|
+
// Detect test framework
|
|
79
|
+
const framework = await detectTestFramework(cwd);
|
|
80
|
+
console.log(chalk.gray(` Test framework: ${framework}\n`));
|
|
81
|
+
|
|
82
|
+
// Get files to generate tests for
|
|
83
|
+
const files = await getFilesToTest(cwd, targetPath || 'src/');
|
|
84
|
+
|
|
85
|
+
if (files.length === 0) {
|
|
86
|
+
console.log(chalk.yellow(' No files found to generate tests for.\n'));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log(chalk.gray(` Found ${files.length} files\n`));
|
|
91
|
+
|
|
92
|
+
const prompt = `
|
|
93
|
+
# Test Generation Request
|
|
94
|
+
|
|
95
|
+
## Project: ${path.basename(cwd)}
|
|
96
|
+
## Test Framework: ${framework}
|
|
97
|
+
|
|
98
|
+
## Files to Generate Tests For:
|
|
99
|
+
${files.map(f => `- ${f}`).join('\n')}
|
|
100
|
+
|
|
101
|
+
## Instructions:
|
|
102
|
+
1. Read each source file
|
|
103
|
+
2. Generate comprehensive tests including:
|
|
104
|
+
- Unit tests for each function/method
|
|
105
|
+
- Edge cases (null, undefined, empty, boundary values)
|
|
106
|
+
- Error handling tests
|
|
107
|
+
- Mock external dependencies
|
|
108
|
+
- Integration tests where appropriate
|
|
109
|
+
|
|
110
|
+
3. Use ${framework} syntax and conventions
|
|
111
|
+
4. Follow AAA pattern (Arrange, Act, Assert)
|
|
112
|
+
5. Add descriptive test names
|
|
113
|
+
6. Include setup/teardown if needed
|
|
114
|
+
|
|
115
|
+
## Output:
|
|
116
|
+
Create test files in __tests__/ or *.test.ts/js format.
|
|
117
|
+
For each source file, create corresponding test file.
|
|
118
|
+
|
|
119
|
+
Generate tests now.
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
const promptFile = path.join(cwd, '.vibecode', 'test-gen-prompt.md');
|
|
123
|
+
await fs.mkdir(path.dirname(promptFile), { recursive: true });
|
|
124
|
+
await fs.writeFile(promptFile, prompt);
|
|
125
|
+
|
|
126
|
+
console.log(chalk.gray(' Generating tests with Claude Code...\n'));
|
|
127
|
+
|
|
128
|
+
await runClaudeCode(prompt, cwd);
|
|
129
|
+
|
|
130
|
+
console.log(chalk.green('\n✅ Tests generated!'));
|
|
131
|
+
console.log(chalk.gray(' Run: npm test\n'));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function detectTestFramework(cwd) {
|
|
135
|
+
try {
|
|
136
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
137
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
|
|
138
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
139
|
+
|
|
140
|
+
if (deps.vitest) return 'vitest';
|
|
141
|
+
if (deps.jest) return 'jest';
|
|
142
|
+
if (deps.mocha) return 'mocha';
|
|
143
|
+
if (deps['@testing-library/react']) return 'jest + @testing-library/react';
|
|
144
|
+
if (deps.ava) return 'ava';
|
|
145
|
+
if (deps.tap) return 'tap';
|
|
146
|
+
if (deps.uvu) return 'uvu';
|
|
147
|
+
|
|
148
|
+
return 'jest'; // default
|
|
149
|
+
} catch {
|
|
150
|
+
return 'jest';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function getFilesToTest(cwd, targetPath) {
|
|
155
|
+
const files = [];
|
|
156
|
+
const fullPath = path.join(cwd, targetPath);
|
|
157
|
+
|
|
158
|
+
async function scan(dir) {
|
|
159
|
+
try {
|
|
160
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
161
|
+
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
if (entry.name.startsWith('.')) continue;
|
|
164
|
+
if (entry.name.includes('.test.') || entry.name.includes('.spec.')) continue;
|
|
165
|
+
if (entry.name === '__tests__') continue;
|
|
166
|
+
if (entry.name === 'node_modules') continue;
|
|
167
|
+
if (entry.name === 'dist' || entry.name === 'build') continue;
|
|
168
|
+
|
|
169
|
+
const entryPath = path.join(dir, entry.name);
|
|
170
|
+
|
|
171
|
+
if (entry.isDirectory()) {
|
|
172
|
+
await scan(entryPath);
|
|
173
|
+
} else if (/\.(js|ts|jsx|tsx)$/.test(entry.name)) {
|
|
174
|
+
files.push(path.relative(cwd, entryPath));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch {}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await scan(fullPath);
|
|
181
|
+
return files.slice(0, 20); // Limit
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function runClaudeCode(prompt, cwd) {
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
const child = spawn('claude', ['-p', prompt, '--dangerously-skip-permissions'], {
|
|
187
|
+
cwd,
|
|
188
|
+
stdio: 'inherit'
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
child.on('close', resolve);
|
|
192
|
+
child.on('error', () => resolve());
|
|
193
|
+
});
|
|
194
|
+
}
|