genbox 1.0.16 → 1.0.18
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 +165 -43
- package/dist/commands/scan.js +148 -49
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -365,6 +365,81 @@ function findAppEnvFiles(apps, rootDir) {
|
|
|
365
365
|
}
|
|
366
366
|
return envFiles;
|
|
367
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* Read existing values from .env.genbox file
|
|
370
|
+
*/
|
|
371
|
+
function readExistingEnvGenbox() {
|
|
372
|
+
const envPath = path_1.default.join(process.cwd(), ENV_FILENAME);
|
|
373
|
+
const values = {};
|
|
374
|
+
if (!fs_1.default.existsSync(envPath)) {
|
|
375
|
+
return values;
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
const content = fs_1.default.readFileSync(envPath, 'utf8');
|
|
379
|
+
for (const line of content.split('\n')) {
|
|
380
|
+
// Skip comments and empty lines
|
|
381
|
+
const trimmed = line.trim();
|
|
382
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
383
|
+
continue;
|
|
384
|
+
// Parse KEY=value
|
|
385
|
+
const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
386
|
+
if (match) {
|
|
387
|
+
let value = match[2].trim();
|
|
388
|
+
// Remove quotes if present
|
|
389
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
390
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
391
|
+
value = value.slice(1, -1);
|
|
392
|
+
}
|
|
393
|
+
values[match[1]] = value;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
// Ignore read errors
|
|
399
|
+
}
|
|
400
|
+
return values;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Mask a secret value for display (show first 4 and last 4 chars)
|
|
404
|
+
*/
|
|
405
|
+
function maskSecret(value) {
|
|
406
|
+
if (value.length <= 8) {
|
|
407
|
+
return '*'.repeat(value.length);
|
|
408
|
+
}
|
|
409
|
+
return value.slice(0, 4) + '*'.repeat(value.length - 8) + value.slice(-4);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Prompt for a secret with existing value support
|
|
413
|
+
*/
|
|
414
|
+
async function promptForSecret(message, existingValue, options = {}) {
|
|
415
|
+
if (existingValue) {
|
|
416
|
+
console.log(chalk_1.default.dim(` Found existing value: ${maskSecret(existingValue)}`));
|
|
417
|
+
const useExisting = await prompts.confirm({
|
|
418
|
+
message: 'Use existing value?',
|
|
419
|
+
default: true,
|
|
420
|
+
});
|
|
421
|
+
if (useExisting) {
|
|
422
|
+
return existingValue;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (options.showInstructions) {
|
|
426
|
+
console.log('');
|
|
427
|
+
console.log(chalk_1.default.dim(' To create a token:'));
|
|
428
|
+
console.log(chalk_1.default.dim(' 1. Go to https://github.com/settings/tokens'));
|
|
429
|
+
console.log(chalk_1.default.dim(' 2. Click "Generate new token" → "Classic"'));
|
|
430
|
+
console.log(chalk_1.default.dim(' 3. Select scope: "repo" (Full control of private repositories)'));
|
|
431
|
+
console.log(chalk_1.default.dim(' 4. Generate and copy the token'));
|
|
432
|
+
console.log('');
|
|
433
|
+
}
|
|
434
|
+
let value = await prompts.password({
|
|
435
|
+
message,
|
|
436
|
+
});
|
|
437
|
+
if (value) {
|
|
438
|
+
// Strip any "KEY=" prefix if user pasted the whole line
|
|
439
|
+
value = value.replace(/^[A-Z_]+=/, '');
|
|
440
|
+
}
|
|
441
|
+
return value || undefined;
|
|
442
|
+
}
|
|
368
443
|
exports.initCommand = new commander_1.Command('init')
|
|
369
444
|
.description('Initialize a new Genbox configuration')
|
|
370
445
|
.option('--v2', 'Use legacy v2 format (single-app only)')
|
|
@@ -397,6 +472,8 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
397
472
|
}
|
|
398
473
|
console.log(chalk_1.default.blue('Initializing Genbox...'));
|
|
399
474
|
console.log('');
|
|
475
|
+
// Read existing .env.genbox values (for defaults when overwriting)
|
|
476
|
+
const existingEnvValues = readExistingEnvGenbox();
|
|
400
477
|
// Track env vars to add to .env.genbox
|
|
401
478
|
const envVarsToAdd = {};
|
|
402
479
|
// Get initial exclusions from CLI options only
|
|
@@ -589,19 +666,8 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
589
666
|
if (hasHttpsRepos && !nonInteractive) {
|
|
590
667
|
console.log('');
|
|
591
668
|
console.log(chalk_1.default.yellow('Private repositories require a GitHub token for cloning.'));
|
|
592
|
-
|
|
593
|
-
console.log(chalk_1.default.dim(' To create a token:'));
|
|
594
|
-
console.log(chalk_1.default.dim(' 1. Go to https://github.com/settings/tokens'));
|
|
595
|
-
console.log(chalk_1.default.dim(' 2. Click "Generate new token" → "Classic"'));
|
|
596
|
-
console.log(chalk_1.default.dim(' 3. Select scope: "repo" (Full control of private repositories)'));
|
|
597
|
-
console.log(chalk_1.default.dim(' 4. Generate and copy the token'));
|
|
598
|
-
console.log('');
|
|
599
|
-
let gitToken = await prompts.password({
|
|
600
|
-
message: 'GitHub Personal Access Token (leave empty to skip):',
|
|
601
|
-
});
|
|
669
|
+
const gitToken = await promptForSecret('GitHub Personal Access Token (leave empty to skip):', existingEnvValues['GIT_TOKEN'], { showInstructions: !existingEnvValues['GIT_TOKEN'] });
|
|
602
670
|
if (gitToken) {
|
|
603
|
-
// Strip any "GIT_TOKEN=" prefix if user pasted the whole line
|
|
604
|
-
gitToken = gitToken.replace(/^GIT_TOKEN=/i, '');
|
|
605
671
|
envVarsToAdd['GIT_TOKEN'] = gitToken;
|
|
606
672
|
console.log(chalk_1.default.green('✓ GIT_TOKEN will be added to .env.genbox'));
|
|
607
673
|
}
|
|
@@ -625,12 +691,8 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
625
691
|
if (scan.git.type === 'https' && !nonInteractive) {
|
|
626
692
|
console.log('');
|
|
627
693
|
console.log(chalk_1.default.yellow('Private repositories require a GitHub token for cloning.'));
|
|
628
|
-
|
|
629
|
-
message: 'GitHub Personal Access Token (leave empty to skip):',
|
|
630
|
-
});
|
|
694
|
+
const gitToken = await promptForSecret('GitHub Personal Access Token (leave empty to skip):', existingEnvValues['GIT_TOKEN'], { showInstructions: !existingEnvValues['GIT_TOKEN'] });
|
|
631
695
|
if (gitToken) {
|
|
632
|
-
// Strip any "GIT_TOKEN=" prefix if user pasted the whole line
|
|
633
|
-
gitToken = gitToken.replace(/^GIT_TOKEN=/i, '');
|
|
634
696
|
envVarsToAdd['GIT_TOKEN'] = gitToken;
|
|
635
697
|
console.log(chalk_1.default.green('✓ GIT_TOKEN will be added to .env.genbox'));
|
|
636
698
|
}
|
|
@@ -672,19 +734,8 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
672
734
|
if (hasHttpsRepos) {
|
|
673
735
|
console.log('');
|
|
674
736
|
console.log(chalk_1.default.yellow('Private repositories require a GitHub token for cloning.'));
|
|
675
|
-
|
|
676
|
-
console.log(chalk_1.default.dim(' To create a token:'));
|
|
677
|
-
console.log(chalk_1.default.dim(' 1. Go to https://github.com/settings/tokens'));
|
|
678
|
-
console.log(chalk_1.default.dim(' 2. Click "Generate new token" → "Classic"'));
|
|
679
|
-
console.log(chalk_1.default.dim(' 3. Select scope: "repo" (Full control of private repositories)'));
|
|
680
|
-
console.log(chalk_1.default.dim(' 4. Generate and copy the token'));
|
|
681
|
-
console.log('');
|
|
682
|
-
let gitToken = await prompts.password({
|
|
683
|
-
message: 'GitHub Personal Access Token (leave empty to skip):',
|
|
684
|
-
});
|
|
737
|
+
const gitToken = await promptForSecret('GitHub Personal Access Token (leave empty to skip):', existingEnvValues['GIT_TOKEN'], { showInstructions: !existingEnvValues['GIT_TOKEN'] });
|
|
685
738
|
if (gitToken) {
|
|
686
|
-
// Strip any "GIT_TOKEN=" prefix if user pasted the whole line
|
|
687
|
-
gitToken = gitToken.replace(/^GIT_TOKEN=/i, '');
|
|
688
739
|
envVarsToAdd['GIT_TOKEN'] = gitToken;
|
|
689
740
|
console.log(chalk_1.default.green('✓ GIT_TOKEN will be added to .env.genbox'));
|
|
690
741
|
}
|
|
@@ -756,7 +807,7 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
756
807
|
// Environment configuration (skip only in non-interactive mode)
|
|
757
808
|
// For --from-scan, we still want to prompt for environments since they're required for genbox to work
|
|
758
809
|
if (!nonInteractive) {
|
|
759
|
-
const envConfig = await setupEnvironments(scan, v4Config, isMultiRepo);
|
|
810
|
+
const envConfig = await setupEnvironments(scan, v4Config, isMultiRepo, existingEnvValues);
|
|
760
811
|
if (envConfig) {
|
|
761
812
|
v4Config.environments = envConfig;
|
|
762
813
|
}
|
|
@@ -1032,7 +1083,7 @@ async function setupGitAuth(gitInfo, projectName) {
|
|
|
1032
1083
|
/**
|
|
1033
1084
|
* Setup staging/production environments (v4 format)
|
|
1034
1085
|
*/
|
|
1035
|
-
async function setupEnvironments(scan, config, isMultiRepo = false) {
|
|
1086
|
+
async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvValues = {}) {
|
|
1036
1087
|
const setupEnvs = await prompts.confirm({
|
|
1037
1088
|
message: 'Configure staging/production environments?',
|
|
1038
1089
|
default: true,
|
|
@@ -1046,6 +1097,9 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
|
|
|
1046
1097
|
console.log(chalk_1.default.dim('Actual secrets go in .env.genbox'));
|
|
1047
1098
|
console.log('');
|
|
1048
1099
|
const environments = {};
|
|
1100
|
+
// Get existing staging API URL if available
|
|
1101
|
+
const existingStagingApiUrl = existingEnvValues['STAGING_API_URL'];
|
|
1102
|
+
const existingProductionApiUrl = existingEnvValues['PRODUCTION_API_URL'] || existingEnvValues['PROD_API_URL'];
|
|
1049
1103
|
if (isMultiRepo) {
|
|
1050
1104
|
// For multi-repo: configure API URLs per backend app
|
|
1051
1105
|
const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
|
|
@@ -1053,6 +1107,20 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
|
|
|
1053
1107
|
console.log(chalk_1.default.dim('Configure staging API URLs for each backend service:'));
|
|
1054
1108
|
const urls = {};
|
|
1055
1109
|
for (const app of backendApps) {
|
|
1110
|
+
// Check for existing app-specific URL or use general staging URL for 'api' app
|
|
1111
|
+
const existingUrl = existingEnvValues[`STAGING_${app.name.toUpperCase()}_URL`] ||
|
|
1112
|
+
(app.name === 'api' ? existingStagingApiUrl : '');
|
|
1113
|
+
if (existingUrl) {
|
|
1114
|
+
console.log(chalk_1.default.dim(` Found existing value for ${app.name}: ${existingUrl}`));
|
|
1115
|
+
const useExisting = await prompts.confirm({
|
|
1116
|
+
message: ` Use existing ${app.name} staging URL?`,
|
|
1117
|
+
default: true,
|
|
1118
|
+
});
|
|
1119
|
+
if (useExisting) {
|
|
1120
|
+
urls[app.name] = existingUrl;
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1056
1124
|
const url = await prompts.input({
|
|
1057
1125
|
message: ` ${app.name} staging URL (leave empty to skip):`,
|
|
1058
1126
|
default: '',
|
|
@@ -1073,10 +1141,23 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
|
|
|
1073
1141
|
}
|
|
1074
1142
|
else {
|
|
1075
1143
|
// No backend apps, just ask for a single URL
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
default:
|
|
1079
|
-
|
|
1144
|
+
let stagingApiUrl = '';
|
|
1145
|
+
if (existingStagingApiUrl) {
|
|
1146
|
+
console.log(chalk_1.default.dim(` Found existing value: ${existingStagingApiUrl}`));
|
|
1147
|
+
const useExisting = await prompts.confirm({
|
|
1148
|
+
message: 'Use existing staging API URL?',
|
|
1149
|
+
default: true,
|
|
1150
|
+
});
|
|
1151
|
+
if (useExisting) {
|
|
1152
|
+
stagingApiUrl = existingStagingApiUrl;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
if (!stagingApiUrl) {
|
|
1156
|
+
stagingApiUrl = await prompts.input({
|
|
1157
|
+
message: 'Staging API URL (leave empty to skip):',
|
|
1158
|
+
default: '',
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1080
1161
|
if (stagingApiUrl) {
|
|
1081
1162
|
environments.staging = {
|
|
1082
1163
|
description: 'Staging environment',
|
|
@@ -1091,10 +1172,23 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
|
|
|
1091
1172
|
}
|
|
1092
1173
|
else {
|
|
1093
1174
|
// Single repo: simple single URL
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
default:
|
|
1097
|
-
|
|
1175
|
+
let stagingApiUrl = '';
|
|
1176
|
+
if (existingStagingApiUrl) {
|
|
1177
|
+
console.log(chalk_1.default.dim(` Found existing value: ${existingStagingApiUrl}`));
|
|
1178
|
+
const useExisting = await prompts.confirm({
|
|
1179
|
+
message: 'Use existing staging API URL?',
|
|
1180
|
+
default: true,
|
|
1181
|
+
});
|
|
1182
|
+
if (useExisting) {
|
|
1183
|
+
stagingApiUrl = existingStagingApiUrl;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
if (!stagingApiUrl) {
|
|
1187
|
+
stagingApiUrl = await prompts.input({
|
|
1188
|
+
message: 'Staging API URL (leave empty to skip):',
|
|
1189
|
+
default: '',
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1098
1192
|
if (stagingApiUrl) {
|
|
1099
1193
|
environments.staging = {
|
|
1100
1194
|
description: 'Staging environment',
|
|
@@ -1117,6 +1211,21 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
|
|
|
1117
1211
|
console.log(chalk_1.default.dim('Configure production API URLs for each backend service:'));
|
|
1118
1212
|
const prodUrls = {};
|
|
1119
1213
|
for (const app of backendApps) {
|
|
1214
|
+
// Check for existing app-specific URL or use general production URL for 'api' app
|
|
1215
|
+
const existingUrl = existingEnvValues[`PRODUCTION_${app.name.toUpperCase()}_URL`] ||
|
|
1216
|
+
existingEnvValues[`PROD_${app.name.toUpperCase()}_URL`] ||
|
|
1217
|
+
(app.name === 'api' ? existingProductionApiUrl : '');
|
|
1218
|
+
if (existingUrl) {
|
|
1219
|
+
console.log(chalk_1.default.dim(` Found existing value for ${app.name}: ${existingUrl}`));
|
|
1220
|
+
const useExisting = await prompts.confirm({
|
|
1221
|
+
message: ` Use existing ${app.name} production URL?`,
|
|
1222
|
+
default: true,
|
|
1223
|
+
});
|
|
1224
|
+
if (useExisting) {
|
|
1225
|
+
prodUrls[app.name] = existingUrl;
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1120
1229
|
const url = await prompts.input({
|
|
1121
1230
|
message: ` ${app.name} production URL:`,
|
|
1122
1231
|
default: '',
|
|
@@ -1139,10 +1248,23 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
|
|
|
1139
1248
|
}
|
|
1140
1249
|
}
|
|
1141
1250
|
else {
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
default:
|
|
1145
|
-
|
|
1251
|
+
let prodApiUrl = '';
|
|
1252
|
+
if (existingProductionApiUrl) {
|
|
1253
|
+
console.log(chalk_1.default.dim(` Found existing value: ${existingProductionApiUrl}`));
|
|
1254
|
+
const useExisting = await prompts.confirm({
|
|
1255
|
+
message: 'Use existing production API URL?',
|
|
1256
|
+
default: true,
|
|
1257
|
+
});
|
|
1258
|
+
if (useExisting) {
|
|
1259
|
+
prodApiUrl = existingProductionApiUrl;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (!prodApiUrl) {
|
|
1263
|
+
prodApiUrl = await prompts.input({
|
|
1264
|
+
message: 'Production API URL:',
|
|
1265
|
+
default: '',
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1146
1268
|
if (prodApiUrl) {
|
|
1147
1269
|
environments.production = {
|
|
1148
1270
|
description: 'Production (use with caution)',
|
package/dist/commands/scan.js
CHANGED
|
@@ -127,16 +127,20 @@ exports.scanCommand = new commander_1.Command('scan')
|
|
|
127
127
|
});
|
|
128
128
|
// Convert scan result to DetectedConfig format
|
|
129
129
|
let detected = convertScanToDetected(scan, cwd);
|
|
130
|
-
// Interactive mode: let user select apps
|
|
131
|
-
if (isInteractive
|
|
132
|
-
detected = await
|
|
130
|
+
// Interactive mode: let user select apps, scripts, infrastructure
|
|
131
|
+
if (isInteractive) {
|
|
132
|
+
detected = await interactiveSelection(detected);
|
|
133
133
|
}
|
|
134
134
|
// Scan env files for service URLs (only for selected frontend apps)
|
|
135
135
|
const frontendApps = Object.entries(detected.apps)
|
|
136
136
|
.filter(([, app]) => app.type === 'frontend')
|
|
137
137
|
.map(([name]) => name);
|
|
138
138
|
if (frontendApps.length > 0) {
|
|
139
|
-
|
|
139
|
+
let serviceUrls = scanEnvFilesForUrls(detected.apps, cwd);
|
|
140
|
+
// In interactive mode, let user select which URLs to configure
|
|
141
|
+
if (isInteractive && serviceUrls.length > 0) {
|
|
142
|
+
serviceUrls = await interactiveUrlSelection(serviceUrls);
|
|
143
|
+
}
|
|
140
144
|
if (serviceUrls.length > 0) {
|
|
141
145
|
detected.service_urls = serviceUrls;
|
|
142
146
|
}
|
|
@@ -160,44 +164,132 @@ exports.scanCommand = new commander_1.Command('scan')
|
|
|
160
164
|
}
|
|
161
165
|
});
|
|
162
166
|
/**
|
|
163
|
-
* Interactive app selection
|
|
167
|
+
* Interactive app and script selection
|
|
164
168
|
*/
|
|
165
|
-
async function
|
|
169
|
+
async function interactiveSelection(detected) {
|
|
170
|
+
let result = { ...detected };
|
|
171
|
+
// === App Selection ===
|
|
166
172
|
const appEntries = Object.entries(detected.apps);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
173
|
+
if (appEntries.length > 0) {
|
|
174
|
+
console.log(chalk_1.default.blue('=== Detected Apps ===\n'));
|
|
175
|
+
// Show detected apps
|
|
176
|
+
for (const [name, app] of appEntries) {
|
|
177
|
+
const parts = [
|
|
178
|
+
chalk_1.default.cyan(name),
|
|
179
|
+
app.type ? `(${app.type})` : '',
|
|
180
|
+
app.framework ? `[${app.framework}]` : '',
|
|
181
|
+
app.port ? `port:${app.port}` : '',
|
|
182
|
+
].filter(Boolean);
|
|
183
|
+
console.log(` ${parts.join(' ')}`);
|
|
184
|
+
if (app.git) {
|
|
185
|
+
console.log(chalk_1.default.dim(` └─ ${app.git.remote}`));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
console.log();
|
|
189
|
+
// Let user select which apps to include
|
|
190
|
+
const appChoices = appEntries.map(([name, app]) => ({
|
|
191
|
+
name: `${name} (${app.type || 'unknown'}${app.framework ? `, ${app.framework}` : ''})`,
|
|
192
|
+
value: name,
|
|
193
|
+
checked: app.type !== 'library', // Default: include non-libraries
|
|
194
|
+
}));
|
|
195
|
+
const selectedApps = await prompts.checkbox({
|
|
196
|
+
message: 'Select apps to include:',
|
|
197
|
+
choices: appChoices,
|
|
198
|
+
});
|
|
199
|
+
// Filter apps to only selected ones
|
|
200
|
+
const filteredApps = {};
|
|
201
|
+
for (const appName of selectedApps) {
|
|
202
|
+
filteredApps[appName] = detected.apps[appName];
|
|
203
|
+
}
|
|
204
|
+
result.apps = filteredApps;
|
|
205
|
+
}
|
|
206
|
+
// === Script Selection ===
|
|
207
|
+
if (detected.scripts && detected.scripts.length > 0) {
|
|
208
|
+
console.log('');
|
|
209
|
+
console.log(chalk_1.default.blue('=== Detected Scripts ===\n'));
|
|
210
|
+
// Group scripts by directory for display
|
|
211
|
+
const scriptsByDir = new Map();
|
|
212
|
+
for (const script of detected.scripts) {
|
|
213
|
+
const dir = script.path.includes('/') ? script.path.split('/')[0] : '(root)';
|
|
214
|
+
const existing = scriptsByDir.get(dir) || [];
|
|
215
|
+
existing.push(script);
|
|
216
|
+
scriptsByDir.set(dir, existing);
|
|
217
|
+
}
|
|
218
|
+
// Show grouped scripts
|
|
219
|
+
for (const [dir, scripts] of scriptsByDir) {
|
|
220
|
+
console.log(chalk_1.default.dim(` ${dir}/ (${scripts.length} scripts)`));
|
|
221
|
+
for (const script of scripts.slice(0, 3)) {
|
|
222
|
+
console.log(` ${chalk_1.default.cyan(script.name)} (${script.stage})`);
|
|
223
|
+
}
|
|
224
|
+
if (scripts.length > 3) {
|
|
225
|
+
console.log(chalk_1.default.dim(` ... and ${scripts.length - 3} more`));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
console.log();
|
|
229
|
+
// Let user select which scripts to include
|
|
230
|
+
const scriptChoices = detected.scripts.map(s => ({
|
|
231
|
+
name: `${s.path} (${s.stage})`,
|
|
232
|
+
value: s.path,
|
|
233
|
+
checked: s.path.startsWith('scripts/'), // Default: include scripts/ directory
|
|
234
|
+
}));
|
|
235
|
+
const selectedScripts = await prompts.checkbox({
|
|
236
|
+
message: 'Select scripts to include:',
|
|
237
|
+
choices: scriptChoices,
|
|
238
|
+
});
|
|
239
|
+
// Filter scripts to only selected ones
|
|
240
|
+
result.scripts = detected.scripts.filter(s => selectedScripts.includes(s.path));
|
|
241
|
+
}
|
|
242
|
+
// === Infrastructure Selection ===
|
|
243
|
+
if (detected.infrastructure && detected.infrastructure.length > 0) {
|
|
244
|
+
console.log('');
|
|
245
|
+
console.log(chalk_1.default.blue('=== Detected Infrastructure ===\n'));
|
|
246
|
+
for (const infra of detected.infrastructure) {
|
|
247
|
+
console.log(` ${chalk_1.default.cyan(infra.name)}: ${infra.type} (${infra.image})`);
|
|
179
248
|
}
|
|
249
|
+
console.log();
|
|
250
|
+
const infraChoices = detected.infrastructure.map(i => ({
|
|
251
|
+
name: `${i.name} (${i.type}, ${i.image})`,
|
|
252
|
+
value: i.name,
|
|
253
|
+
checked: true, // Default: include all
|
|
254
|
+
}));
|
|
255
|
+
const selectedInfra = await prompts.checkbox({
|
|
256
|
+
message: 'Select infrastructure to include:',
|
|
257
|
+
choices: infraChoices,
|
|
258
|
+
});
|
|
259
|
+
// Filter infrastructure to only selected ones
|
|
260
|
+
result.infrastructure = detected.infrastructure.filter(i => selectedInfra.includes(i.name));
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Interactive service URL selection
|
|
266
|
+
*/
|
|
267
|
+
async function interactiveUrlSelection(serviceUrls) {
|
|
268
|
+
if (serviceUrls.length === 0) {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
console.log('');
|
|
272
|
+
console.log(chalk_1.default.blue('=== Detected Service URLs ==='));
|
|
273
|
+
console.log(chalk_1.default.dim('These are local/development URLs found in frontend env files.'));
|
|
274
|
+
console.log(chalk_1.default.dim('Select which ones need staging URL equivalents.\n'));
|
|
275
|
+
// Show detected URLs
|
|
276
|
+
for (const svc of serviceUrls) {
|
|
277
|
+
console.log(` ${chalk_1.default.cyan(svc.base_url)}`);
|
|
278
|
+
console.log(chalk_1.default.dim(` Used by: ${svc.used_by.slice(0, 3).join(', ')}${svc.used_by.length > 3 ? ` +${svc.used_by.length - 3} more` : ''}`));
|
|
180
279
|
}
|
|
181
280
|
console.log();
|
|
182
|
-
// Let user select which
|
|
183
|
-
const
|
|
184
|
-
name: `${
|
|
185
|
-
value:
|
|
186
|
-
checked:
|
|
281
|
+
// Let user select which URLs to configure
|
|
282
|
+
const urlChoices = serviceUrls.map(svc => ({
|
|
283
|
+
name: `${svc.base_url} (${svc.used_by.length} var${svc.used_by.length > 1 ? 's' : ''})`,
|
|
284
|
+
value: svc.base_url,
|
|
285
|
+
checked: true, // Default: include all
|
|
187
286
|
}));
|
|
188
|
-
const
|
|
189
|
-
message: 'Select
|
|
190
|
-
choices,
|
|
287
|
+
const selectedUrls = await prompts.checkbox({
|
|
288
|
+
message: 'Select service URLs to configure for staging:',
|
|
289
|
+
choices: urlChoices,
|
|
191
290
|
});
|
|
192
|
-
// Filter
|
|
193
|
-
|
|
194
|
-
for (const appName of selectedApps) {
|
|
195
|
-
filteredApps[appName] = detected.apps[appName];
|
|
196
|
-
}
|
|
197
|
-
return {
|
|
198
|
-
...detected,
|
|
199
|
-
apps: filteredApps,
|
|
200
|
-
};
|
|
291
|
+
// Filter to selected URLs
|
|
292
|
+
return serviceUrls.filter(svc => selectedUrls.includes(svc.base_url));
|
|
201
293
|
}
|
|
202
294
|
/**
|
|
203
295
|
* Scan env files in app directories for service URLs
|
|
@@ -212,25 +304,37 @@ function scanEnvFilesForUrls(apps, rootDir) {
|
|
|
212
304
|
const appDir = path.join(rootDir, app.path);
|
|
213
305
|
// Find env file
|
|
214
306
|
let envContent;
|
|
215
|
-
let envSource;
|
|
216
307
|
for (const pattern of envPatterns) {
|
|
217
308
|
const envPath = path.join(appDir, pattern);
|
|
218
309
|
if (fs.existsSync(envPath)) {
|
|
219
310
|
envContent = fs.readFileSync(envPath, 'utf8');
|
|
220
|
-
envSource = pattern;
|
|
221
311
|
break;
|
|
222
312
|
}
|
|
223
313
|
}
|
|
224
314
|
if (!envContent)
|
|
225
315
|
continue;
|
|
226
|
-
//
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
316
|
+
// Process each line individually
|
|
317
|
+
for (const line of envContent.split('\n')) {
|
|
318
|
+
// Skip comments and empty lines
|
|
319
|
+
const trimmedLine = line.trim();
|
|
320
|
+
if (!trimmedLine || trimmedLine.startsWith('#'))
|
|
321
|
+
continue;
|
|
322
|
+
// Parse VAR=value format
|
|
323
|
+
const lineMatch = trimmedLine.match(/^([A-Z_][A-Z0-9_]*)=["']?(.+?)["']?$/);
|
|
324
|
+
if (!lineMatch)
|
|
325
|
+
continue;
|
|
326
|
+
const varName = lineMatch[1];
|
|
327
|
+
const value = lineMatch[2];
|
|
328
|
+
// Skip URLs with @ symbol (credentials, connection strings)
|
|
329
|
+
if (value.includes('@'))
|
|
330
|
+
continue;
|
|
331
|
+
// Check if it's a URL
|
|
332
|
+
const urlMatch = value.match(/^(https?:\/\/[a-zA-Z0-9_.-]+(?::\d+)?)/);
|
|
333
|
+
if (!urlMatch)
|
|
334
|
+
continue;
|
|
335
|
+
const baseUrl = urlMatch[1];
|
|
232
336
|
// Extract hostname
|
|
233
|
-
const hostMatch =
|
|
337
|
+
const hostMatch = baseUrl.match(/^https?:\/\/([a-zA-Z0-9_.-]+)/);
|
|
234
338
|
if (!hostMatch)
|
|
235
339
|
continue;
|
|
236
340
|
const hostname = hostMatch[1];
|
|
@@ -240,11 +344,6 @@ function scanEnvFilesForUrls(apps, rootDir) {
|
|
|
240
344
|
/^\d+\.\d+\.\d+\.\d+$/.test(hostname);
|
|
241
345
|
if (!isLocalUrl)
|
|
242
346
|
continue;
|
|
243
|
-
// Extract base URL
|
|
244
|
-
const baseMatch = fullUrl.match(/^(https?:\/\/[a-zA-Z0-9_.-]+(?::\d+)?)/);
|
|
245
|
-
if (!baseMatch)
|
|
246
|
-
continue;
|
|
247
|
-
const baseUrl = baseMatch[1];
|
|
248
347
|
// Add to map
|
|
249
348
|
if (!serviceUrls.has(baseUrl)) {
|
|
250
349
|
serviceUrls.set(baseUrl, { vars: new Set(), apps: new Set() });
|