genbox 1.0.30 → 1.0.32
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/init.js +87 -12
- package/dist/commands/status.js +84 -29
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -621,10 +621,12 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
621
621
|
// Environment configuration - do this BEFORE profiles so profiles can reference environments
|
|
622
622
|
// (skip only in non-interactive mode, always show for --from-scan since environments are required)
|
|
623
623
|
if (!nonInteractive) {
|
|
624
|
-
const
|
|
625
|
-
if (
|
|
626
|
-
v4Config.environments =
|
|
624
|
+
const envResult = await setupEnvironments(scan, v4Config, isMultiRepoStructure, existingEnvValues);
|
|
625
|
+
if (envResult.environments) {
|
|
626
|
+
v4Config.environments = envResult.environments;
|
|
627
627
|
}
|
|
628
|
+
// Add collected database URLs to envVarsToAdd (for .env.genbox)
|
|
629
|
+
Object.assign(envVarsToAdd, envResult.databaseUrls);
|
|
628
630
|
}
|
|
629
631
|
// Ask about profiles (skip prompt when using --from-scan)
|
|
630
632
|
let createProfiles = true;
|
|
@@ -1170,6 +1172,7 @@ async function setupGitAuth(gitInfo, projectName) {
|
|
|
1170
1172
|
* Setup staging/production environments (v4 format)
|
|
1171
1173
|
*/
|
|
1172
1174
|
async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvValues = {}) {
|
|
1175
|
+
const databaseUrls = {};
|
|
1173
1176
|
// First ask which environments they want to configure
|
|
1174
1177
|
const envChoice = await prompts.select({
|
|
1175
1178
|
message: 'Which environments do you want to configure?',
|
|
@@ -1182,16 +1185,18 @@ async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvV
|
|
|
1182
1185
|
default: 'staging',
|
|
1183
1186
|
});
|
|
1184
1187
|
if (envChoice === 'skip') {
|
|
1185
|
-
return undefined;
|
|
1188
|
+
return { environments: undefined, databaseUrls };
|
|
1186
1189
|
}
|
|
1187
1190
|
console.log('');
|
|
1188
1191
|
console.log(chalk_1.default.blue('=== Environment Setup ==='));
|
|
1189
1192
|
console.log(chalk_1.default.dim('These URLs will be used when connecting to external services.'));
|
|
1190
|
-
console.log(chalk_1.default.dim('
|
|
1193
|
+
console.log(chalk_1.default.dim('Database URLs are stored in .env.genbox for database copy operations.'));
|
|
1191
1194
|
const environments = {};
|
|
1192
1195
|
// Get existing URLs if available
|
|
1193
1196
|
const existingStagingApiUrl = existingEnvValues['STAGING_API_URL'];
|
|
1194
1197
|
const existingProductionApiUrl = existingEnvValues['PRODUCTION_API_URL'] || existingEnvValues['PROD_API_URL'];
|
|
1198
|
+
const existingStagingMongoUrl = existingEnvValues['STAGING_MONGODB_URL'];
|
|
1199
|
+
const existingProdMongoUrl = existingEnvValues['PROD_MONGODB_URL'] || existingEnvValues['PRODUCTION_MONGODB_URL'];
|
|
1195
1200
|
const configureStaging = envChoice === 'staging' || envChoice === 'both';
|
|
1196
1201
|
const configureProduction = envChoice === 'production' || envChoice === 'both';
|
|
1197
1202
|
// Configure staging if selected
|
|
@@ -1250,6 +1255,36 @@ async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvV
|
|
|
1250
1255
|
};
|
|
1251
1256
|
}
|
|
1252
1257
|
}
|
|
1258
|
+
// Prompt for staging database URL (for database copy operations)
|
|
1259
|
+
console.log('');
|
|
1260
|
+
console.log(chalk_1.default.dim(' Database URL (for "Copy from staging" database mode):'));
|
|
1261
|
+
console.log(chalk_1.default.dim(' Format: mongodb+srv://user:password@staging.mongodb.net/dbname'));
|
|
1262
|
+
if (existingStagingMongoUrl) {
|
|
1263
|
+
console.log(chalk_1.default.dim(` Found existing value: ${existingStagingMongoUrl.substring(0, 50)}...`));
|
|
1264
|
+
const useExisting = await prompts.confirm({
|
|
1265
|
+
message: ' Use existing staging MongoDB URL?',
|
|
1266
|
+
default: true,
|
|
1267
|
+
});
|
|
1268
|
+
if (useExisting) {
|
|
1269
|
+
databaseUrls['STAGING_MONGODB_URL'] = existingStagingMongoUrl;
|
|
1270
|
+
}
|
|
1271
|
+
else {
|
|
1272
|
+
const mongoUrl = await prompts.input({
|
|
1273
|
+
message: ' Staging MongoDB URL:',
|
|
1274
|
+
});
|
|
1275
|
+
if (mongoUrl) {
|
|
1276
|
+
databaseUrls['STAGING_MONGODB_URL'] = mongoUrl;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
else {
|
|
1281
|
+
const mongoUrl = await prompts.input({
|
|
1282
|
+
message: ' Staging MongoDB URL (optional, press Enter to skip):',
|
|
1283
|
+
});
|
|
1284
|
+
if (mongoUrl) {
|
|
1285
|
+
databaseUrls['STAGING_MONGODB_URL'] = mongoUrl;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1253
1288
|
}
|
|
1254
1289
|
// Configure production if selected
|
|
1255
1290
|
if (configureProduction) {
|
|
@@ -1320,8 +1355,41 @@ async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvV
|
|
|
1320
1355
|
};
|
|
1321
1356
|
}
|
|
1322
1357
|
}
|
|
1358
|
+
// Prompt for production database URL (for database copy operations)
|
|
1359
|
+
console.log('');
|
|
1360
|
+
console.log(chalk_1.default.dim(' Database URL (for "Copy from production" database mode):'));
|
|
1361
|
+
console.log(chalk_1.default.dim(' Format: mongodb+srv://readonly:password@prod.mongodb.net/dbname'));
|
|
1362
|
+
if (existingProdMongoUrl) {
|
|
1363
|
+
console.log(chalk_1.default.dim(` Found existing value: ${existingProdMongoUrl.substring(0, 50)}...`));
|
|
1364
|
+
const useExisting = await prompts.confirm({
|
|
1365
|
+
message: ' Use existing production MongoDB URL?',
|
|
1366
|
+
default: true,
|
|
1367
|
+
});
|
|
1368
|
+
if (useExisting) {
|
|
1369
|
+
databaseUrls['PROD_MONGODB_URL'] = existingProdMongoUrl;
|
|
1370
|
+
}
|
|
1371
|
+
else {
|
|
1372
|
+
const mongoUrl = await prompts.input({
|
|
1373
|
+
message: ' Production MongoDB URL:',
|
|
1374
|
+
});
|
|
1375
|
+
if (mongoUrl) {
|
|
1376
|
+
databaseUrls['PROD_MONGODB_URL'] = mongoUrl;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
else {
|
|
1381
|
+
const mongoUrl = await prompts.input({
|
|
1382
|
+
message: ' Production MongoDB URL (optional, press Enter to skip):',
|
|
1383
|
+
});
|
|
1384
|
+
if (mongoUrl) {
|
|
1385
|
+
databaseUrls['PROD_MONGODB_URL'] = mongoUrl;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1323
1388
|
}
|
|
1324
|
-
return
|
|
1389
|
+
return {
|
|
1390
|
+
environments: Object.keys(environments).length > 0 ? environments : undefined,
|
|
1391
|
+
databaseUrls,
|
|
1392
|
+
};
|
|
1325
1393
|
}
|
|
1326
1394
|
/**
|
|
1327
1395
|
* Prompt for a single API URL with existing value support
|
|
@@ -1378,12 +1446,19 @@ async function setupEnvFile(projectName, config, nonInteractive = false, scan, i
|
|
|
1378
1446
|
if (!extraEnvVars['GIT_TOKEN']) {
|
|
1379
1447
|
segregatedContent += `# GIT_TOKEN=ghp_xxxxxxxxxxxx\n`;
|
|
1380
1448
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1449
|
+
// Add database URL section (only show templates for missing URLs)
|
|
1450
|
+
const hasStagingMongo = !!extraEnvVars['STAGING_MONGODB_URL'];
|
|
1451
|
+
const hasProdMongo = !!extraEnvVars['PROD_MONGODB_URL'];
|
|
1452
|
+
if (!hasStagingMongo || !hasProdMongo) {
|
|
1453
|
+
segregatedContent += `\n# Database URLs (used by profiles with database mode)\n`;
|
|
1454
|
+
if (!hasStagingMongo) {
|
|
1455
|
+
segregatedContent += `# STAGING_MONGODB_URL=mongodb+srv://user:password@staging.mongodb.net\n`;
|
|
1456
|
+
}
|
|
1457
|
+
if (!hasProdMongo) {
|
|
1458
|
+
segregatedContent += `# PROD_MONGODB_URL=mongodb+srv://readonly:password@prod.mongodb.net\n`;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
segregatedContent += '\n';
|
|
1387
1462
|
// For multi-repo: find env files in app directories
|
|
1388
1463
|
if (isMultiRepo && scan) {
|
|
1389
1464
|
const appEnvFiles = findAppEnvFiles(scan.apps, process.cwd());
|
package/dist/commands/status.js
CHANGED
|
@@ -79,6 +79,72 @@ function sshExec(ip, keyPath, command, timeoutSecs = 10) {
|
|
|
79
79
|
return '';
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
+
function formatDuration(secs) {
|
|
83
|
+
const mins = Math.floor(secs / 60);
|
|
84
|
+
const remainingSecs = secs % 60;
|
|
85
|
+
return `${mins}m ${remainingSecs}s`;
|
|
86
|
+
}
|
|
87
|
+
function getTimingBreakdown(ip, keyPath) {
|
|
88
|
+
const timing = {
|
|
89
|
+
sshReady: null,
|
|
90
|
+
cloudInit: null,
|
|
91
|
+
total: null,
|
|
92
|
+
};
|
|
93
|
+
// Get total system uptime
|
|
94
|
+
const uptime = sshExec(ip, keyPath, "cat /proc/uptime 2>/dev/null | cut -d' ' -f1 | cut -d'.' -f1");
|
|
95
|
+
if (uptime) {
|
|
96
|
+
timing.total = parseInt(uptime, 10);
|
|
97
|
+
}
|
|
98
|
+
// Get SSH ready time (sshd service start time relative to boot)
|
|
99
|
+
// ActiveEnterTimestampMonotonic gives microseconds since boot when service became active
|
|
100
|
+
const sshdTimestamp = sshExec(ip, keyPath, "systemctl show sshd --property=ActiveEnterTimestampMonotonic 2>/dev/null | cut -d'=' -f2");
|
|
101
|
+
if (sshdTimestamp && sshdTimestamp.trim()) {
|
|
102
|
+
const microseconds = parseInt(sshdTimestamp.trim(), 10);
|
|
103
|
+
if (!isNaN(microseconds) && microseconds > 0) {
|
|
104
|
+
timing.sshReady = Math.round(microseconds / 1000000); // Convert to seconds
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Get cloud-init execution time from logs
|
|
108
|
+
const cloudInitTime = sshExec(ip, keyPath, "sudo grep -oP 'Setup Complete in \\K[0-9]+m [0-9]+s' /var/log/cloud-init-output.log 2>/dev/null | tail -1");
|
|
109
|
+
if (cloudInitTime && cloudInitTime.trim()) {
|
|
110
|
+
// Parse "Xm Ys" format
|
|
111
|
+
const match = cloudInitTime.trim().match(/(\d+)m\s*(\d+)s/);
|
|
112
|
+
if (match) {
|
|
113
|
+
timing.cloudInit = parseInt(match[1], 10) * 60 + parseInt(match[2], 10);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Fallback: calculate cloud-init time from result.json if not in logs
|
|
117
|
+
if (timing.cloudInit === null && timing.sshReady !== null && timing.total !== null) {
|
|
118
|
+
// Get cloud-init finish timestamp
|
|
119
|
+
const resultTimestamp = sshExec(ip, keyPath, "stat -c %Y /run/cloud-init/result.json 2>/dev/null");
|
|
120
|
+
const bootTimestamp = sshExec(ip, keyPath, "date -d \"$(uptime -s)\" +%s 2>/dev/null");
|
|
121
|
+
if (resultTimestamp && bootTimestamp) {
|
|
122
|
+
const resultTime = parseInt(resultTimestamp.trim(), 10);
|
|
123
|
+
const bootTime = parseInt(bootTimestamp.trim(), 10);
|
|
124
|
+
if (!isNaN(resultTime) && !isNaN(bootTime) && resultTime > bootTime) {
|
|
125
|
+
// Cloud-init time = (finish time - boot time) - ssh ready time
|
|
126
|
+
const totalFromBoot = resultTime - bootTime;
|
|
127
|
+
timing.cloudInit = Math.max(0, totalFromBoot - timing.sshReady);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return timing;
|
|
132
|
+
}
|
|
133
|
+
function displayTimingBreakdown(timing) {
|
|
134
|
+
if (timing.sshReady !== null || timing.cloudInit !== null || timing.total !== null) {
|
|
135
|
+
console.log(chalk_1.default.blue('[INFO] === Timing Breakdown ==='));
|
|
136
|
+
if (timing.sshReady !== null) {
|
|
137
|
+
console.log(` SSH Ready: ${formatDuration(timing.sshReady)}`);
|
|
138
|
+
}
|
|
139
|
+
if (timing.cloudInit !== null) {
|
|
140
|
+
console.log(` Cloud-init: ${formatDuration(timing.cloudInit)}`);
|
|
141
|
+
}
|
|
142
|
+
if (timing.total !== null) {
|
|
143
|
+
console.log(` ${chalk_1.default.bold('Total: ' + formatDuration(timing.total))}`);
|
|
144
|
+
}
|
|
145
|
+
console.log('');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
82
148
|
exports.statusCommand = new commander_1.Command('status')
|
|
83
149
|
.description('Check cloud-init progress and service status of a Genbox')
|
|
84
150
|
.argument('[name]', 'Name of the Genbox (optional - will prompt if not provided)')
|
|
@@ -145,18 +211,24 @@ exports.statusCommand = new commander_1.Command('status')
|
|
|
145
211
|
return;
|
|
146
212
|
}
|
|
147
213
|
if (status.includes('running')) {
|
|
148
|
-
// Get
|
|
149
|
-
const
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
const mins = Math.floor(uptimeSecs / 60);
|
|
153
|
-
const secs = uptimeSecs % 60;
|
|
154
|
-
console.log(chalk_1.default.yellow(`[WARN] Cloud-init is still running... (elapsed: ${mins}m ${secs}s)`));
|
|
214
|
+
// Get timing breakdown
|
|
215
|
+
const timing = getTimingBreakdown(target.ipAddress, keyPath);
|
|
216
|
+
if (timing.total !== null) {
|
|
217
|
+
console.log(chalk_1.default.yellow(`[WARN] Cloud-init is still running... (elapsed: ${formatDuration(timing.total)})`));
|
|
155
218
|
}
|
|
156
219
|
else {
|
|
157
220
|
console.log(chalk_1.default.yellow('[WARN] Cloud-init is still running...'));
|
|
158
221
|
}
|
|
159
222
|
console.log('');
|
|
223
|
+
// Show timing breakdown so far
|
|
224
|
+
if (timing.sshReady !== null) {
|
|
225
|
+
console.log(chalk_1.default.blue('[INFO] === Timing So Far ==='));
|
|
226
|
+
console.log(` SSH Ready: ${formatDuration(timing.sshReady)}`);
|
|
227
|
+
if (timing.total !== null) {
|
|
228
|
+
console.log(` Elapsed: ${formatDuration(timing.total)}`);
|
|
229
|
+
}
|
|
230
|
+
console.log('');
|
|
231
|
+
}
|
|
160
232
|
console.log(chalk_1.default.blue('[INFO] Latest setup progress:'));
|
|
161
233
|
// Tail cloud-init output log for progress
|
|
162
234
|
const logOutput = sshExec(target.ipAddress, keyPath, "sudo tail -30 /var/log/cloud-init-output.log 2>/dev/null | grep -E '^===|^#|Progress:|DONE|error|Error|failed|Failed' || sudo tail -20 /var/log/cloud-init-output.log", 15);
|
|
@@ -165,29 +237,12 @@ exports.statusCommand = new commander_1.Command('status')
|
|
|
165
237
|
}
|
|
166
238
|
}
|
|
167
239
|
else if (status.includes('done')) {
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const setupTime = sshExec(target.ipAddress, keyPath, "sudo grep -oP 'Setup Complete in \\K[0-9]+m [0-9]+s' /var/log/cloud-init-output.log 2>/dev/null | tail -1");
|
|
172
|
-
if (setupTime && setupTime.trim()) {
|
|
173
|
-
completionTimeFormatted = setupTime.trim();
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
// Fallback: Calculate from cloud-init result.json timestamp vs boot time
|
|
177
|
-
// Get boot timestamp and cloud-init finish timestamp
|
|
178
|
-
const timestamps = sshExec(target.ipAddress, keyPath, "echo $(date -d \"$(uptime -s)\" +%s) $(stat -c %Y /run/cloud-init/result.json 2>/dev/null || echo 0)");
|
|
179
|
-
if (timestamps && timestamps.trim()) {
|
|
180
|
-
const [bootTime, finishTime] = timestamps.trim().split(' ').map(Number);
|
|
181
|
-
if (bootTime && finishTime && finishTime > bootTime) {
|
|
182
|
-
const elapsedSecs = finishTime - bootTime;
|
|
183
|
-
const mins = Math.floor(elapsedSecs / 60);
|
|
184
|
-
const secs = elapsedSecs % 60;
|
|
185
|
-
completionTimeFormatted = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
console.log(chalk_1.default.green(`[SUCCESS] Cloud-init completed!${completionTimeFormatted ? ` (${completionTimeFormatted})` : ''}`));
|
|
240
|
+
// Get timing breakdown
|
|
241
|
+
const timing = getTimingBreakdown(target.ipAddress, keyPath);
|
|
242
|
+
console.log(chalk_1.default.green(`[SUCCESS] Cloud-init completed!${timing.total !== null ? ` (${formatDuration(timing.total)})` : ''}`));
|
|
190
243
|
console.log('');
|
|
244
|
+
// Show detailed timing breakdown
|
|
245
|
+
displayTimingBreakdown(timing);
|
|
191
246
|
// Show Database Stats - only if configured
|
|
192
247
|
let dbContainer = '';
|
|
193
248
|
let dbName = '';
|