genbox 1.0.54 → 1.0.55

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.
@@ -41,31 +41,25 @@ const commander_1 = require("commander");
41
41
  const prompts = __importStar(require("@inquirer/prompts"));
42
42
  const chalk_1 = __importDefault(require("chalk"));
43
43
  const ora_1 = __importDefault(require("ora"));
44
+ const fs = __importStar(require("fs"));
44
45
  const config_loader_1 = require("../config-loader");
45
46
  const api_1 = require("../api");
46
47
  const genbox_selector_1 = require("../genbox-selector");
47
- async function executeDbOperation(payload) {
48
- return (0, api_1.fetchApi)('/genboxes/db', {
49
- method: 'POST',
50
- body: JSON.stringify(payload),
51
- });
52
- }
48
+ const db_utils_1 = require("../db-utils");
53
49
  exports.dbSyncCommand = new commander_1.Command('db')
54
50
  .description('Database operations for genbox environments');
55
51
  // Subcommand: db sync
56
52
  exports.dbSyncCommand
57
53
  .command('sync [genbox]')
58
54
  .description('Sync database from staging/production to a genbox')
59
- .option('-s, --source <source>', 'Source environment: staging, production', 'staging')
60
- .option('--collections <collections>', 'Comma-separated list of collections to sync')
61
- .option('--exclude <collections>', 'Comma-separated list of collections to exclude')
55
+ .option('-s, --source <source>', 'Source environment: staging, production', 'production')
56
+ .option('--dump <path>', 'Path to existing database dump file')
62
57
  .option('-f, --force', 'Skip confirmation prompt')
63
- .option('--dry-run', 'Show what would be synced without actually syncing')
58
+ .option('--fresh', 'Create new snapshot even if recent one exists')
64
59
  .action(async (genboxName, options) => {
65
60
  try {
66
61
  const configLoader = new config_loader_1.ConfigLoader();
67
62
  const loadResult = await configLoader.load();
68
- // Get genbox
69
63
  // Select genbox
70
64
  const result = await (0, genbox_selector_1.selectGenbox)(genboxName);
71
65
  if (!result.genbox) {
@@ -74,21 +68,24 @@ exports.dbSyncCommand
74
68
  }
75
69
  return;
76
70
  }
77
- const genbox = result.genbox.name;
78
- const config = loadResult.config;
79
- const source = options.source;
80
- // Validate source environment exists
81
- if (config.version === 4 && !config.environments?.[source]) {
82
- console.log(chalk_1.default.yellow(`Environment '${source}' not configured in genbox.yaml`));
83
- console.log(chalk_1.default.dim('Available environments:'));
84
- for (const env of Object.keys(config.environments || {})) {
85
- console.log(chalk_1.default.dim(` - ${env}`));
86
- }
71
+ const genbox = result.genbox;
72
+ const genboxId = genbox._id;
73
+ // Check project ID
74
+ const projectId = genbox.project;
75
+ if (!projectId) {
76
+ console.log(chalk_1.default.red('Project not synced - cannot sync database.'));
77
+ console.log(chalk_1.default.dim(' Run `genbox init` first to sync your project.'));
87
78
  return;
88
79
  }
89
- // Get source URL
80
+ const config = loadResult.config;
81
+ const source = options.source;
82
+ // Get source URL from .env.genbox
90
83
  const envVars = configLoader.loadEnvVars(process.cwd());
91
- const sourceUrlVar = source === 'production' ? 'PROD_MONGODB_URL' : 'STAGING_MONGODB_URL';
84
+ const sourceUrlVar = source === 'production'
85
+ ? 'PROD_MONGODB_URL'
86
+ : source === 'staging'
87
+ ? 'STAGING_MONGODB_URL'
88
+ : `${source.toUpperCase()}_MONGODB_URL`;
92
89
  const sourceUrl = envVars[sourceUrlVar];
93
90
  if (!sourceUrl) {
94
91
  console.log(chalk_1.default.red(`${sourceUrlVar} not found in .env.genbox`));
@@ -99,34 +96,21 @@ exports.dbSyncCommand
99
96
  console.log('');
100
97
  console.log(chalk_1.default.bold('Database Sync:'));
101
98
  console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
102
- console.log(` ${chalk_1.default.bold('Genbox:')} ${genbox}`);
99
+ console.log(` ${chalk_1.default.bold('Genbox:')} ${genbox.name}`);
103
100
  console.log(` ${chalk_1.default.bold('Source:')} ${source}`);
104
- console.log(` ${chalk_1.default.bold('Database:')} ${config.project?.name || 'default'}`);
105
- if (options.collections) {
106
- console.log(` ${chalk_1.default.bold('Collections:')} ${options.collections}`);
107
- }
108
- if (options.exclude) {
109
- console.log(` ${chalk_1.default.bold('Exclude:')} ${options.exclude}`);
110
- }
101
+ console.log(` ${chalk_1.default.bold('URL:')} ${sourceUrl.replace(/\/\/[^:]+:[^@]+@/, '//***:***@')}`);
111
102
  console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
112
103
  // Warning for production
113
104
  if (source === 'production') {
114
105
  console.log('');
115
106
  console.log(chalk_1.default.yellow('⚠ WARNING: Syncing from production database'));
116
107
  }
117
- // Dry run mode
118
- if (options.dryRun) {
119
- console.log('');
120
- console.log(chalk_1.default.yellow('Dry run mode - no changes made'));
121
- console.log(chalk_1.default.dim('Remove --dry-run to perform the sync'));
122
- return;
123
- }
124
108
  // Confirmation
125
109
  if (!options.force) {
126
110
  console.log('');
127
111
  console.log(chalk_1.default.yellow('⚠ This will replace all data in the genbox database!'));
128
112
  const confirm = await prompts.confirm({
129
- message: `Sync database from ${source} to ${genbox}?`,
113
+ message: `Sync database from ${source} to ${genbox.name}?`,
130
114
  default: false,
131
115
  });
132
116
  if (!confirm) {
@@ -134,147 +118,191 @@ exports.dbSyncCommand
134
118
  return;
135
119
  }
136
120
  }
137
- // Execute sync
138
- const spinner = (0, ora_1.default)(`Syncing database from ${source}...`).start();
139
- try {
140
- const result = await executeDbOperation({
141
- genboxName: genbox,
142
- operation: 'copy',
143
- source,
144
- sourceUrl,
145
- database: config.project?.name,
146
- collections: options.collections?.split(',').map((c) => c.trim()),
147
- excludeCollections: options.exclude?.split(',').map((c) => c.trim()),
148
- });
149
- spinner.succeed(chalk_1.default.green('Database synced successfully!'));
150
- if (result.stats) {
151
- console.log('');
152
- console.log(chalk_1.default.bold('Sync Statistics:'));
153
- console.log(` Collections: ${result.stats.collections || 0}`);
154
- console.log(` Documents: ${result.stats.documents || 0}`);
155
- console.log(` Size: ${result.stats.size || 'N/A'}`);
156
- console.log(` Duration: ${result.stats.duration || 'N/A'}`);
121
+ // Map source to snapshot source type
122
+ const snapshotSource = source === 'production' ? 'production' : source === 'staging' ? 'staging' : 'local';
123
+ let snapshotId;
124
+ let snapshotS3Key;
125
+ let localDumpPath;
126
+ // Check for existing snapshots (unless --fresh specified)
127
+ if (!options.fresh && !options.dump) {
128
+ try {
129
+ const existingSnapshot = await (0, api_1.getLatestSnapshot)(projectId, snapshotSource);
130
+ if (existingSnapshot) {
131
+ const snapshotAge = Date.now() - new Date(existingSnapshot.createdAt).getTime();
132
+ const hoursAgo = Math.floor(snapshotAge / (1000 * 60 * 60));
133
+ const timeAgoStr = hoursAgo < 1 ? 'less than an hour ago' : `${hoursAgo} hours ago`;
134
+ const snapshotChoice = await prompts.select({
135
+ message: `Found existing ${snapshotSource} snapshot (${timeAgoStr}):`,
136
+ choices: [
137
+ {
138
+ name: `Use existing snapshot (${timeAgoStr}, ${(0, db_utils_1.formatBytes)(existingSnapshot.sizeBytes)})`,
139
+ value: 'existing',
140
+ },
141
+ {
142
+ name: 'Create fresh snapshot (dump now)',
143
+ value: 'fresh',
144
+ },
145
+ ],
146
+ });
147
+ if (snapshotChoice === 'existing') {
148
+ snapshotId = existingSnapshot._id;
149
+ snapshotS3Key = existingSnapshot.s3Key;
150
+ console.log(chalk_1.default.green(` ✓ Using existing snapshot`));
151
+ }
152
+ }
157
153
  }
158
- }
159
- catch (error) {
160
- spinner.fail(chalk_1.default.red('Sync failed'));
161
- console.error(chalk_1.default.red(`Error: ${error.message}`));
162
- if (error instanceof api_1.AuthenticationError) {
163
- console.log(chalk_1.default.yellow('\nRun: genbox login'));
154
+ catch {
155
+ // Silently continue if we can't fetch snapshots
164
156
  }
165
157
  }
166
- }
167
- catch (error) {
168
- if (error.name === 'ExitPromptError') {
169
- console.log(chalk_1.default.dim('\nCancelled.'));
170
- return;
171
- }
172
- console.error(chalk_1.default.red(`Error: ${error.message}`));
173
- }
174
- });
175
- // Subcommand: db status
176
- exports.dbSyncCommand
177
- .command('status [genbox]')
178
- .description('Show database status for a genbox')
179
- .action(async (genboxName) => {
180
- try {
181
- const configLoader = new config_loader_1.ConfigLoader();
182
- const loadResult = await configLoader.load();
183
- // Select genbox
184
- const selectResult = await (0, genbox_selector_1.selectGenbox)(genboxName);
185
- if (!selectResult.genbox) {
186
- if (!selectResult.cancelled) {
187
- console.log(chalk_1.default.yellow('No genbox selected'));
158
+ // Create new snapshot if needed
159
+ if (!snapshotId) {
160
+ // Check for user-provided dump file
161
+ if (options.dump) {
162
+ if (!fs.existsSync(options.dump)) {
163
+ console.log(chalk_1.default.red(`Database dump file not found: ${options.dump}`));
164
+ return;
165
+ }
166
+ localDumpPath = options.dump;
167
+ console.log(chalk_1.default.dim(` Using provided dump file: ${options.dump}`));
188
168
  }
169
+ else {
170
+ // Need to run mongodump locally
171
+ if (!(0, db_utils_1.isMongoDumpAvailable)()) {
172
+ console.log(chalk_1.default.red('mongodump not found. Required for database sync.'));
173
+ console.log('');
174
+ console.log((0, db_utils_1.getMongoDumpInstallInstructions)());
175
+ console.log('');
176
+ console.log(chalk_1.default.dim('Alternatively:'));
177
+ console.log(chalk_1.default.dim(' • Use --dump <path> to provide an existing dump file'));
178
+ return;
179
+ }
180
+ const dumpSpinner = (0, ora_1.default)('Creating database dump...').start();
181
+ const dumpResult = await (0, db_utils_1.runLocalMongoDump)(sourceUrl, {
182
+ onProgress: (msg) => dumpSpinner.text = msg,
183
+ });
184
+ if (!dumpResult.success) {
185
+ dumpSpinner.fail(chalk_1.default.red('Database dump failed'));
186
+ console.log(chalk_1.default.red(` ${dumpResult.error}`));
187
+ return;
188
+ }
189
+ dumpSpinner.succeed(chalk_1.default.green(`Database dump created (${(0, db_utils_1.formatBytes)(dumpResult.sizeBytes || 0)})`));
190
+ localDumpPath = dumpResult.dumpPath;
191
+ }
192
+ // Upload to S3
193
+ if (localDumpPath) {
194
+ const uploadSpinner = (0, ora_1.default)('Uploading database snapshot...').start();
195
+ const snapshotResult = await (0, db_utils_1.createAndUploadSnapshot)(localDumpPath, projectId, snapshotSource, {
196
+ sourceUrl: sourceUrl.replace(/\/\/[^:]+:[^@]+@/, '//***:***@'),
197
+ onProgress: (msg) => uploadSpinner.text = msg,
198
+ });
199
+ if (snapshotResult.success) {
200
+ uploadSpinner.succeed(chalk_1.default.green('Database snapshot uploaded'));
201
+ snapshotId = snapshotResult.snapshotId;
202
+ snapshotS3Key = snapshotResult.s3Key;
203
+ // Only cleanup if we created the dump (not user-provided)
204
+ if (!options.dump) {
205
+ (0, db_utils_1.cleanupDump)(localDumpPath);
206
+ }
207
+ }
208
+ else {
209
+ uploadSpinner.fail(chalk_1.default.red('Database snapshot upload failed'));
210
+ console.log(chalk_1.default.dim(` Error: ${snapshotResult.error}`));
211
+ if (!options.dump) {
212
+ (0, db_utils_1.cleanupDump)(localDumpPath);
213
+ }
214
+ return;
215
+ }
216
+ }
217
+ }
218
+ // Trigger restore on genbox
219
+ if (!snapshotId || !snapshotS3Key) {
220
+ console.log(chalk_1.default.red('No snapshot available to restore'));
189
221
  return;
190
222
  }
191
- const genbox = selectResult.genbox.name;
192
- const spinner = (0, ora_1.default)('Fetching database status...').start();
223
+ const restoreSpinner = (0, ora_1.default)('Restoring database on genbox...').start();
193
224
  try {
194
- const result = await executeDbOperation({
195
- genboxName: genbox,
196
- operation: 'status',
225
+ await (0, api_1.fetchApi)(`/genboxes/${genboxId}/db/restore`, {
226
+ method: 'POST',
227
+ body: JSON.stringify({
228
+ snapshotId,
229
+ s3Key: snapshotS3Key,
230
+ }),
197
231
  });
198
- spinner.stop();
232
+ restoreSpinner.succeed(chalk_1.default.green('Database sync completed!'));
199
233
  console.log('');
200
- console.log(chalk_1.default.bold(`Database Status: ${genbox}`));
201
- console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
202
- if (result.status === 'connected') {
203
- console.log(` ${chalk_1.default.bold('Status:')} ${chalk_1.default.green('Connected')}`);
204
- }
205
- else {
206
- console.log(` ${chalk_1.default.bold('Status:')} ${chalk_1.default.red('Disconnected')}`);
207
- }
208
- if (result.mode) {
209
- console.log(` ${chalk_1.default.bold('Mode:')} ${result.mode}`);
210
- }
211
- if (result.source) {
212
- console.log(` ${chalk_1.default.bold('Source:')} ${result.source}`);
213
- }
214
- if (result.database) {
215
- console.log(` ${chalk_1.default.bold('Database:')} ${result.database}`);
216
- }
217
- if (result.collections) {
218
- console.log(` ${chalk_1.default.bold('Collections:')} ${result.collections}`);
219
- }
220
- if (result.size) {
221
- console.log(` ${chalk_1.default.bold('Size:')} ${result.size}`);
222
- }
223
- if (result.lastSync) {
224
- console.log(` ${chalk_1.default.bold('Last Sync:')} ${result.lastSync}`);
225
- }
226
- console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
234
+ console.log(chalk_1.default.dim(` Database has been restored from ${source} snapshot.`));
227
235
  }
228
236
  catch (error) {
229
- spinner.fail(chalk_1.default.red('Failed to get status'));
230
- console.error(chalk_1.default.red(`Error: ${error.message}`));
237
+ restoreSpinner.fail(chalk_1.default.red('Database restore failed'));
238
+ console.error(chalk_1.default.red(` Error: ${error.message}`));
239
+ if (error instanceof api_1.AuthenticationError) {
240
+ console.log(chalk_1.default.yellow('\nRun: genbox login'));
241
+ }
231
242
  }
232
243
  }
233
244
  catch (error) {
245
+ if (error.name === 'ExitPromptError') {
246
+ console.log(chalk_1.default.dim('\nCancelled.'));
247
+ return;
248
+ }
234
249
  console.error(chalk_1.default.red(`Error: ${error.message}`));
235
250
  }
236
251
  });
237
- // Subcommand: db restore
252
+ // Subcommand: db restore (from existing snapshot)
238
253
  exports.dbSyncCommand
239
254
  .command('restore [genbox]')
240
- .description('Restore database from a backup')
241
- .option('-f, --file <file>', 'Path to backup file (local or S3 URL)')
242
- .option('--force', 'Skip confirmation prompt')
255
+ .description('Restore database from an existing snapshot')
256
+ .option('-s, --source <source>', 'Snapshot source: staging, production', 'production')
257
+ .option('--snapshot <id>', 'Specific snapshot ID to restore')
258
+ .option('-f, --force', 'Skip confirmation prompt')
243
259
  .action(async (genboxName, options) => {
244
260
  try {
245
- const configLoader = new config_loader_1.ConfigLoader();
246
- const loadResult = await configLoader.load();
247
261
  // Select genbox
248
- const selectResult = await (0, genbox_selector_1.selectGenbox)(genboxName);
249
- if (!selectResult.genbox) {
250
- if (!selectResult.cancelled) {
262
+ const result = await (0, genbox_selector_1.selectGenbox)(genboxName);
263
+ if (!result.genbox) {
264
+ if (!result.cancelled) {
251
265
  console.log(chalk_1.default.yellow('No genbox selected'));
252
266
  }
253
267
  return;
254
268
  }
255
- const genbox = selectResult.genbox.name;
256
- if (!options.file) {
257
- // List available backups
258
- console.log(chalk_1.default.blue('Fetching available backups...'));
269
+ const genbox = result.genbox;
270
+ const genboxId = genbox._id;
271
+ // Check project ID
272
+ const projectId = genbox.project;
273
+ if (!projectId) {
274
+ console.log(chalk_1.default.red('Project not synced - cannot restore database.'));
275
+ console.log(chalk_1.default.dim(' Run `genbox init` first to sync your project.'));
276
+ return;
277
+ }
278
+ const source = options.source;
279
+ const snapshotSource = source === 'production' ? 'production' : source === 'staging' ? 'staging' : 'local';
280
+ let snapshotId = options.snapshot;
281
+ let snapshotS3Key;
282
+ // Find snapshot if not specified
283
+ if (!snapshotId) {
259
284
  try {
260
- const result = await (0, api_1.fetchApi)(`/genboxes/${genbox}/backups`);
261
- if (!result.backups || result.backups.length === 0) {
262
- console.log(chalk_1.default.yellow('No backups available'));
285
+ const existingSnapshot = await (0, api_1.getLatestSnapshot)(projectId, snapshotSource);
286
+ if (!existingSnapshot) {
287
+ console.log(chalk_1.default.yellow(`No ${source} snapshots found for this project.`));
288
+ console.log(chalk_1.default.dim(' Use `genbox db sync` to create a snapshot first.'));
263
289
  return;
264
290
  }
291
+ const snapshotAge = Date.now() - new Date(existingSnapshot.createdAt).getTime();
292
+ const hoursAgo = Math.floor(snapshotAge / (1000 * 60 * 60));
293
+ const timeAgoStr = hoursAgo < 1 ? 'less than an hour ago' : `${hoursAgo} hours ago`;
265
294
  console.log('');
266
- console.log(chalk_1.default.bold('Available Backups:'));
295
+ console.log(chalk_1.default.bold('Available Snapshot:'));
267
296
  console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
268
- for (const backup of result.backups.slice(0, 10)) {
269
- console.log(` ${backup.name} ${chalk_1.default.dim(backup.date)} ${chalk_1.default.dim(backup.size)}`);
270
- }
297
+ console.log(` ${chalk_1.default.bold('Source:')} ${source}`);
298
+ console.log(` ${chalk_1.default.bold('Created:')} ${timeAgoStr}`);
299
+ console.log(` ${chalk_1.default.bold('Size:')} ${(0, db_utils_1.formatBytes)(existingSnapshot.sizeBytes)}`);
271
300
  console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
272
- console.log('');
273
- console.log(chalk_1.default.dim('Use: genbox db restore <genbox> --file <backup-name>'));
274
- return;
301
+ snapshotId = existingSnapshot._id;
302
+ snapshotS3Key = existingSnapshot.s3Key;
275
303
  }
276
304
  catch (error) {
277
- console.error(chalk_1.default.red(`Error: ${error.message}`));
305
+ console.log(chalk_1.default.red(`Failed to fetch snapshots: ${error.message}`));
278
306
  return;
279
307
  }
280
308
  }
@@ -283,7 +311,7 @@ exports.dbSyncCommand
283
311
  console.log('');
284
312
  console.log(chalk_1.default.yellow('⚠ This will replace all data in the genbox database!'));
285
313
  const confirm = await prompts.confirm({
286
- message: `Restore database from ${options.file}?`,
314
+ message: `Restore database on ${genbox.name}?`,
287
315
  default: false,
288
316
  });
289
317
  if (!confirm) {
@@ -291,26 +319,24 @@ exports.dbSyncCommand
291
319
  return;
292
320
  }
293
321
  }
294
- // Execute restore
295
- const spinner = (0, ora_1.default)('Restoring database...').start();
322
+ // Trigger restore
323
+ const restoreSpinner = (0, ora_1.default)('Restoring database...').start();
296
324
  try {
297
- const result = await executeDbOperation({
298
- genboxName: genbox,
299
- operation: 'restore',
300
- source: options.file,
325
+ await (0, api_1.fetchApi)(`/genboxes/${genboxId}/db/restore`, {
326
+ method: 'POST',
327
+ body: JSON.stringify({
328
+ snapshotId,
329
+ s3Key: snapshotS3Key,
330
+ }),
301
331
  });
302
- spinner.succeed(chalk_1.default.green('Database restored successfully!'));
303
- if (result.stats) {
304
- console.log('');
305
- console.log(chalk_1.default.bold('Restore Statistics:'));
306
- console.log(` Collections: ${result.stats.collections || 0}`);
307
- console.log(` Documents: ${result.stats.documents || 0}`);
308
- console.log(` Duration: ${result.stats.duration || 'N/A'}`);
309
- }
332
+ restoreSpinner.succeed(chalk_1.default.green('Database restored successfully!'));
310
333
  }
311
334
  catch (error) {
312
- spinner.fail(chalk_1.default.red('Restore failed'));
313
- console.error(chalk_1.default.red(`Error: ${error.message}`));
335
+ restoreSpinner.fail(chalk_1.default.red('Restore failed'));
336
+ console.error(chalk_1.default.red(` Error: ${error.message}`));
337
+ if (error instanceof api_1.AuthenticationError) {
338
+ console.log(chalk_1.default.yellow('\nRun: genbox login'));
339
+ }
314
340
  }
315
341
  }
316
342
  catch (error) {
@@ -321,41 +347,56 @@ exports.dbSyncCommand
321
347
  console.error(chalk_1.default.red(`Error: ${error.message}`));
322
348
  }
323
349
  });
324
- // Subcommand: db dump
350
+ // Subcommand: db snapshots (list available snapshots)
325
351
  exports.dbSyncCommand
326
- .command('dump [genbox]')
327
- .description('Create a database dump/backup')
328
- .option('-o, --output <path>', 'Output path for the dump')
329
- .action(async (genboxName, options) => {
352
+ .command('snapshots')
353
+ .description('List available database snapshots for current project')
354
+ .action(async () => {
330
355
  try {
331
356
  const configLoader = new config_loader_1.ConfigLoader();
332
357
  const loadResult = await configLoader.load();
333
- // Select genbox
334
- const selectResult = await (0, genbox_selector_1.selectGenbox)(genboxName);
335
- if (!selectResult.genbox) {
336
- if (!selectResult.cancelled) {
337
- console.log(chalk_1.default.yellow('No genbox selected'));
338
- }
358
+ if (!loadResult.config) {
359
+ console.log(chalk_1.default.red('Not a genbox project'));
360
+ console.log(chalk_1.default.dim('Run "genbox init" to initialize a project.'));
339
361
  return;
340
362
  }
341
- const genbox = selectResult.genbox.name;
342
- const spinner = (0, ora_1.default)('Creating database dump...').start();
363
+ // Get project ID
364
+ const config = loadResult.config;
365
+ const projectName = config.project?.name;
366
+ if (!projectName) {
367
+ console.log(chalk_1.default.red('Project name not found in genbox.yaml'));
368
+ return;
369
+ }
370
+ // Fetch project to get ID
371
+ const spinner = (0, ora_1.default)('Fetching snapshots...').start();
343
372
  try {
344
- const result = await (0, api_1.fetchApi)(`/genboxes/${genbox}/db/dump`, {
345
- method: 'POST',
346
- body: JSON.stringify({ output: options.output }),
347
- });
348
- spinner.succeed(chalk_1.default.green('Database dump created!'));
349
- if (result.path) {
350
- console.log(` Location: ${result.path}`);
373
+ const project = await (0, api_1.fetchApi)(`/projects/by-name/${projectName}`);
374
+ if (!project?._id) {
375
+ spinner.fail(chalk_1.default.yellow('Project not synced'));
376
+ console.log(chalk_1.default.dim(' Run `genbox init` to sync your project.'));
377
+ return;
351
378
  }
352
- if (result.size) {
353
- console.log(` Size: ${result.size}`);
379
+ const snapshots = await (0, api_1.fetchApi)(`/snapshots/project/${project._id}`);
380
+ spinner.stop();
381
+ if (!snapshots || snapshots.length === 0) {
382
+ console.log(chalk_1.default.yellow('No snapshots found for this project.'));
383
+ console.log(chalk_1.default.dim(' Use `genbox db sync` or `genbox create --db copy` to create snapshots.'));
384
+ return;
385
+ }
386
+ console.log('');
387
+ console.log(chalk_1.default.bold('Database Snapshots:'));
388
+ console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
389
+ for (const snapshot of snapshots) {
390
+ const age = Date.now() - new Date(snapshot.createdAt).getTime();
391
+ const hoursAgo = Math.floor(age / (1000 * 60 * 60));
392
+ const timeAgoStr = hoursAgo < 1 ? '<1h ago' : hoursAgo < 24 ? `${hoursAgo}h ago` : `${Math.floor(hoursAgo / 24)}d ago`;
393
+ console.log(` ${chalk_1.default.cyan(snapshot.source.padEnd(12))} ${(0, db_utils_1.formatBytes)(snapshot.sizeBytes).padEnd(10)} ${chalk_1.default.dim(timeAgoStr)}`);
354
394
  }
395
+ console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
355
396
  }
356
397
  catch (error) {
357
- spinner.fail(chalk_1.default.red('Dump failed'));
358
- console.error(chalk_1.default.red(`Error: ${error.message}`));
398
+ spinner.fail(chalk_1.default.red('Failed to fetch snapshots'));
399
+ console.error(chalk_1.default.red(` Error: ${error.message}`));
359
400
  }
360
401
  }
361
402
  catch (error) {
@@ -49,6 +49,7 @@ const profile_resolver_1 = require("../profile-resolver");
49
49
  const api_1 = require("../api");
50
50
  const genbox_selector_1 = require("../genbox-selector");
51
51
  const schema_v4_1 = require("../schema-v4");
52
+ const db_utils_1 = require("../db-utils");
52
53
  function getPublicSshKey() {
53
54
  const home = os.homedir();
54
55
  const potentialKeys = [
@@ -419,6 +420,9 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
419
420
  .option('-b, --branch <branch>', 'Git branch to checkout')
420
421
  .option('-n, --new-branch <name>', 'Create a new branch with this name')
421
422
  .option('-f, --from-branch <branch>', 'Source branch to create new branch from')
423
+ .option('--db <mode>', 'Database mode: fresh, copy, none')
424
+ .option('--db-source <source>', 'Database source for copy mode: staging, production')
425
+ .option('--db-dump <path>', 'Path to existing database dump file')
422
426
  .option('-y, --yes', 'Skip interactive prompts')
423
427
  .action(async (name, options) => {
424
428
  try {
@@ -493,6 +497,10 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
493
497
  // For new branch creation: only use CLI options (user must explicitly request new branch on rebuild)
494
498
  const effectiveNewBranch = newBranchName;
495
499
  const effectiveSourceBranch = options.fromBranch;
500
+ // For database: use CLI option, or stored database config (convert 'snapshot' to 'copy')
501
+ const storedDbMode = genbox.database?.mode === 'snapshot' ? 'copy' : genbox.database?.mode;
502
+ const effectiveDbMode = options.db || storedDbMode;
503
+ const effectiveDbSource = options.dbSource || genbox.database?.source;
496
504
  // Build options for resolving - use stored config, skip interactive prompts
497
505
  const createOptions = {
498
506
  name: selectedName,
@@ -501,6 +509,8 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
501
509
  branch: effectiveBranch,
502
510
  newBranch: effectiveNewBranch,
503
511
  sourceBranch: effectiveSourceBranch,
512
+ db: effectiveDbMode,
513
+ dbSource: effectiveDbSource,
504
514
  // Skip interactive prompts if we have stored config
505
515
  yes: options.yes || !!(storedProfile || storedApps),
506
516
  };
@@ -538,6 +548,12 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
538
548
  console.log(` - ${repo.name}: ${branchInfo}`);
539
549
  }
540
550
  }
551
+ // Display database info
552
+ if (effectiveDbMode && effectiveDbMode !== 'none') {
553
+ console.log('');
554
+ const dbModeDisplay = effectiveDbMode === 'local' ? 'fresh' : effectiveDbMode;
555
+ console.log(` ${chalk_1.default.bold('Database:')} ${dbModeDisplay}${effectiveDbSource ? ` (from ${effectiveDbSource})` : ''}`);
556
+ }
541
557
  console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
542
558
  // Confirm rebuild
543
559
  if (!options.yes) {
@@ -573,8 +589,139 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
573
589
  }
574
590
  }
575
591
  }
592
+ // Handle database copy if requested
593
+ let snapshotId;
594
+ let snapshotS3Key;
595
+ let localDumpPath;
596
+ // Use the effective database mode/source from createOptions
597
+ const dbMode = effectiveDbMode || 'none';
598
+ const dbSource = effectiveDbSource || 'production';
599
+ if (dbMode === 'copy' && resolved.database?.url) {
600
+ // Get project ID for S3 upload
601
+ const projectId = genbox.project;
602
+ if (!projectId) {
603
+ console.log(chalk_1.default.red('Project not synced - cannot upload database snapshot.'));
604
+ console.log(chalk_1.default.dim(' Run `genbox init` first to sync your project.'));
605
+ return;
606
+ }
607
+ // Map source to snapshot source type
608
+ const snapshotSource = dbSource === 'production' ? 'production' : dbSource === 'staging' ? 'staging' : 'local';
609
+ // Check for existing snapshots
610
+ let useExistingSnapshot = false;
611
+ try {
612
+ const existingSnapshot = await (0, api_1.getLatestSnapshot)(projectId, snapshotSource);
613
+ if (existingSnapshot) {
614
+ const snapshotAge = Date.now() - new Date(existingSnapshot.createdAt).getTime();
615
+ const hoursAgo = Math.floor(snapshotAge / (1000 * 60 * 60));
616
+ const timeAgoStr = hoursAgo < 1 ? 'less than an hour ago' : `${hoursAgo} hours ago`;
617
+ if (!options.yes) {
618
+ const snapshotChoice = await prompts.select({
619
+ message: `Found existing ${snapshotSource} snapshot (${timeAgoStr}):`,
620
+ choices: [
621
+ {
622
+ name: `Use existing snapshot (${timeAgoStr}, ${(0, db_utils_1.formatBytes)(existingSnapshot.sizeBytes)})`,
623
+ value: 'existing',
624
+ },
625
+ {
626
+ name: 'Create fresh snapshot (dump now)',
627
+ value: 'fresh',
628
+ },
629
+ ],
630
+ });
631
+ useExistingSnapshot = snapshotChoice === 'existing';
632
+ }
633
+ else if (hoursAgo < 24) {
634
+ useExistingSnapshot = true;
635
+ console.log(chalk_1.default.dim(` Using existing snapshot from ${timeAgoStr}`));
636
+ }
637
+ if (useExistingSnapshot) {
638
+ snapshotId = existingSnapshot._id;
639
+ snapshotS3Key = existingSnapshot.s3Key;
640
+ console.log(chalk_1.default.green(` ✓ Using existing snapshot`));
641
+ }
642
+ }
643
+ }
644
+ catch {
645
+ // Silently continue if we can't fetch snapshots
646
+ }
647
+ // If not using existing snapshot, create a new one
648
+ if (!useExistingSnapshot) {
649
+ // Check for user-provided dump file
650
+ if (options.dbDump) {
651
+ if (!fs.existsSync(options.dbDump)) {
652
+ console.log(chalk_1.default.red(`Database dump file not found: ${options.dbDump}`));
653
+ return;
654
+ }
655
+ localDumpPath = options.dbDump;
656
+ console.log(chalk_1.default.dim(` Using provided dump file: ${options.dbDump}`));
657
+ }
658
+ else {
659
+ // Need to run mongodump locally
660
+ if (!(0, db_utils_1.isMongoDumpAvailable)()) {
661
+ console.log(chalk_1.default.red('mongodump not found. Required for database copy.'));
662
+ console.log('');
663
+ console.log((0, db_utils_1.getMongoDumpInstallInstructions)());
664
+ console.log('');
665
+ console.log(chalk_1.default.dim('Alternatively:'));
666
+ console.log(chalk_1.default.dim(' • Use --db-dump <path> to provide an existing dump file'));
667
+ console.log(chalk_1.default.dim(' • Use --db fresh to start with empty database'));
668
+ return;
669
+ }
670
+ const dbUrl = resolved.database.url;
671
+ console.log('');
672
+ console.log(chalk_1.default.blue('=== Database Copy ==='));
673
+ console.log(chalk_1.default.dim(` Source: ${dbSource}`));
674
+ console.log(chalk_1.default.dim(` URL: ${dbUrl.replace(/\/\/[^:]+:[^@]+@/, '//***:***@')}`));
675
+ const dumpSpinner = (0, ora_1.default)('Creating database dump...').start();
676
+ const dumpResult = await (0, db_utils_1.runLocalMongoDump)(dbUrl, {
677
+ onProgress: (msg) => dumpSpinner.text = msg,
678
+ });
679
+ if (!dumpResult.success) {
680
+ dumpSpinner.fail(chalk_1.default.red('Database dump failed'));
681
+ console.log(chalk_1.default.red(` ${dumpResult.error}`));
682
+ return;
683
+ }
684
+ dumpSpinner.succeed(chalk_1.default.green(`Database dump created (${(0, db_utils_1.formatBytes)(dumpResult.sizeBytes || 0)})`));
685
+ localDumpPath = dumpResult.dumpPath;
686
+ }
687
+ // Upload to S3
688
+ if (localDumpPath) {
689
+ const uploadSpinner = (0, ora_1.default)('Uploading database snapshot...').start();
690
+ const snapshotResult = await (0, db_utils_1.createAndUploadSnapshot)(localDumpPath, projectId, snapshotSource, {
691
+ sourceUrl: resolved.database.url?.replace(/\/\/[^:]+:[^@]+@/, '//***:***@'),
692
+ onProgress: (msg) => uploadSpinner.text = msg,
693
+ });
694
+ if (snapshotResult.success) {
695
+ uploadSpinner.succeed(chalk_1.default.green('Database snapshot uploaded'));
696
+ snapshotId = snapshotResult.snapshotId;
697
+ snapshotS3Key = snapshotResult.s3Key;
698
+ (0, db_utils_1.cleanupDump)(localDumpPath);
699
+ localDumpPath = undefined;
700
+ }
701
+ else {
702
+ uploadSpinner.fail(chalk_1.default.red('Database snapshot upload failed'));
703
+ console.log(chalk_1.default.dim(` Error: ${snapshotResult.error}`));
704
+ console.log(chalk_1.default.dim(' Try again later or use --db fresh to start with empty database.'));
705
+ (0, db_utils_1.cleanupDump)(localDumpPath);
706
+ return;
707
+ }
708
+ }
709
+ }
710
+ }
576
711
  // Build payload
577
712
  const payload = buildRebuildPayload(resolved, config, publicKey, privateKeyContent, configLoader);
713
+ // Add database info to payload if we have a snapshot
714
+ if (snapshotId && snapshotS3Key) {
715
+ payload.database = {
716
+ mode: 'snapshot',
717
+ source: dbSource,
718
+ snapshotId,
719
+ s3Key: snapshotS3Key,
720
+ };
721
+ }
722
+ else if (dbMode === 'local' || dbMode === 'fresh') {
723
+ payload.database = { mode: 'local' };
724
+ }
578
725
  // Execute rebuild
579
726
  const rebuildSpinner = (0, ora_1.default)(`Rebuilding Genbox '${selectedName}'...`).start();
580
727
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.54",
3
+ "version": "1.0.55",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {