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.
- package/dist/api.js +24 -0
- package/dist/commands/backup.js +410 -0
- package/dist/commands/backups.js +193 -0
- package/dist/commands/create.js +254 -4
- package/dist/commands/extend.js +11 -2
- package/dist/commands/init.js +158 -22
- package/dist/commands/status.js +74 -8
- package/dist/index.js +5 -1
- package/package.json +1 -1
package/dist/commands/create.js
CHANGED
|
@@ -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
|
-
//
|
|
605
|
-
|
|
606
|
-
|
|
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}`));
|
package/dist/commands/extend.js
CHANGED
|
@@ -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 ===
|
|
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
|
|
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({
|
package/dist/commands/init.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
1026
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
2068
|
+
const result = {
|
|
2069
|
+
values: {},
|
|
2070
|
+
serviceUrlMappings: [],
|
|
2071
|
+
rawContent: '',
|
|
2072
|
+
};
|
|
1972
2073
|
if (!fs_1.default.existsSync(envPath))
|
|
1973
|
-
return
|
|
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
|
|
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
|
|
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,
|
|
2265
|
+
const { environments, serviceUrlMappings, envVars: envEnvVars } = await setupEnvironmentsAndServiceUrls(detected, existingEnvData);
|
|
2130
2266
|
// =========================================
|
|
2131
2267
|
// PHASE 6: Profiles
|
|
2132
2268
|
// =========================================
|