genbox 1.0.64 → 1.0.65

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.
@@ -43,15 +43,15 @@ const chalk_1 = __importDefault(require("chalk"));
43
43
  const ora_1 = __importDefault(require("ora"));
44
44
  const fs = __importStar(require("fs"));
45
45
  const path = __importStar(require("path"));
46
- const os = __importStar(require("os"));
46
+ const child_process_1 = require("child_process");
47
47
  const config_loader_1 = require("../config-loader");
48
48
  const profile_resolver_1 = require("../profile-resolver");
49
49
  const api_1 = require("../api");
50
50
  const ssh_config_1 = require("../ssh-config");
51
51
  const schema_v4_1 = require("../schema-v4");
52
- const child_process_1 = require("child_process");
53
52
  const random_name_1 = require("../random-name");
54
53
  const db_utils_1 = require("../db-utils");
54
+ const utils_1 = require("../utils");
55
55
  // Credits consumed per hour for each size (matches API billing.config.ts)
56
56
  const CREDITS_PER_HOUR = {
57
57
  cx22: 1,
@@ -104,55 +104,6 @@ async function provisionGenbox(payload) {
104
104
  body: JSON.stringify(payload),
105
105
  });
106
106
  }
107
- function getPublicSshKey() {
108
- const home = os.homedir();
109
- const potentialKeys = [
110
- path.join(home, '.ssh', 'id_ed25519.pub'),
111
- path.join(home, '.ssh', 'id_rsa.pub'),
112
- ];
113
- for (const keyPath of potentialKeys) {
114
- if (fs.existsSync(keyPath)) {
115
- const content = fs.readFileSync(keyPath, 'utf-8').trim();
116
- if (content)
117
- return content;
118
- }
119
- }
120
- throw new Error('No public SSH key found in ~/.ssh/');
121
- }
122
- function getPrivateSshKey() {
123
- const home = os.homedir();
124
- const potentialKeys = [
125
- path.join(home, '.ssh', 'id_ed25519'),
126
- path.join(home, '.ssh', 'id_rsa'),
127
- ];
128
- for (const keyPath of potentialKeys) {
129
- if (fs.existsSync(keyPath)) {
130
- return fs.readFileSync(keyPath, 'utf-8');
131
- }
132
- }
133
- return undefined;
134
- }
135
- /**
136
- * Get local git config for commits on genbox
137
- */
138
- function getGitConfig() {
139
- const { execSync } = require('child_process');
140
- let userName;
141
- let userEmail;
142
- try {
143
- userName = execSync('git config --global user.name', { encoding: 'utf-8' }).trim();
144
- }
145
- catch {
146
- // Git config not set
147
- }
148
- try {
149
- userEmail = execSync('git config --global user.email', { encoding: 'utf-8' }).trim();
150
- }
151
- catch {
152
- // Git config not set
153
- }
154
- return { userName, userEmail };
155
- }
156
107
  /**
157
108
  * Prompt user for environment name
158
109
  */
@@ -343,7 +294,7 @@ exports.createCommand = new commander_1.Command('create')
343
294
  // Interactive branch selection if no branch options were specified
344
295
  // Skip if: -b (existing branch), -f (new branch from source), -n (explicit new branch name), or -y (skip prompts)
345
296
  if (!options.branch && !options.fromBranch && !options.newBranch && !options.yes && resolved.repos.length > 0) {
346
- resolved = await promptForBranchOptions(resolved, config, name);
297
+ resolved = await (0, utils_1.promptForBranchOptionsCreate)(resolved, config, name);
347
298
  }
348
299
  // Default behavior when -y (non-interactive) and no branch options: create new branch from configured default
349
300
  if (!options.branch && !options.fromBranch && !options.newBranch && options.yes && resolved.repos.length > 0) {
@@ -412,7 +363,7 @@ exports.createCommand = new commander_1.Command('create')
412
363
  }
413
364
  }
414
365
  // Get SSH keys
415
- const publicKey = getPublicSshKey();
366
+ const publicKey = (0, utils_1.getPublicSshKey)();
416
367
  // Check if SSH auth is needed for git
417
368
  let privateKeyContent;
418
369
  const v3Config = config;
@@ -424,7 +375,7 @@ exports.createCommand = new commander_1.Command('create')
424
375
  default: true,
425
376
  });
426
377
  if (injectKey) {
427
- privateKeyContent = getPrivateSshKey();
378
+ privateKeyContent = (0, utils_1.getPrivateSshKey)();
428
379
  if (privateKeyContent) {
429
380
  console.log(chalk_1.default.dim(' Using local SSH private key'));
430
381
  }
@@ -432,7 +383,7 @@ exports.createCommand = new commander_1.Command('create')
432
383
  }
433
384
  // Validate git credentials and warn if missing
434
385
  const envVarsForValidation = configLoader.loadEnvVars(process.cwd());
435
- const gitValidation = validateGitCredentials(resolved.repos.map(r => ({ url: r.url, name: r.name })), envVarsForValidation.GIT_TOKEN, privateKeyContent);
386
+ const gitValidation = (0, utils_1.validateGitCredentials)(resolved.repos.map(r => ({ url: r.url, name: r.name })), envVarsForValidation.GIT_TOKEN, privateKeyContent);
436
387
  if (gitValidation.warnings.length > 0) {
437
388
  console.log('');
438
389
  console.log(chalk_1.default.yellow('⚠ Git Authentication Warnings:'));
@@ -736,225 +687,6 @@ function displayResolvedConfig(resolved) {
736
687
  console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
737
688
  console.log('');
738
689
  }
739
- /**
740
- * Prompt for branch options interactively
741
- * Default behavior: create new branch from configured default (or 'main') with branch name = environment name
742
- */
743
- async function promptForBranchOptions(resolved, config, envName) {
744
- // Get the default/base branch from config (used as source for new branches)
745
- const baseBranch = config.defaults?.branch || 'main';
746
- console.log(chalk_1.default.blue('=== Branch Configuration ==='));
747
- console.log('');
748
- const branchChoice = await prompts.select({
749
- message: 'Branch option:',
750
- choices: [
751
- {
752
- name: `${chalk_1.default.green('Create new branch')} '${chalk_1.default.cyan(envName)}' from '${baseBranch}' ${chalk_1.default.dim('(recommended)')}`,
753
- value: 'new-from-default',
754
- },
755
- {
756
- name: `Create new branch from a different source`,
757
- value: 'new-custom',
758
- },
759
- {
760
- name: 'Use an existing branch',
761
- value: 'existing',
762
- },
763
- {
764
- name: `Use '${baseBranch}' directly without creating new branch`,
765
- value: 'default',
766
- },
767
- ],
768
- default: 'new-from-default',
769
- });
770
- if (branchChoice === 'new-from-default') {
771
- // Create new branch from configured default with name = environment name
772
- return {
773
- ...resolved,
774
- repos: resolved.repos.map(repo => ({
775
- ...repo,
776
- branch: envName,
777
- newBranch: envName,
778
- sourceBranch: baseBranch,
779
- })),
780
- };
781
- }
782
- if (branchChoice === 'new-custom') {
783
- const newBranchName = await prompts.input({
784
- message: 'New branch name:',
785
- default: envName,
786
- validate: (value) => {
787
- if (!value.trim())
788
- return 'Branch name is required';
789
- if (!/^[\w\-./]+$/.test(value))
790
- return 'Invalid branch name (use letters, numbers, -, _, /, .)';
791
- return true;
792
- },
793
- });
794
- const sourceBranch = await prompts.input({
795
- message: 'Create from branch:',
796
- default: 'main',
797
- validate: (value) => {
798
- if (!value.trim())
799
- return 'Source branch is required';
800
- return true;
801
- },
802
- });
803
- // Update all repos with new branch info
804
- return {
805
- ...resolved,
806
- repos: resolved.repos.map(repo => ({
807
- ...repo,
808
- branch: newBranchName.trim(),
809
- newBranch: newBranchName.trim(),
810
- sourceBranch: sourceBranch.trim(),
811
- })),
812
- };
813
- }
814
- if (branchChoice === 'existing') {
815
- const branchName = await prompts.input({
816
- message: 'Enter branch name:',
817
- default: baseBranch,
818
- validate: (value) => {
819
- if (!value.trim())
820
- return 'Branch name is required';
821
- return true;
822
- },
823
- });
824
- // Update all repos with the selected branch
825
- return {
826
- ...resolved,
827
- repos: resolved.repos.map(repo => ({
828
- ...repo,
829
- branch: branchName.trim(),
830
- newBranch: undefined,
831
- sourceBranch: undefined,
832
- })),
833
- };
834
- }
835
- if (branchChoice === 'default') {
836
- // Keep resolved repos as-is (no new branch)
837
- return resolved;
838
- }
839
- return resolved;
840
- }
841
- /**
842
- * Parse .env.genbox file into segregated sections
843
- */
844
- function parseEnvGenboxSections(content) {
845
- const sections = new Map();
846
- let currentSection = 'GLOBAL';
847
- let currentContent = [];
848
- for (const line of content.split('\n')) {
849
- const sectionMatch = line.match(/^# === ([^=]+) ===$/);
850
- if (sectionMatch) {
851
- // Save previous section
852
- if (currentContent.length > 0) {
853
- sections.set(currentSection, currentContent.join('\n').trim());
854
- }
855
- currentSection = sectionMatch[1].trim();
856
- currentContent = [];
857
- }
858
- else if (currentSection !== 'END') {
859
- currentContent.push(line);
860
- }
861
- }
862
- // Save last section
863
- if (currentContent.length > 0 && currentSection !== 'END') {
864
- sections.set(currentSection, currentContent.join('\n').trim());
865
- }
866
- return sections;
867
- }
868
- /**
869
- * Build a map of service URL variables based on connection type
870
- * e.g., if connectTo=staging: GATEWAY_URL → STAGING_GATEWAY_URL value
871
- */
872
- function buildServiceUrlMap(envVarsFromFile, connectTo) {
873
- const urlMap = {};
874
- const prefix = connectTo ? `${connectTo.toUpperCase()}_` : 'LOCAL_';
875
- // Find all service URL variables (LOCAL_*_URL and STAGING_*_URL patterns)
876
- const serviceNames = new Set();
877
- for (const key of Object.keys(envVarsFromFile)) {
878
- const match = key.match(/^(LOCAL|STAGING|PRODUCTION)_(.+_URL)$/);
879
- if (match) {
880
- serviceNames.add(match[2]);
881
- }
882
- }
883
- // Build mapping: VARNAME → value from appropriate prefix
884
- for (const serviceName of serviceNames) {
885
- const prefixedKey = `${prefix}${serviceName}`;
886
- const localKey = `LOCAL_${serviceName}`;
887
- // Use prefixed value if available, otherwise fall back to local
888
- const value = envVarsFromFile[prefixedKey] || envVarsFromFile[localKey];
889
- if (value) {
890
- urlMap[serviceName] = value;
891
- }
892
- }
893
- // Also handle legacy API_URL for backwards compatibility
894
- if (!urlMap['API_URL']) {
895
- const apiUrl = envVarsFromFile[`${prefix}API_URL`] ||
896
- envVarsFromFile['LOCAL_API_URL'] ||
897
- envVarsFromFile['STAGING_API_URL'];
898
- if (apiUrl) {
899
- urlMap['API_URL'] = apiUrl;
900
- }
901
- }
902
- return urlMap;
903
- }
904
- /**
905
- * Build env content for a specific app by combining GLOBAL + app-specific sections
906
- */
907
- function buildAppEnvContent(sections, appName, serviceUrlMap) {
908
- const parts = [];
909
- // Always include GLOBAL section
910
- const globalSection = sections.get('GLOBAL');
911
- if (globalSection) {
912
- parts.push(globalSection);
913
- }
914
- // Include app-specific section if exists
915
- const appSection = sections.get(appName);
916
- if (appSection) {
917
- parts.push(appSection);
918
- }
919
- let envContent = parts.join('\n\n');
920
- // Expand all ${VARNAME} references using the service URL map
921
- for (const [varName, value] of Object.entries(serviceUrlMap)) {
922
- const pattern = new RegExp(`\\$\\{${varName}\\}`, 'g');
923
- envContent = envContent.replace(pattern, value);
924
- }
925
- // Keep only actual env vars (filter out pure comment lines but keep var definitions)
926
- envContent = envContent
927
- .split('\n')
928
- .filter(line => {
929
- const trimmed = line.trim();
930
- // Keep empty lines, lines with = (even if commented), and non-comment lines
931
- return trimmed === '' || trimmed.includes('=') || !trimmed.startsWith('#');
932
- })
933
- .join('\n')
934
- .replace(/\n{3,}/g, '\n\n')
935
- .trim();
936
- return envContent;
937
- }
938
- /**
939
- * Validate git configuration and warn about missing credentials
940
- */
941
- function validateGitCredentials(repos, gitToken, privateKey) {
942
- const warnings = [];
943
- const errors = [];
944
- for (const repo of repos) {
945
- const isHttps = repo.url.startsWith('https://');
946
- const isSsh = repo.url.startsWith('git@') || repo.url.includes('ssh://');
947
- const isPrivateRepo = repo.url.includes('github.com') && !repo.url.includes('/public/');
948
- if (isHttps && !gitToken && isPrivateRepo) {
949
- warnings.push(`Repository '${repo.name}' uses HTTPS URL but GIT_TOKEN is not set.`);
950
- warnings.push(` Add GIT_TOKEN=<your-github-token> to .env.genbox for private repos.`);
951
- }
952
- if (isSsh && !privateKey) {
953
- warnings.push(`Repository '${repo.name}' uses SSH URL but no SSH key was injected.`);
954
- }
955
- }
956
- return { warnings, errors };
957
- }
958
690
  /**
959
691
  * Build API payload from resolved config
960
692
  */
@@ -977,22 +709,10 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
977
709
  if (fs.existsSync(envGenboxPath)) {
978
710
  const rawEnvContent = fs.readFileSync(envGenboxPath, 'utf-8');
979
711
  // Parse into sections
980
- const sections = parseEnvGenboxSections(rawEnvContent);
712
+ const sections = (0, utils_1.parseEnvGenboxSections)(rawEnvContent);
981
713
  // Parse GLOBAL section to get API URL values
982
714
  const globalSection = sections.get('GLOBAL') || '';
983
- const envVarsFromFile = {};
984
- for (const line of globalSection.split('\n')) {
985
- const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
986
- if (match) {
987
- let value = match[2].trim();
988
- // Remove quotes if present
989
- if ((value.startsWith('"') && value.endsWith('"')) ||
990
- (value.startsWith("'") && value.endsWith("'"))) {
991
- value = value.slice(1, -1);
992
- }
993
- envVarsFromFile[match[1]] = value;
994
- }
995
- }
715
+ const envVarsFromFile = (0, utils_1.parseEnvVarsFromSection)(globalSection);
996
716
  // Determine connection type from profile's connect_to (v3) or default_connection (v4)
997
717
  let connectTo;
998
718
  if (resolved.profile && config.profiles?.[resolved.profile]) {
@@ -1002,7 +722,7 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
1002
722
  // Build service URL map for variable expansion
1003
723
  // This maps GATEWAY_URL → STAGING_GATEWAY_URL value (if connectTo=staging)
1004
724
  // or GATEWAY_URL → LOCAL_GATEWAY_URL value (if local)
1005
- const serviceUrlMap = buildServiceUrlMap(envVarsFromFile, connectTo);
725
+ const serviceUrlMap = (0, utils_1.buildServiceUrlMap)(envVarsFromFile, connectTo);
1006
726
  // Log what's being expanded for debugging
1007
727
  if (connectTo && Object.keys(serviceUrlMap).length > 0) {
1008
728
  console.log(chalk_1.default.dim(` Using ${connectTo} URLs for variable expansion`));
@@ -1019,7 +739,7 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
1019
739
  for (const serviceSectionName of servicesSections) {
1020
740
  const serviceName = serviceSectionName.split('/')[1];
1021
741
  // Build service-specific env content (GLOBAL + service section)
1022
- const serviceEnvContent = buildAppEnvContent(sections, serviceSectionName, serviceUrlMap);
742
+ const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName, serviceUrlMap);
1023
743
  const stagingName = `${app.name}-${serviceName}.env`;
1024
744
  const targetPath = `${repoPath}/apps/${serviceName}/.env`;
1025
745
  files.push({
@@ -1032,7 +752,7 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
1032
752
  }
1033
753
  else {
1034
754
  // Regular app - build app-specific env content (GLOBAL + app section)
1035
- const appEnvContent = buildAppEnvContent(sections, app.name, serviceUrlMap);
755
+ const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name, serviceUrlMap);
1036
756
  files.push({
1037
757
  path: `/home/dev/.env-staging/${app.name}.env`,
1038
758
  content: appEnvContent,
@@ -1071,7 +791,7 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
1071
791
  };
1072
792
  }
1073
793
  // Get local git config for commits
1074
- const gitConfig = getGitConfig();
794
+ const gitConfig = (0, utils_1.getGitConfig)();
1075
795
  // Load project cache to get project ID
1076
796
  const projectCache = loadProjectCache(process.cwd());
1077
797
  return {
@@ -1211,7 +931,7 @@ async function createLegacy(name, options) {
1211
931
  const { loadConfig, loadEnvVars } = await Promise.resolve().then(() => __importStar(require('../config')));
1212
932
  const config = loadConfig();
1213
933
  const size = options.size || config.system?.server_size || 'small';
1214
- const publicKey = getPublicSshKey();
934
+ const publicKey = (0, utils_1.getPublicSshKey)();
1215
935
  // Check if SSH auth needed
1216
936
  let privateKeyContent;
1217
937
  const usesSSH = config.git_auth?.method === 'ssh' ||
@@ -1222,7 +942,7 @@ async function createLegacy(name, options) {
1222
942
  default: true,
1223
943
  });
1224
944
  if (injectKey) {
1225
- privateKeyContent = getPrivateSshKey();
945
+ privateKeyContent = (0, utils_1.getPrivateSshKey)();
1226
946
  }
1227
947
  }
1228
948
  const spinner = (0, ora_1.default)(`Creating Genbox '${name}'...`).start();
@@ -51,22 +51,10 @@ const api_1 = require("../api");
51
51
  const genbox_selector_1 = require("../genbox-selector");
52
52
  const schema_v4_1 = require("../schema-v4");
53
53
  const db_utils_1 = require("../db-utils");
54
+ const utils_1 = require("../utils");
54
55
  // ============================================================================
55
56
  // SSH Utilities for Soft Rebuild
56
57
  // ============================================================================
57
- function getPrivateSshKeyPath() {
58
- const home = os.homedir();
59
- const potentialKeys = [
60
- path.join(home, '.ssh', 'id_ed25519'),
61
- path.join(home, '.ssh', 'id_rsa'),
62
- ];
63
- for (const keyPath of potentialKeys) {
64
- if (fs.existsSync(keyPath)) {
65
- return keyPath;
66
- }
67
- }
68
- throw new Error('No SSH private key found in ~/.ssh/');
69
- }
70
58
  function sshExec(ip, keyPath, command, timeoutSecs = 30) {
71
59
  const sshOpts = `-i ${keyPath} -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=${timeoutSecs}`;
72
60
  try {
@@ -389,125 +377,12 @@ async function runSoftRebuild(options) {
389
377
  return { success: false, error: error.message };
390
378
  }
391
379
  }
392
- function getPublicSshKey() {
393
- const home = os.homedir();
394
- const potentialKeys = [
395
- path.join(home, '.ssh', 'id_ed25519.pub'),
396
- path.join(home, '.ssh', 'id_rsa.pub'),
397
- ];
398
- for (const keyPath of potentialKeys) {
399
- if (fs.existsSync(keyPath)) {
400
- const content = fs.readFileSync(keyPath, 'utf-8').trim();
401
- if (content)
402
- return content;
403
- }
404
- }
405
- throw new Error('No public SSH key found in ~/.ssh/');
406
- }
407
- function getPrivateSshKey() {
408
- const home = os.homedir();
409
- const potentialKeys = [
410
- path.join(home, '.ssh', 'id_ed25519'),
411
- path.join(home, '.ssh', 'id_rsa'),
412
- ];
413
- for (const keyPath of potentialKeys) {
414
- if (fs.existsSync(keyPath)) {
415
- return fs.readFileSync(keyPath, 'utf-8');
416
- }
417
- }
418
- return undefined;
419
- }
420
380
  async function rebuildGenbox(id, payload) {
421
381
  return (0, api_1.fetchApi)(`/genboxes/${id}/rebuild`, {
422
382
  method: 'POST',
423
383
  body: JSON.stringify(payload),
424
384
  });
425
385
  }
426
- /**
427
- * Parse .env.genbox file into segregated sections
428
- */
429
- function parseEnvGenboxSections(content) {
430
- const sections = new Map();
431
- let currentSection = 'GLOBAL';
432
- let currentContent = [];
433
- for (const line of content.split('\n')) {
434
- const sectionMatch = line.match(/^# === ([^=]+) ===$/);
435
- if (sectionMatch) {
436
- if (currentContent.length > 0) {
437
- sections.set(currentSection, currentContent.join('\n').trim());
438
- }
439
- currentSection = sectionMatch[1].trim();
440
- currentContent = [];
441
- }
442
- else if (currentSection !== 'END') {
443
- currentContent.push(line);
444
- }
445
- }
446
- if (currentContent.length > 0 && currentSection !== 'END') {
447
- sections.set(currentSection, currentContent.join('\n').trim());
448
- }
449
- return sections;
450
- }
451
- /**
452
- * Build a map of service URL variables based on connection type
453
- */
454
- function buildServiceUrlMap(envVarsFromFile, connectTo) {
455
- const urlMap = {};
456
- const prefix = connectTo ? `${connectTo.toUpperCase()}_` : 'LOCAL_';
457
- const serviceNames = new Set();
458
- for (const key of Object.keys(envVarsFromFile)) {
459
- const match = key.match(/^(LOCAL|STAGING|PRODUCTION)_(.+_URL)$/);
460
- if (match) {
461
- serviceNames.add(match[2]);
462
- }
463
- }
464
- for (const serviceName of serviceNames) {
465
- const prefixedKey = `${prefix}${serviceName}`;
466
- const localKey = `LOCAL_${serviceName}`;
467
- const value = envVarsFromFile[prefixedKey] || envVarsFromFile[localKey];
468
- if (value) {
469
- urlMap[serviceName] = value;
470
- }
471
- }
472
- if (!urlMap['API_URL']) {
473
- const apiUrl = envVarsFromFile[`${prefix}API_URL`] ||
474
- envVarsFromFile['LOCAL_API_URL'] ||
475
- envVarsFromFile['STAGING_API_URL'];
476
- if (apiUrl) {
477
- urlMap['API_URL'] = apiUrl;
478
- }
479
- }
480
- return urlMap;
481
- }
482
- /**
483
- * Build env content for a specific app
484
- */
485
- function buildAppEnvContent(sections, appName, serviceUrlMap) {
486
- const parts = [];
487
- const globalSection = sections.get('GLOBAL');
488
- if (globalSection) {
489
- parts.push(globalSection);
490
- }
491
- const appSection = sections.get(appName);
492
- if (appSection) {
493
- parts.push(appSection);
494
- }
495
- let envContent = parts.join('\n\n');
496
- for (const [varName, value] of Object.entries(serviceUrlMap)) {
497
- const pattern = new RegExp(`\\$\\{${varName}\\}`, 'g');
498
- envContent = envContent.replace(pattern, value);
499
- }
500
- envContent = envContent
501
- .split('\n')
502
- .filter(line => {
503
- const trimmed = line.trim();
504
- return trimmed === '' || trimmed.includes('=') || !trimmed.startsWith('#');
505
- })
506
- .join('\n')
507
- .replace(/\n{3,}/g, '\n\n')
508
- .trim();
509
- return envContent;
510
- }
511
386
  /**
512
387
  * Build rebuild payload from resolved config
513
388
  */
@@ -527,26 +402,15 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
527
402
  const envGenboxPath = path.join(process.cwd(), '.env.genbox');
528
403
  if (fs.existsSync(envGenboxPath)) {
529
404
  const rawEnvContent = fs.readFileSync(envGenboxPath, 'utf-8');
530
- const sections = parseEnvGenboxSections(rawEnvContent);
405
+ const sections = (0, utils_1.parseEnvGenboxSections)(rawEnvContent);
531
406
  const globalSection = sections.get('GLOBAL') || '';
532
- const envVarsFromFile = {};
533
- for (const line of globalSection.split('\n')) {
534
- const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
535
- if (match) {
536
- let value = match[2].trim();
537
- if ((value.startsWith('"') && value.endsWith('"')) ||
538
- (value.startsWith("'") && value.endsWith("'"))) {
539
- value = value.slice(1, -1);
540
- }
541
- envVarsFromFile[match[1]] = value;
542
- }
543
- }
407
+ const envVarsFromFile = (0, utils_1.parseEnvVarsFromSection)(globalSection);
544
408
  let connectTo;
545
409
  if (resolved.profile && config.profiles?.[resolved.profile]) {
546
410
  const profile = config.profiles[resolved.profile];
547
411
  connectTo = (0, config_loader_1.getProfileConnection)(profile);
548
412
  }
549
- const serviceUrlMap = buildServiceUrlMap(envVarsFromFile, connectTo);
413
+ const serviceUrlMap = (0, utils_1.buildServiceUrlMap)(envVarsFromFile, connectTo);
550
414
  if (connectTo && Object.keys(serviceUrlMap).length > 0) {
551
415
  console.log(chalk_1.default.dim(` Using ${connectTo} URLs for variable expansion`));
552
416
  }
@@ -558,7 +422,7 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
558
422
  if (servicesSections.length > 0) {
559
423
  for (const serviceSectionName of servicesSections) {
560
424
  const serviceName = serviceSectionName.split('/')[1];
561
- const serviceEnvContent = buildAppEnvContent(sections, serviceSectionName, serviceUrlMap);
425
+ const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName, serviceUrlMap);
562
426
  const stagingName = `${app.name}-${serviceName}.env`;
563
427
  const targetPath = `${repoPath}/apps/${serviceName}/.env`;
564
428
  files.push({
@@ -570,7 +434,7 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
570
434
  }
571
435
  }
572
436
  else {
573
- const appEnvContent = buildAppEnvContent(sections, app.name, serviceUrlMap);
437
+ const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name, serviceUrlMap);
574
438
  files.push({
575
439
  path: `/home/dev/.env-staging/${app.name}.env`,
576
440
  content: appEnvContent,
@@ -600,6 +464,8 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
600
464
  sourceBranch: repo.sourceBranch,
601
465
  };
602
466
  }
467
+ // Get local git config for commits
468
+ const gitConfig = (0, utils_1.getGitConfig)();
603
469
  return {
604
470
  publicKey,
605
471
  services,
@@ -608,110 +474,10 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
608
474
  repos,
609
475
  privateKey,
610
476
  gitToken: envVars.GIT_TOKEN,
477
+ gitUserName: gitConfig.userName,
478
+ gitUserEmail: gitConfig.userEmail,
611
479
  };
612
480
  }
613
- /**
614
- * Validate git configuration and warn about missing credentials
615
- */
616
- function validateGitCredentials(repos, gitToken, privateKey) {
617
- const warnings = [];
618
- const errors = [];
619
- for (const repo of repos) {
620
- const isHttps = repo.url.startsWith('https://');
621
- const isSsh = repo.url.startsWith('git@') || repo.url.includes('ssh://');
622
- const isPrivateRepo = repo.url.includes('github.com') && !repo.url.includes('/public/');
623
- if (isHttps && !gitToken && isPrivateRepo) {
624
- warnings.push(`Repository '${repo.name}' uses HTTPS URL but GIT_TOKEN is not set.`);
625
- warnings.push(` Add GIT_TOKEN=<your-github-token> to .env.genbox for private repos.`);
626
- }
627
- if (isSsh && !privateKey) {
628
- warnings.push(`Repository '${repo.name}' uses SSH URL but no SSH key was injected.`);
629
- }
630
- }
631
- return { warnings, errors };
632
- }
633
- /**
634
- * Prompt for branch options interactively
635
- */
636
- async function promptForBranchOptions(resolved, config) {
637
- // Get the default branch from config or first repo
638
- const defaultBranch = config.defaults?.branch || resolved.repos[0]?.branch || 'main';
639
- console.log(chalk_1.default.blue('=== Branch Configuration ==='));
640
- console.log(chalk_1.default.dim(`Default branch: ${defaultBranch}`));
641
- console.log('');
642
- const branchChoice = await prompts.select({
643
- message: 'Branch option:',
644
- choices: [
645
- {
646
- name: `Use default branch (${defaultBranch})`,
647
- value: 'default',
648
- },
649
- {
650
- name: 'Use a different existing branch',
651
- value: 'existing',
652
- },
653
- {
654
- name: 'Create a new branch',
655
- value: 'new',
656
- },
657
- ],
658
- default: 'default',
659
- });
660
- if (branchChoice === 'default') {
661
- return resolved;
662
- }
663
- if (branchChoice === 'existing') {
664
- const branchName = await prompts.input({
665
- message: 'Enter branch name:',
666
- default: defaultBranch,
667
- validate: (value) => {
668
- if (!value.trim())
669
- return 'Branch name is required';
670
- return true;
671
- },
672
- });
673
- return {
674
- ...resolved,
675
- repos: resolved.repos.map(repo => ({
676
- ...repo,
677
- branch: branchName.trim(),
678
- newBranch: undefined,
679
- sourceBranch: undefined,
680
- })),
681
- };
682
- }
683
- if (branchChoice === 'new') {
684
- const newBranchName = await prompts.input({
685
- message: 'New branch name:',
686
- validate: (value) => {
687
- if (!value.trim())
688
- return 'Branch name is required';
689
- if (!/^[\w\-./]+$/.test(value))
690
- return 'Invalid branch name (use letters, numbers, -, _, /, .)';
691
- return true;
692
- },
693
- });
694
- const sourceBranch = await prompts.input({
695
- message: 'Create from branch:',
696
- default: defaultBranch,
697
- validate: (value) => {
698
- if (!value.trim())
699
- return 'Source branch is required';
700
- return true;
701
- },
702
- });
703
- return {
704
- ...resolved,
705
- repos: resolved.repos.map(repo => ({
706
- ...repo,
707
- branch: newBranchName.trim(),
708
- newBranch: newBranchName.trim(),
709
- sourceBranch: sourceBranch.trim(),
710
- })),
711
- };
712
- }
713
- return resolved;
714
- }
715
481
  exports.rebuildCommand = new commander_1.Command('rebuild')
716
482
  .description('Rebuild an existing Genbox environment with updated configuration')
717
483
  .argument('[name]', 'Name of the Genbox to rebuild (optional - will prompt if not provided)')
@@ -822,7 +588,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
822
588
  // Interactive branch selection only if no branch options specified and no stored branch
823
589
  // Skip if: -b, -f, -n, stored branch, or -y
824
590
  if (!options.branch && !options.fromBranch && !storedBranch && !options.newBranch && !options.yes && resolved.repos.length > 0) {
825
- resolved = await promptForBranchOptions(resolved, config);
591
+ resolved = await (0, utils_1.promptForBranchOptionsRebuild)(resolved, config);
826
592
  }
827
593
  // Display what will be rebuilt
828
594
  console.log(chalk_1.default.bold('Rebuild Configuration:'));
@@ -882,7 +648,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
882
648
  }
883
649
  }
884
650
  // Get SSH keys
885
- const publicKey = getPublicSshKey();
651
+ const publicKey = (0, utils_1.getPublicSshKey)();
886
652
  // Check if SSH auth is needed for git
887
653
  let privateKeyContent;
888
654
  const v3Config = config;
@@ -894,7 +660,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
894
660
  default: true,
895
661
  });
896
662
  if (injectKey) {
897
- privateKeyContent = getPrivateSshKey();
663
+ privateKeyContent = (0, utils_1.getPrivateSshKey)();
898
664
  if (privateKeyContent) {
899
665
  console.log(chalk_1.default.dim(' Using local SSH private key'));
900
666
  }
@@ -1021,7 +787,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
1021
787
  }
1022
788
  // Validate git credentials before rebuilding
1023
789
  const envVarsForValidation = configLoader.loadEnvVars(process.cwd());
1024
- const gitValidation = validateGitCredentials(resolved.repos.map(r => ({ url: r.url, name: r.name })), envVarsForValidation.GIT_TOKEN, privateKeyContent);
790
+ const gitValidation = (0, utils_1.validateGitCredentials)(resolved.repos.map(r => ({ url: r.url, name: r.name })), envVarsForValidation.GIT_TOKEN, privateKeyContent);
1025
791
  if (gitValidation.warnings.length > 0) {
1026
792
  console.log('');
1027
793
  console.log(chalk_1.default.yellow('⚠ Git Authentication Warnings:'));
@@ -1058,7 +824,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
1058
824
  // Get SSH key path
1059
825
  let sshKeyPath;
1060
826
  try {
1061
- sshKeyPath = getPrivateSshKeyPath();
827
+ sshKeyPath = (0, utils_1.getPrivateSshKeyPath)();
1062
828
  }
1063
829
  catch (error) {
1064
830
  console.log(chalk_1.default.red(error.message));
@@ -1080,7 +846,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
1080
846
  const envGenboxPath = path.join(process.cwd(), '.env.genbox');
1081
847
  if (fs.existsSync(envGenboxPath)) {
1082
848
  const rawEnvContent = fs.readFileSync(envGenboxPath, 'utf-8');
1083
- const sections = parseEnvGenboxSections(rawEnvContent);
849
+ const sections = (0, utils_1.parseEnvGenboxSections)(rawEnvContent);
1084
850
  const globalSection = sections.get('GLOBAL') || '';
1085
851
  const envVarsFromFile = {};
1086
852
  for (const line of globalSection.split('\n')) {
@@ -1099,7 +865,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
1099
865
  const profile = config.profiles[resolved.profile];
1100
866
  connectTo = (0, config_loader_1.getProfileConnection)(profile);
1101
867
  }
1102
- const serviceUrlMap = buildServiceUrlMap(envVarsFromFile, connectTo);
868
+ const serviceUrlMap = (0, utils_1.buildServiceUrlMap)(envVarsFromFile, connectTo);
1103
869
  for (const app of resolved.apps) {
1104
870
  const appConfig = config.apps[app.name];
1105
871
  const appPath = appConfig?.path || app.name;
@@ -1109,7 +875,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
1109
875
  if (servicesSections.length > 0) {
1110
876
  for (const serviceSectionName of servicesSections) {
1111
877
  const serviceName = serviceSectionName.split('/')[1];
1112
- const serviceEnvContent = buildAppEnvContent(sections, serviceSectionName, serviceUrlMap);
878
+ const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName, serviceUrlMap);
1113
879
  envFilesForSoftRebuild.push({
1114
880
  stagingName: `${app.name}-${serviceName}.env`,
1115
881
  remotePath: `${repoPath}/apps/${serviceName}/.env`,
@@ -1118,7 +884,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
1118
884
  }
1119
885
  }
1120
886
  else {
1121
- const appEnvContent = buildAppEnvContent(sections, app.name, serviceUrlMap);
887
+ const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name, serviceUrlMap);
1122
888
  envFilesForSoftRebuild.push({
1123
889
  stagingName: `${app.name}.env`,
1124
890
  remotePath: `${repoPath}/.env`,
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+ /**
3
+ * Branch Prompt Utilities
4
+ * Shared functions for interactive branch selection
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ var __importDefault = (this && this.__importDefault) || function (mod) {
40
+ return (mod && mod.__esModule) ? mod : { "default": mod };
41
+ };
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.promptForBranchOptionsCreate = promptForBranchOptionsCreate;
44
+ exports.promptForBranchOptionsRebuild = promptForBranchOptionsRebuild;
45
+ const prompts = __importStar(require("@inquirer/prompts"));
46
+ const chalk_1 = __importDefault(require("chalk"));
47
+ /**
48
+ * Prompt for branch options interactively (for create command)
49
+ * Default behavior: create new branch from configured default (or 'main') with branch name = environment name
50
+ */
51
+ async function promptForBranchOptionsCreate(resolved, config, envName) {
52
+ // Get the default/base branch from config (used as source for new branches)
53
+ const baseBranch = config.defaults?.branch || 'main';
54
+ console.log(chalk_1.default.blue('=== Branch Configuration ==='));
55
+ console.log('');
56
+ const branchChoice = await prompts.select({
57
+ message: 'Branch option:',
58
+ choices: [
59
+ {
60
+ name: `${chalk_1.default.green('Create new branch')} '${chalk_1.default.cyan(envName)}' from '${baseBranch}' ${chalk_1.default.dim('(recommended)')}`,
61
+ value: 'new-from-default',
62
+ },
63
+ {
64
+ name: `Create new branch from a different source`,
65
+ value: 'new-custom',
66
+ },
67
+ {
68
+ name: 'Use an existing branch',
69
+ value: 'existing',
70
+ },
71
+ {
72
+ name: `Use '${baseBranch}' directly without creating new branch`,
73
+ value: 'default',
74
+ },
75
+ ],
76
+ default: 'new-from-default',
77
+ });
78
+ if (branchChoice === 'new-from-default') {
79
+ // Create new branch from configured default with name = environment name
80
+ return {
81
+ ...resolved,
82
+ repos: resolved.repos.map(repo => ({
83
+ ...repo,
84
+ branch: envName,
85
+ newBranch: envName,
86
+ sourceBranch: baseBranch,
87
+ })),
88
+ };
89
+ }
90
+ if (branchChoice === 'new-custom') {
91
+ const newBranchName = await prompts.input({
92
+ message: 'New branch name:',
93
+ default: envName,
94
+ validate: (value) => {
95
+ if (!value.trim())
96
+ return 'Branch name is required';
97
+ if (!/^[\w\-./]+$/.test(value))
98
+ return 'Invalid branch name (use letters, numbers, -, _, /, .)';
99
+ return true;
100
+ },
101
+ });
102
+ const sourceBranch = await prompts.input({
103
+ message: 'Create from branch:',
104
+ default: 'main',
105
+ validate: (value) => {
106
+ if (!value.trim())
107
+ return 'Source branch is required';
108
+ return true;
109
+ },
110
+ });
111
+ // Update all repos with new branch info
112
+ return {
113
+ ...resolved,
114
+ repos: resolved.repos.map(repo => ({
115
+ ...repo,
116
+ branch: newBranchName.trim(),
117
+ newBranch: newBranchName.trim(),
118
+ sourceBranch: sourceBranch.trim(),
119
+ })),
120
+ };
121
+ }
122
+ if (branchChoice === 'existing') {
123
+ const branchName = await prompts.input({
124
+ message: 'Enter branch name:',
125
+ default: baseBranch,
126
+ validate: (value) => {
127
+ if (!value.trim())
128
+ return 'Branch name is required';
129
+ return true;
130
+ },
131
+ });
132
+ // Update all repos with the selected branch
133
+ return {
134
+ ...resolved,
135
+ repos: resolved.repos.map(repo => ({
136
+ ...repo,
137
+ branch: branchName.trim(),
138
+ newBranch: undefined,
139
+ sourceBranch: undefined,
140
+ })),
141
+ };
142
+ }
143
+ if (branchChoice === 'default') {
144
+ // Keep resolved repos as-is (no new branch)
145
+ return resolved;
146
+ }
147
+ return resolved;
148
+ }
149
+ /**
150
+ * Prompt for branch options interactively (for rebuild command)
151
+ * Simpler version - just switch branches or create new
152
+ */
153
+ async function promptForBranchOptionsRebuild(resolved, config) {
154
+ // Get the default branch from config or first repo
155
+ const defaultBranch = config.defaults?.branch || resolved.repos[0]?.branch || 'main';
156
+ console.log(chalk_1.default.blue('=== Branch Configuration ==='));
157
+ console.log(chalk_1.default.dim(`Default branch: ${defaultBranch}`));
158
+ console.log('');
159
+ const branchChoice = await prompts.select({
160
+ message: 'Branch option:',
161
+ choices: [
162
+ {
163
+ name: `Use default branch (${defaultBranch})`,
164
+ value: 'default',
165
+ },
166
+ {
167
+ name: 'Use a different existing branch',
168
+ value: 'existing',
169
+ },
170
+ {
171
+ name: 'Create a new branch',
172
+ value: 'new',
173
+ },
174
+ ],
175
+ default: 'default',
176
+ });
177
+ if (branchChoice === 'default') {
178
+ return resolved;
179
+ }
180
+ if (branchChoice === 'existing') {
181
+ const branchName = await prompts.input({
182
+ message: 'Enter branch name:',
183
+ default: defaultBranch,
184
+ validate: (value) => {
185
+ if (!value.trim())
186
+ return 'Branch name is required';
187
+ return true;
188
+ },
189
+ });
190
+ return {
191
+ ...resolved,
192
+ repos: resolved.repos.map(repo => ({
193
+ ...repo,
194
+ branch: branchName.trim(),
195
+ newBranch: undefined,
196
+ sourceBranch: undefined,
197
+ })),
198
+ };
199
+ }
200
+ if (branchChoice === 'new') {
201
+ const newBranchName = await prompts.input({
202
+ message: 'New branch name:',
203
+ validate: (value) => {
204
+ if (!value.trim())
205
+ return 'Branch name is required';
206
+ if (!/^[\w\-./]+$/.test(value))
207
+ return 'Invalid branch name (use letters, numbers, -, _, /, .)';
208
+ return true;
209
+ },
210
+ });
211
+ const sourceBranch = await prompts.input({
212
+ message: 'Create from branch:',
213
+ default: defaultBranch,
214
+ validate: (value) => {
215
+ if (!value.trim())
216
+ return 'Source branch is required';
217
+ return true;
218
+ },
219
+ });
220
+ return {
221
+ ...resolved,
222
+ repos: resolved.repos.map(repo => ({
223
+ ...repo,
224
+ branch: newBranchName.trim(),
225
+ newBranch: newBranchName.trim(),
226
+ sourceBranch: sourceBranch.trim(),
227
+ })),
228
+ };
229
+ }
230
+ return resolved;
231
+ }
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ /**
3
+ * Environment File Parser Utilities
4
+ * Shared functions for parsing and processing .env.genbox files
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.parseEnvGenboxSections = parseEnvGenboxSections;
8
+ exports.buildServiceUrlMap = buildServiceUrlMap;
9
+ exports.buildAppEnvContent = buildAppEnvContent;
10
+ exports.parseEnvVarsFromSection = parseEnvVarsFromSection;
11
+ /**
12
+ * Parse .env.genbox file into segregated sections
13
+ * Sections are marked with: # === SECTION_NAME ===
14
+ */
15
+ function parseEnvGenboxSections(content) {
16
+ const sections = new Map();
17
+ let currentSection = 'GLOBAL';
18
+ let currentContent = [];
19
+ for (const line of content.split('\n')) {
20
+ const sectionMatch = line.match(/^# === ([^=]+) ===$/);
21
+ if (sectionMatch) {
22
+ // Save previous section
23
+ if (currentContent.length > 0) {
24
+ sections.set(currentSection, currentContent.join('\n').trim());
25
+ }
26
+ currentSection = sectionMatch[1].trim();
27
+ currentContent = [];
28
+ }
29
+ else if (currentSection !== 'END') {
30
+ currentContent.push(line);
31
+ }
32
+ }
33
+ // Save last section
34
+ if (currentContent.length > 0 && currentSection !== 'END') {
35
+ sections.set(currentSection, currentContent.join('\n').trim());
36
+ }
37
+ return sections;
38
+ }
39
+ /**
40
+ * Build a map of service URL variables based on connection type
41
+ * e.g., if connectTo=staging: GATEWAY_URL → STAGING_GATEWAY_URL value
42
+ */
43
+ function buildServiceUrlMap(envVarsFromFile, connectTo) {
44
+ const urlMap = {};
45
+ const prefix = connectTo ? `${connectTo.toUpperCase()}_` : 'LOCAL_';
46
+ // Find all service URL variables (LOCAL_*_URL and STAGING_*_URL patterns)
47
+ const serviceNames = new Set();
48
+ for (const key of Object.keys(envVarsFromFile)) {
49
+ const match = key.match(/^(LOCAL|STAGING|PRODUCTION)_(.+_URL)$/);
50
+ if (match) {
51
+ serviceNames.add(match[2]);
52
+ }
53
+ }
54
+ // Build mapping: VARNAME → value from appropriate prefix
55
+ for (const serviceName of serviceNames) {
56
+ const prefixedKey = `${prefix}${serviceName}`;
57
+ const localKey = `LOCAL_${serviceName}`;
58
+ // Use prefixed value if available, otherwise fall back to local
59
+ const value = envVarsFromFile[prefixedKey] || envVarsFromFile[localKey];
60
+ if (value) {
61
+ urlMap[serviceName] = value;
62
+ }
63
+ }
64
+ // Also handle legacy API_URL for backwards compatibility
65
+ if (!urlMap['API_URL']) {
66
+ const apiUrl = envVarsFromFile[`${prefix}API_URL`] ||
67
+ envVarsFromFile['LOCAL_API_URL'] ||
68
+ envVarsFromFile['STAGING_API_URL'];
69
+ if (apiUrl) {
70
+ urlMap['API_URL'] = apiUrl;
71
+ }
72
+ }
73
+ return urlMap;
74
+ }
75
+ /**
76
+ * Build env content for a specific app by combining GLOBAL + app-specific sections
77
+ */
78
+ function buildAppEnvContent(sections, appName, serviceUrlMap) {
79
+ const parts = [];
80
+ // Always include GLOBAL section
81
+ const globalSection = sections.get('GLOBAL');
82
+ if (globalSection) {
83
+ parts.push(globalSection);
84
+ }
85
+ // Include app-specific section if exists
86
+ const appSection = sections.get(appName);
87
+ if (appSection) {
88
+ parts.push(appSection);
89
+ }
90
+ let envContent = parts.join('\n\n');
91
+ // Expand all ${VARNAME} references using the service URL map
92
+ for (const [varName, value] of Object.entries(serviceUrlMap)) {
93
+ const pattern = new RegExp(`\\$\\{${varName}\\}`, 'g');
94
+ envContent = envContent.replace(pattern, value);
95
+ }
96
+ // Keep only actual env vars (filter out pure comment lines but keep var definitions)
97
+ envContent = envContent
98
+ .split('\n')
99
+ .filter(line => {
100
+ const trimmed = line.trim();
101
+ // Keep empty lines, lines with = (even if commented), and non-comment lines
102
+ return trimmed === '' || trimmed.includes('=') || !trimmed.startsWith('#');
103
+ })
104
+ .join('\n')
105
+ .replace(/\n{3,}/g, '\n\n')
106
+ .trim();
107
+ return envContent;
108
+ }
109
+ /**
110
+ * Parse env vars from a section content string
111
+ */
112
+ function parseEnvVarsFromSection(sectionContent) {
113
+ const envVars = {};
114
+ for (const line of sectionContent.split('\n')) {
115
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
116
+ if (match) {
117
+ let value = match[2].trim();
118
+ // Remove quotes if present
119
+ if ((value.startsWith('"') && value.endsWith('"')) ||
120
+ (value.startsWith("'") && value.endsWith("'"))) {
121
+ value = value.slice(1, -1);
122
+ }
123
+ envVars[match[1]] = value;
124
+ }
125
+ }
126
+ return envVars;
127
+ }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ /**
3
+ * Git Utilities
4
+ * Shared functions for git configuration and validation
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.validateGitCredentials = validateGitCredentials;
8
+ exports.getGitConfig = getGitConfig;
9
+ const child_process_1 = require("child_process");
10
+ /**
11
+ * Validate git configuration and warn about missing credentials
12
+ */
13
+ function validateGitCredentials(repos, gitToken, privateKey) {
14
+ const warnings = [];
15
+ const errors = [];
16
+ for (const repo of repos) {
17
+ const isHttps = repo.url.startsWith('https://');
18
+ const isSsh = repo.url.startsWith('git@') || repo.url.includes('ssh://');
19
+ const isPrivateRepo = repo.url.includes('github.com') && !repo.url.includes('/public/');
20
+ if (isHttps && !gitToken && isPrivateRepo) {
21
+ warnings.push(`Repository '${repo.name}' uses HTTPS URL but GIT_TOKEN is not set.`);
22
+ warnings.push(` Add GIT_TOKEN=<your-github-token> to .env.genbox for private repos.`);
23
+ }
24
+ if (isSsh && !privateKey) {
25
+ warnings.push(`Repository '${repo.name}' uses SSH URL but no SSH key was injected.`);
26
+ }
27
+ }
28
+ return { warnings, errors };
29
+ }
30
+ /**
31
+ * Get local git config for commits on genbox
32
+ */
33
+ function getGitConfig() {
34
+ let userName;
35
+ let userEmail;
36
+ try {
37
+ userName = (0, child_process_1.execSync)('git config --global user.name', { encoding: 'utf-8' }).trim();
38
+ }
39
+ catch {
40
+ // Git config not set
41
+ }
42
+ try {
43
+ userEmail = (0, child_process_1.execSync)('git config --global user.email', { encoding: 'utf-8' }).trim();
44
+ }
45
+ catch {
46
+ // Git config not set
47
+ }
48
+ return { userName, userEmail };
49
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ /**
3
+ * Shared Utilities
4
+ * Re-exports all utility modules for convenient importing
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
18
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ __exportStar(require("./ssh-keys"), exports);
22
+ __exportStar(require("./env-parser"), exports);
23
+ __exportStar(require("./git-utils"), exports);
24
+ __exportStar(require("./branch-prompt"), exports);
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ /**
3
+ * SSH Key Utilities
4
+ * Shared functions for reading SSH keys
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.getPublicSshKey = getPublicSshKey;
41
+ exports.getPrivateSshKey = getPrivateSshKey;
42
+ exports.getPrivateSshKeyPath = getPrivateSshKeyPath;
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const os = __importStar(require("os"));
46
+ /**
47
+ * Get the public SSH key content
48
+ * Tries ed25519 first, then RSA
49
+ */
50
+ function getPublicSshKey() {
51
+ const home = os.homedir();
52
+ const potentialKeys = [
53
+ path.join(home, '.ssh', 'id_ed25519.pub'),
54
+ path.join(home, '.ssh', 'id_rsa.pub'),
55
+ ];
56
+ for (const keyPath of potentialKeys) {
57
+ if (fs.existsSync(keyPath)) {
58
+ const content = fs.readFileSync(keyPath, 'utf-8').trim();
59
+ if (content)
60
+ return content;
61
+ }
62
+ }
63
+ throw new Error('No public SSH key found in ~/.ssh/');
64
+ }
65
+ /**
66
+ * Get the private SSH key content (for injection into remote server)
67
+ * Returns undefined if no key found
68
+ */
69
+ function getPrivateSshKey() {
70
+ const home = os.homedir();
71
+ const potentialKeys = [
72
+ path.join(home, '.ssh', 'id_ed25519'),
73
+ path.join(home, '.ssh', 'id_rsa'),
74
+ ];
75
+ for (const keyPath of potentialKeys) {
76
+ if (fs.existsSync(keyPath)) {
77
+ return fs.readFileSync(keyPath, 'utf-8');
78
+ }
79
+ }
80
+ return undefined;
81
+ }
82
+ /**
83
+ * Get the path to the private SSH key (for local SSH commands)
84
+ * Throws if no key found
85
+ */
86
+ function getPrivateSshKeyPath() {
87
+ const home = os.homedir();
88
+ const potentialKeys = [
89
+ path.join(home, '.ssh', 'id_ed25519'),
90
+ path.join(home, '.ssh', 'id_rsa'),
91
+ ];
92
+ for (const keyPath of potentialKeys) {
93
+ if (fs.existsSync(keyPath)) {
94
+ return keyPath;
95
+ }
96
+ }
97
+ throw new Error('No SSH private key found in ~/.ssh/');
98
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.64",
3
+ "version": "1.0.65",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {