genbox 1.0.71 → 1.0.72

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.
@@ -429,16 +429,23 @@ exports.createCommand = new commander_1.Command('create')
429
429
  console.log('');
430
430
  console.log(chalk_1.default.blue('=== Database Copy ==='));
431
431
  console.log(chalk_1.default.dim(` Source: ${resolved.database.source}`));
432
+ // Warn if snapshot is suspiciously small (< 50KB likely means empty database)
433
+ const isSnapshotTooSmall = existingSnapshot.sizeBytes < 50 * 1024;
434
+ if (isSnapshotTooSmall) {
435
+ console.log(chalk_1.default.yellow(` ⚠ Warning: Existing snapshot is very small (${(0, db_utils_1.formatBytes)(existingSnapshot.sizeBytes)})`));
436
+ console.log(chalk_1.default.yellow(` This likely means the source database was empty when snapshot was created.`));
437
+ console.log(chalk_1.default.yellow(` Consider creating a fresh snapshot to get current data.`));
438
+ }
432
439
  if (!options.yes) {
433
440
  const snapshotChoice = await prompts.select({
434
441
  message: 'Database snapshot:',
435
442
  choices: [
436
443
  {
437
- name: `Use existing snapshot (${timeAgoStr}, ${(0, db_utils_1.formatBytes)(existingSnapshot.sizeBytes)})`,
444
+ name: `Use existing snapshot (${timeAgoStr}, ${(0, db_utils_1.formatBytes)(existingSnapshot.sizeBytes)})${isSnapshotTooSmall ? chalk_1.default.yellow(' ⚠ small') : ''}`,
438
445
  value: 'existing',
439
446
  },
440
447
  {
441
- name: 'Create fresh snapshot (dump now)',
448
+ name: `Create fresh snapshot (dump now)${isSnapshotTooSmall ? chalk_1.default.green(' ← recommended') : ''}`,
442
449
  value: 'fresh',
443
450
  },
444
451
  ],
@@ -774,11 +781,10 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
774
781
  permissions: '0755',
775
782
  });
776
783
  }
777
- // Build post details (scripts to run)
784
+ // Build post details (scripts to run after setup)
785
+ // NOTE: The setup-genbox.sh is NOT included here because the API's cloud-init.service
786
+ // already handles running it. Adding it here would cause it to run twice.
778
787
  const postDetails = [];
779
- if (setupScript) {
780
- postDetails.push('su - dev -c "/home/dev/setup-genbox.sh"');
781
- }
782
788
  // Build repos
783
789
  const repos = {};
784
790
  for (const repo of resolved.repos) {
@@ -131,15 +131,21 @@ exports.dbSyncCommand
131
131
  const snapshotAge = Date.now() - new Date(existingSnapshot.createdAt).getTime();
132
132
  const hoursAgo = Math.floor(snapshotAge / (1000 * 60 * 60));
133
133
  const timeAgoStr = hoursAgo < 1 ? 'less than an hour ago' : `${hoursAgo} hours ago`;
134
+ // Warn if snapshot is suspiciously small (< 50KB likely means empty database)
135
+ const isSnapshotTooSmall = existingSnapshot.sizeBytes < 50 * 1024;
136
+ if (isSnapshotTooSmall) {
137
+ console.log(chalk_1.default.yellow(` ⚠ Warning: Existing snapshot is very small (${(0, db_utils_1.formatBytes)(existingSnapshot.sizeBytes)})`));
138
+ console.log(chalk_1.default.yellow(` This likely means the source database was empty when snapshot was created.`));
139
+ }
134
140
  const snapshotChoice = await prompts.select({
135
141
  message: `Found existing ${snapshotSource} snapshot (${timeAgoStr}):`,
136
142
  choices: [
137
143
  {
138
- name: `Use existing snapshot (${timeAgoStr}, ${(0, db_utils_1.formatBytes)(existingSnapshot.sizeBytes)})`,
144
+ name: `Use existing snapshot (${timeAgoStr}, ${(0, db_utils_1.formatBytes)(existingSnapshot.sizeBytes)})${isSnapshotTooSmall ? chalk_1.default.yellow(' ⚠ small') : ''}`,
139
145
  value: 'existing',
140
146
  },
141
147
  {
142
- name: 'Create fresh snapshot (dump now)',
148
+ name: `Create fresh snapshot (dump now)${isSnapshotTooSmall ? chalk_1.default.green(' ← recommended') : ''}`,
143
149
  value: 'fresh',
144
150
  },
145
151
  ],
@@ -690,6 +690,16 @@ async function setupEnvironmentsAndServiceUrls(detected, existingEnvValues) {
690
690
  console.log('');
691
691
  console.log(chalk_1.default.blue('=== Environment Configuration ==='));
692
692
  console.log('');
693
+ // Auto-detect database URLs from project
694
+ const detectedDbUrls = detectDatabaseUrls(detected._meta.scanned_root, detected);
695
+ if (detectedDbUrls.length > 0) {
696
+ console.log(chalk_1.default.dim('Detected database URLs in project:'));
697
+ for (const db of detectedDbUrls) {
698
+ console.log(chalk_1.default.dim(` • ${db.url}`));
699
+ console.log(chalk_1.default.dim(` └─ from ${db.source}${db.database ? ` (database: ${db.database})` : ''}`));
700
+ }
701
+ console.log('');
702
+ }
693
703
  // Which environments to configure
694
704
  const envChoice = await prompts.select({
695
705
  message: 'Which environments do you want to configure?',
@@ -704,8 +714,11 @@ async function setupEnvironmentsAndServiceUrls(detected, existingEnvValues) {
704
714
  if (envChoice !== 'skip') {
705
715
  console.log('');
706
716
  console.log(chalk_1.default.dim('These URLs will be used when connecting to external services.'));
717
+ console.log(chalk_1.default.dim('MongoDB URLs are used for taking database snapshots to copy to genbox.'));
707
718
  const configureStaging = envChoice === 'staging' || envChoice === 'both';
708
719
  const configureProduction = envChoice === 'production' || envChoice === 'both';
720
+ // Build suggested MongoDB URL from detected sources
721
+ const suggestedMongoUrl = buildMongoDbUrl(detectedDbUrls);
709
722
  if (configureStaging) {
710
723
  console.log('');
711
724
  console.log(chalk_1.default.cyan('Staging Environment:'));
@@ -717,7 +730,13 @@ async function setupEnvironmentsAndServiceUrls(detected, existingEnvValues) {
717
730
  };
718
731
  envVars['STAGING_API_URL'] = stagingApiUrl;
719
732
  }
720
- const stagingMongoUrl = await promptWithExisting(' Staging MongoDB URL (optional):', existingEnvValues['STAGING_MONGODB_URL'], true);
733
+ // Use auto-detected URL as default if no existing value
734
+ const existingStagingMongo = existingEnvValues['STAGING_MONGODB_URL'];
735
+ const stagingMongoDefault = existingStagingMongo || suggestedMongoUrl;
736
+ if (stagingMongoDefault && !existingStagingMongo) {
737
+ console.log(chalk_1.default.dim(` Auto-detected: ${stagingMongoDefault}`));
738
+ }
739
+ const stagingMongoUrl = await promptWithExisting(' Staging MongoDB URL (for snapshots):', stagingMongoDefault, true);
721
740
  if (stagingMongoUrl) {
722
741
  envVars['STAGING_MONGODB_URL'] = stagingMongoUrl;
723
742
  }
@@ -737,7 +756,13 @@ async function setupEnvironmentsAndServiceUrls(detected, existingEnvValues) {
737
756
  };
738
757
  envVars['PRODUCTION_API_URL'] = prodApiUrl;
739
758
  }
740
- const prodMongoUrl = await promptWithExisting(' Production MongoDB URL (optional):', existingEnvValues['PROD_MONGODB_URL'] || existingEnvValues['PRODUCTION_MONGODB_URL'], true);
759
+ // Use auto-detected URL as default if no existing value
760
+ const existingProdMongo = existingEnvValues['PROD_MONGODB_URL'] || existingEnvValues['PRODUCTION_MONGODB_URL'];
761
+ const prodMongoDefault = existingProdMongo || suggestedMongoUrl;
762
+ if (prodMongoDefault && !existingProdMongo) {
763
+ console.log(chalk_1.default.dim(` Auto-detected: ${prodMongoDefault}`));
764
+ }
765
+ const prodMongoUrl = await promptWithExisting(' Production MongoDB URL (for snapshots):', prodMongoDefault, true);
741
766
  if (prodMongoUrl) {
742
767
  envVars['PROD_MONGODB_URL'] = prodMongoUrl;
743
768
  }
@@ -1511,6 +1536,131 @@ function sshToHttps(url) {
1511
1536
  return `https://${match[1]}/${match[2]}`;
1512
1537
  return url;
1513
1538
  }
1539
+ /**
1540
+ * Auto-detect database URLs from project configuration
1541
+ * Scans .env files, docker-compose, and other config sources
1542
+ */
1543
+ function detectDatabaseUrls(rootDir, detected) {
1544
+ const results = [];
1545
+ const seenUrls = new Set();
1546
+ // 1. Scan .env files in various locations for MONGODB_URI, DATABASE_URL, etc.
1547
+ const envPatterns = [
1548
+ '.env',
1549
+ '.env.local',
1550
+ '.env.development',
1551
+ 'api/.env',
1552
+ 'api/.env.local',
1553
+ 'api/.env.development',
1554
+ ];
1555
+ // Also check microservice-specific env files
1556
+ const microserviceDirs = ['gateway', 'auth', 'products', 'notifications', 'partner-api'];
1557
+ for (const service of microserviceDirs) {
1558
+ envPatterns.push(`api/apps/${service}/.env`);
1559
+ envPatterns.push(`api/apps/${service}/.env.local`);
1560
+ envPatterns.push(`api/apps/${service}/.env.development`);
1561
+ }
1562
+ // Database URL variable patterns to look for
1563
+ const dbVarPatterns = [
1564
+ 'MONGODB_URI',
1565
+ 'MONGODB_URL',
1566
+ 'MONGO_URI',
1567
+ 'MONGO_URL',
1568
+ 'DATABASE_URL',
1569
+ 'DATABASE_URI',
1570
+ 'DB_URL',
1571
+ 'DB_URI',
1572
+ ];
1573
+ for (const envPattern of envPatterns) {
1574
+ const envPath = path_1.default.join(rootDir, envPattern);
1575
+ if (!fs_1.default.existsSync(envPath))
1576
+ continue;
1577
+ try {
1578
+ const content = fs_1.default.readFileSync(envPath, 'utf8');
1579
+ for (const line of content.split('\n')) {
1580
+ const trimmed = line.trim();
1581
+ if (!trimmed || trimmed.startsWith('#'))
1582
+ continue;
1583
+ const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
1584
+ if (!match)
1585
+ continue;
1586
+ const [, varName, rawValue] = match;
1587
+ // Check if this is a database URL variable
1588
+ if (!dbVarPatterns.includes(varName))
1589
+ continue;
1590
+ // Clean the value
1591
+ let value = rawValue.trim();
1592
+ if ((value.startsWith('"') && value.endsWith('"')) ||
1593
+ (value.startsWith("'") && value.endsWith("'"))) {
1594
+ value = value.slice(1, -1);
1595
+ }
1596
+ // Skip placeholders and empty values
1597
+ if (!value || value.includes('your-') || value.includes('xxx') ||
1598
+ value === '""' || value === "''")
1599
+ continue;
1600
+ // Skip if already seen
1601
+ if (seenUrls.has(value))
1602
+ continue;
1603
+ seenUrls.add(value);
1604
+ // Extract database name from URL
1605
+ const dbMatch = value.match(/mongodb(?:\+srv)?:\/\/[^/]+\/([^?]+)/);
1606
+ const database = dbMatch?.[1];
1607
+ // Extract port from URL
1608
+ const portMatch = value.match(/mongodb:\/\/[^:]+:(\d+)/);
1609
+ const port = portMatch ? parseInt(portMatch[1], 10) : undefined;
1610
+ results.push({
1611
+ url: value,
1612
+ source: envPattern,
1613
+ database,
1614
+ port,
1615
+ });
1616
+ }
1617
+ }
1618
+ catch {
1619
+ // Skip if can't read
1620
+ }
1621
+ }
1622
+ // 2. Check docker-compose for MongoDB services and construct URLs
1623
+ if (detected.infrastructure) {
1624
+ for (const infra of detected.infrastructure) {
1625
+ if (infra.type !== 'database')
1626
+ continue;
1627
+ const image = infra.image?.toLowerCase() || '';
1628
+ if (!image.includes('mongo'))
1629
+ continue;
1630
+ const port = infra.port || 27017;
1631
+ const url = `mongodb://localhost:${port}`;
1632
+ if (!seenUrls.has(url)) {
1633
+ seenUrls.add(url);
1634
+ results.push({
1635
+ url,
1636
+ source: `docker-compose (${infra.name})`,
1637
+ port,
1638
+ });
1639
+ }
1640
+ }
1641
+ }
1642
+ return results;
1643
+ }
1644
+ /**
1645
+ * Build a suggested MongoDB URL from detected info
1646
+ */
1647
+ function buildMongoDbUrl(detected, preferredDatabase) {
1648
+ if (detected.length === 0)
1649
+ return undefined;
1650
+ // Prefer URLs that have a database name
1651
+ const withDb = detected.filter(d => d.database);
1652
+ if (withDb.length > 0) {
1653
+ // Prefer the one matching preferredDatabase if specified
1654
+ if (preferredDatabase) {
1655
+ const match = withDb.find(d => d.database === preferredDatabase);
1656
+ if (match)
1657
+ return match.url;
1658
+ }
1659
+ return withDb[0].url;
1660
+ }
1661
+ // Fall back to first detected URL
1662
+ return detected[0].url;
1663
+ }
1514
1664
  function httpsToSsh(url) {
1515
1665
  const match = url.match(/https:\/\/([^/]+)\/(.+)/);
1516
1666
  if (match)
@@ -608,16 +608,22 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
608
608
  const snapshotAge = Date.now() - new Date(existingSnapshot.createdAt).getTime();
609
609
  const hoursAgo = Math.floor(snapshotAge / (1000 * 60 * 60));
610
610
  const timeAgoStr = hoursAgo < 1 ? 'less than an hour ago' : `${hoursAgo} hours ago`;
611
+ // Warn if snapshot is suspiciously small (< 50KB likely means empty database)
612
+ const isSnapshotTooSmall = existingSnapshot.sizeBytes < 50 * 1024;
613
+ if (isSnapshotTooSmall) {
614
+ console.log(chalk_1.default.yellow(` ⚠ Warning: Existing snapshot is very small (${(0, db_utils_1.formatBytes)(existingSnapshot.sizeBytes)})`));
615
+ console.log(chalk_1.default.yellow(` This likely means the source database was empty when snapshot was created.`));
616
+ }
611
617
  if (!options.yes) {
612
618
  const snapshotChoice = await prompts.select({
613
619
  message: `Found existing ${snapshotSource} snapshot (${timeAgoStr}):`,
614
620
  choices: [
615
621
  {
616
- name: `Use existing snapshot (${timeAgoStr}, ${(0, db_utils_1.formatBytes)(existingSnapshot.sizeBytes)})`,
622
+ name: `Use existing snapshot (${timeAgoStr}, ${(0, db_utils_1.formatBytes)(existingSnapshot.sizeBytes)})${isSnapshotTooSmall ? chalk_1.default.yellow(' ⚠ small') : ''}`,
617
623
  value: 'existing',
618
624
  },
619
625
  {
620
- name: 'Create fresh snapshot (dump now)',
626
+ name: `Create fresh snapshot (dump now)${isSnapshotTooSmall ? chalk_1.default.green(' ← recommended') : ''}`,
621
627
  value: 'fresh',
622
628
  },
623
629
  ],
@@ -146,6 +146,7 @@ function displayTimingBreakdown(timing) {
146
146
  }
147
147
  }
148
148
  // Watch mode: tail cloud-init logs in realtime
149
+ // Returns an object with the child process and a promise that resolves when setup completes
149
150
  function tailCloudInitLogs(ip, keyPath) {
150
151
  const sshOpts = [
151
152
  '-i', keyPath,
@@ -160,10 +161,44 @@ function tailCloudInitLogs(ip, keyPath) {
160
161
  const child = (0, child_process_1.spawn)('ssh', sshOpts, {
161
162
  stdio: ['pipe', 'pipe', 'pipe'],
162
163
  });
164
+ let setupCompleted = false;
165
+ let resolveCompletion;
166
+ const completionPromise = new Promise((resolve) => {
167
+ resolveCompletion = resolve;
168
+ });
163
169
  child.stdout?.on('data', (data) => {
164
170
  const lines = data.toString().split('\n');
165
171
  for (const line of lines) {
166
172
  if (line.trim()) {
173
+ // Detect setup completion - stop tailing after seeing this
174
+ if (line.includes('Setup Complete in') || line.includes('Setup Complete!')) {
175
+ setupCompleted = true;
176
+ console.log(chalk_1.default.green(line));
177
+ // Give a moment for any final output, then stop
178
+ setTimeout(() => {
179
+ console.log('');
180
+ console.log(chalk_1.default.dim('─'.repeat(60)));
181
+ child.kill();
182
+ resolveCompletion();
183
+ }, 2000);
184
+ continue;
185
+ }
186
+ // Skip post-setup cleanup noise (package removal, cloud-init self-removal)
187
+ if (setupCompleted) {
188
+ // Still show important messages during cleanup
189
+ if (line.includes('=== ') || line.includes('error') || line.includes('Error')) {
190
+ console.log(chalk_1.default.dim(line));
191
+ }
192
+ continue;
193
+ }
194
+ // Skip noisy/unimportant lines during setup
195
+ if (line.includes('Reading database ...') ||
196
+ line.includes('(Reading database ...') ||
197
+ line.includes('Processing triggers') ||
198
+ line.includes('Scanning processes...') ||
199
+ line.includes('Scanning linux images...')) {
200
+ continue;
201
+ }
167
202
  // Color-code certain log lines
168
203
  if (line.startsWith('===') || line.includes('===')) {
169
204
  console.log(chalk_1.default.cyan(line));
@@ -183,7 +218,11 @@ function tailCloudInitLogs(ip, keyPath) {
183
218
  child.stderr?.on('data', (data) => {
184
219
  // Ignore SSH stderr (connection warnings, etc.)
185
220
  });
186
- return child;
221
+ // Also resolve when child process exits
222
+ child.on('close', () => {
223
+ resolveCompletion();
224
+ });
225
+ return { child, completionPromise };
187
226
  }
188
227
  // Sleep helper for async code
189
228
  function sleep(ms) {
@@ -268,9 +307,13 @@ exports.statusCommand = new commander_1.Command('status')
268
307
  console.log(` Current hour ends in: ${chalk_1.default.cyan(minutesUntilBilling + ' min')}`);
269
308
  console.log(` Last activity: ${minutesInactive < 1 ? 'just now' : minutesInactive + ' min ago'}`);
270
309
  console.log(` Total: ${billing.totalHoursUsed} hour${billing.totalHoursUsed !== 1 ? 's' : ''}, ${billing.totalCreditsUsed} credit${billing.totalCreditsUsed !== 1 ? 's' : ''}`);
271
- if (billing.autoDestroyOnInactivity) {
272
- const minutesUntilDestroy = Math.max(0, 60 - minutesInactive);
273
- if (minutesInactive >= 45) {
310
+ if (billing.autoDestroyOnInactivity && billing.currentHourEnd) {
311
+ // Auto-destroy happens at 58 min mark into billing hour (if inactive for 5+ min and email was sent)
312
+ const currentHourEnd = new Date(billing.currentHourEnd);
313
+ const currentHourStart = new Date(currentHourEnd.getTime() - 60 * 60 * 1000);
314
+ const minutesIntoBillingHour = Math.floor((now.getTime() - currentHourStart.getTime()) / (60 * 1000));
315
+ const minutesUntilDestroy = Math.max(0, 60 - minutesIntoBillingHour);
316
+ if (minutesInactive >= 5) {
274
317
  console.log(chalk_1.default.yellow(` Auto-destroy in: ${minutesUntilDestroy} min (inactive)`));
275
318
  }
276
319
  }
@@ -314,10 +357,17 @@ exports.statusCommand = new commander_1.Command('status')
314
357
  process.on('SIGTERM', cleanup);
315
358
  // Wait for SSH to become available
316
359
  let sshAvailable = false;
360
+ const startTime = Date.now();
361
+ let lastMessageTime = 0;
317
362
  while (!sshAvailable && !stopWatching) {
363
+ const elapsedSecs = Math.floor((Date.now() - startTime) / 1000);
318
364
  const status = sshExec(target.ipAddress, keyPath, 'cloud-init status 2>&1');
319
365
  if (!status) {
320
- console.log(chalk_1.default.yellow('[INFO] Waiting for server to boot... (Ctrl+C to exit)'));
366
+ // Only print message every 5 seconds to avoid spam, but show progress
367
+ if (Date.now() - lastMessageTime >= 5000) {
368
+ console.log(chalk_1.default.yellow(`[INFO] Waiting for server to boot... (${elapsedSecs}s, Ctrl+C to exit)`));
369
+ lastMessageTime = Date.now();
370
+ }
321
371
  await sleep(5000);
322
372
  continue;
323
373
  }
@@ -330,7 +380,11 @@ exports.statusCommand = new commander_1.Command('status')
330
380
  'ssh: connect to host',
331
381
  ];
332
382
  if (sshErrors.some(err => status.includes(err))) {
333
- console.log(chalk_1.default.yellow('[INFO] Server is still initializing - SSH is not yet available... (Ctrl+C to exit)'));
383
+ // Only print message every 5 seconds to avoid spam, but show progress
384
+ if (Date.now() - lastMessageTime >= 5000) {
385
+ console.log(chalk_1.default.yellow(`[INFO] Waiting for SSH to become available... (${elapsedSecs}s, Ctrl+C to exit)`));
386
+ lastMessageTime = Date.now();
387
+ }
334
388
  await sleep(5000);
335
389
  continue;
336
390
  }
@@ -357,7 +411,7 @@ exports.statusCommand = new commander_1.Command('status')
357
411
  }
358
412
  console.log(chalk_1.default.blue('[INFO] Tailing setup logs in realtime (Ctrl+C to exit):'));
359
413
  console.log(chalk_1.default.dim('─'.repeat(60)));
360
- const tailProcess = tailCloudInitLogs(target.ipAddress, keyPath);
414
+ const { child: tailProcess, completionPromise } = tailCloudInitLogs(target.ipAddress, keyPath);
361
415
  // Update cleanup to kill tail process
362
416
  process.removeListener('SIGINT', cleanup);
363
417
  process.removeListener('SIGTERM', cleanup);
@@ -369,22 +423,15 @@ exports.statusCommand = new commander_1.Command('status')
369
423
  };
370
424
  process.on('SIGINT', cleanupWithTail);
371
425
  process.on('SIGTERM', cleanupWithTail);
372
- // Wait for tail process to end
373
- await new Promise((resolve) => {
374
- tailProcess.on('close', () => resolve());
375
- tailProcess.on('error', () => resolve());
376
- });
426
+ // Wait for setup completion (detected by "Setup Complete" message) or process exit
427
+ await completionPromise;
377
428
  // Clean up handlers
378
429
  process.removeListener('SIGINT', cleanupWithTail);
379
430
  process.removeListener('SIGTERM', cleanupWithTail);
380
431
  if (!stopWatching) {
381
- console.log('');
382
- console.log(chalk_1.default.blue('[INFO] Log stream ended. Checking final status...'));
383
- console.log('');
384
- await sleep(2000);
385
432
  // Check final status
386
433
  const finalStatus = sshExec(target.ipAddress, keyPath, 'cloud-init status 2>&1');
387
- if (finalStatus.includes('done')) {
434
+ if (finalStatus.includes('done') || finalStatus.includes('not found')) {
388
435
  console.log(chalk_1.default.green('[SUCCESS] Setup completed!'));
389
436
  const finalTiming = getTimingBreakdown(target.ipAddress, keyPath);
390
437
  displayTimingBreakdown(finalTiming);
@@ -393,6 +440,9 @@ exports.statusCommand = new commander_1.Command('status')
393
440
  console.log(chalk_1.default.red('[ERROR] Setup encountered an error!'));
394
441
  console.log(chalk_1.default.dim(' Run `gb connect` to investigate.'));
395
442
  }
443
+ else if (finalStatus.includes('running')) {
444
+ console.log(chalk_1.default.yellow('[INFO] Setup still in progress...'));
445
+ }
396
446
  else {
397
447
  console.log(chalk_1.default.yellow(`[INFO] Final status: ${finalStatus}`));
398
448
  }
@@ -45,8 +45,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.FrameworkDetector = void 0;
46
46
  const fs = __importStar(require("fs"));
47
47
  const path = __importStar(require("path"));
48
+ // IMPORTANT: For Node.js frameworks, devCommand should be 'dev' (the script name from package.json)
49
+ // NOT 'next dev' or 'vite dev' (raw binary commands). The cloud-init service will run these
50
+ // with 'pnpm run <command>' which properly resolves node_modules/.bin binaries.
51
+ // Only use raw binary commands for non-Node.js frameworks or when there's no package.json script.
48
52
  const FRAMEWORK_SIGNATURES = {
49
53
  // Node.js - Fullstack/Frontend
54
+ // For Node.js: devCommand is the npm script name (e.g., 'dev'), not the binary (e.g., 'next dev')
50
55
  nextjs: {
51
56
  language: 'node',
52
57
  detection: [
@@ -58,9 +63,9 @@ const FRAMEWORK_SIGNATURES = {
58
63
  defaults: {
59
64
  type: 'fullstack',
60
65
  defaultPort: 3000,
61
- devCommand: 'next dev',
62
- buildCommand: 'next build',
63
- startCommand: 'next start',
66
+ devCommand: 'dev', // Uses 'pnpm run dev' which runs package.json scripts.dev
67
+ buildCommand: 'build',
68
+ startCommand: 'start',
64
69
  outputDir: '.next',
65
70
  },
66
71
  },
@@ -74,9 +79,9 @@ const FRAMEWORK_SIGNATURES = {
74
79
  defaults: {
75
80
  type: 'fullstack',
76
81
  defaultPort: 3000,
77
- devCommand: 'nuxt dev',
78
- buildCommand: 'nuxt build',
79
- startCommand: 'nuxt start',
82
+ devCommand: 'dev',
83
+ buildCommand: 'build',
84
+ startCommand: 'start',
80
85
  outputDir: '.output',
81
86
  },
82
87
  },
@@ -89,9 +94,9 @@ const FRAMEWORK_SIGNATURES = {
89
94
  defaults: {
90
95
  type: 'fullstack',
91
96
  defaultPort: 3000,
92
- devCommand: 'remix dev',
93
- buildCommand: 'remix build',
94
- startCommand: 'remix-serve build',
97
+ devCommand: 'dev',
98
+ buildCommand: 'build',
99
+ startCommand: 'start',
95
100
  outputDir: 'build',
96
101
  },
97
102
  },
@@ -105,8 +110,8 @@ const FRAMEWORK_SIGNATURES = {
105
110
  defaults: {
106
111
  type: 'frontend',
107
112
  defaultPort: 4321,
108
- devCommand: 'astro dev',
109
- buildCommand: 'astro build',
113
+ devCommand: 'dev',
114
+ buildCommand: 'build',
110
115
  outputDir: 'dist',
111
116
  },
112
117
  },
@@ -120,8 +125,8 @@ const FRAMEWORK_SIGNATURES = {
120
125
  defaults: {
121
126
  type: 'frontend',
122
127
  defaultPort: 8000,
123
- devCommand: 'gatsby develop',
124
- buildCommand: 'gatsby build',
128
+ devCommand: 'dev', // Gatsby uses 'develop' but most setups alias to 'dev'
129
+ buildCommand: 'build',
125
130
  outputDir: 'public',
126
131
  },
127
132
  },
@@ -134,8 +139,8 @@ const FRAMEWORK_SIGNATURES = {
134
139
  defaults: {
135
140
  type: 'fullstack',
136
141
  defaultPort: 5173,
137
- devCommand: 'vite dev',
138
- buildCommand: 'vite build',
142
+ devCommand: 'dev',
143
+ buildCommand: 'build',
139
144
  outputDir: 'build',
140
145
  },
141
146
  },
@@ -148,8 +153,8 @@ const FRAMEWORK_SIGNATURES = {
148
153
  defaults: {
149
154
  type: 'frontend',
150
155
  defaultPort: 3000,
151
- devCommand: 'vite dev',
152
- buildCommand: 'vite build',
156
+ devCommand: 'dev',
157
+ buildCommand: 'build',
153
158
  outputDir: 'dist',
154
159
  },
155
160
  },
@@ -161,8 +166,8 @@ const FRAMEWORK_SIGNATURES = {
161
166
  defaults: {
162
167
  type: 'frontend',
163
168
  defaultPort: 5173,
164
- devCommand: 'vite dev',
165
- buildCommand: 'vite build',
169
+ devCommand: 'dev',
170
+ buildCommand: 'build',
166
171
  outputDir: 'dist',
167
172
  },
168
173
  },
@@ -175,8 +180,8 @@ const FRAMEWORK_SIGNATURES = {
175
180
  defaults: {
176
181
  type: 'frontend',
177
182
  defaultPort: 4200,
178
- devCommand: 'ng serve',
179
- buildCommand: 'ng build',
183
+ devCommand: 'start', // Angular CLI uses 'start' for dev server by convention
184
+ buildCommand: 'build',
180
185
  outputDir: 'dist',
181
186
  },
182
187
  },
@@ -188,8 +193,8 @@ const FRAMEWORK_SIGNATURES = {
188
193
  defaults: {
189
194
  type: 'frontend',
190
195
  defaultPort: 5173,
191
- devCommand: 'vite dev',
192
- buildCommand: 'vite build',
196
+ devCommand: 'dev',
197
+ buildCommand: 'build',
193
198
  outputDir: 'dist',
194
199
  },
195
200
  },
@@ -201,8 +206,8 @@ const FRAMEWORK_SIGNATURES = {
201
206
  defaults: {
202
207
  type: 'frontend',
203
208
  defaultPort: 3000,
204
- devCommand: 'vite dev',
205
- buildCommand: 'vite build',
209
+ devCommand: 'dev',
210
+ buildCommand: 'build',
206
211
  outputDir: 'dist',
207
212
  },
208
213
  },
@@ -216,8 +221,8 @@ const FRAMEWORK_SIGNATURES = {
216
221
  defaults: {
217
222
  type: 'frontend',
218
223
  defaultPort: 5173,
219
- devCommand: 'vite',
220
- buildCommand: 'vite build',
224
+ devCommand: 'dev',
225
+ buildCommand: 'build',
221
226
  outputDir: 'dist',
222
227
  },
223
228
  },
@@ -231,9 +236,9 @@ const FRAMEWORK_SIGNATURES = {
231
236
  defaults: {
232
237
  type: 'backend',
233
238
  defaultPort: 3000,
234
- devCommand: 'nest start --watch',
235
- buildCommand: 'nest build',
236
- startCommand: 'node dist/main',
239
+ devCommand: 'start:dev', // NestJS convention
240
+ buildCommand: 'build',
241
+ startCommand: 'start:prod',
237
242
  outputDir: 'dist',
238
243
  },
239
244
  },
@@ -245,8 +250,8 @@ const FRAMEWORK_SIGNATURES = {
245
250
  defaults: {
246
251
  type: 'backend',
247
252
  defaultPort: 3000,
248
- devCommand: 'node --watch src/index.js',
249
- startCommand: 'node src/index.js',
253
+ devCommand: 'dev',
254
+ startCommand: 'start',
250
255
  },
251
256
  },
252
257
  fastify: {
@@ -257,8 +262,8 @@ const FRAMEWORK_SIGNATURES = {
257
262
  defaults: {
258
263
  type: 'backend',
259
264
  defaultPort: 3000,
260
- devCommand: 'node --watch src/index.js',
261
- startCommand: 'node src/index.js',
265
+ devCommand: 'dev',
266
+ startCommand: 'start',
262
267
  },
263
268
  },
264
269
  koa: {
@@ -269,8 +274,8 @@ const FRAMEWORK_SIGNATURES = {
269
274
  defaults: {
270
275
  type: 'backend',
271
276
  defaultPort: 3000,
272
- devCommand: 'node --watch src/index.js',
273
- startCommand: 'node src/index.js',
277
+ devCommand: 'dev',
278
+ startCommand: 'start',
274
279
  },
275
280
  },
276
281
  hono: {
@@ -281,8 +286,8 @@ const FRAMEWORK_SIGNATURES = {
281
286
  defaults: {
282
287
  type: 'backend',
283
288
  defaultPort: 3000,
284
- devCommand: 'node --watch src/index.ts',
285
- startCommand: 'node dist/index.js',
289
+ devCommand: 'dev',
290
+ startCommand: 'start',
286
291
  },
287
292
  },
288
293
  hapi: {
@@ -293,8 +298,8 @@ const FRAMEWORK_SIGNATURES = {
293
298
  defaults: {
294
299
  type: 'backend',
295
300
  defaultPort: 3000,
296
- devCommand: 'node --watch src/index.js',
297
- startCommand: 'node src/index.js',
301
+ devCommand: 'dev',
302
+ startCommand: 'start',
298
303
  },
299
304
  },
300
305
  // Python
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.71",
3
+ "version": "1.0.72",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {