@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 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 result when done')
146
- .option('-p, --preview', 'Auto-open in browser after build')
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.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": {
@@ -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-open if requested
263
- 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) {
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-open if requested
576
- if (options.preview || options.open) {
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,