mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 14:22:04 +00:00
feat: add interactive terminal and dev server management
Add new features for interactive terminal sessions and dev server control: Terminal Component: - New Terminal.tsx component using xterm.js for full terminal emulation - WebSocket-based PTY communication with bidirectional I/O - Cross-platform support (Windows via winpty, Unix via built-in pty) - Auto-reconnection with exponential backoff - Fix duplicate WebSocket connection bug by checking CONNECTING state - Add manual close flag to prevent auto-reconnect race conditions - Add project tracking to avoid duplicate connects on initial activation Dev Server Management: - New DevServerControl.tsx for starting/stopping dev servers - DevServerManager service for subprocess management - WebSocket streaming of dev server output - Project configuration service for reading package.json scripts Backend Infrastructure: - Terminal router with WebSocket endpoint for PTY I/O - DevServer router for server lifecycle management - Terminal session manager with callback-based output streaming - Enhanced WebSocket schemas for terminal and dev server messages UI Integration: - New Terminal and Dev Server tabs in the main application - Updated DebugLogViewer with improved UI and functionality - Extended useWebSocket hook for terminal message handling Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
280
server/routers/devserver.py
Normal file
280
server/routers/devserver.py
Normal file
@@ -0,0 +1,280 @@
|
||||
"""
|
||||
Dev Server Router
|
||||
=================
|
||||
|
||||
API endpoints for dev server control (start/stop) and configuration.
|
||||
Uses project registry for path lookups and project_config for command detection.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from ..schemas import (
|
||||
DevServerActionResponse,
|
||||
DevServerConfigResponse,
|
||||
DevServerConfigUpdate,
|
||||
DevServerStartRequest,
|
||||
DevServerStatus,
|
||||
)
|
||||
from ..services.dev_server_manager import get_devserver_manager
|
||||
from ..services.project_config import (
|
||||
clear_dev_command,
|
||||
get_dev_command,
|
||||
get_project_config,
|
||||
set_dev_command,
|
||||
)
|
||||
|
||||
# Add root to path for registry import
|
||||
_root = Path(__file__).parent.parent.parent
|
||||
if str(_root) not in sys.path:
|
||||
sys.path.insert(0, str(_root))
|
||||
|
||||
from registry import get_project_path as registry_get_project_path
|
||||
|
||||
|
||||
def _get_project_path(project_name: str) -> Path | None:
|
||||
"""Get project path from registry."""
|
||||
return registry_get_project_path(project_name)
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/projects/{project_name}/devserver", tags=["devserver"])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Helper Functions
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def validate_project_name(name: str) -> str:
|
||||
"""Validate and sanitize project name to prevent path traversal."""
|
||||
if not re.match(r'^[a-zA-Z0-9_-]{1,50}$', name):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invalid project name"
|
||||
)
|
||||
return name
|
||||
|
||||
|
||||
def get_project_dir(project_name: str) -> Path:
|
||||
"""
|
||||
Get the validated project directory for a project name.
|
||||
|
||||
Args:
|
||||
project_name: Name of the project
|
||||
|
||||
Returns:
|
||||
Path to the project directory
|
||||
|
||||
Raises:
|
||||
HTTPException: If project is not found or directory does not exist
|
||||
"""
|
||||
project_name = validate_project_name(project_name)
|
||||
project_dir = _get_project_path(project_name)
|
||||
|
||||
if not project_dir:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Project '{project_name}' not found in registry"
|
||||
)
|
||||
|
||||
if not project_dir.exists():
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Project directory not found: {project_dir}"
|
||||
)
|
||||
|
||||
return project_dir
|
||||
|
||||
|
||||
def get_project_devserver_manager(project_name: str):
|
||||
"""
|
||||
Get the dev server process manager for a project.
|
||||
|
||||
Args:
|
||||
project_name: Name of the project
|
||||
|
||||
Returns:
|
||||
DevServerProcessManager instance for the project
|
||||
|
||||
Raises:
|
||||
HTTPException: If project is not found or directory does not exist
|
||||
"""
|
||||
project_dir = get_project_dir(project_name)
|
||||
return get_devserver_manager(project_name, project_dir)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Endpoints
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/status", response_model=DevServerStatus)
|
||||
async def get_devserver_status(project_name: str) -> DevServerStatus:
|
||||
"""
|
||||
Get the current status of the dev server for a project.
|
||||
|
||||
Returns information about whether the dev server is running,
|
||||
its process ID, detected URL, and the command used to start it.
|
||||
"""
|
||||
manager = get_project_devserver_manager(project_name)
|
||||
|
||||
# Run healthcheck to detect crashed processes
|
||||
await manager.healthcheck()
|
||||
|
||||
return DevServerStatus(
|
||||
status=manager.status,
|
||||
pid=manager.pid,
|
||||
url=manager.detected_url,
|
||||
command=manager._command,
|
||||
started_at=manager.started_at,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/start", response_model=DevServerActionResponse)
|
||||
async def start_devserver(
|
||||
project_name: str,
|
||||
request: DevServerStartRequest = DevServerStartRequest(),
|
||||
) -> DevServerActionResponse:
|
||||
"""
|
||||
Start the dev server for a project.
|
||||
|
||||
If a custom command is provided in the request, it will be used.
|
||||
Otherwise, the effective command from the project configuration is used.
|
||||
|
||||
Args:
|
||||
project_name: Name of the project
|
||||
request: Optional start request with custom command
|
||||
|
||||
Returns:
|
||||
Response indicating success/failure and current status
|
||||
"""
|
||||
manager = get_project_devserver_manager(project_name)
|
||||
project_dir = get_project_dir(project_name)
|
||||
|
||||
# Determine which command to use
|
||||
command: str | None
|
||||
if request.command:
|
||||
command = request.command
|
||||
else:
|
||||
command = get_dev_command(project_dir)
|
||||
|
||||
if not command:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No dev command available. Configure a custom command or ensure project type can be detected."
|
||||
)
|
||||
|
||||
# Now command is definitely str
|
||||
success, message = await manager.start(command)
|
||||
|
||||
return DevServerActionResponse(
|
||||
success=success,
|
||||
status=manager.status,
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/stop", response_model=DevServerActionResponse)
|
||||
async def stop_devserver(project_name: str) -> DevServerActionResponse:
|
||||
"""
|
||||
Stop the dev server for a project.
|
||||
|
||||
Gracefully terminates the dev server process and all its child processes.
|
||||
|
||||
Args:
|
||||
project_name: Name of the project
|
||||
|
||||
Returns:
|
||||
Response indicating success/failure and current status
|
||||
"""
|
||||
manager = get_project_devserver_manager(project_name)
|
||||
|
||||
success, message = await manager.stop()
|
||||
|
||||
return DevServerActionResponse(
|
||||
success=success,
|
||||
status=manager.status,
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/config", response_model=DevServerConfigResponse)
|
||||
async def get_devserver_config(project_name: str) -> DevServerConfigResponse:
|
||||
"""
|
||||
Get the dev server configuration for a project.
|
||||
|
||||
Returns information about:
|
||||
- detected_type: The auto-detected project type (nodejs-vite, python-django, etc.)
|
||||
- detected_command: The default command for the detected type
|
||||
- custom_command: Any user-configured custom command
|
||||
- effective_command: The command that will actually be used (custom or detected)
|
||||
|
||||
Args:
|
||||
project_name: Name of the project
|
||||
|
||||
Returns:
|
||||
Configuration details for the project's dev server
|
||||
"""
|
||||
project_dir = get_project_dir(project_name)
|
||||
config = get_project_config(project_dir)
|
||||
|
||||
return DevServerConfigResponse(
|
||||
detected_type=config["detected_type"],
|
||||
detected_command=config["detected_command"],
|
||||
custom_command=config["custom_command"],
|
||||
effective_command=config["effective_command"],
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/config", response_model=DevServerConfigResponse)
|
||||
async def update_devserver_config(
|
||||
project_name: str,
|
||||
update: DevServerConfigUpdate,
|
||||
) -> DevServerConfigResponse:
|
||||
"""
|
||||
Update the dev server configuration for a project.
|
||||
|
||||
Set custom_command to a string to override the auto-detected command.
|
||||
Set custom_command to null/None to clear the custom command and revert
|
||||
to using the auto-detected command.
|
||||
|
||||
Args:
|
||||
project_name: Name of the project
|
||||
update: Configuration update containing the new custom_command
|
||||
|
||||
Returns:
|
||||
Updated configuration details for the project's dev server
|
||||
"""
|
||||
project_dir = get_project_dir(project_name)
|
||||
|
||||
# Update the custom command
|
||||
if update.custom_command is None:
|
||||
# Clear the custom command
|
||||
try:
|
||||
clear_dev_command(project_dir)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
else:
|
||||
# Set the custom command
|
||||
try:
|
||||
set_dev_command(project_dir, update.custom_command)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except OSError as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to save configuration: {e}"
|
||||
)
|
||||
|
||||
# Return updated config
|
||||
config = get_project_config(project_dir)
|
||||
|
||||
return DevServerConfigResponse(
|
||||
detected_type=config["detected_type"],
|
||||
detected_command=config["detected_command"],
|
||||
custom_command=config["custom_command"],
|
||||
effective_command=config["effective_command"],
|
||||
)
|
||||
Reference in New Issue
Block a user