@nclamvn/vibecode-cli 2.1.0 → 2.2.1

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/bin/vibecode.js CHANGED
@@ -33,6 +33,9 @@ import {
33
33
  securityCommand,
34
34
  askCommand,
35
35
  migrateCommand,
36
+ // Phase M Commands
37
+ templatesCommand,
38
+ previewCommand,
36
39
  VERSION
37
40
  } from '../src/index.js';
38
41
 
@@ -133,13 +136,46 @@ program
133
136
  // ─────────────────────────────────────────────────────────────────────────────
134
137
 
135
138
  program
136
- .command('go <description>')
139
+ .command('go [description...]')
137
140
  .description('Magic mode: one command, full automated build')
138
- .option('-t, --template <name>', 'Use template (landing, saas, cli, api)')
141
+ .option('-t, --template <id>', 'Use a template (run "vibecode templates" to see list)')
142
+ .option('--name <name>', 'Project/Company name')
143
+ .option('--color <color>', 'Primary brand color (hex)')
139
144
  .option('-i, --iterate', 'Enable iterative build mode')
140
145
  .option('-m, --max <n>', 'Max iterations for iterative mode', parseInt)
141
- .option('-o, --open', 'Auto-open result when done')
142
- .action(goCommand);
146
+ .option('-o, --open', 'Auto-open folder when done')
147
+ .option('-p, --preview', 'Start dev server and open browser after build')
148
+ .option('--port <port>', 'Preview port number', '3000')
149
+ .option('--qr', 'Show QR code for mobile preview')
150
+ .action((description, options) => {
151
+ const desc = Array.isArray(description) ? description.join(' ') : description;
152
+ goCommand(desc, options);
153
+ });
154
+
155
+ // ─────────────────────────────────────────────────────────────────────────────
156
+ // Phase M Commands - Templates
157
+ // ─────────────────────────────────────────────────────────────────────────────
158
+
159
+ program
160
+ .command('templates')
161
+ .alias('tpl')
162
+ .description('Browse and use project templates')
163
+ .option('-l, --list', 'List all templates')
164
+ .option('-i, --info <id>', 'Show template details')
165
+ .option('-s, --search <query>', 'Search templates')
166
+ .option('-p, --preview <id>', 'Preview template in browser')
167
+ .option('-q, --quiet', 'Non-interactive mode')
168
+ .action(templatesCommand);
169
+
170
+ program
171
+ .command('preview')
172
+ .description('Start dev server and open in browser')
173
+ .option('-p, --port <port>', 'Port number', '3000')
174
+ .option('-q, --qr', 'Show QR code for mobile')
175
+ .option('-s, --stop', 'Stop running preview server')
176
+ .option('--no-open', 'Do not open browser')
177
+ .option('-d, --detach', 'Run in background')
178
+ .action(previewCommand);
143
179
 
144
180
  // ─────────────────────────────────────────────────────────────────────────────
145
181
  // Phase F Commands - Agent Mode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nclamvn/vibecode-cli",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "Build software with discipline - AI coding with guardrails",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -35,7 +35,9 @@
35
35
  "commander": "^12.0.0",
36
36
  "fs-extra": "^11.2.0",
37
37
  "inquirer": "^9.2.12",
38
+ "open": "^11.0.0",
38
39
  "ora": "^8.0.1",
40
+ "qrcode": "^1.5.4",
39
41
  "yaml": "^2.3.4"
40
42
  },
41
43
  "devDependencies": {
@@ -41,20 +41,31 @@ import {
41
41
  import { runTests } from '../core/test-runner.js';
42
42
  import { analyzeErrors } from '../core/error-analyzer.js';
43
43
  import { ensureDir, appendToFile } from '../utils/files.js';
44
+ import { getTemplate, getCategoryIcon } from '../templates/index.js';
44
45
 
45
46
  const execAsync = promisify(exec);
46
47
 
47
48
  /**
48
49
  * Magic Mode: One command, full build
49
50
  * vibecode go "description" → Everything automated
51
+ * vibecode go --template <id> → Use template
50
52
  */
51
53
  export async function goCommand(description, options = {}) {
54
+ // Handle template mode
55
+ if (options.template) {
56
+ return goWithTemplate(options.template, options);
57
+ }
58
+
52
59
  const startTime = Date.now();
53
60
 
61
+ // Handle description as array (from commander variadic)
62
+ const desc = Array.isArray(description) ? description.join(' ') : description;
63
+
54
64
  // Validate description
55
- if (!description || description.trim().length < 5) {
65
+ if (!desc || desc.trim().length < 5) {
56
66
  printError('Description too short. Please provide more details.');
57
67
  console.log(chalk.gray('Example: vibecode go "Landing page for my startup"'));
68
+ console.log(chalk.gray('Or use: vibecode go --template landing-saas'));
58
69
  process.exit(1);
59
70
  }
60
71
 
@@ -67,7 +78,7 @@ export async function goCommand(description, options = {}) {
67
78
  }
68
79
 
69
80
  // Generate project name
70
- const projectName = generateProjectName(description);
81
+ const projectName = options.name ? sanitizeProjectName(options.name) : generateProjectName(desc);
71
82
  const projectPath = path.join(process.cwd(), projectName);
72
83
 
73
84
  // Check if directory exists
@@ -78,7 +89,7 @@ export async function goCommand(description, options = {}) {
78
89
  }
79
90
 
80
91
  // Show magic header
81
- showMagicHeader(description, projectName);
92
+ showMagicHeader(desc, projectName);
82
93
 
83
94
  // Create backup of current directory before go command
84
95
  // (backup in parent directory since we're creating a new project)
@@ -248,9 +259,26 @@ export async function goCommand(description, options = {}) {
248
259
  const duration = ((Date.now() - startTime) / 1000 / 60).toFixed(1);
249
260
  showMagicSummary(projectName, projectPath, duration, results, options);
250
261
 
251
- // Auto-open if requested
252
- if (options.open) {
262
+ // Auto preview if requested
263
+ if (options.preview) {
264
+ console.log(chalk.cyan('\n 🚀 Starting preview...\n'));
265
+ await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for files to settle
266
+
267
+ try {
268
+ const { autoPreview } = await import('./preview.js');
269
+ await autoPreview(projectPath, {
270
+ qr: options.qr,
271
+ port: options.port
272
+ });
273
+ } catch (error) {
274
+ console.log(chalk.yellow(` ⚠️ Preview failed: ${error.message}`));
275
+ console.log(chalk.gray(` Run manually: cd ${projectName} && vibecode preview\n`));
276
+ }
277
+ } else if (options.open) {
253
278
  await openProject(projectPath);
279
+ } else {
280
+ // Show preview hint
281
+ console.log(chalk.gray(` 💡 Quick preview: ${chalk.cyan(`cd ${projectName} && vibecode preview`)}\n`));
254
282
  }
255
283
 
256
284
  } catch (error) {
@@ -385,3 +413,261 @@ async function openProject(projectPath) {
385
413
  console.log(chalk.gray(`Could not auto-open: ${error.message}`));
386
414
  }
387
415
  }
416
+
417
+ /**
418
+ * Sanitize project name
419
+ */
420
+ function sanitizeProjectName(name) {
421
+ return name
422
+ .toLowerCase()
423
+ .replace(/[^a-z0-9\s-]/g, '')
424
+ .replace(/\s+/g, '-')
425
+ .replace(/-+/g, '-')
426
+ .replace(/^-|-$/g, '')
427
+ .substring(0, 50);
428
+ }
429
+
430
+ /**
431
+ * Template Mode: Use pre-defined template
432
+ * vibecode go --template <id>
433
+ */
434
+ async function goWithTemplate(templateId, options = {}) {
435
+ const template = getTemplate(templateId);
436
+
437
+ if (!template) {
438
+ printError(`Template "${templateId}" not found.`);
439
+ console.log(chalk.gray('Run "vibecode templates" to see available templates.'));
440
+ process.exit(1);
441
+ }
442
+
443
+ // Check Claude Code availability
444
+ const claudeAvailable = await isClaudeCodeAvailable();
445
+ if (!claudeAvailable) {
446
+ printError('Claude Code CLI not found.');
447
+ console.log(chalk.gray('Install with: npm install -g @anthropic-ai/claude-code'));
448
+ process.exit(1);
449
+ }
450
+
451
+ const startTime = Date.now();
452
+ const icon = getCategoryIcon(template.category);
453
+
454
+ // Generate project name
455
+ const projectName = options.name
456
+ ? sanitizeProjectName(options.name)
457
+ : `${templateId}-${Date.now().toString(36)}`;
458
+ const projectPath = path.join(process.cwd(), projectName);
459
+
460
+ // Check if directory exists
461
+ if (await fs.pathExists(projectPath)) {
462
+ printError(`Directory already exists: ${projectName}`);
463
+ console.log(chalk.gray('Use --name to specify a different name.'));
464
+ process.exit(1);
465
+ }
466
+
467
+ // Show template header
468
+ showTemplateHeader(template, projectName, icon);
469
+
470
+ // Build customized prompt from template
471
+ let prompt = template.prompt;
472
+
473
+ // Apply template variables from options
474
+ for (const [key, config] of Object.entries(template.variables)) {
475
+ const value = options[key] !== undefined ? options[key] : config.default;
476
+ // Replace placeholders like {name} in prompt
477
+ prompt = prompt.replace(new RegExp(`\\{${key}\\}`, 'gi'), value);
478
+ }
479
+
480
+ // Add customizations from common options
481
+ if (options.name) {
482
+ prompt += `\n\nProject/Company name: "${options.name}"`;
483
+ }
484
+ if (options.color) {
485
+ prompt += `\n\nPrimary brand color: ${options.color}`;
486
+ }
487
+
488
+ // Create backup
489
+ const backup = new BackupManager(process.cwd());
490
+ await backup.createBackup('go-template');
491
+
492
+ // Define steps
493
+ const steps = [
494
+ { name: 'INIT', label: 'Creating project', weight: 5 },
495
+ { name: 'SETUP', label: 'Setting up template', weight: 5 },
496
+ { name: 'BUILD', label: `Building ${template.name}`, weight: 80 },
497
+ { name: 'REVIEW', label: 'Running tests', weight: 10 }
498
+ ];
499
+
500
+ const results = {
501
+ templateId,
502
+ templateName: template.name
503
+ };
504
+ let currentProgress = 0;
505
+
506
+ try {
507
+ // Step 1: INIT
508
+ await executeStep(steps[0], currentProgress, async () => {
509
+ await fs.ensureDir(projectPath);
510
+ process.chdir(projectPath);
511
+ await createWorkspace();
512
+ results.projectPath = projectPath;
513
+ });
514
+ currentProgress += steps[0].weight;
515
+
516
+ // Step 2: SETUP
517
+ await executeStep(steps[1], currentProgress, async () => {
518
+ const sessionId = await createSession(projectName);
519
+ results.sessionId = sessionId;
520
+
521
+ // Write template info to session
522
+ const templateInfo = `# Template: ${template.name}
523
+
524
+ ## ID: ${templateId}
525
+ ## Category: ${template.category}
526
+ ## Stack: ${template.stack.join(', ')}
527
+
528
+ ## Features
529
+ ${template.features.map(f => `- ${f}`).join('\n')}
530
+
531
+ ## Prompt
532
+ ${prompt}
533
+ `;
534
+ await writeSessionFile('template.md', templateInfo);
535
+ await transitionTo(STATES.INTAKE_CAPTURED, 'template_setup');
536
+ });
537
+ currentProgress += steps[1].weight;
538
+
539
+ // Step 3: BUILD
540
+ await executeStep(steps[2], currentProgress, async () => {
541
+ const sessionPath = await getCurrentSessionPath();
542
+ const evidencePath = path.join(sessionPath, 'evidence');
543
+ await ensureDir(evidencePath);
544
+ const logPath = path.join(evidencePath, 'build.log');
545
+
546
+ await transitionTo(STATES.BUILD_IN_PROGRESS, 'template_build_start');
547
+
548
+ const fullPrompt = await buildPromptWithContext(prompt, process.cwd());
549
+
550
+ await appendToFile(logPath, `[${new Date().toISOString()}] Template Build: ${templateId}\n`);
551
+ await appendToFile(logPath, `Prompt:\n${prompt}\n\n`);
552
+
553
+ const buildResult = await spawnClaudeCode(fullPrompt, {
554
+ cwd: process.cwd(),
555
+ logPath: logPath
556
+ });
557
+
558
+ await appendToFile(logPath, `[${new Date().toISOString()}] Build completed with code: ${buildResult.code}\n`);
559
+
560
+ // Count files created
561
+ const files = await fs.readdir(process.cwd());
562
+ results.filesCreated = files.filter(f => !f.startsWith('.')).length;
563
+
564
+ const freshState = await loadState();
565
+ freshState.build_completed = new Date().toISOString();
566
+ freshState.template_used = templateId;
567
+ await saveState(freshState);
568
+
569
+ await transitionTo(STATES.BUILD_DONE, 'template_build_done');
570
+ });
571
+ currentProgress += steps[2].weight;
572
+
573
+ // Step 4: REVIEW
574
+ await executeStep(steps[3], currentProgress, async () => {
575
+ const testResult = await runTests(process.cwd());
576
+ results.testsPassed = testResult.summary.passed;
577
+ results.testsTotal = testResult.summary.total;
578
+ results.allPassed = testResult.passed;
579
+
580
+ await transitionTo(STATES.REVIEW_PASSED, 'template_review');
581
+ });
582
+ currentProgress = 100;
583
+
584
+ // Show final progress
585
+ console.log(renderProgressBar(100));
586
+ console.log();
587
+
588
+ // Show summary
589
+ const duration = ((Date.now() - startTime) / 1000 / 60).toFixed(1);
590
+ showTemplateSummary(template, projectName, projectPath, duration, results, options);
591
+
592
+ // Auto preview if requested
593
+ if (options.preview) {
594
+ console.log(chalk.cyan('\n 🚀 Starting preview...\n'));
595
+ await sleep(1000); // Wait for files to settle
596
+
597
+ try {
598
+ const { autoPreview } = await import('./preview.js');
599
+ await autoPreview(projectPath, {
600
+ qr: options.qr,
601
+ port: options.port
602
+ });
603
+ } catch (error) {
604
+ console.log(chalk.yellow(` ⚠️ Preview failed: ${error.message}`));
605
+ console.log(chalk.gray(` Run manually: cd ${projectName} && vibecode preview\n`));
606
+ }
607
+ } else if (options.open) {
608
+ await openProject(projectPath);
609
+ } else {
610
+ // Show preview hint
611
+ console.log(chalk.gray(` 💡 Quick preview: ${chalk.cyan(`cd ${projectName} && vibecode preview`)}\n`));
612
+ }
613
+
614
+ } catch (error) {
615
+ console.log();
616
+ printError(`Template build failed: ${error.message}`);
617
+ console.log(chalk.gray(`Project location: ${projectPath}`));
618
+ console.log(chalk.gray('Check .vibecode/sessions/*/evidence/build.log for details.'));
619
+ process.exit(1);
620
+ }
621
+ }
622
+
623
+ function sleep(ms) {
624
+ return new Promise(resolve => setTimeout(resolve, ms));
625
+ }
626
+
627
+ /**
628
+ * Show template mode header
629
+ */
630
+ function showTemplateHeader(template, projectName, icon) {
631
+ console.log();
632
+ console.log(chalk.magenta('╭' + '─'.repeat(68) + '╮'));
633
+ console.log(chalk.magenta('│') + ' '.repeat(68) + chalk.magenta('│'));
634
+ console.log(chalk.magenta('│') + chalk.bold.white(` ${icon} TEMPLATE: ${template.name.toUpperCase()}`) + ' '.repeat(Math.max(0, 53 - template.name.length)) + chalk.magenta('│'));
635
+ console.log(chalk.magenta('│') + ' '.repeat(68) + chalk.magenta('│'));
636
+
637
+ const desc = template.description.substring(0, 55);
638
+ console.log(chalk.magenta('│') + chalk.gray(` ${desc}`) + ' '.repeat(Math.max(0, 65 - desc.length)) + chalk.magenta('│'));
639
+
640
+ console.log(chalk.magenta('│') + chalk.gray(` Stack: ${template.stack.join(', ').substring(0, 52)}`) + ' '.repeat(Math.max(0, 55 - template.stack.join(', ').length)) + chalk.magenta('│'));
641
+ console.log(chalk.magenta('│') + chalk.gray(` → ${projectName}`) + ' '.repeat(Math.max(0, 63 - projectName.length)) + chalk.magenta('│'));
642
+
643
+ console.log(chalk.magenta('│') + ' '.repeat(68) + chalk.magenta('│'));
644
+ console.log(chalk.magenta('╰' + '─'.repeat(68) + '╯'));
645
+ console.log();
646
+ }
647
+
648
+ /**
649
+ * Show template mode summary
650
+ */
651
+ function showTemplateSummary(template, projectName, projectPath, duration, results, options) {
652
+ const icon = getCategoryIcon(template.category);
653
+ const testsStatus = results.allPassed
654
+ ? chalk.green(`${results.testsPassed}/${results.testsTotal} passed`)
655
+ : chalk.yellow(`${results.testsPassed}/${results.testsTotal} passed`);
656
+
657
+ console.log(chalk.green('╭' + '─'.repeat(68) + '╮'));
658
+ console.log(chalk.green('│') + ' '.repeat(68) + chalk.green('│'));
659
+ console.log(chalk.green('│') + chalk.bold.white(` ${icon} ${template.name.toUpperCase()} - COMPLETE!`) + ' '.repeat(Math.max(0, 48 - template.name.length)) + chalk.green('│'));
660
+ console.log(chalk.green('│') + ' '.repeat(68) + chalk.green('│'));
661
+ console.log(chalk.green('│') + chalk.white(` 📁 Project: ${projectName}`) + ' '.repeat(Math.max(0, 51 - projectName.length)) + chalk.green('│'));
662
+ console.log(chalk.green('│') + chalk.white(` 📦 Template: ${template.id}`) + ' '.repeat(Math.max(0, 51 - template.id.length)) + chalk.green('│'));
663
+ console.log(chalk.green('│') + chalk.white(` 📄 Files: ${results.filesCreated} created`) + ' '.repeat(42) + chalk.green('│'));
664
+ console.log(chalk.green('│') + chalk.white(` 🧪 Tests: `) + testsStatus + ' '.repeat(Math.max(0, 40)) + chalk.green('│'));
665
+ console.log(chalk.green('│') + chalk.white(` ⏱️ Duration: ${duration} minutes`) + ' '.repeat(Math.max(0, 44 - duration.length)) + chalk.green('│'));
666
+ console.log(chalk.green('│') + ' '.repeat(68) + chalk.green('│'));
667
+ console.log(chalk.green('│') + chalk.gray(` 💡 Next steps:`) + ' '.repeat(50) + chalk.green('│'));
668
+ console.log(chalk.green('│') + chalk.gray(` cd ${projectName}`) + ' '.repeat(Math.max(0, 57 - projectName.length)) + chalk.green('│'));
669
+ console.log(chalk.green('│') + chalk.gray(` npm install && npm run dev`) + ' '.repeat(33) + chalk.green('│'));
670
+ console.log(chalk.green('│') + ' '.repeat(68) + chalk.green('│'));
671
+ console.log(chalk.green('╰' + '─'.repeat(68) + '╯'));
672
+ console.log();
673
+ }