pdd-cli 0.0.45__py3-none-any.whl → 0.0.118__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. pdd/__init__.py +40 -8
  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 +598 -0
  7. pdd/agentic_crash.py +534 -0
  8. pdd/agentic_e2e_fix.py +319 -0
  9. pdd/agentic_e2e_fix_orchestrator.py +426 -0
  10. pdd/agentic_fix.py +1294 -0
  11. pdd/agentic_langtest.py +162 -0
  12. pdd/agentic_update.py +387 -0
  13. pdd/agentic_verify.py +183 -0
  14. pdd/architecture_sync.py +565 -0
  15. pdd/auth_service.py +210 -0
  16. pdd/auto_deps_main.py +71 -51
  17. pdd/auto_include.py +245 -5
  18. pdd/auto_update.py +125 -47
  19. pdd/bug_main.py +196 -23
  20. pdd/bug_to_unit_test.py +2 -0
  21. pdd/change_main.py +11 -4
  22. pdd/cli.py +22 -1181
  23. pdd/cmd_test_main.py +350 -150
  24. pdd/code_generator.py +60 -18
  25. pdd/code_generator_main.py +790 -57
  26. pdd/commands/__init__.py +48 -0
  27. pdd/commands/analysis.py +306 -0
  28. pdd/commands/auth.py +309 -0
  29. pdd/commands/connect.py +290 -0
  30. pdd/commands/fix.py +163 -0
  31. pdd/commands/generate.py +257 -0
  32. pdd/commands/maintenance.py +175 -0
  33. pdd/commands/misc.py +87 -0
  34. pdd/commands/modify.py +256 -0
  35. pdd/commands/report.py +144 -0
  36. pdd/commands/sessions.py +284 -0
  37. pdd/commands/templates.py +215 -0
  38. pdd/commands/utility.py +110 -0
  39. pdd/config_resolution.py +58 -0
  40. pdd/conflicts_main.py +8 -3
  41. pdd/construct_paths.py +589 -111
  42. pdd/context_generator.py +10 -2
  43. pdd/context_generator_main.py +175 -76
  44. pdd/continue_generation.py +53 -10
  45. pdd/core/__init__.py +33 -0
  46. pdd/core/cli.py +527 -0
  47. pdd/core/cloud.py +237 -0
  48. pdd/core/dump.py +554 -0
  49. pdd/core/errors.py +67 -0
  50. pdd/core/remote_session.py +61 -0
  51. pdd/core/utils.py +90 -0
  52. pdd/crash_main.py +262 -33
  53. pdd/data/language_format.csv +71 -63
  54. pdd/data/llm_model.csv +20 -18
  55. pdd/detect_change_main.py +5 -4
  56. pdd/docs/prompting_guide.md +864 -0
  57. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  58. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  59. pdd/fix_code_loop.py +523 -95
  60. pdd/fix_code_module_errors.py +6 -2
  61. pdd/fix_error_loop.py +491 -92
  62. pdd/fix_errors_from_unit_tests.py +4 -3
  63. pdd/fix_main.py +278 -21
  64. pdd/fix_verification_errors.py +12 -100
  65. pdd/fix_verification_errors_loop.py +529 -286
  66. pdd/fix_verification_main.py +294 -89
  67. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  68. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
  69. pdd/frontend/dist/index.html +376 -0
  70. pdd/frontend/dist/logo.svg +33 -0
  71. pdd/generate_output_paths.py +139 -15
  72. pdd/generate_test.py +218 -146
  73. pdd/get_comment.py +19 -44
  74. pdd/get_extension.py +8 -9
  75. pdd/get_jwt_token.py +318 -22
  76. pdd/get_language.py +8 -7
  77. pdd/get_run_command.py +75 -0
  78. pdd/get_test_command.py +68 -0
  79. pdd/git_update.py +70 -19
  80. pdd/incremental_code_generator.py +2 -2
  81. pdd/insert_includes.py +13 -4
  82. pdd/llm_invoke.py +1711 -181
  83. pdd/load_prompt_template.py +19 -12
  84. pdd/path_resolution.py +140 -0
  85. pdd/pdd_completion.fish +25 -2
  86. pdd/pdd_completion.sh +30 -4
  87. pdd/pdd_completion.zsh +79 -4
  88. pdd/postprocess.py +14 -4
  89. pdd/preprocess.py +293 -24
  90. pdd/preprocess_main.py +41 -6
  91. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  92. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  93. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  94. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  95. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  96. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  97. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  98. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  99. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  100. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  101. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  102. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  103. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
  104. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  105. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  106. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  107. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  108. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  109. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  110. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  111. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  112. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  113. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  114. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  115. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  116. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  117. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  118. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  119. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  120. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  121. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  122. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  123. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  124. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  125. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  126. pdd/prompts/agentic_update_LLM.prompt +925 -0
  127. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  128. pdd/prompts/auto_include_LLM.prompt +122 -905
  129. pdd/prompts/change_LLM.prompt +3093 -1
  130. pdd/prompts/detect_change_LLM.prompt +686 -27
  131. pdd/prompts/example_generator_LLM.prompt +22 -1
  132. pdd/prompts/extract_code_LLM.prompt +5 -1
  133. pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
  134. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  135. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  136. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  137. pdd/prompts/fix_code_module_errors_LLM.prompt +12 -2
  138. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +9 -0
  139. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  140. pdd/prompts/generate_test_LLM.prompt +41 -7
  141. pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
  142. pdd/prompts/increase_tests_LLM.prompt +1 -5
  143. pdd/prompts/insert_includes_LLM.prompt +316 -186
  144. pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
  145. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  146. pdd/prompts/trace_LLM.prompt +25 -22
  147. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  148. pdd/prompts/update_prompt_LLM.prompt +22 -1
  149. pdd/pytest_output.py +127 -12
  150. pdd/remote_session.py +876 -0
  151. pdd/render_mermaid.py +236 -0
  152. pdd/server/__init__.py +52 -0
  153. pdd/server/app.py +335 -0
  154. pdd/server/click_executor.py +587 -0
  155. pdd/server/executor.py +338 -0
  156. pdd/server/jobs.py +661 -0
  157. pdd/server/models.py +241 -0
  158. pdd/server/routes/__init__.py +31 -0
  159. pdd/server/routes/architecture.py +451 -0
  160. pdd/server/routes/auth.py +364 -0
  161. pdd/server/routes/commands.py +929 -0
  162. pdd/server/routes/config.py +42 -0
  163. pdd/server/routes/files.py +603 -0
  164. pdd/server/routes/prompts.py +1322 -0
  165. pdd/server/routes/websocket.py +473 -0
  166. pdd/server/security.py +243 -0
  167. pdd/server/terminal_spawner.py +209 -0
  168. pdd/server/token_counter.py +222 -0
  169. pdd/setup_tool.py +648 -0
  170. pdd/simple_math.py +2 -0
  171. pdd/split_main.py +3 -2
  172. pdd/summarize_directory.py +237 -195
  173. pdd/sync_animation.py +8 -4
  174. pdd/sync_determine_operation.py +839 -112
  175. pdd/sync_main.py +351 -57
  176. pdd/sync_orchestration.py +1400 -756
  177. pdd/sync_tui.py +848 -0
  178. pdd/template_expander.py +161 -0
  179. pdd/template_registry.py +264 -0
  180. pdd/templates/architecture/architecture_json.prompt +237 -0
  181. pdd/templates/generic/generate_prompt.prompt +174 -0
  182. pdd/trace.py +168 -12
  183. pdd/trace_main.py +4 -3
  184. pdd/track_cost.py +140 -63
  185. pdd/unfinished_prompt.py +51 -4
  186. pdd/update_main.py +567 -67
  187. pdd/update_model_costs.py +2 -2
  188. pdd/update_prompt.py +19 -4
  189. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +29 -11
  190. pdd_cli-0.0.118.dist-info/RECORD +227 -0
  191. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +1 -1
  192. pdd_cli-0.0.45.dist-info/RECORD +0 -116
  193. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
  194. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
  195. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
pdd/auto_update.py CHANGED
@@ -1,67 +1,111 @@
1
- """This module provides a function to automatically update the package."""
1
+ """This module provides a function to automatically update the package.
2
+
3
+ It can detect whether the current tool was installed via UV or pip, check
4
+ PyPI for a newer version, prompt the user, and perform an upgrade using the
5
+ appropriate installer with sensible fallbacks.
6
+ """
2
7
  import importlib.metadata
8
+ import os
3
9
  import shutil
4
10
  import subprocess
5
11
  import sys
6
- from typing import Optional
12
+ from typing import Optional, Tuple, List
13
+
7
14
  import requests
8
15
  import semver
9
16
 
10
17
 
11
- def detect_installation_method(sys_executable):
12
- """
13
- Detect if package is installed via UV or pip.
14
-
18
+ def detect_installation_method(sys_executable: str) -> str:
19
+ """Detect whether the package is installed via UV or pip.
20
+
21
+ The detection is based on the path of the Python executable. The path is
22
+ normalized so that both Unix ("/") and Windows ("\\") separators are
23
+ handled uniformly. If typical UV tool installation markers are present in
24
+ the normalized path, "uv" is returned; otherwise "pip" is returned.
25
+
15
26
  Args:
16
- sys_executable (str): Path to the Python executable
17
-
27
+ sys_executable: Path to the Python executable (e.g. ``sys.executable``).
28
+
18
29
  Returns:
19
- str: "uv" if installed via UV, "pip" otherwise
30
+ "uv" if a UV-style installation path is detected, otherwise "pip".
20
31
  """
32
+ # Normalize path separators to support both Unix (/) and Windows (\) paths
33
+ normalized_path = sys_executable.replace("\\", "/")
34
+
21
35
  # Check if executable path contains UV paths
22
- if any(marker in sys_executable for marker in ["/uv/tools/", ".local/share/uv/"]):
36
+ if any(marker in normalized_path for marker in ["/uv/tools/", ".local/share/uv/"]):
23
37
  return "uv"
24
38
  return "pip" # Default to pip for all other cases
25
39
 
26
40
 
27
- def get_upgrade_command(package_name, installation_method):
28
- """
29
- Return appropriate upgrade command based on installation method.
30
-
41
+ def get_upgrade_command(package_name: str, installation_method: str) -> Tuple[List[str], bool]:
42
+ """Build the appropriate upgrade command based on the installation method.
43
+
44
+ For UV, this uses ``uv tool install --force``. For pip, this uses
45
+ ``python -m pip install --upgrade`` with the current Python executable.
46
+
31
47
  Args:
32
- package_name (str): Name of the package to upgrade
33
- installation_method (str): "uv" or "pip"
34
-
48
+ package_name: Name of the package to upgrade.
49
+ installation_method: Either ``"uv"`` or ``"pip"``.
50
+
35
51
  Returns:
36
- tuple: (command_list, shell_mode) where command_list is the command to run
37
- and shell_mode is a boolean indicating if shell=True should be used
52
+ A tuple ``(command_list, shell_mode)`` where ``command_list`` is the
53
+ command and its arguments as a list of strings, and ``shell_mode`` is a
54
+ boolean indicating whether ``subprocess.run`` should be invoked with
55
+ ``shell=True``.
38
56
  """
39
57
  if installation_method == "uv":
40
58
  # For UV commands, we need the full path if available
41
59
  uv_path = shutil.which("uv")
42
60
  if uv_path:
43
- return ([uv_path, "tool", "install", package_name, "--force"], False)
61
+ return [uv_path, "tool", "install", package_name, "--force"], False
44
62
  # If uv isn't in PATH, use shell=True
45
- return (["uv", "tool", "install", package_name, "--force"], True)
63
+ return ["uv", "tool", "install", package_name, "--force"], True
64
+
46
65
  # Default pip method
47
- return ([sys.executable, "-m", "pip", "install", "--upgrade", package_name], False)
66
+ return [sys.executable, "-m", "pip", "install", "--upgrade", package_name], False
48
67
 
49
68
 
50
69
  def _get_latest_version(package_name: str) -> Optional[str]:
51
- """Fetch the latest version of a package from PyPI."""
70
+ """Fetch the latest version of a package from PyPI.
71
+
72
+ This queries the JSON API at ``https://pypi.org/pypi/<package_name>/json``
73
+ with a timeout and extracts the ``info.version`` field.
74
+
75
+ Any exception results in a user-friendly error message and ``None`` being
76
+ returned.
77
+
78
+ Args:
79
+ package_name: The name of the package to query.
80
+
81
+ Returns:
82
+ The latest version string if it could be fetched, otherwise ``None``.
83
+ """
52
84
  # pylint: disable=broad-except
53
85
  try:
54
86
  pypi_url = f"https://pypi.org/pypi/{package_name}/json"
55
87
  response = requests.get(pypi_url, timeout=10)
56
88
  response.raise_for_status()
57
- return response.json()['info']['version']
58
- except Exception as ex:
89
+ return response.json()["info"]["version"]
90
+ except Exception as ex: # noqa: BLE001
59
91
  print(f"Failed to fetch latest version from PyPI: {str(ex)}")
60
92
  return None
61
93
 
62
94
 
63
- def _upgrade_package(package_name: str, installation_method: str):
64
- """Upgrade a package using the specified installation method."""
95
+ def _upgrade_package(package_name: str, installation_method: str) -> bool:
96
+ """Upgrade a package using the specified installation method.
97
+
98
+ This runs the command returned by :func:`get_upgrade_command`, captures
99
+ stdout/stderr, and reports success or failure.
100
+
101
+ Args:
102
+ package_name: Name of the package to upgrade.
103
+ installation_method: Either ``"uv"`` or ``"pip"``.
104
+
105
+ Returns:
106
+ ``True`` if the upgrade command succeeded (exit code 0), otherwise
107
+ ``False``.
108
+ """
65
109
  cmd, use_shell = get_upgrade_command(package_name, installation_method)
66
110
  cmd_str = " ".join(cmd)
67
111
  print(f"\nDetected installation method: {installation_method}")
@@ -69,25 +113,38 @@ def _upgrade_package(package_name: str, installation_method: str):
69
113
 
70
114
  # pylint: disable=broad-except
71
115
  try:
72
- result = subprocess.run(
116
+ result = subprocess.run( # noqa: PLW1510
73
117
  cmd,
74
118
  shell=use_shell,
75
119
  capture_output=True,
76
120
  text=True,
77
- check=False
121
+ check=False,
78
122
  )
79
123
  if result.returncode == 0:
80
124
  print(f"\nSuccessfully upgraded {package_name}")
81
125
  return True
82
126
  print(f"\nUpgrade command failed: {result.stderr}")
83
127
  return False
84
- except Exception as ex:
128
+ except Exception as ex: # noqa: BLE001
85
129
  print(f"\nError during upgrade: {str(ex)}")
86
130
  return False
87
131
 
88
132
 
89
133
  def _is_new_version_available(current_version: str, latest_version: str) -> bool:
90
- """Check if a new version is available."""
134
+ """Determine whether a newer version is available.
135
+
136
+ Semantic versioning is used when possible via :mod:`semver`. If parsing
137
+ fails for either version string, the function falls back to a simple
138
+ inequality comparison of the raw strings.
139
+
140
+ Args:
141
+ current_version: The currently installed version string.
142
+ latest_version: The latest available version string.
143
+
144
+ Returns:
145
+ ``True`` if ``latest_version`` is considered newer than
146
+ ``current_version``, otherwise ``False``.
147
+ """
91
148
  try:
92
149
  current_semver = semver.VersionInfo.parse(current_version)
93
150
  latest_semver = semver.VersionInfo.parse(latest_version)
@@ -96,15 +153,34 @@ def _is_new_version_available(current_version: str, latest_version: str) -> bool
96
153
  return latest_version != current_version
97
154
 
98
155
 
99
- def auto_update(package_name: str = "pdd-cli", latest_version: str = None) -> None:
100
- """
101
- Check if there's a new version of the package available and prompt for upgrade.
102
- Handles both UV and pip installations automatically.
103
-
156
+ def auto_update(package_name: str = "pdd-cli", latest_version: Optional[str] = None) -> None:
157
+ """Check PyPI for a newer version of the package and offer to upgrade.
158
+
159
+ This function:
160
+
161
+ * Determines the currently installed version using :mod:`importlib.metadata`.
162
+ * Optionally accepts a pre-fetched ``latest_version`` for testing; if not
163
+ supplied it will query PyPI.
164
+ * Compares versions using semantic versioning with a string comparison
165
+ fallback.
166
+ * Interactively prompts the user for confirmation before upgrading.
167
+ * Performs the upgrade using UV or pip depending on installation method,
168
+ with a pip fallback if a UV upgrade fails.
169
+
104
170
  Args:
105
- latest_version (str): Known latest version (default: None)
106
- package_name (str): Name of the package to check (default: "pdd-cli")
171
+ package_name: Name of the package to check (default: ``"pdd-cli"``).
172
+ latest_version: Optionally, a known latest version to use instead of
173
+ querying PyPI (primarily for testing).
174
+
175
+ Returns:
176
+ None. All feedback is provided via ``print`` statements.
107
177
  """
178
+ # Skip update check in CI mode, headless mode, or when stdin is not a TTY
179
+ if (os.environ.get('CI') == '1' or
180
+ os.environ.get('PDD_SKIP_UPDATE_CHECK') == '1' or
181
+ not sys.stdin.isatty()):
182
+ return
183
+
108
184
  # pylint: disable=broad-except
109
185
  try:
110
186
  current_version = importlib.metadata.version(package_name)
@@ -117,32 +193,34 @@ def auto_update(package_name: str = "pdd-cli", latest_version: str = None) -> No
117
193
  if not _is_new_version_available(current_version, latest_version):
118
194
  return
119
195
 
120
- print(f"\nNew version of {package_name} available: "
121
- f"{latest_version} (current: {current_version})")
122
-
196
+ print(
197
+ f"\nNew version of {package_name} available: "
198
+ f"{latest_version} (current: {current_version})",
199
+ )
200
+
123
201
  while True:
124
202
  response = input("Would you like to upgrade? [y/N]: ").lower().strip()
125
- if response in ['y', 'yes']:
203
+ if response in ["y", "yes"]:
126
204
  installation_method = detect_installation_method(sys.executable)
127
205
  if _upgrade_package(package_name, installation_method):
128
206
  break
129
-
207
+
130
208
  if installation_method == "uv":
131
209
  print("\nAttempting fallback to pip...")
132
210
  if _upgrade_package(package_name, "pip"):
133
211
  break
134
-
212
+
135
213
  break
136
- if response in ['n', 'no', '']:
214
+ if response in ["n", "no", ""]:
137
215
  print("\nUpgrade cancelled")
138
216
  break
139
217
  print("Please answer 'y' or 'n'")
140
218
 
141
219
  except importlib.metadata.PackageNotFoundError:
142
220
  print(f"Package {package_name} is not installed")
143
- except Exception as ex:
221
+ except Exception as ex: # noqa: BLE001
144
222
  print(f"Error checking for updates: {str(ex)}")
145
223
 
146
224
 
147
225
  if __name__ == "__main__":
148
- auto_update()
226
+ auto_update()
pdd/bug_main.py CHANGED
@@ -1,13 +1,33 @@
1
+ import json
1
2
  import os
2
3
  import sys
3
4
  from typing import Tuple, Optional
5
+
4
6
  import click
5
- from rich import print as rprint
7
+ import requests
6
8
  from pathlib import Path
9
+ from rich import print as rprint
10
+ from rich.console import Console
11
+ from rich.panel import Panel
7
12
 
8
13
  from . import DEFAULT_STRENGTH, DEFAULT_TIME
9
14
  from .construct_paths import construct_paths
10
15
  from .bug_to_unit_test import bug_to_unit_test
16
+ from .core.cloud import CloudConfig
17
+
18
+ # Cloud request timeout
19
+ CLOUD_REQUEST_TIMEOUT = 400 # seconds
20
+
21
+ console = Console()
22
+
23
+
24
+ def _env_flag_enabled(name: str) -> bool:
25
+ """Return True when an env var is set to a truthy value."""
26
+ value = os.environ.get(name)
27
+ if value is None:
28
+ return False
29
+ return str(value).strip().lower() in {"1", "true", "yes", "on"}
30
+
11
31
 
12
32
  def bug_main(
13
33
  ctx: click.Context,
@@ -32,6 +52,14 @@ def bug_main(
32
52
  :param language: Optional programming language for the unit test. Defaults to "Python".
33
53
  :return: A tuple containing the generated unit test, total cost, and model name used.
34
54
  """
55
+ # Initialize variables
56
+ unit_test = ""
57
+ total_cost = 0.0
58
+ model_name = ""
59
+
60
+ verbose = ctx.obj.get('verbose', False)
61
+ quiet = ctx.obj.get('quiet', False)
62
+
35
63
  try:
36
64
  # Construct file paths
37
65
  input_file_paths = {
@@ -48,11 +76,13 @@ def bug_main(
48
76
  resolved_config, input_strings, output_file_paths, detected_language = construct_paths(
49
77
  input_file_paths=input_file_paths,
50
78
  force=ctx.obj.get('force', False),
51
- quiet=ctx.obj.get('quiet', False),
79
+ quiet=quiet,
52
80
  command="bug",
53
- command_options=command_options
81
+ command_options=command_options,
82
+ context_override=ctx.obj.get('context'),
83
+ confirm_callback=ctx.obj.get('confirm_callback')
54
84
  )
55
-
85
+
56
86
  # Use the language detected by construct_paths if none was explicitly provided
57
87
  if language is None:
58
88
  language = detected_language
@@ -64,21 +94,158 @@ def bug_main(
64
94
  current_output_content = input_strings["current_output"]
65
95
  desired_output_content = input_strings["desired_output"]
66
96
 
67
- # Generate unit test
97
+ # Get generation parameters
68
98
  strength = ctx.obj.get('strength', DEFAULT_STRENGTH)
69
99
  temperature = ctx.obj.get('temperature', 0)
70
100
  time_budget = ctx.obj.get('time', DEFAULT_TIME)
71
- unit_test, total_cost, model_name = bug_to_unit_test(
72
- current_output_content,
73
- desired_output_content,
74
- prompt_content,
75
- code_content,
76
- program_content,
77
- strength,
78
- temperature,
79
- time_budget,
80
- language
81
- )
101
+
102
+ # Determine cloud vs local execution preference
103
+ is_local_execution_preferred = ctx.obj.get('local', False)
104
+ cloud_only = _env_flag_enabled("PDD_CLOUD_ONLY") or _env_flag_enabled("PDD_NO_LOCAL_FALLBACK")
105
+ current_execution_is_local = is_local_execution_preferred and not cloud_only
106
+
107
+ # Try cloud execution first if not preferring local
108
+ if not current_execution_is_local:
109
+ if verbose:
110
+ console.print(Panel("Attempting cloud bug test generation...", title="[blue]Mode[/blue]", expand=False))
111
+
112
+ jwt_token = CloudConfig.get_jwt_token(verbose=verbose)
113
+
114
+ if not jwt_token:
115
+ if cloud_only:
116
+ console.print("[red]Cloud authentication failed.[/red]")
117
+ raise click.UsageError("Cloud authentication failed")
118
+ console.print("[yellow]Cloud authentication failed. Falling back to local execution.[/yellow]")
119
+ current_execution_is_local = True
120
+
121
+ if jwt_token and not current_execution_is_local:
122
+ # Build cloud payload
123
+ payload = {
124
+ "promptContent": prompt_content,
125
+ "codeContent": code_content,
126
+ "programContent": program_content,
127
+ "currentOutput": current_output_content,
128
+ "desiredOutput": desired_output_content,
129
+ "language": language,
130
+ "strength": strength,
131
+ "temperature": temperature,
132
+ "time": time_budget,
133
+ "verbose": verbose,
134
+ }
135
+
136
+ headers = {
137
+ "Authorization": f"Bearer {jwt_token}",
138
+ "Content-Type": "application/json"
139
+ }
140
+ cloud_url = CloudConfig.get_endpoint_url("generateBugTest")
141
+
142
+ try:
143
+ response = requests.post(
144
+ cloud_url,
145
+ json=payload,
146
+ headers=headers,
147
+ timeout=CLOUD_REQUEST_TIMEOUT
148
+ )
149
+ response.raise_for_status()
150
+
151
+ response_data = response.json()
152
+ unit_test = response_data.get("generatedTest", "")
153
+ total_cost = float(response_data.get("totalCost", 0.0))
154
+ model_name = response_data.get("modelName", "cloud_model")
155
+
156
+ if not unit_test:
157
+ if cloud_only:
158
+ console.print("[red]Cloud execution returned no test code.[/red]")
159
+ raise click.UsageError("Cloud execution returned no test code")
160
+ console.print("[yellow]Cloud execution returned no test code. Falling back to local.[/yellow]")
161
+ current_execution_is_local = True
162
+ elif verbose:
163
+ console.print(Panel(
164
+ f"Cloud bug test generation successful. Model: {model_name}, Cost: ${total_cost:.6f}",
165
+ title="[green]Cloud Success[/green]",
166
+ expand=False
167
+ ))
168
+
169
+ except requests.exceptions.Timeout:
170
+ if cloud_only:
171
+ console.print(f"[red]Cloud execution timed out ({CLOUD_REQUEST_TIMEOUT}s).[/red]")
172
+ raise click.UsageError("Cloud execution timed out")
173
+ console.print(f"[yellow]Cloud execution timed out ({CLOUD_REQUEST_TIMEOUT}s). Falling back to local.[/yellow]")
174
+ current_execution_is_local = True
175
+
176
+ except requests.exceptions.HTTPError as e:
177
+ status_code = e.response.status_code if e.response else 0
178
+ err_content = e.response.text[:200] if e.response else "No response content"
179
+
180
+ # Non-recoverable errors: do NOT fall back to local
181
+ if status_code == 402: # Insufficient credits
182
+ try:
183
+ error_data = e.response.json()
184
+ current_balance = error_data.get("currentBalance", "unknown")
185
+ estimated_cost = error_data.get("estimatedCost", "unknown")
186
+ console.print(f"[red]Insufficient credits. Current balance: {current_balance}, estimated cost: {estimated_cost}[/red]")
187
+ except Exception:
188
+ console.print(f"[red]Insufficient credits: {err_content}[/red]")
189
+ raise click.UsageError("Insufficient credits for cloud bug test generation")
190
+ elif status_code == 401: # Authentication error
191
+ console.print(f"[red]Authentication failed: {err_content}[/red]")
192
+ raise click.UsageError("Cloud authentication failed")
193
+ elif status_code == 403: # Authorization error (not approved)
194
+ console.print(f"[red]Access denied: {err_content}[/red]")
195
+ raise click.UsageError("Access denied - user not approved")
196
+ elif status_code == 400: # Validation error
197
+ console.print(f"[red]Invalid request: {err_content}[/red]")
198
+ raise click.UsageError(f"Invalid request: {err_content}")
199
+ else:
200
+ # Recoverable errors (5xx, unexpected errors): fall back to local
201
+ if cloud_only:
202
+ console.print(f"[red]Cloud HTTP error ({status_code}): {err_content}[/red]")
203
+ raise click.UsageError(f"Cloud HTTP error ({status_code}): {err_content}")
204
+ console.print(f"[yellow]Cloud HTTP error ({status_code}): {err_content}. Falling back to local.[/yellow]")
205
+ current_execution_is_local = True
206
+
207
+ except requests.exceptions.RequestException as e:
208
+ if cloud_only:
209
+ console.print(f"[red]Cloud network error: {e}[/red]")
210
+ raise click.UsageError(f"Cloud network error: {e}")
211
+ console.print(f"[yellow]Cloud network error: {e}. Falling back to local.[/yellow]")
212
+ current_execution_is_local = True
213
+
214
+ except json.JSONDecodeError:
215
+ if cloud_only:
216
+ console.print("[red]Cloud returned invalid JSON.[/red]")
217
+ raise click.UsageError("Cloud returned invalid JSON")
218
+ console.print("[yellow]Cloud returned invalid JSON. Falling back to local.[/yellow]")
219
+ current_execution_is_local = True
220
+
221
+ # Local execution path
222
+ if current_execution_is_local:
223
+ if verbose:
224
+ console.print(Panel("Performing local bug test generation...", title="[blue]Mode[/blue]", expand=False))
225
+
226
+ unit_test, total_cost, model_name = bug_to_unit_test(
227
+ current_output_content,
228
+ desired_output_content,
229
+ prompt_content,
230
+ code_content,
231
+ program_content,
232
+ strength,
233
+ temperature,
234
+ time_budget,
235
+ language
236
+ )
237
+
238
+ if verbose:
239
+ console.print(Panel(
240
+ f"Local bug test generation successful. Model: {model_name}, Cost: ${total_cost:.6f}",
241
+ title="[green]Local Success[/green]",
242
+ expand=False
243
+ ))
244
+
245
+ # Validate generated content
246
+ if not unit_test or not unit_test.strip():
247
+ rprint("[bold red]Error: Generated unit test content is empty or whitespace-only.[/bold red]")
248
+ return "", 0.0, "Error: Generated unit test content is empty"
82
249
 
83
250
  # Save results if output path is provided
84
251
  if output_file_paths.get("output"):
@@ -87,21 +254,21 @@ def bug_main(
87
254
  if not output_path or output_path.strip() == '':
88
255
  # Use a default output path in the current directory
89
256
  output_path = f"test_{Path(code_file).stem}_bug.{language.lower()}"
90
- if not ctx.obj.get('quiet', False):
257
+ if not quiet:
91
258
  rprint(f"[yellow]Warning: Empty output path detected. Using default: {output_path}[/yellow]")
92
259
  output_file_paths["output"] = output_path
93
-
260
+
94
261
  # Create directory if it doesn't exist
95
262
  dir_path = os.path.dirname(output_path)
96
263
  if dir_path: # Only create directory if there's a directory part in the path
97
264
  os.makedirs(dir_path, exist_ok=True)
98
-
265
+
99
266
  # Write the file
100
- with open(output_path, 'w') as f:
267
+ with open(output_path, 'w', encoding='utf-8') as f:
101
268
  f.write(unit_test)
102
269
 
103
270
  # Provide user feedback
104
- if not ctx.obj.get('quiet', False):
271
+ if not quiet:
105
272
  rprint("[bold green]Unit test generated successfully.[/bold green]")
106
273
  rprint(f"[bold]Model used:[/bold] {model_name}")
107
274
  rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
@@ -114,7 +281,13 @@ def bug_main(
114
281
 
115
282
  return unit_test, total_cost, model_name
116
283
 
284
+ except click.Abort:
285
+ # User cancelled - re-raise to stop the sync loop
286
+ raise
287
+ except click.UsageError:
288
+ # Re-raise usage errors to be handled by CLI
289
+ raise
117
290
  except Exception as e:
118
- if not ctx.obj.get('quiet', False):
291
+ if not quiet:
119
292
  rprint(f"[bold red]Error:[/bold red] {str(e)}")
120
- sys.exit(1)
293
+ return "", 0.0, f"Error: {e}"
pdd/bug_to_unit_test.py CHANGED
@@ -108,6 +108,7 @@ def bug_to_unit_test( # pylint: disable=too-many-arguments, too-many-locals
108
108
  strength=0.89,
109
109
  temperature=temperature,
110
110
  time=time,
111
+ language=language,
111
112
  verbose=False,
112
113
  )
113
114
 
@@ -121,6 +122,7 @@ def bug_to_unit_test( # pylint: disable=too-many-arguments, too-many-locals
121
122
  strength=strength,
122
123
  temperature=temperature,
123
124
  time=time,
125
+ language=language,
124
126
  verbose=True,
125
127
  )
126
128
  total_cost += continued_cost
pdd/change_main.py CHANGED
@@ -17,11 +17,11 @@ from rich import print as rprint
17
17
  from rich.panel import Panel
18
18
 
19
19
  # Use relative imports for internal modules
20
+ from .config_resolution import resolve_effective_config
20
21
  from .construct_paths import construct_paths
21
22
  from .change import change as change_func
22
23
  from .process_csv_change import process_csv_change
23
24
  from .get_extension import get_extension
24
- from . import DEFAULT_STRENGTH, DEFAULT_TIME
25
25
 
26
26
  # Set up logging
27
27
  logger = logging.getLogger(__name__)
@@ -72,9 +72,8 @@ def change_main(
72
72
  # Retrieve global options from context
73
73
  force: bool = ctx.obj.get("force", False)
74
74
  quiet: bool = ctx.obj.get("quiet", False)
75
- strength: float = ctx.obj.get("strength", DEFAULT_STRENGTH)
76
- temperature: float = ctx.obj.get("temperature", 0.0)
77
- time_budget: float = ctx.obj.get("time", DEFAULT_TIME)
75
+ # Note: strength/temperature/time will be resolved after construct_paths
76
+ # using resolve_effective_config for proper priority handling
78
77
  # --- Get language and extension from context ---
79
78
  # These are crucial for knowing the target code file types, especially in CSV mode
80
79
  target_language: str = ctx.obj.get("language", "")
@@ -203,6 +202,7 @@ def change_main(
203
202
  quiet=quiet,
204
203
  command="change",
205
204
  command_options=command_options,
205
+ context_override=ctx.obj.get('context')
206
206
  )
207
207
  logger.debug("construct_paths returned:")
208
208
  logger.debug(" input_strings keys: %s", list(input_strings.keys()))
@@ -215,6 +215,13 @@ def change_main(
215
215
  logger.error(msg, exc_info=True)
216
216
  return msg, 0.0, ""
217
217
 
218
+ # Use centralized config resolution with proper priority:
219
+ # CLI > pddrc > defaults
220
+ effective_config = resolve_effective_config(ctx, resolved_config)
221
+ strength = effective_config["strength"]
222
+ temperature = effective_config["temperature"]
223
+ time_budget = effective_config["time"]
224
+
218
225
  # --- 3. Perform Prompt Modification ---
219
226
  if use_csv:
220
227
  logger.info("Running in CSV mode.")