genbox 1.0.67 → 1.0.68
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/commands/rebuild.js +157 -280
- package/dist/db-utils.js +30 -4
- package/package.json +1 -1
package/dist/commands/rebuild.js
CHANGED
|
@@ -137,7 +137,7 @@ async function scpUpload(localPath, ip, remotePath, keyPath) {
|
|
|
137
137
|
});
|
|
138
138
|
}
|
|
139
139
|
async function runSoftRebuild(options) {
|
|
140
|
-
const { genbox, resolved, config, keyPath, envFiles, snapshotId, snapshotS3Key, gitToken, onStep, onLog } = options;
|
|
140
|
+
const { genbox, resolved, config, keyPath, envFiles, snapshotId, snapshotS3Key, gitToken, hardMode, onStep, onLog } = options;
|
|
141
141
|
const ip = genbox.ipAddress;
|
|
142
142
|
const log = (line, type) => {
|
|
143
143
|
if (onLog) {
|
|
@@ -150,9 +150,16 @@ async function runSoftRebuild(options) {
|
|
|
150
150
|
log('Stopping PM2 processes...', 'dim');
|
|
151
151
|
await sshExec(ip, keyPath, 'source ~/.nvm/nvm.sh 2>/dev/null; pm2 kill 2>/dev/null || true', 30);
|
|
152
152
|
// Find and stop docker compose in repo directories
|
|
153
|
+
// In hard mode, use -v flag to remove volumes
|
|
154
|
+
const dockerDownFlags = hardMode ? 'down -v' : 'down';
|
|
153
155
|
for (const repo of resolved.repos) {
|
|
154
|
-
log(`Stopping Docker Compose in ${repo.path}...`, 'dim');
|
|
155
|
-
await sshExec(ip, keyPath, `cd ${repo.path} 2>/dev/null && docker compose
|
|
156
|
+
log(`Stopping Docker Compose in ${repo.path}${hardMode ? ' (removing volumes)' : ''}...`, 'dim');
|
|
157
|
+
await sshExec(ip, keyPath, `cd ${repo.path} 2>/dev/null && docker compose ${dockerDownFlags} 2>/dev/null || true`, 60);
|
|
158
|
+
}
|
|
159
|
+
// In hard mode, also prune docker volumes
|
|
160
|
+
if (hardMode) {
|
|
161
|
+
log('Pruning unused Docker volumes...', 'dim');
|
|
162
|
+
await sshExec(ip, keyPath, 'docker volume prune -f 2>/dev/null || true', 30);
|
|
156
163
|
}
|
|
157
164
|
// Step 2: Clean up repo directories
|
|
158
165
|
onStep?.('Cleaning up repositories...');
|
|
@@ -272,8 +279,9 @@ async function runSoftRebuild(options) {
|
|
|
272
279
|
}
|
|
273
280
|
}
|
|
274
281
|
}
|
|
275
|
-
// Step 7: Restore database if snapshot provided
|
|
276
|
-
|
|
282
|
+
// Step 7: Restore database if snapshot provided (only in hard mode)
|
|
283
|
+
// In soft mode (default), database is preserved
|
|
284
|
+
if (hardMode && snapshotId && snapshotS3Key) {
|
|
277
285
|
onStep?.('Restoring database...');
|
|
278
286
|
log('Fetching database snapshot...', 'info');
|
|
279
287
|
try {
|
|
@@ -377,107 +385,9 @@ async function runSoftRebuild(options) {
|
|
|
377
385
|
return { success: false, error: error.message };
|
|
378
386
|
}
|
|
379
387
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
body: JSON.stringify(payload),
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
/**
|
|
387
|
-
* Build rebuild payload from resolved config
|
|
388
|
-
*/
|
|
389
|
-
function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoader) {
|
|
390
|
-
const envVars = configLoader.loadEnvVars(process.cwd());
|
|
391
|
-
// Build services map
|
|
392
|
-
const services = {};
|
|
393
|
-
for (const app of resolved.apps) {
|
|
394
|
-
if (app.port) {
|
|
395
|
-
services[app.name] = { port: app.port, healthcheck: app.healthcheck };
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
// Build files bundle
|
|
399
|
-
const files = [];
|
|
400
|
-
const envFilesToMove = [];
|
|
401
|
-
// Process .env.genbox
|
|
402
|
-
const envGenboxPath = path.join(process.cwd(), '.env.genbox');
|
|
403
|
-
if (fs.existsSync(envGenboxPath)) {
|
|
404
|
-
const rawEnvContent = fs.readFileSync(envGenboxPath, 'utf-8');
|
|
405
|
-
const sections = (0, utils_1.parseEnvGenboxSections)(rawEnvContent);
|
|
406
|
-
const globalSection = sections.get('GLOBAL') || '';
|
|
407
|
-
const envVarsFromFile = (0, utils_1.parseEnvVarsFromSection)(globalSection);
|
|
408
|
-
let connectTo;
|
|
409
|
-
if (resolved.profile && config.profiles?.[resolved.profile]) {
|
|
410
|
-
const profile = config.profiles[resolved.profile];
|
|
411
|
-
connectTo = (0, config_loader_1.getProfileConnection)(profile);
|
|
412
|
-
}
|
|
413
|
-
const serviceUrlMap = (0, utils_1.buildServiceUrlMap)(envVarsFromFile, connectTo);
|
|
414
|
-
if (connectTo && Object.keys(serviceUrlMap).length > 0) {
|
|
415
|
-
console.log(chalk_1.default.dim(` Using ${connectTo} URLs for variable expansion`));
|
|
416
|
-
}
|
|
417
|
-
for (const app of resolved.apps) {
|
|
418
|
-
const appPath = config.apps[app.name]?.path || app.name;
|
|
419
|
-
const repoPath = resolved.repos.find(r => r.name === app.name)?.path ||
|
|
420
|
-
(resolved.repos[0]?.path ? `${resolved.repos[0].path}/${appPath}` : `/home/dev/${config.project.name}/${appPath}`);
|
|
421
|
-
const servicesSections = Array.from(sections.keys()).filter(s => s.startsWith(`${app.name}/`));
|
|
422
|
-
if (servicesSections.length > 0) {
|
|
423
|
-
for (const serviceSectionName of servicesSections) {
|
|
424
|
-
const serviceName = serviceSectionName.split('/')[1];
|
|
425
|
-
const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName, serviceUrlMap);
|
|
426
|
-
const stagingName = `${app.name}-${serviceName}.env`;
|
|
427
|
-
const targetPath = `${repoPath}/apps/${serviceName}/.env`;
|
|
428
|
-
files.push({
|
|
429
|
-
path: `/home/dev/.env-staging/${stagingName}`,
|
|
430
|
-
content: serviceEnvContent,
|
|
431
|
-
permissions: '0644',
|
|
432
|
-
});
|
|
433
|
-
envFilesToMove.push({ stagingName, targetPath });
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
else {
|
|
437
|
-
const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name, serviceUrlMap);
|
|
438
|
-
files.push({
|
|
439
|
-
path: `/home/dev/.env-staging/${app.name}.env`,
|
|
440
|
-
content: appEnvContent,
|
|
441
|
-
permissions: '0644',
|
|
442
|
-
});
|
|
443
|
-
envFilesToMove.push({
|
|
444
|
-
stagingName: `${app.name}.env`,
|
|
445
|
-
targetPath: `${repoPath}/.env`,
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
// NOTE: Setup script generation removed from CLI.
|
|
451
|
-
// The API generates a comprehensive setup script with clone commands at /root/setup-genbox.sh
|
|
452
|
-
// and copies it to /home/dev/setup-genbox.sh via runcmd. This avoids having two conflicting
|
|
453
|
-
// scripts in write_files where the CLI version lacked clone commands.
|
|
454
|
-
// The envFilesToMove variable is kept as API needs this info for .env file movement.
|
|
455
|
-
const postDetails = [];
|
|
456
|
-
// Build repos
|
|
457
|
-
const repos = {};
|
|
458
|
-
for (const repo of resolved.repos) {
|
|
459
|
-
repos[repo.name] = {
|
|
460
|
-
url: repo.url,
|
|
461
|
-
path: repo.path,
|
|
462
|
-
branch: repo.branch,
|
|
463
|
-
newBranch: repo.newBranch,
|
|
464
|
-
sourceBranch: repo.sourceBranch,
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
// Get local git config for commits
|
|
468
|
-
const gitConfig = (0, utils_1.getGitConfig)();
|
|
469
|
-
return {
|
|
470
|
-
publicKey,
|
|
471
|
-
services,
|
|
472
|
-
files,
|
|
473
|
-
postDetails,
|
|
474
|
-
repos,
|
|
475
|
-
privateKey,
|
|
476
|
-
gitToken: envVars.GIT_TOKEN,
|
|
477
|
-
gitUserName: gitConfig.userName,
|
|
478
|
-
gitUserEmail: gitConfig.userEmail,
|
|
479
|
-
};
|
|
480
|
-
}
|
|
388
|
+
// ============================================================================
|
|
389
|
+
// End of Soft Rebuild
|
|
390
|
+
// ============================================================================
|
|
481
391
|
exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
482
392
|
.description('Rebuild an existing Genbox environment with updated configuration')
|
|
483
393
|
.argument('[name]', 'Name of the Genbox to rebuild (optional - will prompt if not provided)')
|
|
@@ -490,7 +400,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
490
400
|
.option('--db-source <source>', 'Database source for copy mode: staging, production')
|
|
491
401
|
.option('--db-dump <path>', 'Path to existing database dump file')
|
|
492
402
|
.option('-y, --yes', 'Skip interactive prompts')
|
|
493
|
-
.option('--hard', '
|
|
403
|
+
.option('--hard', 'Wipe docker volumes and restore database from snapshot')
|
|
494
404
|
.action(async (name, options) => {
|
|
495
405
|
try {
|
|
496
406
|
// Select genbox (interactive if no name provided)
|
|
@@ -628,13 +538,19 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
628
538
|
if (!options.yes) {
|
|
629
539
|
console.log('');
|
|
630
540
|
if (options.hard) {
|
|
631
|
-
console.log(chalk_1.default.
|
|
632
|
-
console.log(chalk_1.default.
|
|
541
|
+
console.log(chalk_1.default.blue('Hard rebuild will:'));
|
|
542
|
+
console.log(chalk_1.default.dim(' • Stop services (PM2, Docker Compose)'));
|
|
543
|
+
console.log(chalk_1.default.dim(' • Delete and re-clone repositories'));
|
|
544
|
+
console.log(chalk_1.default.dim(' • Wipe Docker volumes (fresh containers)'));
|
|
545
|
+
console.log(chalk_1.default.dim(' • Restore database from snapshot'));
|
|
546
|
+
console.log(chalk_1.default.dim(' • Reinstall dependencies and restart services'));
|
|
547
|
+
console.log(chalk_1.default.yellow('Note: All local data (volumes, DB) will be reset.'));
|
|
633
548
|
}
|
|
634
549
|
else {
|
|
635
|
-
console.log(chalk_1.default.blue('
|
|
550
|
+
console.log(chalk_1.default.blue('Rebuild will:'));
|
|
636
551
|
console.log(chalk_1.default.dim(' • Stop services (PM2, Docker Compose)'));
|
|
637
552
|
console.log(chalk_1.default.dim(' • Delete and re-clone repositories'));
|
|
553
|
+
console.log(chalk_1.default.dim(' • Preserve Docker volumes and database'));
|
|
638
554
|
console.log(chalk_1.default.dim(' • Reinstall dependencies and restart services'));
|
|
639
555
|
console.log(chalk_1.default.yellow('Note: Uncommitted changes in repos will be lost.'));
|
|
640
556
|
}
|
|
@@ -813,188 +729,149 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
813
729
|
}
|
|
814
730
|
}
|
|
815
731
|
// ================================================================
|
|
816
|
-
//
|
|
732
|
+
// REBUILD VIA SSH
|
|
817
733
|
// ================================================================
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
value = value.slice(1, -1);
|
|
860
|
-
}
|
|
861
|
-
envVarsFromFile[match[1]] = value;
|
|
734
|
+
// Verify server is accessible
|
|
735
|
+
if (!genbox.ipAddress) {
|
|
736
|
+
console.log(chalk_1.default.red('Error: Genbox has no IP address. It may still be provisioning.'));
|
|
737
|
+
console.log(chalk_1.default.dim(' Wait for provisioning to complete before rebuilding.'));
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
// Get SSH key path
|
|
741
|
+
let sshKeyPath;
|
|
742
|
+
try {
|
|
743
|
+
sshKeyPath = (0, utils_1.getPrivateSshKeyPath)();
|
|
744
|
+
}
|
|
745
|
+
catch (error) {
|
|
746
|
+
console.log(chalk_1.default.red(error.message));
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
// Test SSH connection
|
|
750
|
+
console.log('');
|
|
751
|
+
console.log(chalk_1.default.dim('Testing SSH connection...'));
|
|
752
|
+
const testResult = sshExec(genbox.ipAddress, sshKeyPath, 'echo ok', 10);
|
|
753
|
+
if (!testResult.success || testResult.output !== 'ok') {
|
|
754
|
+
console.log(chalk_1.default.red('Error: Cannot connect to genbox via SSH.'));
|
|
755
|
+
console.log(chalk_1.default.dim(` ${testResult.error || 'Connection failed'}`));
|
|
756
|
+
console.log(chalk_1.default.dim(' Check if the server is running with: genbox status'));
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
console.log(chalk_1.default.green('✓ SSH connection verified'));
|
|
760
|
+
// Build env files for soft rebuild
|
|
761
|
+
const envFilesForSoftRebuild = [];
|
|
762
|
+
const envGenboxPath = path.join(process.cwd(), '.env.genbox');
|
|
763
|
+
if (fs.existsSync(envGenboxPath)) {
|
|
764
|
+
const rawEnvContent = fs.readFileSync(envGenboxPath, 'utf-8');
|
|
765
|
+
const sections = (0, utils_1.parseEnvGenboxSections)(rawEnvContent);
|
|
766
|
+
const globalSection = sections.get('GLOBAL') || '';
|
|
767
|
+
const envVarsFromFile = {};
|
|
768
|
+
for (const line of globalSection.split('\n')) {
|
|
769
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
770
|
+
if (match) {
|
|
771
|
+
let value = match[2].trim();
|
|
772
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
773
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
774
|
+
value = value.slice(1, -1);
|
|
862
775
|
}
|
|
776
|
+
envVarsFromFile[match[1]] = value;
|
|
863
777
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
stagingName: `${app.name}-${serviceName}.env`,
|
|
882
|
-
remotePath: `${repoPath}/apps/${serviceName}/.env`,
|
|
883
|
-
content: serviceEnvContent,
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
else {
|
|
888
|
-
const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name, serviceUrlMap);
|
|
778
|
+
}
|
|
779
|
+
let connectTo;
|
|
780
|
+
if (resolved.profile && config.profiles?.[resolved.profile]) {
|
|
781
|
+
const profile = config.profiles[resolved.profile];
|
|
782
|
+
connectTo = (0, config_loader_1.getProfileConnection)(profile);
|
|
783
|
+
}
|
|
784
|
+
const serviceUrlMap = (0, utils_1.buildServiceUrlMap)(envVarsFromFile, connectTo);
|
|
785
|
+
for (const app of resolved.apps) {
|
|
786
|
+
const appConfig = config.apps[app.name];
|
|
787
|
+
const appPath = appConfig?.path || app.name;
|
|
788
|
+
const repoPath = resolved.repos.find(r => r.name === app.name)?.path ||
|
|
789
|
+
(resolved.repos[0]?.path ? `${resolved.repos[0].path}/${appPath}` : `/home/dev/${config.project.name}/${appPath}`);
|
|
790
|
+
const servicesSections = Array.from(sections.keys()).filter(s => s.startsWith(`${app.name}/`));
|
|
791
|
+
if (servicesSections.length > 0) {
|
|
792
|
+
for (const serviceSectionName of servicesSections) {
|
|
793
|
+
const serviceName = serviceSectionName.split('/')[1];
|
|
794
|
+
const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName, serviceUrlMap);
|
|
889
795
|
envFilesForSoftRebuild.push({
|
|
890
|
-
stagingName: `${app.name}.env`,
|
|
891
|
-
remotePath: `${repoPath}/.env`,
|
|
892
|
-
content:
|
|
796
|
+
stagingName: `${app.name}-${serviceName}.env`,
|
|
797
|
+
remotePath: `${repoPath}/apps/${serviceName}/.env`,
|
|
798
|
+
content: serviceEnvContent,
|
|
893
799
|
});
|
|
894
800
|
}
|
|
895
801
|
}
|
|
802
|
+
else {
|
|
803
|
+
const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name, serviceUrlMap);
|
|
804
|
+
envFilesForSoftRebuild.push({
|
|
805
|
+
stagingName: `${app.name}.env`,
|
|
806
|
+
remotePath: `${repoPath}/.env`,
|
|
807
|
+
content: appEnvContent,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
896
810
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
811
|
+
}
|
|
812
|
+
// Get git token from env
|
|
813
|
+
const envVars = configLoader.loadEnvVars(process.cwd());
|
|
814
|
+
// Run soft rebuild
|
|
815
|
+
console.log('');
|
|
816
|
+
console.log(chalk_1.default.blue(`=== Starting ${options.hard ? 'Hard' : 'Soft'} Rebuild ===`));
|
|
817
|
+
console.log('');
|
|
818
|
+
let currentStep = '';
|
|
819
|
+
const rebuildResult = await runSoftRebuild({
|
|
820
|
+
genbox,
|
|
821
|
+
resolved,
|
|
822
|
+
config,
|
|
823
|
+
keyPath: sshKeyPath,
|
|
824
|
+
envFiles: envFilesForSoftRebuild,
|
|
825
|
+
snapshotId,
|
|
826
|
+
snapshotS3Key,
|
|
827
|
+
gitToken: envVars.GIT_TOKEN,
|
|
828
|
+
hardMode: options.hard, // Pass hard mode flag
|
|
829
|
+
onStep: (step) => {
|
|
830
|
+
currentStep = step;
|
|
831
|
+
console.log(chalk_1.default.blue(`\n=== ${step} ===`));
|
|
832
|
+
},
|
|
833
|
+
onLog: (line, type) => {
|
|
834
|
+
switch (type) {
|
|
835
|
+
case 'error':
|
|
836
|
+
console.log(chalk_1.default.red(` ${line}`));
|
|
837
|
+
break;
|
|
838
|
+
case 'success':
|
|
839
|
+
console.log(chalk_1.default.green(` ${line}`));
|
|
840
|
+
break;
|
|
841
|
+
case 'info':
|
|
842
|
+
console.log(chalk_1.default.cyan(` ${line}`));
|
|
843
|
+
break;
|
|
844
|
+
case 'dim':
|
|
845
|
+
default:
|
|
846
|
+
console.log(chalk_1.default.dim(` ${line}`));
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
},
|
|
850
|
+
});
|
|
851
|
+
console.log('');
|
|
852
|
+
if (rebuildResult.success) {
|
|
853
|
+
console.log(chalk_1.default.green(`✓ ${options.hard ? 'Hard' : 'Soft'} rebuild completed successfully!`));
|
|
935
854
|
console.log('');
|
|
936
|
-
|
|
937
|
-
console.log(chalk_1.default.green(`✓ Soft rebuild completed successfully!`));
|
|
938
|
-
console.log('');
|
|
939
|
-
console.log(`Run ${chalk_1.default.cyan(`genbox status ${selectedName}`)} to check service status.`);
|
|
940
|
-
}
|
|
941
|
-
else {
|
|
942
|
-
console.log(chalk_1.default.red(`✗ Soft rebuild failed: ${rebuildResult.error}`));
|
|
943
|
-
console.log(chalk_1.default.dim(` Failed during: ${currentStep}`));
|
|
944
|
-
console.log(chalk_1.default.dim(` Use --hard for a full OS reinstall if needed.`));
|
|
945
|
-
}
|
|
946
|
-
// Notify API about the rebuild (for tracking)
|
|
947
|
-
try {
|
|
948
|
-
await (0, api_1.fetchApi)(`/genboxes/${genbox._id}/soft-rebuild-completed`, {
|
|
949
|
-
method: 'POST',
|
|
950
|
-
body: JSON.stringify({
|
|
951
|
-
success: rebuildResult.success,
|
|
952
|
-
branch: resolved.repos[0]?.branch,
|
|
953
|
-
newBranch: resolved.repos[0]?.newBranch,
|
|
954
|
-
sourceBranch: resolved.repos[0]?.sourceBranch,
|
|
955
|
-
}),
|
|
956
|
-
});
|
|
957
|
-
}
|
|
958
|
-
catch {
|
|
959
|
-
// Silently ignore API notification failures
|
|
960
|
-
}
|
|
961
|
-
return;
|
|
855
|
+
console.log(`Run ${chalk_1.default.cyan(`genbox status ${selectedName}`)} to check service status.`);
|
|
962
856
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
// Add database info to payload if we have a snapshot
|
|
969
|
-
if (snapshotId && snapshotS3Key) {
|
|
970
|
-
payload.database = {
|
|
971
|
-
mode: 'snapshot',
|
|
972
|
-
source: dbSource,
|
|
973
|
-
snapshotId,
|
|
974
|
-
s3Key: snapshotS3Key,
|
|
975
|
-
};
|
|
976
|
-
}
|
|
977
|
-
else if (dbMode === 'local' || dbMode === 'fresh') {
|
|
978
|
-
payload.database = { mode: 'local' };
|
|
979
|
-
}
|
|
980
|
-
// Execute hard rebuild via API
|
|
981
|
-
const rebuildSpinner = (0, ora_1.default)(`Hard rebuilding Genbox '${selectedName}'...`).start();
|
|
857
|
+
else {
|
|
858
|
+
console.log(chalk_1.default.red(`✗ Rebuild failed: ${rebuildResult.error}`));
|
|
859
|
+
console.log(chalk_1.default.dim(` Failed during: ${currentStep}`));
|
|
860
|
+
}
|
|
861
|
+
// Notify API about the rebuild (for tracking)
|
|
982
862
|
try {
|
|
983
|
-
await
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
863
|
+
await (0, api_1.fetchApi)(`/genboxes/${genbox._id}/soft-rebuild-completed`, {
|
|
864
|
+
method: 'POST',
|
|
865
|
+
body: JSON.stringify({
|
|
866
|
+
success: rebuildResult.success,
|
|
867
|
+
branch: resolved.repos[0]?.branch,
|
|
868
|
+
newBranch: resolved.repos[0]?.newBranch,
|
|
869
|
+
sourceBranch: resolved.repos[0]?.sourceBranch,
|
|
870
|
+
}),
|
|
871
|
+
});
|
|
990
872
|
}
|
|
991
|
-
catch
|
|
992
|
-
|
|
993
|
-
if (error instanceof api_1.AuthenticationError) {
|
|
994
|
-
console.log('');
|
|
995
|
-
console.log(chalk_1.default.yellow(' Please authenticate first:'));
|
|
996
|
-
console.log(chalk_1.default.cyan(' $ genbox login'));
|
|
997
|
-
}
|
|
873
|
+
catch {
|
|
874
|
+
// Silently ignore API notification failures
|
|
998
875
|
}
|
|
999
876
|
}
|
|
1000
877
|
catch (error) {
|
package/dist/db-utils.js
CHANGED
|
@@ -286,23 +286,26 @@ async function runRemoteMongoRestoreDynamic(ipAddress, options = {}) {
|
|
|
286
286
|
return new Promise((resolve) => {
|
|
287
287
|
options.onProgress?.('Detecting MongoDB and restoring database...');
|
|
288
288
|
// The restore command - detects MongoDB port dynamically
|
|
289
|
+
// Note: Uses sed/awk instead of grep -P for portability (grep -P not available on Alpine)
|
|
289
290
|
const restoreCmd = `
|
|
290
291
|
set -e
|
|
291
292
|
|
|
292
293
|
# Detect MongoDB port from running docker containers
|
|
293
294
|
# Different projects use different port mappings (e.g., 27037, 27117, etc.)
|
|
294
|
-
|
|
295
|
+
# Use sed instead of grep -P for portability
|
|
296
|
+
MONGO_PORT=$(docker ps --format '{{.Ports}}' | sed -n 's/.*:\\([0-9]*\\)->27017.*/\\1/p' | head -1)
|
|
295
297
|
|
|
296
298
|
if [ -z "$MONGO_PORT" ]; then
|
|
297
299
|
echo "ERROR: Could not detect MongoDB port from running containers"
|
|
298
|
-
|
|
300
|
+
echo "Running containers:"
|
|
301
|
+
docker ps --format '{{.Names}}: {{.Ports}}'
|
|
299
302
|
exit 1
|
|
300
303
|
fi
|
|
301
304
|
|
|
302
305
|
echo "MongoDB detected on port: $MONGO_PORT"
|
|
303
306
|
|
|
304
307
|
# Wait for MongoDB to be responsive
|
|
305
|
-
for i in
|
|
308
|
+
for i in $(seq 1 30); do
|
|
306
309
|
if mongosh --quiet --host localhost --port $MONGO_PORT --eval "db.runCommand({ping:1})" 2>/dev/null; then
|
|
307
310
|
echo "MongoDB is ready"
|
|
308
311
|
break
|
|
@@ -331,6 +334,7 @@ fi
|
|
|
331
334
|
const proc = (0, child_process_1.spawn)('ssh', [
|
|
332
335
|
'-o', 'StrictHostKeyChecking=no',
|
|
333
336
|
'-o', 'UserKnownHostsFile=/dev/null',
|
|
337
|
+
'-o', 'LogLevel=ERROR',
|
|
334
338
|
'-o', 'ConnectTimeout=30',
|
|
335
339
|
`dev@${ipAddress}`,
|
|
336
340
|
'bash', '-c', `'${restoreCmd.replace(/'/g, "'\\''")}'`,
|
|
@@ -351,13 +355,35 @@ fi
|
|
|
351
355
|
stderr += data.toString();
|
|
352
356
|
});
|
|
353
357
|
proc.on('close', (code) => {
|
|
358
|
+
// Filter out SSH warnings from stderr
|
|
359
|
+
const filteredStderr = stderr
|
|
360
|
+
.split('\n')
|
|
361
|
+
.filter(line => !line.includes('Permanently added') && !line.includes('Warning:'))
|
|
362
|
+
.join('\n')
|
|
363
|
+
.trim();
|
|
354
364
|
if (code === 0) {
|
|
355
365
|
resolve({ success: true });
|
|
356
366
|
}
|
|
357
367
|
else {
|
|
368
|
+
// Include both stdout and filtered stderr for better error diagnosis
|
|
369
|
+
// stdout often contains the actual error messages from the script
|
|
370
|
+
let errorMsg = '';
|
|
371
|
+
if (filteredStderr) {
|
|
372
|
+
errorMsg = filteredStderr;
|
|
373
|
+
}
|
|
374
|
+
if (stdout) {
|
|
375
|
+
// Look for ERROR lines in stdout
|
|
376
|
+
const errorLines = stdout.split('\n').filter(line => line.includes('ERROR') || line.includes('Error') || line.includes('error'));
|
|
377
|
+
if (errorLines.length > 0) {
|
|
378
|
+
errorMsg = errorLines.join('; ');
|
|
379
|
+
}
|
|
380
|
+
else if (!errorMsg) {
|
|
381
|
+
errorMsg = stdout.trim().split('\n').slice(-3).join('\n'); // Last 3 lines
|
|
382
|
+
}
|
|
383
|
+
}
|
|
358
384
|
resolve({
|
|
359
385
|
success: false,
|
|
360
|
-
error:
|
|
386
|
+
error: errorMsg || `Restore failed with exit code ${code}`,
|
|
361
387
|
});
|
|
362
388
|
}
|
|
363
389
|
});
|