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/code_generator.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from typing import Tuple
2
2
  from rich.console import Console
3
+ from . import EXTRACTION_STRENGTH
3
4
  from .preprocess import preprocess
4
5
  from .llm_invoke import llm_invoke
5
6
  from .unfinished_prompt import unfinished_prompt
@@ -104,7 +105,7 @@ def code_generator(
104
105
  runnable_code, postprocess_cost, model_name_post = postprocess(
105
106
  llm_output=final_output,
106
107
  language=language,
107
- strength=0.97,
108
+ strength=EXTRACTION_STRENGTH,
108
109
  temperature=0.0,
109
110
  verbose=verbose
110
111
  )
@@ -4,6 +4,7 @@ from rich import print as rprint
4
4
  from rich.markdown import Markdown
5
5
  from .load_prompt_template import load_prompt_template
6
6
  from .llm_invoke import llm_invoke
7
+ from . import EXTRACTION_STRENGTH
7
8
 
8
9
  class ConflictChange(BaseModel):
9
10
  prompt_name: str = Field(description="Name of the prompt that needs to be changed")
@@ -85,7 +86,7 @@ def conflicts_in_prompts(
85
86
  extract_response = llm_invoke(
86
87
  prompt=extract_prompt,
87
88
  input_json=extract_input,
88
- strength=0.97, # As specified
89
+ strength=EXTRACTION_STRENGTH,
89
90
  temperature=temperature,
90
91
  output_pydantic=ConflictResponse,
91
92
  verbose=verbose
pdd/construct_paths.py CHANGED
@@ -1,251 +1,406 @@
1
+ # pdd/construct_paths.py
2
+ from __future__ import annotations
3
+
4
+ import sys
1
5
  import os
2
- import csv
3
6
  from pathlib import Path
4
- from typing import Dict, Tuple, Optional
7
+ from typing import Dict, Tuple, Any, Optional # Added Optional
5
8
 
6
- from rich import print as rich_print
7
- from rich.prompt import Confirm
9
+ import click
10
+ from rich.console import Console
11
+ from rich.theme import Theme
8
12
 
9
- from .generate_output_paths import generate_output_paths
10
13
  from .get_extension import get_extension
11
14
  from .get_language import get_language
15
+ from .generate_output_paths import generate_output_paths
16
+
17
+ # Assume generate_output_paths raises ValueError on unknown command
18
+
19
+ console = Console(theme=Theme({"info": "cyan", "warning": "yellow", "error": "bold red"}))
20
+
21
+
22
+ def _read_file(path: Path) -> str:
23
+ """Read a text file safely and return its contents."""
24
+ try:
25
+ return path.read_text(encoding="utf-8")
26
+ except Exception as exc: # pragma: no cover
27
+ # Error is raised in the main function after this fails
28
+ console.print(f"[error]Could not read {path}: {exc}", style="error")
29
+ raise
30
+
31
+
32
+ def _ensure_error_file(path: Path, quiet: bool) -> None:
33
+ """Create an empty error log file if it doesn't exist."""
34
+ if not path.exists():
35
+ if not quiet:
36
+ # Use console.print from the main module scope
37
+ # Print without Rich tags for easier testing
38
+ console.print(f"Warning: Error file '{path.resolve()}' does not exist. Creating an empty file.", style="warning")
39
+ try:
40
+ path.parent.mkdir(parents=True, exist_ok=True)
41
+ path.touch()
42
+ except Exception as exc: # pragma: no cover
43
+ console.print(f"[error]Could not create error file {path}: {exc}", style="error")
44
+ raise
45
+
46
+
47
+ def _candidate_prompt_path(input_files: Dict[str, Path]) -> Path | None:
48
+ """Return the path most likely to be the prompt file, if any."""
49
+ # Prioritize specific keys known to hold the primary prompt
50
+ for key in (
51
+ "prompt_file", # generate, test, fix, crash, trace, verify, auto-deps
52
+ "input_prompt", # split
53
+ "input_prompt_file", # update, change (non-csv), bug
54
+ "prompt1", # conflicts
55
+ # Less common / potentially ambiguous keys last
56
+ "change_prompt_file", # change (specific case handled in _extract_basename)
57
+ ):
58
+ if key in input_files:
59
+ return input_files[key]
60
+
61
+ # Fallback: first file with a .prompt extension if no specific key matches
62
+ for p in input_files.values():
63
+ if p.suffix == ".prompt":
64
+ return p
65
+ return None
66
+
67
+
68
+ def _strip_language_suffix(path_like: os.PathLike[str]) -> str:
69
+ """
70
+ Remove trailing '_<language>.prompt' or '_<language>' from a filename stem
71
+ if it matches a known language.
72
+ """
73
+ p = Path(path_like)
74
+ stem = p.stem # removes last extension (e.g. '.prompt', '.py')
12
75
 
13
- pdd_path = os.getenv('PDD_PATH')
14
- if pdd_path is None:
15
- raise ValueError("Environment variable 'PDD_PATH' is not set")
16
- csv_file_path = os.path.join(pdd_path, 'data', 'language_format.csv')
76
+ if "_" not in stem: # No underscore, nothing to strip
77
+ return stem
17
78
 
18
- # Initialize the set to store known languages
19
- KNOWN_LANGUAGES = set()
79
+ parts = stem.split("_")
80
+ # Avoid splitting single-word stems like "Makefile_" if that's possible
81
+ if len(parts) < 2:
82
+ return stem
20
83
 
21
- # Read the CSV file and populate KNOWN_LANGUAGES
22
- with open(csv_file_path, mode='r', newline='') as csvfile:
23
- csvreader = csv.DictReader(csvfile)
24
- for row in csvreader:
25
- KNOWN_LANGUAGES.add(row['language'].lower())
84
+ candidate_lang = parts[-1]
85
+
86
+ # Check if the last part is a known language
87
+ if get_extension(candidate_lang) != "": # recognised language
88
+ # If the last part is a language, strip it
89
+ return "_".join(parts[:-1])
90
+ else:
91
+ # Last part is not a language, return original stem
92
+ return stem
93
+
94
+
95
+ def _extract_basename(
96
+ command: str,
97
+ input_file_paths: Dict[str, Path],
98
+ ) -> str:
99
+ """
100
+ Deduce the project basename according to the rules explained in *Step A*.
101
+ """
102
+ # Handle conflicts first due to its unique structure
103
+ if command == "conflicts":
104
+ key1 = "prompt1"
105
+ key2 = "prompt2"
106
+ # Ensure keys exist before proceeding
107
+ if key1 in input_file_paths and key2 in input_file_paths:
108
+ p1 = Path(input_file_paths[key1])
109
+ p2 = Path(input_file_paths[key2])
110
+ base1 = _strip_language_suffix(p1)
111
+ base2 = _strip_language_suffix(p2)
112
+ # Combine basenames, ensure order for consistency (sorted)
113
+ return "_".join(sorted([base1, base2]))
114
+ # else: Fall through might occur if keys missing, handled by general logic/fallback
115
+
116
+ # Special‑case commands that choose a non‑prompt file for the basename
117
+ elif command == "detect":
118
+ key = "change_file"
119
+ if key in input_file_paths:
120
+ # Basename is from change_file, no language suffix stripping needed usually
121
+ return Path(input_file_paths[key]).stem
122
+ elif command == "change":
123
+ # If change_prompt_file is given, use its stem (no language strip needed per convention)
124
+ if "change_prompt_file" in input_file_paths:
125
+ return Path(input_file_paths["change_prompt_file"]).stem
126
+ # If --csv is used or change_prompt_file is absent, fall through to general logic
127
+ pass
128
+
129
+ # General case: Use the primary prompt file
130
+ prompt_path = _candidate_prompt_path(input_file_paths)
131
+ if prompt_path:
132
+ return _strip_language_suffix(prompt_path)
133
+
134
+ # Fallback: If no prompt found (e.g., command only takes code files?),
135
+ # use the first input file's stem. This requires input_file_paths not to be empty.
136
+ # This fallback is reached only if input_file_paths is not empty (checked earlier)
137
+ first_path = next(iter(input_file_paths.values()))
138
+ # Should we strip language here too? Let's be consistent.
139
+ return _strip_language_suffix(first_path)
140
+
141
+
142
+ def _determine_language(
143
+ command_options: Dict[str, Any], # Keep original type hint
144
+ input_file_paths: Dict[str, Path],
145
+ command: str = "", # New parameter for the command name
146
+ ) -> str:
147
+ """
148
+ Apply the language discovery strategy.
149
+ Priority: Explicit option > Code/Test file extension > Prompt filename suffix.
150
+ For 'detect' command, default to 'prompt' as it typically doesn't need a language.
151
+ """
152
+ # Diagnostic check for None (should be handled by caller, but for safety)
153
+ command_options = command_options or {}
154
+ # 1 – explicit option
155
+ explicit_lang = command_options.get("language")
156
+ if explicit_lang:
157
+ lang_lower = explicit_lang.lower()
158
+ # Optional: Validate known language? Let's assume valid for now.
159
+ return lang_lower
160
+
161
+ # 2 – infer from extension of any code/test file (excluding .prompt)
162
+ # Iterate through values, ensuring consistent order if needed (e.g., sort keys)
163
+ # For now, rely on dict order (Python 3.7+)
164
+ for key, p in input_file_paths.items():
165
+ path_obj = Path(p)
166
+ ext = path_obj.suffix
167
+ # Prioritize non-prompt code files
168
+ if ext and ext != ".prompt":
169
+ language = get_language(ext)
170
+ if language:
171
+ return language.lower()
172
+ # Handle files without extension like Makefile
173
+ elif not ext and path_obj.is_file(): # Check it's actually a file
174
+ language = get_language(path_obj.name) # Check name (e.g., 'Makefile')
175
+ if language:
176
+ return language.lower()
177
+
178
+ # 3 – parse from prompt filename suffix
179
+ prompt_path = _candidate_prompt_path(input_file_paths)
180
+ if prompt_path and prompt_path.suffix == ".prompt":
181
+ stem = prompt_path.stem
182
+ if "_" in stem:
183
+ parts = stem.split("_")
184
+ if len(parts) >= 2:
185
+ token = parts[-1]
186
+ # Check if the token is a known language
187
+ if get_extension(token) != "":
188
+ return token.lower()
189
+
190
+ # 4 - Special handling for detect command - default to prompt for LLM prompts
191
+ if command == "detect" and "change_file" in input_file_paths:
192
+ return "prompt" # Default to prompt for detect command
193
+
194
+ # 5 - If no language determined, raise error
195
+ raise ValueError("Could not determine language from input files or options.")
196
+
197
+
198
+ def _paths_exist(paths: Dict[str, Path]) -> bool: # Value type is Path
199
+ """Return True if any of the given paths is an existing file."""
200
+ # Check specifically for files, not directories
201
+ return any(p.is_file() for p in paths.values())
26
202
 
27
- # We also treat "prompt" as a recognized suffix
28
- EXTENDED_LANGUAGES = KNOWN_LANGUAGES.union({"prompt"})
29
203
 
30
204
  def construct_paths(
31
205
  input_file_paths: Dict[str, str],
32
206
  force: bool,
33
207
  quiet: bool,
34
208
  command: str,
35
- command_options: Dict[str, str] = None,
209
+ command_options: Optional[Dict[str, Any]], # Allow None
210
+ create_error_file: bool = True, # Added parameter to control error file creation
36
211
  ) -> Tuple[Dict[str, str], Dict[str, str], str]:
37
212
  """
38
- Generates and checks input/output file paths, handles file requirements, and loads input files.
39
- Returns (input_strings, output_file_paths, language).
213
+ High‑level orchestrator that loads inputs, determines basename/language,
214
+ computes output locations, and verifies overwrite rules.
215
+
216
+ Returns
217
+ -------
218
+ (input_strings, output_file_paths, language)
40
219
  """
220
+ command_options = command_options or {} # Ensure command_options is a dict
41
221
 
42
222
  if not input_file_paths:
43
223
  raise ValueError("No input files provided")
44
224
 
45
- command_options = command_options or {}
46
- input_strings: Dict[str, str] = {}
47
- output_file_paths: Dict[str, str] = {}
48
-
49
- def extract_basename(filename: str) -> str:
50
- """
51
- Extract the 'basename' from the filename, removing any recognized language
52
- suffix (e.g., "_python") or a "_prompt" suffix if present.
53
- """
54
- name = Path(filename).stem # e.g. "regression_bash" if "regression_bash.prompt"
55
- parts = name.split('_')
56
- last_token = parts[-1].lower()
57
- if last_token in EXTENDED_LANGUAGES:
58
- name = '_'.join(parts[:-1])
59
- return name
60
-
61
- def determine_language(filename: str,
62
- cmd_options: Dict[str, str],
63
- code_file: Optional[str] = None) -> str:
64
- """
65
- Figure out the language:
66
- 1) If command_options['language'] is given, return it.
67
- 2) Check if the file's stem ends with a known language suffix (e.g. "_python").
68
- 3) Otherwise, check the file extension or code_file extension.
69
- 4) If none recognized, raise an error.
70
- """
71
- # 1) If user explicitly gave a language in command_options
72
- if cmd_options.get('language'):
73
- return cmd_options['language']
74
-
75
- # 2) Extract last token from the stem
76
- name = Path(filename).stem
77
- parts = name.split('_')
78
- last_token = parts[-1].lower()
79
-
80
- # If the last token is a known language (e.g. "python", "java") or "prompt",
81
- # that is the language. E.g. "my_project_python.prompt" => python
82
- # "main_gen_prompt.prompt" => prompt
83
- if last_token in KNOWN_LANGUAGES:
84
- return last_token
85
- elif last_token == "prompt":
86
- return "prompt"
87
-
88
- # 3) If extension is .prompt, see if code_file helps or if get_language(".prompt") is mocked
89
- ext = Path(filename).suffix.lower()
90
-
91
- # If it’s explicitly ".prompt" but there's no recognized suffix,
92
- # many tests rely on us calling get_language(".prompt") or checking code_file
93
- if ext == ".prompt":
94
- # Maybe the test mocks this to return "python", or we can check code_file:
95
- if code_file:
96
- code_ext = Path(code_file).suffix.lower()
97
- code_lang = get_language(code_ext)
98
- if code_lang:
99
- return code_lang
100
-
101
- # Attempt to see if the test or environment forcibly sets a language for ".prompt"
102
- possibly_mocked = get_language(".prompt")
103
- if possibly_mocked:
104
- return possibly_mocked
105
-
106
- # If not recognized, treat it as an ambiguous prompt
107
- # The older tests typically don't raise an error here; they rely on mocking
108
- # or a code_file. However, if there's absolutely no mock or code file, it is
109
- # "Could not determine...". That's exactly what some tests check for.
110
- raise ValueError("Could not determine language from command options, filename, or code file extension")
111
-
112
- # If extension is .unsupported, raise an error
113
- if ext == ".unsupported":
114
- raise ValueError("Unsupported file extension for language: .unsupported")
115
-
116
- # Otherwise, see if extension is recognized
117
- lang = get_language(ext)
118
- if lang:
119
- return lang
120
-
121
- # If we still cannot figure out the language, try code_file
122
- if code_file:
123
- code_ext = Path(code_file).suffix.lower()
124
- code_lang = get_language(code_ext)
125
- if code_lang:
126
- return code_lang
127
-
128
- # Otherwise, unknown language
129
- raise ValueError("Could not determine language from command options, filename, or code file extension")
130
-
131
- # -----------------
132
- # Step 1: Load input files
133
- # -----------------
225
+ # ------------- normalise & resolve Paths -----------------
226
+ input_paths: Dict[str, Path] = {}
134
227
  for key, path_str in input_file_paths.items():
135
- path = Path(path_str).resolve()
228
+ try:
229
+ path = Path(path_str).expanduser()
230
+ # Resolve non-error files strictly first
231
+ if key != "error_file":
232
+ # Let FileNotFoundError propagate naturally if path doesn't exist
233
+ resolved_path = path.resolve(strict=True)
234
+ input_paths[key] = resolved_path
235
+ else:
236
+ # Resolve error file non-strictly, existence checked later
237
+ input_paths[key] = path.resolve()
238
+ except FileNotFoundError as e:
239
+ # Re-raise standard FileNotFoundError, tests will check path within it
240
+ raise e
241
+ except Exception as exc: # Catch other potential path errors like permission issues
242
+ console.print(f"[error]Invalid path provided for {key}: '{path_str}' - {exc}", style="error")
243
+ raise # Re-raise other exceptions
244
+
245
+
246
+ # ------------- Step 1: load input files ------------------
247
+ input_strings: Dict[str, str] = {}
248
+ for key, path in input_paths.items():
249
+ if key == "error_file":
250
+ if create_error_file:
251
+ _ensure_error_file(path, quiet) # Pass quiet flag
252
+ # Ensure path exists before trying to read
253
+ if not path.exists():
254
+ # _ensure_error_file should have created it, but check again
255
+ # If it still doesn't exist, something went wrong
256
+ raise FileNotFoundError(f"Error file '{path}' could not be created or found.")
257
+ else:
258
+ # When create_error_file is False, error out if the file doesn't exist
259
+ if not path.exists():
260
+ raise FileNotFoundError(f"Error file '{path}' does not exist.")
261
+
262
+ # Check existence again, especially for error_file which might have been created
136
263
  if not path.exists():
137
- if key == "error_file":
138
- # Create if missing
139
- if not quiet:
140
- rich_print(f"[yellow]Warning: Error file '{path}' does not exist. Creating an empty file.[/yellow]")
141
- path.touch()
264
+ # This case should ideally be caught by resolve(strict=True) earlier for non-error files
265
+ # Raise standard FileNotFoundError
266
+ raise FileNotFoundError(f"{path}")
267
+
268
+ if path.is_file(): # Read only if it's a file
269
+ try:
270
+ input_strings[key] = _read_file(path)
271
+ except Exception as exc:
272
+ # Re-raise exceptions during reading
273
+ raise IOError(f"Failed to read input file '{path}' (key='{key}'): {exc}") from exc
274
+ elif path.is_dir():
275
+ # Decide how to handle directories if they are passed unexpectedly
276
+ if not quiet:
277
+ console.print(f"[warning]Warning: Input path '{path}' for key '{key}' is a directory, not reading content.", style="warning")
278
+ # Store the path string or skip? Let's skip for input_strings.
279
+ # input_strings[key] = "" # Or None? Or skip? Skipping seems best.
280
+ # Handle other path types? (symlinks are resolved by resolve())
281
+
282
+
283
+ # ------------- Step 2: basename --------------------------
284
+ try:
285
+ basename = _extract_basename(command, input_paths)
286
+ except ValueError as exc:
287
+ # Check if it's the specific error from the initial check (now done at start)
288
+ # This try/except might not be needed if initial check is robust
289
+ # Let's keep it simple for now and let initial check handle empty inputs
290
+ console.print(f"[error]Unable to extract basename: {exc}", style="error")
291
+ raise ValueError(f"Failed to determine basename: {exc}") from exc
292
+ except Exception as exc: # Catch other exceptions like potential StopIteration
293
+ console.print(f"[error]Unexpected error during basename extraction: {exc}", style="error")
294
+ raise ValueError(f"Failed to determine basename: {exc}") from exc
295
+
296
+
297
+ # ------------- Step 3: language & extension --------------
298
+ try:
299
+ # Pass the potentially updated command_options
300
+ language = _determine_language(command_options, input_paths, command)
301
+
302
+ # Add validation to ensure language is never None
303
+ if language is None:
304
+ # Set a default language based on command, defaulting to 'python' for most commands
305
+ if command == 'bug':
306
+ # The bug command typically defaults to python in bug_main.py
307
+ language = 'python'
142
308
  else:
143
- # Directory might not exist, or file might be missing
144
- if not path.parent.exists():
145
- rich_print(f"[bold red]Error: Directory '{path.parent}' does not exist.[/bold red]")
146
- raise FileNotFoundError(f"Directory '{path.parent}' does not exist.")
147
- rich_print(f"[bold red]Error: Input file '{path}' not found.[/bold red]")
148
- raise FileNotFoundError(f"Input file '{path}' not found.")
149
- else:
150
- # Load its content
151
- try:
152
- with open(path, "r") as f:
153
- input_strings[key] = f.read()
154
- except Exception as exc:
155
- rich_print(f"[bold red]Error: Failed to read input file '{path}': {exc}[/bold red]")
156
- raise
157
-
158
- # -----------------
159
- # Step 2: Determine the correct "basename" for each command
160
- # -----------------
161
- basename_files = {
162
- "generate": "prompt_file",
163
- "example": "prompt_file",
164
- "test": "prompt_file",
165
- "preprocess": "prompt_file",
166
- "fix": "prompt_file",
167
- "update": "input_prompt_file" if "input_prompt_file" in input_file_paths else "prompt_file",
168
- "bug": "prompt_file",
169
- "auto-deps": "prompt_file",
170
- "crash": "prompt_file",
171
- "trace": "prompt_file",
172
- "split": "input_prompt",
173
- "change": "input_prompt_file" if "input_prompt_file" in input_file_paths else "change_prompt_file",
174
- "detect": "change_file",
175
- "conflicts": "prompt1",
309
+ # General fallback for other commands
310
+ language = 'python'
311
+
312
+ # Log the issue for debugging
313
+ if not quiet:
314
+ console.print(
315
+ f"[warning]Warning: Could not determine language for '{command}' command. Using default: {language}[/warning]",
316
+ style="warning"
317
+ )
318
+ except ValueError as e:
319
+ console.print(f"[error]{e}", style="error")
320
+ raise # Re-raise the ValueError from _determine_language
321
+
322
+ # Final safety check before calling get_extension
323
+ if not language or not isinstance(language, str):
324
+ language = 'python' # Absolute fallback
325
+ if not quiet:
326
+ console.print(
327
+ f"[warning]Warning: Invalid language value. Using default: {language}[/warning]",
328
+ style="warning"
329
+ )
330
+
331
+ file_extension = get_extension(language) # Pass determined language
332
+
333
+
334
+ # ------------- Step 3b: build output paths ---------------
335
+ # Filter user‑provided output_* locations from CLI options
336
+ output_location_opts = {
337
+ k: v for k, v in command_options.items()
338
+ if k.startswith("output") and v is not None # Ensure value is not None
176
339
  }
177
340
 
178
- if command not in basename_files:
179
- raise ValueError(f"Invalid command: {command}")
180
-
181
- if command == "conflicts":
182
- # combine two basenames
183
- basename1 = extract_basename(Path(input_file_paths['prompt1']).name)
184
- basename2 = extract_basename(Path(input_file_paths['prompt2']).name)
185
- basename = f"{basename1}_{basename2}"
186
- else:
187
- basename_file_key = basename_files[command]
188
- basename = extract_basename(Path(input_file_paths[basename_file_key]).name)
189
-
190
- # -----------------
191
- # Step 3: Determine language
192
- # -----------------
193
- # We pick whichever file is mapped for the command. (Often 'prompt_file', but not always.)
194
- language = determine_language(
195
- Path(input_file_paths.get(basename_files[command], "")).name,
196
- command_options,
197
- input_file_paths.get("code_file")
198
- )
199
-
200
- # -----------------
201
- # Step 4: Find the correct file extension
202
- # -----------------
203
- if language.lower() == "prompt":
204
- file_extension = ".prompt"
205
- else:
206
- file_extension = get_extension(language)
207
- if not file_extension or file_extension == ".unsupported":
208
- raise ValueError(f"Unsupported file extension for language: {language}")
209
-
210
- # Prepare only output-related keys
211
- output_keys = [
212
- "output", "output_sub", "output_modified", "output_test",
213
- "output_code", "output_results", "output_program",
214
- ]
215
- output_locations = {k: v for k, v in command_options.items() if k in output_keys}
216
-
217
- # -----------------
218
- # Step 5: Construct output file paths (ensuring we do not revert to the old file name)
219
- # -----------------
220
- output_file_paths = generate_output_paths(
221
- command,
222
- output_locations,
223
- basename, # e.g. "regression" (not "regression_bash")
224
- language, # e.g. "bash"
225
- file_extension # e.g. ".sh"
226
- )
227
-
228
- # If not force, confirm overwriting
229
- if not force:
230
- for _, out_path_str in output_file_paths.items():
231
- out_path = Path(out_path_str)
232
- if out_path.exists():
233
- if not Confirm.ask(
234
- f"Output file [bold blue]{out_path}[/bold blue] already exists. Overwrite?",
235
- default=True
236
- ):
237
- rich_print("[bold red]Cancelled by user. Exiting.[/bold red]")
238
- raise SystemExit(1)
239
-
240
- # -----------------
241
- # Step 6: Print details if not quiet
242
- # -----------------
341
+ try:
342
+ # generate_output_paths might return Dict[str, str] or Dict[str, Path]
343
+ # Let's assume it returns Dict[str, str] based on verification error,
344
+ # and convert them to Path objects here.
345
+ output_paths_str: Dict[str, str] = generate_output_paths(
346
+ command=command,
347
+ output_locations=output_location_opts,
348
+ basename=basename,
349
+ language=language,
350
+ file_extension=file_extension,
351
+ )
352
+ # Convert to Path objects for internal use
353
+ output_paths_resolved: Dict[str, Path] = {k: Path(v) for k, v in output_paths_str.items()}
354
+
355
+ except ValueError as e: # Catch ValueError if generate_output_paths raises it
356
+ console.print(f"[error]Error generating output paths: {e}", style="error")
357
+ raise # Re-raise the ValueError
358
+
359
+ # ------------- Step 4: overwrite confirmation ------------
360
+ # Check if any output *file* exists (operate on Path objects)
361
+ existing_files: Dict[str, Path] = {}
362
+ for k, p_obj in output_paths_resolved.items():
363
+ # p_obj = Path(p_val) # Conversion now happens earlier
364
+ if p_obj.is_file():
365
+ existing_files[k] = p_obj # Store the Path object
366
+
367
+ if existing_files and not force:
368
+ if not quiet:
369
+ # Use the Path objects stored in existing_files for resolve()
370
+ # Print without Rich tags for easier testing
371
+ paths_list = "\n".join(f" {p.resolve()}" for p in existing_files.values())
372
+ console.print(
373
+ f"Warning: The following output files already exist and may be overwritten:\n{paths_list}",
374
+ style="warning"
375
+ )
376
+ # Use click.confirm for user interaction
377
+ try:
378
+ if not click.confirm(
379
+ click.style("Overwrite existing files?", fg="yellow"), default=True, show_default=True
380
+ ):
381
+ click.secho("Operation cancelled.", fg="red", err=True)
382
+ sys.exit(1) # Exit if user chooses not to overwrite
383
+ except Exception as e: # Catch potential errors during confirm (like EOFError in non-interactive)
384
+ click.secho(f"Confirmation failed: {e}. Aborting.", fg="red", err=True)
385
+ sys.exit(1)
386
+
387
+
388
+ # ------------- Final reporting ---------------------------
243
389
  if not quiet:
244
- rich_print("[bold blue]Input file paths:[/bold blue]")
245
- for k, v in input_file_paths.items():
246
- rich_print(f" {k}: {v}")
247
- rich_print("\n[bold blue]Output file paths:[/bold blue]")
248
- for k, v in output_file_paths.items():
249
- rich_print(f" {k}: {v}")
250
-
251
- return input_strings, output_file_paths, language
390
+ console.print("[info]Input files:[/info]")
391
+ # Print resolved input paths
392
+ for k, p in input_paths.items():
393
+ console.print(f" [info]{k:<15}[/info] {p.resolve()}") # Use resolve() for consistent absolute paths
394
+ console.print("[info]Output files:[/info]")
395
+ # Print resolved output paths (using the Path objects)
396
+ for k, p in output_paths_resolved.items():
397
+ console.print(f" [info]{k:<15}[/info] {p.resolve()}") # Use resolve()
398
+ console.print(f"[info]Detected language:[/info] {language}")
399
+ console.print(f"[info]Basename:[/info] {basename}")
400
+
401
+ # Return output paths as strings, using the original dict from generate_output_paths
402
+ # if it returned strings, or convert the Path dict back.
403
+ # Since we converted to Path, convert back now.
404
+ output_file_paths_str_return = {k: str(v) for k, v in output_paths_resolved.items()}
405
+
406
+ return input_strings, output_file_paths_str_return, language