@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.
@@ -0,0 +1,554 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════════
2
+ // VIBECODE CLI - Preview Command
3
+ // Auto-start dev server and open browser
4
+ // ═══════════════════════════════════════════════════════════════════════════════
5
+
6
+ import { spawn, exec } from 'child_process';
7
+ import { promisify } from 'util';
8
+ import fs from 'fs/promises';
9
+ import path from 'path';
10
+ import chalk from 'chalk';
11
+ import net from 'net';
12
+ import os from 'os';
13
+
14
+ const execAsync = promisify(exec);
15
+
16
+ // Store running processes
17
+ const runningProcesses = new Map();
18
+
19
+ /**
20
+ * Preview command entry point
21
+ */
22
+ export async function previewCommand(options = {}) {
23
+ const cwd = process.cwd();
24
+
25
+ // Stop preview
26
+ if (options.stop) {
27
+ return stopPreview(cwd);
28
+ }
29
+
30
+ // Check if it's a valid project
31
+ const projectType = await detectProjectType(cwd);
32
+
33
+ if (!projectType) {
34
+ console.log(chalk.red(`
35
+ ╭────────────────────────────────────────────────────────────────────╮
36
+ │ ❌ NOT A VALID PROJECT │
37
+ │ │
38
+ │ No package.json found or unsupported project type. │
39
+ │ │
40
+ │ Supported: Next.js, React, Vue, Vite, Express │
41
+ ╰────────────────────────────────────────────────────────────────────╯
42
+ `));
43
+ return;
44
+ }
45
+
46
+ // Find available port
47
+ const requestedPort = parseInt(options.port) || 3000;
48
+ const port = await findAvailablePort(requestedPort);
49
+
50
+ console.log(chalk.cyan(`
51
+ ╭────────────────────────────────────────────────────────────────────╮
52
+ │ 🚀 VIBECODE PREVIEW │
53
+ ╰────────────────────────────────────────────────────────────────────╯
54
+ `));
55
+
56
+ console.log(chalk.gray(` Project: ${path.basename(cwd)}`));
57
+ console.log(chalk.gray(` Type: ${projectType.name}`));
58
+ console.log(chalk.gray(` Port: ${port}\n`));
59
+
60
+ // Step 1: Install dependencies if needed
61
+ const needsInstall = await checkNeedsInstall(cwd);
62
+
63
+ if (needsInstall) {
64
+ console.log(chalk.yellow(' 📦 Installing dependencies...\n'));
65
+ try {
66
+ await installDependencies(cwd);
67
+ console.log(chalk.green(' ✅ Dependencies installed\n'));
68
+ } catch (error) {
69
+ console.log(chalk.red(` ❌ Install failed: ${error.message}\n`));
70
+ return;
71
+ }
72
+ }
73
+
74
+ // Step 2: Start dev server
75
+ console.log(chalk.yellow(' 🔧 Starting dev server...\n'));
76
+
77
+ let serverProcess;
78
+ try {
79
+ serverProcess = await startDevServer(cwd, projectType, port);
80
+ runningProcesses.set(cwd, serverProcess);
81
+ } catch (error) {
82
+ console.log(chalk.red(` ❌ Failed to start server: ${error.message}\n`));
83
+ return;
84
+ }
85
+
86
+ // Step 3: Wait for server to be ready
87
+ const isReady = await waitForServer(port);
88
+
89
+ if (!isReady) {
90
+ console.log(chalk.yellow(' ⚠️ Server may still be starting...\n'));
91
+ }
92
+
93
+ const url = `http://localhost:${port}`;
94
+
95
+ // Step 4: Display success
96
+ console.log(chalk.green(`
97
+ ╭────────────────────────────────────────────────────────────────────╮
98
+ │ ✅ PREVIEW READY │
99
+ │ │
100
+ │ 🌐 Local: ${url.padEnd(49)}│
101
+ ╰────────────────────────────────────────────────────────────────────╯
102
+ `));
103
+
104
+ // Step 5: Show QR code for mobile
105
+ if (options.qr) {
106
+ await showQRCode(port);
107
+ }
108
+
109
+ // Step 6: Open browser
110
+ if (options.open !== false) {
111
+ try {
112
+ await openBrowser(url);
113
+ console.log(chalk.green(' ✅ Opened in browser\n'));
114
+ } catch (error) {
115
+ console.log(chalk.yellow(` ⚠️ Could not open browser: ${error.message}\n`));
116
+ console.log(chalk.gray(` Open manually: ${url}\n`));
117
+ }
118
+ }
119
+
120
+ // Step 7: Show controls
121
+ console.log(chalk.gray(` Controls:
122
+ ${chalk.cyan('vibecode preview --stop')} Stop server
123
+ ${chalk.cyan('Ctrl+C')} Stop server
124
+ `));
125
+
126
+ // Handle graceful shutdown
127
+ process.on('SIGINT', () => {
128
+ console.log(chalk.yellow('\n\n Stopping server...'));
129
+ if (serverProcess) {
130
+ serverProcess.kill();
131
+ }
132
+ console.log(chalk.green(' ✅ Server stopped\n'));
133
+ process.exit(0);
134
+ });
135
+
136
+ // Keep process running unless detached
137
+ if (!options.detach) {
138
+ await new Promise(() => {}); // Keep alive
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Detect project type from package.json
144
+ */
145
+ async function detectProjectType(cwd) {
146
+ try {
147
+ const pkgPath = path.join(cwd, 'package.json');
148
+ const pkgContent = await fs.readFile(pkgPath, 'utf-8');
149
+ const pkg = JSON.parse(pkgContent);
150
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
151
+ const scripts = pkg.scripts || {};
152
+
153
+ if (deps.next) {
154
+ return {
155
+ name: 'Next.js',
156
+ devScript: 'dev',
157
+ defaultPort: 3000,
158
+ portFlag: '-p'
159
+ };
160
+ }
161
+
162
+ if (deps.vite) {
163
+ return {
164
+ name: 'Vite',
165
+ devScript: 'dev',
166
+ defaultPort: 5173,
167
+ portFlag: '--port'
168
+ };
169
+ }
170
+
171
+ if (deps['react-scripts']) {
172
+ return {
173
+ name: 'Create React App',
174
+ devScript: 'start',
175
+ defaultPort: 3000,
176
+ portEnv: 'PORT'
177
+ };
178
+ }
179
+
180
+ if (deps.vue || deps['@vue/cli-service']) {
181
+ return {
182
+ name: 'Vue',
183
+ devScript: scripts.dev ? 'dev' : 'serve',
184
+ defaultPort: 8080,
185
+ portFlag: '--port'
186
+ };
187
+ }
188
+
189
+ if (deps.nuxt) {
190
+ return {
191
+ name: 'Nuxt',
192
+ devScript: 'dev',
193
+ defaultPort: 3000,
194
+ portFlag: '--port'
195
+ };
196
+ }
197
+
198
+ if (deps.svelte || deps['@sveltejs/kit']) {
199
+ return {
200
+ name: 'SvelteKit',
201
+ devScript: 'dev',
202
+ defaultPort: 5173,
203
+ portFlag: '--port'
204
+ };
205
+ }
206
+
207
+ if (deps.express || deps.fastify || deps.koa || deps.hono) {
208
+ return {
209
+ name: 'Node.js Server',
210
+ devScript: scripts.dev ? 'dev' : 'start',
211
+ defaultPort: 3000,
212
+ portEnv: 'PORT'
213
+ };
214
+ }
215
+
216
+ // Generic with dev script
217
+ if (scripts.dev) {
218
+ return {
219
+ name: 'Generic',
220
+ devScript: 'dev',
221
+ defaultPort: 3000,
222
+ portEnv: 'PORT'
223
+ };
224
+ }
225
+
226
+ if (scripts.start) {
227
+ return {
228
+ name: 'Generic',
229
+ devScript: 'start',
230
+ defaultPort: 3000,
231
+ portEnv: 'PORT'
232
+ };
233
+ }
234
+
235
+ return null;
236
+ } catch {
237
+ return null;
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Check if node_modules needs to be installed
243
+ */
244
+ async function checkNeedsInstall(cwd) {
245
+ const nodeModulesPath = path.join(cwd, 'node_modules');
246
+ try {
247
+ await fs.access(nodeModulesPath);
248
+ // Check if node_modules has content
249
+ const contents = await fs.readdir(nodeModulesPath);
250
+ return contents.length < 5; // Likely incomplete install
251
+ } catch {
252
+ return true;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Install dependencies
258
+ */
259
+ async function installDependencies(cwd) {
260
+ return new Promise((resolve, reject) => {
261
+ const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
262
+ let i = 0;
263
+
264
+ const interval = setInterval(() => {
265
+ process.stdout.write(`\r ${spinner[i++ % spinner.length]} Installing...`);
266
+ }, 100);
267
+
268
+ const child = spawn('npm', ['install'], {
269
+ cwd,
270
+ stdio: ['ignore', 'pipe', 'pipe'],
271
+ shell: true
272
+ });
273
+
274
+ let stderr = '';
275
+ child.stderr.on('data', (data) => {
276
+ stderr += data.toString();
277
+ });
278
+
279
+ child.on('close', (code) => {
280
+ clearInterval(interval);
281
+ process.stdout.write('\r \r');
282
+
283
+ if (code === 0) {
284
+ resolve();
285
+ } else {
286
+ reject(new Error(stderr || `npm install failed with code ${code}`));
287
+ }
288
+ });
289
+
290
+ child.on('error', (err) => {
291
+ clearInterval(interval);
292
+ reject(err);
293
+ });
294
+ });
295
+ }
296
+
297
+ /**
298
+ * Start development server
299
+ */
300
+ async function startDevServer(cwd, projectType, port) {
301
+ const env = { ...process.env };
302
+
303
+ // Set port via environment variable if needed
304
+ if (projectType.portEnv) {
305
+ env[projectType.portEnv] = String(port);
306
+ }
307
+
308
+ // Build the command
309
+ const args = ['run', projectType.devScript];
310
+
311
+ // Add port flag if supported
312
+ if (projectType.portFlag) {
313
+ args.push('--', projectType.portFlag, String(port));
314
+ }
315
+
316
+ const child = spawn('npm', args, {
317
+ cwd,
318
+ stdio: ['ignore', 'pipe', 'pipe'],
319
+ shell: true,
320
+ env,
321
+ detached: false
322
+ });
323
+
324
+ // Log output for debugging
325
+ child.stdout.on('data', (data) => {
326
+ const output = data.toString().trim();
327
+ // Show ready messages
328
+ if (output.toLowerCase().includes('ready') ||
329
+ output.toLowerCase().includes('started') ||
330
+ output.toLowerCase().includes('listening') ||
331
+ output.toLowerCase().includes('compiled')) {
332
+ console.log(chalk.gray(` ${output.substring(0, 60)}`));
333
+ }
334
+ });
335
+
336
+ child.stderr.on('data', (data) => {
337
+ const output = data.toString().trim();
338
+ // Only show actual errors, not warnings
339
+ if (output.toLowerCase().includes('error') &&
340
+ !output.toLowerCase().includes('warning')) {
341
+ console.log(chalk.red(` ${output.substring(0, 60)}`));
342
+ }
343
+ });
344
+
345
+ child.on('error', (error) => {
346
+ console.log(chalk.red(` Server error: ${error.message}`));
347
+ });
348
+
349
+ return child;
350
+ }
351
+
352
+ /**
353
+ * Wait for server to be ready by checking port
354
+ */
355
+ async function waitForServer(port, maxAttempts = 30) {
356
+ const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
357
+ let i = 0;
358
+
359
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
360
+ process.stdout.write(`\r ${spinner[i++ % spinner.length]} Waiting for server...`);
361
+
362
+ const isReady = await checkPort(port);
363
+
364
+ if (isReady) {
365
+ process.stdout.write('\r ✅ Server ready \n');
366
+ return true;
367
+ }
368
+
369
+ await sleep(1000);
370
+ }
371
+
372
+ process.stdout.write('\r ⚠️ Server taking longer than expected\n');
373
+ return false;
374
+ }
375
+
376
+ /**
377
+ * Check if a port is in use (server is running)
378
+ */
379
+ function checkPort(port) {
380
+ return new Promise((resolve) => {
381
+ const socket = new net.Socket();
382
+
383
+ socket.setTimeout(1000);
384
+
385
+ socket.on('connect', () => {
386
+ socket.destroy();
387
+ resolve(true);
388
+ });
389
+
390
+ socket.on('timeout', () => {
391
+ socket.destroy();
392
+ resolve(false);
393
+ });
394
+
395
+ socket.on('error', () => {
396
+ socket.destroy();
397
+ resolve(false);
398
+ });
399
+
400
+ socket.connect(port, '127.0.0.1');
401
+ });
402
+ }
403
+
404
+ /**
405
+ * Find an available port starting from the given port
406
+ */
407
+ async function findAvailablePort(startPort) {
408
+ let port = startPort;
409
+
410
+ while (port < startPort + 100) {
411
+ const inUse = await checkPort(port);
412
+ if (!inUse) {
413
+ return port;
414
+ }
415
+ port++;
416
+ }
417
+
418
+ return startPort;
419
+ }
420
+
421
+ /**
422
+ * Open URL in default browser
423
+ */
424
+ async function openBrowser(url) {
425
+ try {
426
+ const open = (await import('open')).default;
427
+ await open(url);
428
+ } catch (error) {
429
+ // Fallback for different OS
430
+ const { platform } = process;
431
+ const commands = {
432
+ darwin: `open "${url}"`,
433
+ win32: `start "" "${url}"`,
434
+ linux: `xdg-open "${url}"`
435
+ };
436
+
437
+ if (commands[platform]) {
438
+ await execAsync(commands[platform]);
439
+ } else {
440
+ throw new Error('Unsupported platform');
441
+ }
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Show QR code for mobile access
447
+ */
448
+ async function showQRCode(port) {
449
+ try {
450
+ // Get local IP
451
+ const nets = os.networkInterfaces();
452
+ let localIP = 'localhost';
453
+
454
+ for (const name of Object.keys(nets)) {
455
+ for (const netInterface of nets[name]) {
456
+ if (netInterface.family === 'IPv4' && !netInterface.internal) {
457
+ localIP = netInterface.address;
458
+ break;
459
+ }
460
+ }
461
+ if (localIP !== 'localhost') break;
462
+ }
463
+
464
+ const networkUrl = `http://${localIP}:${port}`;
465
+
466
+ // Try to generate QR code
467
+ try {
468
+ const QRCode = (await import('qrcode')).default;
469
+ const qrString = await QRCode.toString(networkUrl, {
470
+ type: 'terminal',
471
+ small: true
472
+ });
473
+
474
+ console.log(chalk.cyan('\n 📱 Scan to open on mobile:\n'));
475
+ // Indent QR code
476
+ const indentedQR = qrString.split('\n').map(line => ' ' + line).join('\n');
477
+ console.log(indentedQR);
478
+ console.log(chalk.gray(`\n Network: ${networkUrl}\n`));
479
+ } catch {
480
+ // QR code package not available, just show URL
481
+ console.log(chalk.cyan('\n 📱 Mobile access:\n'));
482
+ console.log(chalk.white(` ${networkUrl}\n`));
483
+ console.log(chalk.gray(' (Install qrcode for QR: npm i qrcode)\n'));
484
+ }
485
+ } catch (error) {
486
+ console.log(chalk.yellow(` ⚠️ Could not get network info: ${error.message}\n`));
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Stop running preview server
492
+ */
493
+ async function stopPreview(cwd) {
494
+ const runningProcess = runningProcesses.get(cwd);
495
+
496
+ if (runningProcess) {
497
+ runningProcess.kill();
498
+ runningProcesses.delete(cwd);
499
+ console.log(chalk.green('\n ✅ Preview server stopped\n'));
500
+ return;
501
+ }
502
+
503
+ // Try to kill any process on common dev ports
504
+ console.log(chalk.yellow('\n Attempting to stop dev servers...\n'));
505
+
506
+ try {
507
+ const { platform } = process;
508
+
509
+ if (platform === 'win32') {
510
+ // Windows
511
+ await execAsync('netstat -ano | findstr :3000 | findstr LISTENING').catch(() => {});
512
+ } else {
513
+ // Unix-like
514
+ await execAsync('lsof -ti:3000 | xargs kill -9 2>/dev/null || true');
515
+ await execAsync('lsof -ti:3001 | xargs kill -9 2>/dev/null || true');
516
+ await execAsync('lsof -ti:5173 | xargs kill -9 2>/dev/null || true');
517
+ await execAsync('lsof -ti:8080 | xargs kill -9 2>/dev/null || true');
518
+ }
519
+
520
+ console.log(chalk.green(' ✅ Dev servers stopped\n'));
521
+ } catch {
522
+ console.log(chalk.gray(' No running servers found.\n'));
523
+ }
524
+ }
525
+
526
+ /**
527
+ * Sleep helper
528
+ */
529
+ function sleep(ms) {
530
+ return new Promise(resolve => setTimeout(resolve, ms));
531
+ }
532
+
533
+ /**
534
+ * Auto preview for use after builds
535
+ * Called from go.js after successful build
536
+ */
537
+ export async function autoPreview(projectPath, options = {}) {
538
+ const originalCwd = process.cwd();
539
+
540
+ try {
541
+ process.chdir(projectPath);
542
+ await previewCommand({
543
+ open: true,
544
+ qr: options.qr || false,
545
+ port: options.port,
546
+ detach: true, // Don't block in auto mode
547
+ ...options
548
+ });
549
+ } catch (error) {
550
+ console.log(chalk.yellow(` ⚠️ Preview failed: ${error.message}\n`));
551
+ } finally {
552
+ process.chdir(originalCwd);
553
+ }
554
+ }