pdd-cli 0.0.90__py3-none-any.whl → 0.0.118__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pdd/__init__.py +38 -6
- pdd/agentic_bug.py +323 -0
- pdd/agentic_bug_orchestrator.py +497 -0
- pdd/agentic_change.py +231 -0
- pdd/agentic_change_orchestrator.py +526 -0
- pdd/agentic_common.py +521 -786
- pdd/agentic_e2e_fix.py +319 -0
- pdd/agentic_e2e_fix_orchestrator.py +426 -0
- pdd/agentic_fix.py +118 -3
- pdd/agentic_update.py +25 -8
- pdd/architecture_sync.py +565 -0
- pdd/auth_service.py +210 -0
- pdd/auto_deps_main.py +63 -53
- pdd/auto_include.py +185 -3
- pdd/auto_update.py +125 -47
- pdd/bug_main.py +195 -23
- pdd/cmd_test_main.py +345 -197
- pdd/code_generator.py +4 -2
- pdd/code_generator_main.py +118 -32
- pdd/commands/__init__.py +6 -0
- pdd/commands/analysis.py +87 -29
- pdd/commands/auth.py +309 -0
- pdd/commands/connect.py +290 -0
- pdd/commands/fix.py +136 -113
- pdd/commands/maintenance.py +3 -2
- pdd/commands/misc.py +8 -0
- pdd/commands/modify.py +190 -164
- pdd/commands/sessions.py +284 -0
- pdd/construct_paths.py +334 -32
- pdd/context_generator_main.py +167 -170
- pdd/continue_generation.py +6 -3
- pdd/core/__init__.py +33 -0
- pdd/core/cli.py +27 -3
- pdd/core/cloud.py +237 -0
- pdd/core/errors.py +4 -0
- pdd/core/remote_session.py +61 -0
- pdd/crash_main.py +219 -23
- pdd/data/llm_model.csv +4 -4
- pdd/docs/prompting_guide.md +864 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
- pdd/fix_code_loop.py +208 -34
- pdd/fix_code_module_errors.py +6 -2
- pdd/fix_error_loop.py +291 -38
- pdd/fix_main.py +204 -4
- pdd/fix_verification_errors_loop.py +235 -26
- pdd/fix_verification_main.py +269 -83
- pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
- pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
- pdd/frontend/dist/index.html +376 -0
- pdd/frontend/dist/logo.svg +33 -0
- pdd/generate_output_paths.py +46 -5
- pdd/generate_test.py +212 -151
- pdd/get_comment.py +19 -44
- pdd/get_extension.py +8 -9
- pdd/get_jwt_token.py +309 -20
- pdd/get_language.py +8 -7
- pdd/get_run_command.py +7 -5
- pdd/insert_includes.py +2 -1
- pdd/llm_invoke.py +459 -95
- pdd/load_prompt_template.py +15 -34
- pdd/path_resolution.py +140 -0
- pdd/postprocess.py +4 -1
- pdd/preprocess.py +68 -12
- pdd/preprocess_main.py +33 -1
- pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
- pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
- pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
- pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
- pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
- pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
- pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
- pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
- pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
- pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
- pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
- pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
- pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
- pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
- pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
- pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
- pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
- pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
- pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
- pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
- pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
- pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
- pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
- pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
- pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
- pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
- pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
- pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
- pdd/prompts/agentic_update_LLM.prompt +192 -338
- pdd/prompts/auto_include_LLM.prompt +22 -0
- pdd/prompts/change_LLM.prompt +3093 -1
- pdd/prompts/detect_change_LLM.prompt +571 -14
- pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
- pdd/prompts/generate_test_LLM.prompt +20 -1
- pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
- pdd/prompts/insert_includes_LLM.prompt +262 -252
- pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
- pdd/prompts/prompt_diff_LLM.prompt +82 -0
- pdd/remote_session.py +876 -0
- pdd/server/__init__.py +52 -0
- pdd/server/app.py +335 -0
- pdd/server/click_executor.py +587 -0
- pdd/server/executor.py +338 -0
- pdd/server/jobs.py +661 -0
- pdd/server/models.py +241 -0
- pdd/server/routes/__init__.py +31 -0
- pdd/server/routes/architecture.py +451 -0
- pdd/server/routes/auth.py +364 -0
- pdd/server/routes/commands.py +929 -0
- pdd/server/routes/config.py +42 -0
- pdd/server/routes/files.py +603 -0
- pdd/server/routes/prompts.py +1322 -0
- pdd/server/routes/websocket.py +473 -0
- pdd/server/security.py +243 -0
- pdd/server/terminal_spawner.py +209 -0
- pdd/server/token_counter.py +222 -0
- pdd/summarize_directory.py +236 -237
- pdd/sync_animation.py +8 -4
- pdd/sync_determine_operation.py +329 -47
- pdd/sync_main.py +272 -28
- pdd/sync_orchestration.py +136 -75
- pdd/template_expander.py +161 -0
- pdd/templates/architecture/architecture_json.prompt +41 -46
- pdd/trace.py +1 -1
- pdd/track_cost.py +0 -13
- pdd/unfinished_prompt.py +2 -1
- pdd/update_main.py +23 -5
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +15 -10
- pdd_cli-0.0.118.dist-info/RECORD +227 -0
- pdd_cli-0.0.90.dist-info/RECORD +0 -153
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
pdd/server/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PDD Server Package.
|
|
3
|
+
|
|
4
|
+
This package provides the REST API server, job management, and command execution
|
|
5
|
+
infrastructure for the PDD tool. It enables the web frontend to interact with
|
|
6
|
+
the local project environment securely.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .app import create_app, run_server
|
|
12
|
+
from .executor import execute_pdd_command
|
|
13
|
+
from .jobs import Job, JobManager
|
|
14
|
+
from .models import ServerConfig, ServerStatus
|
|
15
|
+
from .routes.websocket import ConnectionManager
|
|
16
|
+
from .security import PathValidator, SecurityError
|
|
17
|
+
|
|
18
|
+
# Global Constants
|
|
19
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
20
|
+
DEFAULT_PORT = 9876
|
|
21
|
+
API_VERSION = "v1"
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
# App
|
|
27
|
+
"create_app",
|
|
28
|
+
"run_server",
|
|
29
|
+
|
|
30
|
+
# Models
|
|
31
|
+
"ServerConfig",
|
|
32
|
+
"ServerStatus",
|
|
33
|
+
|
|
34
|
+
# Jobs
|
|
35
|
+
"Job",
|
|
36
|
+
"JobManager",
|
|
37
|
+
|
|
38
|
+
# Security
|
|
39
|
+
"PathValidator",
|
|
40
|
+
"SecurityError",
|
|
41
|
+
|
|
42
|
+
# Websockets
|
|
43
|
+
"ConnectionManager",
|
|
44
|
+
|
|
45
|
+
# Executor
|
|
46
|
+
"execute_pdd_command",
|
|
47
|
+
|
|
48
|
+
# Constants
|
|
49
|
+
"DEFAULT_HOST",
|
|
50
|
+
"DEFAULT_PORT",
|
|
51
|
+
"API_VERSION",
|
|
52
|
+
]
|
pdd/server/app.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, List, Union
|
|
8
|
+
|
|
9
|
+
import uvicorn
|
|
10
|
+
from fastapi import FastAPI, Request, status
|
|
11
|
+
from fastapi.responses import JSONResponse, FileResponse, HTMLResponse
|
|
12
|
+
from fastapi.exceptions import RequestValidationError
|
|
13
|
+
from fastapi.staticfiles import StaticFiles
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from .models import ServerStatus, ServerConfig
|
|
17
|
+
from .security import (
|
|
18
|
+
PathValidator,
|
|
19
|
+
SecurityError,
|
|
20
|
+
configure_cors,
|
|
21
|
+
SecurityLoggingMiddleware,
|
|
22
|
+
)
|
|
23
|
+
from .jobs import JobManager
|
|
24
|
+
from .routes.websocket import ConnectionManager, create_websocket_routes
|
|
25
|
+
from .routes import architecture, auth, files, commands, prompts
|
|
26
|
+
from .routes import websocket as ws_routes
|
|
27
|
+
from .routes.config import router as config_router
|
|
28
|
+
|
|
29
|
+
# Initialize Rich console
|
|
30
|
+
console = Console()
|
|
31
|
+
|
|
32
|
+
# ============================================================================
|
|
33
|
+
# Application State
|
|
34
|
+
# ============================================================================
|
|
35
|
+
|
|
36
|
+
class AppState:
|
|
37
|
+
"""
|
|
38
|
+
Application state container for dependency injection.
|
|
39
|
+
Holds thread-safe references to shared managers and configuration.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, project_root: Path, config: Optional[ServerConfig] = None):
|
|
43
|
+
self.project_root = project_root.resolve()
|
|
44
|
+
self.start_time = datetime.now(timezone.utc)
|
|
45
|
+
self.version = "0.1.0" # In a real app, load from package metadata
|
|
46
|
+
|
|
47
|
+
# Store server config for port access
|
|
48
|
+
self.config = config or ServerConfig()
|
|
49
|
+
|
|
50
|
+
# Initialize managers
|
|
51
|
+
self.path_validator = PathValidator(self.project_root)
|
|
52
|
+
# SAFETY: Limit concurrent jobs to 3 - LLM calls are resource-intensive
|
|
53
|
+
# Running too many in parallel can exhaust memory/CPU and crash the system
|
|
54
|
+
self.job_manager = JobManager(max_concurrent=3, project_root=self.project_root)
|
|
55
|
+
self.connection_manager = ConnectionManager()
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def server_port(self) -> int:
|
|
59
|
+
"""Get the configured server port."""
|
|
60
|
+
return self.config.port
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def uptime_seconds(self) -> float:
|
|
64
|
+
return (datetime.now(timezone.utc) - self.start_time).total_seconds()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Global state instance (set during app creation)
|
|
68
|
+
_app_state: Optional[AppState] = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_app_state() -> AppState:
|
|
72
|
+
"""Dependency to get the global application state."""
|
|
73
|
+
if _app_state is None:
|
|
74
|
+
raise RuntimeError("Application state not initialized. Call create_app() first.")
|
|
75
|
+
return _app_state
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_path_validator() -> PathValidator:
|
|
79
|
+
"""Dependency to get the path validator."""
|
|
80
|
+
return get_app_state().path_validator
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_job_manager() -> JobManager:
|
|
84
|
+
"""Dependency to get the job manager."""
|
|
85
|
+
return get_app_state().job_manager
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_connection_manager() -> ConnectionManager:
|
|
89
|
+
"""Dependency to get the WebSocket connection manager."""
|
|
90
|
+
return get_app_state().connection_manager
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_server_port() -> int:
|
|
94
|
+
"""Dependency to get the configured server port."""
|
|
95
|
+
return get_app_state().server_port
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ============================================================================
|
|
99
|
+
# Exception Handlers
|
|
100
|
+
# ============================================================================
|
|
101
|
+
|
|
102
|
+
async def security_exception_handler(request: Request, exc: SecurityError):
|
|
103
|
+
"""Handle security violations (403)."""
|
|
104
|
+
# Log the full error with code for server-side debugging
|
|
105
|
+
console.print(f"[bold red]Security Violation:[/bold red] {exc.message} ({exc.code})")
|
|
106
|
+
|
|
107
|
+
# Return only the message to the client to match expected log output behavior
|
|
108
|
+
return JSONResponse(
|
|
109
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
110
|
+
content={"detail": exc.message},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
|
115
|
+
"""Handle Pydantic validation errors (422)."""
|
|
116
|
+
return JSONResponse(
|
|
117
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
118
|
+
content={"detail": exc.errors(), "body": str(exc.body)},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def generic_exception_handler(request: Request, exc: Exception):
|
|
123
|
+
"""Handle unexpected exceptions (500)."""
|
|
124
|
+
console.print(f"[bold red]Server Error:[/bold red] {str(exc)}")
|
|
125
|
+
return JSONResponse(
|
|
126
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
127
|
+
content={"detail": "Internal server error"},
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ============================================================================
|
|
132
|
+
# App Factory & Lifespan
|
|
133
|
+
# ============================================================================
|
|
134
|
+
|
|
135
|
+
@asynccontextmanager
|
|
136
|
+
async def lifespan(app: FastAPI):
|
|
137
|
+
"""
|
|
138
|
+
Application lifespan manager.
|
|
139
|
+
Handles startup initialization and shutdown cleanup.
|
|
140
|
+
"""
|
|
141
|
+
state = get_app_state()
|
|
142
|
+
|
|
143
|
+
# Startup
|
|
144
|
+
console.print(f"[green]PDD Server starting...[/green]")
|
|
145
|
+
console.print(f"Project Root: [bold]{state.project_root}[/bold]")
|
|
146
|
+
|
|
147
|
+
# Start remote session heartbeat and command polling if configured
|
|
148
|
+
from ..remote_session import get_active_session_manager
|
|
149
|
+
session_manager = get_active_session_manager()
|
|
150
|
+
if session_manager:
|
|
151
|
+
session_manager.start_heartbeat()
|
|
152
|
+
session_manager.start_command_polling()
|
|
153
|
+
console.print("[dim]Remote session heartbeat and command polling started[/dim]")
|
|
154
|
+
|
|
155
|
+
yield
|
|
156
|
+
|
|
157
|
+
# Shutdown
|
|
158
|
+
console.print("[yellow]Shutting down PDD Server...[/yellow]")
|
|
159
|
+
|
|
160
|
+
# Stop remote session heartbeat and command polling
|
|
161
|
+
if session_manager:
|
|
162
|
+
try:
|
|
163
|
+
await session_manager.stop_heartbeat()
|
|
164
|
+
await session_manager.stop_command_polling()
|
|
165
|
+
console.print("[dim]Remote session heartbeat and command polling stopped[/dim]")
|
|
166
|
+
except Exception as e:
|
|
167
|
+
console.print(f"[yellow]Warning: Error stopping remote session tasks: {e}[/yellow]")
|
|
168
|
+
|
|
169
|
+
# Cancel active jobs
|
|
170
|
+
try:
|
|
171
|
+
active_jobs = state.job_manager.get_active_jobs()
|
|
172
|
+
if active_jobs:
|
|
173
|
+
console.print(f"Cancelling {len(active_jobs)} active jobs...")
|
|
174
|
+
await state.job_manager.shutdown()
|
|
175
|
+
except Exception as e:
|
|
176
|
+
console.print(f"[red]Error during job manager shutdown: {e}[/red]")
|
|
177
|
+
|
|
178
|
+
console.print("[green]Shutdown complete.[/green]")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def create_app(
|
|
182
|
+
project_root: Path,
|
|
183
|
+
config: Optional[ServerConfig] = None,
|
|
184
|
+
allowed_origins: Optional[List[str]] = None
|
|
185
|
+
) -> FastAPI:
|
|
186
|
+
"""
|
|
187
|
+
Create and configure the FastAPI application.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
project_root: The project directory to serve.
|
|
191
|
+
config: Server configuration object (preferred).
|
|
192
|
+
allowed_origins: List of allowed CORS origins (legacy/fallback).
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Configured FastAPI application.
|
|
196
|
+
"""
|
|
197
|
+
global _app_state
|
|
198
|
+
_app_state = AppState(project_root, config=config)
|
|
199
|
+
|
|
200
|
+
# Determine configuration with proper fallback
|
|
201
|
+
origins = None
|
|
202
|
+
if config:
|
|
203
|
+
origins = config.allowed_origins
|
|
204
|
+
|
|
205
|
+
if origins is None:
|
|
206
|
+
origins = allowed_origins
|
|
207
|
+
|
|
208
|
+
app = FastAPI(
|
|
209
|
+
title="PDD Server",
|
|
210
|
+
description="Local REST server for Prompt Driven Development (PDD) web frontend",
|
|
211
|
+
version=_app_state.version,
|
|
212
|
+
lifespan=lifespan,
|
|
213
|
+
docs_url="/docs",
|
|
214
|
+
redoc_url="/redoc",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# 1. Configure Middleware
|
|
218
|
+
app.add_middleware(SecurityLoggingMiddleware)
|
|
219
|
+
configure_cors(app, origins)
|
|
220
|
+
|
|
221
|
+
# 2. Register Exception Handlers
|
|
222
|
+
app.add_exception_handler(SecurityError, security_exception_handler)
|
|
223
|
+
app.add_exception_handler(RequestValidationError, validation_exception_handler)
|
|
224
|
+
app.add_exception_handler(Exception, generic_exception_handler)
|
|
225
|
+
|
|
226
|
+
# 3. Register Routes
|
|
227
|
+
@app.get("/api/v1/status", response_model=ServerStatus, tags=["status"])
|
|
228
|
+
async def get_status():
|
|
229
|
+
state = get_app_state()
|
|
230
|
+
return ServerStatus(
|
|
231
|
+
version=state.version,
|
|
232
|
+
project_root=str(state.project_root),
|
|
233
|
+
uptime_seconds=state.uptime_seconds,
|
|
234
|
+
active_jobs=len(state.job_manager.get_active_jobs()),
|
|
235
|
+
connected_clients=len(state.connection_manager.active_connections),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
app.dependency_overrides[files.get_path_validator] = get_path_validator
|
|
239
|
+
app.dependency_overrides[commands.get_job_manager] = get_job_manager
|
|
240
|
+
app.dependency_overrides[commands.get_project_root] = lambda: get_app_state().project_root
|
|
241
|
+
app.dependency_overrides[commands.get_server_port] = get_server_port
|
|
242
|
+
app.dependency_overrides[ws_routes.get_job_manager] = get_job_manager
|
|
243
|
+
app.dependency_overrides[ws_routes.get_project_root] = lambda: get_app_state().project_root
|
|
244
|
+
app.dependency_overrides[prompts.get_path_validator] = get_path_validator
|
|
245
|
+
|
|
246
|
+
app.include_router(architecture.router)
|
|
247
|
+
app.include_router(auth.router)
|
|
248
|
+
app.include_router(config_router)
|
|
249
|
+
app.include_router(files.router)
|
|
250
|
+
app.include_router(commands.router)
|
|
251
|
+
app.include_router(prompts.router)
|
|
252
|
+
|
|
253
|
+
create_websocket_routes(app, _app_state.connection_manager, _app_state.job_manager)
|
|
254
|
+
|
|
255
|
+
# 4. Serve Frontend Static Files
|
|
256
|
+
# Look for frontend dist in the pdd package directory
|
|
257
|
+
frontend_dist = Path(__file__).parent.parent / "frontend" / "dist"
|
|
258
|
+
if frontend_dist.exists():
|
|
259
|
+
console.print(f"[green]Serving frontend from:[/green] {frontend_dist}")
|
|
260
|
+
|
|
261
|
+
# Serve static assets (JS, CSS, etc.)
|
|
262
|
+
app.mount("/assets", StaticFiles(directory=frontend_dist / "assets"), name="assets")
|
|
263
|
+
|
|
264
|
+
# Serve index.html for the root and any non-API routes (SPA fallback)
|
|
265
|
+
@app.get("/", response_class=HTMLResponse)
|
|
266
|
+
async def serve_frontend():
|
|
267
|
+
index_file = frontend_dist / "index.html"
|
|
268
|
+
if index_file.exists():
|
|
269
|
+
return FileResponse(index_file)
|
|
270
|
+
return HTMLResponse("<h1>Frontend not found</h1>", status_code=404)
|
|
271
|
+
|
|
272
|
+
# Catch-all for SPA routing (must be last)
|
|
273
|
+
@app.get("/{path:path}")
|
|
274
|
+
async def serve_spa_fallback(path: str):
|
|
275
|
+
# Don't intercept API, docs, or WebSocket routes
|
|
276
|
+
if path.startswith(("api/", "docs", "redoc", "openapi.json", "ws/")):
|
|
277
|
+
return JSONResponse({"detail": "Not found"}, status_code=404)
|
|
278
|
+
|
|
279
|
+
# Try to serve the file directly first
|
|
280
|
+
file_path = frontend_dist / path
|
|
281
|
+
if file_path.exists() and file_path.is_file():
|
|
282
|
+
return FileResponse(file_path)
|
|
283
|
+
|
|
284
|
+
# Fall back to index.html for SPA routing
|
|
285
|
+
index_file = frontend_dist / "index.html"
|
|
286
|
+
if index_file.exists():
|
|
287
|
+
return FileResponse(index_file)
|
|
288
|
+
return JSONResponse({"detail": "Not found"}, status_code=404)
|
|
289
|
+
else:
|
|
290
|
+
console.print(f"[yellow]Frontend not found at {frontend_dist}[/yellow]")
|
|
291
|
+
console.print("[yellow]Run 'npm run build' in pdd/frontend to build the frontend[/yellow]")
|
|
292
|
+
|
|
293
|
+
return app
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ============================================================================
|
|
297
|
+
# Server Runner
|
|
298
|
+
# ============================================================================
|
|
299
|
+
|
|
300
|
+
def run_server(
|
|
301
|
+
project_root: Optional[Path] = None,
|
|
302
|
+
host: str = "127.0.0.1",
|
|
303
|
+
port: int = 9876,
|
|
304
|
+
log_level: str = "info",
|
|
305
|
+
allowed_origins: Optional[List[str]] = None,
|
|
306
|
+
app: Optional[FastAPI] = None,
|
|
307
|
+
config: Optional[ServerConfig] = None
|
|
308
|
+
) -> None:
|
|
309
|
+
"""
|
|
310
|
+
Run the PDD server using Uvicorn.
|
|
311
|
+
"""
|
|
312
|
+
if config:
|
|
313
|
+
final_host = config.host
|
|
314
|
+
final_port = config.port
|
|
315
|
+
final_log_level = config.log_level
|
|
316
|
+
else:
|
|
317
|
+
final_host = host
|
|
318
|
+
final_port = port
|
|
319
|
+
final_log_level = log_level
|
|
320
|
+
|
|
321
|
+
if app is None:
|
|
322
|
+
if project_root is None:
|
|
323
|
+
raise ValueError("Must provide either 'app' or 'project_root'.")
|
|
324
|
+
app = create_app(project_root, config=config, allowed_origins=allowed_origins)
|
|
325
|
+
|
|
326
|
+
console.print(f"[bold green]PDD Server running on http://{final_host}:{final_port}[/bold green]")
|
|
327
|
+
console.print(f"API Documentation: http://{final_host}:{final_port}/docs")
|
|
328
|
+
|
|
329
|
+
uvicorn.run(
|
|
330
|
+
app,
|
|
331
|
+
host=final_host,
|
|
332
|
+
port=final_port,
|
|
333
|
+
log_level=final_log_level,
|
|
334
|
+
access_log=False,
|
|
335
|
+
)
|