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.
Files changed (144) hide show
  1. pdd/__init__.py +38 -6
  2. pdd/agentic_bug.py +323 -0
  3. pdd/agentic_bug_orchestrator.py +497 -0
  4. pdd/agentic_change.py +231 -0
  5. pdd/agentic_change_orchestrator.py +526 -0
  6. pdd/agentic_common.py +521 -786
  7. pdd/agentic_e2e_fix.py +319 -0
  8. pdd/agentic_e2e_fix_orchestrator.py +426 -0
  9. pdd/agentic_fix.py +118 -3
  10. pdd/agentic_update.py +25 -8
  11. pdd/architecture_sync.py +565 -0
  12. pdd/auth_service.py +210 -0
  13. pdd/auto_deps_main.py +63 -53
  14. pdd/auto_include.py +185 -3
  15. pdd/auto_update.py +125 -47
  16. pdd/bug_main.py +195 -23
  17. pdd/cmd_test_main.py +345 -197
  18. pdd/code_generator.py +4 -2
  19. pdd/code_generator_main.py +118 -32
  20. pdd/commands/__init__.py +6 -0
  21. pdd/commands/analysis.py +87 -29
  22. pdd/commands/auth.py +309 -0
  23. pdd/commands/connect.py +290 -0
  24. pdd/commands/fix.py +136 -113
  25. pdd/commands/maintenance.py +3 -2
  26. pdd/commands/misc.py +8 -0
  27. pdd/commands/modify.py +190 -164
  28. pdd/commands/sessions.py +284 -0
  29. pdd/construct_paths.py +334 -32
  30. pdd/context_generator_main.py +167 -170
  31. pdd/continue_generation.py +6 -3
  32. pdd/core/__init__.py +33 -0
  33. pdd/core/cli.py +27 -3
  34. pdd/core/cloud.py +237 -0
  35. pdd/core/errors.py +4 -0
  36. pdd/core/remote_session.py +61 -0
  37. pdd/crash_main.py +219 -23
  38. pdd/data/llm_model.csv +4 -4
  39. pdd/docs/prompting_guide.md +864 -0
  40. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  41. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  42. pdd/fix_code_loop.py +208 -34
  43. pdd/fix_code_module_errors.py +6 -2
  44. pdd/fix_error_loop.py +291 -38
  45. pdd/fix_main.py +204 -4
  46. pdd/fix_verification_errors_loop.py +235 -26
  47. pdd/fix_verification_main.py +269 -83
  48. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  49. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
  50. pdd/frontend/dist/index.html +376 -0
  51. pdd/frontend/dist/logo.svg +33 -0
  52. pdd/generate_output_paths.py +46 -5
  53. pdd/generate_test.py +212 -151
  54. pdd/get_comment.py +19 -44
  55. pdd/get_extension.py +8 -9
  56. pdd/get_jwt_token.py +309 -20
  57. pdd/get_language.py +8 -7
  58. pdd/get_run_command.py +7 -5
  59. pdd/insert_includes.py +2 -1
  60. pdd/llm_invoke.py +459 -95
  61. pdd/load_prompt_template.py +15 -34
  62. pdd/path_resolution.py +140 -0
  63. pdd/postprocess.py +4 -1
  64. pdd/preprocess.py +68 -12
  65. pdd/preprocess_main.py +33 -1
  66. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  67. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  68. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  69. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  70. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  71. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  72. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  73. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  74. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  75. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  76. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  77. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  78. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
  79. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  80. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  81. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  82. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  83. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  84. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  85. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  86. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  87. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  88. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  89. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  90. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  91. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  92. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  93. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  94. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  95. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  96. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  97. pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
  98. pdd/prompts/agentic_update_LLM.prompt +192 -338
  99. pdd/prompts/auto_include_LLM.prompt +22 -0
  100. pdd/prompts/change_LLM.prompt +3093 -1
  101. pdd/prompts/detect_change_LLM.prompt +571 -14
  102. pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
  103. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
  104. pdd/prompts/generate_test_LLM.prompt +20 -1
  105. pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
  106. pdd/prompts/insert_includes_LLM.prompt +262 -252
  107. pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
  108. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  109. pdd/remote_session.py +876 -0
  110. pdd/server/__init__.py +52 -0
  111. pdd/server/app.py +335 -0
  112. pdd/server/click_executor.py +587 -0
  113. pdd/server/executor.py +338 -0
  114. pdd/server/jobs.py +661 -0
  115. pdd/server/models.py +241 -0
  116. pdd/server/routes/__init__.py +31 -0
  117. pdd/server/routes/architecture.py +451 -0
  118. pdd/server/routes/auth.py +364 -0
  119. pdd/server/routes/commands.py +929 -0
  120. pdd/server/routes/config.py +42 -0
  121. pdd/server/routes/files.py +603 -0
  122. pdd/server/routes/prompts.py +1322 -0
  123. pdd/server/routes/websocket.py +473 -0
  124. pdd/server/security.py +243 -0
  125. pdd/server/terminal_spawner.py +209 -0
  126. pdd/server/token_counter.py +222 -0
  127. pdd/summarize_directory.py +236 -237
  128. pdd/sync_animation.py +8 -4
  129. pdd/sync_determine_operation.py +329 -47
  130. pdd/sync_main.py +272 -28
  131. pdd/sync_orchestration.py +136 -75
  132. pdd/template_expander.py +161 -0
  133. pdd/templates/architecture/architecture_json.prompt +41 -46
  134. pdd/trace.py +1 -1
  135. pdd/track_cost.py +0 -13
  136. pdd/unfinished_prompt.py +2 -1
  137. pdd/update_main.py +23 -5
  138. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +15 -10
  139. pdd_cli-0.0.118.dist-info/RECORD +227 -0
  140. pdd_cli-0.0.90.dist-info/RECORD +0 -153
  141. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
  142. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
  143. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +0 -0
  144. {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