pdd-cli 0.0.2__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 (95) hide show
  1. pdd/__init__.py +0 -0
  2. pdd/auto_deps_main.py +98 -0
  3. pdd/auto_include.py +175 -0
  4. pdd/auto_update.py +73 -0
  5. pdd/bug_main.py +99 -0
  6. pdd/bug_to_unit_test.py +159 -0
  7. pdd/change.py +141 -0
  8. pdd/change_main.py +240 -0
  9. pdd/cli.py +607 -0
  10. pdd/cmd_test_main.py +155 -0
  11. pdd/code_generator.py +117 -0
  12. pdd/code_generator_main.py +66 -0
  13. pdd/comment_line.py +35 -0
  14. pdd/conflicts_in_prompts.py +143 -0
  15. pdd/conflicts_main.py +90 -0
  16. pdd/construct_paths.py +251 -0
  17. pdd/context_generator.py +133 -0
  18. pdd/context_generator_main.py +73 -0
  19. pdd/continue_generation.py +140 -0
  20. pdd/crash_main.py +127 -0
  21. pdd/data/language_format.csv +61 -0
  22. pdd/data/llm_model.csv +15 -0
  23. pdd/detect_change.py +142 -0
  24. pdd/detect_change_main.py +100 -0
  25. pdd/find_section.py +28 -0
  26. pdd/fix_code_loop.py +212 -0
  27. pdd/fix_code_module_errors.py +143 -0
  28. pdd/fix_error_loop.py +216 -0
  29. pdd/fix_errors_from_unit_tests.py +240 -0
  30. pdd/fix_main.py +138 -0
  31. pdd/generate_output_paths.py +194 -0
  32. pdd/generate_test.py +140 -0
  33. pdd/get_comment.py +55 -0
  34. pdd/get_extension.py +52 -0
  35. pdd/get_language.py +41 -0
  36. pdd/git_update.py +84 -0
  37. pdd/increase_tests.py +93 -0
  38. pdd/insert_includes.py +150 -0
  39. pdd/llm_invoke.py +304 -0
  40. pdd/load_prompt_template.py +59 -0
  41. pdd/pdd_completion.fish +72 -0
  42. pdd/pdd_completion.sh +141 -0
  43. pdd/pdd_completion.zsh +418 -0
  44. pdd/postprocess.py +121 -0
  45. pdd/postprocess_0.py +52 -0
  46. pdd/preprocess.py +199 -0
  47. pdd/preprocess_main.py +72 -0
  48. pdd/process_csv_change.py +182 -0
  49. pdd/prompts/auto_include_LLM.prompt +230 -0
  50. pdd/prompts/bug_to_unit_test_LLM.prompt +17 -0
  51. pdd/prompts/change_LLM.prompt +34 -0
  52. pdd/prompts/conflict_LLM.prompt +23 -0
  53. pdd/prompts/continue_generation_LLM.prompt +3 -0
  54. pdd/prompts/detect_change_LLM.prompt +65 -0
  55. pdd/prompts/example_generator_LLM.prompt +10 -0
  56. pdd/prompts/extract_auto_include_LLM.prompt +6 -0
  57. pdd/prompts/extract_code_LLM.prompt +22 -0
  58. pdd/prompts/extract_conflict_LLM.prompt +19 -0
  59. pdd/prompts/extract_detect_change_LLM.prompt +19 -0
  60. pdd/prompts/extract_program_code_fix_LLM.prompt +16 -0
  61. pdd/prompts/extract_prompt_change_LLM.prompt +7 -0
  62. pdd/prompts/extract_prompt_split_LLM.prompt +9 -0
  63. pdd/prompts/extract_prompt_update_LLM.prompt +8 -0
  64. pdd/prompts/extract_promptline_LLM.prompt +11 -0
  65. pdd/prompts/extract_unit_code_fix_LLM.prompt +332 -0
  66. pdd/prompts/extract_xml_LLM.prompt +7 -0
  67. pdd/prompts/fix_code_module_errors_LLM.prompt +17 -0
  68. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +62 -0
  69. pdd/prompts/generate_test_LLM.prompt +12 -0
  70. pdd/prompts/increase_tests_LLM.prompt +16 -0
  71. pdd/prompts/insert_includes_LLM.prompt +30 -0
  72. pdd/prompts/split_LLM.prompt +94 -0
  73. pdd/prompts/summarize_file_LLM.prompt +11 -0
  74. pdd/prompts/trace_LLM.prompt +30 -0
  75. pdd/prompts/trim_results_LLM.prompt +83 -0
  76. pdd/prompts/trim_results_start_LLM.prompt +45 -0
  77. pdd/prompts/unfinished_prompt_LLM.prompt +18 -0
  78. pdd/prompts/update_prompt_LLM.prompt +19 -0
  79. pdd/prompts/xml_convertor_LLM.prompt +54 -0
  80. pdd/split.py +119 -0
  81. pdd/split_main.py +103 -0
  82. pdd/summarize_directory.py +212 -0
  83. pdd/trace.py +135 -0
  84. pdd/trace_main.py +108 -0
  85. pdd/track_cost.py +102 -0
  86. pdd/unfinished_prompt.py +114 -0
  87. pdd/update_main.py +96 -0
  88. pdd/update_prompt.py +115 -0
  89. pdd/xml_tagger.py +122 -0
  90. pdd_cli-0.0.2.dist-info/LICENSE +7 -0
  91. pdd_cli-0.0.2.dist-info/METADATA +225 -0
  92. pdd_cli-0.0.2.dist-info/RECORD +95 -0
  93. pdd_cli-0.0.2.dist-info/WHEEL +5 -0
  94. pdd_cli-0.0.2.dist-info/entry_points.txt +2 -0
  95. pdd_cli-0.0.2.dist-info/top_level.txt +1 -0
pdd/cli.py ADDED
@@ -0,0 +1,607 @@
1
+ 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
7
+
8
+ import click
9
+ from rich import print as rprint
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+
13
+ # ----------------------------------------------------------------------
14
+ # Dynamically determine PDD_PATH at runtime.
15
+ # ----------------------------------------------------------------------
16
+ def get_local_pdd_path() -> str:
17
+ """
18
+ Return the PDD_PATH directory.
19
+ First check the environment variable. If not set, attempt to
20
+ deduce it via importlib.resources. If that fails, abort.
21
+ """
22
+ if "PDD_PATH" in os.environ:
23
+ return os.environ["PDD_PATH"]
24
+ else:
25
+ try:
26
+ with importlib.resources.path("pdd", "cli.py") as p:
27
+ fallback_path = str(p.parent)
28
+ # Also set it back into the environment for consistency
29
+ os.environ["PDD_PATH"] = fallback_path
30
+ return fallback_path
31
+ except ImportError:
32
+ rprint(
33
+ "[red]Error: Could not determine the path to the 'pdd' package. "
34
+ "Please set the PDD_PATH environment variable manually.[/red]"
35
+ )
36
+ sys.exit(1)
37
+
38
+ get_local_pdd_path()
39
+ # ----------------------------------------------------------------------
40
+ # Import sub-command modules
41
+ # ----------------------------------------------------------------------
42
+ from .code_generator_main import code_generator_main
43
+ from .context_generator_main import context_generator_main
44
+ from .cmd_test_main import cmd_test_main
45
+ from .preprocess_main import preprocess_main
46
+ from .fix_main import fix_main
47
+ from .split_main import split_main
48
+ from .change_main import change_main
49
+ from .update_main import update_main
50
+ from .detect_change_main import detect_change_main
51
+ from .conflicts_main import conflicts_main
52
+ from .crash_main import crash_main
53
+ from .trace_main import trace_main
54
+ from .bug_main import bug_main
55
+ from .track_cost import track_cost
56
+ from .auto_update import auto_update
57
+ from .auto_deps_main import auto_deps_main
58
+
59
+ console = Console()
60
+
61
+ @click.group()
62
+ @click.option("--force", is_flag=True, help="Overwrite existing files without asking for confirmation.")
63
+ @click.option("--strength", type=float, default=0.5, help="Set the strength of the AI model (0.0 to 1.0).")
64
+ @click.option("--temperature", type=float, default=0.0, help="Set the temperature of the AI model.")
65
+ @click.option("--verbose", is_flag=True, help="Increase output verbosity for more detailed information.")
66
+ @click.option("--quiet", is_flag=True, help="Decrease output verbosity for minimal information.")
67
+ @click.option("--output-cost", type=click.Path(), help="Enable cost tracking and output a CSV file with usage details.")
68
+ @click.option("--review-examples", is_flag=True,
69
+ help="Review and optionally exclude few-shot examples before command execution.")
70
+ @click.version_option(version="0.0.2")
71
+ @click.pass_context
72
+ def cli(
73
+ ctx,
74
+ force: bool,
75
+ strength: float,
76
+ temperature: float,
77
+ verbose: bool,
78
+ quiet: bool,
79
+ output_cost: Optional[str],
80
+ review_examples: bool,
81
+ ):
82
+ """
83
+ PDD (Prompt-Driven Development) Command Line Interface
84
+ """
85
+ ctx.ensure_object(dict)
86
+ ctx.obj["force"] = force
87
+ ctx.obj["strength"] = strength
88
+ ctx.obj["temperature"] = temperature
89
+ ctx.obj["verbose"] = verbose
90
+ ctx.obj["quiet"] = quiet
91
+ ctx.obj["output_cost"] = output_cost or os.environ.get("PDD_OUTPUT_COST_PATH")
92
+ ctx.obj["review_examples"] = review_examples
93
+
94
+ # Auto-update check, but handle EOF errors so tests do not crash.
95
+ auto_update_enabled = os.environ.get("PDD_AUTO_UPDATE", "true").lower() == "true"
96
+ if auto_update_enabled and sys.stdin.isatty():
97
+ try:
98
+ auto_update()
99
+ except EOFError:
100
+ pass
101
+
102
+
103
+ @cli.command()
104
+ @click.argument("prompt_file", type=click.Path(exists=True))
105
+ @click.option("--output", type=click.Path(), help="Specify where to save the generated code.")
106
+ @click.pass_context
107
+ @track_cost
108
+ def generate(ctx, prompt_file: str, output: Optional[str]) -> Tuple[str, float, str]:
109
+ """Create runnable code from a prompt file."""
110
+ return code_generator_main(ctx, prompt_file, output)
111
+
112
+
113
+ @cli.command()
114
+ @click.argument("prompt_file", type=click.Path(exists=True))
115
+ @click.argument("code_file", type=click.Path(exists=True))
116
+ @click.option("--output", type=click.Path(), help="Specify where to save the generated example code.")
117
+ @click.pass_context
118
+ @track_cost
119
+ def example(
120
+ ctx,
121
+ prompt_file: str,
122
+ code_file: str,
123
+ output: Optional[str]
124
+ ) -> Tuple[str, float, str]:
125
+ """Create an example file from an existing code file and the prompt that generated it."""
126
+ return context_generator_main(ctx, prompt_file, code_file, output)
127
+
128
+
129
+ @cli.command()
130
+ @click.argument("prompt_file", type=click.Path(exists=True))
131
+ @click.argument("code_file", type=click.Path(exists=True))
132
+ @click.option("--output", type=click.Path(), help="Specify where to save the generated test file.")
133
+ @click.option("--language", help="Specify the programming language.")
134
+ @click.option(
135
+ "--coverage-report",
136
+ type=click.Path(exists=True),
137
+ default=None,
138
+ help="Path to a coverage report for enhancing tests."
139
+ )
140
+ @click.option(
141
+ "--existing-tests",
142
+ type=click.Path(exists=True),
143
+ default=None,
144
+ help="Existing test file to merge or build upon."
145
+ )
146
+ @click.option("--target-coverage", type=float, default=None, help="Desired coverage percentage.")
147
+ @click.option("--merge", is_flag=True, default=False, help="Merge new tests into existing tests.")
148
+ @click.pass_context
149
+ @track_cost
150
+ def test(
151
+ ctx,
152
+ prompt_file: str,
153
+ code_file: str,
154
+ output: Optional[str],
155
+ language: Optional[str],
156
+ coverage_report: Optional[str],
157
+ existing_tests: Optional[str],
158
+ target_coverage: Optional[float],
159
+ merge: bool,
160
+ ) -> Tuple[str, float, str]:
161
+ """
162
+ Generate or enhance unit tests for a given code file and its corresponding prompt file.
163
+ """
164
+ return cmd_test_main(
165
+ ctx,
166
+ prompt_file,
167
+ code_file,
168
+ output,
169
+ language,
170
+ coverage_report,
171
+ existing_tests,
172
+ target_coverage,
173
+ merge,
174
+ )
175
+
176
+
177
+ @cli.command()
178
+ @click.argument("prompt_file", type=click.Path(exists=True))
179
+ @click.option("--output", type=click.Path(), help="Specify where to save the preprocessed prompt file.")
180
+ @click.option("--xml", is_flag=True, help="Automatically insert XML delimiters for complex prompts.")
181
+ @click.option("--recursive", is_flag=True, help="Recursively preprocess all prompt files in the prompt file.")
182
+ @click.option("--double", is_flag=True, help="Curly brackets will be doubled.")
183
+ @click.option("--exclude", multiple=True, help="List of keys to exclude from curly bracket doubling.")
184
+ @click.pass_context
185
+ @track_cost
186
+ def preprocess(
187
+ ctx,
188
+ prompt_file: str,
189
+ output: Optional[str],
190
+ xml: bool,
191
+ recursive: bool,
192
+ double: bool,
193
+ exclude: List[str]
194
+ ) -> Tuple[str, float, str]:
195
+ """Preprocess prompt files and save the results."""
196
+ return preprocess_main(ctx, prompt_file, output, xml, recursive, double, exclude)
197
+
198
+
199
+ @cli.command()
200
+ @click.argument("prompt_file", type=click.Path(exists=True))
201
+ @click.argument("code_file", type=click.Path(exists=True))
202
+ @click.argument("unit_test_file", type=click.Path(exists=True))
203
+ @click.argument("error_file", type=click.Path(exists=False))
204
+ @click.option("--output-test", type=click.Path(), help="Where to save the fixed unit test file.")
205
+ @click.option("--output-code", type=click.Path(), help="Where to save the fixed code file.")
206
+ @click.option(
207
+ "--output-results",
208
+ type=click.Path(),
209
+ help="Where to save the results from the error fixing process."
210
+ )
211
+ @click.option("--loop", is_flag=True, help="Enable iterative fixing process.")
212
+ @click.option(
213
+ "--verification-program",
214
+ type=click.Path(exists=True),
215
+ help="Path to a Python program that verifies code correctness."
216
+ )
217
+ @click.option("--max-attempts", type=int, default=3, help="Maximum fix attempts before giving up.")
218
+ @click.option("--budget", type=float, default=5.0, help="Maximum cost allowed for the fixing process.")
219
+ @click.option(
220
+ "--auto-submit",
221
+ is_flag=True,
222
+ help="Automatically submit the example if all unit tests pass during the fix loop."
223
+ )
224
+ @click.pass_context
225
+ @track_cost
226
+ def fix(
227
+ ctx,
228
+ prompt_file: str,
229
+ code_file: str,
230
+ unit_test_file: str,
231
+ error_file: str,
232
+ output_test: Optional[str],
233
+ output_code: Optional[str],
234
+ output_results: Optional[str],
235
+ loop: bool,
236
+ verification_program: Optional[str],
237
+ max_attempts: int,
238
+ budget: float,
239
+ auto_submit: bool
240
+ ) -> Tuple[bool, str, str, int, float, str]:
241
+ """Fix errors in code and unit tests based on error messages and the original prompt file."""
242
+ return fix_main(
243
+ ctx,
244
+ prompt_file,
245
+ code_file,
246
+ unit_test_file,
247
+ error_file,
248
+ output_test,
249
+ output_code,
250
+ output_results,
251
+ loop,
252
+ verification_program,
253
+ max_attempts,
254
+ budget,
255
+ auto_submit,
256
+ )
257
+
258
+
259
+ @cli.command()
260
+ @click.argument("input_prompt", type=click.Path(exists=True))
261
+ @click.argument("input_code", type=click.Path(exists=True))
262
+ @click.argument("example_code", type=click.Path(exists=True))
263
+ @click.option("--output-sub", type=click.Path(), help="Where to save the generated sub-prompt file.")
264
+ @click.option("--output-modified", type=click.Path(), help="Where to save the modified prompt file.")
265
+ @click.pass_context
266
+ @track_cost
267
+ def split(
268
+ ctx,
269
+ input_prompt: str,
270
+ input_code: str,
271
+ example_code: str,
272
+ output_sub: Optional[str],
273
+ output_modified: Optional[str],
274
+ ) -> Tuple[str, str, float]:
275
+ """Split large complex prompt files into smaller, more manageable prompt files."""
276
+ return split_main(ctx, input_prompt, input_code, example_code, output_sub, output_modified)
277
+
278
+
279
+ @cli.command()
280
+ @click.argument("change_prompt_file", type=click.Path(exists=True))
281
+ @click.argument("input_code", type=click.Path(exists=True))
282
+ @click.argument("input_prompt_file", type=click.Path(exists=False), required=False)
283
+ @click.option("--output", type=click.Path(), help="Where to save the modified prompt file.")
284
+ @click.option("--csv", is_flag=True, help="Use a CSV file for change prompts instead of a single text file.")
285
+ @click.pass_context
286
+ @track_cost
287
+ def change(
288
+ ctx,
289
+ change_prompt_file: str,
290
+ input_code: str,
291
+ input_prompt_file: Optional[str],
292
+ output: Optional[str],
293
+ csv: bool
294
+ ) -> Tuple[str, float, str]:
295
+ """Modify an input prompt file based on a change prompt and the corresponding input code."""
296
+ return change_main(ctx, change_prompt_file, input_code, input_prompt_file, output, csv)
297
+
298
+
299
+ @cli.command()
300
+ @click.argument("input_prompt_file", type=click.Path(exists=True))
301
+ @click.argument("modified_code_file", type=click.Path(exists=True))
302
+ @click.argument("input_code_file", type=click.Path(exists=True), required=False)
303
+ @click.option("--output", type=click.Path(), help="Where to save the modified prompt file.")
304
+ @click.option(
305
+ "--git",
306
+ is_flag=True,
307
+ help="Use git history to find the original code file instead of providing INPUT_CODE_FILE."
308
+ )
309
+ @click.pass_context
310
+ @track_cost
311
+ def update(
312
+ ctx,
313
+ input_prompt_file: str,
314
+ modified_code_file: str,
315
+ input_code_file: Optional[str],
316
+ output: Optional[str],
317
+ git: bool,
318
+ ) -> Tuple[str, float, str]:
319
+ """Update the original prompt file based on the original code and the modified code."""
320
+ return update_main(ctx, input_prompt_file, modified_code_file, input_code_file, output, git)
321
+
322
+
323
+ @cli.command()
324
+ @click.argument("prompt_files", nargs=-1, type=click.Path(exists=True))
325
+ @click.argument("change_file", type=click.Path(exists=True))
326
+ @click.option("--output", type=click.Path(), help="Where to save CSV analysis results.")
327
+ @click.pass_context
328
+ @track_cost
329
+ def detect(
330
+ ctx,
331
+ prompt_files: List[str],
332
+ change_file: str,
333
+ output: Optional[str]
334
+ ) -> Tuple[List[dict], float, str]:
335
+ """Analyze a list of prompt files and a change description to see which prompts need changes."""
336
+ return detect_change_main(ctx, prompt_files, change_file, output)
337
+
338
+
339
+ @cli.command()
340
+ @click.argument("prompt1", type=click.Path(exists=True))
341
+ @click.argument("prompt2", type=click.Path(exists=True))
342
+ @click.option("--output", type=click.Path(), help="Where to save the conflict analysis CSV.")
343
+ @click.pass_context
344
+ @track_cost
345
+ def conflicts(
346
+ ctx,
347
+ prompt1: str,
348
+ prompt2: str,
349
+ output: Optional[str]
350
+ ) -> Tuple[List[dict], float, str]:
351
+ """Analyze two prompt files to find conflicts and suggest resolutions."""
352
+ return conflicts_main(ctx, prompt1, prompt2, output)
353
+
354
+
355
+ @cli.command()
356
+ @click.argument("prompt_file", type=click.Path(exists=True))
357
+ @click.argument("code_file", type=click.Path(exists=True))
358
+ @click.argument("program_file", type=click.Path(exists=True))
359
+ @click.argument("error_file", type=click.Path())
360
+ @click.option("--output", type=click.Path(), help="Where to save the fixed code file.")
361
+ @click.option("--output-program", type=click.Path(), help="Where to save the fixed program file.")
362
+ @click.option("--loop", is_flag=True, help="Enable iterative fixing process.")
363
+ @click.option("--max-attempts", type=int, default=3, help="Maximum fix attempts before giving up.")
364
+ @click.option("--budget", type=float, default=5.0, help="Maximum cost allowed for the fixing process.")
365
+ @click.pass_context
366
+ @track_cost
367
+ def crash(
368
+ ctx,
369
+ prompt_file: str,
370
+ code_file: str,
371
+ program_file: str,
372
+ error_file: str,
373
+ output: Optional[str],
374
+ output_program: Optional[str],
375
+ loop: bool,
376
+ max_attempts: int,
377
+ budget: float
378
+ ) -> Tuple[bool, str, str, int, float, str]:
379
+ """Fix errors in a code module that caused a program to crash."""
380
+ return crash_main(
381
+ ctx,
382
+ prompt_file,
383
+ code_file,
384
+ program_file,
385
+ error_file,
386
+ output,
387
+ output_program,
388
+ loop,
389
+ max_attempts,
390
+ budget,
391
+ )
392
+
393
+ # ----------------------------------------------------------------------
394
+ # Simplified shell RC path logic
395
+ # ----------------------------------------------------------------------
396
+ def get_shell_rc_path(shell: str) -> Optional[str]:
397
+ """Return the default RC file path for a given shell name."""
398
+ home = os.path.expanduser("~")
399
+ if shell == "bash":
400
+ return os.path.join(home, ".bashrc")
401
+ elif shell == "zsh":
402
+ return os.path.join(home, ".zshrc")
403
+ elif shell == "fish":
404
+ return os.path.join(home, ".config", "fish", "config.fish")
405
+ return None
406
+
407
+
408
+ def get_current_shell() -> Optional[str]:
409
+
410
+
411
+ """Determine the currently running shell more reliably."""
412
+ if not os.environ.get('PYTEST_CURRENT_TEST'):
413
+ # Method 1: Check process name using 'ps'
414
+ try:
415
+ import subprocess
416
+ result = subprocess.run(['ps', '-p', str(os.getppid()), '-o', 'comm='],
417
+ capture_output=True, text=True)
418
+ if result.returncode == 0:
419
+ # Strip whitespace and get basename without path
420
+ shell = os.path.basename(result.stdout.strip())
421
+ # Remove leading dash if present (login shell)
422
+ return shell.lstrip('-')
423
+ except (subprocess.SubprocessError, FileNotFoundError):
424
+ pass
425
+
426
+ # Method 2: Check $0 special parameter
427
+ try:
428
+ result = subprocess.run(['sh', '-c', 'echo "$0"'],
429
+ capture_output=True, text=True)
430
+ if result.returncode == 0:
431
+ shell = os.path.basename(result.stdout.strip())
432
+ return shell.lstrip('-')
433
+ except (subprocess.SubprocessError, FileNotFoundError):
434
+ pass
435
+
436
+ # Fallback to SHELL env var if all else fails
437
+ return os.path.basename(os.environ.get("SHELL", ""))
438
+
439
+
440
+ def get_completion_script_extension(shell: str) -> str:
441
+ """Get the appropriate file extension for shell completion scripts."""
442
+ mapping = {
443
+ "bash": "sh",
444
+ "zsh": "zsh",
445
+ "fish": "fish"
446
+ }
447
+ return mapping.get(shell, shell)
448
+
449
+
450
+ @cli.command(name="install_completion")
451
+ def install_completion():
452
+ """
453
+ Install shell completion for the PDD CLI by detecting the user’s shell,
454
+ copying the relevant completion script, and appending a source command
455
+ to the user’s shell RC file if not already present.
456
+ """
457
+ shell = get_current_shell()
458
+ rc_file = get_shell_rc_path(shell)
459
+ if not rc_file:
460
+ rprint(f"[red]Unsupported shell: {shell}[/red]")
461
+ raise click.Abort()
462
+
463
+ ext = get_completion_script_extension(shell)
464
+
465
+ # Dynamically look up the local path at runtime:
466
+ local_pdd_path = get_local_pdd_path()
467
+ completion_script_path = os.path.join(local_pdd_path, f"pdd_completion.{ext}")
468
+
469
+ if not os.path.exists(completion_script_path):
470
+ rprint(f"[red]Completion script not found: {completion_script_path}[/red]")
471
+ raise click.Abort()
472
+
473
+ source_command = f"source {completion_script_path}"
474
+
475
+ try:
476
+ # Ensure the RC file exists (create if missing).
477
+ if not os.path.exists(rc_file):
478
+ os.makedirs(os.path.dirname(rc_file), exist_ok=True)
479
+ with open(rc_file, "w", encoding="utf-8") as cf:
480
+ cf.write("")
481
+
482
+ # Read existing content
483
+ with open(rc_file, "r", encoding="utf-8") as cf:
484
+ content = cf.read()
485
+
486
+ if source_command not in content:
487
+ with open(rc_file, "a", encoding="utf-8") as rf:
488
+ rf.write(f"\n# PDD CLI completion\n{source_command}\n")
489
+
490
+ rprint(f"[green]Shell completion installed for {shell}.[/green]")
491
+ rprint(f"Please restart your shell or run 'source {rc_file}' to enable completion.")
492
+ else:
493
+ rprint(f"[yellow]Shell completion already installed for {shell}.[/yellow]")
494
+ except OSError as exc:
495
+ rprint(f"[red]Failed to install shell completion: {exc}[/red]")
496
+ raise click.Abort()
497
+
498
+
499
+ @cli.command()
500
+ @click.argument("prompt_file", type=click.Path(exists=True))
501
+ @click.argument("code_file", type=click.Path(exists=True))
502
+ @click.argument("code_line", type=int)
503
+ @click.option("--output", type=click.Path(), help="Where to save the trace analysis results.")
504
+ @click.pass_context
505
+ @track_cost
506
+ def trace(
507
+ ctx,
508
+ prompt_file: str,
509
+ code_file: str,
510
+ code_line: int,
511
+ output: Optional[str]
512
+ ) -> Tuple[str, float, str]:
513
+ """
514
+ Find the associated line number between a prompt file and the generated code.
515
+ """
516
+ return trace_main(ctx, prompt_file, code_file, code_line, output)
517
+
518
+
519
+ @cli.command()
520
+ @click.argument("prompt_file", type=click.Path(exists=True))
521
+ @click.argument("code_file", type=click.Path(exists=True))
522
+ @click.argument("program_file", type=click.Path(exists=True))
523
+ @click.argument("current_output", type=click.Path(exists=True))
524
+ @click.argument("desired_output", type=click.Path(exists=True))
525
+ @click.option(
526
+ "--output",
527
+ metavar="LOCATION",
528
+ type=click.Path(),
529
+ help="Where to save the bug-related unit test."
530
+ )
531
+ @click.option("--language", default="Python", help="Specify the programming language.")
532
+ @click.pass_context
533
+ @track_cost
534
+ def bug(
535
+ ctx,
536
+ prompt_file: str,
537
+ code_file: str,
538
+ program_file: str,
539
+ current_output: str,
540
+ desired_output: str,
541
+ output: Optional[str],
542
+ language: Optional[str]
543
+ ) -> Tuple[str, float, str]:
544
+ """
545
+ Generate a unit test based on observed and desired outputs for given code and prompt.
546
+ """
547
+ return bug_main(
548
+ ctx,
549
+ prompt_file,
550
+ code_file,
551
+ program_file,
552
+ current_output,
553
+ desired_output,
554
+ output,
555
+ language
556
+ )
557
+
558
+
559
+ @cli.command()
560
+ @click.argument("prompt_file", type=click.Path(exists=True))
561
+ @click.argument("directory_path", type=str)
562
+ @click.option(
563
+ "--output",
564
+ type=click.Path(),
565
+ help="Specify where to save the modified prompt file with dependencies inserted."
566
+ )
567
+ @click.option(
568
+ "--csv",
569
+ type=click.Path(),
570
+ default="./project_dependencies.csv",
571
+ help="Specify the CSV file with dependency info."
572
+ )
573
+ @click.option(
574
+ "--force-scan",
575
+ is_flag=True,
576
+ help="Force rescanning of all potential dependency files."
577
+ )
578
+ @click.pass_context
579
+ @track_cost
580
+ def auto_deps(
581
+ ctx,
582
+ prompt_file: str,
583
+ directory_path: str,
584
+ output: Optional[str],
585
+ csv: Optional[str],
586
+ force_scan: bool
587
+ ) -> Tuple[str, float, str]:
588
+ """
589
+ Analyze a prompt file and a directory of potential dependencies,
590
+ inserting needed dependencies into the prompt.
591
+ """
592
+ # Strip quotes if present
593
+ if directory_path.startswith('"') and directory_path.endswith('"'):
594
+ directory_path = directory_path[1:-1]
595
+
596
+ return auto_deps_main(
597
+ ctx=ctx,
598
+ prompt_file=prompt_file,
599
+ directory_path=directory_path,
600
+ auto_deps_csv_path=csv,
601
+ output=output,
602
+ force_scan=force_scan
603
+ )
604
+
605
+
606
+ if __name__ == "__main__":
607
+ cli()