genbox 1.0.11 → 1.0.12

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.
@@ -348,6 +348,64 @@ function displayResolvedConfig(resolved) {
348
348
  console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
349
349
  console.log('');
350
350
  }
351
+ /**
352
+ * Parse .env.genbox file into segregated sections
353
+ */
354
+ function parseEnvGenboxSections(content) {
355
+ const sections = new Map();
356
+ let currentSection = 'GLOBAL';
357
+ let currentContent = [];
358
+ for (const line of content.split('\n')) {
359
+ const sectionMatch = line.match(/^# === ([^=]+) ===$/);
360
+ if (sectionMatch) {
361
+ // Save previous section
362
+ if (currentContent.length > 0) {
363
+ sections.set(currentSection, currentContent.join('\n').trim());
364
+ }
365
+ currentSection = sectionMatch[1].trim();
366
+ currentContent = [];
367
+ }
368
+ else if (currentSection !== 'END') {
369
+ currentContent.push(line);
370
+ }
371
+ }
372
+ // Save last section
373
+ if (currentContent.length > 0 && currentSection !== 'END') {
374
+ sections.set(currentSection, currentContent.join('\n').trim());
375
+ }
376
+ return sections;
377
+ }
378
+ /**
379
+ * Build env content for a specific app by combining GLOBAL + app-specific sections
380
+ */
381
+ function buildAppEnvContent(sections, appName, apiUrl) {
382
+ const parts = [];
383
+ // Always include GLOBAL section
384
+ const globalSection = sections.get('GLOBAL');
385
+ if (globalSection) {
386
+ parts.push(globalSection);
387
+ }
388
+ // Include app-specific section if exists
389
+ const appSection = sections.get(appName);
390
+ if (appSection) {
391
+ parts.push(appSection);
392
+ }
393
+ let envContent = parts.join('\n\n');
394
+ // Expand ${API_URL} references
395
+ envContent = envContent.replace(/\$\{API_URL\}/g, apiUrl);
396
+ // Keep only actual env vars (filter out pure comment lines but keep var definitions)
397
+ envContent = envContent
398
+ .split('\n')
399
+ .filter(line => {
400
+ const trimmed = line.trim();
401
+ // Keep empty lines, lines with = (even if commented), and non-comment lines
402
+ return trimmed === '' || trimmed.includes('=') || !trimmed.startsWith('#');
403
+ })
404
+ .join('\n')
405
+ .replace(/\n{3,}/g, '\n\n')
406
+ .trim();
407
+ return envContent;
408
+ }
351
409
  /**
352
410
  * Build API payload from resolved config
353
411
  */
@@ -368,24 +426,82 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
368
426
  }
369
427
  // Build files bundle
370
428
  const files = [];
429
+ // Track env files to move in setup script (staging approach to avoid blocking git clone)
430
+ const envFilesToMove = [];
371
431
  // Send .env.genbox content to server for each app
372
432
  const envGenboxPath = path.join(process.cwd(), '.env.genbox');
373
433
  if (fs.existsSync(envGenboxPath)) {
374
- const envContent = fs.readFileSync(envGenboxPath, 'utf-8');
375
- // Add env file for each app (user should have already updated URLs manually)
434
+ const rawEnvContent = fs.readFileSync(envGenboxPath, 'utf-8');
435
+ // Parse into sections
436
+ const sections = parseEnvGenboxSections(rawEnvContent);
437
+ // Parse GLOBAL section to get API URL values
438
+ const globalSection = sections.get('GLOBAL') || '';
439
+ const envVarsFromFile = {};
440
+ for (const line of globalSection.split('\n')) {
441
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
442
+ if (match) {
443
+ let value = match[2].trim();
444
+ // Remove quotes if present
445
+ if ((value.startsWith('"') && value.endsWith('"')) ||
446
+ (value.startsWith("'") && value.endsWith("'"))) {
447
+ value = value.slice(1, -1);
448
+ }
449
+ envVarsFromFile[match[1]] = value;
450
+ }
451
+ }
452
+ // Determine API_URL based on profile's connect_to setting
453
+ const connectTo = resolved.profile ?
454
+ (config.profiles?.[resolved.profile]?.connect_to) : undefined;
455
+ let apiUrl;
456
+ if (connectTo) {
457
+ // Use the environment-specific API URL (e.g., STAGING_API_URL)
458
+ const envApiVarName = `${connectTo.toUpperCase()}_API_URL`;
459
+ apiUrl = envVarsFromFile[envApiVarName] || resolved.env['API_URL'] || 'http://localhost:3050';
460
+ }
461
+ else {
462
+ // Use local API URL
463
+ apiUrl = envVarsFromFile['LOCAL_API_URL'] || 'http://localhost:3050';
464
+ }
465
+ // Add env file for each app - filtered by selected apps only
376
466
  for (const app of resolved.apps) {
377
467
  const appPath = config.apps[app.name]?.path || app.name;
378
468
  const repoPath = resolved.repos.find(r => r.name === app.name)?.path ||
379
469
  (resolved.repos[0]?.path ? `${resolved.repos[0].path}/${appPath}` : `/home/dev/${config.project.name}/${appPath}`);
380
- files.push({
381
- path: `${repoPath}/.env`,
382
- content: envContent,
383
- permissions: '0644',
384
- });
470
+ // Check if this app has microservices (sections like api/gateway, api/auth)
471
+ const servicesSections = Array.from(sections.keys()).filter(s => s.startsWith(`${app.name}/`));
472
+ if (servicesSections.length > 0) {
473
+ // App has microservices - create env file for each service
474
+ for (const serviceSectionName of servicesSections) {
475
+ const serviceName = serviceSectionName.split('/')[1];
476
+ // Build service-specific env content (GLOBAL + service section)
477
+ const serviceEnvContent = buildAppEnvContent(sections, serviceSectionName, apiUrl);
478
+ const stagingName = `${app.name}-${serviceName}.env`;
479
+ const targetPath = `${repoPath}/apps/${serviceName}/.env`;
480
+ files.push({
481
+ path: `/home/dev/.env-staging/${stagingName}`,
482
+ content: serviceEnvContent,
483
+ permissions: '0644',
484
+ });
485
+ envFilesToMove.push({ stagingName, targetPath });
486
+ }
487
+ }
488
+ else {
489
+ // Regular app - build app-specific env content (GLOBAL + app section)
490
+ const appEnvContent = buildAppEnvContent(sections, app.name, apiUrl);
491
+ files.push({
492
+ path: `/home/dev/.env-staging/${app.name}.env`,
493
+ content: appEnvContent,
494
+ permissions: '0644',
495
+ });
496
+ envFilesToMove.push({
497
+ stagingName: `${app.name}.env`,
498
+ targetPath: `${repoPath}/.env`,
499
+ });
500
+ }
385
501
  }
386
502
  }
387
503
  // Add setup script if generated
388
- const setupScript = generateSetupScript(resolved, config);
504
+ const setupScript = generateSetupScript(resolved, config, envFilesToMove);
389
505
  if (setupScript) {
390
506
  files.push({
391
507
  path: '/home/dev/setup-genbox.sh',
@@ -431,13 +547,27 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
431
547
  /**
432
548
  * Generate setup script
433
549
  */
434
- function generateSetupScript(resolved, config) {
550
+ function generateSetupScript(resolved, config, envFilesToMove = []) {
435
551
  const lines = [
436
552
  '#!/bin/bash',
437
553
  '# Generated by genbox create',
438
554
  'set -e',
439
555
  '',
440
556
  ];
557
+ // Move .env files from staging to their correct locations
558
+ // This runs after git clone has completed
559
+ if (envFilesToMove.length > 0) {
560
+ lines.push('# Move .env files from staging to app directories');
561
+ for (const { stagingName, targetPath } of envFilesToMove) {
562
+ lines.push(`if [ -f "/home/dev/.env-staging/${stagingName}" ]; then`);
563
+ lines.push(` mkdir -p "$(dirname "${targetPath}")"`);
564
+ lines.push(` mv "/home/dev/.env-staging/${stagingName}" "${targetPath}"`);
565
+ lines.push(` echo "Moved .env to ${targetPath}"`);
566
+ lines.push('fi');
567
+ }
568
+ lines.push('rm -rf /home/dev/.env-staging 2>/dev/null || true');
569
+ lines.push('');
570
+ }
441
571
  // Change to project directory
442
572
  if (resolved.repos.length > 0) {
443
573
  lines.push(`cd ${resolved.repos[0].path} || exit 1`);
@@ -103,13 +103,15 @@ function detectAppGitRepos(apps, rootDir) {
103
103
  return repos;
104
104
  }
105
105
  /**
106
- * Find .env files in app directories
106
+ * Find .env files in app directories (including nested microservices)
107
107
  */
108
108
  function findAppEnvFiles(apps, rootDir) {
109
109
  const envFiles = [];
110
110
  const envPatterns = ['.env', '.env.local', '.env.development'];
111
111
  for (const app of apps) {
112
112
  const appDir = path_1.default.join(rootDir, app.path);
113
+ // Check for direct .env file in app directory
114
+ let foundDirectEnv = false;
113
115
  for (const pattern of envPatterns) {
114
116
  const envPath = path_1.default.join(appDir, pattern);
115
117
  if (fs_1.default.existsSync(envPath)) {
@@ -118,7 +120,35 @@ function findAppEnvFiles(apps, rootDir) {
118
120
  envFile: pattern,
119
121
  fullPath: envPath,
120
122
  });
121
- break; // Only take the first match per app
123
+ foundDirectEnv = true;
124
+ break;
125
+ }
126
+ }
127
+ // Check for nested microservices (e.g., api/apps/*)
128
+ const appsSubdir = path_1.default.join(appDir, 'apps');
129
+ if (fs_1.default.existsSync(appsSubdir) && fs_1.default.statSync(appsSubdir).isDirectory()) {
130
+ try {
131
+ const services = fs_1.default.readdirSync(appsSubdir);
132
+ for (const service of services) {
133
+ const serviceDir = path_1.default.join(appsSubdir, service);
134
+ if (!fs_1.default.statSync(serviceDir).isDirectory())
135
+ continue;
136
+ for (const pattern of envPatterns) {
137
+ const envPath = path_1.default.join(serviceDir, pattern);
138
+ if (fs_1.default.existsSync(envPath)) {
139
+ envFiles.push({
140
+ appName: `${app.name}/${service}`,
141
+ envFile: pattern,
142
+ fullPath: envPath,
143
+ isService: true,
144
+ });
145
+ break;
146
+ }
147
+ }
148
+ }
149
+ }
150
+ catch {
151
+ // Ignore errors reading subdirectories
122
152
  }
123
153
  }
124
154
  }
@@ -454,6 +484,20 @@ exports.initCommand = new commander_1.Command('init')
454
484
  });
455
485
  fs_1.default.writeFileSync(configPath, yamlContent);
456
486
  console.log(chalk_1.default.green(`\n✔ Configuration saved to ${CONFIG_FILENAME}`));
487
+ // Add API URLs from environments to envVarsToAdd
488
+ // Always add LOCAL_API_URL for local development
489
+ envVarsToAdd['LOCAL_API_URL'] = 'http://localhost:3050';
490
+ if (v3Config.environments) {
491
+ for (const [envName, envConfig] of Object.entries(v3Config.environments)) {
492
+ const apiUrl = envConfig.api?.api ||
493
+ envConfig.api?.url ||
494
+ envConfig.api?.gateway;
495
+ if (apiUrl) {
496
+ const varName = `${envName.toUpperCase()}_API_URL`;
497
+ envVarsToAdd[varName] = apiUrl;
498
+ }
499
+ }
500
+ }
457
501
  // Generate .env.genbox
458
502
  await setupEnvFile(projectName, v3Config, nonInteractive, scan, isMultiRepo, envVarsToAdd, overwriteExisting);
459
503
  // Show warnings
@@ -467,26 +511,35 @@ exports.initCommand = new commander_1.Command('init')
467
511
  // Show API URL guidance if environments are configured
468
512
  if (v3Config.environments && Object.keys(v3Config.environments).length > 0) {
469
513
  console.log('');
470
- console.log(chalk_1.default.yellow('Important: Update API URLs in .env.genbox for remote environments'));
514
+ console.log(chalk_1.default.blue('=== API URL Configuration ==='));
515
+ console.log(chalk_1.default.dim('The following API URLs were added to .env.genbox:'));
471
516
  console.log('');
517
+ console.log(chalk_1.default.dim(' LOCAL_API_URL=http://localhost:3050'));
472
518
  for (const [envName, envConfig] of Object.entries(v3Config.environments)) {
473
519
  const apiUrl = envConfig.api?.api ||
474
520
  envConfig.api?.url ||
475
521
  envConfig.api?.gateway;
476
522
  if (apiUrl) {
477
- console.log(chalk_1.default.dim(` For ${envName} profiles (connect_to: ${envName}):`));
478
- console.log(chalk_1.default.cyan(` VITE_API_BASE_URL="${apiUrl}"`));
479
- console.log(chalk_1.default.cyan(` VITE_AUTH_SERVICE_URL="${apiUrl}"`));
480
- console.log(chalk_1.default.cyan(` NEXT_PUBLIC_API_BASE_URL="${apiUrl}"`));
481
- console.log('');
523
+ const varName = `${envName.toUpperCase()}_API_URL`;
524
+ console.log(chalk_1.default.dim(` ${varName}=${apiUrl}`));
482
525
  }
483
526
  }
527
+ console.log('');
528
+ console.log(chalk_1.default.yellow('To use dynamic API URLs:'));
529
+ console.log(chalk_1.default.dim(' Use ${API_URL} in your app env vars, e.g.:'));
530
+ console.log(chalk_1.default.cyan(' VITE_API_BASE_URL=${API_URL}'));
531
+ console.log(chalk_1.default.cyan(' NEXT_PUBLIC_API_URL=${API_URL}'));
532
+ console.log('');
533
+ console.log(chalk_1.default.dim(' At create time, ${API_URL} expands based on profile:'));
534
+ console.log(chalk_1.default.dim(' • connect_to: staging → uses STAGING_API_URL'));
535
+ console.log(chalk_1.default.dim(' • connect_to: production → uses PRODUCTION_API_URL'));
536
+ console.log(chalk_1.default.dim(' • local/no connect_to → uses LOCAL_API_URL'));
484
537
  }
485
538
  // Next steps
486
539
  console.log('');
487
540
  console.log(chalk_1.default.bold('Next steps:'));
488
541
  console.log(chalk_1.default.dim(` 1. Review and edit ${CONFIG_FILENAME}`));
489
- console.log(chalk_1.default.dim(` 2. Update API URLs in ${ENV_FILENAME} for staging/production`));
542
+ console.log(chalk_1.default.dim(` 2. Update ${ENV_FILENAME} to use API URL variables where needed`));
490
543
  console.log(chalk_1.default.dim(` 3. Run 'genbox profiles' to see available profiles`));
491
544
  console.log(chalk_1.default.dim(` 4. Run 'genbox create <name> --profile <profile>' to create an environment`));
492
545
  }
@@ -764,7 +817,7 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
764
817
  return Object.keys(environments).length > 0 ? environments : undefined;
765
818
  }
766
819
  /**
767
- * Setup .env.genbox file
820
+ * Setup .env.genbox file with segregated app sections
768
821
  */
769
822
  async function setupEnvFile(projectName, config, nonInteractive = false, scan, isMultiRepo = false, extraEnvVars = {}, overwriteExisting = false) {
770
823
  const envPath = path_1.default.join(process.cwd(), ENV_FILENAME);
@@ -778,66 +831,84 @@ async function setupEnvFile(projectName, config, nonInteractive = false, scan, i
778
831
  return;
779
832
  }
780
833
  }
834
+ // Build segregated content with GLOBAL section first
835
+ let segregatedContent = `# Genbox Environment Variables
836
+ # Project: ${projectName}
837
+ # DO NOT COMMIT THIS FILE
838
+ #
839
+ # This file uses segregated sections for each app/service.
840
+ # At 'genbox create' time, only GLOBAL + selected app sections are used.
841
+ # Use \${API_URL} for dynamic API URLs based on profile's connect_to setting.
842
+
843
+ # === GLOBAL ===
844
+ # These variables are always included regardless of which apps are selected
845
+
846
+ `;
847
+ // Add global env vars
848
+ for (const [key, value] of Object.entries(extraEnvVars)) {
849
+ segregatedContent += `${key}=${value}\n`;
850
+ }
851
+ // Add GIT authentication placeholder if not already added
852
+ if (!extraEnvVars['GIT_TOKEN']) {
853
+ segregatedContent += `# GIT_TOKEN=ghp_xxxxxxxxxxxx\n`;
854
+ }
855
+ segregatedContent += `
856
+ # Database URLs (used by profiles with database mode)
857
+ # STAGING_MONGODB_URL=mongodb+srv://user:password@staging.mongodb.net
858
+ # PROD_MONGODB_URL=mongodb+srv://readonly:password@prod.mongodb.net
859
+
860
+ `;
781
861
  // For multi-repo: find env files in app directories
782
862
  if (isMultiRepo && scan) {
783
863
  const appEnvFiles = findAppEnvFiles(scan.apps, process.cwd());
784
864
  if (appEnvFiles.length > 0 && !nonInteractive) {
785
865
  console.log('');
786
866
  console.log(chalk_1.default.blue('=== Environment Files ==='));
787
- console.log(chalk_1.default.dim(`Found .env files in ${appEnvFiles.length} app directories`));
867
+ // Group by app type
868
+ const directApps = appEnvFiles.filter(e => !e.isService);
869
+ const serviceApps = appEnvFiles.filter(e => e.isService);
870
+ if (directApps.length > 0) {
871
+ console.log(chalk_1.default.dim(`Found ${directApps.length} app env files`));
872
+ }
873
+ if (serviceApps.length > 0) {
874
+ console.log(chalk_1.default.dim(`Found ${serviceApps.length} microservice env files`));
875
+ }
788
876
  const envChoices = appEnvFiles.map(env => ({
789
- name: `${env.appName}/${env.envFile}`,
877
+ name: env.isService ? `${env.appName} (service)` : env.appName,
790
878
  value: env.fullPath,
791
879
  checked: true,
792
880
  }));
793
881
  const selectedEnvFiles = await prompts.checkbox({
794
- message: 'Select .env files to merge into .env.genbox:',
882
+ message: 'Select .env files to include in .env.genbox:',
795
883
  choices: envChoices,
796
884
  });
797
885
  if (selectedEnvFiles.length > 0) {
798
- let mergedContent = `# Genbox Environment Variables
799
- # Merged from: ${selectedEnvFiles.map(f => path_1.default.relative(process.cwd(), f)).join(', ')}
800
- # DO NOT COMMIT THIS FILE
801
- #
802
- # Add staging/production URLs:
803
- # STAGING_MONGODB_URL=mongodb+srv://...
804
- # STAGING_REDIS_URL=redis://...
805
- # PROD_MONGODB_URL=mongodb+srv://...
806
- #
807
- # Git authentication:
808
- # GIT_TOKEN=ghp_xxxxxxxxxxxx
809
-
810
- `;
811
886
  for (const envFilePath of selectedEnvFiles) {
812
887
  const appInfo = appEnvFiles.find(e => e.fullPath === envFilePath);
813
- const content = fs_1.default.readFileSync(envFilePath, 'utf8');
814
- mergedContent += `\n# === ${appInfo?.appName || path_1.default.dirname(envFilePath)} ===\n`;
815
- mergedContent += content;
816
- mergedContent += '\n';
888
+ if (!appInfo)
889
+ continue;
890
+ const content = fs_1.default.readFileSync(envFilePath, 'utf8').trim();
891
+ // Add section header and content
892
+ segregatedContent += `# === ${appInfo.appName} ===\n`;
893
+ segregatedContent += content;
894
+ segregatedContent += '\n\n';
817
895
  }
818
- fs_1.default.writeFileSync(envPath, mergedContent);
819
- console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${selectedEnvFiles.length} app env files`));
820
896
  }
821
897
  }
822
898
  else if (appEnvFiles.length > 0 && nonInteractive) {
823
899
  // Non-interactive: merge all env files
824
- let mergedContent = `# Genbox Environment Variables
825
- # Merged from app directories
826
- # DO NOT COMMIT THIS FILE
827
-
828
- `;
829
900
  for (const envFile of appEnvFiles) {
830
- const content = fs_1.default.readFileSync(envFile.fullPath, 'utf8');
831
- mergedContent += `\n# === ${envFile.appName} ===\n`;
832
- mergedContent += content;
833
- mergedContent += '\n';
901
+ const content = fs_1.default.readFileSync(envFile.fullPath, 'utf8').trim();
902
+ segregatedContent += `# === ${envFile.appName} ===\n`;
903
+ segregatedContent += content;
904
+ segregatedContent += '\n\n';
834
905
  }
835
- fs_1.default.writeFileSync(envPath, mergedContent);
836
- console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${appEnvFiles.length} app env files`));
837
906
  }
838
907
  }
839
- // If no env file created yet, check for root .env
840
- if (!fs_1.default.existsSync(envPath)) {
908
+ // If no app env files found, check for root .env
909
+ const hasAppSections = segregatedContent.includes('# === ') &&
910
+ !segregatedContent.endsWith('# === GLOBAL ===\n');
911
+ if (!hasAppSections) {
841
912
  const existingEnvFiles = ['.env.local', '.env', '.env.development'];
842
913
  let existingEnvPath;
843
914
  for (const envFile of existingEnvFiles) {
@@ -849,51 +920,28 @@ async function setupEnvFile(projectName, config, nonInteractive = false, scan, i
849
920
  }
850
921
  if (existingEnvPath) {
851
922
  const copyExisting = nonInteractive ? true : await prompts.confirm({
852
- message: `Found ${path_1.default.basename(existingEnvPath)}. Copy to ${ENV_FILENAME}?`,
923
+ message: `Found ${path_1.default.basename(existingEnvPath)}. Include in ${ENV_FILENAME}?`,
853
924
  default: true,
854
925
  });
855
926
  if (copyExisting) {
856
- const content = fs_1.default.readFileSync(existingEnvPath, 'utf8');
857
- const header = `# Genbox Environment Variables
858
- # Generated from ${path_1.default.basename(existingEnvPath)}
859
- # DO NOT COMMIT THIS FILE
860
- #
861
- # Add staging/production URLs:
862
- # STAGING_MONGODB_URL=mongodb+srv://...
863
- # STAGING_REDIS_URL=redis://...
864
- # PROD_MONGODB_URL=mongodb+srv://...
865
- #
866
- # Git authentication:
867
- # GIT_TOKEN=ghp_xxxxxxxxxxxx
868
-
869
- `;
870
- fs_1.default.writeFileSync(envPath, header + content);
871
- console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${path_1.default.basename(existingEnvPath)}`));
872
- }
873
- }
874
- else {
875
- const createEnv = nonInteractive ? true : await prompts.confirm({
876
- message: `Create ${ENV_FILENAME} template?`,
877
- default: true,
878
- });
879
- if (createEnv) {
880
- const template = generateEnvTemplate(projectName, config);
881
- fs_1.default.writeFileSync(envPath, template);
882
- console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} template`));
927
+ const content = fs_1.default.readFileSync(existingEnvPath, 'utf8').trim();
928
+ segregatedContent += `# === root ===\n`;
929
+ segregatedContent += `# From ${path_1.default.basename(existingEnvPath)}\n`;
930
+ segregatedContent += content;
931
+ segregatedContent += '\n\n';
883
932
  }
884
933
  }
885
934
  }
886
- // Append extra env vars (like GIT_TOKEN) to the file
887
- if (Object.keys(extraEnvVars).length > 0 && fs_1.default.existsSync(envPath)) {
888
- let content = fs_1.default.readFileSync(envPath, 'utf8');
889
- // Add extra env vars section
890
- let extraSection = '\n# === Added by genbox init ===\n';
891
- for (const [key, value] of Object.entries(extraEnvVars)) {
892
- // Remove any existing commented placeholder
893
- content = content.replace(new RegExp(`^#\\s*${key}=.*$`, 'gm'), '');
894
- extraSection += `${key}=${value}\n`;
895
- }
896
- fs_1.default.writeFileSync(envPath, content.trim() + '\n' + extraSection);
935
+ // Add END marker
936
+ segregatedContent += `# === END ===\n`;
937
+ // Write the file
938
+ fs_1.default.writeFileSync(envPath, segregatedContent);
939
+ const sectionCount = (segregatedContent.match(/# === [^=]+ ===/g) || []).length - 2; // Exclude GLOBAL and END
940
+ if (sectionCount > 0) {
941
+ console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} with ${sectionCount} app section(s)`));
942
+ }
943
+ else {
944
+ console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME}`));
897
945
  }
898
946
  // Add to .gitignore
899
947
  const gitignorePath = path_1.default.join(process.cwd(), '.gitignore');
@@ -915,6 +963,19 @@ function generateEnvTemplate(projectName, config) {
915
963
  '# DO NOT COMMIT THIS FILE',
916
964
  '',
917
965
  '# ============================================',
966
+ '# API URL CONFIGURATION',
967
+ '# ============================================',
968
+ '# Use ${API_URL} in your app env vars (e.g., VITE_API_BASE_URL=${API_URL})',
969
+ '# At create time, ${API_URL} expands based on profile:',
970
+ '# - connect_to: staging → uses STAGING_API_URL',
971
+ '# - connect_to: production → uses PRODUCTION_API_URL',
972
+ '# - local/no connect_to → uses LOCAL_API_URL',
973
+ '',
974
+ 'LOCAL_API_URL=http://localhost:3050',
975
+ 'STAGING_API_URL=https://api.staging.example.com',
976
+ '# PRODUCTION_API_URL=https://api.example.com',
977
+ '',
978
+ '# ============================================',
918
979
  '# STAGING ENVIRONMENT',
919
980
  '# ============================================',
920
981
  '',
@@ -957,6 +1018,15 @@ function generateEnvTemplate(projectName, config) {
957
1018
  'STRIPE_SECRET_KEY=sk_test_xxx',
958
1019
  'STRIPE_WEBHOOK_SECRET=whsec_xxx',
959
1020
  '',
1021
+ '# ============================================',
1022
+ '# APPLICATION ENV VARS',
1023
+ '# ============================================',
1024
+ '# Use ${API_URL} for dynamic API URLs',
1025
+ '',
1026
+ '# Example:',
1027
+ '# VITE_API_BASE_URL=${API_URL}',
1028
+ '# NEXT_PUBLIC_API_URL=${API_URL}',
1029
+ '',
960
1030
  ];
961
1031
  return lines.join('\n');
962
1032
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {