pdd-cli 0.0.24__py3-none-any.whl → 0.0.25__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.

Potentially problematic release.


This version of pdd-cli might be problematic. Click here for more details.

Files changed (43) hide show
  1. pdd/__init__.py +7 -1
  2. pdd/bug_main.py +5 -1
  3. pdd/bug_to_unit_test.py +16 -5
  4. pdd/change.py +2 -1
  5. pdd/change_main.py +407 -189
  6. pdd/cli.py +853 -301
  7. pdd/code_generator.py +2 -1
  8. pdd/conflicts_in_prompts.py +2 -1
  9. pdd/construct_paths.py +377 -222
  10. pdd/context_generator.py +2 -1
  11. pdd/continue_generation.py +3 -2
  12. pdd/crash_main.py +55 -20
  13. pdd/detect_change.py +2 -1
  14. pdd/fix_code_loop.py +465 -160
  15. pdd/fix_code_module_errors.py +7 -4
  16. pdd/fix_error_loop.py +9 -9
  17. pdd/fix_errors_from_unit_tests.py +207 -365
  18. pdd/fix_main.py +31 -4
  19. pdd/fix_verification_errors.py +60 -34
  20. pdd/fix_verification_errors_loop.py +842 -768
  21. pdd/fix_verification_main.py +412 -0
  22. pdd/generate_output_paths.py +427 -189
  23. pdd/generate_test.py +3 -2
  24. pdd/increase_tests.py +2 -2
  25. pdd/llm_invoke.py +14 -3
  26. pdd/preprocess.py +3 -3
  27. pdd/process_csv_change.py +466 -154
  28. pdd/prompts/extract_prompt_update_LLM.prompt +11 -5
  29. pdd/prompts/extract_unit_code_fix_LLM.prompt +2 -2
  30. pdd/prompts/fix_code_module_errors_LLM.prompt +29 -0
  31. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +5 -5
  32. pdd/prompts/generate_test_LLM.prompt +9 -3
  33. pdd/prompts/update_prompt_LLM.prompt +3 -3
  34. pdd/split.py +6 -5
  35. pdd/split_main.py +13 -4
  36. pdd/trace_main.py +7 -0
  37. pdd/xml_tagger.py +2 -1
  38. {pdd_cli-0.0.24.dist-info → pdd_cli-0.0.25.dist-info}/METADATA +4 -4
  39. {pdd_cli-0.0.24.dist-info → pdd_cli-0.0.25.dist-info}/RECORD +43 -42
  40. {pdd_cli-0.0.24.dist-info → pdd_cli-0.0.25.dist-info}/WHEEL +1 -1
  41. {pdd_cli-0.0.24.dist-info → pdd_cli-0.0.25.dist-info}/entry_points.txt +0 -0
  42. {pdd_cli-0.0.24.dist-info → pdd_cli-0.0.25.dist-info}/licenses/LICENSE +0 -0
  43. {pdd_cli-0.0.24.dist-info → pdd_cli-0.0.25.dist-info}/top_level.txt +0 -0
pdd/cli.py CHANGED
@@ -1,56 +1,128 @@
1
+ # pdd/cli.py
2
+ from __future__ import annotations
3
+
1
4
  import os
2
- import sys
3
- import importlib.resources
4
- from datetime import datetime
5
- from functools import wraps
6
- from typing import Callable, List, Optional, Tuple
5
+ from typing import Any, Dict, List, Optional, Tuple
6
+ from pathlib import Path # Import Path
7
7
 
8
8
  import click
9
- from rich import print as rprint
10
9
  from rich.console import Console
11
- from rich.panel import Panel
12
-
13
- from .install_completion import install_completion as install_completion_main
14
- import pdd.install_completion
15
- from pdd import __version__
10
+ from rich.theme import Theme
11
+ from rich.markup import MarkupError, escape
16
12
 
17
- pdd.install_completion.get_local_pdd_path()
18
- # ----------------------------------------------------------------------
19
- # Import sub-command modules
20
- # ----------------------------------------------------------------------
13
+ # --- Relative Imports for Internal Modules ---
14
+ from . import DEFAULT_STRENGTH, __version__
15
+ from .auto_deps_main import auto_deps_main
16
+ from .auto_update import auto_update
17
+ from .bug_main import bug_main
18
+ from .change_main import change_main
19
+ from .cmd_test_main import cmd_test_main
21
20
  from .code_generator_main import code_generator_main
21
+ from .conflicts_main import conflicts_main
22
+ # Need to import construct_paths for tests patching pdd.cli.construct_paths
23
+ from .construct_paths import construct_paths
22
24
  from .context_generator_main import context_generator_main
23
- from .cmd_test_main import cmd_test_main
24
- from .preprocess_main import preprocess_main
25
+ from .crash_main import crash_main
26
+ from .detect_change_main import detect_change_main
25
27
  from .fix_main import fix_main
28
+ from .fix_verification_main import fix_verification_main
29
+ from .install_completion import install_completion
30
+ from .preprocess_main import preprocess_main
26
31
  from .split_main import split_main
27
- from .change_main import change_main
28
- from .update_main import update_main
29
- from .detect_change_main import detect_change_main
30
- from .conflicts_main import conflicts_main
31
- from .crash_main import crash_main
32
32
  from .trace_main import trace_main
33
- from .bug_main import bug_main
34
33
  from .track_cost import track_cost
35
- from .auto_update import auto_update
36
- from .auto_deps_main import auto_deps_main
34
+ from .update_main import update_main
35
+
36
+
37
+ # --- Initialize Rich Console ---
38
+ # Define a custom theme for consistent styling
39
+ custom_theme = Theme({
40
+ "info": "cyan",
41
+ "warning": "yellow",
42
+ "error": "bold red",
43
+ "success": "green",
44
+ "path": "dim blue",
45
+ "command": "bold magenta",
46
+ })
47
+ console = Console(theme=custom_theme)
48
+
49
+ # --- Helper Function for Error Handling ---
50
+ def handle_error(e: Exception, command_name: str, quiet: bool):
51
+ """Prints error messages using Rich console.""" # Modified docstring
52
+ if not quiet:
53
+ console.print(f"[error]Error during '{command_name}' command:[/error]", style="error")
54
+ if isinstance(e, FileNotFoundError):
55
+ console.print(f" [error]File not found:[/error] {e}", style="error")
56
+ elif isinstance(e, (ValueError, IOError)):
57
+ console.print(f" [error]Input/Output Error:[/error] {e}", style="error")
58
+ elif isinstance(e, click.UsageError): # Handle Click usage errors explicitly if needed
59
+ console.print(f" [error]Usage Error:[/error] {e}", style="error")
60
+ # click.UsageError should typically exit with 2, but we are handling it.
61
+ elif isinstance(e, MarkupError):
62
+ console.print(" [error]Markup Error:[/error] Invalid Rich markup encountered.", style="error")
63
+ # Print the error message safely escaped
64
+ console.print(escape(str(e)))
65
+ else:
66
+ console.print(f" [error]An unexpected error occurred:[/error] {e}", style="error")
67
+ # Do NOT re-raise e here. Let the command function return None.
68
+
37
69
 
38
- console = Console()
39
-
40
- @click.group()
41
- @click.option("--force", is_flag=True, help="Overwrite existing files without asking for confirmation.")
42
- @click.option("--strength", type=float, default=0.5, help="Set the strength of the AI model (0.0 to 1.0).")
43
- @click.option("--temperature", type=float, default=0.0, help="Set the temperature of the AI model.")
44
- @click.option("--verbose", is_flag=True, help="Increase output verbosity for more detailed information.")
45
- @click.option("--quiet", is_flag=True, help="Decrease output verbosity for minimal information.")
46
- @click.option("--output-cost", type=click.Path(), help="Enable cost tracking and output a CSV file with usage details.")
47
- @click.option("--review-examples", is_flag=True,
48
- help="Review and optionally exclude few-shot examples before command execution.")
49
- @click.option('--local', is_flag=True, help='Run commands locally instead of in the cloud.')
50
- @click.version_option(version=__version__)
70
+ # --- Main CLI Group ---
71
+ @click.group(chain=True, help="PDD (Prompt-Driven Development) Command Line Interface.")
72
+ @click.option(
73
+ "--force",
74
+ is_flag=True,
75
+ default=False,
76
+ help="Overwrite existing files without asking for confirmation.",
77
+ )
78
+ @click.option(
79
+ "--strength",
80
+ type=click.FloatRange(0.0, 1.0),
81
+ default=DEFAULT_STRENGTH,
82
+ show_default=True,
83
+ help="Set the strength of the AI model (0.0 to 1.0).",
84
+ )
85
+ @click.option(
86
+ "--temperature",
87
+ type=click.FloatRange(0.0, 2.0), # Allow higher temperatures if needed
88
+ default=0.0,
89
+ show_default=True,
90
+ help="Set the temperature of the AI model.",
91
+ )
92
+ @click.option(
93
+ "--verbose",
94
+ is_flag=True,
95
+ default=False,
96
+ help="Increase output verbosity for more detailed information.",
97
+ )
98
+ @click.option(
99
+ "--quiet",
100
+ is_flag=True,
101
+ default=False,
102
+ help="Decrease output verbosity for minimal information.",
103
+ )
104
+ @click.option(
105
+ "--output-cost",
106
+ type=click.Path(dir_okay=False, writable=True),
107
+ default=None,
108
+ help="Enable cost tracking and output a CSV file with usage details.",
109
+ )
110
+ @click.option(
111
+ "--review-examples",
112
+ is_flag=True,
113
+ default=False,
114
+ help="Review and optionally exclude few-shot examples before command execution.",
115
+ )
116
+ @click.option(
117
+ "--local",
118
+ is_flag=True,
119
+ default=False,
120
+ help="Run commands locally instead of in the cloud.",
121
+ )
122
+ @click.version_option(version=__version__, package_name="pdd-cli")
51
123
  @click.pass_context
52
124
  def cli(
53
- ctx,
125
+ ctx: click.Context,
54
126
  force: bool,
55
127
  strength: float,
56
128
  temperature: float,
@@ -58,10 +130,11 @@ def cli(
58
130
  quiet: bool,
59
131
  output_cost: Optional[str],
60
132
  review_examples: bool,
61
- local: bool
133
+ local: bool,
62
134
  ):
63
135
  """
64
- PDD (Prompt-Driven Development) Command Line Interface
136
+ Main entry point for the PDD CLI. Handles global options and initializes context.
137
+ Supports multi-command chaining.
65
138
  """
66
139
  ctx.ensure_object(dict)
67
140
  ctx.obj["force"] = force
@@ -69,68 +142,184 @@ def cli(
69
142
  ctx.obj["temperature"] = temperature
70
143
  ctx.obj["verbose"] = verbose
71
144
  ctx.obj["quiet"] = quiet
72
- ctx.obj["output_cost"] = output_cost or os.environ.get("PDD_OUTPUT_COST_PATH")
145
+ ctx.obj["output_cost"] = output_cost
73
146
  ctx.obj["review_examples"] = review_examples
74
- ctx.obj['local'] = local
147
+ ctx.obj["local"] = local
75
148
 
76
- # Auto-update check, but handle EOF errors so tests do not crash.
77
- auto_update_enabled = os.environ.get("PDD_AUTO_UPDATE", "true").lower() == "true"
78
- if auto_update_enabled and sys.stdin.isatty():
149
+ # Suppress verbose if quiet is enabled
150
+ if quiet:
151
+ ctx.obj["verbose"] = False
152
+
153
+ # Perform auto-update check unless disabled
154
+ if os.getenv("PDD_AUTO_UPDATE", "true").lower() != "false":
79
155
  try:
156
+ if not quiet:
157
+ console.print("[info]Checking for updates...[/info]")
158
+ # Removed quiet=quiet argument as it caused TypeError
80
159
  auto_update()
81
- except EOFError:
82
- pass
160
+ except Exception as e:
161
+ if not quiet:
162
+ console.print(f"[warning]Auto-update check failed:[/warning] {e}", style="warning")
163
+
164
+ # --- Result Callback for Chained Commands ---
165
+ @cli.result_callback()
166
+ @click.pass_context
167
+ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float, str]]], **kwargs):
168
+ """
169
+ Processes the results from chained commands.
170
+
171
+ Receives a list of tuples, typically (result, cost, model_name),
172
+ or None from each command function.
173
+ """
174
+ total_chain_cost = 0.0
175
+ # Get invoked subcommands directly from the group context if available (safer for testing)
176
+ # Note: This might yield "Unknown Command" during tests with CliRunner
177
+ invoked_subcommands = getattr(ctx, 'invoked_subcommands', [])
178
+ num_commands = len(invoked_subcommands)
179
+ num_results = len(results) # Number of results actually received
180
+
181
+ if not ctx.obj.get("quiet"):
182
+ console.print("\n[info]--- Command Chain Execution Summary ---[/info]")
183
+
184
+ for i, result_tuple in enumerate(results):
185
+ # Use the retrieved subcommand name (might be "Unknown Command X" in tests)
186
+ command_name = invoked_subcommands[i] if i < num_commands else f"Unknown Command {i+1}"
187
+
188
+ # Check if the command failed (returned None)
189
+ if result_tuple is None:
190
+ if not ctx.obj.get("quiet"):
191
+ # Check if it was install_completion (which normally returns None)
192
+ if command_name == "install_completion":
193
+ console.print(f" [info]Step {i+1} ({command_name}):[/info] Command completed.")
194
+ # Check if it was preprocess (which returns a dummy tuple on success)
195
+ # This case handles actual failure for preprocess
196
+ elif command_name == "preprocess":
197
+ console.print(f" [error]Step {i+1} ({command_name}):[/error] Command failed.")
198
+ else:
199
+ console.print(f" [error]Step {i+1} ({command_name}):[/error] Command failed.")
200
+ # Check if the result is the expected tuple structure from @track_cost or preprocess success
201
+ elif isinstance(result_tuple, tuple) and len(result_tuple) == 3:
202
+ _result_data, cost, model_name = result_tuple
203
+ total_chain_cost += cost
204
+ if not ctx.obj.get("quiet"):
205
+ # Special handling for preprocess success message (check actual command name)
206
+ actual_command_name = invoked_subcommands[i] if i < num_commands else None # Get actual name if possible
207
+ if actual_command_name == "preprocess" and cost == 0.0 and model_name == "local":
208
+ console.print(f" [info]Step {i+1} ({command_name}):[/info] Command completed (local).")
209
+ else:
210
+ # Generic output using potentially "Unknown Command" name
211
+ console.print(f" [info]Step {i+1} ({command_name}):[/info] Cost: ${cost:.6f}, Model: {model_name}")
212
+ else:
213
+ # Handle unexpected return types if necessary
214
+ if not ctx.obj.get("quiet"):
215
+ # Provide more detail on the unexpected type
216
+ console.print(f" [warning]Step {i+1} ({command_name}):[/warning] Unexpected result format: {type(result_tuple).__name__} - {str(result_tuple)[:50]}...")
217
+
83
218
 
219
+ if not ctx.obj.get("quiet"):
220
+ # Only print total cost if at least one command potentially contributed cost
221
+ if any(res is not None and isinstance(res, tuple) and len(res) == 3 for res in results):
222
+ console.print(f"[info]Total Estimated Cost for Chain:[/info] ${total_chain_cost:.6f}")
223
+ # Indicate if the chain might have been incomplete due to errors
224
+ if num_results < num_commands and not all(res is None for res in results): # Avoid printing if all failed
225
+ console.print("[warning]Note: Chain may have terminated early due to errors.[/warning]")
226
+ console.print("[info]-------------------------------------[/info]")
84
227
 
85
- @cli.command()
86
- @click.argument("prompt_file", type=click.Path(exists=True))
87
- @click.option("--output", type=click.Path(), help="Specify where to save the generated code.")
228
+
229
+ # --- Command Definitions ---
230
+
231
+ @cli.command("generate")
232
+ @click.argument("prompt_file", type=click.Path(exists=True, dir_okay=False))
233
+ @click.option(
234
+ "--output",
235
+ type=click.Path(writable=True), # Allows file or dir
236
+ default=None,
237
+ help="Specify where to save the generated code (file or directory).",
238
+ )
88
239
  @click.pass_context
89
240
  @track_cost
90
- def generate(ctx, prompt_file: str, output: Optional[str]) -> Tuple[str, float, str]:
241
+ def generate(ctx: click.Context, prompt_file: str, output: Optional[str]) -> Optional[Tuple[str, float, str]]: # Modified return type
91
242
  """Create runnable code from a prompt file."""
92
- return code_generator_main(ctx, prompt_file, output)
243
+ quiet = ctx.obj.get("quiet", False)
244
+ command_name = "generate"
245
+ try:
246
+ generated_code, total_cost, model_name = code_generator_main(
247
+ ctx=ctx,
248
+ prompt_file=prompt_file,
249
+ output=output,
250
+ )
251
+ return generated_code, total_cost, model_name
252
+ except Exception as e:
253
+ handle_error(e, command_name, quiet)
254
+ return None # Return None on failure
93
255
 
94
256
 
95
- @cli.command()
96
- @click.argument("prompt_file", type=click.Path(exists=True))
97
- @click.argument("code_file", type=click.Path(exists=True))
98
- @click.option("--output", type=click.Path(), help="Specify where to save the generated example code.")
257
+ @cli.command("example")
258
+ @click.argument("prompt_file", type=click.Path(exists=True, dir_okay=False))
259
+ @click.argument("code_file", type=click.Path(exists=True, dir_okay=False))
260
+ @click.option(
261
+ "--output",
262
+ type=click.Path(writable=True),
263
+ default=None,
264
+ help="Specify where to save the generated example code (file or directory).",
265
+ )
99
266
  @click.pass_context
100
267
  @track_cost
101
- def example(
102
- ctx,
103
- prompt_file: str,
104
- code_file: str,
105
- output: Optional[str]
106
- ) -> Tuple[str, float, str]:
107
- """Create an example file from an existing code file and the prompt that generated it."""
108
- return context_generator_main(ctx, prompt_file, code_file, output)
268
+ def example(ctx: click.Context, prompt_file: str, code_file: str, output: Optional[str]) -> Optional[Tuple[str, float, str]]: # Modified return type
269
+ """Create a compact example demonstrating functionality."""
270
+ quiet = ctx.obj.get("quiet", False)
271
+ command_name = "example"
272
+ try:
273
+ example_code, total_cost, model_name = context_generator_main(
274
+ ctx=ctx,
275
+ prompt_file=prompt_file,
276
+ code_file=code_file,
277
+ output=output,
278
+ )
279
+ return example_code, total_cost, model_name
280
+ except Exception as e:
281
+ handle_error(e, command_name, quiet)
282
+ return None # Return None on failure
109
283
 
110
284
 
111
- @cli.command()
112
- @click.argument("prompt_file", type=click.Path(exists=True))
113
- @click.argument("code_file", type=click.Path(exists=True))
114
- @click.option("--output", type=click.Path(), help="Specify where to save the generated test file.")
115
- @click.option("--language", help="Specify the programming language.")
285
+ @cli.command("test")
286
+ @click.argument("prompt_file", type=click.Path(exists=True, dir_okay=False))
287
+ @click.argument("code_file", type=click.Path(exists=True, dir_okay=False))
288
+ @click.option(
289
+ "--output",
290
+ type=click.Path(writable=True),
291
+ default=None,
292
+ help="Specify where to save the generated test file (file or directory).",
293
+ )
294
+ @click.option("--language", type=str, default=None, help="Specify the programming language.")
116
295
  @click.option(
117
296
  "--coverage-report",
118
- type=click.Path(exists=True),
297
+ type=click.Path(exists=True, dir_okay=False),
119
298
  default=None,
120
- help="Path to a coverage report for enhancing tests."
299
+ help="Path to the coverage report file for existing tests.",
121
300
  )
122
301
  @click.option(
123
302
  "--existing-tests",
124
- type=click.Path(exists=True),
303
+ type=click.Path(exists=True, dir_okay=False),
125
304
  default=None,
126
- help="Existing test file to merge or build upon."
305
+ help="Path to the existing unit test file.",
306
+ )
307
+ @click.option(
308
+ "--target-coverage",
309
+ type=click.FloatRange(0.0, 100.0),
310
+ default=None, # Use None, default handled in cmd_test_main or env var
311
+ help="Desired code coverage percentage (default: 90.0 or PDD_TEST_COVERAGE_TARGET).",
312
+ )
313
+ @click.option(
314
+ "--merge",
315
+ is_flag=True,
316
+ default=False,
317
+ help="Merge new tests with existing test file instead of creating a separate file.",
127
318
  )
128
- @click.option("--target-coverage", type=float, default=None, help="Desired coverage percentage.")
129
- @click.option("--merge", is_flag=True, default=False, help="Merge new tests into existing tests.")
130
319
  @click.pass_context
131
320
  @track_cost
132
321
  def test(
133
- ctx,
322
+ ctx: click.Context,
134
323
  prompt_file: str,
135
324
  code_file: str,
136
325
  output: Optional[str],
@@ -139,74 +328,145 @@ def test(
139
328
  existing_tests: Optional[str],
140
329
  target_coverage: Optional[float],
141
330
  merge: bool,
142
- ) -> Tuple[str, float, str]:
143
- """
144
- Generate or enhance unit tests for a given code file and its corresponding prompt file.
145
- """
146
- return cmd_test_main(
147
- ctx,
148
- prompt_file,
149
- code_file,
150
- output,
151
- language,
152
- coverage_report,
153
- existing_tests,
154
- target_coverage,
155
- merge,
156
- )
157
-
158
-
159
- @cli.command()
160
- @click.argument("prompt_file", type=click.Path(exists=True))
161
- @click.option("--output", type=click.Path(), help="Specify where to save the preprocessed prompt file.")
162
- @click.option("--xml", is_flag=True, help="Automatically insert XML delimiters for complex prompts.")
163
- @click.option("--recursive", is_flag=True, help="Recursively preprocess all prompt files in the prompt file.")
164
- @click.option("--double", is_flag=True, help="Curly brackets will be doubled.")
165
- @click.option("--exclude", multiple=True, help="List of keys to exclude from curly bracket doubling.")
331
+ ) -> Optional[Tuple[str, float, str]]: # Modified return type
332
+ """Generate or enhance unit tests."""
333
+ quiet = ctx.obj.get("quiet", False)
334
+ command_name = "test"
335
+ try:
336
+ generated_test_code, total_cost, model_name = cmd_test_main(
337
+ ctx=ctx,
338
+ prompt_file=prompt_file,
339
+ code_file=code_file,
340
+ output=output,
341
+ language=language,
342
+ coverage_report=coverage_report,
343
+ existing_tests=existing_tests,
344
+ target_coverage=target_coverage,
345
+ merge=merge,
346
+ )
347
+ return generated_test_code, total_cost, model_name
348
+ except Exception as e:
349
+ handle_error(e, command_name, quiet)
350
+ return None # Return None on failure
351
+
352
+
353
+ @cli.command("preprocess")
354
+ @click.argument("prompt_file", type=click.Path(exists=True, dir_okay=False))
355
+ @click.option(
356
+ "--output",
357
+ type=click.Path(writable=True),
358
+ default=None,
359
+ help="Specify where to save the preprocessed prompt file (file or directory).",
360
+ )
361
+ @click.option(
362
+ "--xml",
363
+ is_flag=True,
364
+ default=False,
365
+ help="Insert XML delimiters for structure (minimal preprocessing).",
366
+ )
367
+ @click.option(
368
+ "--recursive",
369
+ is_flag=True,
370
+ default=False,
371
+ help="Recursively preprocess includes.",
372
+ )
373
+ @click.option(
374
+ "--double",
375
+ is_flag=True,
376
+ default=False,
377
+ help="Double curly brackets.",
378
+ )
379
+ @click.option(
380
+ "--exclude",
381
+ multiple=True,
382
+ default=None,
383
+ help="List of keys to exclude from curly bracket doubling.",
384
+ )
166
385
  @click.pass_context
167
- @track_cost
386
+ # No @track_cost as preprocessing is local, but return dummy tuple for callback
168
387
  def preprocess(
169
- ctx,
388
+ ctx: click.Context,
170
389
  prompt_file: str,
171
390
  output: Optional[str],
172
391
  xml: bool,
173
392
  recursive: bool,
174
393
  double: bool,
175
- exclude: List[str]
176
- ) -> Tuple[str, float, str]:
394
+ exclude: Optional[Tuple[str, ...]],
395
+ ) -> Optional[Tuple[str, float, str]]: # Modified return type (Optional)
177
396
  """Preprocess prompt files and save the results."""
178
- return preprocess_main(ctx, prompt_file, output, xml, recursive, double, exclude)
397
+ quiet = ctx.obj.get("quiet", False)
398
+ command_name = "preprocess"
399
+ try:
400
+ preprocess_main(
401
+ ctx=ctx,
402
+ prompt_file=prompt_file,
403
+ output=output,
404
+ xml=xml,
405
+ recursive=recursive,
406
+ double=double,
407
+ exclude=list(exclude) if exclude else [],
408
+ )
409
+ # Return dummy values ONLY on success
410
+ return "Preprocessing complete.", 0.0, "local"
411
+ except Exception as e:
412
+ handle_error(e, command_name, quiet)
413
+ return None # Return None on failure
179
414
 
180
415
 
181
- @cli.command()
182
- @click.argument("prompt_file", type=click.Path(exists=True))
183
- @click.argument("code_file", type=click.Path(exists=True))
184
- @click.argument("unit_test_file", type=click.Path(exists=True))
185
- @click.argument("error_file", type=click.Path(exists=False))
186
- @click.option("--output-test", type=click.Path(), help="Where to save the fixed unit test file.")
187
- @click.option("--output-code", type=click.Path(), help="Where to save the fixed code file.")
416
+ @cli.command("fix")
417
+ @click.argument("prompt_file", type=click.Path(exists=True, dir_okay=False))
418
+ @click.argument("code_file", type=click.Path(exists=True, dir_okay=False))
419
+ @click.argument("unit_test_file", type=click.Path(exists=True, dir_okay=False))
420
+ @click.argument("error_file", type=click.Path(dir_okay=False)) # Allow non-existent for loop mode
421
+ @click.option(
422
+ "--output-test",
423
+ type=click.Path(writable=True),
424
+ default=None,
425
+ help="Specify where to save the fixed unit test file (file or directory).",
426
+ )
427
+ @click.option(
428
+ "--output-code",
429
+ type=click.Path(writable=True),
430
+ default=None,
431
+ help="Specify where to save the fixed code file (file or directory).",
432
+ )
188
433
  @click.option(
189
434
  "--output-results",
190
- type=click.Path(),
191
- help="Where to save the results from the error fixing process."
435
+ type=click.Path(writable=True),
436
+ default=None,
437
+ help="Specify where to save the results log (file or directory).",
192
438
  )
193
- @click.option("--loop", is_flag=True, help="Enable iterative fixing process.")
439
+ @click.option("--loop", is_flag=True, default=False, help="Enable iterative fixing process.")
194
440
  @click.option(
195
441
  "--verification-program",
196
- type=click.Path(exists=True),
197
- help="Path to a Python program that verifies code correctness."
442
+ type=click.Path(exists=True, dir_okay=False),
443
+ default=None,
444
+ help="Path to a Python program that verifies the fix.",
445
+ )
446
+ @click.option(
447
+ "--max-attempts",
448
+ type=int,
449
+ default=3,
450
+ show_default=True,
451
+ help="Maximum number of fix attempts.",
452
+ )
453
+ @click.option(
454
+ "--budget",
455
+ type=float,
456
+ default=5.0,
457
+ show_default=True,
458
+ help="Maximum cost allowed for the fixing process.",
198
459
  )
199
- @click.option("--max-attempts", type=int, default=3, help="Maximum fix attempts before giving up.")
200
- @click.option("--budget", type=float, default=5.0, help="Maximum cost allowed for the fixing process.")
201
460
  @click.option(
202
461
  "--auto-submit",
203
462
  is_flag=True,
204
- help="Automatically submit the example if all unit tests pass during the fix loop."
463
+ default=False,
464
+ help="Automatically submit the example if all unit tests pass.",
205
465
  )
206
466
  @click.pass_context
207
- @track_cost
467
+ @track_cost # fix_main returns cost/model info
208
468
  def fix(
209
- ctx,
469
+ ctx: click.Context,
210
470
  prompt_file: str,
211
471
  code_file: str,
212
472
  unit_test_file: str,
@@ -218,276 +478,568 @@ def fix(
218
478
  verification_program: Optional[str],
219
479
  max_attempts: int,
220
480
  budget: float,
221
- auto_submit: bool
222
- ) -> Tuple[bool, str, str, int, float, str]:
223
- """Fix errors in code and unit tests based on error messages and the original prompt file."""
224
- return fix_main(
225
- ctx,
226
- prompt_file,
227
- code_file,
228
- unit_test_file,
229
- error_file,
230
- output_test,
231
- output_code,
232
- output_results,
233
- loop,
234
- verification_program,
235
- max_attempts,
236
- budget,
237
- auto_submit,
238
- )
239
-
240
-
241
- @cli.command()
242
- @click.argument("input_prompt", type=click.Path(exists=True))
243
- @click.argument("input_code", type=click.Path(exists=True))
244
- @click.argument("example_code", type=click.Path(exists=True))
245
- @click.option("--output-sub", type=click.Path(), help="Where to save the generated sub-prompt file.")
246
- @click.option("--output-modified", type=click.Path(), help="Where to save the modified prompt file.")
481
+ auto_submit: bool,
482
+ ) -> Optional[Tuple[Dict[str, Any], float, str]]: # Modified return type
483
+ """Fix errors in code and unit tests based on error messages."""
484
+ quiet = ctx.obj.get("quiet", False)
485
+ command_name = "fix"
486
+ try:
487
+
488
+ # fix_main returns: success, fixed_test_content, fixed_code_content, attempts, cost, model
489
+ # We need to adapt this to the (result, cost, model) structure for the callback
490
+ success, fixed_test, fixed_code, attempts, cost, model = fix_main(
491
+ ctx=ctx,
492
+ prompt_file=prompt_file,
493
+ code_file=code_file,
494
+ unit_test_file=unit_test_file,
495
+ error_file=error_file,
496
+ output_test=output_test,
497
+ output_code=output_code,
498
+ output_results=output_results,
499
+ loop=loop,
500
+ verification_program=verification_program,
501
+ max_attempts=max_attempts,
502
+ budget=budget,
503
+ auto_submit=auto_submit,
504
+ )
505
+ # Package results into a dictionary for the first element of the tuple
506
+ result_data = {
507
+ "success": success,
508
+ "attempts": attempts,
509
+ "fixed_test_path": output_test,
510
+ "fixed_code_path": output_code,
511
+ "results_log_path": output_results,
512
+ }
513
+ return result_data, cost, model
514
+ except Exception as e:
515
+ handle_error(e, command_name, quiet)
516
+ return None # Return None on failure
517
+
518
+
519
+ @cli.command("split")
520
+ @click.argument("input_prompt", type=click.Path(exists=True, dir_okay=False))
521
+ @click.argument("input_code", type=click.Path(exists=True, dir_okay=False))
522
+ @click.argument("example_code", type=click.Path(exists=True, dir_okay=False))
523
+ @click.option(
524
+ "--output-sub",
525
+ type=click.Path(writable=True),
526
+ default=None,
527
+ help="Specify where to save the generated sub-prompt file (file or directory).",
528
+ )
529
+ @click.option(
530
+ "--output-modified",
531
+ type=click.Path(writable=True),
532
+ default=None,
533
+ help="Specify where to save the modified prompt file (file or directory).",
534
+ )
247
535
  @click.pass_context
248
536
  @track_cost
249
537
  def split(
250
- ctx,
538
+ ctx: click.Context,
251
539
  input_prompt: str,
252
540
  input_code: str,
253
541
  example_code: str,
254
542
  output_sub: Optional[str],
255
543
  output_modified: Optional[str],
256
- ) -> Tuple[str, str, float]:
257
- """Split large complex prompt files into smaller, more manageable prompt files."""
258
- return split_main(ctx, input_prompt, input_code, example_code, output_sub, output_modified)
544
+ ) -> Optional[Tuple[Dict[str, str], float, str]]: # Modified return type
545
+ """Split large complex prompt files into smaller ones."""
546
+ quiet = ctx.obj.get("quiet", False)
547
+ command_name = "split"
548
+ try:
549
+ result_data, total_cost, model_name = split_main(
550
+ ctx=ctx,
551
+ input_prompt_file=input_prompt,
552
+ input_code_file=input_code,
553
+ example_code_file=example_code,
554
+ output_sub=output_sub,
555
+ output_modified=output_modified,
556
+ )
557
+ return result_data, total_cost, model_name
558
+ except Exception as e:
559
+ handle_error(e, command_name, quiet)
560
+ return None # Return None on failure
259
561
 
260
562
 
261
- @cli.command()
262
- @click.argument("change_prompt_file", type=click.Path(exists=True))
263
- @click.argument("input_code", type=click.Path(exists=True))
264
- @click.argument("input_prompt_file", type=click.Path(exists=False), required=False)
265
- @click.option("--output", type=click.Path(), help="Where to save the modified prompt file.")
266
- @click.option("--csv", is_flag=True, help="Use a CSV file for change prompts instead of a single text file.")
563
+ @cli.command("change")
564
+ @click.argument("change_prompt_file", type=click.Path(exists=True, dir_okay=False))
565
+ @click.argument("input_code", type=click.Path(exists=True)) # Can be file or dir
566
+ @click.argument("input_prompt_file", type=click.Path(exists=True, dir_okay=False), required=False)
567
+ @click.option(
568
+ "--output",
569
+ type=click.Path(writable=True),
570
+ default=None,
571
+ help="Specify where to save the modified prompt file (file or directory).",
572
+ )
573
+ @click.option(
574
+ "--csv",
575
+ "use_csv",
576
+ is_flag=True,
577
+ default=False,
578
+ help="Use a CSV file for batch change prompts.",
579
+ )
267
580
  @click.pass_context
268
581
  @track_cost
269
582
  def change(
270
- ctx,
583
+ ctx: click.Context,
271
584
  change_prompt_file: str,
272
585
  input_code: str,
273
586
  input_prompt_file: Optional[str],
274
587
  output: Optional[str],
275
- csv: bool
276
- ) -> Tuple[str, float, str]:
277
- """Modify an input prompt file based on a change prompt and the corresponding input code."""
278
- return change_main(ctx, change_prompt_file, input_code, input_prompt_file, output, csv)
588
+ use_csv: bool,
589
+ ) -> Optional[Tuple[str | Dict, float, str]]: # Modified return type
590
+ """Modify prompt(s) based on change instructions."""
591
+ quiet = ctx.obj.get("quiet", False)
592
+ command_name = "change"
593
+ try:
594
+ # --- ADD VALIDATION LOGIC HERE ---
595
+ input_code_path = Path(input_code) # Convert to Path object
596
+ if use_csv:
597
+ if not input_code_path.is_dir():
598
+ raise click.UsageError("INPUT_CODE must be a directory when using --csv.")
599
+ if input_prompt_file:
600
+ raise click.UsageError("Cannot use --csv and specify an INPUT_PROMPT_FILE simultaneously.")
601
+ else: # Not using CSV
602
+ if not input_prompt_file:
603
+ # This check might be better inside change_main, but can be here too
604
+ raise click.UsageError("INPUT_PROMPT_FILE is required when not using --csv.")
605
+ if not input_code_path.is_file():
606
+ # This check might be better inside change_main, but can be here too
607
+ raise click.UsageError("INPUT_CODE must be a file when not using --csv.")
608
+ # --- END VALIDATION LOGIC ---
279
609
 
610
+ result_data, total_cost, model_name = change_main(
611
+ ctx=ctx,
612
+ change_prompt_file=change_prompt_file,
613
+ input_code=input_code,
614
+ input_prompt_file=input_prompt_file,
615
+ output=output,
616
+ use_csv=use_csv,
617
+ )
618
+ return result_data, total_cost, model_name
619
+ except (click.UsageError, Exception) as e: # Catch specific and general exceptions
620
+ handle_error(e, command_name, quiet)
621
+ return None # Return None on failure
280
622
 
281
- @cli.command()
282
- @click.argument("input_prompt_file", type=click.Path(exists=True))
283
- @click.argument("modified_code_file", type=click.Path(exists=True))
284
- @click.argument("input_code_file", type=click.Path(exists=True), required=False)
285
- @click.option("--output", type=click.Path(), help="Where to save the modified prompt file.")
623
+
624
+ @cli.command("update")
625
+ @click.argument("input_prompt_file", type=click.Path(exists=True, dir_okay=False))
626
+ @click.argument("modified_code_file", type=click.Path(exists=True, dir_okay=False))
627
+ @click.argument("input_code_file", type=click.Path(exists=True, dir_okay=False), required=False)
628
+ @click.option(
629
+ "--output",
630
+ type=click.Path(writable=True),
631
+ default=None,
632
+ help="Specify where to save the updated prompt file (file or directory).",
633
+ )
286
634
  @click.option(
287
635
  "--git",
288
636
  is_flag=True,
289
- help="Use git history to find the original code file instead of providing INPUT_CODE_FILE."
637
+ default=False,
638
+ help="Use git history to find the original code file.",
290
639
  )
291
640
  @click.pass_context
292
641
  @track_cost
293
642
  def update(
294
- ctx,
643
+ ctx: click.Context,
295
644
  input_prompt_file: str,
296
645
  modified_code_file: str,
297
646
  input_code_file: Optional[str],
298
647
  output: Optional[str],
299
648
  git: bool,
300
- ) -> Tuple[str, float, str]:
301
- """Update the original prompt file based on the original code and the modified code."""
302
- return update_main(ctx, input_prompt_file, modified_code_file, input_code_file, output, git)
649
+ ) -> Optional[Tuple[str, float, str]]: # Modified return type
650
+ """Update the original prompt file based on modified code."""
651
+ quiet = ctx.obj.get("quiet", False)
652
+ command_name = "update"
653
+ try:
654
+ if git and input_code_file:
655
+ raise click.UsageError("Cannot use --git and specify an INPUT_CODE_FILE simultaneously.")
656
+ if not git and not input_code_file:
657
+ raise click.UsageError("INPUT_CODE_FILE is required when not using --git.")
303
658
 
659
+ updated_prompt, total_cost, model_name = update_main(
660
+ ctx=ctx,
661
+ input_prompt_file=input_prompt_file,
662
+ modified_code_file=modified_code_file,
663
+ input_code_file=input_code_file,
664
+ output=output,
665
+ git=git,
666
+ )
667
+ return updated_prompt, total_cost, model_name
668
+ except (click.UsageError, Exception) as e: # Catch specific and general exceptions
669
+ handle_error(e, command_name, quiet)
670
+ return None # Return None on failure
304
671
 
305
- @cli.command()
306
- @click.argument("prompt_files", nargs=-1, type=click.Path(exists=True))
307
- @click.argument("change_file", type=click.Path(exists=True))
308
- @click.option("--output", type=click.Path(), help="Where to save CSV analysis results.")
672
+
673
+ @cli.command("detect")
674
+ @click.argument("prompt_files", nargs=-1, type=click.Path(exists=True, dir_okay=False))
675
+ @click.argument("change_file", type=click.Path(exists=True, dir_okay=False))
676
+ @click.option(
677
+ "--output",
678
+ type=click.Path(writable=True),
679
+ default=None,
680
+ help="Specify where to save the CSV analysis results (file or directory).",
681
+ )
309
682
  @click.pass_context
310
683
  @track_cost
311
684
  def detect(
312
- ctx,
313
- prompt_files: List[str],
685
+ ctx: click.Context,
686
+ prompt_files: Tuple[str, ...],
314
687
  change_file: str,
315
- output: Optional[str]
316
- ) -> Tuple[List[dict], float, str]:
317
- """Analyze a list of prompt files and a change description to see which prompts need changes."""
318
- return detect_change_main(ctx, prompt_files, change_file, output)
688
+ output: Optional[str],
689
+ ) -> Optional[Tuple[List[Dict[str, str]], float, str]]: # Modified return type
690
+ """Analyze prompts and a change description to find needed changes."""
691
+ quiet = ctx.obj.get("quiet", False)
692
+ command_name = "detect"
693
+ try:
694
+ if not prompt_files:
695
+ raise click.UsageError("At least one PROMPT_FILE must be provided.")
319
696
 
697
+ changes_list, total_cost, model_name = detect_change_main(
698
+ ctx=ctx,
699
+ prompt_files=list(prompt_files),
700
+ change_file=change_file,
701
+ output=output,
702
+ )
703
+ return changes_list, total_cost, model_name
704
+ except (click.UsageError, Exception) as e: # Catch specific and general exceptions
705
+ handle_error(e, command_name, quiet)
706
+ return None # Return None on failure
320
707
 
321
- @cli.command()
322
- @click.argument("prompt1", type=click.Path(exists=True))
323
- @click.argument("prompt2", type=click.Path(exists=True))
324
- @click.option("--output", type=click.Path(), help="Where to save the conflict analysis CSV.")
708
+
709
+ @cli.command("conflicts")
710
+ @click.argument("prompt1", type=click.Path(exists=True, dir_okay=False))
711
+ @click.argument("prompt2", type=click.Path(exists=True, dir_okay=False))
712
+ @click.option(
713
+ "--output",
714
+ type=click.Path(writable=True),
715
+ default=None,
716
+ help="Specify where to save the CSV conflict analysis results (file or directory).",
717
+ )
325
718
  @click.pass_context
326
719
  @track_cost
327
720
  def conflicts(
328
- ctx,
721
+ ctx: click.Context,
329
722
  prompt1: str,
330
723
  prompt2: str,
331
- output: Optional[str]
332
- ) -> Tuple[List[dict], float, str]:
333
- """Analyze two prompt files to find conflicts and suggest resolutions."""
334
- return conflicts_main(ctx, prompt1, prompt2, output)
335
-
336
-
337
- @cli.command()
338
- @click.argument("prompt_file", type=click.Path(exists=True))
339
- @click.argument("code_file", type=click.Path(exists=True))
340
- @click.argument("program_file", type=click.Path(exists=True))
341
- @click.argument("error_file", type=click.Path())
342
- @click.option("--output", type=click.Path(), help="Where to save the fixed code file.")
343
- @click.option("--output-program", type=click.Path(), help="Where to save the fixed program file.")
344
- @click.option("--loop", is_flag=True, help="Enable iterative fixing process.")
345
- @click.option("--max-attempts", type=int, default=3, help="Maximum fix attempts before giving up.")
346
- @click.option("--budget", type=float, default=5.0, help="Maximum cost allowed for the fixing process.")
724
+ output: Optional[str],
725
+ ) -> Optional[Tuple[List[Dict[str, str]], float, str]]: # Modified return type
726
+ """Analyze two prompt files to find conflicts."""
727
+ quiet = ctx.obj.get("quiet", False)
728
+ command_name = "conflicts"
729
+ try:
730
+ conflicts_list, total_cost, model_name = conflicts_main(
731
+ ctx=ctx,
732
+ prompt1=prompt1,
733
+ prompt2=prompt2,
734
+ output=output,
735
+ )
736
+ return conflicts_list, total_cost, model_name
737
+ except Exception as e:
738
+ handle_error(e, command_name, quiet)
739
+ return None # Return None on failure
740
+
741
+
742
+ @cli.command("crash")
743
+ @click.argument("prompt_file", type=click.Path(exists=True, dir_okay=False))
744
+ @click.argument("code_file", type=click.Path(exists=True, dir_okay=False))
745
+ @click.argument("program_file", type=click.Path(exists=True, dir_okay=False))
746
+ @click.argument("error_file", type=click.Path(dir_okay=False)) # Allow non-existent
747
+ @click.option(
748
+ "--output", # Corresponds to output_code in crash_main
749
+ type=click.Path(writable=True),
750
+ default=None,
751
+ help="Specify where to save the fixed code file (file or directory).",
752
+ )
753
+ @click.option(
754
+ "--output-program",
755
+ type=click.Path(writable=True),
756
+ default=None,
757
+ help="Specify where to save the fixed program file (file or directory).",
758
+ )
759
+ @click.option("--loop", is_flag=True, default=False, help="Enable iterative fixing process.")
760
+ @click.option(
761
+ "--max-attempts",
762
+ type=int,
763
+ default=3,
764
+ show_default=True,
765
+ help="Maximum number of fix attempts.",
766
+ )
767
+ @click.option(
768
+ "--budget",
769
+ type=float,
770
+ default=5.0,
771
+ show_default=True,
772
+ help="Maximum cost allowed for the fixing process.",
773
+ )
347
774
  @click.pass_context
348
775
  @track_cost
349
776
  def crash(
350
- ctx,
777
+ ctx: click.Context,
351
778
  prompt_file: str,
352
779
  code_file: str,
353
780
  program_file: str,
354
781
  error_file: str,
355
- output: Optional[str],
782
+ output: Optional[str], # Maps to output_code
356
783
  output_program: Optional[str],
357
784
  loop: bool,
358
785
  max_attempts: int,
359
- budget: float
360
- ) -> Tuple[bool, str, str, int, float, str]:
361
- """Fix errors in a code module that caused a program to crash."""
362
- return crash_main(
363
- ctx,
364
- prompt_file,
365
- code_file,
366
- program_file,
367
- error_file,
368
- output,
369
- output_program,
370
- loop,
371
- max_attempts,
372
- budget,
373
- )
374
-
375
- @cli.command(name="install_completion")
376
- def install_completion():
377
- """
378
- Install shell completion for the PDD CLI by detecting the user's shell,
379
- copying the relevant completion script, and appending a source command
380
- to the user's shell RC file if not already present.
381
- """
382
- return install_completion_main()
786
+ budget: float,
787
+ ) -> Optional[Tuple[Dict[str, Any], float, str]]: # Modified return type
788
+ """Fix errors in a code module and calling program that caused a crash."""
789
+ quiet = ctx.obj.get("quiet", False)
790
+ command_name = "crash"
791
+ try:
792
+ success, fixed_code, fixed_program, attempts, cost, model = crash_main(
793
+ ctx=ctx,
794
+ prompt_file=prompt_file,
795
+ code_file=code_file,
796
+ program_file=program_file,
797
+ error_file=error_file,
798
+ output=output,
799
+ output_program=output_program,
800
+ loop=loop,
801
+ max_attempts=max_attempts,
802
+ budget=budget,
803
+ )
804
+ result_data = {
805
+ "success": success,
806
+ "attempts": attempts,
807
+ "fixed_code_path": output,
808
+ "fixed_program_path": output_program,
809
+ }
810
+ return result_data, cost, model
811
+ except Exception as e:
812
+ handle_error(e, command_name, quiet)
813
+ return None # Return None on failure
383
814
 
384
815
 
385
- @cli.command()
386
- @click.argument("prompt_file", type=click.Path(exists=True))
387
- @click.argument("code_file", type=click.Path(exists=True))
816
+ @cli.command("trace")
817
+ @click.argument("prompt_file", type=click.Path(exists=True, dir_okay=False))
818
+ @click.argument("code_file", type=click.Path(exists=True, dir_okay=False))
388
819
  @click.argument("code_line", type=int)
389
- @click.option("--output", type=click.Path(), help="Where to save the trace analysis results.")
820
+ @click.option(
821
+ "--output",
822
+ type=click.Path(writable=True),
823
+ default=None,
824
+ help="Specify where to save the trace analysis results log (file or directory).",
825
+ )
390
826
  @click.pass_context
391
827
  @track_cost
392
828
  def trace(
393
- ctx,
829
+ ctx: click.Context,
394
830
  prompt_file: str,
395
831
  code_file: str,
396
832
  code_line: int,
397
- output: Optional[str]
398
- ) -> Tuple[str, float, str]:
399
- """
400
- Find the associated line number between a prompt file and the generated code.
401
- """
402
- return trace_main(ctx, prompt_file, code_file, code_line, output)
833
+ output: Optional[str],
834
+ ) -> Optional[Tuple[int | str, float, str]]: # Modified return type
835
+ """Find the associated line number between a prompt file and generated code."""
836
+ quiet = ctx.obj.get("quiet", False)
837
+ command_name = "trace"
838
+ try:
839
+ prompt_line_result, total_cost, model_name = trace_main(
840
+ ctx=ctx,
841
+ prompt_file=prompt_file,
842
+ code_file=code_file,
843
+ code_line=code_line,
844
+ output=output,
845
+ )
846
+ # Check if trace_main indicated failure (e.g., by returning None or specific error)
847
+ # This depends on trace_main's implementation; assuming it raises exceptions for now.
848
+ if prompt_line_result is None and total_cost == 0.0 and model_name == 'local_error': # Example check if trace_main returns specific tuple on failure
849
+ # Optionally handle specific non-exception failures differently if needed
850
+ # For now, rely on exceptions being raised for errors like out-of-range.
851
+ pass
852
+ return prompt_line_result, total_cost, model_name
853
+ except Exception as e:
854
+ handle_error(e, command_name, quiet)
855
+ # Exit with non-zero status code on any exception
856
+ ctx.exit(1)
403
857
 
404
858
 
405
- @cli.command()
406
- @click.argument("prompt_file", type=click.Path(exists=True))
407
- @click.argument("code_file", type=click.Path(exists=True))
408
- @click.argument("program_file", type=click.Path(exists=True))
409
- @click.argument("current_output", type=click.Path(exists=True))
410
- @click.argument("desired_output", type=click.Path(exists=True))
859
+ @cli.command("bug")
860
+ @click.argument("prompt_file", type=click.Path(exists=True, dir_okay=False))
861
+ @click.argument("code_file", type=click.Path(exists=True, dir_okay=False))
862
+ @click.argument("program_file", type=click.Path(exists=True, dir_okay=False))
863
+ @click.argument("current_output_file", type=click.Path(exists=True, dir_okay=False))
864
+ @click.argument("desired_output_file", type=click.Path(exists=True, dir_okay=False))
411
865
  @click.option(
412
866
  "--output",
413
- metavar="LOCATION",
414
- type=click.Path(),
415
- help="Where to save the bug-related unit test."
867
+ type=click.Path(writable=True),
868
+ default=None,
869
+ help="Specify where to save the generated unit test (file or directory).",
416
870
  )
417
- @click.option("--language", default="Python", help="Specify the programming language.")
871
+ @click.option("--language", type=str, default=None, help="Specify the programming language (default: Python).")
418
872
  @click.pass_context
419
873
  @track_cost
