genbox 1.0.26 → 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.
package/dist/api.js CHANGED
@@ -49,10 +49,26 @@ async function fetchApi(endpoint, options = {}) {
49
49
  if (token) {
50
50
  headers['Authorization'] = `Bearer ${token}`;
51
51
  }
52
- const response = await fetch(url, {
53
- ...options,
54
- headers,
55
- });
52
+ let response;
53
+ try {
54
+ response = await fetch(url, {
55
+ ...options,
56
+ headers,
57
+ });
58
+ }
59
+ catch (error) {
60
+ // Handle network-level errors (DNS, connection refused, timeout, etc.)
61
+ if (error.cause?.code === 'ECONNREFUSED') {
62
+ throw new Error(`Cannot connect to Genbox API at ${API_URL}. Is the server running?`);
63
+ }
64
+ if (error.cause?.code === 'ENOTFOUND') {
65
+ throw new Error(`Cannot resolve Genbox API host. Check your internet connection.`);
66
+ }
67
+ if (error.message?.includes('fetch failed')) {
68
+ throw new Error(`Cannot connect to Genbox API at ${API_URL}. Check your internet connection or try again later.`);
69
+ }
70
+ throw new Error(`Network error: ${error.message}`);
71
+ }
56
72
  if (!response.ok) {
57
73
  // Handle authentication errors with a friendly message
58
74
  if (response.status === 401) {
@@ -763,6 +763,14 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
763
763
  gitToken: envVars.GIT_TOKEN,
764
764
  envVars: resolved.env,
765
765
  apps: resolved.apps.map(a => a.name),
766
+ appConfigs: resolved.apps.map(a => ({
767
+ name: a.name,
768
+ path: a.path.startsWith('/') ? a.path : `${resolved.repos[0]?.path || '/home/dev'}/${a.path}`,
769
+ type: a.type,
770
+ port: a.port,
771
+ framework: a.framework,
772
+ commands: a.commands,
773
+ })),
766
774
  infrastructure: resolved.infrastructure.map(i => ({
767
775
  name: i.name,
768
776
  type: i.type,
@@ -1012,35 +1012,59 @@ exports.initCommand = new commander_1.Command('init')
1012
1012
  * Create default profiles (sync version for non-interactive mode)
1013
1013
  */
1014
1014
  function createDefaultProfilesSync(scan, config) {
1015
- return createProfilesFromScan(scan);
1015
+ const definedEnvs = Object.keys(config.environments || {});
1016
+ return createProfilesFromScan(scan, definedEnvs);
1016
1017
  }
1017
1018
  async function createDefaultProfiles(scan, config) {
1018
- return createProfilesFromScan(scan);
1019
+ // Get defined environments to use in profiles
1020
+ const definedEnvs = Object.keys(config.environments || {});
1021
+ return createProfilesFromScan(scan, definedEnvs);
1019
1022
  }
1020
- function createProfilesFromScan(scan) {
1023
+ function createProfilesFromScan(scan, definedEnvironments = []) {
1021
1024
  const profiles = {};
1022
1025
  const frontendApps = scan.apps.filter(a => a.type === 'frontend');
1023
1026
  const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
1024
1027
  const hasApi = scan.apps.some(a => a.name === 'api' || a.type === 'backend');
1028
+ // Determine which environment to use for remote connections
1029
+ // Priority: staging > production > first defined
1030
+ const remoteEnv = definedEnvironments.includes('staging')
1031
+ ? 'staging'
1032
+ : definedEnvironments.includes('production')
1033
+ ? 'production'
1034
+ : definedEnvironments[0];
1025
1035
  // Quick UI profiles for each frontend (v4: use default_connection instead of connect_to)
1026
- for (const frontend of frontendApps.slice(0, 3)) {
1027
- profiles[`${frontend.name}-quick`] = {
1028
- description: `${frontend.name} only, connected to staging`,
1029
- size: 'small',
1030
- apps: [frontend.name],
1031
- default_connection: 'staging',
1032
- };
1036
+ // Only create if we have a remote environment defined
1037
+ if (remoteEnv) {
1038
+ for (const frontend of frontendApps.slice(0, 3)) {
1039
+ profiles[`${frontend.name}-quick`] = {
1040
+ description: `${frontend.name} only, connected to ${remoteEnv}`,
1041
+ size: 'small',
1042
+ apps: [frontend.name],
1043
+ default_connection: remoteEnv,
1044
+ };
1045
+ }
1033
1046
  }
1034
- // Full local development
1047
+ else {
1048
+ // No remote environment - create local-only profiles
1049
+ for (const frontend of frontendApps.slice(0, 3)) {
1050
+ profiles[`${frontend.name}-local`] = {
1051
+ description: `${frontend.name} with local API`,
1052
+ size: 'medium',
1053
+ apps: [frontend.name, ...(hasApi ? ['api'] : [])],
1054
+ };
1055
+ }
1056
+ }
1057
+ // Full local development (with DB copy from available environment)
1035
1058
  if (hasApi && frontendApps.length > 0) {
1036
1059
  const primaryFrontend = frontendApps[0];
1060
+ const dbSource = remoteEnv || 'staging'; // Use defined env or default
1037
1061
  profiles[`${primaryFrontend.name}-full`] = {
1038
1062
  description: `${primaryFrontend.name} + local API + DB copy`,
1039
1063
  size: 'large',
1040
1064
  apps: [primaryFrontend.name, 'api'],
1041
1065
  database: {
1042
- mode: 'copy',
1043
- source: 'staging',
1066
+ mode: remoteEnv ? 'copy' : 'local', // Only copy if we have a source
1067
+ ...(remoteEnv && { source: dbSource }),
1044
1068
  },
1045
1069
  };
1046
1070
  }
@@ -1055,24 +1079,25 @@ function createProfilesFromScan(scan) {
1055
1079
  },
1056
1080
  };
1057
1081
  }
1058
- // All frontends + staging (v4: use default_connection)
1059
- if (frontendApps.length > 1) {
1060
- profiles['frontends-staging'] = {
1061
- description: 'All frontends with staging backend',
1082
+ // All frontends + remote backend (only if remote env defined)
1083
+ if (frontendApps.length > 1 && remoteEnv) {
1084
+ profiles[`frontends-${remoteEnv}`] = {
1085
+ description: `All frontends with ${remoteEnv} backend`,
1062
1086
  size: 'medium',
1063
1087
  apps: frontendApps.map(a => a.name),
1064
- default_connection: 'staging',
1088
+ default_connection: remoteEnv,
1065
1089
  };
1066
1090
  }
1067
1091
  // Full stack
1068
1092
  if (scan.apps.length > 1) {
1093
+ const dbSource = remoteEnv || 'staging';
1069
1094
  profiles['full-stack'] = {
1070
- description: 'Everything local with DB copy',
1095
+ description: 'Everything local' + (remoteEnv ? ' with DB copy' : ''),
1071
1096
  size: 'xl',
1072
1097
  apps: scan.apps.filter(a => a.type !== 'library').map(a => a.name),
1073
1098
  database: {
1074
- mode: 'copy',
1075
- source: 'staging',
1099
+ mode: remoteEnv ? 'copy' : 'local',
1100
+ ...(remoteEnv && { source: dbSource }),
1076
1101
  },
1077
1102
  };
1078
1103
  }
@@ -1150,127 +1175,96 @@ async function setupGitAuth(gitInfo, projectName) {
1150
1175
  * Setup staging/production environments (v4 format)
1151
1176
  */
1152
1177
  async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvValues = {}) {
1153
- const setupEnvs = await prompts.confirm({
1154
- message: 'Configure staging/production environments?',
1155
- 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',
1156
1188
  });
1157
- if (!setupEnvs) {
1189
+ if (envChoice === 'skip') {
1158
1190
  return undefined;
1159
1191
  }
1160
1192
  console.log('');
1161
1193
  console.log(chalk_1.default.blue('=== Environment Setup ==='));
1162
1194
  console.log(chalk_1.default.dim('These URLs will be used when connecting to external services.'));
1163
1195
  console.log(chalk_1.default.dim('Actual secrets go in .env.genbox'));
1164
- console.log('');
1165
1196
  const environments = {};
1166
- // Get existing staging API URL if available
1197
+ // Get existing URLs if available
1167
1198
  const existingStagingApiUrl = existingEnvValues['STAGING_API_URL'];
1168
1199
  const existingProductionApiUrl = existingEnvValues['PRODUCTION_API_URL'] || existingEnvValues['PROD_API_URL'];
1169
- if (isMultiRepo) {
1170
- // For multi-repo: configure API URLs per backend app
1171
- const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
1172
- if (backendApps.length > 0) {
1173
- console.log(chalk_1.default.dim('Configure staging API URLs for each backend service:'));
1174
- const urls = {};
1175
- for (const app of backendApps) {
1176
- // Check for existing app-specific URL or use general staging URL for 'api' app
1177
- const existingUrl = existingEnvValues[`STAGING_${app.name.toUpperCase()}_URL`] ||
1178
- (app.name === 'api' ? existingStagingApiUrl : '');
1179
- if (existingUrl) {
1180
- console.log(chalk_1.default.dim(` Found existing value for ${app.name}: ${existingUrl}`));
1181
- const useExisting = await prompts.confirm({
1182
- message: ` Use existing ${app.name} staging URL?`,
1183
- 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: '',
1184
1227
  });
1185
- if (useExisting) {
1186
- urls[app.name] = existingUrl;
1187
- continue;
1228
+ if (url) {
1229
+ urls[app.name] = url;
1188
1230
  }
1189
1231
  }
1190
- const url = await prompts.input({
1191
- message: ` ${app.name} staging URL (leave empty to skip):`,
1192
- default: '',
1193
- });
1194
- if (url) {
1195
- urls[app.name] = url;
1232
+ if (Object.keys(urls).length > 0) {
1233
+ environments.staging = {
1234
+ description: 'Staging environment',
1235
+ urls,
1236
+ };
1196
1237
  }
1197
1238
  }
1198
- if (Object.keys(urls).length > 0) {
1199
- environments.staging = {
1200
- description: 'Staging environment',
1201
- urls,
1202
- };
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
+ }
1203
1247
  }
1204
1248
  }
1205
1249
  else {
1206
- // No backend apps, just ask for a single URL
1207
- let stagingApiUrl = '';
1208
- if (existingStagingApiUrl) {
1209
- console.log(chalk_1.default.dim(` Found existing value: ${existingStagingApiUrl}`));
1210
- const useExisting = await prompts.confirm({
1211
- message: 'Use existing staging API URL?',
1212
- default: true,
1213
- });
1214
- if (useExisting) {
1215
- stagingApiUrl = existingStagingApiUrl;
1216
- }
1217
- }
1218
- if (!stagingApiUrl) {
1219
- stagingApiUrl = await prompts.input({
1220
- message: 'Staging API URL (leave empty to skip):',
1221
- default: '',
1222
- });
1223
- }
1250
+ const stagingApiUrl = await promptForApiUrl('staging', existingStagingApiUrl);
1224
1251
  if (stagingApiUrl) {
1225
1252
  environments.staging = {
1226
1253
  description: 'Staging environment',
1227
- urls: {
1228
- api: stagingApiUrl,
1229
- },
1254
+ urls: { api: stagingApiUrl },
1230
1255
  };
1231
1256
  }
1232
1257
  }
1233
1258
  }
1234
- else {
1235
- // Single repo: simple single URL
1236
- let stagingApiUrl = '';
1237
- if (existingStagingApiUrl) {
1238
- console.log(chalk_1.default.dim(` Found existing value: ${existingStagingApiUrl}`));
1239
- const useExisting = await prompts.confirm({
1240
- message: 'Use existing staging API URL?',
1241
- default: true,
1242
- });
1243
- if (useExisting) {
1244
- stagingApiUrl = existingStagingApiUrl;
1245
- }
1246
- }
1247
- if (!stagingApiUrl) {
1248
- stagingApiUrl = await prompts.input({
1249
- message: 'Staging API URL (leave empty to skip):',
1250
- default: '',
1251
- });
1252
- }
1253
- if (stagingApiUrl) {
1254
- environments.staging = {
1255
- description: 'Staging environment',
1256
- urls: {
1257
- api: stagingApiUrl,
1258
- },
1259
- };
1260
- }
1261
- }
1262
- const setupProd = await prompts.confirm({
1263
- message: 'Also configure production environment?',
1264
- default: false,
1265
- });
1266
- if (setupProd) {
1259
+ // Configure production if selected
1260
+ if (configureProduction) {
1261
+ console.log('');
1262
+ console.log(chalk_1.default.cyan('Production Environment:'));
1267
1263
  if (isMultiRepo) {
1268
1264
  const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
1269
1265
  if (backendApps.length > 0) {
1270
- console.log(chalk_1.default.dim('Configure production API URLs for each backend service:'));
1271
1266
  const prodUrls = {};
1272
1267
  for (const app of backendApps) {
1273
- // Check for existing app-specific URL or use general production URL for 'api' app
1274
1268
  const existingUrl = existingEnvValues[`PRODUCTION_${app.name.toUpperCase()}_URL`] ||
1275
1269
  existingEnvValues[`PROD_${app.name.toUpperCase()}_URL`] ||
1276
1270
  (app.name === 'api' ? existingProductionApiUrl : '');
@@ -1304,31 +1298,26 @@ async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvV
1304
1298
  };
1305
1299
  }
1306
1300
  }
1307
- }
1308
- else {
1309
- let prodApiUrl = '';
1310
- if (existingProductionApiUrl) {
1311
- console.log(chalk_1.default.dim(` Found existing value: ${existingProductionApiUrl}`));
1312
- const useExisting = await prompts.confirm({
1313
- message: 'Use existing production API URL?',
1314
- default: true,
1315
- });
1316
- if (useExisting) {
1317
- 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
+ };
1318
1312
  }
1319
1313
  }
1320
- if (!prodApiUrl) {
1321
- prodApiUrl = await prompts.input({
1322
- message: 'Production API URL:',
1323
- default: '',
1324
- });
1325
- }
1314
+ }
1315
+ else {
1316
+ const prodApiUrl = await promptForApiUrl('production', existingProductionApiUrl);
1326
1317
  if (prodApiUrl) {
1327
1318
  environments.production = {
1328
1319
  description: 'Production (use with caution)',
1329
- urls: {
1330
- api: prodApiUrl,
1331
- },
1320
+ urls: { api: prodApiUrl },
1332
1321
  safety: {
1333
1322
  read_only: true,
1334
1323
  require_confirmation: true,
@@ -1339,6 +1328,25 @@ async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvV
1339
1328
  }
1340
1329
  return Object.keys(environments).length > 0 ? environments : undefined;
1341
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
+ }
1342
1350
  /**
1343
1351
  * Setup .env.genbox file with segregated app sections
1344
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.26",
3
+ "version": "1.0.28",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {