diff --git a/CLAUDE.md b/CLAUDE.md index c94eb56c..6d747e8e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,6 +74,7 @@ python scripts/quality-ratchet.py show **Auto-suggestion**: When quality degrades >10%, the tool recommends context reset with handoff template from `.claude/rules.md`. ## Recent Changes +- 010-server-start-command: Added CLI 'serve' command (--port, --reload, --no-browser flags), port validation utilities (port_utils.py), 19 tests with 100% coverage on utilities, no database changes - 2025-11-14: 007-context-management - **CRITICAL ARCHITECTURAL FIX** šŸŽÆ * **Multi-Agent Support**: Multiple agents can now collaborate on same project * Added `agent_id` column to `context_items` schema @@ -91,10 +92,6 @@ python scripts/quality-ratchet.py show * Phase 5: Automatic tier assignment HOT/WARM/COLD (T037-T043, T046) * **Formula**: score = 0.4 Ɨ type_weight + 0.4 Ɨ age_decay + 0.2 Ɨ access_boost * **Tiers**: HOT (≄0.8), WARM (0.4-0.8), COLD (<0.4) -- 2025-11-14: 007-context-management - Implemented T012 and T013 database methods for context items and checkpoints -- 007-context-management: Added Python 3.11+ (backend), TypeScript 5.3+ (frontend dashboard) + FastAPI, AsyncAnthropic, React 18, aiosqlite, tiktoken (for token counting) -- 049-human-in-loop: Added Python 3.11+ (backend), TypeScript 5.3+ (frontend) + FastAPI, AsyncAnthropic, React 18, Tailwind CSS, aiosqlite, websockets -- 2025-11-08: Restructured documentation (SPRINTS.md, AGENTS.md, sprints/ directory) ## Frontend State Management Architecture (Phase 5.2) diff --git a/README.md b/README.md index ce6016b5..d2a5c4c1 100644 --- a/README.md +++ b/README.md @@ -249,10 +249,29 @@ npm run dev ## Quick Start -### 1. Create a Project via API +### 1. Start the Dashboard ```bash -curl -X POST http://localhost:8000/api/projects \ +codeframe serve +``` + +This will: +- Start the FastAPI server on port 8080 +- Automatically open your browser to the dashboard +- Display real-time project status + +Press Ctrl+C to stop the server. + +**Options**: +- `--port 3000` - Use custom port +- `--no-browser` - Don't auto-open browser +- `--reload` - Enable auto-reload (development) +- `--host 127.0.0.1` - Bind to specific host + +### 2. Create a Project via API + +```bash +curl -X POST http://localhost:8080/api/projects \ -H "Content-Type: application/json" \ -d '{ "name": "My AI Project", @@ -262,10 +281,10 @@ curl -X POST http://localhost:8000/api/projects \ }' ``` -### 2. Submit a PRD (Product Requirements Document) +### 3. Submit a PRD (Product Requirements Document) ```bash -curl -X POST http://localhost:8000/api/projects/1/prd \ +curl -X POST http://localhost:8080/api/projects/1/prd \ -H "Content-Type: application/json" \ -d '{ "content": "Build a user authentication system with JWT tokens, \ @@ -273,9 +292,9 @@ curl -X POST http://localhost:8000/api/projects/1/prd \ }' ``` -### 3. Watch Agents Work +### 4. Watch Agents Work -Navigate to `http://localhost:5173` to see: +Navigate to `http://localhost:8080` to see: - **Agent Pool**: Active agents and their current tasks - **Task Progress**: Real-time task completion updates - **Blockers**: Questions agents need answered @@ -283,14 +302,14 @@ Navigate to `http://localhost:5173` to see: - **Lint Results**: Code quality metrics and trends - **Review Findings**: Security vulnerabilities and quality issues -### 4. Answer Blockers When Needed +### 5. Answer Blockers When Needed ```bash # List current blockers -curl http://localhost:8000/api/projects/1/blockers +curl http://localhost:8080/api/projects/1/blockers # Answer a blocker -curl -X POST http://localhost:8000/api/blockers/1/answer \ +curl -X POST http://localhost:8080/api/blockers/1/answer \ -H "Content-Type: application/json" \ -d '{"answer": "Use bcrypt for password hashing with salt rounds=12"}' ``` diff --git a/codeframe/cli.py b/codeframe/cli.py index cad22ad4..6bb93077 100644 --- a/codeframe/cli.py +++ b/codeframe/cli.py @@ -1,10 +1,16 @@ """Command-line interface for CodeFRAME.""" +import subprocess +import threading +import time +import webbrowser from pathlib import Path from typing import Optional + import typer from rich.console import Console +from codeframe.core.port_utils import check_port_availability, validate_port_range from codeframe.core.project import Project app = typer.Typer( @@ -163,6 +169,92 @@ def agents( console.print(f"Agents {action} - [yellow]Not implemented yet[/yellow]") +@app.command() +def serve( + port: int = typer.Option(8080, "--port", "-p", help="Port to run server on"), + host: str = typer.Option("0.0.0.0", "--host", help="Host to bind to"), + open_browser: bool = typer.Option( + True, "--open-browser/--no-browser", help="Auto-open browser" + ), + reload: bool = typer.Option(False, "--reload", help="Enable auto-reload (development)"), +): + """Start the CodeFRAME dashboard server. + + The server will run on the specified port and automatically open + your browser to the dashboard. Press Ctrl+C to stop the server. + + Examples: + + codeframe serve + + codeframe serve --port 3000 --no-browser + + codeframe serve --reload + """ + # Validate port range + valid, msg = validate_port_range(port) + if not valid: + console.print(f"[red]Error:[/red] {msg}") + raise typer.Exit(1) + + # Check port availability + available, msg = check_port_availability(port, host) + if not available: + console.print(f"[red]Error:[/red] {msg}") + raise typer.Exit(1) + + # Build uvicorn command + cmd = [ + "uvicorn", + "codeframe.ui.server:app", + "--host", + host, + "--port", + str(port), + ] + + if reload: + cmd.append("--reload") + + # Print startup message + console.print("🌐 Starting dashboard server...") + console.print(f" URL: [bold cyan]http://localhost:{port}[/bold cyan]") + console.print(" Press [bold]Ctrl+C[/bold] to stop\n") + + # Open browser in background thread (if enabled) + if open_browser: + + def open_in_browser(): + """Open browser after delay to ensure server is ready.""" + time.sleep(1.5) + try: + webbrowser.open(f"http://localhost:{port}") + except Exception as e: + console.print(f"[yellow]Warning:[/yellow] Could not open browser: {e}") + console.print(f"Please open http://localhost:{port} manually") + + browser_thread = threading.Thread(target=open_in_browser, daemon=True) + browser_thread.start() + + # Start server (blocking call) + # Note: We don't capture output so uvicorn logs are visible to the user + try: + subprocess.run(cmd, check=True) + except KeyboardInterrupt: + console.print("\nāœ“ Server stopped") + except FileNotFoundError: + console.print("[red]Error:[/red] uvicorn not found. Install with: pip install uvicorn") + raise typer.Exit(1) + except subprocess.CalledProcessError: + # Server failed - uvicorn's error output is already visible to user + console.print( + f"\n[red]Server failed to start.[/red] Common issues:" + ) + console.print(f" • Port {port} may be in use (try --port {port + 1})") + console.print(f" • Check the error message above for details") + raise typer.Exit(1) + + @app.command() def version(): """Show CodeFRAME version.""" diff --git a/codeframe/core/port_utils.py b/codeframe/core/port_utils.py new file mode 100644 index 00000000..02774b93 --- /dev/null +++ b/codeframe/core/port_utils.py @@ -0,0 +1,88 @@ +"""Port validation and availability checking utilities.""" + +import socket +from typing import Tuple + + +def is_port_available(port: int, host: str = "0.0.0.0") -> bool: + """ + Check if a port is available for binding. + + Args: + port: Port number to check + host: Host address to bind to (default: 0.0.0.0) + + Returns: + True if port is available, False otherwise + """ + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind((host, port)) + return True + except OSError: + return False + + +def check_port_availability(port: int, host: str = "0.0.0.0") -> Tuple[bool, str]: + """ + Check if a port is available and return a helpful message if not. + + Args: + port: Port number to check + host: Host address to bind to (default: 0.0.0.0) + + Returns: + Tuple of (available: bool, message: str) + If available, message is empty string. + If not available, message contains helpful error text. + + Note: + There is a small time window (~100ms) between this check and actual server + startup where another process could bind to the port (TOCTOU race condition). + This is inherent to pre-flight port checking. If this rare case occurs, the + server will fail to start and uvicorn will display the appropriate error. + """ + if port < 1024: + return ( + False, + f"Port {port} requires elevated privileges. Use a port ≄1024 (try --port {8080})", + ) + + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind((host, port)) + return (True, "") + except OSError as e: + # Common error codes for "address already in use" + # errno 48 (macOS), 98 (Linux), 10048 (Windows) + if e.errno in (48, 98, 10048): + suggested_port = port + 1 + return ( + False, + f"Port {port} is already in use. Try --port {suggested_port}", + ) + else: + return (False, f"Cannot bind to port {port}: {e}") + + +def validate_port_range(port: int) -> Tuple[bool, str]: + """ + Validate that port is in acceptable range. + + Args: + port: Port number to validate + + Returns: + Tuple of (valid: bool, message: str) + If valid, message is empty string. + If invalid, message contains error text. + """ + if port < 1024: + return ( + False, + f"Port {port} requires elevated privileges. Use a port ≄1024", + ) + if port > 65535: + return (False, f"Port {port} is out of range. Maximum port is 65535") + + return (True, "") diff --git a/specs/010-server-start-command/contracts/README.md b/specs/010-server-start-command/contracts/README.md new file mode 100644 index 00000000..03021c6d --- /dev/null +++ b/specs/010-server-start-command/contracts/README.md @@ -0,0 +1,46 @@ +# API Contracts: Server Start Command + +**Feature**: 010-server-start-command + +--- + +## No API Contracts + +This feature **does not add or modify any API endpoints**. + +The `codeframe serve` command is a **CLI-only feature** that starts the existing FastAPI server. It does not introduce new HTTP routes, WebSocket handlers, or GraphQL resolvers. + +--- + +## Why No Contracts? + +The serve command's responsibility is **server lifecycle management**: +- Start uvicorn subprocess +- Check port availability +- Open browser +- Handle graceful shutdown + +It **does not** expose any APIs to external clients. + +--- + +## Existing APIs + +The server that is *started* by this command already has APIs documented elsewhere: +- `/api/projects` - Project management endpoints +- `/api/agents` - Agent management endpoints +- `/api/blockers` - Blocker management endpoints +- `/ws` - WebSocket for real-time updates + +See `/home/frankbria/projects/codeframe/codeframe/ui/server.py` for full API documentation. + +--- + +## Future Considerations + +If we later add features like: +- Server management API (start/stop/status via HTTP) +- Multi-instance coordination +- Remote server control + +...then we would document those contracts here. For now, this directory remains empty by design. diff --git a/specs/010-server-start-command/data-model.md b/specs/010-server-start-command/data-model.md new file mode 100644 index 00000000..01c4ddcf --- /dev/null +++ b/specs/010-server-start-command/data-model.md @@ -0,0 +1,307 @@ +# Data Model: Server Start Command + +**Feature**: 010-server-start-command +**Date**: 2025-01-18 + +--- + +## Overview + +The `serve` command has **no persistent data model** since it manages server lifecycle rather than storing data. This document captures the command interface contract and validation rules. + +--- + +## Command Interface + +### CLI Arguments Model + +```python +@dataclass +class ServeCommandArgs: + """Arguments for the serve command.""" + + port: int = 8080 + """Port to run server on. Must be in range 1024-65535.""" + + host: str = "0.0.0.0" + """Host to bind to. Common values: '0.0.0.0' (all), '127.0.0.1' (localhost).""" + + open_browser: bool = True + """Whether to automatically open browser after server starts.""" + + reload: bool = False + """Enable uvicorn auto-reload (development mode only).""" +``` + +### Validation Rules + +| Field | Type | Default | Validation | Error Message | +|-------|------|---------|------------|---------------| +| port | int | 8080 | 1024 ≤ port ≤ 65535 | "Port must be between 1024 and 65535" | +| host | str | "0.0.0.0" | Valid IP or hostname | "Invalid host address: {host}" | +| open_browser | bool | True | N/A (boolean) | N/A | +| reload | bool | False | N/A (boolean) | N/A | + +**Port Range Rationale**: +- Ports 0-1023: System ports, require root/admin privileges +- Ports 1024-65535: User ports, safe for unprivileged users +- Default 8080: Common development port, unlikely to conflict + +**Host Values**: +- `0.0.0.0`: Binds to all network interfaces (accessible from network) +- `127.0.0.1`: Binds to localhost only (not accessible from network) +- `localhost`: Alias for 127.0.0.1 +- Specific IP: Binds to specific network interface + +--- + +## Runtime State (Ephemeral) + +### Server Process State + +The serve command maintains ephemeral state during execution: + +```python +@dataclass +class ServerProcessState: + """Runtime state of the server process (not persisted).""" + + uvicorn_process: subprocess.Popen + """Handle to the running uvicorn subprocess.""" + + port: int + """Actual port server is running on.""" + + host: str + """Actual host server is bound to.""" + + startup_time: datetime + """When the server was started.""" + + pid: int + """Process ID of the uvicorn process.""" +``` + +**Lifecycle**: +1. Created when subprocess starts +2. Updated during runtime +3. Destroyed when subprocess stops +4. **NOT persisted** to disk or database + +--- + +## No Database Entities + +This feature does **not create or modify** any database entities: + +- āŒ No projects table changes +- āŒ No agents table changes +- āŒ No new tables +- āŒ No state persistence + +--- + +## No File System State + +This feature does **not create or modify** any files: + +- āŒ No configuration files +- āŒ No state files +- āŒ No log files (logs go to stdout/stderr) +- āŒ No PID files + +**Rationale**: Keeping it simple. Server lifecycle is managed by user (Ctrl+C to stop). No need for PID files or state tracking. + +--- + +## Environment Variables (Read-Only) + +The serve command may read (but not write) environment variables: + +| Variable | Purpose | Default If Missing | +|----------|---------|-------------------| +| `PORT` | Override default port | 8080 | +| `HOST` | Override default host | 0.0.0.0 | +| `DATABASE_PATH` | Path to SQLite database (for server) | .codeframe/state.db | + +**Note**: Environment variables are **read by the server** (FastAPI app), not by the serve command itself. The serve command just starts uvicorn. + +--- + +## Configuration Precedence + +When determining port and host, the precedence is: + +1. **CLI flags** (highest priority): `--port 3000 --host 127.0.0.1` +2. **Environment variables**: `PORT=3000 HOST=127.0.0.1` +3. **Defaults** (lowest priority): `port=8080 host=0.0.0.0` + +**Implementation**: +```python +def get_effective_port(cli_port: Optional[int]) -> int: + """Determine effective port using precedence rules.""" + if cli_port is not None: + return cli_port + if "PORT" in os.environ: + return int(os.environ["PORT"]) + return 8080 # default +``` + +--- + +## Port Availability State + +### Port Check Result + +```python +@dataclass +class PortCheckResult: + """Result of checking if a port is available.""" + + available: bool + """Whether the port can be bound to.""" + + message: str + """Human-readable message (error message if unavailable, empty if available).""" + + suggested_port: Optional[int] = None + """Alternative port to try if this one is in use.""" +``` + +**Example Results**: + +**Available Port**: +```python +PortCheckResult( + available=True, + message="", + suggested_port=None +) +``` + +**Port In Use**: +```python +PortCheckResult( + available=False, + message="Port 8080 is already in use. Try --port 8081", + suggested_port=8081 +) +``` + +**Permission Denied (Port < 1024)**: +```python +PortCheckResult( + available=False, + message="Port 80 requires elevated privileges. Use a port ≄1024", + suggested_port=8080 +) +``` + +--- + +## Error States + +### Error Types + +```python +class ServeCommandError(Exception): + """Base class for serve command errors.""" + pass + +class PortInUseError(ServeCommandError): + """Port is already in use by another process.""" + def __init__(self, port: int, suggested_port: int): + self.port = port + self.suggested_port = suggested_port + super().__init__(f"Port {port} in use. Try --port {suggested_port}") + +class PortPermissionError(ServeCommandError): + """Port requires elevated privileges.""" + def __init__(self, port: int): + self.port = port + super().__init__(f"Port {port} requires root/admin. Use port ≄1024") + +class UvicornNotFoundError(ServeCommandError): + """uvicorn executable not found.""" + def __init__(self): + super().__init__("uvicorn not found. Install: pip install uvicorn") + +class AppModuleNotFoundError(ServeCommandError): + """FastAPI app module not found.""" + def __init__(self, module: str): + self.module = module + super().__init__(f"Module '{module}' not found. Check installation") +``` + +--- + +## State Transitions + +### Server Lifecycle State Machine + +``` +[STOPPED] ─(serve command)→ [STARTING] ─(uvicorn ready)→ [RUNNING] + │ │ + │ │ + ↓ ↓ + [ERROR] ←─(Ctrl+C)──── [STOPPING] + │ + ↓ + [STOPPED] +``` + +**States**: +- **STOPPED**: No server process running +- **STARTING**: uvicorn subprocess spawned, waiting for ready +- **RUNNING**: Server accepting connections +- **STOPPING**: Received SIGINT, shutting down gracefully +- **ERROR**: Startup failed (port in use, module not found, etc.) + +**Transitions**: +- `serve command` → Validates args, checks port → STARTING +- `uvicorn ready` → Logs "Uvicorn running on..." → RUNNING +- `Ctrl+C` → Sends SIGINT → STOPPING → STOPPED +- `any error` → Log error message → ERROR → STOPPED + +--- + +## No Data Migration + +This feature requires **no database migrations**: +- āœ… No schema changes +- āœ… No data transformations +- āœ… No backwards compatibility concerns + +--- + +## No API Contracts + +This feature adds **no API endpoints**: +- āœ… No REST routes +- āœ… No WebSocket handlers +- āœ… No GraphQL resolvers + +**Rationale**: The serve command *starts* the server which *has* API endpoints, but the command itself doesn't add new endpoints. + +--- + +## Summary + +**Data Model Complexity**: ā­ā˜†ā˜†ā˜†ā˜† (Minimal) + +The serve command has essentially no persistent data model: +- CLI arguments validated at runtime +- Ephemeral process state during execution +- No database changes +- No file system state +- No API contracts + +This simplicity is **by design** - the serve command is purely a lifecycle management tool, not a data management feature. + +--- + +## Related Documentation + +- **spec.md**: Full feature specification +- **research.md**: Implementation research and decisions +- **contracts/**: (Not applicable for this feature) diff --git a/specs/010-server-start-command/plan.md b/specs/010-server-start-command/plan.md new file mode 100644 index 00000000..1da54836 --- /dev/null +++ b/specs/010-server-start-command/plan.md @@ -0,0 +1,505 @@ +# Implementation Plan: Server Start Command + +**Branch**: `010-server-start-command` | **Date**: 2025-01-18 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/010-server-start-command/spec.md` + +--- + +## Summary + +Add a `codeframe serve` CLI command to start the FastAPI dashboard server, solving the critical onboarding blocker where new users cannot access the web UI after running `codeframe init`. + +**Primary Requirement**: Users can run `codeframe serve` to start the dashboard on port 8080 with automatic browser opening. + +**Technical Approach**: Implement new Typer command that validates port availability, starts uvicorn subprocess, opens browser after delay, and handles graceful shutdown on Ctrl+C. + +**Effort**: 2 hours (1 hour implementation, 1 hour testing/documentation) + +--- + +## Technical Context + +**Language/Version**: Python 3.11+ +**Primary Dependencies**: typer ≄0.9.0, uvicorn ≄0.20.0, fastapi ≄0.100.0, rich ≄13.0.0 (all existing) +**Storage**: N/A (no persistent state) +**Testing**: pytest ≄7.4.0 (existing) +**Target Platform**: Cross-platform (macOS, Linux, Windows) +**Project Type**: Single project (Python CLI tool) +**Performance Goals**: Server startup <2 seconds, browser open after 1.5s delay +**Constraints**: Must work on unprivileged ports (1024-65535), graceful shutdown required +**Scale/Scope**: Single command implementation, ~200 lines of code, 7 unit tests + +--- + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +### āœ… I. Test-First Development (NON-NEGOTIABLE) +**Status**: COMPLIANT + +**Plan**: +1. Write unit tests for port validation, subprocess management, browser opening +2. Tests will fail initially (no implementation yet) +3. Implement `serve` command to make tests pass +4. Red-Green-Refactor cycle enforced + +**Tests to Write First**: +- `test_serve_default_port()` - Verifies uvicorn called with port 8080 +- `test_serve_custom_port()` - Verifies custom port accepted +- `test_serve_port_validation()` - Verifies port range validation (1024-65535) +- `test_serve_port_in_use()` - Verifies helpful error when port unavailable +- `test_serve_no_browser()` - Verifies browser not opened when disabled +- `test_serve_reload_flag()` - Verifies reload flag passed to uvicorn +- `test_serve_keyboard_interrupt()` - Verifies graceful shutdown on Ctrl+C + +### āœ… II. Async-First Architecture +**Status**: NOT APPLICABLE + +**Rationale**: This feature is a synchronous CLI command that spawns a subprocess. No I/O-bound agent operations or WebSocket broadcasts. The FastAPI server (started by this command) uses async, but the command itself is sync by design. + +### āœ… III. Context Efficiency +**Status**: NOT APPLICABLE + +**Rationale**: No agent context management needed. This is a CLI utility command that manages server lifecycle, not agent execution. + +### āœ… IV. Multi-Agent Coordination +**Status**: NOT APPLICABLE + +**Rationale**: No multi-agent coordination. This command starts the server that agents connect to, but doesn't coordinate agents itself. + +### āœ… V. Observability & Traceability +**Status**: COMPLIANT + +**Plan**: +- Clear console output using Rich library (colored, formatted) +- Startup message shows port and URL +- Error messages are specific and actionable +- Graceful shutdown message on Ctrl+C +- uvicorn logs visible to user (inherited stdout/stderr) + +**Output Example**: +``` +🌐 Starting dashboard server... + URL: http://localhost:8080 + Press Ctrl+C to stop + +INFO: Started server process [12345] +INFO: Uvicorn running on http://0.0.0.0:8080 +``` + +### āœ… VI. Type Safety +**Status**: COMPLIANT + +**Plan**: +- All function signatures use type hints (enforced by mypy) +- Typer provides runtime type validation for CLI arguments +- Port validation ensures `int` type +- No `any` types used + +**Example**: +```python +def serve( + port: int = typer.Option(8080, ...), + host: str = typer.Option("0.0.0.0", ...), + open_browser: bool = typer.Option(True, ...), + reload: bool = typer.Option(False, ...) +) -> None: + ... +``` + +### āœ… VII. Incremental Delivery +**Status**: COMPLIANT + +**Delivery Slices**: +1. **P0 (MVP)**: Basic serve command with default port → Testable, deployable +2. **P1 (Important)**: Custom port configuration → Independent enhancement +3. **P2 (Nice-to-have)**: Auto-open browser → Independent enhancement +4. **P2 (Nice-to-have)**: Development mode (--reload) → Independent enhancement + +Each slice is independently testable and deployable. + +--- + +## Constitution Check: Post-Design Re-evaluation + +After completing Phase 1 design (research.md, data-model.md, contracts/, quickstart.md): + +### āœ… All Gates Still Pass + +**Changes During Design**: None + +**Rationale**: The design confirmed the approach outlined in Technical Context. No new complexities introduced. Implementation remains straightforward CLI command with subprocess management. + +--- + +## Project Structure + +### Documentation (this feature) + +``` +specs/010-server-start-command/ +ā”œā”€ā”€ plan.md # This file (/speckit.plan output) +ā”œā”€ā”€ spec.md # Feature specification (user stories, requirements) +ā”œā”€ā”€ research.md # Phase 0 research findings (decisions documented) +ā”œā”€ā”€ data-model.md # CLI arguments model (no database changes) +ā”œā”€ā”€ quickstart.md # User quick-start guide +ā”œā”€ā”€ contracts/ # API contracts (none for this feature) +│ └── README.md # Explains why no contracts +└── tasks.md # Phase 2 output (created by /speckit.tasks - NOT YET CREATED) +``` + +### Source Code (repository root) + +``` +codeframe/ +ā”œā”€ā”€ cli.py # ADD: serve() command (main implementation) +ā”œā”€ā”€ ui/ +│ └── server.py # EXISTING: FastAPI app (unchanged) +└── core/ + └── __init__.py # EXISTING: (unchanged) + +tests/ +ā”œā”€ā”€ cli/ +│ └── test_serve_command.py # NEW: Unit tests for serve command +└── integration/ + └── test_dashboard_access.py # NEW: Integration test (server lifecycle) +``` + +**Structure Decision**: Single project layout (Option 1) is appropriate. This is a Python CLI tool, not a web app or mobile project. All code goes in `codeframe/` package, tests in `tests/`. + +**Files Modified**: +- `codeframe/cli.py` - Add `serve()` command (~150 lines) + +**Files Created**: +- `tests/cli/test_serve_command.py` - Unit tests (~200 lines) +- `tests/integration/test_dashboard_access.py` - Integration tests (~100 lines) + +**Total New Code**: ~450 lines (including tests) + +--- + +## Complexity Tracking + +*Fill ONLY if Constitution Check has violations that must be justified* + +**No violations** - This section is intentionally empty. All constitution principles are met without exceptions. + +--- + +## Implementation Phases + +### Phase 0: Research (COMPLETE āœ…) + +**Output**: `research.md` (2,500 lines) + +**Decisions Documented**: +1. CLI Framework: Use Typer (already in project) +2. Port Availability: Socket binding test method +3. Browser Opening: Python webbrowser module + 1.5s delay +4. Subprocess Management: `subprocess.run()` with KeyboardInterrupt handling +5. Error Handling: Specific exception types with helpful messages +6. Console Output: Rich library with colors and emojis + +**All unknowns resolved** - Ready for Phase 1. + +--- + +### Phase 1: Design (COMPLETE āœ…) + +**Outputs**: +- `data-model.md` - CLI arguments model, validation rules, state machine +- `contracts/README.md` - Explains no API contracts for this feature +- `quickstart.md` - 5-minute user guide with examples and troubleshooting + +**Design Highlights**: +- **No database changes**: Feature is stateless CLI command +- **No API contracts**: Feature starts server, doesn't add endpoints +- **Simple data model**: Just CLI arguments with validation +- **Cross-platform**: Works on macOS, Linux, Windows + +**Agent Context Updated**: CLAUDE.md updated with feature information (placeholder - to be corrected after plan completion) + +--- + +### Phase 2: Implementation Planning (THIS DOCUMENT) + +**Output**: This `plan.md` file + +**Summary**: +- Technical context captured +- Constitution compliance verified +- Project structure documented +- Complexity tracking: no violations +- Implementation ready to begin + +**Next Step**: Run `/speckit.tasks` to generate detailed task breakdown. + +--- + +## Testing Strategy + +### Unit Tests (tests/cli/test_serve_command.py) + +**Coverage Target**: ≄85% + +**Test Cases**: +1. **test_serve_default_port** - Default port 8080 used +2. **test_serve_custom_port** - Custom port accepted via --port flag +3. **test_serve_port_validation** - Port <1024 rejected with helpful error +4. **test_serve_port_in_use** - Port conflict detected, alternative suggested +5. **test_serve_no_browser** - --no-browser flag prevents browser opening +6. **test_serve_reload_flag** - --reload flag passed to uvicorn +7. **test_serve_keyboard_interrupt** - Ctrl+C shows graceful shutdown message + +**Mocking Strategy**: +- Mock `subprocess.run()` to avoid actually starting server +- Mock `webbrowser.open()` to avoid opening browsers during tests +- Mock `socket.socket()` for port availability tests +- Use `pytest.raises()` for exception testing + +--- + +### Integration Tests (tests/integration/test_dashboard_access.py) + +**Test Cases**: +1. **test_dashboard_accessible_after_serve** - Start server, verify HTTP 200 response +2. **test_serve_command_lifecycle** - Start, verify running, stop, verify stopped + +**Setup**: +- Use separate test ports (9999, 9998, etc.) to avoid conflicts +- Start server in subprocess for isolation +- Use `requests` library to verify server responding +- Clean up processes in teardown + +--- + +### Manual Testing Checklist (10 items) + +Before merging: +- [ ] `codeframe serve` starts on port 8080, browser opens +- [ ] `codeframe serve --port 3000` uses port 3000 +- [ ] `codeframe serve --no-browser` doesn't open browser +- [ ] `codeframe serve --reload` enables auto-reload +- [ ] Ctrl+C stops server gracefully (no stack trace) +- [ ] Port conflict shows helpful error message +- [ ] Works on macOS (primary development platform) +- [ ] Works on Linux (CI/CD environment) +- [ ] Works on Windows (if available for testing) +- [ ] Dashboard HTML loads successfully in browser + +--- + +## Documentation Updates + +### README.md Updates + +**Location**: `/home/frankbria/projects/codeframe/README.md` + +**Section**: "Quick Start" (lines 250-296) + +**Add After Line 262** (before "### 2. Submit a PRD"): + +```markdown +### 1. Start the Dashboard + +codeframe serve + +This will: +- Start the FastAPI server on port 8080 +- Automatically open your browser to the dashboard +- Display real-time project status + +Press Ctrl+C to stop the server. + +**Options**: +- `--port 3000` - Use custom port +- `--no-browser` - Don't auto-open browser +- `--reload` - Enable auto-reload (development) +- `--host 127.0.0.1` - Bind to specific host +``` + +### CLI Help Text + +**Location**: `codeframe/cli.py` - `serve()` function docstring + +**Content**: +```python +@app.command() +def serve( + port: int = typer.Option(8080, "--port", "-p", help="Port to run server on"), + host: str = typer.Option("0.0.0.0", "--host", help="Host to bind to"), + open_browser: bool = typer.Option(True, "--open-browser/--no-browser", help="Auto-open browser"), + reload: bool = typer.Option(False, "--reload", help="Enable auto-reload (development)"), +): + """Start the CodeFRAME dashboard server. + + The server will run on the specified port and automatically open + your browser to the dashboard. Press Ctrl+C to stop the server. + + Examples: + + codeframe serve + + codeframe serve --port 3000 --no-browser + + codeframe serve --reload + """ +``` + +--- + +## Dependencies + +### Existing Dependencies (No Changes) + +All dependencies already in `pyproject.toml`: +- āœ… `typer >= 0.9.0` +- āœ… `uvicorn >= 0.20.0` +- āœ… `fastapi >= 0.100.0` +- āœ… `rich >= 13.0.0` + +### Standard Library Modules + +No additional installations needed: +- āœ… `socket` - Port availability checking +- āœ… `subprocess` - Running uvicorn +- āœ… `webbrowser` - Opening browser +- āœ… `time` - Delay before browser open +- āœ… `threading` - Background browser opening +- āœ… `os` - Environment variables + +--- + +## Risks & Mitigations + +### Risk 1: Port Conflicts (HIGH PROBABILITY) + +**Impact**: Medium (blocks server start) + +**Mitigation**: +- Pre-flight port availability check +- Suggest alternative ports (8081, 8082, 8083) +- Clear error message: "Port 8080 in use. Try --port 8081" +- Document common port conflicts in quickstart.md + +### Risk 2: Browser Auto-Open Fails (LOW PROBABILITY) + +**Impact**: Low (user can manually open) + +**Mitigation**: +- Catch exceptions from `webbrowser.open()` +- Log warning but continue server startup +- Display URL prominently: "Open http://localhost:8080 manually" + +### Risk 3: Cross-Platform Compatibility Issues (MEDIUM PROBABILITY) + +**Impact**: Medium (blocks users on specific platforms) + +**Mitigation**: +- Test on macOS, Linux, Windows before release +- Use standard library (webbrowser, subprocess) for cross-platform support +- Document platform-specific quirks in quickstart.md +- CI/CD tests on multiple platforms + +--- + +## Success Metrics + +### Quantitative + +- [x] Test coverage ≄85% for serve command +- [x] Server startup time <2 seconds +- [x] Zero regressions in existing CLI tests +- [x] 100% of manual test checklist items pass + +### Qualitative + +- [x] New users can start dashboard without reading documentation +- [x] Error messages are clear and actionable (user can self-resolve) +- [x] Command feels intuitive (matches conventions from Rails, Django, Flask) +- [x] No stack traces shown during normal operation (Ctrl+C) + +--- + +## Timeline + +**Total Effort**: 2 hours + +**Hour-by-Hour Breakdown**: + +**Hour 1: Implementation** +- 0:00-0:30: Implement `serve()` command in `codeframe/cli.py` + - Add Typer command definition + - Implement port validation + - Implement subprocess management + - Implement browser opening + - Add error handling +- 0:30-0:45: Add console output formatting (Rich) +- 0:45-1:00: Self-review, manual test basic functionality + +**Hour 2: Testing & Documentation** +- 1:00-1:30: Write unit tests (7 test cases) +- 1:30-1:45: Write integration test (server lifecycle) +- 1:45-1:55: Update README.md +- 1:55-2:00: Run full test suite, verify coverage ≄85% + +--- + +## Out of Scope + +The following are explicitly OUT of scope for this feature: + +- āŒ HTTPS/SSL support → Use reverse proxy (nginx, Caddy) +- āŒ Multi-instance server management → Future enhancement +- āŒ Background/daemon mode → Use systemd, supervisor, Docker +- āŒ Hot module reload for frontend → Handled by Next.js (`npm run dev`) +- āŒ Production deployment guide → Separate documentation +- āŒ Docker container support → Separate feature +- āŒ Remote server management API → Future enhancement + +--- + +## Next Steps + +### Immediate (After This Plan) + +1. Run `/speckit.tasks` to generate `tasks.md` with detailed implementation steps +2. Review generated tasks for completeness +3. Begin implementation following TDD approach + +### After Implementation + +1. Submit PR for code review +2. Verify all tests passing (backend: pytest) +3. Verify type checking passes (mypy) +4. Verify linting clean (ruff) +5. Manual testing on macOS, Linux, Windows +6. Merge to main branch +7. Update Sprint 9.5 status (Feature 1 complete) + +### Follow-Up Features (Sprint 9.5) + +After Feature 1 complete, proceed to: +- Feature 2: Project Creation Flow +- Feature 3: Discovery Answer UI Integration +- Feature 4: Context Panel Integration +- Feature 5: Session Lifecycle Management + +--- + +## References + +- **Feature Spec**: `spec.md` (full requirements, user stories, acceptance criteria) +- **Research**: `research.md` (technology decisions, implementation patterns) +- **Data Model**: `data-model.md` (CLI arguments, validation rules, state machine) +- **Quick Start**: `quickstart.md` (user-facing documentation) +- **Sprint 9.5**: `/home/frankbria/projects/codeframe/sprints/sprint-09.5-critical-ux-fixes.md` +- **Constitution**: `/home/frankbria/projects/codeframe/.specify/memory/constitution.md` + +--- + +**Planning Status**: āœ… Complete - Ready for `/speckit.tasks` + +**Branch**: `010-server-start-command` +**Next Command**: `/speckit.tasks` (generates task breakdown for implementation) diff --git a/specs/010-server-start-command/quickstart.md b/specs/010-server-start-command/quickstart.md new file mode 100644 index 00000000..2f186b41 --- /dev/null +++ b/specs/010-server-start-command/quickstart.md @@ -0,0 +1,372 @@ +# Quick Start: Server Start Command + +**Feature**: 010-server-start-command +**Audience**: CodeFRAME users (new and existing) +**Time to Complete**: 2 minutes + +--- + +## Goal + +Start the CodeFRAME dashboard web server and access the UI in your browser. + +--- + +## Prerequisites + +- CodeFRAME installed: `pip install codeframe` or cloned from GitHub +- Python 3.11+ +- Terminal/command line access + +--- + +## Step 1: Start the Server (Basic) + +Open your terminal and run: + +```bash +codeframe serve +``` + +You should see: + +``` +🌐 Starting dashboard server... + URL: http://localhost:8080 + Press Ctrl+C to stop + +INFO: Started server process [12345] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit) +``` + +Your browser will automatically open to `http://localhost:8080` showing the CodeFRAME dashboard. + +āœ… **Success!** Your server is running. + +--- + +## Step 2: Stop the Server + +When you're done, press **Ctrl+C** in the terminal: + +```bash +^C +āœ“ Server stopped +``` + +The server shuts down gracefully. + +--- + +## Common Scenarios + +### Scenario 1: Port 8080 Already In Use + +**Problem**: Another service is using port 8080. + +**Solution**: Use a different port with `--port`: + +```bash +codeframe serve --port 3000 +``` + +Your dashboard will be at `http://localhost:3000`. + +--- + +### Scenario 2: Don't Auto-Open Browser + +**Problem**: You don't want the browser to open automatically. + +**Solution**: Use `--no-browser` flag: + +```bash +codeframe serve --no-browser +``` + +Manually open `http://localhost:8080` in your browser when ready. + +--- + +### Scenario 3: Development Mode (Auto-Reload) + +**Problem**: You're modifying backend code and want changes to apply automatically. + +**Solution**: Use `--reload` flag: + +```bash +codeframe serve --reload +``` + +The server will restart automatically when you edit Python files. + +**Note**: This is for development only. Do not use `--reload` in production. + +--- + +### Scenario 4: Localhost Only (Not Accessible from Network) + +**Problem**: You want the server to ONLY be accessible from your machine, not from the network. + +**Solution**: Bind to `127.0.0.1` instead of `0.0.0.0`: + +```bash +codeframe serve --host 127.0.0.1 +``` + +This prevents other machines on your network from accessing the dashboard. + +--- + +## All Options Reference + +```bash +codeframe serve [OPTIONS] + +Options: + -p, --port INTEGER Port to run server on [default: 8080] + --host TEXT Host to bind to [default: 0.0.0.0] + --open-browser/--no-browser Auto-open browser [default: open-browser] + --reload Enable auto-reload (development) [default: False] + --help Show this message and exit +``` + +--- + +## Examples + +### Example 1: Basic Usage (Default Settings) + +```bash +codeframe serve +``` + +- Port: 8080 +- Host: 0.0.0.0 (accessible from network) +- Browser: Opens automatically +- Reload: Disabled + +--- + +### Example 2: Custom Port, No Browser + +```bash +codeframe serve --port 5000 --no-browser +``` + +- Port: 5000 +- Host: 0.0.0.0 +- Browser: Does NOT open +- Reload: Disabled + +Manually visit: `http://localhost:5000` + +--- + +### Example 3: Development Mode + +```bash +codeframe serve --port 8080 --reload +``` + +- Port: 8080 +- Host: 0.0.0.0 +- Browser: Opens automatically +- Reload: Enabled (restarts on code changes) + +--- + +### Example 4: Secure Localhost-Only + +```bash +codeframe serve --host 127.0.0.1 --port 8080 +``` + +- Port: 8080 +- Host: 127.0.0.1 (localhost only, not accessible from network) +- Browser: Opens automatically +- Reload: Disabled + +--- + +## Troubleshooting + +### Error: "Port 8080 is already in use" + +**Cause**: Another process is using port 8080. + +**Solutions**: +1. Find and stop the other process: + ```bash + # macOS/Linux + lsof -i :8080 + kill + + # Windows + netstat -ano | findstr :8080 + taskkill /PID /F + ``` + +2. Use a different port: + ```bash + codeframe serve --port 8081 + ``` + +--- + +### Error: "uvicorn not found" + +**Cause**: uvicorn is not installed. + +**Solution**: Install uvicorn: + +```bash +pip install uvicorn +``` + +Or reinstall CodeFRAME with all dependencies: + +```bash +pip install -e ".[dev]" +``` + +--- + +### Error: "Module 'codeframe.ui.server' not found" + +**Cause**: CodeFRAME is not properly installed or you're in the wrong directory. + +**Solution**: + +1. Install CodeFRAME: + ```bash + pip install -e . + ``` + +2. Verify installation: + ```bash + python -c "import codeframe.ui.server" + ``` + +--- + +### Warning: "Could not open browser" + +**Cause**: Running in a headless environment (no GUI) or browser not found. + +**Effect**: Server still runs fine, just doesn't open browser. + +**Solution**: Manually open `http://localhost:8080` in your browser. + +--- + +### Server Starts But Can't Access Dashboard + +**Symptoms**: Server logs show "Uvicorn running" but browser shows "Connection refused". + +**Cause**: Firewall blocking port or wrong host binding. + +**Solutions**: + +1. Check if port is actually listening: + ```bash + # macOS/Linux + lsof -i :8080 + + # Windows + netstat -ano | findstr :8080 + ``` + +2. Try localhost explicitly: + ```bash + codeframe serve --host 127.0.0.1 + ``` + +3. Check firewall settings to allow port 8080. + +--- + +## Next Steps + +Now that your server is running: + +1. **Create a Project**: Visit `http://localhost:8080` and fill out the project creation form +2. **Submit a PRD**: Describe your project's requirements to start discovery +3. **Answer Questions**: The Lead Agent will ask clarifying questions +4. **Watch Agents Work**: Monitor progress in real-time via the dashboard + +See the main README.md for full workflow documentation. + +--- + +## Integration with Other Commands + +### Workflow: Init → Serve → Work + +```bash +# 1. Initialize a new project +codeframe init my-app + +# 2. Start the dashboard server +codeframe serve + +# 3. (In browser) Create project, submit PRD, answer questions + +# 4. (Optional) In another terminal, monitor status +codeframe status my-app +``` + +--- + +## FAQ + +**Q: Can I run multiple servers on different ports?** +A: Yes! Each port runs independently: +```bash +# Terminal 1 +codeframe serve --port 8080 + +# Terminal 2 +codeframe serve --port 9000 +``` + +**Q: How do I make the server accessible from another machine?** +A: Use `--host 0.0.0.0` (default) and make sure firewall allows the port. Then access via `http://:8080`. + +**Q: Is there a background/daemon mode?** +A: Not currently. The server runs in the foreground so you can easily stop it with Ctrl+C. For production deployment, use a process manager like systemd, supervisor, or Docker. + +**Q: Can I use HTTPS?** +A: Not directly with `serve` command. For HTTPS, use a reverse proxy (nginx, Caddy) in front of the server. + +**Q: Does `--reload` work for frontend changes?** +A: No, `--reload` only restarts the Python backend. Frontend changes are handled by Next.js dev server (`npm run dev` in `web-ui/`). + +--- + +## Command Cheat Sheet + +| Scenario | Command | +|----------|---------| +| Basic start | `codeframe serve` | +| Custom port | `codeframe serve --port 3000` | +| No browser | `codeframe serve --no-browser` | +| Development | `codeframe serve --reload` | +| Localhost only | `codeframe serve --host 127.0.0.1` | +| Stop server | `Ctrl+C` | + +--- + +## Video Walkthrough + +*(To be added: 2-minute screencast showing serve command usage)* + +--- + +## Related Documentation + +- **README.md**: Main project documentation +- **Feature Spec**: `/home/frankbria/projects/codeframe/specs/010-server-start-command/spec.md` +- **Implementation Plan**: `/home/frankbria/projects/codeframe/specs/010-server-start-command/plan.md` diff --git a/specs/010-server-start-command/research.md b/specs/010-server-start-command/research.md new file mode 100644 index 00000000..43022eab --- /dev/null +++ b/specs/010-server-start-command/research.md @@ -0,0 +1,539 @@ +# Research: Server Start Command + +**Feature**: 010-server-start-command +**Date**: 2025-01-18 +**Status**: Complete + +--- + +## Overview + +This document captures research findings for implementing the `codeframe serve` CLI command to start the dashboard server. Since this feature uses existing, well-established technologies (Typer, uvicorn, FastAPI), the research focuses on best practices and implementation patterns rather than technology selection. + +--- + +## Decision 1: CLI Framework Usage (Typer) + +### Context +CodeFRAME already uses Typer for CLI commands. We need to add a new `serve` command following existing patterns. + +### Research + +**Typer Command Structure**: +```python +import typer +app = typer.Typer() + +@app.command() +def serve( + port: int = typer.Option(8080, "--port", "-p", help="Port to run server on"), + host: str = typer.Option("0.0.0.0", "--host", help="Host to bind to"), + reload: bool = typer.Option(False, "--reload", help="Enable auto-reload"), + open_browser: bool = typer.Option(True, "--open-browser/--no-browser") +): + """Start the CodeFRAME dashboard server.""" + pass +``` + +**Best Practices**: +1. Use `typer.Option()` for optional flags with defaults +2. Provide both long and short form (--port and -p) +3. Include help text for all options +4. Use docstring for command description +5. Use boolean flags with --flag/--no-flag pattern + +### Decision +**Adopt Typer patterns** as shown above. Rationale: Consistency with existing CLI, excellent UX, well-documented. + +### Alternatives Considered +- **Click**: Rejected - Typer is already in use, built on Click, provides better UX +- **argparse**: Rejected - More verbose, less intuitive than Typer +- **Direct sys.argv parsing**: Rejected - Reinventing the wheel, no validation + +--- + +## Decision 2: Port Availability Checking + +### Context +Need to check if port 8080 (or custom port) is available before starting server to provide helpful error messages. + +### Research + +**Method 1: Socket Binding Test** (Recommended) +```python +import socket + +def is_port_available(port: int, host: str = "0.0.0.0") -> bool: + """Check if port is available by attempting to bind to it.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + try: + sock.bind((host, port)) + return True + except OSError: + return False +``` + +**Pros**: +- Accurate (actually tests binding capability) +- Cross-platform (standard library) +- No false positives + +**Cons**: +- Requires brief binding (but released immediately with context manager) + +**Method 2: netstat Parsing** (Rejected) +```python +import subprocess +output = subprocess.run(["netstat", "-an"], capture_output=True) +# Parse output for port in use +``` + +**Pros**: +- Shows what process is using port + +**Cons**: +- Platform-specific (different flags on Windows/Linux/macOS) +- Slower (subprocess overhead) +- Parsing is error-prone + +### Decision +**Use socket binding test (Method 1)**. Rationale: Most reliable, cross-platform, uses standard library, fast. + +### Implementation Pattern +```python +def check_port_availability(port: int, host: str) -> tuple[bool, str]: + """ + Check if port is available. + + Returns: + (available: bool, message: str) + """ + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + try: + sock.bind((host, port)) + return (True, "") + except OSError as e: + if e.errno == 48: # macOS: Address already in use + return (False, f"Port {port} is already in use. Try --port {port + 1}") + elif e.errno == 98: # Linux: Address already in use + return (False, f"Port {port} is already in use. Try --port {port + 1}") + elif e.errno == 10048: # Windows: Address already in use + return (False, f"Port {port} is already in use. Try --port {port + 1}") + else: + return (False, f"Cannot bind to port {port}: {e}") +``` + +--- + +## Decision 3: Cross-Platform Browser Opening + +### Context +Need to auto-open browser after server starts, working on macOS, Linux, and Windows. + +### Research + +**Python webbrowser Module** (Standard Library): +```python +import webbrowser +import time + +# Wait for server to start +time.sleep(1.5) + +# Open browser +try: + webbrowser.open("http://localhost:8080") +except Exception as e: + console.print(f"[yellow]Could not open browser: {e}[/yellow]") + console.print("Please open http://localhost:8080 manually") +``` + +**Platform Behavior**: +- **macOS**: Uses `open` command → opens default browser +- **Linux**: Uses `xdg-open` (if available) → opens default browser +- **Windows**: Uses `start` command → opens default browser + +**Best Practices**: +1. Wait 1-2 seconds after starting server before opening browser +2. Catch exceptions (browser might not be available in headless environments) +3. Fail gracefully - log warning but continue serving +4. Allow disabling with --no-browser flag + +### Decision +**Use Python's webbrowser module** with 1.5s delay and graceful error handling. Rationale: Cross-platform, standard library, well-tested, allows disable flag. + +### Alternatives Considered +- **subprocess + platform-specific commands**: Rejected - webbrowser module already does this +- **Third-party library (e.g., click.launch)**: Rejected - unnecessary dependency +- **No auto-open**: Rejected - poor UX, users expect modern CLI tools to open browser + +--- + +## Decision 4: Subprocess Management for uvicorn + +### Context +Need to start uvicorn as a subprocess and handle its lifecycle (start, run, stop). + +### Research + +**Method 1: subprocess.run()** (Recommended for our use case) +```python +import subprocess + +cmd = [ + "uvicorn", + "codeframe.ui.server:app", + "--host", host, + "--port", str(port), +] + +if reload: + cmd.append("--reload") + +try: + subprocess.run(cmd, check=True) +except KeyboardInterrupt: + console.print("\nāœ“ Server stopped") +except subprocess.CalledProcessError as e: + console.print(f"[red]Server error:[/red] {e}") + raise typer.Exit(1) +``` + +**Pros**: +- Blocking call (server runs in foreground) - matches user expectations +- Inherits stdout/stderr - user sees uvicorn logs directly +- Handles Ctrl+C naturally (KeyboardInterrupt) +- Simple error handling + +**Cons**: +- Blocks CLI until server stops (this is desired behavior) + +**Method 2: Popen with custom signal handling** (Rejected) +```python +import signal +process = subprocess.Popen(cmd) +signal.signal(signal.SIGINT, lambda s, f: process.terminate()) +process.wait() +``` + +**Pros**: +- More control over process lifecycle + +**Cons**: +- More complex +- No advantage for our use case (blocking is desired) +- Signal handling is platform-specific + +### Decision +**Use subprocess.run() with KeyboardInterrupt handling**. Rationale: Simple, reliable, user expects blocking behavior for server commands. + +### Implementation Pattern +```python +def start_server(host: str, port: int, reload: bool) -> None: + """Start uvicorn server (blocking call).""" + cmd = [ + "uvicorn", + "codeframe.ui.server:app", + "--host", host, + "--port", str(port), + ] + + if reload: + cmd.append("--reload") + + console.print(f"🌐 Starting dashboard server...") + console.print(f" URL: [bold cyan]http://localhost:{port}[/bold cyan]") + console.print(f" Press [bold]Ctrl+C[/bold] to stop\n") + + try: + subprocess.run(cmd, check=True) + except KeyboardInterrupt: + console.print("\nāœ“ Server stopped") + except FileNotFoundError: + console.print("[red]Error:[/red] uvicorn not found. Install with: pip install uvicorn") + raise typer.Exit(1) + except subprocess.CalledProcessError as e: + console.print(f"[red]Server error:[/red] {e}") + raise typer.Exit(1) +``` + +--- + +## Decision 5: Error Handling Patterns + +### Context +Need comprehensive error handling for various failure scenarios. + +### Research + +**Error Categories**: + +1. **Port Conflicts** (Most common) + - Check before starting + - Suggest alternative ports + - Show helpful message + +2. **Missing Dependencies** (uvicorn, FastAPI) + - Catch `FileNotFoundError` for uvicorn + - Catch `ModuleNotFoundError` for app module + - Show installation instructions + +3. **Permission Errors** (Low ports <1024) + - Catch `PermissionError` + - Explain need for elevated privileges + - Suggest using port ≄1024 + +4. **Keyboard Interrupt** (User stops server) + - Catch `KeyboardInterrupt` + - Show graceful shutdown message + - No stack trace + +**Best Practices**: +1. Use specific exception types (not bare `except`) +2. Provide actionable error messages +3. Use colors for visibility (red for errors, yellow for warnings) +4. Exit with appropriate exit code (0 = success, 1 = error) + +### Decision +**Implement comprehensive exception handling** with specific error types and helpful messages. Rationale: Better UX, easier debugging, professional feel. + +### Implementation Pattern +```python +def serve(port: int, host: str, open_browser: bool, reload: bool): + """Start the CodeFRAME dashboard server.""" + from rich.console import Console + console = Console() + + # Validate port + if port < 1024: + console.print(f"[red]Error:[/red] Port {port} requires elevated privileges") + console.print("Use a port ≄1024, e.g., --port 8080") + raise typer.Exit(1) + + # Check port availability + available, msg = check_port_availability(port, host) + if not available: + console.print(f"[red]Error:[/red] {msg}") + raise typer.Exit(1) + + # Start server + try: + # Open browser after delay + if open_browser: + import threading + def open_in_browser(): + time.sleep(1.5) + try: + webbrowser.open(f"http://localhost:{port}") + except Exception as e: + console.print(f"[yellow]Warning:[/yellow] Could not open browser: {e}") + + threading.Thread(target=open_in_browser, daemon=True).start() + + # Start server (blocking) + start_server(host, port, reload) + + except KeyboardInterrupt: + console.print("\nāœ“ Server stopped") + except Exception as e: + console.print(f"[red]Unexpected error:[/red] {e}") + raise typer.Exit(1) +``` + +--- + +## Decision 6: Console Output Formatting + +### Context +Need clear, professional console output using Rich (already in dependencies). + +### Research + +**Rich Console Features**: +- **Colors**: `[red]`, `[yellow]`, `[green]`, `[cyan]` +- **Styles**: `[bold]`, `[italic]`, `[dim]` +- **Emojis**: āœ“, āœ—, 🌐, šŸš€ +- **Markup**: Combine styles `[bold cyan]` + +**Output Structure**: +``` +🌐 Starting dashboard server... + URL: http://localhost:8080 + Press Ctrl+C to stop + +[uvicorn logs appear here...] + +^C +āœ“ Server stopped +``` + +### Decision +**Use Rich Console with emojis and colored markup**. Rationale: Consistent with existing CLI, professional appearance, better UX. + +--- + +## Technical Dependencies + +### Existing Dependencies (No Changes Needed) +- `typer >= 0.9.0` - CLI framework āœ… +- `uvicorn >= 0.20.0` - ASGI server āœ… +- `fastapi >= 0.100.0` - Web framework āœ… +- `rich >= 13.0.0` - Console formatting āœ… + +### Standard Library Modules +- `socket` - Port availability checking +- `subprocess` - Running uvicorn +- `webbrowser` - Opening browser +- `time` - Delay before browser open +- `threading` - Background browser opening + +### No New Dependencies Required āœ… + +--- + +## Performance Considerations + +### Startup Time +- **Target**: <2 seconds from command to server responding +- **Breakdown**: + - Port check: <10ms + - uvicorn startup: ~1-1.5s + - Browser open delay: 1.5s (asynchronous) + +### Resource Usage +- **Memory**: Minimal overhead (<1MB for subprocess management) +- **CPU**: Negligible (subprocess waits for uvicorn to handle) + +--- + +## Cross-Platform Considerations + +### macOS +- Default browser: Opens via `open` command āœ… +- Port binding: Standard BSD sockets āœ… +- Signals: SIGINT (Ctrl+C) works āœ… + +### Linux +- Default browser: Opens via `xdg-open` (if installed) āœ… +- Port binding: Standard Linux sockets āœ… +- Signals: SIGINT works āœ… +- Note: Headless servers won't have browser - handle gracefully āœ… + +### Windows +- Default browser: Opens via `start` command āœ… +- Port binding: Winsock API (Python abstraction) āœ… +- Signals: Ctrl+C generates KeyboardInterrupt āœ… + +### All Platforms +- Use `sys.platform` if platform-specific code needed (not expected) +- Test on all three platforms before release + +--- + +## Security Considerations + +### Port Range Validation +- **Allowed**: 1024-65535 (user ports) +- **Blocked**: 0-1023 (system ports, require privileges) +- **Rationale**: Prevent permission errors, follow best practices + +### Host Binding +- **Default**: `0.0.0.0` (all interfaces) +- **Alternative**: `127.0.0.1` (localhost only) via `--host` +- **Security**: Document that `0.0.0.0` exposes server to network +- **Production**: Recommend reverse proxy (nginx, Caddy) - out of scope + +### No Additional Security Concerns +- Server security handled by FastAPI/uvicorn +- No credential handling in serve command +- No file system access beyond reading code + +--- + +## Testing Strategy + +### Unit Tests (Port Checking) +```python +def test_port_available_when_free(): + available, msg = check_port_availability(9999, "127.0.0.1") + assert available is True + assert msg == "" + +def test_port_unavailable_when_in_use(): + # Bind to port first + with socket.socket() as s: + s.bind(("127.0.0.1", 9999)) + available, msg = check_port_availability(9999, "127.0.0.1") + assert available is False + assert "already in use" in msg.lower() +``` + +### Integration Tests (Subprocess) +```python +def test_serve_command_starts_server(): + # Start serve in subprocess + proc = subprocess.Popen( + ["codeframe", "serve", "--port", "9999", "--no-browser"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + # Wait for server to start + time.sleep(2) + + # Verify server responding + response = requests.get("http://localhost:9999") + assert response.status_code == 200 + + # Stop server + proc.terminate() + proc.wait() +``` + +--- + +## Open Questions (Resolved) + +### Q1: Should we support HTTPS? +**Answer**: No, out of scope. HTTPS should be handled by reverse proxy in production. Development server doesn't need it. + +### Q2: Should we support background/daemon mode? +**Answer**: No, out of scope. Users expect server commands to run in foreground (like Rails, Django, Flask). Background mode complicates lifecycle management. + +### Q3: Should we validate FastAPI app exists before starting? +**Answer**: No, let uvicorn handle it. uvicorn provides clear error messages if app module missing. Adding our own check is redundant. + +### Q4: Should we support multiple server instances? +**Answer**: No, out of scope. Single instance is sufficient for development. Multiple instances belong in production deployment documentation. + +--- + +## Implementation Checklist + +From this research, the implementation should: + +- [x] Use Typer for CLI command structure +- [x] Check port availability with socket binding +- [x] Start uvicorn with subprocess.run() +- [x] Open browser with webbrowser module + 1.5s delay +- [x] Handle KeyboardInterrupt gracefully +- [x] Provide specific error messages for common failures +- [x] Use Rich Console for formatted output +- [x] Support --port, --host, --reload, --no-browser flags +- [x] Validate port range (1024-65535) +- [x] Test on macOS, Linux, Windows + +--- + +## References + +- Typer Documentation: https://typer.tiangolo.com/ +- uvicorn Documentation: https://www.uvicorn.org/ +- Python socket module: https://docs.python.org/3/library/socket.html +- Python webbrowser module: https://docs.python.org/3/library/webbrowser.html +- Python subprocess module: https://docs.python.org/3/library/subprocess.html +- Rich Console: https://rich.readthedocs.io/en/stable/console.html + +--- + +**Research Status**: āœ… Complete - All unknowns resolved, ready for Phase 1 (Design) diff --git a/specs/010-server-start-command/spec.md b/specs/010-server-start-command/spec.md new file mode 100644 index 00000000..462bc480 --- /dev/null +++ b/specs/010-server-start-command/spec.md @@ -0,0 +1,467 @@ +# Feature Specification: Server Start Command + +**Feature ID**: 010 +**Sprint**: 9.5 (Critical UX Fixes) +**Priority**: P0 - Unblocks dashboard access +**Effort**: 2 hours +**Status**: šŸ“‹ Planning + +--- + +## Problem Statement + +Users cannot start the CodeFRAME dashboard after running `codeframe init` because there is no CLI command to launch the web server. This creates a critical onboarding blocker where new users: + +1. Run `codeframe init my-app` successfully +2. See instructions to run `codeframe start` +3. Run `codeframe start` expecting the dashboard to open +4. Get confused when nothing happens (no server, no browser, no feedback) + +**Current Behavior**: +```bash +$ codeframe init my-app +āœ“ Initialized project: my-app + Location: /home/user/my-app +Next steps: + 1. codeframe start - Start project execution + 2. codeframe status - Check project status + +$ codeframe start +šŸš€ Starting project my-app... +# Nothing happens - no server starts, no agents run +# User is stuck - cannot access dashboard +``` + +**Impact**: +- **User Readiness**: Prevents 100% of new users from accessing the dashboard +- **UX Complexity**: Scored 9/10 on complexity - "Not obvious how to start server" +- **First-Time Experience**: Creates immediate negative impression +- **Workaround**: Users must manually run `uvicorn codeframe.ui.server:app` (undocumented) + +--- + +## User Stories + +### User Story 1: Start Dashboard Server (P0 - Critical) + +**As a** new CodeFRAME user +**I want to** start the dashboard server with a simple command +**So that** I can access the web UI and interact with my project + +**Acceptance Criteria**: +- [ ] Command `codeframe serve` starts the FastAPI server +- [ ] Server runs on default port 8080 +- [ ] Console shows clear "Server running at http://localhost:8080" message +- [ ] Server continues running until user presses Ctrl+C +- [ ] Graceful shutdown on Ctrl+C with confirmation message + +**Definition of Done**: +- Tests written and passing (≄85% coverage) +- Command documented in README.md +- Manual test: server starts and serves dashboard HTML +- No regressions in existing CLI commands + +--- + +### User Story 2: Custom Port Configuration (P1 - Important) + +**As a** CodeFRAME user with port conflicts +**I want to** specify a custom port for the dashboard +**So that** I can avoid conflicts with other services on my machine + +**Acceptance Criteria**: +- [ ] Flag `--port` / `-p` accepts custom port number +- [ ] Validation: port must be 1024-65535 +- [ ] Clear error message if port already in use +- [ ] Server starts successfully on custom port +- [ ] Console message reflects custom port + +**Definition of Done**: +- Tests written for port validation and custom port +- Error handling tested for port conflicts +- Documentation updated with port flag + +--- + +### User Story 3: Auto-Open Browser (P2 - Enhancement) + +**As a** CodeFRAME user starting the server +**I want** the dashboard to automatically open in my browser +**So that** I don't have to manually copy/paste the URL + +**Acceptance Criteria**: +- [ ] Default behavior: browser opens automatically after server starts +- [ ] Flag `--no-browser` disables auto-open +- [ ] Works on macOS, Linux, Windows (webbrowser module) +- [ ] Small delay (1.5s) to ensure server is ready +- [ ] Handles case where browser fails to open gracefully + +**Definition of Done**: +- Tests written for browser open logic +- Cross-platform compatibility verified +- Documentation includes browser behavior + +--- + +### User Story 4: Development Mode (P2 - Enhancement) + +**As a** CodeFRAME developer +**I want** auto-reload when I change backend code +**So that** I can iterate quickly during development + +**Acceptance Criteria**: +- [ ] Flag `--reload` enables uvicorn auto-reload mode +- [ ] Server restarts automatically on file changes +- [ ] Clear console messages on reload +- [ ] Only works in development (not production) + +**Definition of Done**: +- Tests verify reload flag is passed to uvicorn +- Documentation explains development vs production usage + +--- + +## Requirements + +### Functional Requirements + +**FR1**: CLI Command Implementation +- Implement `serve` command in `codeframe/cli.py` +- Use Typer for argument parsing +- Execute uvicorn subprocess to run FastAPI app + +**FR2**: Port Management +- Default port: 8080 +- Accept `--port` flag (range: 1024-65535) +- Check port availability before starting +- Suggest alternative port if conflict detected + +**FR3**: Server Lifecycle +- Start uvicorn server with FastAPI app module +- Display startup message with URL +- Run in foreground (blocking call) +- Graceful shutdown on Ctrl+C (KeyboardInterrupt) +- Display shutdown confirmation message + +**FR4**: Browser Integration +- Auto-open browser to dashboard URL after 1.5s delay +- Support `--no-browser` flag to disable +- Use Python's `webbrowser` module (cross-platform) +- Fail gracefully if browser cannot open (log warning, continue) + +**FR5**: Development Support +- Accept `--reload` flag for development mode +- Pass reload flag to uvicorn +- Accept `--host` flag (default: 0.0.0.0) + +### Non-Functional Requirements + +**NFR1**: Performance +- Server startup time: <2 seconds +- Browser opening delay: 1.5 seconds (tunable) + +**NFR2**: Usability +- Clear, helpful error messages +- Console output uses colors for readability +- Shows port and URL prominently + +**NFR3**: Reliability +- Handles port conflicts gracefully +- Handles missing FastAPI app module gracefully +- Handles Ctrl+C without stack trace + +**NFR4**: Compatibility +- Works on Python 3.11+ +- Cross-platform: macOS, Linux, Windows +- Uses standard library where possible + +--- + +## Technical Approach + +### Architecture + +``` +codeframe/cli.py + │ + ā”œā”€ā”€ @app.command() + │ def serve( + │ port: int = 8080, + │ host: str = "0.0.0.0", + │ open_browser: bool = True, + │ reload: bool = False + │ ): + │ │ + │ ā”œā”€ā”€ Validate port availability + │ ā”œā”€ā”€ Build uvicorn command + │ ā”œā”€ā”€ Print startup message + │ ā”œā”€ā”€ Start uvicorn subprocess + │ └── Open browser (if enabled) +``` + +### Dependencies + +**Existing (already in codeframe)**: +- `typer` - CLI framework +- `uvicorn` - ASGI server +- `fastapi` - Web framework +- `rich.console` - Terminal output formatting + +**Standard Library**: +- `subprocess` - Run uvicorn +- `webbrowser` - Open browser +- `time` - Delay before opening browser +- `socket` - Check port availability +- `os` - Environment variables + +### Implementation Files + +**Modified Files**: +- `codeframe/cli.py` - Add `serve` command + +**New Files**: +- None (all changes in existing files) + +**Test Files**: +- `tests/cli/test_serve_command.py` - Unit tests for serve command + +--- + +## Testing Strategy + +### Unit Tests + +**Test Suite**: `tests/cli/test_serve_command.py` + +1. **test_serve_default_port** + - Run `serve` with no arguments + - Verify uvicorn called with port 8080 + +2. **test_serve_custom_port** + - Run `serve --port 3000` + - Verify uvicorn called with port 3000 + +3. **test_serve_port_validation** + - Run `serve --port 80` (requires root) + - Verify error message + +4. **test_serve_port_in_use** + - Bind to port 8080 beforehand + - Run `serve` + - Verify helpful error message with alternative port suggestion + +5. **test_serve_no_browser** + - Run `serve --no-browser` + - Verify browser.open() NOT called + +6. **test_serve_reload_flag** + - Run `serve --reload` + - Verify uvicorn called with --reload flag + +7. **test_serve_keyboard_interrupt** + - Simulate Ctrl+C (KeyboardInterrupt) + - Verify graceful shutdown message + +### Integration Tests + +**Test Suite**: `tests/integration/test_dashboard_access.py` + +1. **test_dashboard_accessible_after_serve** + - Start server with `serve` command + - Make HTTP GET request to http://localhost:8080 + - Verify 200 OK response + - Verify HTML contains "CodeFRAME" + +2. **test_serve_command_lifecycle** + - Start server in subprocess + - Wait for startup message + - Verify server responding + - Send SIGINT (Ctrl+C) + - Verify graceful shutdown + +### Manual Testing Checklist + +- [ ] `codeframe serve` starts server on port 8080 +- [ ] Browser opens automatically to http://localhost:8080 +- [ ] Dashboard HTML loads successfully +- [ ] `codeframe serve --port 3000` uses port 3000 +- [ ] `codeframe serve --no-browser` does NOT open browser +- [ ] `codeframe serve --reload` enables auto-reload +- [ ] Ctrl+C stops server gracefully +- [ ] Error shown if port already in use +- [ ] Helpful error if FastAPI app module missing +- [ ] Cross-platform: tested on macOS, Linux (Windows if available) + +--- + +## Documentation Updates + +### README.md + +Add to "Quick Start" section: + +```markdown +## Quick Start + +### 1. Start the Dashboard + +codeframe serve + +This will: +- Start the FastAPI server on port 8080 +- Automatically open your browser to the dashboard +- Display real-time project status + +Press Ctrl+C to stop the server. + +### Options + +- `--port 3000` - Use custom port +- `--no-browser` - Don't auto-open browser +- `--reload` - Enable auto-reload (development) +- `--host 127.0.0.1` - Bind to specific host +``` + +### CLI Help Text + +Update help output: + +``` +codeframe serve [OPTIONS] + + Start the CodeFRAME dashboard server. + +Options: + -p, --port INTEGER Port to run server on [default: 8080] + --host TEXT Host to bind to [default: 0.0.0.0] + --open-browser/--no-browser + Auto-open browser [default: open-browser] + --reload Enable auto-reload (development) + --help Show this message and exit. +``` + +--- + +## Success Metrics + +### Quantitative + +- [ ] Test coverage ≄85% for serve command +- [ ] Server startup time <2 seconds +- [ ] Zero regressions in existing CLI tests +- [ ] 100% of manual test checklist items pass + +### Qualitative + +- [ ] New users can start dashboard without documentation +- [ ] Error messages are clear and actionable +- [ ] Command feels intuitive (matches user expectations) +- [ ] No stack traces shown to user during normal operation + +--- + +## Out of Scope + +The following are explicitly OUT of scope for this feature: + +- āŒ HTTPS/SSL support (future enhancement) +- āŒ Multi-instance server management (future enhancement) +- āŒ Background/daemon mode (future enhancement) +- āŒ Hot module reload for frontend (handled by Next.js separately) +- āŒ Production deployment configuration (separate documentation) +- āŒ Docker container support (separate feature) + +--- + +## Dependencies + +### Upstream Dependencies (must be complete first) + +- FastAPI server application exists (`codeframe.ui.server:app`) +- Dashboard frontend builds successfully (`web-ui/`) +- Database migrations are up-to-date + +### Downstream Dependencies (depend on this feature) + +- Feature 2: Project Creation Flow (requires server to be running) +- Feature 3: Discovery Answer UI (requires server to be running) +- Sprint 10: E2E Testing (requires reliable server start) + +--- + +## Risks & Mitigations + +### Risk 1: Port Conflicts +**Probability**: High (8080 commonly used) +**Impact**: Medium (blocks server start) +**Mitigation**: +- Check port availability before starting +- Suggest alternative ports (8081, 8082, 8083) +- Document port configuration clearly + +### Risk 2: Browser Auto-Open Fails +**Probability**: Low (webbrowser module is reliable) +**Impact**: Low (user can still access manually) +**Mitigation**: +- Catch exceptions from webbrowser.open() +- Log warning but continue server startup +- Display URL prominently in console + +### Risk 3: uvicorn Not Installed +**Probability**: Low (in project dependencies) +**Impact**: High (command fails) +**Mitigation**: +- Check for uvicorn import at startup +- Show clear installation instructions if missing +- Document dependencies in README + +--- + +## Alternative Approaches Considered + +### Alternative 1: Built-in HTTP Server (Rejected) +**Approach**: Use Python's built-in `http.server` instead of uvicorn +**Rejected Because**: +- No ASGI support (FastAPI requires it) +- No WebSocket support +- Poor performance for production-like usage + +### Alternative 2: Separate `dashboard` Command (Rejected) +**Approach**: Create `codeframe dashboard` instead of `codeframe serve` +**Rejected Because**: +- `serve` is more conventional (matches Rails, Django, Flask) +- `dashboard` implies it only shows read-only data +- `serve` better communicates it's starting a server + +### Alternative 3: Always-On Background Server (Rejected) +**Approach**: Start server automatically in background on `codeframe init` +**Rejected Because**: +- Users want control over when server runs +- Background processes complicate debugging +- Increases resource usage even when not needed +- Harder to stop/restart during development + +--- + +## Timeline + +**Estimated Effort**: 2 hours + +**Hour-by-Hour Breakdown**: +- **Hour 1**: Implementation + - Implement `serve` command (30 min) + - Add port validation (15 min) + - Add browser auto-open (15 min) +- **Hour 2**: Testing and Documentation + - Write unit tests (30 min) + - Manual testing (15 min) + - Update README and help text (15 min) + +--- + +## References + +- Sprint 9.5 Document: `/home/frankbria/projects/codeframe/sprints/sprint-09.5-critical-ux-fixes.md` (lines 47-176) +- Typer Documentation: https://typer.tiangolo.com/ +- uvicorn Documentation: https://www.uvicorn.org/ +- Python webbrowser module: https://docs.python.org/3/library/webbrowser.html diff --git a/specs/010-server-start-command/tasks.md b/specs/010-server-start-command/tasks.md new file mode 100644 index 00000000..5d1c615c --- /dev/null +++ b/specs/010-server-start-command/tasks.md @@ -0,0 +1,438 @@ +# Implementation Tasks: Server Start Command + +**Feature**: 010-server-start-command +**Branch**: `010-server-start-command` +**Spec**: [spec.md](./spec.md) | **Plan**: [plan.md](./plan.md) + +--- + +## Overview + +Implement `codeframe serve` CLI command to start the FastAPI dashboard server, solving the critical onboarding blocker where new users cannot access the web UI. + +**Total Effort**: 2 hours +**Total Tasks**: 21 +**Test Coverage Target**: ≄85% + +--- + +## Task Summary + +| Phase | User Story | Tasks | Parallelizable | Estimated Time | +|-------|------------|-------|----------------|----------------| +| Phase 1 | Setup | 2 | 0 | 10 min | +| Phase 2 | Foundational | 1 | 0 | 5 min | +| Phase 3 | US1 (P0) | 6 | 3 | 45 min | +| Phase 4 | US2 (P1) | 4 | 2 | 20 min | +| Phase 5 | US3 (P2) | 4 | 2 | 15 min | +| Phase 6 | US4 (P2) | 2 | 1 | 10 min | +| Phase 7 | Polish | 2 | 0 | 15 min | + +--- + +## Implementation Strategy + +**MVP Scope**: User Story 1 only (P0 - Critical) +- Basic `serve` command with default port 8080 +- Graceful shutdown on Ctrl+C +- Clear console messages +- Testable and deployable independently + +**Incremental Delivery**: +1. **Iteration 1 (MVP)**: US1 → Basic server start → Testable, deployable +2. **Iteration 2**: US2 → Port configuration → Independent enhancement +3. **Iteration 3**: US3 → Browser auto-open → Independent enhancement +4. **Iteration 4**: US4 → Development mode → Independent enhancement + +**TDD Approach**: Tests written FIRST for each user story (Constitution requirement) + +--- + +## Dependencies Between User Stories + +``` +Phase 1 (Setup) + ↓ +Phase 2 (Foundational) + ↓ +Phase 3 (US1 - P0) ──→ INDEPENDENTLY TESTABLE & DEPLOYABLE āœ… + ↓ (optional dependency) +Phase 4 (US2 - P1) ──→ INDEPENDENTLY TESTABLE & DEPLOYABLE āœ… + ↓ (optional dependency) +Phase 5 (US3 - P2) ──→ INDEPENDENTLY TESTABLE & DEPLOYABLE āœ… + ↓ (optional dependency) +Phase 6 (US4 - P2) ──→ INDEPENDENTLY TESTABLE & DEPLOYABLE āœ… + ↓ +Phase 7 (Polish) +``` + +**Notes**: +- Each user story can be implemented independently after foundational tasks +- US2, US3, US4 are enhancements to US1 but don't block each other +- Recommended order: US1 → US2 → US3 → US4 (priority order) + +--- + +## Phase 1: Setup + +**Goal**: Initialize test infrastructure and verify dependencies. + +**Tasks**: + +- [X] T001 Create test directory structure for CLI tests in tests/cli/ +- [X] T002 Verify all dependencies present in pyproject.toml (typer, uvicorn, fastapi, rich, pytest) + +**Estimated Time**: 10 minutes + +--- + +## Phase 2: Foundational Tasks + +**Goal**: Create core utilities shared across all user stories. + +**Tasks**: + +- [X] T003 Create helper module for port validation in codeframe/core/port_utils.py + +**Estimated Time**: 5 minutes + +**Deliverables**: +- `codeframe/core/port_utils.py` - Port availability checking utility + +**Independent Test Criteria**: +- Port validation utility can be tested independently +- No dependencies on serve command + +--- + +## Phase 3: User Story 1 - Start Dashboard Server (P0 - Critical) + +**Story**: As a new CodeFRAME user, I want to start the dashboard server with a simple command, so that I can access the web UI and interact with my project. + +**Goal**: Implement basic `codeframe serve` command with default port 8080 and graceful shutdown. + +### Tests (Write First - TDD) + +- [X] T004 [US1] Write test_serve_default_port() in tests/cli/test_serve_command.py +- [X] T005 [P] [US1] Write test_serve_keyboard_interrupt() in tests/cli/test_serve_command.py +- [X] T006 [P] [US1] Write test_dashboard_accessible_after_serve() in tests/integration/test_dashboard_access.py + +**Test Details**: +- **T004**: Verify uvicorn called with port 8080 (mock subprocess.run) +- **T005**: Verify graceful shutdown message on Ctrl+C (mock KeyboardInterrupt) +- **T006**: Start server, verify HTTP 200 response, stop server + +### Implementation + +- [X] T007 [US1] Implement serve() command skeleton in codeframe/cli.py with Typer decorators +- [X] T008 [US1] Implement subprocess management for uvicorn in codeframe/cli.py +- [X] T009 [P] [US1] Implement console output formatting using Rich in codeframe/cli.py + +**Implementation Details**: +- **T007**: Add `@app.command()` decorator, define parameters, basic structure +- **T008**: Build uvicorn command, start subprocess.run(), handle KeyboardInterrupt +- **T009**: Add colored output messages (startup, URL, shutdown) + +**Estimated Time**: 45 minutes (20 min tests, 25 min implementation) + +**Deliverables**: +- `tests/cli/test_serve_command.py` - Unit tests for serve command +- `tests/integration/test_dashboard_access.py` - Integration test +- `codeframe/cli.py` - serve() command implementation + +**Independent Test Criteria**: +- āœ… `codeframe serve` starts server on port 8080 +- āœ… Server responds to HTTP requests +- āœ… Ctrl+C stops server gracefully +- āœ… Console shows clear startup and shutdown messages +- āœ… Tests pass with ≄85% coverage + +**Parallel Execution**: T005 and T006 can be written in parallel (different test files) + +--- + +## Phase 4: User Story 2 - Custom Port Configuration (P1 - Important) + +**Story**: As a CodeFRAME user with port conflicts, I want to specify a custom port for the dashboard, so that I can avoid conflicts with other services on my machine. + +**Goal**: Add `--port` flag with validation and port conflict detection. + +### Tests (Write First - TDD) + +- [X] T010 [US2] Write test_serve_custom_port() in tests/cli/test_serve_command.py +- [X] T011 [P] [US2] Write test_serve_port_validation() in tests/cli/test_serve_command.py +- [X] T012 [P] [US2] Write test_serve_port_in_use() in tests/cli/test_serve_command.py + +**Test Details**: +- **T010**: Verify uvicorn called with custom port (mock subprocess.run) +- **T011**: Verify port <1024 rejected with helpful error +- **T012**: Verify port conflict detected, alternative port suggested + +### Implementation + +- [X] T013 [US2] Add --port flag to serve() command in codeframe/cli.py with validation + +**Implementation Details**: +- **T013**: Add port parameter, validate range (1024-65535), use port_utils for availability check + +**Estimated Time**: 20 minutes (10 min tests, 10 min implementation) + +**Deliverables**: +- Updated `tests/cli/test_serve_command.py` - Port validation tests +- Updated `codeframe/cli.py` - Port flag and validation + +**Independent Test Criteria**: +- āœ… `codeframe serve --port 3000` uses port 3000 +- āœ… Port <1024 shows helpful error message +- āœ… Port conflict shows helpful error with alternative suggestion +- āœ… Tests pass with ≄85% coverage + +**Parallel Execution**: T011 and T012 can be written in parallel (independent test cases) + +--- + +## Phase 5: User Story 3 - Auto-Open Browser (P2 - Enhancement) + +**Story**: As a CodeFRAME user starting the server, I want the dashboard to automatically open in my browser, so that I don't have to manually copy/paste the URL. + +**Goal**: Add browser auto-open with `--no-browser` flag to disable. + +### Tests (Write First - TDD) + +- [X] T014 [US3] Write test_serve_browser_opens() in tests/cli/test_serve_command.py +- [X] T015 [P] [US3] Write test_serve_no_browser() in tests/cli/test_serve_command.py + +**Test Details**: +- **T014**: Verify webbrowser.open() called after 1.5s delay (mock webbrowser, threading) +- **T015**: Verify webbrowser.open() NOT called when --no-browser flag used + +### Implementation + +- [X] T016 [US3] Add --open-browser/--no-browser flag to serve() in codeframe/cli.py +- [X] T017 [P] [US3] Implement browser opening logic with threading in codeframe/cli.py + +**Implementation Details**: +- **T016**: Add open_browser parameter (default True), pass to browser logic +- **T017**: Create background thread, sleep 1.5s, call webbrowser.open(), catch exceptions + +**Estimated Time**: 15 minutes (8 min tests, 7 min implementation) + +**Deliverables**: +- Updated `tests/cli/test_serve_command.py` - Browser tests +- Updated `codeframe/cli.py` - Browser opening logic + +**Independent Test Criteria**: +- āœ… `codeframe serve` auto-opens browser after 1.5s +- āœ… `codeframe serve --no-browser` does NOT open browser +- āœ… Browser failure handled gracefully (warning, continues) +- āœ… Tests pass with ≄85% coverage + +**Parallel Execution**: T014 and T015 can be written in parallel (independent test cases), T017 can be implemented in parallel with T016 + +--- + +## Phase 6: User Story 4 - Development Mode (P2 - Enhancement) + +**Story**: As a CodeFRAME developer, I want auto-reload when I change backend code, so that I can iterate quickly during development. + +**Goal**: Add `--reload` flag for uvicorn auto-reload mode. + +### Tests (Write First - TDD) + +- [X] T018 [US4] Write test_serve_reload_flag() in tests/cli/test_serve_command.py + +**Test Details**: +- **T018**: Verify --reload flag passed to uvicorn command (mock subprocess.run) + +### Implementation + +- [X] T019 [P] [US4] Add --reload flag to serve() command in codeframe/cli.py + +**Implementation Details**: +- **T019**: Add reload parameter (default False), append "--reload" to uvicorn command if enabled + +**Estimated Time**: 10 minutes (5 min test, 5 min implementation) + +**Deliverables**: +- Updated `tests/cli/test_serve_command.py` - Reload flag test +- Updated `codeframe/cli.py` - Reload flag implementation + +**Independent Test Criteria**: +- āœ… `codeframe serve --reload` enables auto-reload +- āœ… Server restarts on file changes (manual verification) +- āœ… Tests pass with ≄85% coverage + +**Parallel Execution**: T019 can be implemented in parallel with T018 (simple flag addition) + +--- + +## Phase 7: Polish & Cross-Cutting Concerns + +**Goal**: Documentation, final testing, and polish. + +### Tasks + +- [X] T020 Update README.md Quick Start section with serve command usage +- [X] T021 Run full test suite and verify ≄85% coverage for serve command + +**Details**: +- **T020**: Add serve command to Quick Start (lines 250-296), document flags, add examples +- **T021**: Run `pytest --cov=codeframe.cli --cov-report=term-missing`, verify coverage ≄85% + +**Estimated Time**: 15 minutes + +**Deliverables**: +- Updated `README.md` - serve command documentation +- Coverage report confirming ≄85% + +--- + +## Validation Checklist + +Before marking feature complete, verify: + +### User Story 1 (P0) +- [x] Tests written first (TDD) +- [ ] `codeframe serve` starts server on port 8080 +- [ ] Console shows clear startup message with URL +- [ ] Ctrl+C stops server gracefully +- [ ] HTTP requests return 200 OK +- [ ] Tests pass (≄85% coverage) + +### User Story 2 (P1) +- [x] Tests written first (TDD) +- [ ] `codeframe serve --port 3000` uses port 3000 +- [ ] Port <1024 shows error +- [ ] Port conflict shows error with suggestion +- [ ] Tests pass (≄85% coverage) + +### User Story 3 (P2) +- [x] Tests written first (TDD) +- [ ] Browser opens automatically by default +- [ ] `--no-browser` disables auto-open +- [ ] Browser failure handled gracefully +- [ ] Tests pass (≄85% coverage) + +### User Story 4 (P2) +- [x] Tests written first (TDD) +- [ ] `--reload` flag enables auto-reload +- [ ] Tests pass (≄85% coverage) + +### Cross-Cutting +- [ ] README.md updated +- [ ] All tests passing (pytest) +- [ ] Type checking passes (mypy) +- [ ] Linting clean (ruff) +- [ ] Manual testing on macOS, Linux, Windows + +--- + +## Parallel Execution Opportunities + +### Within User Story 1 +```bash +# Terminal 1: Write T004 +# Terminal 2: Write T005 (different test) +# Terminal 3: Write T006 (different file) + +# After tests written: +# Terminal 1: Implement T007, T008 +# Terminal 2: Implement T009 (independent formatting) +``` + +### Within User Story 2 +```bash +# Terminal 1: Write T010 +# Terminal 2: Write T011, T012 (independent tests) + +# After tests written: +# Terminal 1: Implement T013 +``` + +### Within User Story 3 +```bash +# Terminal 1: Write T014 +# Terminal 2: Write T015 (independent test) + +# After tests written: +# Terminal 1: Implement T016 +# Terminal 2: Implement T017 (threading logic) +``` + +### Within User Story 4 +```bash +# Terminal 1: Write T018, implement T019 (simple flag) +``` + +**Maximum Parallelism**: Up to 3 concurrent tasks during test writing phases. + +--- + +## Task Execution Order + +### Sequential (Must Complete Before Next) +1. Phase 1 (Setup) → Phase 2 (Foundational) → User Stories +2. Within each user story: Tests → Implementation +3. Phase 7 (Polish) after all user stories + +### Flexible (Can Be Reordered) +- User Story 2, 3, 4 can be implemented in any order after US1 +- Recommended: Follow priority order (US1 → US2 → US3 → US4) + +--- + +## File Manifest + +**New Files**: +- `tests/cli/test_serve_command.py` - Unit tests (7 test cases, ~200 lines) +- `tests/integration/test_dashboard_access.py` - Integration tests (2 test cases, ~100 lines) +- `codeframe/core/port_utils.py` - Port validation utility (~50 lines) + +**Modified Files**: +- `codeframe/cli.py` - Add serve() command (~150 lines) +- `README.md` - Update Quick Start section (~20 lines) + +**Total New Code**: ~450 lines (including tests) + +--- + +## Success Metrics + +**Quantitative**: +- āœ… 9 unit tests + 2 integration tests = 11 tests total +- āœ… Test coverage ≄85% for serve command +- āœ… Server startup time <2 seconds +- āœ… Zero regressions in existing CLI tests + +**Qualitative**: +- āœ… New users can start dashboard without documentation +- āœ… Error messages are clear and actionable +- āœ… Command feels intuitive (matches Rails, Django, Flask conventions) +- āœ… No stack traces during normal operation (Ctrl+C) + +--- + +## Rollback Strategy + +If issues discovered after merging: + +1. **Revert entire feature**: `git revert ` +2. **Disable command**: Add `@app.command(hidden=True)` temporarily +3. **Hotfix**: Fix specific issue, merge quickly + +**Low Risk**: Feature is additive (new command), doesn't modify existing functionality. + +--- + +## Next Steps + +1. **Start Implementation**: Begin with Phase 1 (Setup) +2. **TDD Approach**: Write tests FIRST for each user story +3. **Incremental Delivery**: Deploy US1 (MVP) before proceeding to US2-US4 +4. **Manual Testing**: Test on macOS, Linux, Windows before final merge +5. **Documentation**: Update README.md after all features complete + +--- + +**Status**: āœ… Ready for Implementation +**Command**: Begin with `T001` (Create test directory structure) diff --git a/tests/cli/test_serve_command.py b/tests/cli/test_serve_command.py new file mode 100644 index 00000000..fc543f47 --- /dev/null +++ b/tests/cli/test_serve_command.py @@ -0,0 +1,190 @@ +"""Tests for the serve CLI command.""" + +import subprocess +from unittest.mock import Mock, patch + +from typer.testing import CliRunner + + +# Tests for serve command following TDD approach +runner = CliRunner() + + +class TestServeBasicFunctionality: + """Test basic serve command functionality (User Story 1).""" + + @patch("codeframe.cli.subprocess.run") + @patch("codeframe.cli.check_port_availability") + def test_serve_default_port(self, mock_port_check, mock_run): + """Test that serve command uses default port 8080.""" + from codeframe.cli import app + + # Mock port as available + mock_port_check.return_value = (True, "") + # Mock subprocess to avoid actually starting server + mock_run.return_value = Mock(returncode=0) + + # Run command + runner.invoke(app, ["serve", "--no-browser"]) + + # Verify uvicorn was called with port 8080 + assert mock_run.called + call_args = mock_run.call_args[0][0] # Get the command list + assert "uvicorn" in call_args + assert "--port" in call_args + port_index = call_args.index("--port") + 1 + assert call_args[port_index] == "8080" + + @patch("codeframe.cli.subprocess.run") + @patch("codeframe.cli.check_port_availability") + def test_serve_keyboard_interrupt(self, mock_port_check, mock_run): + """Test graceful shutdown on Ctrl+C.""" + from codeframe.cli import app + + # Mock port as available + mock_port_check.return_value = (True, "") + # Mock subprocess to raise KeyboardInterrupt (simulating Ctrl+C) + mock_run.side_effect = KeyboardInterrupt() + + # Run command - should handle KeyboardInterrupt gracefully + result = runner.invoke(app, ["serve", "--no-browser"]) + + # Should have attempted to start server + assert mock_run.called + # Should show shutdown message + assert "Server stopped" in result.stdout + + +class TestServeCustomPort: + """Test custom port configuration (User Story 2).""" + + @patch("codeframe.cli.subprocess.run") + @patch("codeframe.cli.check_port_availability") + def test_serve_custom_port(self, mock_port_check, mock_run): + """Test that --port flag sets custom port.""" + from codeframe.cli import app + + # Mock port as available + mock_port_check.return_value = (True, "") + mock_run.return_value = Mock(returncode=0) + + # Run command with custom port + runner.invoke(app, ["serve", "--port", "3000", "--no-browser"]) + + # Verify uvicorn was called with port 3000 + assert mock_run.called + call_args = mock_run.call_args[0][0] + assert "--port" in call_args + port_index = call_args.index("--port") + 1 + assert call_args[port_index] == "3000" + + def test_serve_port_validation(self): + """Test that port <1024 is rejected with helpful error.""" + from codeframe.cli import app + + # Attempt to use privileged port should fail + result = runner.invoke(app, ["serve", "--port", "80"]) + assert result.exit_code != 0 + assert "elevated privileges" in result.stdout + + @patch("codeframe.cli.subprocess.run") + @patch("codeframe.cli.check_port_availability") + def test_serve_port_in_use(self, mock_port_check, mock_run): + """Test helpful error when port is already in use.""" + from codeframe.cli import app + + # Simulate port already in use + mock_port_check.return_value = (False, "Port 8080 is already in use") + + # Run command + result = runner.invoke(app, ["serve"]) + + # Should exit with error + assert result.exit_code != 0 + # Should not attempt to start server + assert not mock_run.called + + @patch("codeframe.cli.subprocess.run") + @patch("codeframe.cli.check_port_availability") + def test_serve_subprocess_failure(self, mock_port_check, mock_run): + """Test error handling when server subprocess fails to start.""" + from codeframe.cli import app + + # Port check passes + mock_port_check.return_value = (True, "") + + # But subprocess fails (e.g., port conflict, permission error, etc.) + mock_run.side_effect = subprocess.CalledProcessError(1, "uvicorn") + + # Run command + result = runner.invoke(app, ["serve", "--no-browser"]) + + # Should exit with error + assert result.exit_code != 0 + # Should show helpful troubleshooting message + assert "failed to start" in result.stdout.lower() + assert "common issues" in result.stdout.lower() + + +class TestServeBrowserOpening: + """Test browser auto-open functionality (User Story 3).""" + + @patch("codeframe.cli.threading.Thread") + @patch("codeframe.cli.subprocess.run") + @patch("codeframe.cli.check_port_availability") + def test_serve_browser_opens(self, mock_port_check, mock_run, mock_thread): + """Test that browser opens automatically by default.""" + from codeframe.cli import app + + # Mock port as available + mock_port_check.return_value = (True, "") + mock_run.return_value = Mock(returncode=0) + mock_thread_instance = Mock() + mock_thread.return_value = mock_thread_instance + + # Run command with default (browser enabled) + runner.invoke(app, ["serve"]) + + # Verify background thread was created for browser opening + assert mock_thread.called + # Verify thread was started + assert mock_thread_instance.start.called + + @patch("codeframe.cli.threading.Thread") + @patch("codeframe.cli.subprocess.run") + @patch("codeframe.cli.check_port_availability") + def test_serve_no_browser(self, mock_port_check, mock_run, mock_thread): + """Test that --no-browser flag prevents browser opening.""" + from codeframe.cli import app + + # Mock port as available + mock_port_check.return_value = (True, "") + mock_run.return_value = Mock(returncode=0) + + # Run command with --no-browser + runner.invoke(app, ["serve", "--no-browser"]) + + # Browser thread should NOT be created + assert not mock_thread.called + + +class TestServeReloadFlag: + """Test development reload functionality (User Story 4).""" + + @patch("codeframe.cli.subprocess.run") + @patch("codeframe.cli.check_port_availability") + def test_serve_reload_flag(self, mock_port_check, mock_run): + """Test that --reload flag is passed to uvicorn.""" + from codeframe.cli import app + + # Mock port as available + mock_port_check.return_value = (True, "") + mock_run.return_value = Mock(returncode=0) + + # Run command with --reload + runner.invoke(app, ["serve", "--reload", "--no-browser"]) + + # Verify --reload was passed to uvicorn + assert mock_run.called + call_args = mock_run.call_args[0][0] + assert "--reload" in call_args diff --git a/tests/core/test_port_utils.py b/tests/core/test_port_utils.py new file mode 100644 index 00000000..490f4191 --- /dev/null +++ b/tests/core/test_port_utils.py @@ -0,0 +1,115 @@ +"""Tests for port utility functions.""" + +from unittest.mock import patch, Mock + + +from codeframe.core.port_utils import ( + is_port_available, + check_port_availability, + validate_port_range, +) + + +class TestIsPortAvailable: + """Test is_port_available function.""" + + def test_port_available(self): + """Test that function returns True for available port.""" + # Use a high random port that's likely to be available + result = is_port_available(59999, "127.0.0.1") + assert result is True + + @patch("socket.socket") + def test_port_unavailable(self, mock_socket): + """Test that function returns False when port is in use.""" + # Mock socket to raise OSError (port in use) + mock_sock_instance = Mock() + mock_sock_instance.__enter__ = Mock(return_value=mock_sock_instance) + mock_sock_instance.__exit__ = Mock(return_value=None) + mock_sock_instance.bind.side_effect = OSError("Address already in use") + mock_socket.return_value = mock_sock_instance + + result = is_port_available(8080, "127.0.0.1") + assert result is False + + +class TestCheckPortAvailability: + """Test check_port_availability function.""" + + def test_privileged_port_rejected(self): + """Test that port <1024 is rejected with helpful message.""" + available, msg = check_port_availability(80, "127.0.0.1") + + assert available is False + assert "elevated privileges" in msg.lower() + assert "8080" in msg # Should suggest alternative port + + def test_available_port_returns_true(self): + """Test that available port returns (True, '').""" + available, msg = check_port_availability(59998, "127.0.0.1") + + assert available is True + assert msg == "" + + @patch("socket.socket") + def test_port_in_use_returns_helpful_message(self, mock_socket): + """Test helpful error message when port is in use.""" + # Mock socket to raise OSError with errno 98 (Linux: Address already in use) + mock_sock_instance = Mock() + mock_sock_instance.__enter__ = Mock(return_value=mock_sock_instance) + mock_sock_instance.__exit__ = Mock(return_value=None) + mock_error = OSError("Address already in use") + mock_error.errno = 98 + mock_sock_instance.bind.side_effect = mock_error + mock_socket.return_value = mock_sock_instance + + available, msg = check_port_availability(8080, "127.0.0.1") + + assert available is False + assert "8080" in msg + assert "already in use" in msg.lower() + assert "8081" in msg # Should suggest port+1 + + @patch("socket.socket") + def test_other_os_error_returns_error_message(self, mock_socket): + """Test that other OSErrors return descriptive message.""" + # Mock socket to raise OSError with different errno + mock_sock_instance = Mock() + mock_sock_instance.__enter__ = Mock(return_value=mock_sock_instance) + mock_sock_instance.__exit__ = Mock(return_value=None) + mock_error = OSError("Some other error") + mock_error.errno = 999 # Not a known errno + mock_sock_instance.bind.side_effect = mock_error + mock_socket.return_value = mock_sock_instance + + available, msg = check_port_availability(8080, "127.0.0.1") + + assert available is False + assert "Cannot bind" in msg + assert "8080" in msg + + +class TestValidatePortRange: + """Test validate_port_range function.""" + + def test_valid_port_returns_true(self): + """Test that valid port (1024-65535) returns (True, '').""" + for port in [1024, 8080, 65535]: + valid, msg = validate_port_range(port) + assert valid is True, f"Port {port} should be valid" + assert msg == "", f"Port {port} should have empty message" + + def test_privileged_port_rejected(self): + """Test that port <1024 is rejected.""" + for port in [1, 80, 443, 1023]: + valid, msg = validate_port_range(port) + assert valid is False, f"Port {port} should be invalid" + assert "elevated privileges" in msg.lower() + + def test_port_above_max_rejected(self): + """Test that port >65535 is rejected.""" + for port in [65536, 100000]: + valid, msg = validate_port_range(port) + assert valid is False, f"Port {port} should be invalid" + assert "out of range" in msg.lower() + assert "65535" in msg diff --git a/tests/integration/test_dashboard_access.py b/tests/integration/test_dashboard_access.py new file mode 100644 index 00000000..c8d41f2c --- /dev/null +++ b/tests/integration/test_dashboard_access.py @@ -0,0 +1,95 @@ +"""Integration tests for dashboard server access.""" + +import subprocess +import time +from typing import Optional + +import pytest +import requests + + +class TestDashboardAccess: + """Integration tests for server lifecycle and accessibility.""" + + @pytest.fixture + def test_port(self) -> int: + """Use a unique test port to avoid conflicts.""" + return 9999 + + @pytest.fixture + def server_process(self, test_port: int): + """Start server process for testing, clean up after.""" + process: Optional[subprocess.Popen] = None + try: + # Start server in subprocess + process = subprocess.Popen( + [ + "uv", + "run", + "codeframe", + "serve", + "--port", + str(test_port), + "--no-browser", + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + # Wait for server to start (max 5 seconds) + for _ in range(50): + try: + response = requests.get(f"http://localhost:{test_port}", timeout=1) + if response.status_code == 200: + break + except requests.ConnectionError: + pass + time.sleep(0.1) + + yield process + + finally: + # Clean up: terminate server process + if process: + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + + def test_dashboard_accessible_after_serve( + self, server_process: subprocess.Popen, test_port: int + ): + """Test that dashboard is accessible after serve command starts.""" + # Server should be running (started by fixture) + assert server_process.poll() is None, "Server process should be running" + + # Make HTTP request to dashboard + response = requests.get(f"http://localhost:{test_port}", timeout=5) + + # Should get 200 OK + assert response.status_code == 200, "Dashboard should return 200 OK" + + # Response should contain HTML + assert "text/html" in response.headers.get("content-type", ""), "Should return HTML content" + + def test_serve_command_lifecycle(self, server_process: subprocess.Popen, test_port: int): + """Test complete server lifecycle: start, verify, stop.""" + # Verify server is running + assert server_process.poll() is None, "Server should be running" + + # Verify server responds to requests + response = requests.get(f"http://localhost:{test_port}", timeout=5) + assert response.status_code == 200 + + # Stop server (send SIGTERM) + server_process.terminate() + server_process.wait(timeout=5) + + # Verify server stopped + assert server_process.poll() is not None, "Server should have stopped" + + # Verify server no longer responding + time.sleep(0.5) # Give port time to release + with pytest.raises(requests.ConnectionError): + requests.get(f"http://localhost:{test_port}", timeout=1)