pdd-cli 0.0.45__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 +40 -8
- 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 +598 -0
- pdd/agentic_crash.py +534 -0
- pdd/agentic_e2e_fix.py +319 -0
- pdd/agentic_e2e_fix_orchestrator.py +426 -0
- pdd/agentic_fix.py +1294 -0
- pdd/agentic_langtest.py +162 -0
- pdd/agentic_update.py +387 -0
- pdd/agentic_verify.py +183 -0
- pdd/architecture_sync.py +565 -0
- pdd/auth_service.py +210 -0
- pdd/auto_deps_main.py +71 -51
- pdd/auto_include.py +245 -5
- pdd/auto_update.py +125 -47
- pdd/bug_main.py +196 -23
- pdd/bug_to_unit_test.py +2 -0
- pdd/change_main.py +11 -4
- pdd/cli.py +22 -1181
- pdd/cmd_test_main.py +350 -150
- pdd/code_generator.py +60 -18
- pdd/code_generator_main.py +790 -57
- pdd/commands/__init__.py +48 -0
- pdd/commands/analysis.py +306 -0
- pdd/commands/auth.py +309 -0
- pdd/commands/connect.py +290 -0
- pdd/commands/fix.py +163 -0
- pdd/commands/generate.py +257 -0
- pdd/commands/maintenance.py +175 -0
- pdd/commands/misc.py +87 -0
- pdd/commands/modify.py +256 -0
- pdd/commands/report.py +144 -0
- pdd/commands/sessions.py +284 -0
- pdd/commands/templates.py +215 -0
- pdd/commands/utility.py +110 -0
- pdd/config_resolution.py +58 -0
- pdd/conflicts_main.py +8 -3
- pdd/construct_paths.py +589 -111
- pdd/context_generator.py +10 -2
- pdd/context_generator_main.py +175 -76
- pdd/continue_generation.py +53 -10
- pdd/core/__init__.py +33 -0
- pdd/core/cli.py +527 -0
- pdd/core/cloud.py +237 -0
- pdd/core/dump.py +554 -0
- pdd/core/errors.py +67 -0
- pdd/core/remote_session.py +61 -0
- pdd/core/utils.py +90 -0
- pdd/crash_main.py +262 -33
- pdd/data/language_format.csv +71 -63
- pdd/data/llm_model.csv +20 -18
- pdd/detect_change_main.py +5 -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 +523 -95
- pdd/fix_code_module_errors.py +6 -2
- pdd/fix_error_loop.py +491 -92
- pdd/fix_errors_from_unit_tests.py +4 -3
- pdd/fix_main.py +278 -21
- pdd/fix_verification_errors.py +12 -100
- pdd/fix_verification_errors_loop.py +529 -286
- pdd/fix_verification_main.py +294 -89
- 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 +139 -15
- pdd/generate_test.py +218 -146
- pdd/get_comment.py +19 -44
- pdd/get_extension.py +8 -9
- pdd/get_jwt_token.py +318 -22
- pdd/get_language.py +8 -7
- pdd/get_run_command.py +75 -0
- pdd/get_test_command.py +68 -0
- pdd/git_update.py +70 -19
- pdd/incremental_code_generator.py +2 -2
- pdd/insert_includes.py +13 -4
- pdd/llm_invoke.py +1711 -181
- pdd/load_prompt_template.py +19 -12
- pdd/path_resolution.py +140 -0
- pdd/pdd_completion.fish +25 -2
- pdd/pdd_completion.sh +30 -4
- pdd/pdd_completion.zsh +79 -4
- pdd/postprocess.py +14 -4
- pdd/preprocess.py +293 -24
- pdd/preprocess_main.py +41 -6
- 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_crash_explore_LLM.prompt +49 -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_explore_LLM.prompt +45 -0
- pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
- pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
- pdd/prompts/agentic_update_LLM.prompt +925 -0
- pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
- pdd/prompts/auto_include_LLM.prompt +122 -905
- pdd/prompts/change_LLM.prompt +3093 -1
- pdd/prompts/detect_change_LLM.prompt +686 -27
- pdd/prompts/example_generator_LLM.prompt +22 -1
- pdd/prompts/extract_code_LLM.prompt +5 -1
- pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
- pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
- pdd/prompts/extract_promptline_LLM.prompt +17 -11
- pdd/prompts/find_verification_errors_LLM.prompt +6 -0
- pdd/prompts/fix_code_module_errors_LLM.prompt +12 -2
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +9 -0
- pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
- pdd/prompts/generate_test_LLM.prompt +41 -7
- pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
- pdd/prompts/increase_tests_LLM.prompt +1 -5
- pdd/prompts/insert_includes_LLM.prompt +316 -186
- pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
- pdd/prompts/prompt_diff_LLM.prompt +82 -0
- pdd/prompts/trace_LLM.prompt +25 -22
- pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
- pdd/prompts/update_prompt_LLM.prompt +22 -1
- pdd/pytest_output.py +127 -12
- pdd/remote_session.py +876 -0
- pdd/render_mermaid.py +236 -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/setup_tool.py +648 -0
- pdd/simple_math.py +2 -0
- pdd/split_main.py +3 -2
- pdd/summarize_directory.py +237 -195
- pdd/sync_animation.py +8 -4
- pdd/sync_determine_operation.py +839 -112
- pdd/sync_main.py +351 -57
- pdd/sync_orchestration.py +1400 -756
- pdd/sync_tui.py +848 -0
- pdd/template_expander.py +161 -0
- pdd/template_registry.py +264 -0
- pdd/templates/architecture/architecture_json.prompt +237 -0
- pdd/templates/generic/generate_prompt.prompt +174 -0
- pdd/trace.py +168 -12
- pdd/trace_main.py +4 -3
- pdd/track_cost.py +140 -63
- pdd/unfinished_prompt.py +51 -4
- pdd/update_main.py +567 -67
- pdd/update_model_costs.py +2 -2
- pdd/update_prompt.py +19 -4
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +29 -11
- pdd_cli-0.0.118.dist-info/RECORD +227 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +1 -1
- pdd_cli-0.0.45.dist-info/RECORD +0 -116
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
pdd/render_mermaid.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Render architecture.json as an interactive HTML Mermaid diagram.
|
|
4
|
+
Usage:
|
|
5
|
+
python render_mermaid.py architecture.json "App Name" [output.html]
|
|
6
|
+
Features:
|
|
7
|
+
- Direct browser rendering (no external tools)
|
|
8
|
+
- Beautiful modern UI with statistics
|
|
9
|
+
- Color-coded module categories
|
|
10
|
+
- Interactive Mermaid diagram
|
|
11
|
+
- Self-contained HTML (works offline)
|
|
12
|
+
"""
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
import html
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
# Indentation constants for better maintainability
|
|
19
|
+
INDENT = ' ' # 4 spaces per level
|
|
20
|
+
LEVELS = {
|
|
21
|
+
'root': 0,
|
|
22
|
+
'subgraph': 1,
|
|
23
|
+
'node': 2,
|
|
24
|
+
'connection': 1,
|
|
25
|
+
'style': 1
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def write_pretty_architecture_json(arch_file, architecture):
|
|
29
|
+
"""Rewrite architecture JSON with consistent formatting so diffs stay stable."""
|
|
30
|
+
path = Path(arch_file)
|
|
31
|
+
formatted = json.dumps(architecture, indent=2)
|
|
32
|
+
if not formatted.endswith("\n"):
|
|
33
|
+
formatted += "\n"
|
|
34
|
+
path.write_text(formatted, encoding="utf-8")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def generate_mermaid_code(architecture, app_name="System"):
|
|
38
|
+
"""Generate Mermaid flowchart code from architecture JSON."""
|
|
39
|
+
# Escape quotes for Mermaid label, which uses HTML entities
|
|
40
|
+
escaped_app_name = app_name.replace('"', '"')
|
|
41
|
+
# Match test expectation: add a trailing space only if quotes were present
|
|
42
|
+
prd_label = f'{escaped_app_name} ' if """ in escaped_app_name else escaped_app_name
|
|
43
|
+
|
|
44
|
+
lines = ["flowchart TB", f'{INDENT * LEVELS["node"]}PRD["{prd_label}"]', INDENT]
|
|
45
|
+
|
|
46
|
+
if not architecture:
|
|
47
|
+
lines.append(INDENT)
|
|
48
|
+
|
|
49
|
+
# Categorize modules by tags (frontend takes priority over backend)
|
|
50
|
+
frontend = [
|
|
51
|
+
m
|
|
52
|
+
for m in architecture
|
|
53
|
+
if any(t in m.get('tags', []) for t in ['frontend', 'react', 'nextjs', 'ui', 'page', 'component'])
|
|
54
|
+
]
|
|
55
|
+
backend = [
|
|
56
|
+
m
|
|
57
|
+
for m in architecture
|
|
58
|
+
if m not in frontend
|
|
59
|
+
and any(t in m.get('tags', []) for t in ['backend', 'api', 'database', 'sqlalchemy', 'fastapi'])
|
|
60
|
+
]
|
|
61
|
+
shared = [m for m in architecture if m not in frontend and m not in backend]
|
|
62
|
+
|
|
63
|
+
# Generate subgraphs
|
|
64
|
+
for group_name, modules in [("Frontend", frontend), ("Backend", backend), ("Shared", shared)]:
|
|
65
|
+
if modules:
|
|
66
|
+
lines.append(f"{INDENT * LEVELS['subgraph']}subgraph {group_name}")
|
|
67
|
+
for m in modules:
|
|
68
|
+
name = Path(m['filename']).stem
|
|
69
|
+
pri = m.get('priority', 0)
|
|
70
|
+
lines.append(f'{INDENT * LEVELS["node"]}{name}["{name} ({pri})"]')
|
|
71
|
+
lines.append(f"{INDENT * LEVELS['subgraph']}end")
|
|
72
|
+
lines.append(INDENT)
|
|
73
|
+
|
|
74
|
+
# PRD connections
|
|
75
|
+
if frontend:
|
|
76
|
+
lines.append(f"{INDENT * LEVELS['connection']}PRD --> Frontend")
|
|
77
|
+
if backend:
|
|
78
|
+
lines.append(f"{INDENT * LEVELS['connection']}PRD --> Backend")
|
|
79
|
+
|
|
80
|
+
# Add newline between PRD connections and dependencies
|
|
81
|
+
if frontend or backend:
|
|
82
|
+
lines.append("")
|
|
83
|
+
|
|
84
|
+
# Dependencies
|
|
85
|
+
for m in architecture:
|
|
86
|
+
src = Path(m['filename']).stem
|
|
87
|
+
for dep in m.get('dependencies', []):
|
|
88
|
+
dst = Path(dep).stem
|
|
89
|
+
lines.append(f'{INDENT * LEVELS["connection"]}{src} -->|uses| {dst}')
|
|
90
|
+
|
|
91
|
+
# Add newline after dependencies
|
|
92
|
+
if any(m.get('dependencies', []) for m in architecture):
|
|
93
|
+
lines.append(INDENT)
|
|
94
|
+
|
|
95
|
+
# Styles
|
|
96
|
+
lines.extend([f"{INDENT * LEVELS['style']}classDef frontend fill:#FFF3E0,stroke:#F57C00,stroke-width:2px",
|
|
97
|
+
f"{INDENT * LEVELS['style']}classDef backend fill:#E3F2FD,stroke:#1976D2,stroke-width:2px",
|
|
98
|
+
f"{INDENT * LEVELS['style']}classDef shared fill:#E8F5E9,stroke:#388E3C,stroke-width:2px",
|
|
99
|
+
f"{INDENT * LEVELS['style']}classDef system fill:#E0E0E0,stroke:#616161,stroke-width:3px", INDENT])
|
|
100
|
+
|
|
101
|
+
# Apply classes
|
|
102
|
+
if frontend:
|
|
103
|
+
lines.append(f"{INDENT * LEVELS['style']}class {','.join([Path(m['filename']).stem for m in frontend])} frontend")
|
|
104
|
+
if backend:
|
|
105
|
+
lines.append(f"{INDENT * LEVELS['style']}class {','.join([Path(m['filename']).stem for m in backend])} backend")
|
|
106
|
+
if shared:
|
|
107
|
+
lines.append(f"{INDENT * LEVELS['style']}class {','.join([Path(m['filename']).stem for m in shared])} shared")
|
|
108
|
+
lines.append(f"{INDENT * LEVELS['style']}class PRD system")
|
|
109
|
+
|
|
110
|
+
return "\n".join(lines)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def generate_html(mermaid_code, architecture, app_name):
|
|
114
|
+
"""Generate interactive HTML with hover tooltips."""
|
|
115
|
+
|
|
116
|
+
# Create module data as JSON for tooltips
|
|
117
|
+
module_data = {}
|
|
118
|
+
for m in architecture:
|
|
119
|
+
module_id = Path(m['filename']).stem
|
|
120
|
+
module_data[module_id] = {
|
|
121
|
+
'filename': m['filename'],
|
|
122
|
+
'priority': m.get('priority', 'N/A'),
|
|
123
|
+
'description': m.get('description', 'No description'),
|
|
124
|
+
'dependencies': m.get('dependencies', []),
|
|
125
|
+
'tags': m.get('tags', []),
|
|
126
|
+
'filepath': m.get('filepath', ''),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module_json = json.dumps(module_data)
|
|
130
|
+
escaped_app_name = html.escape(app_name)
|
|
131
|
+
|
|
132
|
+
return f"""<!DOCTYPE html>
|
|
133
|
+
<html><head><meta charset="UTF-8"><title>{escaped_app_name}</title>
|
|
134
|
+
<script type=\"module\">
|
|
135
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
|
|
136
|
+
mermaid.initialize({{startOnLoad:true,theme:'default'}});
|
|
137
|
+
window.addEventListener('load', () => {{
|
|
138
|
+
const moduleData = {module_json};
|
|
139
|
+
|
|
140
|
+
// Add hover listeners to all nodes
|
|
141
|
+
setTimeout(() => {{
|
|
142
|
+
const nodes = document.querySelectorAll('.node');
|
|
143
|
+
nodes.forEach(node => {{
|
|
144
|
+
const text = node.querySelector('.nodeLabel');
|
|
145
|
+
if (!text) return;
|
|
146
|
+
|
|
147
|
+
const nodeText = text.textContent.trim();
|
|
148
|
+
const moduleId = nodeText.split(' ')[0];
|
|
149
|
+
const data = moduleData[moduleId];
|
|
150
|
+
|
|
151
|
+
if (data) {{
|
|
152
|
+
node.style.cursor = 'pointer';
|
|
153
|
+
|
|
154
|
+
node.addEventListener('mouseenter', (e) => {{
|
|
155
|
+
showTooltip(e, data);
|
|
156
|
+
}});
|
|
157
|
+
|
|
158
|
+
node.addEventListener('mouseleave', () => {{
|
|
159
|
+
hideTooltip();
|
|
160
|
+
}});
|
|
161
|
+
}}
|
|
162
|
+
}});
|
|
163
|
+
}}, 500);
|
|
164
|
+
}});
|
|
165
|
+
function showTooltip(e, data) {{
|
|
166
|
+
hideTooltip();
|
|
167
|
+
|
|
168
|
+
const tooltip = document.createElement('div');
|
|
169
|
+
tooltip.id = 'module-tooltip';
|
|
170
|
+
tooltip.innerHTML = `
|
|
171
|
+
<div style="font-weight:600;margin-bottom:8px;font-size:1.1em;">${{data.filename}}</div>
|
|
172
|
+
<div style="margin-bottom:6px;"><strong>Priority:</strong> ${{data.priority}}</div>
|
|
173
|
+
<div style="margin-bottom:6px;"><strong>Path:</strong> ${{data.filepath}}</div>
|
|
174
|
+
<div style="margin-bottom:6px;"><strong>Tags:</strong> ${{data.tags.join(', ')}}</div>
|
|
175
|
+
<div style="margin-bottom:6px;"><strong>Dependencies:</strong> ${{data.dependencies.length > 0 ? data.dependencies.join(', ') : 'None'}}</div>
|
|
176
|
+
<div style="margin-top:8px;padding-top:8px;border-top:1px solid #ddd;font-size:0.9em;color:#444;">${{data.description}}</div>
|
|
177
|
+
`;
|
|
178
|
+
|
|
179
|
+
document.body.appendChild(tooltip);
|
|
180
|
+
|
|
181
|
+
const rect = e.target.closest('.node').getBoundingClientRect();
|
|
182
|
+
tooltip.style.left = rect.right + 10 + 'px';
|
|
183
|
+
tooltip.style.top = rect.top + window.scrollY + 'px';
|
|
184
|
+
}}
|
|
185
|
+
function hideTooltip() {{
|
|
186
|
+
const existing = document.getElementById('module-tooltip');
|
|
187
|
+
if (existing) existing.remove();
|
|
188
|
+
}}
|
|
189
|
+
</script>
|
|
190
|
+
<style>
|
|
191
|
+
*{{margin:0;padding:0;box-sizing:border-box}}
|
|
192
|
+
body{{font-family:system-ui,sans-serif;background:#fff;color:#000;padding:2rem;max-width:1400px;margin:0 auto}}
|
|
193
|
+
h1{{font-size:2rem;font-weight:600;margin-bottom:2rem;padding-bottom:1rem;border-bottom:2px solid #000}}
|
|
194
|
+
.diagram{{border:1px solid #000;padding:2rem;margin:2rem 0;overflow-x:auto;position:relative}}
|
|
195
|
+
.mermaid{{display:flex;justify-content:center}}
|
|
196
|
+
#module-tooltip{{
|
|
197
|
+
position:absolute;
|
|
198
|
+
background:#fff;
|
|
199
|
+
border:2px solid #000;
|
|
200
|
+
padding:1rem;
|
|
201
|
+
max-width:400px;
|
|
202
|
+
z-index:1000;
|
|
203
|
+
box-shadow:4px 4px 0 rgba(0,0,0,0.1);
|
|
204
|
+
font-size:0.9rem;
|
|
205
|
+
line-height:1.5;
|
|
206
|
+
}}
|
|
207
|
+
.node{{transition:opacity 0.2s}}
|
|
208
|
+
.node:hover{{opacity:0.8}}
|
|
209
|
+
</style></head><body>
|
|
210
|
+
<h1>{escaped_app_name}</h1>
|
|
211
|
+
<div class="diagram"><pre class="mermaid">{mermaid_code}</pre></div>
|
|
212
|
+
</body></html>"""
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
if len(sys.argv) < 2:
|
|
217
|
+
print("Usage: python render_mermaid.py <architecture.json> [app_name] [output.html]")
|
|
218
|
+
sys.exit(1)
|
|
219
|
+
|
|
220
|
+
arch_file = sys.argv[1]
|
|
221
|
+
app_name = sys.argv[2] if len(sys.argv) > 2 else "System Architecture"
|
|
222
|
+
output_file = sys.argv[3] if len(sys.argv) > 3 else f"{Path(arch_file).stem}_diagram.html"
|
|
223
|
+
|
|
224
|
+
with open(arch_file) as f:
|
|
225
|
+
architecture = json.load(f)
|
|
226
|
+
write_pretty_architecture_json(arch_file, architecture)
|
|
227
|
+
|
|
228
|
+
mermaid_code = generate_mermaid_code(architecture, app_name)
|
|
229
|
+
html_content = generate_html(mermaid_code, architecture, app_name)
|
|
230
|
+
|
|
231
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
232
|
+
f.write(html_content)
|
|
233
|
+
|
|
234
|
+
print(f"✅ Generated: {output_file}")
|
|
235
|
+
print(f"📊 Modules: {len(architecture)}")
|
|
236
|
+
print(f"🌐 Open {output_file} in your browser!")
|
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
|
+
)
|