From 08c17bcf4b68d3ccc3c6bce1b891626f9a6a425d Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 22 Jan 2026 17:52:51 -0600 Subject: [PATCH 1/7] feat: add per-start port injection for dev server - Add inject_port_into_command() to project_config.py Handles port injection for various project types (Vite, CRA, Django, FastAPI) - Add port field to DevServerStartRequest schema - Inject port in devserver router's start endpoint - Add port selection dialog to DevServerControl UI - Update API client to pass port parameter This enables per-start port configuration. Persistent port config (get_dev_port, set_dev_port, config schema updates) to be added in follow-up. Co-Authored-By: Claude Opus 4.5 --- server/routers/devserver.py | 7 +++ server/schemas.py | 1 + server/services/project_config.py | 42 +++++++++++++++ ui/src/components/DevServerControl.tsx | 74 +++++++++++++++++++++++--- ui/src/lib/api.ts | 5 +- 5 files changed, 120 insertions(+), 9 deletions(-) diff --git a/server/routers/devserver.py b/server/routers/devserver.py index 673bc3ed..aacb5a5a 100644 --- a/server/routers/devserver.py +++ b/server/routers/devserver.py @@ -22,8 +22,10 @@ from ..services.dev_server_manager import get_devserver_manager from ..services.project_config import ( clear_dev_command, + detect_project_type, get_dev_command, get_project_config, + inject_port_into_command, set_dev_command, ) @@ -167,6 +169,11 @@ async def start_devserver( detail="No dev command available. Configure a custom command or ensure project type can be detected." ) + # Inject port into command if specified + if request.port is not None: + project_type = detect_project_type(project_dir) + command = inject_port_into_command(command, request.port, project_type) + # Now command is definitely str success, message = await manager.start(command) diff --git a/server/schemas.py b/server/schemas.py index 844aaa11..6ac69712 100644 --- a/server/schemas.py +++ b/server/schemas.py @@ -420,6 +420,7 @@ def validate_testing_ratio(cls, v: int | None) -> int | None: class DevServerStartRequest(BaseModel): """Request schema for starting the dev server.""" command: str | None = None # If None, uses effective command from config + port: int | None = None # If provided, injects port into the command class DevServerStatus(BaseModel): diff --git a/server/services/project_config.py b/server/services/project_config.py index f6e50d07..4a651310 100644 --- a/server/services/project_config.py +++ b/server/services/project_config.py @@ -351,6 +351,48 @@ def get_dev_command(project_dir: Path) -> str | None: return get_default_dev_command(project_dir) +def inject_port_into_command(command: str, port: int, project_type: str | None) -> str: + """ + Inject a port number into a dev command based on project type. + + Different project types require different syntax for specifying ports: + - nodejs-vite: Append `-- --port {port}` to pass through npm scripts + - nodejs-cra: Prepend `PORT={port}` as environment variable + - python-django: Replace `runserver` with `runserver 0.0.0.0:{port}` + - python-fastapi/python-poetry: Append `--port {port}` for uvicorn + - rust/go: No modification (no standard port flag) + + Args: + command: The base dev command to modify. + port: The port number to inject. + project_type: The detected project type, or None if unknown. + + Returns: + The command with port injection applied, or the original command + if no port injection is applicable. + """ + if project_type == "nodejs-vite": + # npm run dev -- --port 4000 + return f"{command} -- --port {port}" + + if project_type == "nodejs-cra": + # PORT=4000 npm start (works cross-platform with npm) + return f"PORT={port} {command}" + + if project_type == "python-django": + # python manage.py runserver 0.0.0.0:4000 + if "runserver" in command: + return command.replace("runserver", f"runserver 0.0.0.0:{port}") + return command + + if project_type in ("python-fastapi", "python-poetry"): + # uvicorn commands: append --port flag + return f"{command} --port {port}" + + # rust, go, or unknown: return unmodified + return command + + def set_dev_command(project_dir: Path, command: str) -> None: """ Save a custom dev command for a project. diff --git a/ui/src/components/DevServerControl.tsx b/ui/src/components/DevServerControl.tsx index a6182c57..2f0c9e1d 100644 --- a/ui/src/components/DevServerControl.tsx +++ b/ui/src/components/DevServerControl.tsx @@ -1,4 +1,5 @@ -import { Globe, Square, Loader2, ExternalLink, AlertTriangle } from 'lucide-react' +import { useState } from 'react' +import { Globe, Square, Loader2, ExternalLink, AlertTriangle, X } from 'lucide-react' import { useMutation, useQueryClient } from '@tanstack/react-query' import type { DevServerStatus } from '../lib/types' import { startDevServer, stopDevServer } from '../lib/api' @@ -18,7 +19,7 @@ function useStartDevServer(projectName: string) { const queryClient = useQueryClient() return useMutation({ - mutationFn: () => startDevServer(projectName), + mutationFn: (port?: number) => startDevServer(projectName, undefined, port), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['dev-server-status', projectName] }) }, @@ -60,16 +61,28 @@ interface DevServerControlProps { * - Uses neobrutalism design with cyan accent when running */ export function DevServerControl({ projectName, status, url }: DevServerControlProps) { + const [showPortDialog, setShowPortDialog] = useState(false) + const [portValue, setPortValue] = useState(3000) const startDevServerMutation = useStartDevServer(projectName) const stopDevServerMutation = useStopDevServer(projectName) const isLoading = startDevServerMutation.isPending || stopDevServerMutation.isPending - const handleStart = () => { - // Clear any previous errors before starting + const handleStartClick = () => { + // Clear any previous errors before showing dialog stopDevServerMutation.reset() - startDevServerMutation.mutate() + setShowPortDialog(true) } + + const handleConfirmStart = () => { + setShowPortDialog(false) + startDevServerMutation.mutate(portValue) + } + + const handleCancelStart = () => { + setShowPortDialog(false) + } + const handleStop = () => { // Clear any previous errors before stopping startDevServerMutation.reset() @@ -84,10 +97,10 @@ export function DevServerControl({ projectName, status, url }: DevServerControlP const isCrashed = status === 'crashed' return ( -
+
{isStopped ? ( +
+ setPortValue(parseInt(e.target.value) || 3000)} + className="w-full px-2 py-1 text-sm font-mono border-2 border-[var(--color-neo-border)] rounded mb-2" + min={1} + max={65535} + autoFocus + onKeyDown={(e) => { + if (e.key === 'Enter') handleConfirmStart() + if (e.key === 'Escape') handleCancelStart() + }} + /> +
+ + +
+
+ )} + {/* Show URL as clickable link when server is running */} {isRunning && url && ( { return fetchJSON(`/projects/${encodeURIComponent(projectName)}/devserver/start`, { method: 'POST', - body: JSON.stringify({ command }), + body: JSON.stringify({ command, port }), }) } From 27f569b850ce8e497e9b3890790dc7d1efda2454 Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 22 Jan 2026 17:56:33 -0600 Subject: [PATCH 2/7] feat: add get_dev_port function to project_config Add get_dev_port(project_dir: Path) -> int | None function following the existing get_dev_command pattern. Returns the configured dev port from the project config file, or None if not configured. Co-Authored-By: Claude Opus 4.5 --- server/services/project_config.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/server/services/project_config.py b/server/services/project_config.py index 4a651310..b03cba2e 100644 --- a/server/services/project_config.py +++ b/server/services/project_config.py @@ -418,6 +418,26 @@ def set_dev_command(project_dir: Path, command: str) -> None: logger.info("Set custom dev command for %s: %s", project_dir.name, command) +def get_dev_port(project_dir: Path) -> int | None: + """ + Get the configured dev port for a project. + + Args: + project_dir: Path to the project directory. + + Returns: + The configured port number, or None if no port is configured. + """ + project_dir = Path(project_dir).resolve() + config = _load_config(project_dir) + port = config.get("dev_port") + + if port is not None and isinstance(port, int): + return port + + return None + + def clear_dev_command(project_dir: Path) -> None: """ Remove the custom dev command, reverting to auto-detection. From 6af76ada86e9e1678ab92dd9b60fdb02d99645a3 Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 22 Jan 2026 17:57:41 -0600 Subject: [PATCH 3/7] feat: add port field to DevServerConfigResponse schema Add port: int | None = None field to return configured port to the UI. This completes the schema needed for port configuration feature. Co-Authored-By: Claude Opus 4.5 --- server/schemas.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server/schemas.py b/server/schemas.py index 6ac69712..cd5d0b0a 100644 --- a/server/schemas.py +++ b/server/schemas.py @@ -445,6 +445,7 @@ class DevServerConfigResponse(BaseModel): detected_command: str | None = None custom_command: str | None = None effective_command: str | None = None + port: int | None = None class DevServerConfigUpdate(BaseModel): From b4e08f0e6b84a8ba13808348e7d4012ad0aba27f Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 22 Jan 2026 17:58:42 -0600 Subject: [PATCH 4/7] feat: add port field to DevServerConfigUpdate schema - Allows UI to update the configured port via PATCH endpoint - Follows existing custom_command pattern with optional None value Co-Authored-By: Claude Opus 4.5 --- server/schemas.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server/schemas.py b/server/schemas.py index cd5d0b0a..6ce9fa22 100644 --- a/server/schemas.py +++ b/server/schemas.py @@ -451,6 +451,7 @@ class DevServerConfigResponse(BaseModel): class DevServerConfigUpdate(BaseModel): """Request schema for updating dev server configuration.""" custom_command: str | None = None # None clears the custom command + port: int | None = None # None clears the configured port # ============================================================================ From c9c5fc6be7f4fdbcb23a458cd1c18d9ffd7a04e5 Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 22 Jan 2026 17:59:48 -0600 Subject: [PATCH 5/7] feat: add set_dev_port function for persistent port configuration - Follow existing set_dev_command pattern for consistency - Validate port range (1-65535) with descriptive ValueError - Use _validate_project_dir, _load_config, _save_config helpers - Add function after get_dev_port as specified Co-Authored-By: Claude Opus 4.5 --- server/services/project_config.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/server/services/project_config.py b/server/services/project_config.py index b03cba2e..3551d809 100644 --- a/server/services/project_config.py +++ b/server/services/project_config.py @@ -438,6 +438,32 @@ def get_dev_port(project_dir: Path) -> int | None: return None +def set_dev_port(project_dir: Path, port: int) -> None: + """ + Save a custom dev port for a project. + + Args: + project_dir: Path to the project directory. + port: The port number to save (must be 1-65535). + + Raises: + ValueError: If port is not a valid integer in range 1-65535, + or if project_dir is invalid. + OSError: If the config file cannot be written. + """ + if not isinstance(port, int) or port < 1 or port > 65535: + raise ValueError("Port must be an integer between 1 and 65535") + + project_dir = _validate_project_dir(project_dir) + + # Load existing config and update + config = _load_config(project_dir) + config["dev_port"] = port + + _save_config(project_dir, config) + logger.info("Set custom dev port for %s: %d", project_dir.name, port) + + def clear_dev_command(project_dir: Path) -> None: """ Remove the custom dev command, reverting to auto-detection. From 2810b50547e5f8500645031a94b853dc89c799d4 Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 22 Jan 2026 18:00:59 -0600 Subject: [PATCH 6/7] feat: add port to devserver GET config response - Import get_dev_port from project_config module - Call get_dev_port(project_dir) to retrieve configured port - Include port field in DevServerConfigResponse - Update docstring to document the new port field Co-Authored-By: Claude Opus 4.5 --- server/routers/devserver.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/routers/devserver.py b/server/routers/devserver.py index aacb5a5a..2895e15a 100644 --- a/server/routers/devserver.py +++ b/server/routers/devserver.py @@ -24,6 +24,7 @@ clear_dev_command, detect_project_type, get_dev_command, + get_dev_port, get_project_config, inject_port_into_command, set_dev_command, @@ -218,6 +219,7 @@ async def get_devserver_config(project_name: str) -> DevServerConfigResponse: - 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) + - port: The configured port number (if set) Args: project_name: Name of the project @@ -227,12 +229,14 @@ async def get_devserver_config(project_name: str) -> DevServerConfigResponse: """ project_dir = get_project_dir(project_name) config = get_project_config(project_dir) + configured_port = get_dev_port(project_dir) return DevServerConfigResponse( detected_type=config["detected_type"], detected_command=config["detected_command"], custom_command=config["custom_command"], effective_command=config["effective_command"], + port=configured_port, ) From 642735f0399982c63e60b2a6739f31487006c973 Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 22 Jan 2026 18:02:30 -0600 Subject: [PATCH 7/7] feat: add port handling to PATCH /devserver/config endpoint - Import set_dev_port from project_config service - Handle update.port in update_devserver_config function - Validate port with ValueError handling for invalid values - Include port field in DevServerConfigResponse Co-Authored-By: Claude Opus 4.5 --- server/routers/devserver.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/server/routers/devserver.py b/server/routers/devserver.py index 2895e15a..b6429a23 100644 --- a/server/routers/devserver.py +++ b/server/routers/devserver.py @@ -28,6 +28,7 @@ get_project_config, inject_port_into_command, set_dev_command, + set_dev_port, ) # Add root to path for registry import @@ -252,9 +253,12 @@ async def update_devserver_config( Set custom_command to null/None to clear the custom command and revert to using the auto-detected command. + Set port to an integer to configure a custom dev port. + Set port to null/None to clear the configured port. + Args: project_name: Name of the project - update: Configuration update containing the new custom_command + update: Configuration update containing the new custom_command and/or port Returns: Updated configuration details for the project's dev server @@ -280,12 +284,26 @@ async def update_devserver_config( detail=f"Failed to save configuration: {e}" ) + # Update the port if provided + if update.port is not None: + try: + set_dev_port(project_dir, update.port) + 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) + configured_port = get_dev_port(project_dir) return DevServerConfigResponse( detected_type=config["detected_type"], detected_command=config["detected_command"], custom_command=config["custom_command"], effective_command=config["effective_command"], + port=configured_port, )