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.
@@ -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
- console.log('');
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
- let gitToken = await prompts.password({
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
- console.log('');
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
- const stagingApiUrl = await prompts.input({
1077
- message: 'Staging API URL (leave empty to skip):',
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
- const stagingApiUrl = await prompts.input({
1095
- message: 'Staging API URL (leave empty to skip):',
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
- const prodApiUrl = await prompts.input({
1143
- message: 'Production API URL:',
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)',
@@ -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 && Object.keys(detected.apps).length > 0) {
132
- detected = await interactiveAppSelection(detected);
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
- const serviceUrls = scanEnvFilesForUrls(detected.apps, cwd);
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 interactiveAppSelection(detected) {
169
+ async function interactiveSelection(detected) {
170
+ let result = { ...detected };
171
+ // === App Selection ===
166
172
  const appEntries = Object.entries(detected.apps);
167
- console.log(chalk_1.default.blue('=== Detected Apps ===\n'));
168
- // Show detected apps
169
- for (const [name, app] of appEntries) {
170
- const parts = [
171
- chalk_1.default.cyan(name),
172
- app.type ? `(${app.type})` : '',
173
- app.framework ? `[${app.framework}]` : '',
174
- app.port ? `port:${app.port}` : '',
175
- ].filter(Boolean);
176
- console.log(` ${parts.join(' ')}`);
177
- if (app.git) {
178
- console.log(chalk_1.default.dim(` └─ ${app.git.remote}`));
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 apps to include
183
- const choices = appEntries.map(([name, app]) => ({
184
- name: `${name} (${app.type || 'unknown'}${app.framework ? `, ${app.framework}` : ''})`,
185
- value: name,
186
- checked: app.type !== 'library', // Default: include non-libraries
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 selectedApps = await prompts.checkbox({
189
- message: 'Select apps to include in detected.yaml:',
190
- choices,
287
+ const selectedUrls = await prompts.checkbox({
288
+ message: 'Select service URLs to configure for staging:',
289
+ choices: urlChoices,
191
290
  });
192
- // Filter apps to only selected ones
193
- const filteredApps = {};
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
- // Find all HTTP URLs
227
- const urlRegex = /^([A-Z_][A-Z0-9_]*)=["']?(https?:\/\/[a-zA-Z0-9_.-]+(?::\d+)?[^"'\s]*)["']?/gm;
228
- let match;
229
- while ((match = urlRegex.exec(envContent)) !== null) {
230
- const varName = match[1];
231
- const fullUrl = match[2];
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 = fullUrl.match(/^https?:\/\/([a-zA-Z0-9_.-]+)/);
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() });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {