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
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Click Command Executor for PDD Server.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to programmatically execute Click commands with:
|
|
5
|
+
- Isolated Click context creation
|
|
6
|
+
- Output capture (stdout/stderr)
|
|
7
|
+
- Error handling
|
|
8
|
+
- Real-time streaming via callback
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import io
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Callable, Dict, Optional
|
|
18
|
+
from unittest.mock import MagicMock
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _setup_headless_environment():
|
|
24
|
+
"""
|
|
25
|
+
Set up environment variables for headless command execution.
|
|
26
|
+
|
|
27
|
+
This ensures commands run in non-interactive mode without TUI,
|
|
28
|
+
which is necessary when running programmatically through the server.
|
|
29
|
+
|
|
30
|
+
NOTE: This should only be called when actually executing commands through
|
|
31
|
+
the server, NOT at module import time. Calling at import time would
|
|
32
|
+
affect ALL pdd commands (including CLI usage) because the connect command
|
|
33
|
+
imports this module transitively.
|
|
34
|
+
"""
|
|
35
|
+
# Skip if already configured (idempotent)
|
|
36
|
+
if os.environ.get('CI') == '1':
|
|
37
|
+
return
|
|
38
|
+
os.environ['CI'] = '1' # Triggers headless mode in sync and other commands
|
|
39
|
+
os.environ['PDD_FORCE'] = '1' # Skip confirmation prompts
|
|
40
|
+
os.environ['TERM'] = 'dumb' # Disable fancy terminal features
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# NOTE: Do NOT call _setup_headless_environment() here at import time!
|
|
44
|
+
# It will be called by ClickCommandExecutor when executing commands.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ============================================================================
|
|
48
|
+
# Output Capture
|
|
49
|
+
# ============================================================================
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class CapturedOutput:
|
|
53
|
+
"""Container for captured command output."""
|
|
54
|
+
stdout: str = ""
|
|
55
|
+
stderr: str = ""
|
|
56
|
+
exit_code: int = 0
|
|
57
|
+
exception: Optional[Exception] = None
|
|
58
|
+
cost: float = 0.0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class StreamingWriter:
|
|
62
|
+
"""
|
|
63
|
+
Writer that both buffers output and calls a callback for streaming.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
buffer: io.StringIO,
|
|
69
|
+
callback: Optional[Callable[[str, str], None]],
|
|
70
|
+
stream_type: str,
|
|
71
|
+
):
|
|
72
|
+
self._buffer = buffer
|
|
73
|
+
self._callback = callback
|
|
74
|
+
self._stream_type = stream_type
|
|
75
|
+
|
|
76
|
+
def write(self, text: str) -> int:
|
|
77
|
+
self._buffer.write(text)
|
|
78
|
+
if self._callback and text:
|
|
79
|
+
self._callback(self._stream_type, text)
|
|
80
|
+
return len(text)
|
|
81
|
+
|
|
82
|
+
def flush(self):
|
|
83
|
+
self._buffer.flush()
|
|
84
|
+
|
|
85
|
+
def isatty(self) -> bool:
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class OutputCapture:
|
|
90
|
+
"""
|
|
91
|
+
Captures stdout and stderr during command execution.
|
|
92
|
+
|
|
93
|
+
Usage:
|
|
94
|
+
with OutputCapture() as capture:
|
|
95
|
+
# Execute command
|
|
96
|
+
result = some_function()
|
|
97
|
+
|
|
98
|
+
print(capture.stdout)
|
|
99
|
+
print(capture.stderr)
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, callback: Optional[Callable[[str, str], None]] = None):
|
|
103
|
+
"""
|
|
104
|
+
Initialize output capture.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
callback: Optional callback(stream_type, text) for real-time streaming
|
|
108
|
+
"""
|
|
109
|
+
self._callback = callback
|
|
110
|
+
self._stdout_buffer = io.StringIO()
|
|
111
|
+
self._stderr_buffer = io.StringIO()
|
|
112
|
+
self._original_stdout = None
|
|
113
|
+
self._original_stderr = None
|
|
114
|
+
|
|
115
|
+
def __enter__(self) -> "OutputCapture":
|
|
116
|
+
self._original_stdout = sys.stdout
|
|
117
|
+
self._original_stderr = sys.stderr
|
|
118
|
+
|
|
119
|
+
if self._callback:
|
|
120
|
+
# Use streaming wrappers
|
|
121
|
+
sys.stdout = StreamingWriter(self._stdout_buffer, self._callback, "stdout")
|
|
122
|
+
sys.stderr = StreamingWriter(self._stderr_buffer, self._callback, "stderr")
|
|
123
|
+
else:
|
|
124
|
+
sys.stdout = self._stdout_buffer
|
|
125
|
+
sys.stderr = self._stderr_buffer
|
|
126
|
+
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
130
|
+
sys.stdout = self._original_stdout
|
|
131
|
+
sys.stderr = self._original_stderr
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def stdout(self) -> str:
|
|
136
|
+
return self._stdout_buffer.getvalue()
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def stderr(self) -> str:
|
|
140
|
+
return self._stderr_buffer.getvalue()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ============================================================================
|
|
144
|
+
# Click Context Factory
|
|
145
|
+
# ============================================================================
|
|
146
|
+
|
|
147
|
+
def create_isolated_context(
|
|
148
|
+
command: click.Command,
|
|
149
|
+
obj: Optional[Dict[str, Any]] = None,
|
|
150
|
+
color: bool = False,
|
|
151
|
+
) -> click.Context:
|
|
152
|
+
"""
|
|
153
|
+
Create an isolated Click context for programmatic command execution.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
command: The Click command to create context for
|
|
157
|
+
obj: Context object (ctx.obj) with shared state
|
|
158
|
+
color: Whether to enable ANSI colors in output
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Configured Click context
|
|
162
|
+
"""
|
|
163
|
+
# Create context with the command
|
|
164
|
+
ctx = click.Context(command, color=color)
|
|
165
|
+
|
|
166
|
+
# Set context object (shared state between commands)
|
|
167
|
+
ctx.obj = obj or {
|
|
168
|
+
"strength": 0.5,
|
|
169
|
+
"temperature": 0.1,
|
|
170
|
+
"time": 0.25,
|
|
171
|
+
"verbose": False,
|
|
172
|
+
"force": False,
|
|
173
|
+
"quiet": False,
|
|
174
|
+
"output_cost": None,
|
|
175
|
+
"review_examples": False,
|
|
176
|
+
"local": False,
|
|
177
|
+
"context": None,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# Mock parameter source checking (returns DEFAULT for all)
|
|
181
|
+
mock_source = MagicMock()
|
|
182
|
+
mock_source.name = "DEFAULT"
|
|
183
|
+
ctx.get_parameter_source = MagicMock(return_value=mock_source)
|
|
184
|
+
|
|
185
|
+
return ctx
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ============================================================================
|
|
189
|
+
# Command Executor
|
|
190
|
+
# ============================================================================
|
|
191
|
+
|
|
192
|
+
# Options that should be integers when passed to Click commands
|
|
193
|
+
INTEGER_OPTIONS = {
|
|
194
|
+
'max_attempts', 'target_coverage', 'depth', 'limit',
|
|
195
|
+
'max_tokens', 'timeout', 'retries', 'iterations',
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# Options that should be floats when passed to Click commands
|
|
199
|
+
FLOAT_OPTIONS = {
|
|
200
|
+
'strength', 'temperature', 'time', 'threshold', 'budget',
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# Options that should be booleans when passed to Click commands
|
|
204
|
+
BOOLEAN_OPTIONS = {
|
|
205
|
+
'verbose', 'quiet', 'force', 'loop', 'skip_verify', 'skip_tests',
|
|
206
|
+
'local', 'dry_run', 'auto_submit', 'recursive',
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _convert_option_type(key: str, value: Any) -> Any:
|
|
211
|
+
"""
|
|
212
|
+
Convert option value to the appropriate type based on the option name.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
key: The option name (with underscores, not hyphens)
|
|
216
|
+
value: The value to convert
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
The value converted to the appropriate type
|
|
220
|
+
"""
|
|
221
|
+
if value is None:
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
normalized_key = key.replace("-", "_")
|
|
225
|
+
|
|
226
|
+
# Handle integers
|
|
227
|
+
if normalized_key in INTEGER_OPTIONS:
|
|
228
|
+
if isinstance(value, int):
|
|
229
|
+
return value
|
|
230
|
+
if isinstance(value, str):
|
|
231
|
+
try:
|
|
232
|
+
return int(value)
|
|
233
|
+
except ValueError:
|
|
234
|
+
return value
|
|
235
|
+
return value
|
|
236
|
+
|
|
237
|
+
# Handle floats
|
|
238
|
+
if normalized_key in FLOAT_OPTIONS:
|
|
239
|
+
if isinstance(value, (int, float)):
|
|
240
|
+
return float(value)
|
|
241
|
+
if isinstance(value, str):
|
|
242
|
+
try:
|
|
243
|
+
return float(value)
|
|
244
|
+
except ValueError:
|
|
245
|
+
return value
|
|
246
|
+
return value
|
|
247
|
+
|
|
248
|
+
# Handle booleans
|
|
249
|
+
if normalized_key in BOOLEAN_OPTIONS:
|
|
250
|
+
if isinstance(value, bool):
|
|
251
|
+
return value
|
|
252
|
+
if isinstance(value, str):
|
|
253
|
+
return value.lower() in ('true', '1', 'yes', 'on')
|
|
254
|
+
return bool(value)
|
|
255
|
+
|
|
256
|
+
return value
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _get_command_positional_args(command: click.Command) -> List[str]:
|
|
260
|
+
"""Extract positional argument names from Click command in order."""
|
|
261
|
+
positionals = []
|
|
262
|
+
for param in command.params:
|
|
263
|
+
if isinstance(param, click.Argument):
|
|
264
|
+
positionals.append(param.name)
|
|
265
|
+
return positionals
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _is_variadic_argument(command: click.Command, param_name: str) -> bool:
|
|
269
|
+
"""Check if a parameter is a variadic argument (nargs=-1)."""
|
|
270
|
+
for param in command.params:
|
|
271
|
+
if param.name == param_name and isinstance(param, click.Argument):
|
|
272
|
+
return param.nargs == -1
|
|
273
|
+
return False
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _is_multiple_option(command: click.Command, param_name: str) -> bool:
|
|
277
|
+
"""Check if an option accepts multiple values (multiple=True)."""
|
|
278
|
+
for param in command.params:
|
|
279
|
+
if param.name == param_name and isinstance(param, click.Option):
|
|
280
|
+
return param.multiple
|
|
281
|
+
return False
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class ClickCommandExecutor:
|
|
285
|
+
"""
|
|
286
|
+
Executes Click commands programmatically with output capture.
|
|
287
|
+
|
|
288
|
+
This class provides:
|
|
289
|
+
- Isolated context creation
|
|
290
|
+
- Output capture (stdout/stderr)
|
|
291
|
+
- Error handling
|
|
292
|
+
- Real-time streaming via callback
|
|
293
|
+
|
|
294
|
+
Usage:
|
|
295
|
+
executor = ClickCommandExecutor()
|
|
296
|
+
|
|
297
|
+
# Execute a command
|
|
298
|
+
result = executor.execute(
|
|
299
|
+
command=sync_command,
|
|
300
|
+
params={"basename": "hello", "max_attempts": 3},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
print(result.stdout)
|
|
304
|
+
print(f"Exit code: {result.exit_code}")
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
def __init__(
|
|
308
|
+
self,
|
|
309
|
+
base_context_obj: Optional[Dict[str, Any]] = None,
|
|
310
|
+
output_callback: Optional[Callable[[str, str], None]] = None,
|
|
311
|
+
):
|
|
312
|
+
"""
|
|
313
|
+
Initialize the executor.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
base_context_obj: Base context object for all commands
|
|
317
|
+
output_callback: Callback for real-time output streaming
|
|
318
|
+
"""
|
|
319
|
+
self._base_context_obj = base_context_obj or {}
|
|
320
|
+
self._output_callback = output_callback
|
|
321
|
+
|
|
322
|
+
def execute(
|
|
323
|
+
self,
|
|
324
|
+
command: click.Command,
|
|
325
|
+
args: Optional[Dict[str, Any]] = None,
|
|
326
|
+
options: Optional[Dict[str, Any]] = None,
|
|
327
|
+
capture_output: bool = True,
|
|
328
|
+
) -> CapturedOutput:
|
|
329
|
+
"""
|
|
330
|
+
Execute a Click command with the given parameters.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
command: Click command to execute
|
|
334
|
+
args: Positional arguments to pass to the command
|
|
335
|
+
options: Options/flags to pass to the command
|
|
336
|
+
capture_output: If True, capture stdout/stderr. If False, output goes to terminal.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
CapturedOutput with stdout, stderr, exit_code
|
|
340
|
+
"""
|
|
341
|
+
# Set up headless environment for server-executed commands
|
|
342
|
+
_setup_headless_environment()
|
|
343
|
+
|
|
344
|
+
# Global options that belong in ctx.obj, not passed as command params
|
|
345
|
+
# These match the global options defined in pdd/core/cli.py
|
|
346
|
+
GLOBAL_OPTIONS = {
|
|
347
|
+
"force", "strength", "temperature", "time", "verbose", "quiet",
|
|
348
|
+
"output_cost", "review_examples", "local", "context", "list_contexts", "core_dump"
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
# 1. Build ctx.obj with ONLY global options
|
|
352
|
+
obj = {**self._base_context_obj}
|
|
353
|
+
if options:
|
|
354
|
+
for key, value in options.items():
|
|
355
|
+
normalized_key = key.replace("-", "_")
|
|
356
|
+
if normalized_key in GLOBAL_OPTIONS:
|
|
357
|
+
obj[normalized_key] = value
|
|
358
|
+
|
|
359
|
+
# 2. Build params dict for command execution
|
|
360
|
+
params = {}
|
|
361
|
+
|
|
362
|
+
# Manual mode file key mappings for fix/change commands
|
|
363
|
+
# These commands use variadic "args" for BOTH modes, but the frontend sends semantic keys
|
|
364
|
+
# for manual mode which we need to convert to ordered positional arguments
|
|
365
|
+
MANUAL_MODE_FILE_KEYS = {
|
|
366
|
+
"fix": ["prompt_file", "code_file", "unit_test_files", "error_file"],
|
|
367
|
+
"change": ["change_prompt_file", "input_code", "input_prompt_file"],
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
# Handle fix/change manual mode: convert semantic file keys to positional args
|
|
371
|
+
command_name = command.name if hasattr(command, 'name') else str(command)
|
|
372
|
+
if command_name in MANUAL_MODE_FILE_KEYS and args and "args" not in args:
|
|
373
|
+
file_keys = MANUAL_MODE_FILE_KEYS[command_name]
|
|
374
|
+
if any(k in args for k in file_keys):
|
|
375
|
+
# Convert file keys to ordered positional args list (order matters!)
|
|
376
|
+
positional_values = []
|
|
377
|
+
for key in file_keys:
|
|
378
|
+
if key in args and args[key] is not None:
|
|
379
|
+
positional_values.append(str(args[key]))
|
|
380
|
+
# Collect remaining args that aren't file keys (e.g., verification_program)
|
|
381
|
+
remaining_args = {k: v for k, v in args.items() if k not in file_keys}
|
|
382
|
+
# Build new args with positional values
|
|
383
|
+
args = {"args": positional_values}
|
|
384
|
+
# Add --manual flag to options
|
|
385
|
+
if options is None:
|
|
386
|
+
options = {}
|
|
387
|
+
options["manual"] = True
|
|
388
|
+
# Move remaining args to options (they should be CLI options like --verification-program)
|
|
389
|
+
for key, value in remaining_args.items():
|
|
390
|
+
options[key] = value
|
|
391
|
+
|
|
392
|
+
# Handle args dict
|
|
393
|
+
if args:
|
|
394
|
+
# Special case: "args" key with list (for bug, fix, change)
|
|
395
|
+
# These commands have @click.argument("args", nargs=-1)
|
|
396
|
+
if "args" in args and isinstance(args["args"], list):
|
|
397
|
+
# This is a variadic positional - pass as tuple
|
|
398
|
+
params["args"] = tuple(args["args"])
|
|
399
|
+
else:
|
|
400
|
+
# Regular positional arguments
|
|
401
|
+
for key, value in args.items():
|
|
402
|
+
normalized_key = key.replace("-", "_")
|
|
403
|
+
|
|
404
|
+
# Skip if this is a global option (shouldn't be in args, but be safe)
|
|
405
|
+
if normalized_key in GLOBAL_OPTIONS:
|
|
406
|
+
continue
|
|
407
|
+
|
|
408
|
+
# Check if it's a variadic argument (nargs=-1)
|
|
409
|
+
if _is_variadic_argument(command, normalized_key):
|
|
410
|
+
# Convert list to tuple for variadic arguments
|
|
411
|
+
if isinstance(value, list):
|
|
412
|
+
params[normalized_key] = tuple(value)
|
|
413
|
+
elif value is not None:
|
|
414
|
+
# Single value - wrap in tuple
|
|
415
|
+
params[normalized_key] = (value,)
|
|
416
|
+
else:
|
|
417
|
+
params[normalized_key] = ()
|
|
418
|
+
else:
|
|
419
|
+
# Regular argument - apply type conversion
|
|
420
|
+
params[normalized_key] = _convert_option_type(normalized_key, value)
|
|
421
|
+
|
|
422
|
+
# Handle options (command-specific only)
|
|
423
|
+
if options:
|
|
424
|
+
for key, value in options.items():
|
|
425
|
+
normalized_key = key.replace("-", "_")
|
|
426
|
+
|
|
427
|
+
# Skip global options - already in ctx.obj
|
|
428
|
+
if normalized_key in GLOBAL_OPTIONS:
|
|
429
|
+
continue
|
|
430
|
+
|
|
431
|
+
# Check if it's a multiple option (multiple=True)
|
|
432
|
+
if _is_multiple_option(command, normalized_key):
|
|
433
|
+
# Convert list to tuple for multiple options
|
|
434
|
+
if isinstance(value, list):
|
|
435
|
+
params[normalized_key] = tuple(value)
|
|
436
|
+
else:
|
|
437
|
+
# Single value or other type - keep as-is
|
|
438
|
+
params[normalized_key] = value
|
|
439
|
+
else:
|
|
440
|
+
# Regular option - apply type conversion
|
|
441
|
+
params[normalized_key] = _convert_option_type(normalized_key, value)
|
|
442
|
+
|
|
443
|
+
# Create isolated context
|
|
444
|
+
ctx = create_isolated_context(command, obj)
|
|
445
|
+
|
|
446
|
+
if capture_output:
|
|
447
|
+
# Capture output mode
|
|
448
|
+
capture = OutputCapture(callback=self._output_callback)
|
|
449
|
+
|
|
450
|
+
try:
|
|
451
|
+
with capture:
|
|
452
|
+
with ctx:
|
|
453
|
+
# Invoke the command with parameters
|
|
454
|
+
result = ctx.invoke(command, **params)
|
|
455
|
+
|
|
456
|
+
return CapturedOutput(
|
|
457
|
+
stdout=capture.stdout,
|
|
458
|
+
stderr=capture.stderr,
|
|
459
|
+
exit_code=0,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
except click.Abort:
|
|
463
|
+
return CapturedOutput(
|
|
464
|
+
stdout=capture.stdout,
|
|
465
|
+
stderr=capture.stderr,
|
|
466
|
+
exit_code=1,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
except click.ClickException as e:
|
|
470
|
+
return CapturedOutput(
|
|
471
|
+
stdout=capture.stdout,
|
|
472
|
+
stderr=capture.stderr + f"\nError: {e.format_message()}",
|
|
473
|
+
exit_code=e.exit_code,
|
|
474
|
+
exception=e,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
except Exception as e:
|
|
478
|
+
return CapturedOutput(
|
|
479
|
+
stdout=capture.stdout,
|
|
480
|
+
stderr=capture.stderr + f"\nException: {str(e)}",
|
|
481
|
+
exit_code=1,
|
|
482
|
+
exception=e,
|
|
483
|
+
)
|
|
484
|
+
else:
|
|
485
|
+
# Terminal mode - output goes directly to terminal
|
|
486
|
+
try:
|
|
487
|
+
with ctx:
|
|
488
|
+
result = ctx.invoke(command, **params)
|
|
489
|
+
return CapturedOutput(exit_code=0)
|
|
490
|
+
|
|
491
|
+
except click.Abort:
|
|
492
|
+
return CapturedOutput(exit_code=1)
|
|
493
|
+
|
|
494
|
+
except click.ClickException as e:
|
|
495
|
+
print(f"Error: {e.format_message()}", file=sys.stderr)
|
|
496
|
+
return CapturedOutput(exit_code=e.exit_code, exception=e)
|
|
497
|
+
|
|
498
|
+
except Exception as e:
|
|
499
|
+
print(f"Exception: {str(e)}", file=sys.stderr)
|
|
500
|
+
return CapturedOutput(exit_code=1, exception=e)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
# ============================================================================
|
|
504
|
+
# PDD Command Registry
|
|
505
|
+
# ============================================================================
|
|
506
|
+
|
|
507
|
+
# Command registry - lazily populated to avoid circular imports
|
|
508
|
+
_command_cache: Dict[str, click.Command] = {}
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def get_pdd_command(command_name: str) -> Optional[click.Command]:
|
|
512
|
+
"""
|
|
513
|
+
Get a PDD Click command by name.
|
|
514
|
+
|
|
515
|
+
This function maps command names to their Click command objects.
|
|
516
|
+
Commands are imported lazily to avoid circular imports.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
command_name: Name of the command (e.g., "sync", "generate")
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
Click command object or None if not found
|
|
523
|
+
"""
|
|
524
|
+
# Return from cache if available
|
|
525
|
+
if command_name in _command_cache:
|
|
526
|
+
return _command_cache[command_name]
|
|
527
|
+
|
|
528
|
+
# Import commands lazily
|
|
529
|
+
try:
|
|
530
|
+
if command_name == "sync":
|
|
531
|
+
from pdd.commands.maintenance import sync
|
|
532
|
+
_command_cache[command_name] = sync
|
|
533
|
+
return sync
|
|
534
|
+
|
|
535
|
+
elif command_name == "update":
|
|
536
|
+
from pdd.commands.modify import update
|
|
537
|
+
_command_cache[command_name] = update
|
|
538
|
+
return update
|
|
539
|
+
|
|
540
|
+
elif command_name == "generate":
|
|
541
|
+
from pdd.commands.generate import generate
|
|
542
|
+
_command_cache[command_name] = generate
|
|
543
|
+
return generate
|
|
544
|
+
|
|
545
|
+
elif command_name == "test":
|
|
546
|
+
from pdd.commands.generate import test
|
|
547
|
+
_command_cache[command_name] = test
|
|
548
|
+
return test
|
|
549
|
+
|
|
550
|
+
elif command_name == "fix":
|
|
551
|
+
from pdd.commands.fix import fix
|
|
552
|
+
_command_cache[command_name] = fix
|
|
553
|
+
return fix
|
|
554
|
+
|
|
555
|
+
elif command_name == "example":
|
|
556
|
+
from pdd.commands.generate import example
|
|
557
|
+
_command_cache[command_name] = example
|
|
558
|
+
return example
|
|
559
|
+
|
|
560
|
+
elif command_name == "bug":
|
|
561
|
+
from pdd.commands.analysis import bug
|
|
562
|
+
_command_cache[command_name] = bug
|
|
563
|
+
return bug
|
|
564
|
+
|
|
565
|
+
elif command_name == "change":
|
|
566
|
+
from pdd.commands.modify import change
|
|
567
|
+
_command_cache[command_name] = change
|
|
568
|
+
return change
|
|
569
|
+
|
|
570
|
+
elif command_name == "crash":
|
|
571
|
+
from pdd.commands.analysis import crash
|
|
572
|
+
_command_cache[command_name] = crash
|
|
573
|
+
return crash
|
|
574
|
+
|
|
575
|
+
elif command_name == "verify":
|
|
576
|
+
from pdd.commands.utility import verify
|
|
577
|
+
_command_cache[command_name] = verify
|
|
578
|
+
return verify
|
|
579
|
+
|
|
580
|
+
else:
|
|
581
|
+
return None
|
|
582
|
+
|
|
583
|
+
except ImportError as e:
|
|
584
|
+
# Log import error but don't crash
|
|
585
|
+
import logging
|
|
586
|
+
logging.warning(f"Failed to import command '{command_name}': {e}")
|
|
587
|
+
return None
|