genbox 1.0.101 → 1.0.103

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.
@@ -81,6 +81,151 @@ function spawnSshConfigSetup(genboxId, name) {
81
81
  // Allow parent to exit independently
82
82
  child.unref();
83
83
  }
84
+ /**
85
+ * Wait for genbox to reach a specific status
86
+ */
87
+ async function waitForGenboxStatus(genboxId, targetStatus, maxAttempts = 120, // 10 minutes with 5s interval
88
+ intervalMs = 5000) {
89
+ for (let i = 0; i < maxAttempts; i++) {
90
+ try {
91
+ const genbox = await (0, api_1.fetchApi)(`/genboxes/${genboxId}`);
92
+ if (targetStatus.includes(genbox.status)) {
93
+ return { success: true, genbox };
94
+ }
95
+ if (genbox.status === 'failed' || genbox.status === 'error') {
96
+ return { success: false, error: `Genbox provisioning failed: ${genbox.statusMessage || 'unknown error'}` };
97
+ }
98
+ }
99
+ catch (error) {
100
+ // Ignore fetch errors and continue polling
101
+ }
102
+ await new Promise(resolve => setTimeout(resolve, intervalMs));
103
+ }
104
+ return { success: false, error: 'Timeout waiting for genbox to be ready' };
105
+ }
106
+ /**
107
+ * Run restore script on genbox via SSH
108
+ */
109
+ async function runRestore(ipAddress, downloadUrl, backup, onProgress) {
110
+ return new Promise((resolve) => {
111
+ // Build restore script
112
+ const restoreScript = `
113
+ set -e
114
+
115
+ BACKUP_DIR="/tmp/backup-restore"
116
+ DOWNLOAD_URL="${downloadUrl}"
117
+
118
+ echo "=== Starting Restore ==="
119
+ echo "Backup: ${backup.name}"
120
+
121
+ # Create temp directory
122
+ rm -rf "$BACKUP_DIR"
123
+ mkdir -p "$BACKUP_DIR"
124
+ cd "$BACKUP_DIR"
125
+
126
+ # Download backup
127
+ echo "Downloading backup..."
128
+ curl -sS -L -o backup.tar.gz "$DOWNLOAD_URL"
129
+ if [ ! -f backup.tar.gz ]; then
130
+ echo "ERROR: Failed to download backup"
131
+ exit 1
132
+ fi
133
+
134
+ # Extract
135
+ echo "Extracting backup..."
136
+ tar -xzf backup.tar.gz
137
+ rm backup.tar.gz
138
+
139
+ # Restore git patches (uncommitted changes)
140
+ if [ -d "repos" ]; then
141
+ echo "Restoring git changes..."
142
+ for patch in repos/*.patch; do
143
+ if [ -f "$patch" ]; then
144
+ # Extract repo path from patch filename (format: repo-path.patch)
145
+ repo_name=$(basename "$patch" .patch)
146
+ # Try to find the repo
147
+ if [ -d "/home/dev/$repo_name" ]; then
148
+ cd "/home/dev/$repo_name"
149
+ git apply "$BACKUP_DIR/repos/$repo_name.patch" 2>/dev/null || echo " Warning: Could not apply patch for $repo_name"
150
+ cd "$BACKUP_DIR"
151
+ echo " Applied patch: $repo_name"
152
+ fi
153
+ fi
154
+ done
155
+ fi
156
+
157
+ # Restore database
158
+ if [ -f "database.archive.gz" ]; then
159
+ echo "Restoring database..."
160
+ DB_NAME="${backup.database?.name || 'genbox_db'}"
161
+ mongorestore --gzip --archive="database.archive.gz" --nsFrom="\${DB_NAME}.*" --nsTo="\${DB_NAME}.*" --drop 2>/dev/null || {
162
+ # Try without namespace mapping
163
+ mongorestore --gzip --archive="database.archive.gz" --drop 2>/dev/null || echo " Warning: Database restore had issues"
164
+ }
165
+ echo " Database restored"
166
+ fi
167
+
168
+ # Restore Claude history
169
+ if [ -f "claude-history.tar.gz" ]; then
170
+ echo "Restoring Claude history..."
171
+ mkdir -p /home/dev/.claude
172
+ tar -xzf claude-history.tar.gz -C /home/dev/.claude 2>/dev/null || true
173
+ echo " Claude history restored"
174
+ fi
175
+
176
+ # Restore config files
177
+ if [ -f "configs.tar.gz" ]; then
178
+ echo "Restoring config files..."
179
+ tar -xzf configs.tar.gz -C /home/dev 2>/dev/null || true
180
+ echo " Config files restored"
181
+ fi
182
+
183
+ # Cleanup
184
+ rm -rf "$BACKUP_DIR"
185
+
186
+ echo ""
187
+ echo "=== Restore Complete ==="
188
+ echo "Run 'gb restart' to start services"
189
+ `;
190
+ const ssh = (0, child_process_1.spawn)('ssh', [
191
+ '-o', 'StrictHostKeyChecking=no',
192
+ '-o', 'UserKnownHostsFile=/dev/null',
193
+ '-o', 'ConnectTimeout=30',
194
+ `dev@${ipAddress}`,
195
+ restoreScript
196
+ ]);
197
+ let stdout = '';
198
+ let stderr = '';
199
+ ssh.stdout.on('data', (data) => {
200
+ const line = data.toString();
201
+ stdout += line;
202
+ if (onProgress) {
203
+ // Extract meaningful progress messages
204
+ const lines = line.split('\n').filter((l) => l.trim());
205
+ for (const l of lines) {
206
+ if (l.startsWith('===') || l.startsWith('Downloading') || l.startsWith('Extracting') ||
207
+ l.startsWith('Restoring') || l.startsWith(' ')) {
208
+ onProgress(l.trim());
209
+ }
210
+ }
211
+ }
212
+ });
213
+ ssh.stderr.on('data', (data) => {
214
+ stderr += data.toString();
215
+ });
216
+ ssh.on('close', (code) => {
217
+ if (code === 0) {
218
+ resolve({ success: true });
219
+ }
220
+ else {
221
+ resolve({ success: false, error: stderr || `SSH exited with code ${code}` });
222
+ }
223
+ });
224
+ ssh.on('error', (error) => {
225
+ resolve({ success: false, error: error.message });
226
+ });
227
+ });
228
+ }
84
229
  const DETECTED_DIR = '.genbox';
85
230
  const PROJECT_CACHE_FILENAME = 'project.json';
86
231
  /**
@@ -197,8 +342,12 @@ exports.createCommand = new commander_1.Command('create')
197
342
  .option('-f, --from-branch <branch>', 'Source branch to create new branch from (defaults to config default or main)')
198
343
  .option('-y, --yes', 'Skip interactive prompts')
199
344
  .option('--dry-run', 'Show what would be created without actually creating')
345
+ .option('-r, --restore', 'Restore from backup (uses genbox name to find backup)')
200
346
  .action(async (nameArg, options) => {
201
347
  try {
348
+ // Handle restore mode
349
+ let restoreBackup = null;
350
+ let restoreDownloadUrl = null;
202
351
  // Load configuration
203
352
  const configLoader = new config_loader_1.ConfigLoader();
204
353
  const loadResult = await configLoader.load();
@@ -217,6 +366,66 @@ exports.createCommand = new commander_1.Command('create')
217
366
  // Support both v3 and v4 configs
218
367
  const config = loadResult.config;
219
368
  const profileResolver = new profile_resolver_1.ProfileResolver(configLoader);
369
+ // Get workspace early for backup resolution
370
+ const workspace = config.project?.name || 'default';
371
+ // Handle --restore option: fetch backup info by genbox name
372
+ if (options.restore) {
373
+ // Require genbox name for restore
374
+ if (!nameArg) {
375
+ console.log(chalk_1.default.red('Name required for restore'));
376
+ console.log(chalk_1.default.dim(' Usage: gb create <genbox-name> --restore'));
377
+ return;
378
+ }
379
+ console.log(chalk_1.default.blue('=== Restore Mode ==='));
380
+ console.log('');
381
+ const restoreSpinner = (0, ora_1.default)(`Looking for backup of '${nameArg}'...`).start();
382
+ try {
383
+ // Find backup by genbox name
384
+ restoreBackup = await (0, api_1.getLatestBackup)(nameArg, workspace);
385
+ if (!restoreBackup) {
386
+ restoreSpinner.fail(chalk_1.default.red(`No backup found for '${nameArg}'`));
387
+ console.log('');
388
+ console.log(chalk_1.default.dim(' Create a backup first with: gb backup ' + nameArg));
389
+ return;
390
+ }
391
+ // Verify backup is completed
392
+ if (restoreBackup.status !== 'completed') {
393
+ restoreSpinner.fail(chalk_1.default.red(`Backup is not ready (status: ${restoreBackup.status})`));
394
+ return;
395
+ }
396
+ // Get download URL
397
+ const downloadResult = await (0, api_1.getBackupDownloadUrl)(restoreBackup._id);
398
+ restoreDownloadUrl = downloadResult.downloadUrl;
399
+ restoreSpinner.succeed(chalk_1.default.green('Backup found'));
400
+ // Display backup details
401
+ console.log('');
402
+ console.log(` ${chalk_1.default.bold('Size:')} ${restoreBackup.sourceSize}`);
403
+ console.log(` ${chalk_1.default.bold('Backed up:')} ${new Date(restoreBackup.createdAt).toLocaleString()}`);
404
+ // Show contents
405
+ const contents = [];
406
+ if (restoreBackup.repos && restoreBackup.repos.some(r => r.hadUncommittedChanges)) {
407
+ contents.push('uncommitted changes');
408
+ }
409
+ if (restoreBackup.database) {
410
+ contents.push('database');
411
+ }
412
+ if (restoreBackup.claudeHistoryIncluded) {
413
+ contents.push('Claude history');
414
+ }
415
+ if (contents.length > 0) {
416
+ console.log(` ${chalk_1.default.bold('Contains:')} ${contents.join(', ')}`);
417
+ }
418
+ console.log('');
419
+ }
420
+ catch (error) {
421
+ restoreSpinner.fail(chalk_1.default.red(`Failed to fetch backup: ${error.message}`));
422
+ if (error instanceof api_1.AuthenticationError) {
423
+ console.log(chalk_1.default.yellow(' Please authenticate first:'));
424
+ console.log(chalk_1.default.cyan(' $ genbox login'));
425
+ }
426
+ return;
427
+ }
428
+ }
220
429
  // Interactive name prompt if not provided
221
430
  let name = nameArg;
222
431
  if (!name && !options.yes) {
@@ -233,7 +442,6 @@ exports.createCommand = new commander_1.Command('create')
233
442
  selectedProfile = await promptForProfile(config.profiles);
234
443
  }
235
444
  // Check if name is available in workspace, add suffix if taken
236
- const workspace = config.project?.name || 'default';
237
445
  try {
238
446
  let { available } = await (0, api_1.checkNameAvailability)(name, workspace);
239
447
  if (!available) {
@@ -601,9 +809,51 @@ exports.createCommand = new commander_1.Command('create')
601
809
  }
602
810
  // Display results
603
811
  displayGenboxInfo(genbox, resolved);
604
- // Inform user about server provisioning
605
- console.log('');
606
- console.log(chalk_1.default.dim('Server is provisioning. Run `genbox connect` once ready.'));
812
+ // Handle restore if backup was specified
813
+ if (restoreBackup && restoreDownloadUrl && genbox._id) {
814
+ console.log('');
815
+ console.log(chalk_1.default.blue('=== Restoring from Backup ==='));
816
+ // Wait for genbox to be ready
817
+ const waitSpinner = (0, ora_1.default)('Waiting for genbox to be ready...').start();
818
+ const waitResult = await waitForGenboxStatus(genbox._id, ['running']);
819
+ if (!waitResult.success) {
820
+ waitSpinner.fail(chalk_1.default.red(waitResult.error || 'Failed to wait for genbox'));
821
+ console.log(chalk_1.default.dim(' You can manually restore later: gb connect, then run the restore script'));
822
+ return;
823
+ }
824
+ waitSpinner.succeed(chalk_1.default.green('Genbox is ready'));
825
+ // Get IP address from the ready genbox
826
+ const readyGenbox = waitResult.genbox;
827
+ const ipAddress = readyGenbox?.ipAddress || genbox.ipAddress;
828
+ if (!ipAddress) {
829
+ console.log(chalk_1.default.red('Could not get genbox IP address'));
830
+ console.log(chalk_1.default.dim(' You can manually restore by connecting and downloading the backup'));
831
+ return;
832
+ }
833
+ // Run restore
834
+ const restoreSpinner = (0, ora_1.default)('Restoring backup...').start();
835
+ const restoreResult = await runRestore(ipAddress, restoreDownloadUrl, restoreBackup, (msg) => {
836
+ restoreSpinner.text = msg;
837
+ });
838
+ if (restoreResult.success) {
839
+ restoreSpinner.succeed(chalk_1.default.green('Backup restored successfully!'));
840
+ console.log('');
841
+ console.log(chalk_1.default.green('✓ Restore complete'));
842
+ console.log('');
843
+ console.log(chalk_1.default.bold('Next steps:'));
844
+ console.log(` 1. Connect: ${chalk_1.default.cyan(`gb connect ${name}`)}`);
845
+ console.log(` 2. Start services: ${chalk_1.default.cyan('gb restart')}`);
846
+ }
847
+ else {
848
+ restoreSpinner.fail(chalk_1.default.red(`Restore failed: ${restoreResult.error}`));
849
+ console.log(chalk_1.default.dim(' You can manually restore by connecting and running the restore script'));
850
+ }
851
+ }
852
+ else {
853
+ // Inform user about server provisioning
854
+ console.log('');
855
+ console.log(chalk_1.default.dim('Server is provisioning. Run `genbox connect` once ready.'));
856
+ }
607
857
  }
608
858
  catch (error) {
609
859
  spinner.fail(chalk_1.default.red(`Failed to create Genbox: ${error.message}`));
@@ -53,9 +53,15 @@ exports.extendCommand = new commander_1.Command('extend')
53
53
  { name: '4 hours', value: 4 },
54
54
  { name: '8 hours', value: 8 },
55
55
  { name: 'Custom...', value: 0 },
56
+ { name: 'Remove auto expiry', value: -1 },
56
57
  ],
57
58
  });
58
- if (choice === 0) {
59
+ if (choice === -1) {
60
+ // Remove auto expiry - set minimal hours and disable auto-destroy
61
+ hours = 1;
62
+ options.autoDestroy = false; // This will set disableAutoDestroy = true
63
+ }
64
+ else if (choice === 0) {
59
65
  const customHours = await (0, input_1.default)({
60
66
  message: 'Enter number of hours:',
61
67
  validate: (val) => {
@@ -81,7 +87,10 @@ exports.extendCommand = new commander_1.Command('extend')
81
87
  else if (options.enableAutoDestroy) {
82
88
  disableAutoDestroy = false; // --enable-auto-destroy flag
83
89
  }
84
- const spinner = (0, ora_1.default)(`Extending ${target.name} by ${hours} hour${hours > 1 ? 's' : ''}...`).start();
90
+ const spinnerText = disableAutoDestroy
91
+ ? `Removing auto expiry for ${target.name}...`
92
+ : `Extending ${target.name} by ${hours} hour${hours > 1 ? 's' : ''}...`;
93
+ const spinner = (0, ora_1.default)(spinnerText).start();
85
94
  const result = await (0, api_1.fetchApi)(`/genboxes/${target._id}/extend`, {
86
95
  method: 'POST',
87
96
  body: JSON.stringify({
@@ -333,6 +333,21 @@ function loadDetectedConfig(rootDir) {
333
333
  return null;
334
334
  }
335
335
  }
336
+ /**
337
+ * Load existing genbox.yaml config if present
338
+ */
339
+ function loadExistingConfig(rootDir) {
340
+ const configPath = path_1.default.join(rootDir, CONFIG_FILENAME);
341
+ if (!fs_1.default.existsSync(configPath))
342
+ return null;
343
+ try {
344
+ const content = fs_1.default.readFileSync(configPath, 'utf8');
345
+ return yaml.load(content);
346
+ }
347
+ catch {
348
+ return null;
349
+ }
350
+ }
336
351
  /**
337
352
  * Save detected config to .genbox/detected.yaml
338
353
  */
@@ -922,7 +937,8 @@ async function selectScripts(detected) {
922
937
  /**
923
938
  * Combined environment and service URL configuration
924
939
  */
925
- async function setupEnvironmentsAndServiceUrls(detected, existingEnvValues) {
940
+ async function setupEnvironmentsAndServiceUrls(detected, existingEnvData) {
941
+ const existingEnvValues = existingEnvData.values;
926
942
  const envVars = {};
927
943
  let environments = {};
928
944
  console.log('');
@@ -998,8 +1014,8 @@ async function setupEnvironmentsAndServiceUrls(detected, existingEnvValues) {
998
1014
  }
999
1015
  }
1000
1016
  }
1001
- // Service URL detection and mapping
1002
- const serviceUrlMappings = await setupServiceUrls(detected, environments, existingEnvValues);
1017
+ // Service URL detection and mapping (pass existing mappings for retention option)
1018
+ const serviceUrlMappings = await setupServiceUrls(detected, environments, existingEnvData);
1003
1019
  // Always add LOCAL_API_URL - get port from detected api app
1004
1020
  const apiApp = detected.apps?.['api'];
1005
1021
  const apiPort = apiApp?.port || 3050;
@@ -1013,29 +1029,64 @@ async function setupEnvironmentsAndServiceUrls(detected, existingEnvValues) {
1013
1029
  /**
1014
1030
  * Setup service URL mappings for frontend apps
1015
1031
  */
1016
- async function setupServiceUrls(detected, environments, existingEnvValues) {
1032
+ async function setupServiceUrls(detected, environments, existingEnvData) {
1033
+ const existingMappings = existingEnvData.serviceUrlMappings;
1034
+ const mappings = [];
1035
+ // Check for existing service URL mappings first
1036
+ if (existingMappings.length > 0) {
1037
+ console.log('');
1038
+ console.log(chalk_1.default.blue('=== Existing Service URL Mappings ==='));
1039
+ console.log(chalk_1.default.dim('Found service URL mappings in existing .env.genbox:'));
1040
+ console.log('');
1041
+ for (const mapping of existingMappings) {
1042
+ console.log(` ${chalk_1.default.cyan(mapping.localUrl)}`);
1043
+ if (mapping.remoteUrl && mapping.remoteEnv) {
1044
+ console.log(chalk_1.default.dim(` → ${mapping.remoteEnv}: ${mapping.remoteUrl}`));
1045
+ }
1046
+ }
1047
+ console.log('');
1048
+ // Let user select which existing mappings to retain
1049
+ const retainChoices = existingMappings.map(m => ({
1050
+ name: `${m.localUrl}${m.remoteUrl ? ` → ${m.remoteUrl}` : ''}`,
1051
+ value: m.localUrl,
1052
+ checked: true,
1053
+ }));
1054
+ const retainedUrls = await prompts.checkbox({
1055
+ message: 'Select existing service URL mappings to retain:',
1056
+ choices: retainChoices,
1057
+ });
1058
+ // Add retained mappings
1059
+ for (const mapping of existingMappings) {
1060
+ if (retainedUrls.includes(mapping.localUrl)) {
1061
+ mappings.push(mapping);
1062
+ }
1063
+ }
1064
+ }
1017
1065
  const frontendApps = Object.entries(detected.apps)
1018
1066
  .filter(([, app]) => app.type === 'frontend')
1019
1067
  .map(([name]) => name);
1020
1068
  if (frontendApps.length === 0) {
1021
- return [];
1069
+ return mappings;
1022
1070
  }
1023
1071
  // Scan env files for service URLs
1024
1072
  const serviceUrls = scanEnvFilesForUrls(detected.apps, detected._meta.scanned_root);
1025
- if (serviceUrls.length === 0) {
1026
- return [];
1073
+ // Filter out URLs that are already in retained mappings
1074
+ const retainedLocalUrls = new Set(mappings.map(m => m.localUrl));
1075
+ const newServiceUrls = serviceUrls.filter(svc => !retainedLocalUrls.has(svc.base_url));
1076
+ if (newServiceUrls.length === 0) {
1077
+ return mappings;
1027
1078
  }
1028
1079
  console.log('');
1029
1080
  console.log(chalk_1.default.blue('=== Service URL Configuration ==='));
1030
1081
  console.log(chalk_1.default.dim('Detected local service URLs in frontend env files:'));
1031
1082
  console.log('');
1032
- for (const svc of serviceUrls) {
1083
+ for (const svc of newServiceUrls) {
1033
1084
  console.log(` ${chalk_1.default.cyan(svc.base_url)}`);
1034
1085
  console.log(chalk_1.default.dim(` Used by: ${svc.used_by.slice(0, 3).join(', ')}${svc.used_by.length > 3 ? ` +${svc.used_by.length - 3} more` : ''}`));
1035
1086
  }
1036
1087
  console.log('');
1037
1088
  // Let user select which to configure
1038
- const urlChoices = serviceUrls.map(svc => ({
1089
+ const urlChoices = newServiceUrls.map(svc => ({
1039
1090
  name: `${svc.base_url} (${svc.used_by.length} var${svc.used_by.length > 1 ? 's' : ''})`,
1040
1091
  value: svc.base_url,
1041
1092
  checked: true,
@@ -1044,8 +1095,7 @@ async function setupServiceUrls(detected, environments, existingEnvValues) {
1044
1095
  message: 'Select service URLs to configure for remote environments:',
1045
1096
  choices: urlChoices,
1046
1097
  });
1047
- const selectedServices = serviceUrls.filter(svc => selectedUrls.includes(svc.base_url));
1048
- const mappings = [];
1098
+ const selectedServices = newServiceUrls.filter(svc => selectedUrls.includes(svc.base_url));
1049
1099
  // Determine primary remote environment
1050
1100
  const envNames = Object.keys(environments || {});
1051
1101
  const primaryEnv = envNames.includes('staging') ? 'staging' :
@@ -1100,16 +1150,63 @@ async function setupProfiles(detected, environments) {
1100
1150
  console.log(chalk_1.default.blue('=== Profile Configuration ==='));
1101
1151
  console.log('');
1102
1152
  // Generate default profiles
1103
- const profiles = generateDefaultProfiles(detected, environments);
1153
+ const defaultProfiles = generateDefaultProfiles(detected, environments);
1154
+ const defaultProfileNames = new Set(Object.keys(defaultProfiles));
1155
+ // Load existing profiles from genbox.yaml
1156
+ const existingConfig = loadExistingConfig(detected._meta.scanned_root);
1157
+ const existingProfiles = existingConfig?.profiles || {};
1158
+ // Identify user-created profiles (profiles that don't match auto-generated names)
1159
+ const userCreatedProfiles = {};
1160
+ for (const [name, profile] of Object.entries(existingProfiles)) {
1161
+ if (!defaultProfileNames.has(name)) {
1162
+ userCreatedProfiles[name] = profile;
1163
+ }
1164
+ }
1165
+ // Start with default profiles
1166
+ let profiles = { ...defaultProfiles };
1167
+ // If there are existing profiles (including user-created ones), offer to retain them
1168
+ if (Object.keys(existingProfiles).length > 0) {
1169
+ console.log(chalk_1.default.dim('Found existing profiles in genbox.yaml:'));
1170
+ console.log('');
1171
+ // Display existing profiles with indication if they're user-created
1172
+ for (const [name, profile] of Object.entries(existingProfiles)) {
1173
+ const isUserCreated = !defaultProfileNames.has(name);
1174
+ const label = isUserCreated ? chalk_1.default.yellow(' (user-created)') : chalk_1.default.dim(' (auto-generated)');
1175
+ console.log(` ${chalk_1.default.cyan(name)}${label}`);
1176
+ console.log(chalk_1.default.dim(` ${profile.description || 'No description'}`));
1177
+ console.log(chalk_1.default.dim(` Apps: ${profile.apps?.join(', ') || 'all'}`));
1178
+ console.log('');
1179
+ }
1180
+ // Let user select which existing profiles to retain
1181
+ const retainChoices = Object.entries(existingProfiles).map(([name, profile]) => {
1182
+ const isUserCreated = !defaultProfileNames.has(name);
1183
+ return {
1184
+ name: `${name}${isUserCreated ? ' (user-created)' : ''} - ${profile.description || 'No description'}`,
1185
+ value: name,
1186
+ checked: isUserCreated, // User-created profiles are checked by default
1187
+ };
1188
+ });
1189
+ const retainedNames = await prompts.checkbox({
1190
+ message: 'Select existing profiles to retain:',
1191
+ choices: retainChoices,
1192
+ });
1193
+ // Add retained profiles (they override defaults with same name)
1194
+ for (const name of retainedNames) {
1195
+ profiles[name] = existingProfiles[name];
1196
+ }
1197
+ }
1104
1198
  if (Object.keys(profiles).length === 0) {
1105
1199
  console.log(chalk_1.default.dim(' No profiles generated (no runnable apps detected).'));
1106
1200
  return {};
1107
1201
  }
1108
- // Display profiles
1109
- console.log(chalk_1.default.dim('Generated profiles:'));
1202
+ // Display final profiles
1203
+ console.log('');
1204
+ console.log(chalk_1.default.dim('Final profiles:'));
1110
1205
  console.log('');
1111
1206
  for (const [name, profile] of Object.entries(profiles)) {
1112
- console.log(` ${chalk_1.default.cyan(name)}`);
1207
+ const isRetained = existingProfiles[name] && !defaultProfileNames.has(name);
1208
+ const label = isRetained ? chalk_1.default.yellow(' (retained)') : '';
1209
+ console.log(` ${chalk_1.default.cyan(name)}${label}`);
1113
1210
  console.log(chalk_1.default.dim(` ${profile.description || 'No description'}`));
1114
1211
  console.log(chalk_1.default.dim(` Apps: ${profile.apps?.join(', ') || 'all'}`));
1115
1212
  console.log(chalk_1.default.dim(` Size: ${profile.size || 'default'}`));
@@ -1968,11 +2065,17 @@ function httpsToSsh(url) {
1968
2065
  }
1969
2066
  function readExistingEnvGenbox() {
1970
2067
  const envPath = path_1.default.join(process.cwd(), ENV_FILENAME);
1971
- const values = {};
2068
+ const result = {
2069
+ values: {},
2070
+ serviceUrlMappings: [],
2071
+ rawContent: '',
2072
+ };
1972
2073
  if (!fs_1.default.existsSync(envPath))
1973
- return values;
2074
+ return result;
1974
2075
  try {
1975
2076
  const content = fs_1.default.readFileSync(envPath, 'utf8');
2077
+ result.rawContent = content;
2078
+ // Parse all key-value pairs
1976
2079
  for (const line of content.split('\n')) {
1977
2080
  const trimmed = line.trim();
1978
2081
  if (!trimmed || trimmed.startsWith('#'))
@@ -1983,12 +2086,44 @@ function readExistingEnvGenbox() {
1983
2086
  if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
1984
2087
  value = value.slice(1, -1);
1985
2088
  }
1986
- values[match[1]] = value;
2089
+ result.values[match[1]] = value;
2090
+ }
2091
+ }
2092
+ // Parse service URL mappings from the "# Service URL Configuration" section
2093
+ // Pattern: LOCAL_<name>_URL=<url> and <ENV>_<name>_URL=<url>
2094
+ const localUrlEntries = {};
2095
+ const remoteUrlEntries = {};
2096
+ for (const [key, value] of Object.entries(result.values)) {
2097
+ // Match LOCAL_*_URL patterns
2098
+ const localMatch = key.match(/^LOCAL_(.+)_URL$/);
2099
+ if (localMatch && value.startsWith('http')) {
2100
+ localUrlEntries[localMatch[1]] = value;
2101
+ }
2102
+ // Match STAGING_*_URL or PRODUCTION_*_URL patterns (for service URLs)
2103
+ const remoteMatch = key.match(/^(STAGING|PRODUCTION)_(.+)_URL$/);
2104
+ if (remoteMatch && value.startsWith('http')) {
2105
+ const envName = remoteMatch[1].toLowerCase();
2106
+ const varName = remoteMatch[2];
2107
+ // Skip environment-level API URLs like STAGING_API_URL (no PORT_ prefix)
2108
+ if (varName !== 'API' && varName !== 'MONGODB') {
2109
+ remoteUrlEntries[varName] = { url: value, env: envName };
2110
+ }
1987
2111
  }
1988
2112
  }
2113
+ // Build service URL mappings from local entries
2114
+ for (const [varName, localUrl] of Object.entries(localUrlEntries)) {
2115
+ const remote = remoteUrlEntries[varName];
2116
+ result.serviceUrlMappings.push({
2117
+ varName: `${varName}_URL`,
2118
+ localUrl,
2119
+ remoteUrl: remote?.url,
2120
+ remoteEnv: remote?.env,
2121
+ description: `Service at ${localUrl}`,
2122
+ });
2123
+ }
1989
2124
  }
1990
2125
  catch { }
1991
- return values;
2126
+ return result;
1992
2127
  }
1993
2128
  // =============================================================================
1994
2129
  // Main Command
@@ -2017,8 +2152,9 @@ exports.initCommand = new commander_1.Command('init')
2017
2152
  return;
2018
2153
  }
2019
2154
  console.log(chalk_1.default.blue('\nInitializing Genbox...\n'));
2020
- // Read existing .env.genbox values
2021
- const existingEnvValues = readExistingEnvGenbox();
2155
+ // Read existing .env.genbox values and service URL mappings
2156
+ const existingEnvData = readExistingEnvGenbox();
2157
+ const existingEnvValues = existingEnvData.values;
2022
2158
  // =========================================
2023
2159
  // PHASE 1: Scan or Load
2024
2160
  // =========================================
@@ -2126,7 +2262,7 @@ exports.initCommand = new commander_1.Command('init')
2126
2262
  // =========================================
2127
2263
  // PHASE 5: Environments & Service URLs
2128
2264
  // =========================================
2129
- const { environments, serviceUrlMappings, envVars: envEnvVars } = await setupEnvironmentsAndServiceUrls(detected, existingEnvValues);
2265
+ const { environments, serviceUrlMappings, envVars: envEnvVars } = await setupEnvironmentsAndServiceUrls(detected, existingEnvData);
2130
2266
  // =========================================
2131
2267
  // PHASE 6: Profiles
2132
2268
  // =========================================