genbox 1.0.16 → 1.0.17

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,9 +127,9 @@ 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)
@@ -160,44 +160,102 @@ exports.scanCommand = new commander_1.Command('scan')
160
160
  }
161
161
  });
162
162
  /**
163
- * Interactive app selection
163
+ * Interactive app and script selection
164
164
  */
165
- async function interactiveAppSelection(detected) {
165
+ async function interactiveSelection(detected) {
166
+ let result = { ...detected };
167
+ // === App Selection ===
166
168
  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}`));
169
+ if (appEntries.length > 0) {
170
+ console.log(chalk_1.default.blue('=== Detected Apps ===\n'));
171
+ // Show detected apps
172
+ for (const [name, app] of appEntries) {
173
+ const parts = [
174
+ chalk_1.default.cyan(name),
175
+ app.type ? `(${app.type})` : '',
176
+ app.framework ? `[${app.framework}]` : '',
177
+ app.port ? `port:${app.port}` : '',
178
+ ].filter(Boolean);
179
+ console.log(` ${parts.join(' ')}`);
180
+ if (app.git) {
181
+ console.log(chalk_1.default.dim(` └─ ${app.git.remote}`));
182
+ }
183
+ }
184
+ console.log();
185
+ // Let user select which apps to include
186
+ const appChoices = appEntries.map(([name, app]) => ({
187
+ name: `${name} (${app.type || 'unknown'}${app.framework ? `, ${app.framework}` : ''})`,
188
+ value: name,
189
+ checked: app.type !== 'library', // Default: include non-libraries
190
+ }));
191
+ const selectedApps = await prompts.checkbox({
192
+ message: 'Select apps to include:',
193
+ choices: appChoices,
194
+ });
195
+ // Filter apps to only selected ones
196
+ const filteredApps = {};
197
+ for (const appName of selectedApps) {
198
+ filteredApps[appName] = detected.apps[appName];
199
+ }
200
+ result.apps = filteredApps;
201
+ }
202
+ // === Script Selection ===
203
+ if (detected.scripts && detected.scripts.length > 0) {
204
+ console.log('');
205
+ console.log(chalk_1.default.blue('=== Detected Scripts ===\n'));
206
+ // Group scripts by directory for display
207
+ const scriptsByDir = new Map();
208
+ for (const script of detected.scripts) {
209
+ const dir = script.path.includes('/') ? script.path.split('/')[0] : '(root)';
210
+ const existing = scriptsByDir.get(dir) || [];
211
+ existing.push(script);
212
+ scriptsByDir.set(dir, existing);
213
+ }
214
+ // Show grouped scripts
215
+ for (const [dir, scripts] of scriptsByDir) {
216
+ console.log(chalk_1.default.dim(` ${dir}/ (${scripts.length} scripts)`));
217
+ for (const script of scripts.slice(0, 3)) {
218
+ console.log(` ${chalk_1.default.cyan(script.name)} (${script.stage})`);
219
+ }
220
+ if (scripts.length > 3) {
221
+ console.log(chalk_1.default.dim(` ... and ${scripts.length - 3} more`));
222
+ }
179
223
  }
224
+ console.log();
225
+ // Let user select which scripts to include
226
+ const scriptChoices = detected.scripts.map(s => ({
227
+ name: `${s.path} (${s.stage})`,
228
+ value: s.path,
229
+ checked: s.path.startsWith('scripts/'), // Default: include scripts/ directory
230
+ }));
231
+ const selectedScripts = await prompts.checkbox({
232
+ message: 'Select scripts to include:',
233
+ choices: scriptChoices,
234
+ });
235
+ // Filter scripts to only selected ones
236
+ result.scripts = detected.scripts.filter(s => selectedScripts.includes(s.path));
180
237
  }
181
- 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
187
- }));
188
- const selectedApps = await prompts.checkbox({
189
- message: 'Select apps to include in detected.yaml:',
190
- choices,
191
- });
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
- };
238
+ // === Infrastructure Selection ===
239
+ if (detected.infrastructure && detected.infrastructure.length > 0) {
240
+ console.log('');
241
+ console.log(chalk_1.default.blue('=== Detected Infrastructure ===\n'));
242
+ for (const infra of detected.infrastructure) {
243
+ console.log(` ${chalk_1.default.cyan(infra.name)}: ${infra.type} (${infra.image})`);
244
+ }
245
+ console.log();
246
+ const infraChoices = detected.infrastructure.map(i => ({
247
+ name: `${i.name} (${i.type}, ${i.image})`,
248
+ value: i.name,
249
+ checked: true, // Default: include all
250
+ }));
251
+ const selectedInfra = await prompts.checkbox({
252
+ message: 'Select infrastructure to include:',
253
+ choices: infraChoices,
254
+ });
255
+ // Filter infrastructure to only selected ones
256
+ result.infrastructure = detected.infrastructure.filter(i => selectedInfra.includes(i.name));
257
+ }
258
+ return result;
201
259
  }
202
260
  /**
203
261
  * Scan env files in app directories for service URLs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {