genbox 1.0.66 → 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.
@@ -227,10 +227,7 @@ exports.dbSyncCommand
227
227
  uploadSpinner.succeed(chalk_1.default.green('Database snapshot uploaded'));
228
228
  snapshotId = snapshotResult.snapshotId;
229
229
  snapshotS3Key = snapshotResult.s3Key;
230
- // Only cleanup if we created the dump (not user-provided)
231
- if (!options.dump) {
232
- (0, db_utils_1.cleanupDump)(localDumpPath);
233
- }
230
+ // Don't cleanup yet - we still need the file for SCP to genbox
234
231
  }
235
232
  else {
236
233
  uploadSpinner.fail(chalk_1.default.red('Database snapshot upload failed'));
@@ -261,6 +258,9 @@ exports.dbSyncCommand
261
258
  if (!uploadResult.success) {
262
259
  restoreSpinner.fail(chalk_1.default.red('Upload failed'));
263
260
  console.error(chalk_1.default.red(` Error: ${uploadResult.error}`));
261
+ if (localDumpPath && !options.dump) {
262
+ (0, db_utils_1.cleanupDump)(localDumpPath);
263
+ }
264
264
  return;
265
265
  }
266
266
  restoreSpinner.text = 'Restoring database...';
@@ -271,15 +271,26 @@ exports.dbSyncCommand
271
271
  if (!restoreResult.success) {
272
272
  restoreSpinner.fail(chalk_1.default.red('Restore failed'));
273
273
  console.error(chalk_1.default.red(` Error: ${restoreResult.error}`));
274
+ if (localDumpPath && !options.dump) {
275
+ (0, db_utils_1.cleanupDump)(localDumpPath);
276
+ }
274
277
  return;
275
278
  }
276
279
  restoreSpinner.succeed(chalk_1.default.green('Database sync completed!'));
277
280
  console.log('');
278
281
  console.log(chalk_1.default.dim(` Database has been restored from ${source} snapshot.`));
282
+ // Cleanup local dump file now that we're done
283
+ if (localDumpPath && !options.dump) {
284
+ (0, db_utils_1.cleanupDump)(localDumpPath);
285
+ }
279
286
  }
280
287
  catch (error) {
281
288
  restoreSpinner.fail(chalk_1.default.red('Database restore failed'));
282
289
  console.error(chalk_1.default.red(` Error: ${error.message}`));
290
+ // Cleanup on error too
291
+ if (localDumpPath && !options.dump) {
292
+ (0, db_utils_1.cleanupDump)(localDumpPath);
293
+ }
283
294
  }
284
295
  }
285
296
  catch (error) {
@@ -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 down 2>/dev/null || true`, 60);
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
- if (snapshotId && snapshotS3Key) {
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
- async function rebuildGenbox(id, payload) {
381
- return (0, api_1.fetchApi)(`/genboxes/${id}/rebuild`, {
382
- method: 'POST',
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', 'Full rebuild (reinstall OS) instead of soft rebuild')
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)
@@ -562,9 +472,10 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
562
472
  // For branch: CLI options override, then stored newBranch (if it was created), then stored branch
563
473
  const effectiveBranch = options.branch || storedNewBranch || storedBranch;
564
474
  // For new branch creation: use CLI options OR stored values (to re-create branch if first attempt failed)
565
- // If storedNewBranch exists with storedSourceBranch, the branch may not have been created yet
566
- const effectiveNewBranch = newBranchName || (storedNewBranch && storedSourceBranch ? storedNewBranch : undefined);
567
- const effectiveSourceBranch = options.fromBranch || (storedNewBranch && storedSourceBranch ? storedSourceBranch : undefined);
475
+ // If storedNewBranch exists, we need to re-create it - default sourceBranch to 'main' if not stored
476
+ const defaultSourceBranch = config.defaults?.branch || 'main';
477
+ const effectiveNewBranch = newBranchName || storedNewBranch;
478
+ const effectiveSourceBranch = options.fromBranch || (storedNewBranch ? (storedSourceBranch || defaultSourceBranch) : undefined);
568
479
  // For database: use CLI option, or stored database config (convert 'snapshot' to 'copy')
569
480
  const storedDbMode = genbox.database?.mode === 'snapshot' ? 'copy' : genbox.database?.mode;
570
481
  const effectiveDbMode = options.db || storedDbMode;
@@ -627,13 +538,19 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
627
538
  if (!options.yes) {
628
539
  console.log('');
629
540
  if (options.hard) {
630
- console.log(chalk_1.default.yellow('Warning: Hard rebuild will reinstall the OS and rerun setup.'));
631
- console.log(chalk_1.default.yellow('All unsaved work on the server will be lost.'));
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.'));
632
548
  }
633
549
  else {
634
- console.log(chalk_1.default.blue('Soft rebuild will:'));
550
+ console.log(chalk_1.default.blue('Rebuild will:'));
635
551
  console.log(chalk_1.default.dim(' • Stop services (PM2, Docker Compose)'));
636
552
  console.log(chalk_1.default.dim(' • Delete and re-clone repositories'));
553
+ console.log(chalk_1.default.dim(' • Preserve Docker volumes and database'));
637
554
  console.log(chalk_1.default.dim(' • Reinstall dependencies and restart services'));
638
555
  console.log(chalk_1.default.yellow('Note: Uncommitted changes in repos will be lost.'));
639
556
  }
@@ -812,188 +729,149 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
812
729
  }
813
730
  }
814
731
  // ================================================================
815
- // SOFT REBUILD (default)
732
+ // REBUILD VIA SSH
816
733
  // ================================================================
817
- if (!options.hard) {
818
- // Verify server is accessible
819
- if (!genbox.ipAddress) {
820
- console.log(chalk_1.default.red('Error: Genbox has no IP address. It may still be provisioning.'));
821
- console.log(chalk_1.default.dim(' Use --hard for a full rebuild, or wait for provisioning to complete.'));
822
- return;
823
- }
824
- // Get SSH key path
825
- let sshKeyPath;
826
- try {
827
- sshKeyPath = (0, utils_1.getPrivateSshKeyPath)();
828
- }
829
- catch (error) {
830
- console.log(chalk_1.default.red(error.message));
831
- return;
832
- }
833
- // Test SSH connection
834
- console.log('');
835
- console.log(chalk_1.default.dim('Testing SSH connection...'));
836
- const testResult = sshExec(genbox.ipAddress, sshKeyPath, 'echo ok', 10);
837
- if (!testResult.success || testResult.output !== 'ok') {
838
- console.log(chalk_1.default.red('Error: Cannot connect to genbox via SSH.'));
839
- console.log(chalk_1.default.dim(` ${testResult.error || 'Connection failed'}`));
840
- console.log(chalk_1.default.dim(' Use --hard for a full rebuild if the server is unresponsive.'));
841
- return;
842
- }
843
- console.log(chalk_1.default.green('✓ SSH connection verified'));
844
- // Build env files for soft rebuild
845
- const envFilesForSoftRebuild = [];
846
- const envGenboxPath = path.join(process.cwd(), '.env.genbox');
847
- if (fs.existsSync(envGenboxPath)) {
848
- const rawEnvContent = fs.readFileSync(envGenboxPath, 'utf-8');
849
- const sections = (0, utils_1.parseEnvGenboxSections)(rawEnvContent);
850
- const globalSection = sections.get('GLOBAL') || '';
851
- const envVarsFromFile = {};
852
- for (const line of globalSection.split('\n')) {
853
- const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
854
- if (match) {
855
- let value = match[2].trim();
856
- if ((value.startsWith('"') && value.endsWith('"')) ||
857
- (value.startsWith("'") && value.endsWith("'"))) {
858
- value = value.slice(1, -1);
859
- }
860
- 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);
861
775
  }
776
+ envVarsFromFile[match[1]] = value;
862
777
  }
863
- let connectTo;
864
- if (resolved.profile && config.profiles?.[resolved.profile]) {
865
- const profile = config.profiles[resolved.profile];
866
- connectTo = (0, config_loader_1.getProfileConnection)(profile);
867
- }
868
- const serviceUrlMap = (0, utils_1.buildServiceUrlMap)(envVarsFromFile, connectTo);
869
- for (const app of resolved.apps) {
870
- const appConfig = config.apps[app.name];
871
- const appPath = appConfig?.path || app.name;
872
- const repoPath = resolved.repos.find(r => r.name === app.name)?.path ||
873
- (resolved.repos[0]?.path ? `${resolved.repos[0].path}/${appPath}` : `/home/dev/${config.project.name}/${appPath}`);
874
- const servicesSections = Array.from(sections.keys()).filter(s => s.startsWith(`${app.name}/`));
875
- if (servicesSections.length > 0) {
876
- for (const serviceSectionName of servicesSections) {
877
- const serviceName = serviceSectionName.split('/')[1];
878
- const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName, serviceUrlMap);
879
- envFilesForSoftRebuild.push({
880
- stagingName: `${app.name}-${serviceName}.env`,
881
- remotePath: `${repoPath}/apps/${serviceName}/.env`,
882
- content: serviceEnvContent,
883
- });
884
- }
885
- }
886
- else {
887
- 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);
888
795
  envFilesForSoftRebuild.push({
889
- stagingName: `${app.name}.env`,
890
- remotePath: `${repoPath}/.env`,
891
- content: appEnvContent,
796
+ stagingName: `${app.name}-${serviceName}.env`,
797
+ remotePath: `${repoPath}/apps/${serviceName}/.env`,
798
+ content: serviceEnvContent,
892
799
  });
893
800
  }
894
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
+ }
895
810
  }
896
- // Get git token from env
897
- const envVars = configLoader.loadEnvVars(process.cwd());
898
- // Run soft rebuild
899
- console.log('');
900
- console.log(chalk_1.default.blue('=== Starting Soft Rebuild ==='));
901
- console.log('');
902
- let currentStep = '';
903
- const rebuildResult = await runSoftRebuild({
904
- genbox,
905
- resolved,
906
- config,
907
- keyPath: sshKeyPath,
908
- envFiles: envFilesForSoftRebuild,
909
- snapshotId,
910
- snapshotS3Key,
911
- gitToken: envVars.GIT_TOKEN,
912
- onStep: (step) => {
913
- currentStep = step;
914
- console.log(chalk_1.default.blue(`\n=== ${step} ===`));
915
- },
916
- onLog: (line, type) => {
917
- switch (type) {
918
- case 'error':
919
- console.log(chalk_1.default.red(` ${line}`));
920
- break;
921
- case 'success':
922
- console.log(chalk_1.default.green(` ${line}`));
923
- break;
924
- case 'info':
925
- console.log(chalk_1.default.cyan(` ${line}`));
926
- break;
927
- case 'dim':
928
- default:
929
- console.log(chalk_1.default.dim(` ${line}`));
930
- break;
931
- }
932
- },
933
- });
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!`));
934
854
  console.log('');
935
- if (rebuildResult.success) {
936
- console.log(chalk_1.default.green(`✓ Soft rebuild completed successfully!`));
937
- console.log('');
938
- console.log(`Run ${chalk_1.default.cyan(`genbox status ${selectedName}`)} to check service status.`);
939
- }
940
- else {
941
- console.log(chalk_1.default.red(`✗ Soft rebuild failed: ${rebuildResult.error}`));
942
- console.log(chalk_1.default.dim(` Failed during: ${currentStep}`));
943
- console.log(chalk_1.default.dim(` Use --hard for a full OS reinstall if needed.`));
944
- }
945
- // Notify API about the rebuild (for tracking)
946
- try {
947
- await (0, api_1.fetchApi)(`/genboxes/${genbox._id}/soft-rebuild-completed`, {
948
- method: 'POST',
949
- body: JSON.stringify({
950
- success: rebuildResult.success,
951
- branch: resolved.repos[0]?.branch,
952
- newBranch: resolved.repos[0]?.newBranch,
953
- sourceBranch: resolved.repos[0]?.sourceBranch,
954
- }),
955
- });
956
- }
957
- catch {
958
- // Silently ignore API notification failures
959
- }
960
- return;
855
+ console.log(`Run ${chalk_1.default.cyan(`genbox status ${selectedName}`)} to check service status.`);
961
856
  }
962
- // ================================================================
963
- // HARD REBUILD (--hard flag)
964
- // ================================================================
965
- // Build payload
966
- const payload = buildRebuildPayload(resolved, config, publicKey, privateKeyContent, configLoader);
967
- // Add database info to payload if we have a snapshot
968
- if (snapshotId && snapshotS3Key) {
969
- payload.database = {
970
- mode: 'snapshot',
971
- source: dbSource,
972
- snapshotId,
973
- s3Key: snapshotS3Key,
974
- };
975
- }
976
- else if (dbMode === 'local' || dbMode === 'fresh') {
977
- payload.database = { mode: 'local' };
978
- }
979
- // Execute hard rebuild via API
980
- 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)
981
862
  try {
982
- await rebuildGenbox(genbox._id, payload);
983
- rebuildSpinner.succeed(chalk_1.default.green(`Genbox '${selectedName}' hard rebuild initiated!`));
984
- console.log('');
985
- console.log(chalk_1.default.dim('Server is rebuilding. This may take a few minutes.'));
986
- console.log(chalk_1.default.dim('SSH connection will be temporarily unavailable.'));
987
- console.log('');
988
- console.log(`Run ${chalk_1.default.cyan(`genbox status ${selectedName}`)} to check progress.`);
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
+ });
989
872
  }
990
- catch (error) {
991
- rebuildSpinner.fail(chalk_1.default.red(`Failed to rebuild: ${error.message}`));
992
- if (error instanceof api_1.AuthenticationError) {
993
- console.log('');
994
- console.log(chalk_1.default.yellow(' Please authenticate first:'));
995
- console.log(chalk_1.default.cyan(' $ genbox login'));
996
- }
873
+ catch {
874
+ // Silently ignore API notification failures
997
875
  }
998
876
  }
999
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
- MONGO_PORT=$(docker ps --format '{{.Ports}}' | grep -oP '\\d+(?=->27017)' | head -1)
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
- docker ps
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 {1..30}; do
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: stderr || stdout || 'Restore failed',
386
+ error: errorMsg || `Restore failed with exit code ${code}`,
361
387
  });
362
388
  }
363
389
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.66",
3
+ "version": "1.0.68",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {