|
| 1 | +"""Development environment CLI commands. |
| 2 | +
|
| 3 | +This module provides commands for managing the Docker Compose |
| 4 | +development environment including Keycloak, PostgreSQL, Redis, and Temporal. |
| 5 | +
|
| 6 | +Commands: |
| 7 | + up - Start the development environment |
| 8 | + down - Stop the development environment |
| 9 | + status - Show status of development services |
| 10 | + logs - View logs from a service |
| 11 | + restart - Restart a specific service |
| 12 | +""" |
| 13 | + |
| 14 | +from pathlib import Path |
| 15 | + |
| 16 | +import typer |
| 17 | + |
| 18 | +from src.cli.deployment import DevDeployer |
| 19 | +from src.cli.deployment.helm_deployer.image_builder import DeploymentError |
| 20 | + |
| 21 | +from .shared import ( |
| 22 | + confirm_action, |
| 23 | + console, |
| 24 | + get_project_root, |
| 25 | + handle_error, |
| 26 | + print_header, |
| 27 | +) |
| 28 | + |
| 29 | +# Create the dev command group |
| 30 | +app = typer.Typer( |
| 31 | + name="dev", |
| 32 | + help="🔧 Development environment commands (Docker Compose)", |
| 33 | + no_args_is_help=True, |
| 34 | +) |
| 35 | + |
| 36 | + |
| 37 | +def _get_deployer() -> DevDeployer: |
| 38 | + """Create a DevDeployer instance with current project context.""" |
| 39 | + return DevDeployer(console, Path(get_project_root())) |
| 40 | + |
| 41 | + |
| 42 | +# ============================================================================= |
| 43 | +# Commands |
| 44 | +# ============================================================================= |
| 45 | + |
| 46 | + |
| 47 | +@app.command() |
| 48 | +def up( |
| 49 | + force: bool = typer.Option( |
| 50 | + False, |
| 51 | + "--force", |
| 52 | + "-f", |
| 53 | + help="Force restart even if services are already running", |
| 54 | + ), |
| 55 | + no_wait: bool = typer.Option( |
| 56 | + False, |
| 57 | + "--no-wait", |
| 58 | + help="Don't wait for services to be healthy", |
| 59 | + ), |
| 60 | + start_server: bool = typer.Option( |
| 61 | + True, |
| 62 | + "--start-server/--no-start-server", |
| 63 | + help="Start FastAPI dev server after services are ready", |
| 64 | + ), |
| 65 | +) -> None: |
| 66 | + """🚀 Start the development environment. |
| 67 | +
|
| 68 | + Starts all development services (Keycloak, PostgreSQL, Redis, Temporal) |
| 69 | + using Docker Compose, then optionally starts the FastAPI development server. |
| 70 | +
|
| 71 | + Examples: |
| 72 | + # Start everything including dev server |
| 73 | + api-forge-cli dev up |
| 74 | +
|
| 75 | + # Start services only, no dev server |
| 76 | + api-forge-cli dev up --no-start-server |
| 77 | +
|
| 78 | + # Force restart all services |
| 79 | + api-forge-cli dev up --force |
| 80 | + """ |
| 81 | + print_header("Starting Development Environment") |
| 82 | + |
| 83 | + try: |
| 84 | + deployer = _get_deployer() |
| 85 | + deployer.deploy(force=force, no_wait=no_wait, start_server=start_server) |
| 86 | + except DeploymentError as e: |
| 87 | + handle_error(f"Deployment failed: {e.message}", e.details) |
| 88 | + |
| 89 | + |
| 90 | +@app.command() |
| 91 | +def down( |
| 92 | + volumes: bool = typer.Option( |
| 93 | + False, |
| 94 | + "--volumes", |
| 95 | + "-v", |
| 96 | + help="Also remove data volumes (DESTROYS ALL DATA)", |
| 97 | + ), |
| 98 | + yes: bool = typer.Option( |
| 99 | + False, |
| 100 | + "--yes", |
| 101 | + "-y", |
| 102 | + help="Skip confirmation prompt", |
| 103 | + ), |
| 104 | +) -> None: |
| 105 | + """⏹️ Stop the development environment. |
| 106 | +
|
| 107 | + Stops all Docker Compose services. Use --volumes to also remove |
| 108 | + persistent data (databases, caches). |
| 109 | +
|
| 110 | + Examples: |
| 111 | + # Stop services (preserves data) |
| 112 | + api-forge-cli dev down |
| 113 | +
|
| 114 | + # Stop and remove all data |
| 115 | + api-forge-cli dev down --volumes |
| 116 | + """ |
| 117 | + details = "This will stop all development Docker Compose services." |
| 118 | + extra_warning = None |
| 119 | + |
| 120 | + if volumes: |
| 121 | + extra_warning = ( |
| 122 | + "⚠️ --volumes flag is set: ALL DATA WILL BE PERMANENTLY DELETED!\n" |
| 123 | + " This includes databases, caches, and any persistent storage." |
| 124 | + ) |
| 125 | + |
| 126 | + if not confirm_action( |
| 127 | + action="Stop development environment", |
| 128 | + details=details, |
| 129 | + extra_warning=extra_warning, |
| 130 | + force=yes, |
| 131 | + ): |
| 132 | + console.print("[dim]Operation cancelled.[/dim]") |
| 133 | + raise typer.Exit(0) |
| 134 | + |
| 135 | + print_header("Stopping Development Environment", style="red") |
| 136 | + |
| 137 | + try: |
| 138 | + deployer = _get_deployer() |
| 139 | + deployer.teardown(volumes=volumes) |
| 140 | + except DeploymentError as e: |
| 141 | + handle_error(f"Teardown failed: {e.message}", e.details) |
| 142 | + |
| 143 | + |
| 144 | +@app.command() |
| 145 | +def status() -> None: |
| 146 | + """📊 Show status of development services. |
| 147 | +
|
| 148 | + Displays the current status of all development services including |
| 149 | + health check results and connection information. |
| 150 | +
|
| 151 | + Examples: |
| 152 | + api-forge-cli dev status |
| 153 | + """ |
| 154 | + deployer = _get_deployer() |
| 155 | + deployer.show_status() |
| 156 | + |
| 157 | + |
| 158 | +@app.command() |
| 159 | +def logs( |
| 160 | + service: str = typer.Argument( |
| 161 | + None, |
| 162 | + help="Service name (keycloak, postgres, redis, temporal). Shows all if omitted.", |
| 163 | + ), |
| 164 | + follow: bool = typer.Option( |
| 165 | + False, |
| 166 | + "--follow", |
| 167 | + "-f", |
| 168 | + help="Follow log output", |
| 169 | + ), |
| 170 | + tail: int = typer.Option( |
| 171 | + 100, |
| 172 | + "--tail", |
| 173 | + "-n", |
| 174 | + help="Number of lines to show from the end", |
| 175 | + ), |
| 176 | +) -> None: |
| 177 | + """📜 View logs from development services. |
| 178 | +
|
| 179 | + Shows logs from Docker Compose services. Specify a service name |
| 180 | + to view logs from a single service. |
| 181 | +
|
| 182 | + Examples: |
| 183 | + # View all logs |
| 184 | + api-forge-cli dev logs |
| 185 | +
|
| 186 | + # View PostgreSQL logs |
| 187 | + api-forge-cli dev logs postgres |
| 188 | +
|
| 189 | + # Follow Keycloak logs |
| 190 | + api-forge-cli dev logs keycloak --follow |
| 191 | + """ |
| 192 | + import subprocess |
| 193 | + |
| 194 | + compose_file = "docker-compose.dev.yml" |
| 195 | + cmd = ["docker", "compose", "-f", compose_file, "logs"] |
| 196 | + |
| 197 | + if tail: |
| 198 | + cmd.extend(["--tail", str(tail)]) |
| 199 | + |
| 200 | + if follow: |
| 201 | + cmd.append("--follow") |
| 202 | + |
| 203 | + if service: |
| 204 | + # Map friendly names to Docker Compose service names |
| 205 | + service_map = { |
| 206 | + "keycloak": "keycloak", |
| 207 | + "postgres": "postgres", |
| 208 | + "redis": "redis", |
| 209 | + "temporal": "temporal", |
| 210 | + "temporal-ui": "temporal-web", |
| 211 | + } |
| 212 | + compose_service = service_map.get(service.lower(), service) |
| 213 | + cmd.append(compose_service) |
| 214 | + |
| 215 | + try: |
| 216 | + subprocess.run(cmd, cwd=get_project_root(), check=True) |
| 217 | + except subprocess.CalledProcessError as e: |
| 218 | + handle_error(f"Failed to get logs: {e}") |
| 219 | + except KeyboardInterrupt: |
| 220 | + pass # User cancelled with Ctrl+C |
| 221 | + |
| 222 | + |
| 223 | +@app.command() |
| 224 | +def restart( |
| 225 | + service: str = typer.Argument( |
| 226 | + ..., |
| 227 | + help="Service to restart (keycloak, postgres, redis, temporal)", |
| 228 | + ), |
| 229 | +) -> None: |
| 230 | + """🔄 Restart a specific development service. |
| 231 | +
|
| 232 | + Restarts a single service without affecting other services. |
| 233 | +
|
| 234 | + Examples: |
| 235 | + # Restart PostgreSQL |
| 236 | + api-forge-cli dev restart postgres |
| 237 | +
|
| 238 | + # Restart Keycloak |
| 239 | + api-forge-cli dev restart keycloak |
| 240 | + """ |
| 241 | + import subprocess |
| 242 | + |
| 243 | + compose_file = "docker-compose.dev.yml" |
| 244 | + |
| 245 | + # Map friendly names to Docker Compose service names |
| 246 | + service_map = { |
| 247 | + "keycloak": "keycloak", |
| 248 | + "postgres": "postgres", |
| 249 | + "redis": "redis", |
| 250 | + "temporal": "temporal", |
| 251 | + "temporal-ui": "temporal-web", |
| 252 | + } |
| 253 | + |
| 254 | + compose_service = service_map.get(service.lower(), service) |
| 255 | + |
| 256 | + console.print(f"[bold]Restarting {service}...[/bold]") |
| 257 | + |
| 258 | + cmd = ["docker", "compose", "-f", compose_file, "restart", compose_service] |
| 259 | + |
| 260 | + try: |
| 261 | + subprocess.run(cmd, cwd=get_project_root(), check=True) |
| 262 | + console.print(f"[green]✅ {service} restarted successfully[/green]") |
| 263 | + except subprocess.CalledProcessError as e: |
| 264 | + handle_error(f"Failed to restart {service}: {e}") |
0 commit comments