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.
- package/dist/commands/create.js +12 -6
- package/dist/commands/db-sync.js +8 -2
- package/dist/commands/init.js +152 -2
- package/dist/commands/rebuild.js +8 -2
- package/dist/commands/status.js +67 -17
- package/dist/scanner/framework-detector.js +45 -40
- package/package.json +1 -1
package/dist/commands/create.js
CHANGED
|
@@ -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:
|
|
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) {
|
package/dist/commands/db-sync.js
CHANGED
|
@@ -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:
|
|
148
|
+
name: `Create fresh snapshot (dump now)${isSnapshotTooSmall ? chalk_1.default.green(' ← recommended') : ''}`,
|
|
143
149
|
value: 'fresh',
|
|
144
150
|
},
|
|
145
151
|
],
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|
package/dist/commands/rebuild.js
CHANGED
|
@@ -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:
|
|
626
|
+
name: `Create fresh snapshot (dump now)${isSnapshotTooSmall ? chalk_1.default.green(' ← recommended') : ''}`,
|
|
621
627
|
value: 'fresh',
|
|
622
628
|
},
|
|
623
629
|
],
|
package/dist/commands/status.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
373
|
-
await
|
|
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: '
|
|
62
|
-
buildCommand: '
|
|
63
|
-
startCommand: '
|
|
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: '
|
|
78
|
-
buildCommand: '
|
|
79
|
-
startCommand: '
|
|
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: '
|
|
93
|
-
buildCommand: '
|
|
94
|
-
startCommand: '
|
|
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: '
|
|
109
|
-
buildCommand: '
|
|
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: '
|
|
124
|
-
buildCommand: '
|
|
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: '
|
|
138
|
-
buildCommand: '
|
|
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: '
|
|
152
|
-
buildCommand: '
|
|
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: '
|
|
165
|
-
buildCommand: '
|
|
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: '
|
|
179
|
-
buildCommand: '
|
|
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: '
|
|
192
|
-
buildCommand: '
|
|
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: '
|
|
205
|
-
buildCommand: '
|
|
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: '
|
|
220
|
-
buildCommand: '
|
|
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: '
|
|
235
|
-
buildCommand: '
|
|
236
|
-
startCommand: '
|
|
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: '
|
|
249
|
-
startCommand: '
|
|
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: '
|
|
261
|
-
startCommand: '
|
|
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: '
|
|
273
|
-
startCommand: '
|
|
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: '
|
|
285
|
-
startCommand: '
|
|
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: '
|
|
297
|
-
startCommand: '
|
|
301
|
+
devCommand: 'dev',
|
|
302
|
+
startCommand: 'start',
|
|
298
303
|
},
|
|
299
304
|
},
|
|
300
305
|
// Python
|