genbox 1.0.4 → 1.0.5

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 +347 -113
  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)')
@@ -187,10 +260,52 @@ exports.initCommand = new commander_1.Command('init')
187
260
  v3Config.defaults = {};
188
261
  }
189
262
  v3Config.defaults.size = serverSize;
190
- // Git repository setup
191
- if (scan.git) {
263
+ // Git repository setup - different handling for multi-repo vs single-repo
264
+ const isMultiRepo = scan.structure.type === 'hybrid';
265
+ if (isMultiRepo) {
266
+ // Multi-repo workspace: detect git repos in app directories
267
+ const appGitRepos = detectAppGitRepos(scan.apps, process.cwd());
268
+ if (appGitRepos.length > 0 && !nonInteractive) {
269
+ console.log('');
270
+ console.log(chalk_1.default.blue('=== Git Repositories ==='));
271
+ console.log(chalk_1.default.dim(`Found ${appGitRepos.length} git repositories in app directories`));
272
+ const repoChoices = appGitRepos.map(repo => ({
273
+ name: `${repo.appName} - ${repo.remote}`,
274
+ value: repo.appName,
275
+ checked: true, // Default to include all
276
+ }));
277
+ const selectedRepos = await prompts.checkbox({
278
+ message: 'Select repositories to include:',
279
+ choices: repoChoices,
280
+ });
281
+ if (selectedRepos.length > 0) {
282
+ v3Config.repos = {};
283
+ for (const repoName of selectedRepos) {
284
+ const repo = appGitRepos.find(r => r.appName === repoName);
285
+ v3Config.repos[repo.appName] = {
286
+ url: repo.remote,
287
+ path: `/home/dev/${projectName}/${repo.appPath}`,
288
+ branch: repo.branch !== 'main' && repo.branch !== 'master' ? repo.branch : undefined,
289
+ auth: repo.type === 'ssh' ? 'ssh' : 'token',
290
+ };
291
+ }
292
+ }
293
+ }
294
+ else if (appGitRepos.length > 0) {
295
+ // Non-interactive: include all repos
296
+ v3Config.repos = {};
297
+ for (const repo of appGitRepos) {
298
+ v3Config.repos[repo.appName] = {
299
+ url: repo.remote,
300
+ path: `/home/dev/${projectName}/${repo.appPath}`,
301
+ auth: repo.type === 'ssh' ? 'ssh' : 'token',
302
+ };
303
+ }
304
+ }
305
+ }
306
+ else if (scan.git) {
307
+ // Single repo or monorepo with root git
192
308
  if (nonInteractive) {
193
- // Use detected git config with defaults
194
309
  const repoName = path_1.default.basename(scan.git.remote, '.git').replace(/.*[:/]/, '');
195
310
  v3Config.repos = {
196
311
  [repoName]: {
@@ -210,7 +325,8 @@ exports.initCommand = new commander_1.Command('init')
210
325
  }
211
326
  }
212
327
  }
213
- else if (!nonInteractive) {
328
+ else if (!nonInteractive && !isMultiRepo) {
329
+ // Only ask to add repo for non-multi-repo projects
214
330
  const addRepo = await prompts.confirm({
215
331
  message: 'No git remote detected. Add a repository?',
216
332
  default: false,
@@ -239,66 +355,55 @@ exports.initCommand = new commander_1.Command('init')
239
355
  }
240
356
  // Environment configuration (skip in non-interactive mode)
241
357
  if (!nonInteractive) {
242
- const envConfig = await setupEnvironments(scan, v3Config);
358
+ const envConfig = await setupEnvironments(scan, v3Config, isMultiRepo);
243
359
  if (envConfig) {
244
360
  v3Config.environments = envConfig;
245
361
  }
246
362
  }
247
- // Script selection (skip in non-interactive mode)
363
+ // Script selection - always show multi-select UI (skip in non-interactive mode)
248
364
  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
- }
365
+ // Scan for scripts
366
+ const scriptsSpinner = (0, ora_1.default)('Scanning for scripts...').start();
367
+ const fullScan = await scanner.scan(process.cwd(), { exclude, skipScripts: false });
368
+ scriptsSpinner.stop();
369
+ if (fullScan.scripts.length > 0) {
370
+ console.log('');
371
+ console.log(chalk_1.default.blue('=== Setup Scripts ==='));
372
+ // Group scripts by directory for display
373
+ const scriptsByDir = new Map();
374
+ for (const script of fullScan.scripts) {
375
+ const dir = script.path.includes('/') ? script.path.split('/')[0] : '(root)';
376
+ const existing = scriptsByDir.get(dir) || [];
377
+ existing.push(script);
378
+ scriptsByDir.set(dir, existing);
297
379
  }
298
- else {
299
- console.log(chalk_1.default.dim('No scripts found.'));
380
+ // Show grouped scripts
381
+ for (const [dir, scripts] of scriptsByDir) {
382
+ console.log(chalk_1.default.dim(` ${dir}/ (${scripts.length} scripts)`));
383
+ }
384
+ // Let user select scripts with multi-select
385
+ const scriptChoices = fullScan.scripts.map(s => ({
386
+ name: `${s.path} (${s.stage})`,
387
+ value: s.path,
388
+ checked: s.path.startsWith('scripts/'), // Default select scripts/ directory
389
+ }));
390
+ const selectedScripts = await prompts.checkbox({
391
+ message: 'Select scripts to include (space to toggle, enter to confirm):',
392
+ choices: scriptChoices,
393
+ });
394
+ if (selectedScripts.length > 0) {
395
+ v3Config.scripts = fullScan.scripts
396
+ .filter(s => selectedScripts.includes(s.path))
397
+ .map(s => ({
398
+ name: s.name,
399
+ path: s.path,
400
+ stage: s.stage,
401
+ }));
300
402
  }
301
403
  }
404
+ else {
405
+ console.log(chalk_1.default.dim('No scripts found.'));
406
+ }
302
407
  }
303
408
  // Save configuration
304
409
  const yamlContent = yaml.dump(v3Config, {
@@ -309,7 +414,7 @@ exports.initCommand = new commander_1.Command('init')
309
414
  fs_1.default.writeFileSync(configPath, yamlContent);
310
415
  console.log(chalk_1.default.green(`\n✔ Configuration saved to ${CONFIG_FILENAME}`));
311
416
  // Generate .env.genbox
312
- await setupEnvFile(projectName, v3Config, nonInteractive);
417
+ await setupEnvFile(projectName, v3Config, nonInteractive, scan, isMultiRepo);
313
418
  // Show warnings
314
419
  if (generated.warnings.length > 0) {
315
420
  console.log('');
@@ -480,7 +585,7 @@ async function setupGitAuth(gitInfo, projectName) {
480
585
  /**
481
586
  * Setup staging/production environments
482
587
  */
483
- async function setupEnvironments(scan, config) {
588
+ async function setupEnvironments(scan, config, isMultiRepo = false) {
484
589
  const setupEnvs = await prompts.confirm({
485
590
  message: 'Configure staging/production environments?',
486
591
  default: true,
@@ -493,37 +598,108 @@ async function setupEnvironments(scan, config) {
493
598
  console.log(chalk_1.default.dim('These URLs will be used when connecting to external services.'));
494
599
  console.log(chalk_1.default.dim('Actual secrets go in .env.genbox'));
495
600
  console.log('');
496
- const stagingApiUrl = await prompts.input({
497
- message: 'Staging API URL (leave empty to skip):',
498
- default: '',
499
- });
500
601
  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
- };
602
+ if (isMultiRepo) {
603
+ // For multi-repo: configure API URLs per backend app
604
+ const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
605
+ if (backendApps.length > 0) {
606
+ console.log(chalk_1.default.dim('Configure staging API URLs for each backend service:'));
607
+ const stagingApi = {};
608
+ for (const app of backendApps) {
609
+ const url = await prompts.input({
610
+ message: ` ${app.name} staging URL (leave empty to skip):`,
611
+ default: '',
612
+ });
613
+ if (url) {
614
+ stagingApi[app.name] = url;
615
+ }
616
+ }
617
+ if (Object.keys(stagingApi).length > 0) {
618
+ environments.staging = {
619
+ description: 'Staging environment',
620
+ api: stagingApi,
621
+ mongodb: { url: '${STAGING_MONGODB_URL}' },
622
+ redis: { url: '${STAGING_REDIS_URL}' },
623
+ };
624
+ }
625
+ }
626
+ else {
627
+ // No backend apps, just ask for a single URL
628
+ const stagingApiUrl = await prompts.input({
629
+ message: 'Staging API URL (leave empty to skip):',
630
+ default: '',
631
+ });
632
+ if (stagingApiUrl) {
633
+ environments.staging = {
634
+ description: 'Staging environment',
635
+ api: { gateway: stagingApiUrl },
636
+ mongodb: { url: '${STAGING_MONGODB_URL}' },
637
+ redis: { url: '${STAGING_REDIS_URL}' },
638
+ };
639
+ }
640
+ }
641
+ }
642
+ else {
643
+ // Single repo: simple single URL
644
+ const stagingApiUrl = await prompts.input({
645
+ message: 'Staging API URL (leave empty to skip):',
646
+ default: '',
647
+ });
648
+ if (stagingApiUrl) {
649
+ environments.staging = {
650
+ description: 'Staging environment',
651
+ api: { gateway: stagingApiUrl },
652
+ mongodb: { url: '${STAGING_MONGODB_URL}' },
653
+ redis: { url: '${STAGING_REDIS_URL}' },
654
+ };
655
+ }
508
656
  }
509
657
  const setupProd = await prompts.confirm({
510
658
  message: 'Also configure production environment?',
511
659
  default: false,
512
660
  });
513
661
  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
- };
662
+ if (isMultiRepo) {
663
+ const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
664
+ if (backendApps.length > 0) {
665
+ console.log(chalk_1.default.dim('Configure production API URLs for each backend service:'));
666
+ const prodApi = {};
667
+ for (const app of backendApps) {
668
+ const url = await prompts.input({
669
+ message: ` ${app.name} production URL:`,
670
+ default: '',
671
+ });
672
+ if (url) {
673
+ prodApi[app.name] = url;
674
+ }
675
+ }
676
+ if (Object.keys(prodApi).length > 0) {
677
+ environments.production = {
678
+ description: 'Production (use with caution)',
679
+ api: prodApi,
680
+ mongodb: {
681
+ url: '${PROD_MONGODB_URL}',
682
+ read_only: true,
683
+ },
684
+ };
685
+ }
686
+ }
687
+ }
688
+ else {
689
+ const prodApiUrl = await prompts.input({
690
+ message: 'Production API URL:',
691
+ default: '',
692
+ });
693
+ if (prodApiUrl) {
694
+ environments.production = {
695
+ description: 'Production (use with caution)',
696
+ api: { gateway: prodApiUrl },
697
+ mongodb: {
698
+ url: '${PROD_MONGODB_URL}',
699
+ read_only: true,
700
+ },
701
+ };
702
+ }
527
703
  }
528
704
  }
529
705
  return Object.keys(environments).length > 0 ? environments : undefined;
@@ -531,31 +707,89 @@ async function setupEnvironments(scan, config) {
531
707
  /**
532
708
  * Setup .env.genbox file
533
709
  */
534
- async function setupEnvFile(projectName, config, nonInteractive = false) {
710
+ async function setupEnvFile(projectName, config, nonInteractive = false, scan, isMultiRepo = false) {
535
711
  const envPath = path_1.default.join(process.cwd(), ENV_FILENAME);
536
712
  if (fs_1.default.existsSync(envPath)) {
537
713
  console.log(chalk_1.default.dim(` ${ENV_FILENAME} already exists, skipping...`));
538
714
  return;
539
715
  }
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;
716
+ // For multi-repo: find env files in app directories
717
+ if (isMultiRepo && scan) {
718
+ const appEnvFiles = findAppEnvFiles(scan.apps, process.cwd());
719
+ if (appEnvFiles.length > 0 && !nonInteractive) {
720
+ console.log('');
721
+ console.log(chalk_1.default.blue('=== Environment Files ==='));
722
+ console.log(chalk_1.default.dim(`Found .env files in ${appEnvFiles.length} app directories`));
723
+ const envChoices = appEnvFiles.map(env => ({
724
+ name: `${env.appName}/${env.envFile}`,
725
+ value: env.fullPath,
726
+ checked: true,
727
+ }));
728
+ const selectedEnvFiles = await prompts.checkbox({
729
+ message: 'Select .env files to merge into .env.genbox:',
730
+ choices: envChoices,
731
+ });
732
+ if (selectedEnvFiles.length > 0) {
733
+ let mergedContent = `# Genbox Environment Variables
734
+ # Merged from: ${selectedEnvFiles.map(f => path_1.default.relative(process.cwd(), f)).join(', ')}
735
+ # DO NOT COMMIT THIS FILE
736
+ #
737
+ # Add staging/production URLs:
738
+ # STAGING_MONGODB_URL=mongodb+srv://...
739
+ # STAGING_REDIS_URL=redis://...
740
+ # PROD_MONGODB_URL=mongodb+srv://...
741
+ #
742
+ # Git authentication:
743
+ # GIT_TOKEN=ghp_xxxxxxxxxxxx
744
+
745
+ `;
746
+ for (const envFilePath of selectedEnvFiles) {
747
+ const appInfo = appEnvFiles.find(e => e.fullPath === envFilePath);
748
+ const content = fs_1.default.readFileSync(envFilePath, 'utf8');
749
+ mergedContent += `\n# === ${appInfo?.appName || path_1.default.dirname(envFilePath)} ===\n`;
750
+ mergedContent += content;
751
+ mergedContent += '\n';
752
+ }
753
+ fs_1.default.writeFileSync(envPath, mergedContent);
754
+ console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${selectedEnvFiles.length} app env files`));
755
+ }
756
+ }
757
+ else if (appEnvFiles.length > 0 && nonInteractive) {
758
+ // Non-interactive: merge all env files
759
+ let mergedContent = `# Genbox Environment Variables
760
+ # Merged from app directories
761
+ # DO NOT COMMIT THIS FILE
762
+
763
+ `;
764
+ for (const envFile of appEnvFiles) {
765
+ const content = fs_1.default.readFileSync(envFile.fullPath, 'utf8');
766
+ mergedContent += `\n# === ${envFile.appName} ===\n`;
767
+ mergedContent += content;
768
+ mergedContent += '\n';
769
+ }
770
+ fs_1.default.writeFileSync(envPath, mergedContent);
771
+ console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${appEnvFiles.length} app env files`));
548
772
  }
549
773
  }
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
774
+ // If no env file created yet, check for root .env
775
+ if (!fs_1.default.existsSync(envPath)) {
776
+ const existingEnvFiles = ['.env.local', '.env', '.env.development'];
777
+ let existingEnvPath;
778
+ for (const envFile of existingEnvFiles) {
779
+ const fullPath = path_1.default.join(process.cwd(), envFile);
780
+ if (fs_1.default.existsSync(fullPath)) {
781
+ existingEnvPath = fullPath;
782
+ break;
783
+ }
784
+ }
785
+ if (existingEnvPath) {
786
+ const copyExisting = nonInteractive ? true : await prompts.confirm({
787
+ message: `Found ${path_1.default.basename(existingEnvPath)}. Copy to ${ENV_FILENAME}?`,
788
+ default: true,
789
+ });
790
+ if (copyExisting) {
791
+ const content = fs_1.default.readFileSync(existingEnvPath, 'utf8');
792
+ const header = `# Genbox Environment Variables
559
793
  # Generated from ${path_1.default.basename(existingEnvPath)}
560
794
  # DO NOT COMMIT THIS FILE
561
795
  #
@@ -568,20 +802,20 @@ async function setupEnvFile(projectName, config, nonInteractive = false) {
568
802
  # GIT_TOKEN=ghp_xxxxxxxxxxxx
569
803
 
570
804
  `;
571
- fs_1.default.writeFileSync(envPath, header + content);
572
- console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${path_1.default.basename(existingEnvPath)}`));
805
+ fs_1.default.writeFileSync(envPath, header + content);
806
+ console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${path_1.default.basename(existingEnvPath)}`));
807
+ }
573
808
  }
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`));
809
+ else {
810
+ const createEnv = nonInteractive ? true : await prompts.confirm({
811
+ message: `Create ${ENV_FILENAME} template?`,
812
+ default: true,
813
+ });
814
+ if (createEnv) {
815
+ const template = generateEnvTemplate(projectName, config);
816
+ fs_1.default.writeFileSync(envPath, template);
817
+ console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} template`));
818
+ }
585
819
  }
586
820
  }
587
821
  // 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.5",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {