genbox 1.0.64 → 1.0.66

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.
@@ -51,22 +51,10 @@ const api_1 = require("../api");
51
51
  const genbox_selector_1 = require("../genbox-selector");
52
52
  const schema_v4_1 = require("../schema-v4");
53
53
  const db_utils_1 = require("../db-utils");
54
+ const utils_1 = require("../utils");
54
55
  // ============================================================================
55
56
  // SSH Utilities for Soft Rebuild
56
57
  // ============================================================================
57
- function getPrivateSshKeyPath() {
58
- const home = os.homedir();
59
- const potentialKeys = [
60
- path.join(home, '.ssh', 'id_ed25519'),
61
- path.join(home, '.ssh', 'id_rsa'),
62
- ];
63
- for (const keyPath of potentialKeys) {
64
- if (fs.existsSync(keyPath)) {
65
- return keyPath;
66
- }
67
- }
68
- throw new Error('No SSH private key found in ~/.ssh/');
69
- }
70
58
  function sshExec(ip, keyPath, command, timeoutSecs = 30) {
71
59
  const sshOpts = `-i ${keyPath} -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=${timeoutSecs}`;
72
60
  try {
@@ -389,125 +377,12 @@ async function runSoftRebuild(options) {
389
377
  return { success: false, error: error.message };
390
378
  }
391
379
  }
392
- function getPublicSshKey() {
393
- const home = os.homedir();
394
- const potentialKeys = [
395
- path.join(home, '.ssh', 'id_ed25519.pub'),
396
- path.join(home, '.ssh', 'id_rsa.pub'),
397
- ];
398
- for (const keyPath of potentialKeys) {
399
- if (fs.existsSync(keyPath)) {
400
- const content = fs.readFileSync(keyPath, 'utf-8').trim();
401
- if (content)
402
- return content;
403
- }
404
- }
405
- throw new Error('No public SSH key found in ~/.ssh/');
406
- }
407
- function getPrivateSshKey() {
408
- const home = os.homedir();
409
- const potentialKeys = [
410
- path.join(home, '.ssh', 'id_ed25519'),
411
- path.join(home, '.ssh', 'id_rsa'),
412
- ];
413
- for (const keyPath of potentialKeys) {
414
- if (fs.existsSync(keyPath)) {
415
- return fs.readFileSync(keyPath, 'utf-8');
416
- }
417
- }
418
- return undefined;
419
- }
420
380
  async function rebuildGenbox(id, payload) {
421
381
  return (0, api_1.fetchApi)(`/genboxes/${id}/rebuild`, {
422
382
  method: 'POST',
423
383
  body: JSON.stringify(payload),
424
384
  });
425
385
  }
426
- /**
427
- * Parse .env.genbox file into segregated sections
428
- */
429
- function parseEnvGenboxSections(content) {
430
- const sections = new Map();
431
- let currentSection = 'GLOBAL';
432
- let currentContent = [];
433
- for (const line of content.split('\n')) {
434
- const sectionMatch = line.match(/^# === ([^=]+) ===$/);
435
- if (sectionMatch) {
436
- if (currentContent.length > 0) {
437
- sections.set(currentSection, currentContent.join('\n').trim());
438
- }
439
- currentSection = sectionMatch[1].trim();
440
- currentContent = [];
441
- }
442
- else if (currentSection !== 'END') {
443
- currentContent.push(line);
444
- }
445
- }
446
- if (currentContent.length > 0 && currentSection !== 'END') {
447
- sections.set(currentSection, currentContent.join('\n').trim());
448
- }
449
- return sections;
450
- }
451
- /**
452
- * Build a map of service URL variables based on connection type
453
- */
454
- function buildServiceUrlMap(envVarsFromFile, connectTo) {
455
- const urlMap = {};
456
- const prefix = connectTo ? `${connectTo.toUpperCase()}_` : 'LOCAL_';
457
- const serviceNames = new Set();
458
- for (const key of Object.keys(envVarsFromFile)) {
459
- const match = key.match(/^(LOCAL|STAGING|PRODUCTION)_(.+_URL)$/);
460
- if (match) {
461
- serviceNames.add(match[2]);
462
- }
463
- }
464
- for (const serviceName of serviceNames) {
465
- const prefixedKey = `${prefix}${serviceName}`;
466
- const localKey = `LOCAL_${serviceName}`;
467
- const value = envVarsFromFile[prefixedKey] || envVarsFromFile[localKey];
468
- if (value) {
469
- urlMap[serviceName] = value;
470
- }
471
- }
472
- if (!urlMap['API_URL']) {
473
- const apiUrl = envVarsFromFile[`${prefix}API_URL`] ||
474
- envVarsFromFile['LOCAL_API_URL'] ||
475
- envVarsFromFile['STAGING_API_URL'];
476
- if (apiUrl) {
477
- urlMap['API_URL'] = apiUrl;
478
- }
479
- }
480
- return urlMap;
481
- }
482
- /**
483
- * Build env content for a specific app
484
- */
485
- function buildAppEnvContent(sections, appName, serviceUrlMap) {
486
- const parts = [];
487
- const globalSection = sections.get('GLOBAL');
488
- if (globalSection) {
489
- parts.push(globalSection);
490
- }
491
- const appSection = sections.get(appName);
492
- if (appSection) {
493
- parts.push(appSection);
494
- }
495
- let envContent = parts.join('\n\n');
496
- for (const [varName, value] of Object.entries(serviceUrlMap)) {
497
- const pattern = new RegExp(`\\$\\{${varName}\\}`, 'g');
498
- envContent = envContent.replace(pattern, value);
499
- }
500
- envContent = envContent
501
- .split('\n')
502
- .filter(line => {
503
- const trimmed = line.trim();
504
- return trimmed === '' || trimmed.includes('=') || !trimmed.startsWith('#');
505
- })
506
- .join('\n')
507
- .replace(/\n{3,}/g, '\n\n')
508
- .trim();
509
- return envContent;
510
- }
511
386
  /**
512
387
  * Build rebuild payload from resolved config
513
388
  */
@@ -527,26 +402,15 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
527
402
  const envGenboxPath = path.join(process.cwd(), '.env.genbox');
528
403
  if (fs.existsSync(envGenboxPath)) {
529
404
  const rawEnvContent = fs.readFileSync(envGenboxPath, 'utf-8');
530
- const sections = parseEnvGenboxSections(rawEnvContent);
405
+ const sections = (0, utils_1.parseEnvGenboxSections)(rawEnvContent);
531
406
  const globalSection = sections.get('GLOBAL') || '';
532
- const envVarsFromFile = {};
533
- for (const line of globalSection.split('\n')) {
534
- const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
535
- if (match) {
536
- let value = match[2].trim();
537
- if ((value.startsWith('"') && value.endsWith('"')) ||
538
- (value.startsWith("'") && value.endsWith("'"))) {
539
- value = value.slice(1, -1);
540
- }
541
- envVarsFromFile[match[1]] = value;
542
- }
543
- }
407
+ const envVarsFromFile = (0, utils_1.parseEnvVarsFromSection)(globalSection);
544
408
  let connectTo;
545
409
  if (resolved.profile && config.profiles?.[resolved.profile]) {
546
410
  const profile = config.profiles[resolved.profile];
547
411
  connectTo = (0, config_loader_1.getProfileConnection)(profile);
548
412
  }
549
- const serviceUrlMap = buildServiceUrlMap(envVarsFromFile, connectTo);
413
+ const serviceUrlMap = (0, utils_1.buildServiceUrlMap)(envVarsFromFile, connectTo);
550
414
  if (connectTo && Object.keys(serviceUrlMap).length > 0) {
551
415
  console.log(chalk_1.default.dim(` Using ${connectTo} URLs for variable expansion`));
552
416
  }
@@ -558,7 +422,7 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
558
422
  if (servicesSections.length > 0) {
559
423
  for (const serviceSectionName of servicesSections) {
560
424
  const serviceName = serviceSectionName.split('/')[1];
561
- const serviceEnvContent = buildAppEnvContent(sections, serviceSectionName, serviceUrlMap);
425
+ const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName, serviceUrlMap);
562
426
  const stagingName = `${app.name}-${serviceName}.env`;
563
427
  const targetPath = `${repoPath}/apps/${serviceName}/.env`;
564
428
  files.push({
@@ -570,7 +434,7 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
570
434
  }
571
435
  }
572
436
  else {
573
- const appEnvContent = buildAppEnvContent(sections, app.name, serviceUrlMap);
437
+ const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name, serviceUrlMap);
574
438
  files.push({
575
439
  path: `/home/dev/.env-staging/${app.name}.env`,
576
440
  content: appEnvContent,
@@ -600,6 +464,8 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
600
464
  sourceBranch: repo.sourceBranch,
601
465
  };
602
466
  }
467
+ // Get local git config for commits
468
+ const gitConfig = (0, utils_1.getGitConfig)();
603
469
  return {
604
470
  publicKey,
605
471
  services,
@@ -608,110 +474,10 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
608
474
  repos,
609
475
  privateKey,
610
476
  gitToken: envVars.GIT_TOKEN,
477
+ gitUserName: gitConfig.userName,
478
+ gitUserEmail: gitConfig.userEmail,
611
479
  };
612
480
  }
613
- /**
614
- * Validate git configuration and warn about missing credentials
615
- */
616
- function validateGitCredentials(repos, gitToken, privateKey) {
617
- const warnings = [];
618
- const errors = [];
619
- for (const repo of repos) {
620
- const isHttps = repo.url.startsWith('https://');
621
- const isSsh = repo.url.startsWith('git@') || repo.url.includes('ssh://');
622
- const isPrivateRepo = repo.url.includes('github.com') && !repo.url.includes('/public/');
623
- if (isHttps && !gitToken && isPrivateRepo) {
624
- warnings.push(`Repository '${repo.name}' uses HTTPS URL but GIT_TOKEN is not set.`);
625
- warnings.push(` Add GIT_TOKEN=<your-github-token> to .env.genbox for private repos.`);
626
- }
627
- if (isSsh && !privateKey) {
628
- warnings.push(`Repository '${repo.name}' uses SSH URL but no SSH key was injected.`);
629
- }
630
- }
631
- return { warnings, errors };
632
- }
633
- /**
634
- * Prompt for branch options interactively
635
- */
636
- async function promptForBranchOptions(resolved, config) {
637
- // Get the default branch from config or first repo
638
- const defaultBranch = config.defaults?.branch || resolved.repos[0]?.branch || 'main';
639
- console.log(chalk_1.default.blue('=== Branch Configuration ==='));
640
- console.log(chalk_1.default.dim(`Default branch: ${defaultBranch}`));
641
- console.log('');
642
- const branchChoice = await prompts.select({
643
- message: 'Branch option:',
644
- choices: [
645
- {
646
- name: `Use default branch (${defaultBranch})`,
647
- value: 'default',
648
- },
649
- {
650
- name: 'Use a different existing branch',
651
- value: 'existing',
652
- },
653
- {
654
- name: 'Create a new branch',
655
- value: 'new',
656
- },
657
- ],
658
- default: 'default',
659
- });
660
- if (branchChoice === 'default') {
661
- return resolved;
662
- }
663
- if (branchChoice === 'existing') {
664
- const branchName = await prompts.input({
665
- message: 'Enter branch name:',
666
- default: defaultBranch,
667
- validate: (value) => {
668
- if (!value.trim())
669
- return 'Branch name is required';
670
- return true;
671
- },
672
- });
673
- return {
674
- ...resolved,
675
- repos: resolved.repos.map(repo => ({
676
- ...repo,
677
- branch: branchName.trim(),
678
- newBranch: undefined,
679
- sourceBranch: undefined,
680
- })),
681
- };
682
- }
683
- if (branchChoice === 'new') {
684
- const newBranchName = await prompts.input({
685
- message: 'New branch name:',
686
- validate: (value) => {
687
- if (!value.trim())
688
- return 'Branch name is required';
689
- if (!/^[\w\-./]+$/.test(value))
690
- return 'Invalid branch name (use letters, numbers, -, _, /, .)';
691
- return true;
692
- },
693
- });
694
- const sourceBranch = await prompts.input({
695
- message: 'Create from branch:',
696
- default: defaultBranch,
697
- validate: (value) => {
698
- if (!value.trim())
699
- return 'Source branch is required';
700
- return true;
701
- },
702
- });
703
- return {
704
- ...resolved,
705
- repos: resolved.repos.map(repo => ({
706
- ...repo,
707
- branch: newBranchName.trim(),
708
- newBranch: newBranchName.trim(),
709
- sourceBranch: sourceBranch.trim(),
710
- })),
711
- };
712
- }
713
- return resolved;
714
- }
715
481
  exports.rebuildCommand = new commander_1.Command('rebuild')
716
482
  .description('Rebuild an existing Genbox environment with updated configuration')
717
483
  .argument('[name]', 'Name of the Genbox to rebuild (optional - will prompt if not provided)')
@@ -822,7 +588,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
822
588
  // Interactive branch selection only if no branch options specified and no stored branch
823
589
  // Skip if: -b, -f, -n, stored branch, or -y
824
590
  if (!options.branch && !options.fromBranch && !storedBranch && !options.newBranch && !options.yes && resolved.repos.length > 0) {
825
- resolved = await promptForBranchOptions(resolved, config);
591
+ resolved = await (0, utils_1.promptForBranchOptionsRebuild)(resolved, config);
826
592
  }
827
593
  // Display what will be rebuilt
828
594
  console.log(chalk_1.default.bold('Rebuild Configuration:'));
@@ -882,7 +648,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
882
648
  }
883
649
  }
884
650
  // Get SSH keys
885
- const publicKey = getPublicSshKey();
651
+ const publicKey = (0, utils_1.getPublicSshKey)();
886
652
  // Check if SSH auth is needed for git
887
653
  let privateKeyContent;
888
654
  const v3Config = config;
@@ -894,7 +660,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
894
660
  default: true,
895
661
  });
896
662
  if (injectKey) {
897
- privateKeyContent = getPrivateSshKey();
663
+ privateKeyContent = (0, utils_1.getPrivateSshKey)();
898
664
  if (privateKeyContent) {
899
665
  console.log(chalk_1.default.dim(' Using local SSH private key'));
900
666
  }
@@ -1021,7 +787,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
1021
787
  }
1022
788
  // Validate git credentials before rebuilding
1023
789
  const envVarsForValidation = configLoader.loadEnvVars(process.cwd());
1024
- const gitValidation = validateGitCredentials(resolved.repos.map(r => ({ url: r.url, name: r.name })), envVarsForValidation.GIT_TOKEN, privateKeyContent);
790
+ const gitValidation = (0, utils_1.validateGitCredentials)(resolved.repos.map(r => ({ url: r.url, name: r.name })), envVarsForValidation.GIT_TOKEN, privateKeyContent);
1025
791
  if (gitValidation.warnings.length > 0) {
1026
792
  console.log('');
1027
793
  console.log(chalk_1.default.yellow('⚠ Git Authentication Warnings:'));
@@ -1058,7 +824,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
1058
824
  // Get SSH key path
1059
825
  let sshKeyPath;
1060
826
  try {
1061
- sshKeyPath = getPrivateSshKeyPath();
827
+ sshKeyPath = (0, utils_1.getPrivateSshKeyPath)();
1062
828
  }
1063
829
  catch (error) {
1064
830
  console.log(chalk_1.default.red(error.message));
@@ -1080,7 +846,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
1080
846
  const envGenboxPath = path.join(process.cwd(), '.env.genbox');
1081
847
  if (fs.existsSync(envGenboxPath)) {
1082
848
  const rawEnvContent = fs.readFileSync(envGenboxPath, 'utf-8');
1083
- const sections = parseEnvGenboxSections(rawEnvContent);
849
+ const sections = (0, utils_1.parseEnvGenboxSections)(rawEnvContent);
1084
850
  const globalSection = sections.get('GLOBAL') || '';
1085
851
  const envVarsFromFile = {};
1086
852
  for (const line of globalSection.split('\n')) {
@@ -1099,7 +865,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
1099
865
  const profile = config.profiles[resolved.profile];
1100
866
  connectTo = (0, config_loader_1.getProfileConnection)(profile);
1101
867
  }
1102
- const serviceUrlMap = buildServiceUrlMap(envVarsFromFile, connectTo);
868
+ const serviceUrlMap = (0, utils_1.buildServiceUrlMap)(envVarsFromFile, connectTo);
1103
869
  for (const app of resolved.apps) {
1104
870
  const appConfig = config.apps[app.name];
1105
871
  const appPath = appConfig?.path || app.name;
@@ -1109,7 +875,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
1109
875
  if (servicesSections.length > 0) {
1110
876
  for (const serviceSectionName of servicesSections) {
1111
877
  const serviceName = serviceSectionName.split('/')[1];
1112
- const serviceEnvContent = buildAppEnvContent(sections, serviceSectionName, serviceUrlMap);
878
+ const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName, serviceUrlMap);
1113
879
  envFilesForSoftRebuild.push({
1114
880
  stagingName: `${app.name}-${serviceName}.env`,
1115
881
  remotePath: `${repoPath}/apps/${serviceName}/.env`,
@@ -1118,7 +884,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
1118
884
  }
1119
885
  }
1120
886
  else {
1121
- const appEnvContent = buildAppEnvContent(sections, app.name, serviceUrlMap);
887
+ const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name, serviceUrlMap);
1122
888
  envFilesForSoftRebuild.push({
1123
889
  stagingName: `${app.name}.env`,
1124
890
  remotePath: `${repoPath}/.env`,
package/dist/db-utils.js CHANGED
@@ -47,9 +47,11 @@ exports.getMongoDumpInstallInstructions = getMongoDumpInstallInstructions;
47
47
  exports.runLocalMongoDump = runLocalMongoDump;
48
48
  exports.uploadDumpToGenbox = uploadDumpToGenbox;
49
49
  exports.runRemoteMongoRestore = runRemoteMongoRestore;
50
+ exports.runRemoteMongoRestoreDynamic = runRemoteMongoRestoreDynamic;
50
51
  exports.cleanupDump = cleanupDump;
51
52
  exports.formatBytes = formatBytes;
52
53
  exports.waitForSshAccess = waitForSshAccess;
54
+ exports.downloadSnapshotFromS3 = downloadSnapshotFromS3;
53
55
  exports.uploadDumpToS3 = uploadDumpToS3;
54
56
  exports.createAndUploadSnapshot = createAndUploadSnapshot;
55
57
  const child_process_1 = require("child_process");
@@ -276,6 +278,94 @@ async function runRemoteMongoRestore(ipAddress, dbName, options = {}) {
276
278
  });
277
279
  });
278
280
  }
281
+ /**
282
+ * Run mongorestore on genbox via SSH with dynamic port detection
283
+ * This version doesn't hardcode container names or ports - it detects them dynamically
284
+ */
285
+ async function runRemoteMongoRestoreDynamic(ipAddress, options = {}) {
286
+ return new Promise((resolve) => {
287
+ options.onProgress?.('Detecting MongoDB and restoring database...');
288
+ // The restore command - detects MongoDB port dynamically
289
+ const restoreCmd = `
290
+ set -e
291
+
292
+ # Detect MongoDB port from running docker containers
293
+ # 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
+
296
+ if [ -z "$MONGO_PORT" ]; then
297
+ echo "ERROR: Could not detect MongoDB port from running containers"
298
+ docker ps
299
+ exit 1
300
+ fi
301
+
302
+ echo "MongoDB detected on port: $MONGO_PORT"
303
+
304
+ # Wait for MongoDB to be responsive
305
+ for i in {1..30}; do
306
+ if mongosh --quiet --host localhost --port $MONGO_PORT --eval "db.runCommand({ping:1})" 2>/dev/null; then
307
+ echo "MongoDB is ready"
308
+ break
309
+ fi
310
+ if [ $i -eq 30 ]; then
311
+ echo "ERROR: MongoDB not responding after 30 attempts"
312
+ exit 1
313
+ fi
314
+ echo "Waiting for MongoDB... ($i/30)"
315
+ sleep 2
316
+ done
317
+
318
+ # Restore the database from the uploaded dump
319
+ if [ -f /home/dev/.db-dump.gz ]; then
320
+ echo "Restoring database..."
321
+ # The dump is a raw mongodump --archive file (gzipped), not a tar archive
322
+ # Don't use --db flag with --archive as it causes issues
323
+ mongorestore --host localhost:$MONGO_PORT --drop --gzip --archive=/home/dev/.db-dump.gz
324
+ rm -f /home/dev/.db-dump.gz
325
+ echo "Database restored successfully"
326
+ else
327
+ echo "Error: Dump file not found at /home/dev/.db-dump.gz"
328
+ exit 1
329
+ fi
330
+ `;
331
+ const proc = (0, child_process_1.spawn)('ssh', [
332
+ '-o', 'StrictHostKeyChecking=no',
333
+ '-o', 'UserKnownHostsFile=/dev/null',
334
+ '-o', 'ConnectTimeout=30',
335
+ `dev@${ipAddress}`,
336
+ 'bash', '-c', `'${restoreCmd.replace(/'/g, "'\\''")}'`,
337
+ ], {
338
+ stdio: ['ignore', 'pipe', 'pipe'],
339
+ });
340
+ let stdout = '';
341
+ let stderr = '';
342
+ proc.stdout?.on('data', (data) => {
343
+ const line = data.toString();
344
+ stdout += line;
345
+ // Report progress
346
+ if (line.includes('MongoDB detected') || line.includes('Restoring') || line.includes('restored') || line.includes('Waiting')) {
347
+ options.onProgress?.(line.trim());
348
+ }
349
+ });
350
+ proc.stderr?.on('data', (data) => {
351
+ stderr += data.toString();
352
+ });
353
+ proc.on('close', (code) => {
354
+ if (code === 0) {
355
+ resolve({ success: true });
356
+ }
357
+ else {
358
+ resolve({
359
+ success: false,
360
+ error: stderr || stdout || 'Restore failed',
361
+ });
362
+ }
363
+ });
364
+ proc.on('error', (err) => {
365
+ resolve({ success: false, error: `Failed to run ssh: ${err.message}` });
366
+ });
367
+ });
368
+ }
279
369
  /**
280
370
  * Clean up temporary dump files
281
371
  */
@@ -324,6 +414,36 @@ async function waitForSshAccess(ipAddress, maxWaitSeconds = 300, onProgress) {
324
414
  }
325
415
  return false;
326
416
  }
417
+ /**
418
+ * Download a snapshot from S3 using pre-signed URL
419
+ */
420
+ async function downloadSnapshotFromS3(downloadUrl, options = {}) {
421
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'genbox-dbdump-'));
422
+ const dumpPath = path.join(tempDir, 'dump.gz');
423
+ options.onProgress?.('Downloading snapshot from cloud...');
424
+ try {
425
+ const response = await fetch(downloadUrl);
426
+ if (!response.ok) {
427
+ fs.rmSync(tempDir, { recursive: true, force: true });
428
+ return {
429
+ success: false,
430
+ error: `Download failed: ${response.status} ${response.statusText}`,
431
+ };
432
+ }
433
+ const buffer = await response.arrayBuffer();
434
+ fs.writeFileSync(dumpPath, Buffer.from(buffer));
435
+ const stats = fs.statSync(dumpPath);
436
+ options.onProgress?.(`Downloaded snapshot (${formatBytes(stats.size)})`);
437
+ return { success: true, dumpPath };
438
+ }
439
+ catch (error) {
440
+ fs.rmSync(tempDir, { recursive: true, force: true });
441
+ return {
442
+ success: false,
443
+ error: `Download failed: ${error.message}`,
444
+ };
445
+ }
446
+ }
327
447
  /**
328
448
  * Upload dump file to S3 using pre-signed URL
329
449
  */