From b9dde90cc618e4b3d91b0dc20b84cde5eb825e7d Mon Sep 17 00:00:00 2001 From: Grzegorz Wierzowiecki <221403+gwpl@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:24:25 +0200 Subject: [PATCH] Auto power-on after resize and interactive disk resize option (#50) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Auto power-on after resize and add 'nodisk' confirmation option DigitalOcean leaves droplets powered off after resize, with no API flag to auto-restart. Previously, `dropkit resize` completed silently with the droplet off — users had to discover this and run `dropkit on` manually. Now the resize command: 1. Automatically powers the droplet back on after resize completes, using the same pattern as `dropkit on` (with status polling and progress messages). 2. Offers a "nodisk" answer in the confirmation prompt when disk resize would increase disk size. This supports the common workflow of temporarily scaling up CPU/RAM for heavy builds or benchmarks and scaling back down later — which requires NOT resizing the disk (disk resize is permanent and prevents future downsizing). The prompt changes from: Are you sure? [yes/no] to: Are you sure? [yes/nodisk/no] with a tip explaining the option. The "nodisk" choice only appears when relevant (disk flag is true AND new size has larger disk). Expected terminal experience after resize: ✓ Resize completed successfully Powering on droplet... ✓ Power on action started (ID: 3105287233) Waiting for droplet to power on... ✓ Droplet powered on successfully Droplet claude-code-box has been resized to s-2vcpu-4gb and is now active Co-Authored-By: Claude Opus 4.6 (1M context) * Make disk resize an interactive option when not specified via flag Per reviewer feedback, instead of a "nodisk" escape hatch in the confirmation prompt, make --disk/--no-disk a tri-state (True/False/None). When neither flag is passed, the user is asked interactively — consistent with how region, size, and image are already handled. The interactive question only appears when the new size has a different disk size. It defaults to "no" (skip disk resize) since disk resize is permanent and prevents future downsizing. Flow with no flags: Changes: Disk: 25 GB → 80 GB (+55 GB) Disk resize is PERMANENT and cannot be undone. Skipping disk resize keeps the option to downsize later. Resize disk too? [yes/no] (no): Flow with --no-disk: skips the question, shows "not resized" Flow with --disk: skips the question, proceeds with disk resize Co-Authored-By: Claude Opus 4.6 (1M context) * Move interactive disk question before table display and drop isinstance guards Address ret2libc's review feedback: - Ask the interactive disk question BEFORE building the changes table, so the displayed disk row reflects the user's actual choice - Remove unnecessary isinstance(disk_diff, int) guards since disk_diff is always int (computed with an isinstance ternary that defaults to 0) Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- dropkit/main.py | 76 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/dropkit/main.py b/dropkit/main.py index 0d426f9..7b31fa1 100644 --- a/dropkit/main.py +++ b/dropkit/main.py @@ -3291,8 +3291,8 @@ def rename( def resize( droplet_name: str = typer.Argument(..., autocompletion=complete_droplet_or_snapshot_name), size: str | None = typer.Option(None, "--size", "-s", help="New size slug (e.g., s-4vcpu-8gb)"), - disk: bool = typer.Option( - True, "--disk/--no-disk", help="Resize disk (permanent, default: True)" + disk: bool | None = typer.Option( + None, "--disk/--no-disk", help="Resize disk (permanent, asked interactively if omitted)" ), ): """ @@ -3493,13 +3493,36 @@ def resize( if isinstance(new_disk, int) and isinstance(current_disk, int) else 0 ) + + # If --disk/--no-disk was not explicitly passed, ask interactively + # when the new size has a different disk. This surfaces the + # permanent/irreversible nature of disk resize at decision time, + # consistent with how region/size/image are handled interactively. + # Resolved BEFORE building the table so the row reflects the choice. + if disk is None: + if disk_diff != 0: + console.print() + console.print( + "[bold yellow]Disk resize is PERMANENT and cannot be undone.[/bold yellow]" + ) + console.print("[dim]Skipping disk resize keeps the option to downsize later.[/dim]") + disk_choice = Prompt.ask( + "[bold]Resize disk too?[/bold]", + choices=["yes", "no"], + default="no", + ) + disk = disk_choice == "yes" + else: + # No disk change — default to True (no-op for disk) + disk = True + disk_change = f"{current_disk} GB → {new_disk} GB" - if disk and disk_diff > 0: - disk_change += f" [green](+{disk_diff} GB)[/green]" - elif disk and disk_diff < 0: - disk_change += f" [yellow]({disk_diff} GB)[/yellow]" - elif not disk: + if disk is False: disk_change = f"{current_disk} GB (not resized)" + elif disk_diff > 0: + disk_change += f" [green](+{disk_diff} GB)[/green]" + elif disk_diff < 0: + disk_change += f" [yellow]({disk_diff} GB)[/yellow]" changes_table.add_row("Disk:", disk_change) # Price @@ -3516,16 +3539,17 @@ def resize( # Show warnings console.print() console.print( - "[bold yellow]⚠ WARNING: This operation will cause downtime (droplet will be powered off)[/bold yellow]" + "[bold yellow]⚠ WARNING: This operation will cause downtime " + "(droplet will be powered off)[/bold yellow]" ) - if disk: + if disk and disk_diff > 0: console.print( "[bold red]⚠ WARNING: Disk resize is PERMANENT and cannot be undone![/bold red]" ) - else: + elif not disk: console.print( - "[dim]Note: Disk will NOT be resized. You can resize it later, but it's permanent.[/dim]" + "[dim]Note: Disk will NOT be resized. You can resize back down later.[/dim]" ) # Confirmation @@ -3562,9 +3586,37 @@ def resize( api.wait_for_action_complete(action_id, timeout=600) # 10 minutes console.print("[green]✓[/green] Resize completed successfully") + + # Power the droplet back on — DigitalOcean leaves it powered off + # after resize (no auto-power-on API flag exists). Since the user + # almost certainly wants their droplet running, we power it on + # automatically instead of leaving them to discover it's off. + console.print() + console.print("[dim]Powering on droplet...[/dim]") + + power_action = api.power_on_droplet(droplet_id) + power_action_id = power_action.get("id") + + if not power_action_id: + console.print( + "[yellow]Warning: Could not get power-on action ID. " + f"You may need to run: dropkit on {droplet_name}[/yellow]" + ) + else: + console.print( + f"[green]✓[/green] Power on action started (ID: [cyan]{power_action_id}[/cyan])" + ) + console.print("[dim]Waiting for droplet to power on...[/dim]") + + with console.status("[cyan]Powering on...[/cyan]"): + api.wait_for_action_complete(power_action_id, timeout=120) + + console.print("[green]✓[/green] Droplet powered on successfully") + console.print() console.print( - f"[bold green]Droplet {droplet_name} has been resized to {new_size_slug}[/bold green]" + f"[bold green]Droplet {droplet_name} has been resized to " + f"{new_size_slug} and is now active[/bold green]" ) except DigitalOceanAPIError as e: