@nclamvn/vibecode-cli 1.3.0 → 1.5.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.
@@ -0,0 +1,380 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════════
2
+ // VIBECODE CLI - Magic Mode (go command)
3
+ // One command = Full workflow
4
+ // ═══════════════════════════════════════════════════════════════════════════════
5
+
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+ import path from 'path';
9
+ import fs from 'fs-extra';
10
+ import { exec } from 'child_process';
11
+ import { promisify } from 'util';
12
+
13
+ import { createWorkspace, workspaceExists, getProjectName, loadState, saveState } from '../core/workspace.js';
14
+ import {
15
+ createSession,
16
+ getCurrentSessionId,
17
+ getCurrentSessionPath,
18
+ writeSessionFile,
19
+ readSessionFile,
20
+ sessionFileExists
21
+ } from '../core/session.js';
22
+ import { getCurrentState, transitionTo } from '../core/state-machine.js';
23
+ import { validateContract } from '../core/contract.js';
24
+ import { generateSpecHash } from '../utils/hash.js';
25
+ import { STATES } from '../config/constants.js';
26
+ import {
27
+ getIntakeTemplate,
28
+ getBlueprintTemplate,
29
+ getContractTemplate,
30
+ getPlanTemplate,
31
+ getCoderPackTemplate
32
+ } from '../config/templates.js';
33
+ import { printBox, printError, printSuccess } from '../ui/output.js';
34
+ import {
35
+ spawnClaudeCode,
36
+ isClaudeCodeAvailable,
37
+ buildPromptWithContext
38
+ } from '../providers/index.js';
39
+ import { runTests } from '../core/test-runner.js';
40
+ import { analyzeErrors } from '../core/error-analyzer.js';
41
+ import { ensureDir, appendToFile } from '../utils/files.js';
42
+
43
+ const execAsync = promisify(exec);
44
+
45
+ /**
46
+ * Magic Mode: One command, full build
47
+ * vibecode go "description" → Everything automated
48
+ */
49
+ export async function goCommand(description, options = {}) {
50
+ const startTime = Date.now();
51
+
52
+ // Validate description
53
+ if (!description || description.trim().length < 5) {
54
+ printError('Description too short. Please provide more details.');
55
+ console.log(chalk.gray('Example: vibecode go "Landing page for my startup"'));
56
+ process.exit(1);
57
+ }
58
+
59
+ // Check Claude Code availability
60
+ const claudeAvailable = await isClaudeCodeAvailable();
61
+ if (!claudeAvailable) {
62
+ printError('Claude Code CLI not found.');
63
+ console.log(chalk.gray('Install with: npm install -g @anthropic-ai/claude-code'));
64
+ process.exit(1);
65
+ }
66
+
67
+ // Generate project name
68
+ const projectName = generateProjectName(description);
69
+ const projectPath = path.join(process.cwd(), projectName);
70
+
71
+ // Check if directory exists
72
+ if (await fs.pathExists(projectPath)) {
73
+ printError(`Directory already exists: ${projectName}`);
74
+ console.log(chalk.gray('Choose a different name or delete the existing directory.'));
75
+ process.exit(1);
76
+ }
77
+
78
+ // Show magic header
79
+ showMagicHeader(description, projectName);
80
+
81
+ // Define steps
82
+ const steps = [
83
+ { name: 'INIT', label: 'Creating project', weight: 5 },
84
+ { name: 'INTAKE', label: 'Capturing requirements', weight: 5 },
85
+ { name: 'BLUEPRINT', label: 'Designing architecture', weight: 5 },
86
+ { name: 'CONTRACT', label: 'Generating contract', weight: 5 },
87
+ { name: 'LOCK', label: 'Locking contract', weight: 5 },
88
+ { name: 'PLAN', label: 'Creating execution plan', weight: 5 },
89
+ { name: 'BUILD', label: 'Building with AI', weight: 60 },
90
+ { name: 'REVIEW', label: 'Running tests', weight: 10 }
91
+ ];
92
+
93
+ const results = {};
94
+ let currentProgress = 0;
95
+
96
+ try {
97
+ // Step 1: INIT
98
+ await executeStep(steps[0], currentProgress, async () => {
99
+ await fs.ensureDir(projectPath);
100
+ process.chdir(projectPath);
101
+ await createWorkspace();
102
+ results.projectPath = projectPath;
103
+ });
104
+ currentProgress += steps[0].weight;
105
+
106
+ // Step 2: INTAKE
107
+ await executeStep(steps[1], currentProgress, async () => {
108
+ const sessionId = await createSession(projectName);
109
+ const intakeContent = getIntakeTemplate(projectName, description, sessionId);
110
+ await writeSessionFile('intake.md', intakeContent);
111
+ await transitionTo(STATES.INTAKE_CAPTURED, 'magic_intake');
112
+ results.sessionId = sessionId;
113
+ });
114
+ currentProgress += steps[1].weight;
115
+
116
+ // Step 3: BLUEPRINT
117
+ await executeStep(steps[2], currentProgress, async () => {
118
+ const sessionId = await getCurrentSessionId();
119
+ const blueprintContent = getBlueprintTemplate(projectName, sessionId);
120
+ await writeSessionFile('blueprint.md', blueprintContent);
121
+ await transitionTo(STATES.BLUEPRINT_DRAFTED, 'magic_blueprint');
122
+ });
123
+ currentProgress += steps[2].weight;
124
+
125
+ // Step 4: CONTRACT
126
+ await executeStep(steps[3], currentProgress, async () => {
127
+ const sessionId = await getCurrentSessionId();
128
+ const intakeContent = await readSessionFile('intake.md');
129
+ const blueprintContent = await readSessionFile('blueprint.md');
130
+ const contractContent = getContractTemplate(projectName, sessionId, intakeContent, blueprintContent);
131
+ await writeSessionFile('contract.md', contractContent);
132
+ await transitionTo(STATES.CONTRACT_DRAFTED, 'magic_contract');
133
+ });
134
+ currentProgress += steps[3].weight;
135
+
136
+ // Step 5: LOCK
137
+ await executeStep(steps[4], currentProgress, async () => {
138
+ const contractContent = await readSessionFile('contract.md');
139
+ const specHash = generateSpecHash(contractContent);
140
+
141
+ // Update contract with hash
142
+ const updatedContract = contractContent.replace(
143
+ /## Spec Hash: \[hash when locked\]/,
144
+ `## Spec Hash: ${specHash}`
145
+ ).replace(
146
+ /## Status: DRAFT/,
147
+ '## Status: LOCKED'
148
+ );
149
+ await writeSessionFile('contract.md', updatedContract);
150
+
151
+ // Save to state
152
+ const stateData = await loadState();
153
+ stateData.spec_hash = specHash;
154
+ stateData.contract_locked = new Date().toISOString();
155
+ await saveState(stateData);
156
+
157
+ await transitionTo(STATES.CONTRACT_LOCKED, 'magic_lock');
158
+ results.specHash = specHash.substring(0, 8);
159
+ });
160
+ currentProgress += steps[4].weight;
161
+
162
+ // Step 6: PLAN
163
+ await executeStep(steps[5], currentProgress, async () => {
164
+ const sessionId = await getCurrentSessionId();
165
+ const contractContent = await readSessionFile('contract.md');
166
+ const blueprintContent = await readSessionFile('blueprint.md');
167
+ const intakeContent = await readSessionFile('intake.md');
168
+
169
+ const stateData = await loadState();
170
+ const specHash = stateData.spec_hash;
171
+
172
+ const planContent = getPlanTemplate(projectName, sessionId, specHash, contractContent);
173
+ await writeSessionFile('plan.md', planContent);
174
+
175
+ const coderPackContent = getCoderPackTemplate(
176
+ projectName, sessionId, specHash, contractContent, blueprintContent, intakeContent
177
+ );
178
+ await writeSessionFile('coder_pack.md', coderPackContent);
179
+
180
+ await transitionTo(STATES.PLAN_CREATED, 'magic_plan');
181
+ });
182
+ currentProgress += steps[5].weight;
183
+
184
+ // Step 7: BUILD
185
+ await executeStep(steps[6], currentProgress, async () => {
186
+ const sessionPath = await getCurrentSessionPath();
187
+ const evidencePath = path.join(sessionPath, 'evidence');
188
+ await ensureDir(evidencePath);
189
+ const logPath = path.join(evidencePath, 'build.log');
190
+
191
+ await transitionTo(STATES.BUILD_IN_PROGRESS, 'magic_build_start');
192
+
193
+ const coderPackContent = await readSessionFile('coder_pack.md');
194
+ const fullPrompt = await buildPromptWithContext(coderPackContent, process.cwd());
195
+
196
+ await appendToFile(logPath, `[${new Date().toISOString()}] Magic Mode Build Started\n`);
197
+
198
+ const buildResult = await spawnClaudeCode(fullPrompt, {
199
+ cwd: process.cwd(),
200
+ logPath: logPath
201
+ });
202
+
203
+ await appendToFile(logPath, `[${new Date().toISOString()}] Build completed with code: ${buildResult.code}\n`);
204
+
205
+ // Count files created
206
+ const files = await fs.readdir(process.cwd());
207
+ results.filesCreated = files.filter(f => !f.startsWith('.')).length;
208
+
209
+ // Reload state fresh before saving
210
+ const freshState = await loadState();
211
+ freshState.build_completed = new Date().toISOString();
212
+ await saveState(freshState);
213
+
214
+ await transitionTo(STATES.BUILD_DONE, 'magic_build_done');
215
+ });
216
+ currentProgress += steps[6].weight;
217
+
218
+ // Step 8: REVIEW
219
+ await executeStep(steps[7], currentProgress, async () => {
220
+ const testResult = await runTests(process.cwd());
221
+ results.testsPassed = testResult.summary.passed;
222
+ results.testsTotal = testResult.summary.total;
223
+ results.allPassed = testResult.passed;
224
+
225
+ if (testResult.passed) {
226
+ await transitionTo(STATES.REVIEW_PASSED, 'magic_review_passed');
227
+ } else {
228
+ const errors = analyzeErrors(testResult);
229
+ results.errors = errors.length;
230
+ // Still mark as review passed for magic mode (best effort)
231
+ await transitionTo(STATES.REVIEW_PASSED, 'magic_review_completed');
232
+ }
233
+ });
234
+ currentProgress = 100;
235
+
236
+ // Show final progress
237
+ console.log(renderProgressBar(100));
238
+ console.log();
239
+
240
+ // Show summary
241
+ const duration = ((Date.now() - startTime) / 1000 / 60).toFixed(1);
242
+ showMagicSummary(projectName, projectPath, duration, results, options);
243
+
244
+ // Auto-open if requested
245
+ if (options.open) {
246
+ await openProject(projectPath);
247
+ }
248
+
249
+ } catch (error) {
250
+ console.log();
251
+ printError(`Magic mode failed: ${error.message}`);
252
+ console.log(chalk.gray(`Project location: ${projectPath}`));
253
+ console.log(chalk.gray('Check .vibecode/sessions/*/evidence/build.log for details.'));
254
+ process.exit(1);
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Execute a single step with progress display
260
+ */
261
+ async function executeStep(step, currentProgress, fn) {
262
+ const spinner = ora({
263
+ text: chalk.gray(step.label),
264
+ prefixText: renderProgressBar(currentProgress)
265
+ }).start();
266
+
267
+ try {
268
+ await fn();
269
+ spinner.stopAndPersist({
270
+ symbol: chalk.green('✓'),
271
+ text: chalk.green(step.name),
272
+ prefixText: renderProgressBar(currentProgress + step.weight)
273
+ });
274
+ } catch (error) {
275
+ spinner.stopAndPersist({
276
+ symbol: chalk.red('✗'),
277
+ text: chalk.red(`${step.name}: ${error.message}`),
278
+ prefixText: renderProgressBar(currentProgress)
279
+ });
280
+ throw error;
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Generate project name from description
286
+ */
287
+ function generateProjectName(description) {
288
+ const stopWords = ['a', 'an', 'the', 'for', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'with', 'my', 'our'];
289
+
290
+ const words = description
291
+ .toLowerCase()
292
+ .replace(/[^a-z0-9\s]/g, '')
293
+ .split(/\s+/)
294
+ .filter(w => w.length > 2 && !stopWords.includes(w))
295
+ .slice(0, 3);
296
+
297
+ if (words.length === 0) {
298
+ return `vibecode-${Date.now().toString(36)}`;
299
+ }
300
+
301
+ return words.join('-');
302
+ }
303
+
304
+ /**
305
+ * Render progress bar
306
+ */
307
+ function renderProgressBar(percent) {
308
+ const width = 40;
309
+ const filled = Math.round(width * percent / 100);
310
+ const empty = width - filled;
311
+ const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
312
+ return `[${bar}] ${String(percent).padStart(3)}%`;
313
+ }
314
+
315
+ /**
316
+ * Show magic mode header
317
+ */
318
+ function showMagicHeader(description, projectName) {
319
+ const truncatedDesc = description.length > 50
320
+ ? description.substring(0, 47) + '...'
321
+ : description;
322
+
323
+ console.log();
324
+ console.log(chalk.cyan('╭' + '─'.repeat(68) + '╮'));
325
+ console.log(chalk.cyan('│') + ' '.repeat(68) + chalk.cyan('│'));
326
+ console.log(chalk.cyan('│') + chalk.bold.white(' 🚀 VIBECODE MAGIC MODE') + ' '.repeat(42) + chalk.cyan('│'));
327
+ console.log(chalk.cyan('│') + ' '.repeat(68) + chalk.cyan('│'));
328
+ console.log(chalk.cyan('│') + chalk.gray(` "${truncatedDesc}"`) + ' '.repeat(Math.max(0, 65 - truncatedDesc.length - 3)) + chalk.cyan('│'));
329
+ console.log(chalk.cyan('│') + chalk.gray(` → ${projectName}`) + ' '.repeat(Math.max(0, 65 - projectName.length - 5)) + chalk.cyan('│'));
330
+ console.log(chalk.cyan('│') + ' '.repeat(68) + chalk.cyan('│'));
331
+ console.log(chalk.cyan('╰' + '─'.repeat(68) + '╯'));
332
+ console.log();
333
+ }
334
+
335
+ /**
336
+ * Show magic mode summary
337
+ */
338
+ function showMagicSummary(projectName, projectPath, duration, results, options) {
339
+ const testsStatus = results.allPassed
340
+ ? chalk.green(`${results.testsPassed}/${results.testsTotal} passed`)
341
+ : chalk.yellow(`${results.testsPassed}/${results.testsTotal} passed`);
342
+
343
+ console.log(chalk.green('╭' + '─'.repeat(68) + '╮'));
344
+ console.log(chalk.green('│') + ' '.repeat(68) + chalk.green('│'));
345
+ console.log(chalk.green('│') + chalk.bold.white(' 🎉 BUILD COMPLETE!') + ' '.repeat(46) + chalk.green('│'));
346
+ console.log(chalk.green('│') + ' '.repeat(68) + chalk.green('│'));
347
+ console.log(chalk.green('│') + chalk.white(` 📁 Project: ${projectName}`) + ' '.repeat(Math.max(0, 51 - projectName.length)) + chalk.green('│'));
348
+ console.log(chalk.green('│') + chalk.white(` 📂 Location: ${projectPath}`) + ' '.repeat(Math.max(0, 51 - projectPath.length)) + chalk.green('│'));
349
+ console.log(chalk.green('│') + chalk.white(` 🔐 Spec: ${results.specHash}...`) + ' '.repeat(42) + chalk.green('│'));
350
+ console.log(chalk.green('│') + chalk.white(` 📄 Files: ${results.filesCreated} created`) + ' '.repeat(42) + chalk.green('│'));
351
+ console.log(chalk.green('│') + chalk.white(` 🧪 Tests: `) + testsStatus + ' '.repeat(Math.max(0, 40)) + chalk.green('│'));
352
+ console.log(chalk.green('│') + chalk.white(` ⏱️ Duration: ${duration} minutes`) + ' '.repeat(Math.max(0, 44 - duration.length)) + chalk.green('│'));
353
+ console.log(chalk.green('│') + ' '.repeat(68) + chalk.green('│'));
354
+ console.log(chalk.green('│') + chalk.gray(` 💡 Next: cd ${projectName}`) + ' '.repeat(Math.max(0, 51 - projectName.length)) + chalk.green('│'));
355
+ console.log(chalk.green('│') + ' '.repeat(68) + chalk.green('│'));
356
+ console.log(chalk.green('╰' + '─'.repeat(68) + '╯'));
357
+ console.log();
358
+ }
359
+
360
+ /**
361
+ * Open project in file explorer / browser
362
+ */
363
+ async function openProject(projectPath) {
364
+ try {
365
+ const platform = process.platform;
366
+ let cmd;
367
+
368
+ if (platform === 'darwin') {
369
+ cmd = `open "${projectPath}"`;
370
+ } else if (platform === 'win32') {
371
+ cmd = `explorer "${projectPath}"`;
372
+ } else {
373
+ cmd = `xdg-open "${projectPath}"`;
374
+ }
375
+
376
+ await execAsync(cmd);
377
+ } catch (error) {
378
+ console.log(chalk.gray(`Could not auto-open: ${error.message}`));
379
+ }
380
+ }
@@ -43,9 +43,10 @@ export async function runTests(projectPath) {
43
43
  results.tests.push(npmTest);
44
44
  }
45
45
 
46
- // 3. Run npm run lint (if script exists)
46
+ // 3. Run npm run lint (if script exists) - soft fail (warnings only)
47
47
  if (pkg.scripts?.lint) {
48
48
  const npmLint = await runCommand('npm run lint', projectPath, 'npm lint');
49
+ npmLint.softFail = true; // Lint errors are warnings, don't block build
49
50
  results.tests.push(npmLint);
50
51
  }
51
52
 
@@ -70,11 +71,21 @@ export async function runTests(projectPath) {
70
71
  }
71
72
 
72
73
  // 7. Aggregate results
74
+ // Separate hard tests from soft-fail tests (like lint)
75
+ const hardTests = results.tests.filter(t => !t.softFail);
76
+ const softTests = results.tests.filter(t => t.softFail);
77
+
73
78
  results.summary.total = results.tests.length;
74
79
  results.summary.passed = results.tests.filter(t => t.passed).length;
75
80
  results.summary.failed = results.tests.filter(t => !t.passed).length;
76
- results.passed = results.tests.length === 0 || results.tests.every(t => t.passed);
77
- results.errors = results.tests.filter(t => !t.passed).flatMap(t => t.errors || []);
81
+ results.summary.warnings = softTests.filter(t => !t.passed).length;
82
+
83
+ // Only hard tests determine pass/fail
84
+ results.passed = hardTests.length === 0 || hardTests.every(t => t.passed);
85
+
86
+ // Collect errors, but mark soft-fail errors as warnings
87
+ results.errors = results.tests.filter(t => !t.passed && !t.softFail).flatMap(t => t.errors || []);
88
+ results.warnings = softTests.filter(t => !t.passed).flatMap(t => t.errors || []);
78
89
  results.duration = Date.now() - startTime;
79
90
 
80
91
  return results;
@@ -230,12 +241,17 @@ export function formatTestResults(results) {
230
241
  const lines = [];
231
242
 
232
243
  lines.push(`Tests: ${results.summary.passed}/${results.summary.total} passed`);
244
+ if (results.summary.warnings > 0) {
245
+ lines.push(`Warnings: ${results.summary.warnings} (lint)`);
246
+ }
233
247
  lines.push(`Duration: ${(results.duration / 1000).toFixed(1)}s`);
234
248
 
235
- if (!results.passed) {
249
+ // Show hard failures
250
+ const hardFailures = results.tests.filter(t => !t.passed && !t.softFail);
251
+ if (hardFailures.length > 0) {
236
252
  lines.push('');
237
253
  lines.push('Failed tests:');
238
- for (const test of results.tests.filter(t => !t.passed)) {
254
+ for (const test of hardFailures) {
239
255
  lines.push(` ❌ ${test.name}`);
240
256
  for (const error of test.errors || []) {
241
257
  const loc = error.file ? `${error.file}:${error.line || '?'}` : '';
@@ -244,5 +260,22 @@ export function formatTestResults(results) {
244
260
  }
245
261
  }
246
262
 
263
+ // Show soft failures (warnings)
264
+ const softFailures = results.tests.filter(t => !t.passed && t.softFail);
265
+ if (softFailures.length > 0) {
266
+ lines.push('');
267
+ lines.push('Warnings (non-blocking):');
268
+ for (const test of softFailures) {
269
+ lines.push(` ⚠️ ${test.name}`);
270
+ for (const error of (test.errors || []).slice(0, 3)) {
271
+ const loc = error.file ? `${error.file}:${error.line || '?'}` : '';
272
+ lines.push(` ${loc} ${error.message?.substring(0, 80) || ''}`);
273
+ }
274
+ if ((test.errors?.length || 0) > 3) {
275
+ lines.push(` ... and ${test.errors.length - 3} more`);
276
+ }
277
+ }
278
+ }
279
+
247
280
  return lines.join('\n');
248
281
  }