genbox 1.0.84 → 1.0.86

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.
@@ -170,132 +170,139 @@ async function handleBulkDelete(options) {
170
170
  console.log(chalk_1.default.yellow(`Completed: ${successCount} destroyed, ${failCount} failed.`));
171
171
  }
172
172
  }
173
+ /**
174
+ * Trigger save-work on genbox and display progress
175
+ * Returns the result with status and repo details
176
+ */
177
+ async function triggerSaveWork(genbox) {
178
+ const spinner = (0, ora_1.default)('Saving uncommitted changes...').start();
179
+ spinner.text = 'Checking for uncommitted changes...';
180
+ try {
181
+ const result = await (0, api_1.fetchApi)(`/genboxes/${genbox._id}/save-work`, {
182
+ method: 'POST',
183
+ });
184
+ // Handle different statuses
185
+ switch (result.status) {
186
+ case 'no_changes':
187
+ spinner.succeed(chalk_1.default.dim('No uncommitted changes found'));
188
+ return result;
189
+ case 'success':
190
+ spinner.succeed(chalk_1.default.green('All changes saved successfully'));
191
+ break;
192
+ case 'partial':
193
+ spinner.warn(chalk_1.default.yellow('Some changes saved (partial success)'));
194
+ break;
195
+ case 'failed':
196
+ spinner.fail(chalk_1.default.red('Failed to save changes'));
197
+ if (result.error) {
198
+ console.log(chalk_1.default.dim(` Error: ${result.error}`));
199
+ }
200
+ break;
201
+ case 'unreachable':
202
+ spinner.fail(chalk_1.default.red('Genbox is not reachable'));
203
+ if (result.error) {
204
+ console.log(chalk_1.default.dim(` ${result.error}`));
205
+ }
206
+ return result;
207
+ default:
208
+ spinner.info(chalk_1.default.dim(`Save work status: ${result.status}`));
209
+ }
210
+ // Display repo results
211
+ if (result.repos && result.repos.length > 0) {
212
+ console.log('');
213
+ for (const repo of result.repos) {
214
+ const repoName = repo.path.split('/').pop() || repo.path;
215
+ if (!repo.hadChanges) {
216
+ console.log(chalk_1.default.dim(` ${repoName}: No changes`));
217
+ continue;
218
+ }
219
+ if (repo.committed && repo.pushed) {
220
+ const statusIcon = repo.prCreated ? chalk_1.default.green('✓') : chalk_1.default.yellow('↑');
221
+ const branchInfo = chalk_1.default.cyan(repo.branch);
222
+ if (repo.prCreated && repo.prUrl) {
223
+ console.log(` ${statusIcon} ${repoName}: PR created → ${chalk_1.default.underline(repo.prUrl)}`);
224
+ }
225
+ else {
226
+ console.log(` ${statusIcon} ${repoName}: Pushed to ${branchInfo}`);
227
+ }
228
+ }
229
+ else if (repo.error) {
230
+ console.log(` ${chalk_1.default.red('✗')} ${repoName}: ${chalk_1.default.red(repo.error)}`);
231
+ }
232
+ else {
233
+ console.log(` ${chalk_1.default.red('✗')} ${repoName}: Failed to save`);
234
+ }
235
+ }
236
+ }
237
+ return result;
238
+ }
239
+ catch (error) {
240
+ spinner.fail(chalk_1.default.red(`Failed to save work: ${error.message}`));
241
+ return { status: 'failed', error: error.message };
242
+ }
243
+ }
173
244
  /**
174
245
  * Check for uncommitted changes and handle them before destroy
175
246
  */
176
247
  async function handleUncommittedChanges(genbox, options) {
177
- // Skip if --yes flag is used (user explicitly wants to skip this)
248
+ // Skip if --yes flag is used without save-changes (user explicitly wants to skip)
178
249
  if (options.yes && !options.saveChanges) {
179
250
  return { proceed: true };
180
251
  }
181
- // If save-changes is explicitly set, use that value
252
+ // If save-changes is set to 'trash' or 'skip', don't save
182
253
  if (options.saveChanges === 'trash' || options.saveChanges === 'skip') {
183
- return { proceed: true, action: options.saveChanges };
254
+ return { proceed: true };
255
+ }
256
+ // If save-changes is set to 'save', auto-save without prompting
257
+ if (options.saveChanges === 'save') {
258
+ console.log('');
259
+ const result = await triggerSaveWork(genbox);
260
+ return { proceed: true, saveWorkResult: result };
184
261
  }
185
- // Check for uncommitted changes by running git status on the genbox
262
+ // Interactive mode - ask user what to do
186
263
  console.log('');
187
264
  console.log(chalk_1.default.blue('Checking for uncommitted changes...'));
188
- try {
189
- // Try to get git status from the genbox
190
- const statusResponse = await (0, api_1.fetchApi)(`/genboxes/${genbox._id}/exec`, {
191
- method: 'POST',
192
- body: JSON.stringify({
193
- command: 'cd /home/dev/* 2>/dev/null && git status --porcelain 2>/dev/null || echo ""',
194
- timeout: 10000,
195
- }),
196
- });
197
- const gitStatus = statusResponse?.stdout?.trim() || '';
198
- if (!gitStatus) {
199
- console.log(chalk_1.default.dim(' No uncommitted changes detected.'));
200
- return { proceed: true, action: 'none' };
201
- }
202
- // There are uncommitted changes
203
- const changeCount = gitStatus.split('\n').filter(line => line.trim()).length;
204
- console.log(chalk_1.default.yellow(` Found ${changeCount} uncommitted change${changeCount === 1 ? '' : 's'}.`));
265
+ // First, do a quick check via save-work to see if there are changes
266
+ // The save-work endpoint will return 'no_changes' if nothing to save
267
+ const result = await triggerSaveWork(genbox);
268
+ // If no changes or unreachable, just proceed
269
+ if (result.status === 'no_changes' || result.status === 'unreachable') {
270
+ return { proceed: true, saveWorkResult: result };
271
+ }
272
+ // If save was successful or partial, we're done
273
+ if (result.status === 'success' || result.status === 'partial') {
274
+ return { proceed: true, saveWorkResult: result };
275
+ }
276
+ // If failed, ask user if they want to continue
277
+ if (result.status === 'failed') {
205
278
  console.log('');
206
- // If save-changes is set to 'pr', auto-select that option
207
- if (options.saveChanges === 'pr') {
208
- return { proceed: true, action: 'pr' };
209
- }
210
- // Prompt user for action
211
279
  const action = await (0, select_1.default)({
212
- message: 'What would you like to do with uncommitted changes?',
280
+ message: 'Save work failed. What would you like to do?',
213
281
  choices: [
214
282
  {
215
- name: 'Commit, push, and create PR (recommended)',
216
- value: 'pr',
217
- description: 'Creates a PR with your changes before destroying',
218
- },
219
- {
220
- name: 'Trash changes',
221
- value: 'trash',
222
- description: 'Discard all uncommitted changes',
283
+ name: 'Continue with destroy (changes will be lost)',
284
+ value: 'continue',
223
285
  },
224
286
  {
225
287
  name: 'Cancel destroy',
226
288
  value: 'cancel',
227
- description: 'Keep the genbox running',
228
289
  },
229
290
  ],
230
- default: 'pr',
291
+ default: 'cancel',
231
292
  });
232
293
  if (action === 'cancel') {
233
- return { proceed: false };
234
- }
235
- return { proceed: true, action };
236
- }
237
- catch (error) {
238
- // If we can't check (e.g., genbox is not running), just proceed
239
- console.log(chalk_1.default.dim(' Could not check for changes (genbox may not be accessible).'));
240
- return { proceed: true, action: 'skip' };
241
- }
242
- }
243
- /**
244
- * Commit, push and create PR for changes
245
- */
246
- async function createPRForChanges(genbox) {
247
- const spinner = (0, ora_1.default)('Creating PR for uncommitted changes...').start();
248
- try {
249
- // Execute git commands on the genbox to commit, push, and create PR
250
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
251
- const branchName = `genbox-save/${genbox.name}-${timestamp}`;
252
- const commands = [
253
- // Ensure we're in the project directory
254
- 'cd /home/dev/*',
255
- // Create a new branch for the changes
256
- `git checkout -b "${branchName}"`,
257
- // Stage all changes
258
- 'git add -A',
259
- // Commit with a descriptive message
260
- `git commit -m "Save changes from genbox '${genbox.name}' before destroy"`,
261
- // Push to origin
262
- `git push -u origin "${branchName}"`,
263
- // Try to create PR using gh CLI if available
264
- `gh pr create --title "Changes from genbox '${genbox.name}'" --body "Auto-saved changes before genbox destroy" 2>/dev/null || echo "PR_SKIP"`,
265
- ].join(' && ');
266
- const result = await (0, api_1.fetchApi)(`/genboxes/${genbox._id}/exec`, {
267
- method: 'POST',
268
- body: JSON.stringify({
269
- command: commands,
270
- timeout: 60000,
271
- }),
272
- });
273
- if (result?.exitCode !== 0 && !result?.stdout?.includes('PR_SKIP')) {
274
- spinner.fail(chalk_1.default.red('Failed to save changes'));
275
- console.log(chalk_1.default.dim(` Error: ${result?.stderr || 'Unknown error'}`));
276
- return false;
277
- }
278
- if (result?.stdout?.includes('PR_SKIP')) {
279
- spinner.succeed(chalk_1.default.green(`Changes pushed to branch '${branchName}'`));
280
- console.log(chalk_1.default.dim(' Note: Could not create PR automatically. Create it manually on GitHub.'));
281
- }
282
- else {
283
- spinner.succeed(chalk_1.default.green('PR created successfully'));
294
+ return { proceed: false, saveWorkResult: result };
284
295
  }
285
- return true;
286
- }
287
- catch (error) {
288
- spinner.fail(chalk_1.default.red(`Failed to save changes: ${error.message}`));
289
- return false;
290
296
  }
297
+ return { proceed: true, saveWorkResult: result };
291
298
  }
292
299
  exports.destroyCommand = new commander_1.Command('destroy')
293
300
  .alias('delete')
294
301
  .description('Destroy one or more Genboxes')
295
302
  .argument('[name]', 'Name of the Genbox to destroy (optional - will prompt if not provided)')
296
- .option('-y, --yes', 'Skip confirmation')
303
+ .option('-y, --yes', 'Skip confirmation and save-work prompts')
297
304
  .option('-a, --all', 'Bulk delete mode - select multiple genboxes to destroy')
298
- .option('--save-changes <action>', 'Handle uncommitted changes: pr (commit/push/PR), trash (discard), skip (ignore)')
305
+ .option('--save-changes <action>', 'Handle uncommitted changes: save (commit/push/PR), trash (discard), skip (ignore)')
299
306
  .action(async (name, options) => {
300
307
  try {
301
308
  // Bulk delete mode when --all is used without a specific name
@@ -317,27 +324,14 @@ exports.destroyCommand = new commander_1.Command('destroy')
317
324
  }
318
325
  // Check for uncommitted changes if genbox is running
319
326
  if (target.status === 'running') {
320
- const { proceed, action } = await handleUncommittedChanges(target, options);
327
+ const { proceed } = await handleUncommittedChanges(target, options);
321
328
  if (!proceed) {
322
329
  console.log(chalk_1.default.dim('Destroy cancelled.'));
323
330
  return;
324
331
  }
325
- // Handle the action
326
- if (action === 'pr') {
327
- const success = await createPRForChanges(target);
328
- if (!success) {
329
- const continueAnyway = await (0, confirm_1.default)({
330
- message: 'Failed to save changes. Continue with destroy anyway?',
331
- default: false,
332
- });
333
- if (!continueAnyway) {
334
- console.log(chalk_1.default.dim('Destroy cancelled.'));
335
- return;
336
- }
337
- }
338
- }
339
332
  }
340
333
  // Confirm
334
+ console.log('');
341
335
  let confirmed = options.yes;
342
336
  if (!confirmed) {
343
337
  confirmed = await (0, confirm_1.default)({
@@ -391,21 +391,19 @@ exports.statusCommand = new commander_1.Command('status')
391
391
  sshAvailable = true;
392
392
  // Check if cloud-init is still running
393
393
  if (status.includes('running')) {
394
- // Get timing breakdown
395
- const timing = getTimingBreakdown(target.ipAddress, keyPath);
396
- if (timing.total !== null) {
397
- console.log(chalk_1.default.yellow(`[INFO] Setup in progress... (elapsed: ${formatDuration(timing.total)})`));
398
- }
399
- else {
400
- console.log(chalk_1.default.yellow('[INFO] Setup in progress...'));
401
- }
394
+ // Use the total wait time from CLI's perspective (includes SSH wait)
395
+ const totalWaitSecs = Math.floor((Date.now() - startTime) / 1000);
396
+ console.log(chalk_1.default.yellow(`[INFO] Setup in progress... (elapsed: ${formatDuration(totalWaitSecs)})`));
402
397
  console.log('');
403
- // Show timing breakdown so far
404
- if (timing.sshReady !== null) {
405
- console.log(chalk_1.default.blue('[INFO] === Timing So Far ==='));
406
- console.log(` SSH Ready: ${formatDuration(timing.sshReady)}`);
398
+ // Show timing breakdown so far (from server's perspective)
399
+ const timing = getTimingBreakdown(target.ipAddress, keyPath);
400
+ if (timing.sshReady !== null || timing.total !== null) {
401
+ console.log(chalk_1.default.blue('[INFO] === Server Timing ==='));
402
+ if (timing.sshReady !== null) {
403
+ console.log(` SSH Ready: ${formatDuration(timing.sshReady)} (from boot)`);
404
+ }
407
405
  if (timing.total !== null) {
408
- console.log(` Elapsed: ${formatDuration(timing.total)}`);
406
+ console.log(` Server Uptime: ${formatDuration(timing.total)}`);
409
407
  }
410
408
  console.log('');
411
409
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.84",
3
+ "version": "1.0.86",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {