genbox 1.0.27 → 1.0.28

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.
@@ -1175,127 +1175,96 @@ async function setupGitAuth(gitInfo, projectName) {
1175
1175
  * Setup staging/production environments (v4 format)
1176
1176
  */
1177
1177
  async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvValues = {}) {
1178
- const setupEnvs = await prompts.confirm({
1179
- message: 'Configure staging/production environments?',
1180
- default: true,
1178
+ // First ask which environments they want to configure
1179
+ const envChoice = await prompts.select({
1180
+ message: 'Which environments do you want to configure?',
1181
+ choices: [
1182
+ { name: 'Staging only', value: 'staging', description: 'Connect to staging API' },
1183
+ { name: 'Production only', value: 'production', description: 'Connect to production API' },
1184
+ { name: 'Both staging and production', value: 'both', description: 'Configure both environments' },
1185
+ { name: 'Skip for now', value: 'skip', description: 'No remote environments' },
1186
+ ],
1187
+ default: 'staging',
1181
1188
  });
1182
- if (!setupEnvs) {
1189
+ if (envChoice === 'skip') {
1183
1190
  return undefined;
1184
1191
  }
1185
1192
  console.log('');
1186
1193
  console.log(chalk_1.default.blue('=== Environment Setup ==='));
1187
1194
  console.log(chalk_1.default.dim('These URLs will be used when connecting to external services.'));
1188
1195
  console.log(chalk_1.default.dim('Actual secrets go in .env.genbox'));
1189
- console.log('');
1190
1196
  const environments = {};
1191
- // Get existing staging API URL if available
1197
+ // Get existing URLs if available
1192
1198
  const existingStagingApiUrl = existingEnvValues['STAGING_API_URL'];
1193
1199
  const existingProductionApiUrl = existingEnvValues['PRODUCTION_API_URL'] || existingEnvValues['PROD_API_URL'];
1194
- if (isMultiRepo) {
1195
- // For multi-repo: configure API URLs per backend app
1196
- const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
1197
- if (backendApps.length > 0) {
1198
- console.log(chalk_1.default.dim('Configure staging API URLs for each backend service:'));
1199
- const urls = {};
1200
- for (const app of backendApps) {
1201
- // Check for existing app-specific URL or use general staging URL for 'api' app
1202
- const existingUrl = existingEnvValues[`STAGING_${app.name.toUpperCase()}_URL`] ||
1203
- (app.name === 'api' ? existingStagingApiUrl : '');
1204
- if (existingUrl) {
1205
- console.log(chalk_1.default.dim(` Found existing value for ${app.name}: ${existingUrl}`));
1206
- const useExisting = await prompts.confirm({
1207
- message: ` Use existing ${app.name} staging URL?`,
1208
- default: true,
1200
+ const configureStaging = envChoice === 'staging' || envChoice === 'both';
1201
+ const configureProduction = envChoice === 'production' || envChoice === 'both';
1202
+ // Configure staging if selected
1203
+ if (configureStaging) {
1204
+ console.log('');
1205
+ console.log(chalk_1.default.cyan('Staging Environment:'));
1206
+ if (isMultiRepo) {
1207
+ const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
1208
+ if (backendApps.length > 0) {
1209
+ const urls = {};
1210
+ for (const app of backendApps) {
1211
+ const existingUrl = existingEnvValues[`STAGING_${app.name.toUpperCase()}_URL`] ||
1212
+ (app.name === 'api' ? existingStagingApiUrl : '');
1213
+ if (existingUrl) {
1214
+ console.log(chalk_1.default.dim(` Found existing value for ${app.name}: ${existingUrl}`));
1215
+ const useExisting = await prompts.confirm({
1216
+ message: ` Use existing ${app.name} staging URL?`,
1217
+ default: true,
1218
+ });
1219
+ if (useExisting) {
1220
+ urls[app.name] = existingUrl;
1221
+ continue;
1222
+ }
1223
+ }
1224
+ const url = await prompts.input({
1225
+ message: ` ${app.name} staging URL:`,
1226
+ default: '',
1209
1227
  });
1210
- if (useExisting) {
1211
- urls[app.name] = existingUrl;
1212
- continue;
1228
+ if (url) {
1229
+ urls[app.name] = url;
1213
1230
  }
1214
1231
  }
1215
- const url = await prompts.input({
1216
- message: ` ${app.name} staging URL (leave empty to skip):`,
1217
- default: '',
1218
- });
1219
- if (url) {
1220
- urls[app.name] = url;
1232
+ if (Object.keys(urls).length > 0) {
1233
+ environments.staging = {
1234
+ description: 'Staging environment',
1235
+ urls,
1236
+ };
1221
1237
  }
1222
1238
  }
1223
- if (Object.keys(urls).length > 0) {
1224
- environments.staging = {
1225
- description: 'Staging environment',
1226
- urls,
1227
- };
1239
+ else {
1240
+ const stagingApiUrl = await promptForApiUrl('staging', existingStagingApiUrl);
1241
+ if (stagingApiUrl) {
1242
+ environments.staging = {
1243
+ description: 'Staging environment',
1244
+ urls: { api: stagingApiUrl },
1245
+ };
1246
+ }
1228
1247
  }
1229
1248
  }
1230
1249
  else {
1231
- // No backend apps, just ask for a single URL
1232
- let stagingApiUrl = '';
1233
- if (existingStagingApiUrl) {
1234
- console.log(chalk_1.default.dim(` Found existing value: ${existingStagingApiUrl}`));
1235
- const useExisting = await prompts.confirm({
1236
- message: 'Use existing staging API URL?',
1237
- default: true,
1238
- });
1239
- if (useExisting) {
1240
- stagingApiUrl = existingStagingApiUrl;
1241
- }
1242
- }
1243
- if (!stagingApiUrl) {
1244
- stagingApiUrl = await prompts.input({
1245
- message: 'Staging API URL (leave empty to skip):',
1246
- default: '',
1247
- });
1248
- }
1250
+ const stagingApiUrl = await promptForApiUrl('staging', existingStagingApiUrl);
1249
1251
  if (stagingApiUrl) {
1250
1252
  environments.staging = {
1251
1253
  description: 'Staging environment',
1252
- urls: {
1253
- api: stagingApiUrl,
1254
- },
1254
+ urls: { api: stagingApiUrl },
1255
1255
  };
1256
1256
  }
1257
1257
  }
1258
1258
  }
1259
- else {
1260
- // Single repo: simple single URL
1261
- let stagingApiUrl = '';
1262
- if (existingStagingApiUrl) {
1263
- console.log(chalk_1.default.dim(` Found existing value: ${existingStagingApiUrl}`));
1264
- const useExisting = await prompts.confirm({
1265
- message: 'Use existing staging API URL?',
1266
- default: true,
1267
- });
1268
- if (useExisting) {
1269
- stagingApiUrl = existingStagingApiUrl;
1270
- }
1271
- }
1272
- if (!stagingApiUrl) {
1273
- stagingApiUrl = await prompts.input({
1274
- message: 'Staging API URL (leave empty to skip):',
1275
- default: '',
1276
- });
1277
- }
1278
- if (stagingApiUrl) {
1279
- environments.staging = {
1280
- description: 'Staging environment',
1281
- urls: {
1282
- api: stagingApiUrl,
1283
- },
1284
- };
1285
- }
1286
- }
1287
- const setupProd = await prompts.confirm({
1288
- message: 'Also configure production environment?',
1289
- default: false,
1290
- });
1291
- if (setupProd) {
1259
+ // Configure production if selected
1260
+ if (configureProduction) {
1261
+ console.log('');
1262
+ console.log(chalk_1.default.cyan('Production Environment:'));
1292
1263
  if (isMultiRepo) {
1293
1264
  const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
1294
1265
  if (backendApps.length > 0) {
1295
- console.log(chalk_1.default.dim('Configure production API URLs for each backend service:'));
1296
1266
  const prodUrls = {};
1297
1267
  for (const app of backendApps) {
1298
- // Check for existing app-specific URL or use general production URL for 'api' app
1299
1268
  const existingUrl = existingEnvValues[`PRODUCTION_${app.name.toUpperCase()}_URL`] ||
1300
1269
  existingEnvValues[`PROD_${app.name.toUpperCase()}_URL`] ||
1301
1270
  (app.name === 'api' ? existingProductionApiUrl : '');
@@ -1329,31 +1298,26 @@ async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvV
1329
1298
  };
1330
1299
  }
1331
1300
  }
1332
- }
1333
- else {
1334
- let prodApiUrl = '';
1335
- if (existingProductionApiUrl) {
1336
- console.log(chalk_1.default.dim(` Found existing value: ${existingProductionApiUrl}`));
1337
- const useExisting = await prompts.confirm({
1338
- message: 'Use existing production API URL?',
1339
- default: true,
1340
- });
1341
- if (useExisting) {
1342
- prodApiUrl = existingProductionApiUrl;
1301
+ else {
1302
+ const prodApiUrl = await promptForApiUrl('production', existingProductionApiUrl);
1303
+ if (prodApiUrl) {
1304
+ environments.production = {
1305
+ description: 'Production (use with caution)',
1306
+ urls: { api: prodApiUrl },
1307
+ safety: {
1308
+ read_only: true,
1309
+ require_confirmation: true,
1310
+ },
1311
+ };
1343
1312
  }
1344
1313
  }
1345
- if (!prodApiUrl) {
1346
- prodApiUrl = await prompts.input({
1347
- message: 'Production API URL:',
1348
- default: '',
1349
- });
1350
- }
1314
+ }
1315
+ else {
1316
+ const prodApiUrl = await promptForApiUrl('production', existingProductionApiUrl);
1351
1317
  if (prodApiUrl) {
1352
1318
  environments.production = {
1353
1319
  description: 'Production (use with caution)',
1354
- urls: {
1355
- api: prodApiUrl,
1356
- },
1320
+ urls: { api: prodApiUrl },
1357
1321
  safety: {
1358
1322
  read_only: true,
1359
1323
  require_confirmation: true,
@@ -1364,6 +1328,25 @@ async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvV
1364
1328
  }
1365
1329
  return Object.keys(environments).length > 0 ? environments : undefined;
1366
1330
  }
1331
+ /**
1332
+ * Prompt for a single API URL with existing value support
1333
+ */
1334
+ async function promptForApiUrl(envName, existingUrl) {
1335
+ if (existingUrl) {
1336
+ console.log(chalk_1.default.dim(` Found existing value: ${existingUrl}`));
1337
+ const useExisting = await prompts.confirm({
1338
+ message: ` Use existing ${envName} API URL?`,
1339
+ default: true,
1340
+ });
1341
+ if (useExisting) {
1342
+ return existingUrl;
1343
+ }
1344
+ }
1345
+ return await prompts.input({
1346
+ message: ` ${envName.charAt(0).toUpperCase() + envName.slice(1)} API URL:`,
1347
+ default: '',
1348
+ });
1349
+ }
1367
1350
  /**
1368
1351
  * Setup .env.genbox file with segregated app sections
1369
1352
  */
@@ -165,16 +165,28 @@ exports.statusCommand = new commander_1.Command('status')
165
165
  }
166
166
  }
167
167
  else if (status.includes('done')) {
168
- // Get uptime for timing display
169
- const uptimeRaw = sshExec(target.ipAddress, keyPath, "cat /proc/uptime 2>/dev/null | cut -d' ' -f1");
170
- let uptimeFormatted = '';
171
- if (uptimeRaw) {
172
- const uptimeSecs = Math.floor(parseFloat(uptimeRaw));
173
- const mins = Math.floor(uptimeSecs / 60);
174
- const secs = uptimeSecs % 60;
175
- uptimeFormatted = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
168
+ // Try to get actual cloud-init completion time from logs first
169
+ let completionTimeFormatted = '';
170
+ // First, try to extract "Setup Complete in Xm Ys" from cloud-init logs
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
+ }
176
188
  }
177
- console.log(chalk_1.default.green(`[SUCCESS] Cloud-init completed!${uptimeFormatted ? ` (${uptimeFormatted})` : ''}`));
189
+ console.log(chalk_1.default.green(`[SUCCESS] Cloud-init completed!${completionTimeFormatted ? ` (${completionTimeFormatted})` : ''}`));
178
190
  console.log('');
179
191
  // Show Database Stats - only if configured
180
192
  let dbContainer = '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.27",
3
+ "version": "1.0.28",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {