genbox 1.0.4 → 1.0.6

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.
Files changed (2) hide show
  1. package/dist/commands/init.js +371 -126
  2. package/package.json +1 -1
@@ -51,6 +51,79 @@ const config_generator_1 = require("../scanner/config-generator");
51
51
  const scan_1 = require("../scan");
52
52
  const CONFIG_FILENAME = 'genbox.yaml';
53
53
  const ENV_FILENAME = '.env.genbox';
54
+ /**
55
+ * Detect git repositories in app directories (for multi-repo workspaces)
56
+ */
57
+ function detectAppGitRepos(apps, rootDir) {
58
+ const { execSync } = require('child_process');
59
+ const repos = [];
60
+ for (const app of apps) {
61
+ const appDir = path_1.default.join(rootDir, app.path);
62
+ const gitDir = path_1.default.join(appDir, '.git');
63
+ if (!fs_1.default.existsSync(gitDir))
64
+ continue;
65
+ try {
66
+ const remote = execSync('git remote get-url origin', {
67
+ cwd: appDir,
68
+ stdio: 'pipe',
69
+ encoding: 'utf8',
70
+ }).trim();
71
+ if (!remote)
72
+ continue;
73
+ const isSSH = remote.startsWith('git@') || remote.startsWith('ssh://');
74
+ let provider = 'other';
75
+ if (remote.includes('github.com'))
76
+ provider = 'github';
77
+ else if (remote.includes('gitlab.com'))
78
+ provider = 'gitlab';
79
+ else if (remote.includes('bitbucket.org'))
80
+ provider = 'bitbucket';
81
+ let branch = 'main';
82
+ try {
83
+ branch = execSync('git rev-parse --abbrev-ref HEAD', {
84
+ cwd: appDir,
85
+ stdio: 'pipe',
86
+ encoding: 'utf8',
87
+ }).trim();
88
+ }
89
+ catch { }
90
+ repos.push({
91
+ appName: app.name,
92
+ appPath: app.path,
93
+ remote,
94
+ type: isSSH ? 'ssh' : 'https',
95
+ provider,
96
+ branch,
97
+ });
98
+ }
99
+ catch {
100
+ // No git remote in this directory
101
+ }
102
+ }
103
+ return repos;
104
+ }
105
+ /**
106
+ * Find .env files in app directories
107
+ */
108
+ function findAppEnvFiles(apps, rootDir) {
109
+ const envFiles = [];
110
+ const envPatterns = ['.env', '.env.local', '.env.development'];
111
+ for (const app of apps) {
112
+ const appDir = path_1.default.join(rootDir, app.path);
113
+ for (const pattern of envPatterns) {
114
+ const envPath = path_1.default.join(appDir, pattern);
115
+ if (fs_1.default.existsSync(envPath)) {
116
+ envFiles.push({
117
+ appName: app.name,
118
+ envFile: pattern,
119
+ fullPath: envPath,
120
+ });
121
+ break; // Only take the first match per app
122
+ }
123
+ }
124
+ }
125
+ return envFiles;
126
+ }
54
127
  exports.initCommand = new commander_1.Command('init')
55
128
  .description('Initialize a new Genbox configuration')
56
129
  .option('--v2', 'Use legacy v2 format (single-app only)')
@@ -80,24 +153,15 @@ exports.initCommand = new commander_1.Command('init')
80
153
  }
81
154
  console.log(chalk_1.default.blue('Initializing Genbox...'));
82
155
  console.log('');
83
- // Get directory exclusions
156
+ // Get initial exclusions from CLI options only
84
157
  let exclude = [];
85
158
  if (options.exclude) {
86
159
  exclude = options.exclude.split(',').map((d) => d.trim()).filter(Boolean);
87
160
  }
88
- else if (!nonInteractive) {
89
- const excludeDirs = await prompts.input({
90
- message: 'Directories to exclude (comma-separated, or empty to skip):',
91
- default: '',
92
- });
93
- exclude = excludeDirs
94
- ? excludeDirs.split(',').map((d) => d.trim()).filter(Boolean)
95
- : [];
96
- }
97
- // Scan project (skip scripts initially - we'll ask about them later)
161
+ // Scan project first (skip scripts initially)
98
162
  const spinner = (0, ora_1.default)('Scanning project...').start();
99
163
  const scanner = new scanner_1.ProjectScanner();
100
- const scan = await scanner.scan(process.cwd(), { exclude, skipScripts: true });
164
+ let scan = await scanner.scan(process.cwd(), { exclude, skipScripts: true });
101
165
  spinner.succeed('Project scanned');
102
166
  // Display scan results
103
167
  console.log('');
@@ -114,7 +178,27 @@ exports.initCommand = new commander_1.Command('init')
114
178
  const frameworkStr = scan.frameworks.map(f => f.name).join(', ');
115
179
  console.log(` ${chalk_1.default.dim('Frameworks:')} ${frameworkStr}`);
116
180
  }
117
- if (scan.apps.length > 0) {
181
+ // For multi-repo: show apps and let user select which to include
182
+ const isMultiRepoStructure = scan.structure.type === 'hybrid';
183
+ let selectedApps = scan.apps;
184
+ if (isMultiRepoStructure && scan.apps.length > 0 && !nonInteractive) {
185
+ console.log('');
186
+ console.log(chalk_1.default.blue('=== Apps Detected ==='));
187
+ const appChoices = scan.apps.map(app => ({
188
+ name: `${app.name} (${app.type}${app.framework ? `, ${app.framework}` : ''})`,
189
+ value: app.name,
190
+ checked: app.type !== 'library', // Default select non-libraries
191
+ }));
192
+ const selectedAppNames = await prompts.checkbox({
193
+ message: 'Select apps to include:',
194
+ choices: appChoices,
195
+ });
196
+ // Filter scan.apps to only selected ones
197
+ selectedApps = scan.apps.filter(a => selectedAppNames.includes(a.name));
198
+ // Update scan with filtered apps
199
+ scan = { ...scan, apps: selectedApps };
200
+ }
201
+ else if (scan.apps.length > 0) {
118
202
  console.log(` ${chalk_1.default.dim('Apps:')} ${scan.apps.length} discovered`);
119
203
  for (const app of scan.apps.slice(0, 5)) {
120
204
  console.log(` - ${app.name} (${app.type}${app.framework ? `, ${app.framework}` : ''})`);
@@ -187,10 +271,52 @@ exports.initCommand = new commander_1.Command('init')
187
271
  v3Config.defaults = {};
188
272
  }
189
273
  v3Config.defaults.size = serverSize;
190
- // Git repository setup
191
- if (scan.git) {
274
+ // Git repository setup - different handling for multi-repo vs single-repo
275
+ const isMultiRepo = isMultiRepoStructure;
276
+ if (isMultiRepo) {
277
+ // Multi-repo workspace: detect git repos in app directories
278
+ const appGitRepos = detectAppGitRepos(scan.apps, process.cwd());
279
+ if (appGitRepos.length > 0 && !nonInteractive) {
280
+ console.log('');
281
+ console.log(chalk_1.default.blue('=== Git Repositories ==='));
282
+ console.log(chalk_1.default.dim(`Found ${appGitRepos.length} git repositories in app directories`));
283
+ const repoChoices = appGitRepos.map(repo => ({
284
+ name: `${repo.appName} - ${repo.remote}`,
285
+ value: repo.appName,
286
+ checked: true, // Default to include all
287
+ }));
288
+ const selectedRepos = await prompts.checkbox({
289
+ message: 'Select repositories to include:',
290
+ choices: repoChoices,
291
+ });
292
+ if (selectedRepos.length > 0) {
293
+ v3Config.repos = {};
294
+ for (const repoName of selectedRepos) {
295
+ const repo = appGitRepos.find(r => r.appName === repoName);
296
+ v3Config.repos[repo.appName] = {
297
+ url: repo.remote,
298
+ path: `/home/dev/${projectName}/${repo.appPath}`,
299
+ branch: repo.branch !== 'main' && repo.branch !== 'master' ? repo.branch : undefined,
300
+ auth: repo.type === 'ssh' ? 'ssh' : 'token',
301
+ };
302
+ }
303
+ }
304
+ }
305
+ else if (appGitRepos.length > 0) {
306
+ // Non-interactive: include all repos
307
+ v3Config.repos = {};
308
+ for (const repo of appGitRepos) {
309
+ v3Config.repos[repo.appName] = {
310
+ url: repo.remote,
311
+ path: `/home/dev/${projectName}/${repo.appPath}`,
312
+ auth: repo.type === 'ssh' ? 'ssh' : 'token',
313
+ };
314
+ }
315
+ }
316
+ }
317
+ else if (scan.git) {
318
+ // Single repo or monorepo with root git
192
319
  if (nonInteractive) {
193
- // Use detected git config with defaults
194
320
  const repoName = path_1.default.basename(scan.git.remote, '.git').replace(/.*[:/]/, '');
195
321
  v3Config.repos = {
196
322
  [repoName]: {
@@ -210,7 +336,8 @@ exports.initCommand = new commander_1.Command('init')
210
336
  }
211
337
  }
212
338
  }
213
- else if (!nonInteractive) {
339
+ else if (!nonInteractive && !isMultiRepo) {
340
+ // Only ask to add repo for non-multi-repo projects
214
341
  const addRepo = await prompts.confirm({
215
342
  message: 'No git remote detected. Add a repository?',
216
343
  default: false,
@@ -239,66 +366,55 @@ exports.initCommand = new commander_1.Command('init')
239
366
  }
240
367
  // Environment configuration (skip in non-interactive mode)
241
368
  if (!nonInteractive) {
242
- const envConfig = await setupEnvironments(scan, v3Config);
369
+ const envConfig = await setupEnvironments(scan, v3Config, isMultiRepo);
243
370
  if (envConfig) {
244
371
  v3Config.environments = envConfig;
245
372
  }
246
373
  }
247
- // Script selection (skip in non-interactive mode)
374
+ // Script selection - always show multi-select UI (skip in non-interactive mode)
248
375
  if (!nonInteractive) {
249
- const includeScripts = await prompts.confirm({
250
- message: 'Include setup scripts in configuration?',
251
- default: false,
252
- });
253
- if (includeScripts) {
254
- // Scan for scripts now
255
- const scriptsSpinner = (0, ora_1.default)('Scanning for scripts...').start();
256
- const fullScan = await scanner.scan(process.cwd(), { exclude, skipScripts: false });
257
- scriptsSpinner.stop();
258
- if (fullScan.scripts.length > 0) {
259
- console.log(chalk_1.default.dim(`\nFound ${fullScan.scripts.length} scripts:`));
260
- // Group scripts by directory
261
- const scriptsByDir = new Map();
262
- for (const script of fullScan.scripts) {
263
- const dir = script.path.includes('/') ? script.path.split('/')[0] : '(root)';
264
- const existing = scriptsByDir.get(dir) || [];
265
- existing.push(script);
266
- scriptsByDir.set(dir, existing);
267
- }
268
- // Show grouped scripts
269
- for (const [dir, scripts] of scriptsByDir) {
270
- console.log(chalk_1.default.dim(` ${dir}/`));
271
- for (const s of scripts.slice(0, 5)) {
272
- console.log(chalk_1.default.dim(` - ${s.name}`));
273
- }
274
- if (scripts.length > 5) {
275
- console.log(chalk_1.default.dim(` ... and ${scripts.length - 5} more`));
276
- }
277
- }
278
- // Let user select scripts
279
- const scriptChoices = fullScan.scripts.map(s => ({
280
- name: `${s.path} (${s.stage})`,
281
- value: s.path,
282
- checked: s.path.startsWith('scripts/'), // Default select scripts/ directory
283
- }));
284
- const selectedScripts = await prompts.checkbox({
285
- message: 'Select scripts to include:',
286
- choices: scriptChoices,
287
- });
288
- if (selectedScripts.length > 0) {
289
- v3Config.scripts = fullScan.scripts
290
- .filter(s => selectedScripts.includes(s.path))
291
- .map(s => ({
292
- name: s.name,
293
- path: s.path,
294
- stage: s.stage,
295
- }));
296
- }
376
+ // Scan for scripts
377
+ const scriptsSpinner = (0, ora_1.default)('Scanning for scripts...').start();
378
+ const fullScan = await scanner.scan(process.cwd(), { exclude, skipScripts: false });
379
+ scriptsSpinner.stop();
380
+ if (fullScan.scripts.length > 0) {
381
+ console.log('');
382
+ console.log(chalk_1.default.blue('=== Setup Scripts ==='));
383
+ // Group scripts by directory for display
384
+ const scriptsByDir = new Map();
385
+ for (const script of fullScan.scripts) {
386
+ const dir = script.path.includes('/') ? script.path.split('/')[0] : '(root)';
387
+ const existing = scriptsByDir.get(dir) || [];
388
+ existing.push(script);
389
+ scriptsByDir.set(dir, existing);
297
390
  }
298
- else {
299
- console.log(chalk_1.default.dim('No scripts found.'));
391
+ // Show grouped scripts
392
+ for (const [dir, scripts] of scriptsByDir) {
393
+ console.log(chalk_1.default.dim(` ${dir}/ (${scripts.length} scripts)`));
394
+ }
395
+ // Let user select scripts with multi-select
396
+ const scriptChoices = fullScan.scripts.map(s => ({
397
+ name: `${s.path} (${s.stage})`,
398
+ value: s.path,
399
+ checked: s.path.startsWith('scripts/'), // Default select scripts/ directory
400
+ }));
401
+ const selectedScripts = await prompts.checkbox({
402
+ message: 'Select scripts to include (space to toggle, enter to confirm):',
403
+ choices: scriptChoices,
404
+ });
405
+ if (selectedScripts.length > 0) {
406
+ v3Config.scripts = fullScan.scripts
407
+ .filter(s => selectedScripts.includes(s.path))
408
+ .map(s => ({
409
+ name: s.name,
410
+ path: s.path,
411
+ stage: s.stage,
412
+ }));
300
413
  }
301
414
  }
415
+ else {
416
+ console.log(chalk_1.default.dim('No scripts found.'));
417
+ }
302
418
  }
303
419
  // Save configuration
304
420
  const yamlContent = yaml.dump(v3Config, {
@@ -309,7 +425,7 @@ exports.initCommand = new commander_1.Command('init')
309
425
  fs_1.default.writeFileSync(configPath, yamlContent);
310
426
  console.log(chalk_1.default.green(`\n✔ Configuration saved to ${CONFIG_FILENAME}`));
311
427
  // Generate .env.genbox
312
- await setupEnvFile(projectName, v3Config, nonInteractive);
428
+ await setupEnvFile(projectName, v3Config, nonInteractive, scan, isMultiRepo);
313
429
  // Show warnings
314
430
  if (generated.warnings.length > 0) {
315
431
  console.log('');
@@ -480,7 +596,7 @@ async function setupGitAuth(gitInfo, projectName) {
480
596
  /**
481
597
  * Setup staging/production environments
482
598
  */
483
- async function setupEnvironments(scan, config) {
599
+ async function setupEnvironments(scan, config, isMultiRepo = false) {
484
600
  const setupEnvs = await prompts.confirm({
485
601
  message: 'Configure staging/production environments?',
486
602
  default: true,
@@ -493,37 +609,108 @@ async function setupEnvironments(scan, config) {
493
609
  console.log(chalk_1.default.dim('These URLs will be used when connecting to external services.'));
494
610
  console.log(chalk_1.default.dim('Actual secrets go in .env.genbox'));
495
611
  console.log('');
496
- const stagingApiUrl = await prompts.input({
497
- message: 'Staging API URL (leave empty to skip):',
498
- default: '',
499
- });
500
612
  const environments = {};
501
- if (stagingApiUrl) {
502
- environments.staging = {
503
- description: 'Staging environment',
504
- api: { gateway: stagingApiUrl },
505
- mongodb: { url: '${STAGING_MONGODB_URL}' },
506
- redis: { url: '${STAGING_REDIS_URL}' },
507
- };
613
+ if (isMultiRepo) {
614
+ // For multi-repo: configure API URLs per backend app
615
+ const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
616
+ if (backendApps.length > 0) {
617
+ console.log(chalk_1.default.dim('Configure staging API URLs for each backend service:'));
618
+ const stagingApi = {};
619
+ for (const app of backendApps) {
620
+ const url = await prompts.input({
621
+ message: ` ${app.name} staging URL (leave empty to skip):`,
622
+ default: '',
623
+ });
624
+ if (url) {
625
+ stagingApi[app.name] = url;
626
+ }
627
+ }
628
+ if (Object.keys(stagingApi).length > 0) {
629
+ environments.staging = {
630
+ description: 'Staging environment',
631
+ api: stagingApi,
632
+ mongodb: { url: '${STAGING_MONGODB_URL}' },
633
+ redis: { url: '${STAGING_REDIS_URL}' },
634
+ };
635
+ }
636
+ }
637
+ else {
638
+ // No backend apps, just ask for a single URL
639
+ const stagingApiUrl = await prompts.input({
640
+ message: 'Staging API URL (leave empty to skip):',
641
+ default: '',
642
+ });
643
+ if (stagingApiUrl) {
644
+ environments.staging = {
645
+ description: 'Staging environment',
646
+ api: { gateway: stagingApiUrl },
647
+ mongodb: { url: '${STAGING_MONGODB_URL}' },
648
+ redis: { url: '${STAGING_REDIS_URL}' },
649
+ };
650
+ }
651
+ }
652
+ }
653
+ else {
654
+ // Single repo: simple single URL
655
+ const stagingApiUrl = await prompts.input({
656
+ message: 'Staging API URL (leave empty to skip):',
657
+ default: '',
658
+ });
659
+ if (stagingApiUrl) {
660
+ environments.staging = {
661
+ description: 'Staging environment',
662
+ api: { gateway: stagingApiUrl },
663
+ mongodb: { url: '${STAGING_MONGODB_URL}' },
664
+ redis: { url: '${STAGING_REDIS_URL}' },
665
+ };
666
+ }
508
667
  }
509
668
  const setupProd = await prompts.confirm({
510
669
  message: 'Also configure production environment?',
511
670
  default: false,
512
671
  });
513
672
  if (setupProd) {
514
- const prodApiUrl = await prompts.input({
515
- message: 'Production API URL:',
516
- default: '',
517
- });
518
- if (prodApiUrl) {
519
- environments.production = {
520
- description: 'Production (use with caution)',
521
- api: { gateway: prodApiUrl },
522
- mongodb: {
523
- url: '${PROD_MONGODB_URL}',
524
- read_only: true,
525
- },
526
- };
673
+ if (isMultiRepo) {
674
+ const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
675
+ if (backendApps.length > 0) {
676
+ console.log(chalk_1.default.dim('Configure production API URLs for each backend service:'));
677
+ const prodApi = {};
678
+ for (const app of backendApps) {
679
+ const url = await prompts.input({
680
+ message: ` ${app.name} production URL:`,
681
+ default: '',
682
+ });
683
+ if (url) {
684
+ prodApi[app.name] = url;
685
+ }
686
+ }
687
+ if (Object.keys(prodApi).length > 0) {
688
+ environments.production = {
689
+ description: 'Production (use with caution)',
690
+ api: prodApi,
691
+ mongodb: {
692
+ url: '${PROD_MONGODB_URL}',
693
+ read_only: true,
694
+ },
695
+ };
696
+ }
697
+ }
698
+ }
699
+ else {
700
+ const prodApiUrl = await prompts.input({
701
+ message: 'Production API URL:',
702
+ default: '',
703
+ });
704
+ if (prodApiUrl) {
705
+ environments.production = {
706
+ description: 'Production (use with caution)',
707
+ api: { gateway: prodApiUrl },
708
+ mongodb: {
709
+ url: '${PROD_MONGODB_URL}',
710
+ read_only: true,
711
+ },
712
+ };
713
+ }
527
714
  }
528
715
  }
529
716
  return Object.keys(environments).length > 0 ? environments : undefined;
@@ -531,31 +718,89 @@ async function setupEnvironments(scan, config) {
531
718
  /**
532
719
  * Setup .env.genbox file
533
720
  */
534
- async function setupEnvFile(projectName, config, nonInteractive = false) {
721
+ async function setupEnvFile(projectName, config, nonInteractive = false, scan, isMultiRepo = false) {
535
722
  const envPath = path_1.default.join(process.cwd(), ENV_FILENAME);
536
723
  if (fs_1.default.existsSync(envPath)) {
537
724
  console.log(chalk_1.default.dim(` ${ENV_FILENAME} already exists, skipping...`));
538
725
  return;
539
726
  }
540
- // Check for existing .env
541
- const existingEnvFiles = ['.env.local', '.env', '.env.development'];
542
- let existingEnvPath;
543
- for (const envFile of existingEnvFiles) {
544
- const fullPath = path_1.default.join(process.cwd(), envFile);
545
- if (fs_1.default.existsSync(fullPath)) {
546
- existingEnvPath = fullPath;
547
- break;
727
+ // For multi-repo: find env files in app directories
728
+ if (isMultiRepo && scan) {
729
+ const appEnvFiles = findAppEnvFiles(scan.apps, process.cwd());
730
+ if (appEnvFiles.length > 0 && !nonInteractive) {
731
+ console.log('');
732
+ console.log(chalk_1.default.blue('=== Environment Files ==='));
733
+ console.log(chalk_1.default.dim(`Found .env files in ${appEnvFiles.length} app directories`));
734
+ const envChoices = appEnvFiles.map(env => ({
735
+ name: `${env.appName}/${env.envFile}`,
736
+ value: env.fullPath,
737
+ checked: true,
738
+ }));
739
+ const selectedEnvFiles = await prompts.checkbox({
740
+ message: 'Select .env files to merge into .env.genbox:',
741
+ choices: envChoices,
742
+ });
743
+ if (selectedEnvFiles.length > 0) {
744
+ let mergedContent = `# Genbox Environment Variables
745
+ # Merged from: ${selectedEnvFiles.map(f => path_1.default.relative(process.cwd(), f)).join(', ')}
746
+ # DO NOT COMMIT THIS FILE
747
+ #
748
+ # Add staging/production URLs:
749
+ # STAGING_MONGODB_URL=mongodb+srv://...
750
+ # STAGING_REDIS_URL=redis://...
751
+ # PROD_MONGODB_URL=mongodb+srv://...
752
+ #
753
+ # Git authentication:
754
+ # GIT_TOKEN=ghp_xxxxxxxxxxxx
755
+
756
+ `;
757
+ for (const envFilePath of selectedEnvFiles) {
758
+ const appInfo = appEnvFiles.find(e => e.fullPath === envFilePath);
759
+ const content = fs_1.default.readFileSync(envFilePath, 'utf8');
760
+ mergedContent += `\n# === ${appInfo?.appName || path_1.default.dirname(envFilePath)} ===\n`;
761
+ mergedContent += content;
762
+ mergedContent += '\n';
763
+ }
764
+ fs_1.default.writeFileSync(envPath, mergedContent);
765
+ console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${selectedEnvFiles.length} app env files`));
766
+ }
767
+ }
768
+ else if (appEnvFiles.length > 0 && nonInteractive) {
769
+ // Non-interactive: merge all env files
770
+ let mergedContent = `# Genbox Environment Variables
771
+ # Merged from app directories
772
+ # DO NOT COMMIT THIS FILE
773
+
774
+ `;
775
+ for (const envFile of appEnvFiles) {
776
+ const content = fs_1.default.readFileSync(envFile.fullPath, 'utf8');
777
+ mergedContent += `\n# === ${envFile.appName} ===\n`;
778
+ mergedContent += content;
779
+ mergedContent += '\n';
780
+ }
781
+ fs_1.default.writeFileSync(envPath, mergedContent);
782
+ console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${appEnvFiles.length} app env files`));
548
783
  }
549
784
  }
550
- if (existingEnvPath) {
551
- // In non-interactive mode, default to copying existing env
552
- const copyExisting = nonInteractive ? true : await prompts.confirm({
553
- message: `Found ${path_1.default.basename(existingEnvPath)}. Copy to ${ENV_FILENAME}?`,
554
- default: true,
555
- });
556
- if (copyExisting) {
557
- const content = fs_1.default.readFileSync(existingEnvPath, 'utf8');
558
- const header = `# Genbox Environment Variables
785
+ // If no env file created yet, check for root .env
786
+ if (!fs_1.default.existsSync(envPath)) {
787
+ const existingEnvFiles = ['.env.local', '.env', '.env.development'];
788
+ let existingEnvPath;
789
+ for (const envFile of existingEnvFiles) {
790
+ const fullPath = path_1.default.join(process.cwd(), envFile);
791
+ if (fs_1.default.existsSync(fullPath)) {
792
+ existingEnvPath = fullPath;
793
+ break;
794
+ }
795
+ }
796
+ if (existingEnvPath) {
797
+ const copyExisting = nonInteractive ? true : await prompts.confirm({
798
+ message: `Found ${path_1.default.basename(existingEnvPath)}. Copy to ${ENV_FILENAME}?`,
799
+ default: true,
800
+ });
801
+ if (copyExisting) {
802
+ const content = fs_1.default.readFileSync(existingEnvPath, 'utf8');
803
+ const header = `# Genbox Environment Variables
559
804
  # Generated from ${path_1.default.basename(existingEnvPath)}
560
805
  # DO NOT COMMIT THIS FILE
561
806
  #
@@ -568,20 +813,20 @@ async function setupEnvFile(projectName, config, nonInteractive = false) {
568
813
  # GIT_TOKEN=ghp_xxxxxxxxxxxx
569
814
 
570
815
  `;
571
- fs_1.default.writeFileSync(envPath, header + content);
572
- console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${path_1.default.basename(existingEnvPath)}`));
816
+ fs_1.default.writeFileSync(envPath, header + content);
817
+ console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${path_1.default.basename(existingEnvPath)}`));
818
+ }
573
819
  }
574
- }
575
- else {
576
- // In non-interactive mode, default to creating template
577
- const createEnv = nonInteractive ? true : await prompts.confirm({
578
- message: `Create ${ENV_FILENAME} template?`,
579
- default: true,
580
- });
581
- if (createEnv) {
582
- const template = generateEnvTemplate(projectName, config);
583
- fs_1.default.writeFileSync(envPath, template);
584
- console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} template`));
820
+ else {
821
+ const createEnv = nonInteractive ? true : await prompts.confirm({
822
+ message: `Create ${ENV_FILENAME} template?`,
823
+ default: true,
824
+ });
825
+ if (createEnv) {
826
+ const template = generateEnvTemplate(projectName, config);
827
+ fs_1.default.writeFileSync(envPath, template);
828
+ console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} template`));
829
+ }
585
830
  }
586
831
  }
587
832
  // Add to .gitignore
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {