genbox 1.0.27 → 1.0.29
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 +157 -152
- package/dist/commands/status.js +21 -9
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -153,44 +153,65 @@ function getServiceNameFromUrl(baseUrl) {
|
|
|
153
153
|
return { name: 'unknown', varPrefix: 'UNKNOWN', description: 'Unknown Service' };
|
|
154
154
|
}
|
|
155
155
|
/**
|
|
156
|
-
*
|
|
156
|
+
* Create service URL mappings based on configured environments
|
|
157
|
+
* Uses URLs from configured environments instead of prompting again
|
|
157
158
|
*/
|
|
158
|
-
async function
|
|
159
|
+
async function createServiceUrlMappings(serviceUrls, configuredEnvs) {
|
|
159
160
|
const mappings = [];
|
|
160
161
|
if (serviceUrls.size === 0) {
|
|
161
162
|
return mappings;
|
|
162
163
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
// Determine which environment to use (prefer staging, then production)
|
|
165
|
+
const envNames = Object.keys(configuredEnvs || {});
|
|
166
|
+
const primaryEnv = envNames.includes('staging') ? 'staging' :
|
|
167
|
+
envNames.includes('production') ? 'production' :
|
|
168
|
+
envNames[0];
|
|
169
|
+
if (!primaryEnv || !configuredEnvs?.[primaryEnv]) {
|
|
170
|
+
// No environments configured - skip service URL mapping
|
|
171
|
+
return mappings;
|
|
172
|
+
}
|
|
173
|
+
const envConfig = configuredEnvs[primaryEnv];
|
|
174
|
+
const apiUrl = envConfig.urls?.api;
|
|
175
|
+
if (!apiUrl) {
|
|
176
|
+
// No API URL configured - skip
|
|
177
|
+
return mappings;
|
|
178
|
+
}
|
|
168
179
|
// Sort by port number for consistent ordering
|
|
169
180
|
const sortedServices = Array.from(serviceUrls.entries()).sort((a, b) => {
|
|
170
181
|
const portA = parseInt(a[0].match(/:(\d+)/)?.[1] || '0');
|
|
171
182
|
const portB = parseInt(b[0].match(/:(\d+)/)?.[1] || '0');
|
|
172
183
|
return portA - portB;
|
|
173
184
|
});
|
|
174
|
-
|
|
185
|
+
console.log('');
|
|
186
|
+
console.log(chalk_1.default.blue('=== Service URL Mapping ==='));
|
|
187
|
+
console.log(chalk_1.default.dim(`Mapping localhost URLs to ${primaryEnv} environment`));
|
|
188
|
+
console.log('');
|
|
189
|
+
for (const [baseUrl, { vars }] of sortedServices) {
|
|
175
190
|
const serviceInfo = getServiceNameFromUrl(baseUrl);
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
191
|
+
// Auto-map API/gateway URLs to the configured API URL
|
|
192
|
+
const isApiService = serviceInfo.name === 'gateway' ||
|
|
193
|
+
baseUrl.includes(':3050') ||
|
|
194
|
+
baseUrl.includes(':3105') ||
|
|
195
|
+
vars.some(v => v.toLowerCase().includes('api'));
|
|
196
|
+
if (isApiService && apiUrl) {
|
|
197
|
+
console.log(chalk_1.default.green(`✓ ${serviceInfo.description}: ${baseUrl} → ${apiUrl}`));
|
|
198
|
+
mappings.push({
|
|
199
|
+
varName: `${serviceInfo.varPrefix}_URL`,
|
|
200
|
+
localUrl: baseUrl,
|
|
201
|
+
remoteUrl: apiUrl,
|
|
202
|
+
remoteEnv: primaryEnv,
|
|
203
|
+
description: serviceInfo.description,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
// For non-API services, just record the local URL without remote
|
|
208
|
+
console.log(chalk_1.default.dim(` ${serviceInfo.description}: ${baseUrl} (local only)`));
|
|
209
|
+
mappings.push({
|
|
210
|
+
varName: `${serviceInfo.varPrefix}_URL`,
|
|
211
|
+
localUrl: baseUrl,
|
|
212
|
+
description: serviceInfo.description,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
194
215
|
}
|
|
195
216
|
return mappings;
|
|
196
217
|
}
|
|
@@ -202,19 +223,19 @@ function transformEnvWithVariables(envContent, mappings, frontendApps) {
|
|
|
202
223
|
return envContent;
|
|
203
224
|
}
|
|
204
225
|
let result = envContent;
|
|
226
|
+
// Determine the environment name for comments
|
|
227
|
+
const envName = mappings.find(m => m.remoteEnv)?.remoteEnv?.toUpperCase() || 'REMOTE';
|
|
205
228
|
// Build GLOBAL section additions
|
|
206
229
|
const globalAdditions = [
|
|
207
230
|
'',
|
|
208
231
|
'# Service URL Configuration',
|
|
209
|
-
|
|
232
|
+
`# These expand based on profile: \${GATEWAY_URL} → LOCAL or ${envName} value`,
|
|
210
233
|
];
|
|
211
234
|
for (const mapping of mappings) {
|
|
212
235
|
globalAdditions.push(`LOCAL_${mapping.varName}=${mapping.localUrl}`);
|
|
213
|
-
if (mapping.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
else {
|
|
217
|
-
globalAdditions.push(`# STAGING_${mapping.varName}=https://your-staging-url.com`);
|
|
236
|
+
if (mapping.remoteUrl && mapping.remoteEnv) {
|
|
237
|
+
const prefix = mapping.remoteEnv.toUpperCase();
|
|
238
|
+
globalAdditions.push(`${prefix}_${mapping.varName}=${mapping.remoteUrl}`);
|
|
218
239
|
}
|
|
219
240
|
}
|
|
220
241
|
globalAdditions.push('');
|
|
@@ -1175,127 +1196,96 @@ async function setupGitAuth(gitInfo, projectName) {
|
|
|
1175
1196
|
* Setup staging/production environments (v4 format)
|
|
1176
1197
|
*/
|
|
1177
1198
|
async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvValues = {}) {
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1199
|
+
// First ask which environments they want to configure
|
|
1200
|
+
const envChoice = await prompts.select({
|
|
1201
|
+
message: 'Which environments do you want to configure?',
|
|
1202
|
+
choices: [
|
|
1203
|
+
{ name: 'Staging only', value: 'staging', description: 'Connect to staging API' },
|
|
1204
|
+
{ name: 'Production only', value: 'production', description: 'Connect to production API' },
|
|
1205
|
+
{ name: 'Both staging and production', value: 'both', description: 'Configure both environments' },
|
|
1206
|
+
{ name: 'Skip for now', value: 'skip', description: 'No remote environments' },
|
|
1207
|
+
],
|
|
1208
|
+
default: 'staging',
|
|
1181
1209
|
});
|
|
1182
|
-
if (
|
|
1210
|
+
if (envChoice === 'skip') {
|
|
1183
1211
|
return undefined;
|
|
1184
1212
|
}
|
|
1185
1213
|
console.log('');
|
|
1186
1214
|
console.log(chalk_1.default.blue('=== Environment Setup ==='));
|
|
1187
1215
|
console.log(chalk_1.default.dim('These URLs will be used when connecting to external services.'));
|
|
1188
1216
|
console.log(chalk_1.default.dim('Actual secrets go in .env.genbox'));
|
|
1189
|
-
console.log('');
|
|
1190
1217
|
const environments = {};
|
|
1191
|
-
// Get existing
|
|
1218
|
+
// Get existing URLs if available
|
|
1192
1219
|
const existingStagingApiUrl = existingEnvValues['STAGING_API_URL'];
|
|
1193
1220
|
const existingProductionApiUrl = existingEnvValues['PRODUCTION_API_URL'] || existingEnvValues['PROD_API_URL'];
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
default:
|
|
1221
|
+
const configureStaging = envChoice === 'staging' || envChoice === 'both';
|
|
1222
|
+
const configureProduction = envChoice === 'production' || envChoice === 'both';
|
|
1223
|
+
// Configure staging if selected
|
|
1224
|
+
if (configureStaging) {
|
|
1225
|
+
console.log('');
|
|
1226
|
+
console.log(chalk_1.default.cyan('Staging Environment:'));
|
|
1227
|
+
if (isMultiRepo) {
|
|
1228
|
+
const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
|
|
1229
|
+
if (backendApps.length > 0) {
|
|
1230
|
+
const urls = {};
|
|
1231
|
+
for (const app of backendApps) {
|
|
1232
|
+
const existingUrl = existingEnvValues[`STAGING_${app.name.toUpperCase()}_URL`] ||
|
|
1233
|
+
(app.name === 'api' ? existingStagingApiUrl : '');
|
|
1234
|
+
if (existingUrl) {
|
|
1235
|
+
console.log(chalk_1.default.dim(` Found existing value for ${app.name}: ${existingUrl}`));
|
|
1236
|
+
const useExisting = await prompts.confirm({
|
|
1237
|
+
message: ` Use existing ${app.name} staging URL?`,
|
|
1238
|
+
default: true,
|
|
1239
|
+
});
|
|
1240
|
+
if (useExisting) {
|
|
1241
|
+
urls[app.name] = existingUrl;
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
const url = await prompts.input({
|
|
1246
|
+
message: ` ${app.name} staging URL:`,
|
|
1247
|
+
default: '',
|
|
1209
1248
|
});
|
|
1210
|
-
if (
|
|
1211
|
-
urls[app.name] =
|
|
1212
|
-
continue;
|
|
1249
|
+
if (url) {
|
|
1250
|
+
urls[app.name] = url;
|
|
1213
1251
|
}
|
|
1214
1252
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
urls[app.name] = url;
|
|
1253
|
+
if (Object.keys(urls).length > 0) {
|
|
1254
|
+
environments.staging = {
|
|
1255
|
+
description: 'Staging environment',
|
|
1256
|
+
urls,
|
|
1257
|
+
};
|
|
1221
1258
|
}
|
|
1222
1259
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1260
|
+
else {
|
|
1261
|
+
const stagingApiUrl = await promptForApiUrl('staging', existingStagingApiUrl);
|
|
1262
|
+
if (stagingApiUrl) {
|
|
1263
|
+
environments.staging = {
|
|
1264
|
+
description: 'Staging environment',
|
|
1265
|
+
urls: { api: stagingApiUrl },
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1228
1268
|
}
|
|
1229
1269
|
}
|
|
1230
1270
|
else {
|
|
1231
|
-
|
|
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
|
-
}
|
|
1271
|
+
const stagingApiUrl = await promptForApiUrl('staging', existingStagingApiUrl);
|
|
1249
1272
|
if (stagingApiUrl) {
|
|
1250
1273
|
environments.staging = {
|
|
1251
1274
|
description: 'Staging environment',
|
|
1252
|
-
urls: {
|
|
1253
|
-
api: stagingApiUrl,
|
|
1254
|
-
},
|
|
1275
|
+
urls: { api: stagingApiUrl },
|
|
1255
1276
|
};
|
|
1256
1277
|
}
|
|
1257
1278
|
}
|
|
1258
1279
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
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) {
|
|
1280
|
+
// Configure production if selected
|
|
1281
|
+
if (configureProduction) {
|
|
1282
|
+
console.log('');
|
|
1283
|
+
console.log(chalk_1.default.cyan('Production Environment:'));
|
|
1292
1284
|
if (isMultiRepo) {
|
|
1293
1285
|
const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
|
|
1294
1286
|
if (backendApps.length > 0) {
|
|
1295
|
-
console.log(chalk_1.default.dim('Configure production API URLs for each backend service:'));
|
|
1296
1287
|
const prodUrls = {};
|
|
1297
1288
|
for (const app of backendApps) {
|
|
1298
|
-
// Check for existing app-specific URL or use general production URL for 'api' app
|
|
1299
1289
|
const existingUrl = existingEnvValues[`PRODUCTION_${app.name.toUpperCase()}_URL`] ||
|
|
1300
1290
|
existingEnvValues[`PROD_${app.name.toUpperCase()}_URL`] ||
|
|
1301
1291
|
(app.name === 'api' ? existingProductionApiUrl : '');
|
|
@@ -1329,31 +1319,26 @@ async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvV
|
|
|
1329
1319
|
};
|
|
1330
1320
|
}
|
|
1331
1321
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1322
|
+
else {
|
|
1323
|
+
const prodApiUrl = await promptForApiUrl('production', existingProductionApiUrl);
|
|
1324
|
+
if (prodApiUrl) {
|
|
1325
|
+
environments.production = {
|
|
1326
|
+
description: 'Production (use with caution)',
|
|
1327
|
+
urls: { api: prodApiUrl },
|
|
1328
|
+
safety: {
|
|
1329
|
+
read_only: true,
|
|
1330
|
+
require_confirmation: true,
|
|
1331
|
+
},
|
|
1332
|
+
};
|
|
1343
1333
|
}
|
|
1344
1334
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
default: '',
|
|
1349
|
-
});
|
|
1350
|
-
}
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
const prodApiUrl = await promptForApiUrl('production', existingProductionApiUrl);
|
|
1351
1338
|
if (prodApiUrl) {
|
|
1352
1339
|
environments.production = {
|
|
1353
1340
|
description: 'Production (use with caution)',
|
|
1354
|
-
urls: {
|
|
1355
|
-
api: prodApiUrl,
|
|
1356
|
-
},
|
|
1341
|
+
urls: { api: prodApiUrl },
|
|
1357
1342
|
safety: {
|
|
1358
1343
|
read_only: true,
|
|
1359
1344
|
require_confirmation: true,
|
|
@@ -1364,6 +1349,25 @@ async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvV
|
|
|
1364
1349
|
}
|
|
1365
1350
|
return Object.keys(environments).length > 0 ? environments : undefined;
|
|
1366
1351
|
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Prompt for a single API URL with existing value support
|
|
1354
|
+
*/
|
|
1355
|
+
async function promptForApiUrl(envName, existingUrl) {
|
|
1356
|
+
if (existingUrl) {
|
|
1357
|
+
console.log(chalk_1.default.dim(` Found existing value: ${existingUrl}`));
|
|
1358
|
+
const useExisting = await prompts.confirm({
|
|
1359
|
+
message: ` Use existing ${envName} API URL?`,
|
|
1360
|
+
default: true,
|
|
1361
|
+
});
|
|
1362
|
+
if (useExisting) {
|
|
1363
|
+
return existingUrl;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return await prompts.input({
|
|
1367
|
+
message: ` ${envName.charAt(0).toUpperCase() + envName.slice(1)} API URL:`,
|
|
1368
|
+
default: '',
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1367
1371
|
/**
|
|
1368
1372
|
* Setup .env.genbox file with segregated app sections
|
|
1369
1373
|
*/
|
|
@@ -1512,17 +1516,18 @@ async function setupEnvFile(projectName, config, nonInteractive = false, scan, i
|
|
|
1512
1516
|
// Fall back to extracting from collected env content
|
|
1513
1517
|
serviceUrls = extractFrontendHttpUrls(segregatedContent, frontendApps);
|
|
1514
1518
|
}
|
|
1515
|
-
if (serviceUrls.size > 0) {
|
|
1516
|
-
//
|
|
1517
|
-
const
|
|
1518
|
-
(config.environments?.staging?.urls?.api);
|
|
1519
|
-
// Prompt for staging equivalents
|
|
1520
|
-
const urlMappings = await promptForStagingUrls(serviceUrls, existingStagingApiUrl);
|
|
1519
|
+
if (serviceUrls.size > 0 && config.environments) {
|
|
1520
|
+
// Create mappings based on configured environments (no prompting needed)
|
|
1521
|
+
const urlMappings = await createServiceUrlMappings(serviceUrls, config.environments);
|
|
1521
1522
|
// Transform content with expandable variables
|
|
1522
1523
|
if (urlMappings.length > 0) {
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1524
|
+
const mappedCount = urlMappings.filter(m => m.remoteUrl).length;
|
|
1525
|
+
if (mappedCount > 0) {
|
|
1526
|
+
segregatedContent = transformEnvWithVariables(segregatedContent, urlMappings, frontendApps);
|
|
1527
|
+
const envName = urlMappings.find(m => m.remoteEnv)?.remoteEnv || 'remote';
|
|
1528
|
+
console.log('');
|
|
1529
|
+
console.log(chalk_1.default.green(`✓ Configured ${mappedCount} service URL(s) for ${envName} environment`));
|
|
1530
|
+
}
|
|
1526
1531
|
}
|
|
1527
1532
|
}
|
|
1528
1533
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -165,16 +165,28 @@ exports.statusCommand = new commander_1.Command('status')
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
else if (status.includes('done')) {
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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!${
|
|
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 = '';
|