@nclamvn/vibecode-cli 2.2.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 +15 -2
- package/package.json +3 -1
- package/src/commands/go.js +42 -4
- package/src/commands/preview.js +554 -0
- package/src/index.js +3 -1
package/bin/vibecode.js
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
migrateCommand,
|
|
36
36
|
// Phase M Commands
|
|
37
37
|
templatesCommand,
|
|
38
|
+
previewCommand,
|
|
38
39
|
VERSION
|
|
39
40
|
} from '../src/index.js';
|
|
40
41
|
|
|
@@ -142,8 +143,10 @@ program
|
|
|
142
143
|
.option('--color <color>', 'Primary brand color (hex)')
|
|
143
144
|
.option('-i, --iterate', 'Enable iterative build mode')
|
|
144
145
|
.option('-m, --max <n>', 'Max iterations for iterative mode', parseInt)
|
|
145
|
-
.option('-o, --open', 'Auto-open
|
|
146
|
-
.option('-p, --preview', '
|
|
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')
|
|
147
150
|
.action((description, options) => {
|
|
148
151
|
const desc = Array.isArray(description) ? description.join(' ') : description;
|
|
149
152
|
goCommand(desc, options);
|
|
@@ -164,6 +167,16 @@ program
|
|
|
164
167
|
.option('-q, --quiet', 'Non-interactive mode')
|
|
165
168
|
.action(templatesCommand);
|
|
166
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);
|
|
179
|
+
|
|
167
180
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
168
181
|
// Phase F Commands - Agent Mode
|
|
169
182
|
// ─────────────────────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nclamvn/vibecode-cli",
|
|
3
|
-
"version": "2.2.
|
|
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": {
|
package/src/commands/go.js
CHANGED
|
@@ -259,9 +259,26 @@ export async function goCommand(description, options = {}) {
|
|
|
259
259
|
const duration = ((Date.now() - startTime) / 1000 / 60).toFixed(1);
|
|
260
260
|
showMagicSummary(projectName, projectPath, duration, results, options);
|
|
261
261
|
|
|
262
|
-
// Auto
|
|
263
|
-
if (options.
|
|
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) {
|
|
264
278
|
await openProject(projectPath);
|
|
279
|
+
} else {
|
|
280
|
+
// Show preview hint
|
|
281
|
+
console.log(chalk.gray(` 💡 Quick preview: ${chalk.cyan(`cd ${projectName} && vibecode preview`)}\n`));
|
|
265
282
|
}
|
|
266
283
|
|
|
267
284
|
} catch (error) {
|
|
@@ -572,9 +589,26 @@ ${prompt}
|
|
|
572
589
|
const duration = ((Date.now() - startTime) / 1000 / 60).toFixed(1);
|
|
573
590
|
showTemplateSummary(template, projectName, projectPath, duration, results, options);
|
|
574
591
|
|
|
575
|
-
// Auto
|
|
576
|
-
if (options.preview
|
|
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) {
|
|
577
608
|
await openProject(projectPath);
|
|
609
|
+
} else {
|
|
610
|
+
// Show preview hint
|
|
611
|
+
console.log(chalk.gray(` 💡 Quick preview: ${chalk.cyan(`cd ${projectName} && vibecode preview`)}\n`));
|
|
578
612
|
}
|
|
579
613
|
|
|
580
614
|
} catch (error) {
|
|
@@ -586,6 +620,10 @@ ${prompt}
|
|
|
586
620
|
}
|
|
587
621
|
}
|
|
588
622
|
|
|
623
|
+
function sleep(ms) {
|
|
624
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
625
|
+
}
|
|
626
|
+
|
|
589
627
|
/**
|
|
590
628
|
* Show template mode header
|
|
591
629
|
*/
|
|
@@ -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
|
+
}
|
package/src/index.js
CHANGED
|
@@ -58,7 +58,7 @@ export { securityCommand } from './commands/security.js';
|
|
|
58
58
|
export { askCommand } from './commands/ask.js';
|
|
59
59
|
export { migrateCommand } from './commands/migrate.js';
|
|
60
60
|
|
|
61
|
-
// Phase M Commands - Templates
|
|
61
|
+
// Phase M Commands - Templates & Preview
|
|
62
62
|
export { templatesCommand } from './commands/templates.js';
|
|
63
63
|
export {
|
|
64
64
|
TEMPLATES,
|
|
@@ -71,6 +71,8 @@ export {
|
|
|
71
71
|
isValidTemplate
|
|
72
72
|
} from './templates/index.js';
|
|
73
73
|
|
|
74
|
+
export { previewCommand, autoPreview } from './commands/preview.js';
|
|
75
|
+
|
|
74
76
|
// UI exports (Phase H2: Dashboard)
|
|
75
77
|
export {
|
|
76
78
|
ProgressDashboard,
|