420
874
  def bug(
421
- ctx,
875
+ ctx: click.Context,
422
876
  prompt_file: str,
423
877
  code_file: str,
424
878
  program_file: str,
425
- current_output: str,
426
- desired_output: str,
879
+ current_output_file: str,
880
+ desired_output_file: str,
427
881
  output: Optional[str],
428
- language: Optional[str]
429
- ) -> Tuple[str, float, str]:
430
- """
431
- Generate a unit test based on observed and desired outputs for given code and prompt.
432
- """
433
- return bug_main(
434
- ctx,
435
- prompt_file,
436
- code_file,
437
- program_file,
438
- current_output,
439
- desired_output,
440
- output,
441
- language
442
- )
443
-
444
-
445
- @cli.command()
446
- @click.argument("prompt_file", type=click.Path(exists=True))
447
- @click.argument("directory_path", type=str)
882
+ language: Optional[str],
883
+ ) -> Optional[Tuple[str, float, str]]: # Modified return type
884
+ """Generate a unit test based on observed and desired outputs."""
885
+ quiet = ctx.obj.get("quiet", False)
886
+ command_name = "bug"
887
+ try:
888
+ unit_test_content, total_cost, model_name = bug_main(
889
+ ctx=ctx,
890
+ prompt_file=prompt_file,
891
+ code_file=code_file,
892
+ program_file=program_file,
893
+ current_output=current_output_file,
894
+ desired_output=desired_output_file,
895
+ output=output,
896
+ language=language,
897
+ )
898
+ return unit_test_content, total_cost, model_name
899
+ except Exception as e:
900
+ handle_error(e, command_name, quiet)
901
+ return None # Return None on failure
902
+
903
+
904
+ @cli.command("auto-deps")
905
+ @click.argument("prompt_file", type=click.Path(exists=True, dir_okay=False))
906
+ @click.argument("directory_path", type=str) # Path with potential glob pattern
448
907
  @click.option(
449
908
  "--output",
450
- type=click.Path(),
451
- help="Specify where to save the modified prompt file with dependencies inserted."
909
+ type=click.Path(writable=True),
910
+ default=None,
911
+ help="Specify where to save the modified prompt file (file or directory).",
452
912
  )
453
913
  @click.option(
454
914
  "--csv",
455
- type=click.Path(),
456
- default="./project_dependencies.csv",
457
- help="Specify the CSV file with dependency info."
915
+ "auto_deps_csv_path",
916
+ type=click.Path(dir_okay=False), # CSV path is a file
917
+ default=None, # Default handled by auto_deps_main or env var
918
+ help="Specify the CSV file for dependency info (default: project_dependencies.csv or PDD_AUTO_DEPS_CSV_PATH).",
458
919
  )
459
920
  @click.option(
460
921
  "--force-scan",
461
922
  is_flag=True,
462
- help="Force rescanning of all potential dependency files."
923
+ default=False,
924
+ help="Force rescanning of all potential dependency files.",
463
925
  )
464
926
  @click.pass_context
465
927
  @track_cost
466
928
  def auto_deps(
467
- ctx,
929
+ ctx: click.Context,
468
930
  prompt_file: str,
469
931
  directory_path: str,
470
932
  output: Optional[str],
471
- csv: Optional[str],
472
- force_scan: bool
473
- ) -> Tuple[str, float, str]:
474
- """
475
- Analyze a prompt file and a directory of potential dependencies,
476
- inserting needed dependencies into the prompt.
477
- """
478
- # Strip quotes if present
479
- if directory_path.startswith('"') and directory_path.endswith('"'):
480
- directory_path = directory_path[1:-1]
933
+ auto_deps_csv_path: Optional[str],
934
+ force_scan: bool,
935
+ ) -> Optional[Tuple[str, float, str]]: # Modified return type
936
+ """Analyze prompt and insert dependencies from a directory."""
937
+ quiet = ctx.obj.get("quiet", False)
938
+ command_name = "auto-deps"
939
+ try:
940
+ clean_directory_path = directory_path.strip('\"')
481
941
 
482
- return auto_deps_main(
483
- ctx=ctx,
484
- prompt_file=prompt_file,
485
- directory_path=directory_path,
486
- auto_deps_csv_path=csv,
487
- output=output,
488
- force_scan=force_scan
489
- )
942
+ modified_prompt, total_cost, model_name = auto_deps_main(
943
+ ctx=ctx,
944
+ prompt_file=prompt_file,
945
+ directory_path=clean_directory_path,
946
+ auto_deps_csv_path=auto_deps_csv_path,
947
+ output=output,
948
+ force_scan=force_scan,
949
+ )
950
+ return modified_prompt, total_cost, model_name
951
+ except Exception as e:
952
+ handle_error(e, command_name, quiet)
953
+ return None # Return None on failure
954
+
955
+
956
+ @cli.command("verify")
957
+ @click.argument("prompt_file", type=click.Path(exists=True, dir_okay=False))
958
+ @click.argument("code_file", type=click.Path(exists=True, dir_okay=False))
959
+ @click.argument("program_file", type=click.Path(exists=True, dir_okay=False))
960
+ @click.option(
961
+ "--output-results",
962
+ type=click.Path(writable=True),
963
+ default=None,
964
+ help="Specify where to save the verification results log (file or directory).",
965
+ )
966
+ @click.option(
967
+ "--output-code",
968
+ type=click.Path(writable=True),
969
+ default=None,
970
+ help="Specify where to save the verified code file (file or directory).",
971
+ )
972
+ @click.option(
973
+ "--max-attempts",
974
+ type=int,
975
+ default=3,
976
+ show_default=True,
977
+ help="Maximum number of fix attempts within the verification loop.",
978
+ )
979
+ @click.option(
980
+ "--budget",
981
+ type=float,
982
+ default=5.0,
983
+ show_default=True,
984
+ help="Maximum cost allowed for the verification and fixing process.",
985
+ )
986
+ @click.pass_context
987
+ @track_cost
988
+ def verify(
989
+ ctx: click.Context,
990
+ prompt_file: str,
991
+ code_file: str,
992
+ program_file: str,
993
+ output_results: Optional[str],
994
+ output_code: Optional[str],
995
+ max_attempts: int,
996
+ budget: float,
997
+ ) -> Optional[Tuple[Dict[str, Any], float, str]]: # Modified return type
998
+ """Verify code correctness against prompt using LLM judgment."""
999
+ quiet = ctx.obj.get("quiet", False)
1000
+ command_name = "verify"
1001
+ try:
1002
+ success, final_program, final_code, attempts, cost, model = fix_verification_main(
1003
+ ctx=ctx,
1004
+ prompt_file=prompt_file,
1005
+ code_file=code_file,
1006
+ program_file=program_file,
1007
+ output_results=output_results,
1008
+ output_code=output_code,
1009
+ loop=True,
1010
+ verification_program=program_file,
1011
+ max_attempts=max_attempts,
1012
+ budget=budget,
1013
+ )
1014
+ result_data = {
1015
+ "success": success,
1016
+ "attempts": attempts,
1017
+ "verified_code_path": output_code,
1018
+ "results_log_path": output_results,
1019
+ }
1020
+ return result_data, cost, model
1021
+ except Exception as e:
1022
+ handle_error(e, command_name, quiet)
1023
+ return None # Return None on failure
1024
+
1025
+
1026
+ @cli.command("install_completion")
1027
+ @click.pass_context
1028
+ # No @track_cost
1029
+ def install_completion_cmd(ctx: click.Context) -> None: # Return type remains None
1030
+ """Install shell completion for PDD."""
1031
+ quiet = ctx.obj.get("quiet", False)
1032
+ command_name = "install_completion"
1033
+ try:
1034
+ install_completion(quiet=quiet)
1035
+ # Return None on success (as intended)
1036
+ return None
1037
+ except Exception as e:
1038
+ handle_error(e, command_name, quiet)
1039
+ # Explicitly return None on failure as well, consistent with other commands
1040
+ return None
490
1041
 
491
1042
 
1043
+ # --- Entry Point ---
492
1044
  if __name__ == "__main__":
493
- cli()
1045
+ cli()