pdd-cli 0.0.42__py3-none-any.whl → 0.0.90__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.
- pdd/__init__.py +4 -4
- pdd/agentic_common.py +863 -0
- pdd/agentic_crash.py +534 -0
- pdd/agentic_fix.py +1179 -0
- pdd/agentic_langtest.py +162 -0
- pdd/agentic_update.py +370 -0
- pdd/agentic_verify.py +183 -0
- pdd/auto_deps_main.py +15 -5
- pdd/auto_include.py +63 -5
- pdd/bug_main.py +3 -2
- pdd/bug_to_unit_test.py +2 -0
- pdd/change_main.py +11 -4
- pdd/cli.py +22 -1181
- pdd/cmd_test_main.py +80 -19
- pdd/code_generator.py +58 -18
- pdd/code_generator_main.py +672 -25
- pdd/commands/__init__.py +42 -0
- pdd/commands/analysis.py +248 -0
- pdd/commands/fix.py +140 -0
- pdd/commands/generate.py +257 -0
- pdd/commands/maintenance.py +174 -0
- pdd/commands/misc.py +79 -0
- pdd/commands/modify.py +230 -0
- pdd/commands/report.py +144 -0
- pdd/commands/templates.py +215 -0
- pdd/commands/utility.py +110 -0
- pdd/config_resolution.py +58 -0
- pdd/conflicts_main.py +8 -3
- pdd/construct_paths.py +281 -81
- pdd/context_generator.py +10 -2
- pdd/context_generator_main.py +113 -11
- pdd/continue_generation.py +47 -7
- pdd/core/__init__.py +0 -0
- pdd/core/cli.py +503 -0
- pdd/core/dump.py +554 -0
- pdd/core/errors.py +63 -0
- pdd/core/utils.py +90 -0
- pdd/crash_main.py +44 -11
- pdd/data/language_format.csv +71 -62
- pdd/data/llm_model.csv +20 -18
- pdd/detect_change_main.py +5 -4
- pdd/fix_code_loop.py +331 -77
- pdd/fix_error_loop.py +209 -60
- pdd/fix_errors_from_unit_tests.py +4 -3
- pdd/fix_main.py +75 -18
- pdd/fix_verification_errors.py +12 -100
- pdd/fix_verification_errors_loop.py +319 -272
- pdd/fix_verification_main.py +57 -17
- pdd/generate_output_paths.py +93 -10
- pdd/generate_test.py +16 -5
- pdd/get_jwt_token.py +48 -9
- pdd/get_run_command.py +73 -0
- pdd/get_test_command.py +68 -0
- pdd/git_update.py +70 -19
- pdd/increase_tests.py +7 -0
- pdd/incremental_code_generator.py +2 -2
- pdd/insert_includes.py +11 -3
- pdd/llm_invoke.py +1278 -110
- pdd/load_prompt_template.py +36 -10
- pdd/pdd_completion.fish +25 -2
- pdd/pdd_completion.sh +30 -4
- pdd/pdd_completion.zsh +79 -4
- pdd/postprocess.py +10 -3
- pdd/preprocess.py +228 -15
- pdd/preprocess_main.py +8 -5
- pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
- pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
- pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
- pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
- pdd/prompts/agentic_update_LLM.prompt +1071 -0
- pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
- pdd/prompts/auto_include_LLM.prompt +98 -101
- pdd/prompts/change_LLM.prompt +1 -3
- pdd/prompts/detect_change_LLM.prompt +562 -3
- pdd/prompts/example_generator_LLM.prompt +22 -1
- pdd/prompts/extract_code_LLM.prompt +5 -1
- pdd/prompts/extract_program_code_fix_LLM.prompt +14 -2
- pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
- pdd/prompts/extract_promptline_LLM.prompt +17 -11
- pdd/prompts/find_verification_errors_LLM.prompt +6 -0
- pdd/prompts/fix_code_module_errors_LLM.prompt +16 -4
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +6 -41
- pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
- pdd/prompts/generate_test_LLM.prompt +21 -6
- pdd/prompts/increase_tests_LLM.prompt +1 -2
- pdd/prompts/insert_includes_LLM.prompt +1181 -6
- pdd/prompts/split_LLM.prompt +1 -62
- pdd/prompts/trace_LLM.prompt +25 -22
- pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
- pdd/prompts/update_prompt_LLM.prompt +22 -1
- pdd/prompts/xml_convertor_LLM.prompt +3246 -7
- pdd/pytest_output.py +188 -21
- pdd/python_env_detector.py +151 -0
- pdd/render_mermaid.py +236 -0
- pdd/setup_tool.py +648 -0
- pdd/simple_math.py +2 -0
- pdd/split_main.py +3 -2
- pdd/summarize_directory.py +56 -7
- pdd/sync_determine_operation.py +918 -186
- pdd/sync_main.py +82 -32
- pdd/sync_orchestration.py +1456 -453
- pdd/sync_tui.py +848 -0
- pdd/template_registry.py +264 -0
- pdd/templates/architecture/architecture_json.prompt +242 -0
- pdd/templates/generic/generate_prompt.prompt +174 -0
- pdd/trace.py +168 -12
- pdd/trace_main.py +4 -3
- pdd/track_cost.py +151 -61
- pdd/unfinished_prompt.py +49 -3
- pdd/update_main.py +549 -67
- pdd/update_model_costs.py +2 -2
- pdd/update_prompt.py +19 -4
- {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +20 -7
- pdd_cli-0.0.90.dist-info/RECORD +153 -0
- {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
- pdd_cli-0.0.42.dist-info/RECORD +0 -115
- {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/top_level.txt +0 -0
pdd/construct_paths.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
import sys
|
|
5
5
|
import os
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Dict, Tuple, Any, Optional, List
|
|
7
|
+
from typing import Dict, Tuple, Any, Optional, List, Callable
|
|
8
8
|
import fnmatch
|
|
9
9
|
import logging
|
|
10
10
|
|
|
@@ -56,6 +56,23 @@ def _load_pddrc_config(pddrc_path: Path) -> Dict[str, Any]:
|
|
|
56
56
|
except Exception as e:
|
|
57
57
|
raise ValueError(f"Error loading .pddrc: {e}")
|
|
58
58
|
|
|
59
|
+
def list_available_contexts(start_path: Optional[Path] = None) -> list[str]:
|
|
60
|
+
"""Return sorted context names from the nearest .pddrc.
|
|
61
|
+
|
|
62
|
+
- Searches upward from `start_path` (or CWD) for a `.pddrc` file.
|
|
63
|
+
- If found, loads and validates it, then returns sorted context names.
|
|
64
|
+
- If no `.pddrc` exists, returns ["default"].
|
|
65
|
+
- Propagates ValueError for malformed `.pddrc` to allow callers to render
|
|
66
|
+
helpful errors.
|
|
67
|
+
"""
|
|
68
|
+
pddrc = _find_pddrc_file(start_path)
|
|
69
|
+
if not pddrc:
|
|
70
|
+
return ["default"]
|
|
71
|
+
config = _load_pddrc_config(pddrc)
|
|
72
|
+
contexts = config.get("contexts", {})
|
|
73
|
+
names = sorted(contexts.keys()) if isinstance(contexts, dict) else []
|
|
74
|
+
return names or ["default"]
|
|
75
|
+
|
|
59
76
|
def _detect_context(current_dir: Path, config: Dict[str, Any], context_override: Optional[str] = None) -> Optional[str]:
|
|
60
77
|
"""Detect the appropriate context based on current directory path."""
|
|
61
78
|
if context_override:
|
|
@@ -108,8 +125,9 @@ def _resolve_config_hierarchy(
|
|
|
108
125
|
# Configuration keys to resolve
|
|
109
126
|
config_keys = {
|
|
110
127
|
'generate_output_path': 'PDD_GENERATE_OUTPUT_PATH',
|
|
111
|
-
'test_output_path': 'PDD_TEST_OUTPUT_PATH',
|
|
128
|
+
'test_output_path': 'PDD_TEST_OUTPUT_PATH',
|
|
112
129
|
'example_output_path': 'PDD_EXAMPLE_OUTPUT_PATH',
|
|
130
|
+
'prompts_dir': 'PDD_PROMPTS_DIR',
|
|
113
131
|
'default_language': 'PDD_DEFAULT_LANGUAGE',
|
|
114
132
|
'target_coverage': 'PDD_TEST_COVERAGE_TARGET',
|
|
115
133
|
'strength': None,
|
|
@@ -176,52 +194,41 @@ def _candidate_prompt_path(input_files: Dict[str, Path]) -> Path | None:
|
|
|
176
194
|
for p in input_files.values():
|
|
177
195
|
if p.suffix == ".prompt":
|
|
178
196
|
return p
|
|
197
|
+
|
|
198
|
+
# Final fallback: Return the first file path available (e.g. for pdd update <code_file>)
|
|
199
|
+
if input_files:
|
|
200
|
+
return next(iter(input_files.values()))
|
|
201
|
+
|
|
179
202
|
return None
|
|
180
203
|
|
|
181
204
|
|
|
182
205
|
# New helper function to check if a language is known
|
|
183
206
|
def _is_known_language(language_name: str) -> bool:
|
|
184
|
-
"""
|
|
207
|
+
"""Return True if the language is recognized.
|
|
208
|
+
|
|
209
|
+
Prefer CSV in PDD_PATH if available; otherwise fall back to a built-in set
|
|
210
|
+
so basename/language inference does not fail when PDD_PATH is unset.
|
|
211
|
+
"""
|
|
212
|
+
language_name_lower = (language_name or "").lower()
|
|
213
|
+
if not language_name_lower:
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
builtin_languages = {
|
|
217
|
+
'python', 'javascript', 'typescript', 'java', 'cpp', 'c', 'go', 'ruby', 'rust',
|
|
218
|
+
'kotlin', 'swift', 'csharp', 'php', 'scala', 'r', 'lua', 'perl', 'bash', 'shell',
|
|
219
|
+
'powershell', 'sql', 'prompt', 'html', 'css', 'makefile',
|
|
220
|
+
# Common data and config formats for architecture prompts and configs
|
|
221
|
+
'json', 'jsonl', 'yaml', 'yml', 'toml', 'ini'
|
|
222
|
+
}
|
|
223
|
+
|
|
185
224
|
pdd_path_str = os.getenv('PDD_PATH')
|
|
186
225
|
if not pdd_path_str:
|
|
187
|
-
|
|
188
|
-
# Or, for an internal helper, we might decide to log and return False,
|
|
189
|
-
# but raising an error for missing config is generally safer.
|
|
190
|
-
# However, _determine_language (the caller) already raises ValueError
|
|
191
|
-
# if language cannot be found, so this path might not be strictly necessary
|
|
192
|
-
# if we assume PDD_PATH is validated earlier or by other get_extension/get_language calls.
|
|
193
|
-
# For robustness here, let's keep a check but perhaps make it less severe if called internally.
|
|
194
|
-
# For now, align with how get_extension might handle it.
|
|
195
|
-
# console.print("[error]PDD_PATH environment variable is not set. Cannot validate language.", style="error")
|
|
196
|
-
# return False # Or raise error
|
|
197
|
-
# Given this is internal and other functions (get_extension) already depend on PDD_PATH,
|
|
198
|
-
# we can assume if those ran, PDD_PATH is set. If not, they'd fail first.
|
|
199
|
-
# So, we can simplify or rely on that pre-condition.
|
|
200
|
-
# Let's assume PDD_PATH will be set if other language functions are working.
|
|
201
|
-
# If it's critical, an explicit check and raise ValueError is better.
|
|
202
|
-
# For now, let's proceed assuming PDD_PATH is available if this point is reached.
|
|
203
|
-
pass # Assuming PDD_PATH is checked by get_extension/get_language if they are called
|
|
204
|
-
|
|
205
|
-
# If PDD_PATH is not set, this will likely fail earlier if get_extension/get_language are used.
|
|
206
|
-
# If we want this helper to be fully independent, it needs robust PDD_PATH handling.
|
|
207
|
-
# Let's assume for now, PDD_PATH is available if this point is reached through normal flow.
|
|
208
|
-
|
|
209
|
-
# Re-evaluate: PDD_PATH is critical for this function. Let's keep the check.
|
|
210
|
-
if not pdd_path_str:
|
|
211
|
-
# This helper might be called before get_extension in some logic paths
|
|
212
|
-
# if _determine_language prioritizes suffix checking first.
|
|
213
|
-
# So, it needs its own PDD_PATH check.
|
|
214
|
-
# Raise ValueError to be consistent with get_extension's behavior.
|
|
215
|
-
raise ValueError("PDD_PATH environment variable is not set. Cannot validate language.")
|
|
226
|
+
return language_name_lower in builtin_languages
|
|
216
227
|
|
|
217
228
|
csv_file_path = Path(pdd_path_str) / 'data' / 'language_format.csv'
|
|
218
|
-
|
|
219
229
|
if not csv_file_path.is_file():
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
language_name_lower = language_name.lower()
|
|
224
|
-
|
|
230
|
+
return language_name_lower in builtin_languages
|
|
231
|
+
|
|
225
232
|
try:
|
|
226
233
|
with open(csv_file_path, mode='r', encoding='utf-8', newline='') as csvfile:
|
|
227
234
|
reader = csv.DictReader(csvfile)
|
|
@@ -229,10 +236,10 @@ def _is_known_language(language_name: str) -> bool:
|
|
|
229
236
|
if row.get('language', '').lower() == language_name_lower:
|
|
230
237
|
return True
|
|
231
238
|
except csv.Error as e:
|
|
232
|
-
# Log and return False or raise a custom error
|
|
233
239
|
console.print(f"[error]CSV Error reading {csv_file_path}: {e}", style="error")
|
|
234
|
-
return
|
|
235
|
-
|
|
240
|
+
return language_name_lower in builtin_languages
|
|
241
|
+
|
|
242
|
+
return language_name_lower in builtin_languages
|
|
236
243
|
|
|
237
244
|
|
|
238
245
|
def _strip_language_suffix(path_like: os.PathLike[str]) -> str:
|
|
@@ -264,6 +271,24 @@ def _extract_basename(
|
|
|
264
271
|
"""
|
|
265
272
|
Deduce the project basename according to the rules explained in *Step A*.
|
|
266
273
|
"""
|
|
274
|
+
# Handle 'fix' command specifically to create a unique basename per test file
|
|
275
|
+
if command == "fix":
|
|
276
|
+
prompt_path = _candidate_prompt_path(input_file_paths)
|
|
277
|
+
if not prompt_path:
|
|
278
|
+
raise ValueError("Could not determine prompt file for 'fix' command.")
|
|
279
|
+
|
|
280
|
+
prompt_basename = _strip_language_suffix(prompt_path)
|
|
281
|
+
|
|
282
|
+
unit_test_path = input_file_paths.get("unit_test_file")
|
|
283
|
+
if not unit_test_path:
|
|
284
|
+
# Fallback to just the prompt basename if no unit test file is provided
|
|
285
|
+
# This might happen in some edge cases, but 'fix' command structure requires it
|
|
286
|
+
return prompt_basename
|
|
287
|
+
|
|
288
|
+
# Use the stem of the unit test file to make the basename unique
|
|
289
|
+
test_basename = Path(unit_test_path).stem
|
|
290
|
+
return f"{prompt_basename}_{test_basename}"
|
|
291
|
+
|
|
267
292
|
# Handle conflicts first due to its unique structure
|
|
268
293
|
if command == "conflicts":
|
|
269
294
|
key1 = "prompt1"
|
|
@@ -331,14 +356,48 @@ def _determine_language(
|
|
|
331
356
|
ext = path_obj.suffix
|
|
332
357
|
# Prioritize non-prompt code files
|
|
333
358
|
if ext and ext != ".prompt":
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
359
|
+
try:
|
|
360
|
+
language = get_language(ext)
|
|
361
|
+
if language:
|
|
362
|
+
return language.lower()
|
|
363
|
+
except ValueError:
|
|
364
|
+
# Fallback: load language CSV file directly when PDD_PATH is not set
|
|
365
|
+
try:
|
|
366
|
+
import csv
|
|
367
|
+
import os
|
|
368
|
+
# Try to find the CSV file relative to this script
|
|
369
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
370
|
+
csv_path = os.path.join(script_dir, 'data', 'language_format.csv')
|
|
371
|
+
if os.path.exists(csv_path):
|
|
372
|
+
with open(csv_path, 'r') as csvfile:
|
|
373
|
+
reader = csv.DictReader(csvfile)
|
|
374
|
+
for row in reader:
|
|
375
|
+
if row['extension'].lower() == ext.lower():
|
|
376
|
+
return row['language'].lower()
|
|
377
|
+
except (FileNotFoundError, csv.Error):
|
|
378
|
+
pass
|
|
337
379
|
# Handle files without extension like Makefile
|
|
338
380
|
elif not ext and path_obj.is_file(): # Check it's actually a file
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
381
|
+
try:
|
|
382
|
+
language = get_language(path_obj.name) # Check name (e.g., 'Makefile')
|
|
383
|
+
if language:
|
|
384
|
+
return language.lower()
|
|
385
|
+
except ValueError:
|
|
386
|
+
# Fallback: load language CSV file directly for files without extension
|
|
387
|
+
try:
|
|
388
|
+
import csv
|
|
389
|
+
import os
|
|
390
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
391
|
+
csv_path = os.path.join(script_dir, 'data', 'language_format.csv')
|
|
392
|
+
if os.path.exists(csv_path):
|
|
393
|
+
with open(csv_path, 'r') as csvfile:
|
|
394
|
+
reader = csv.DictReader(csvfile)
|
|
395
|
+
for row in reader:
|
|
396
|
+
# Check if the filename matches (for files without extension)
|
|
397
|
+
if not row['extension'] and path_obj.name.lower() == row['language'].lower():
|
|
398
|
+
return row['language'].lower()
|
|
399
|
+
except (FileNotFoundError, csv.Error):
|
|
400
|
+
pass
|
|
342
401
|
|
|
343
402
|
# 3 – parse from prompt filename suffix
|
|
344
403
|
prompt_path = _candidate_prompt_path(input_file_paths)
|
|
@@ -354,7 +413,7 @@ def _determine_language(
|
|
|
354
413
|
|
|
355
414
|
# 4 - Special handling for detect command - default to prompt for LLM prompts
|
|
356
415
|
if command == "detect" and "change_file" in input_file_paths:
|
|
357
|
-
return "prompt"
|
|
416
|
+
return "prompt"
|
|
358
417
|
|
|
359
418
|
# 5 - If no language determined, raise error
|
|
360
419
|
raise ValueError("Could not determine language from input files or options.")
|
|
@@ -374,6 +433,8 @@ def construct_paths(
|
|
|
374
433
|
command_options: Optional[Dict[str, Any]], # Allow None
|
|
375
434
|
create_error_file: bool = True, # Added parameter to control error file creation
|
|
376
435
|
context_override: Optional[str] = None, # Added parameter for context override
|
|
436
|
+
confirm_callback: Optional[Callable[[str, str], bool]] = None, # Callback for interactive confirmation
|
|
437
|
+
path_resolution_mode: Optional[str] = None, # "cwd" or "config_base" - if None, use command default
|
|
377
438
|
) -> Tuple[Dict[str, Any], Dict[str, str], Dict[str, str], str]:
|
|
378
439
|
"""
|
|
379
440
|
High‑level orchestrator that loads inputs, determines basename/language,
|
|
@@ -390,8 +451,10 @@ def construct_paths(
|
|
|
390
451
|
|
|
391
452
|
# ------------- Load .pddrc configuration -----------------
|
|
392
453
|
pddrc_config = {}
|
|
454
|
+
pddrc_path: Optional[Path] = None
|
|
393
455
|
context = None
|
|
394
456
|
context_config = {}
|
|
457
|
+
original_context_config = {} # Keep track of original context config for sync discovery
|
|
395
458
|
|
|
396
459
|
try:
|
|
397
460
|
# Find and load .pddrc file
|
|
@@ -405,6 +468,7 @@ def construct_paths(
|
|
|
405
468
|
|
|
406
469
|
# Get context-specific configuration
|
|
407
470
|
context_config = _get_context_config(pddrc_config, context)
|
|
471
|
+
original_context_config = context_config.copy() # Store original before modifications
|
|
408
472
|
|
|
409
473
|
if not quiet and context:
|
|
410
474
|
console.print(f"[info]Using .pddrc context:[/info] {context}")
|
|
@@ -448,13 +512,45 @@ def construct_paths(
|
|
|
448
512
|
language="python", # Dummy language
|
|
449
513
|
file_extension=".py", # Dummy extension
|
|
450
514
|
context_config=context_config,
|
|
515
|
+
config_base_dir=str(pddrc_path.parent) if pddrc_path else None,
|
|
516
|
+
path_resolution_mode="cwd", # Sync resolves paths relative to CWD
|
|
451
517
|
)
|
|
518
|
+
|
|
452
519
|
# Infer base directories from a sample output path
|
|
453
520
|
gen_path = Path(output_paths_str.get("generate_output_path", "src"))
|
|
454
|
-
|
|
455
|
-
|
|
521
|
+
|
|
522
|
+
# First, check current working directory for prompt files matching the basename pattern
|
|
523
|
+
current_dir = Path.cwd()
|
|
524
|
+
prompt_pattern = f"{basename}_*.prompt"
|
|
525
|
+
if list(current_dir.glob(prompt_pattern)):
|
|
526
|
+
# Found prompt files in current working directory
|
|
527
|
+
resolved_config["prompts_dir"] = str(current_dir)
|
|
528
|
+
resolved_config["code_dir"] = str(current_dir)
|
|
529
|
+
if not quiet:
|
|
530
|
+
console.print(f"[info]Found prompt files in current directory:[/info] {current_dir}")
|
|
531
|
+
else:
|
|
532
|
+
# Fall back to context-aware logic
|
|
533
|
+
# Use original_context_config to avoid checking augmented config with env vars
|
|
534
|
+
if original_context_config and any(key.endswith('_output_path') for key in original_context_config):
|
|
535
|
+
# For configured contexts, use prompts_dir from config if provided,
|
|
536
|
+
# otherwise default to "prompts" at the same level as output dirs
|
|
537
|
+
resolved_config["prompts_dir"] = original_context_config.get("prompts_dir", "prompts")
|
|
538
|
+
resolved_config["code_dir"] = str(gen_path.parent)
|
|
539
|
+
else:
|
|
540
|
+
# For default contexts, maintain relative relationship
|
|
541
|
+
# e.g., if code goes to "pi.py", prompts should be at "prompts/" (siblings)
|
|
542
|
+
resolved_config["prompts_dir"] = str(gen_path.parent / "prompts")
|
|
543
|
+
resolved_config["code_dir"] = str(gen_path.parent)
|
|
544
|
+
|
|
456
545
|
resolved_config["tests_dir"] = str(Path(output_paths_str.get("test_output_path", "tests")).parent)
|
|
457
|
-
|
|
546
|
+
# example_output_path can be a directory (e.g., "context/") or a file path (e.g., "examples/foo.py")
|
|
547
|
+
# If it ends with / or has no file extension, treat as directory; otherwise use parent
|
|
548
|
+
example_path_str = output_paths_str.get("example_output_path", "examples")
|
|
549
|
+
example_path = Path(example_path_str)
|
|
550
|
+
if example_path_str.endswith('/') or '.' not in example_path.name:
|
|
551
|
+
resolved_config["examples_dir"] = example_path_str.rstrip('/')
|
|
552
|
+
else:
|
|
553
|
+
resolved_config["examples_dir"] = str(example_path.parent)
|
|
458
554
|
|
|
459
555
|
except Exception as e:
|
|
460
556
|
console.print(f"[error]Failed to determine initial paths for sync: {e}", style="error")
|
|
@@ -473,11 +569,15 @@ def construct_paths(
|
|
|
473
569
|
for key, path_str in input_file_paths.items():
|
|
474
570
|
try:
|
|
475
571
|
path = Path(path_str).expanduser()
|
|
476
|
-
# Resolve non-error files strictly first
|
|
572
|
+
# Resolve non-error files strictly first, but be more lenient for sync command
|
|
477
573
|
if key != "error_file":
|
|
478
|
-
#
|
|
479
|
-
|
|
480
|
-
|
|
574
|
+
# For sync command, be more tolerant of non-existent files since we're just determining paths
|
|
575
|
+
if command == "sync":
|
|
576
|
+
input_paths[key] = path.resolve()
|
|
577
|
+
else:
|
|
578
|
+
# Let FileNotFoundError propagate naturally if path doesn't exist
|
|
579
|
+
resolved_path = path.resolve(strict=True)
|
|
580
|
+
input_paths[key] = resolved_path
|
|
481
581
|
else:
|
|
482
582
|
# Resolve error file non-strictly, existence checked later
|
|
483
583
|
input_paths[key] = path.resolve()
|
|
@@ -507,9 +607,14 @@ def construct_paths(
|
|
|
507
607
|
|
|
508
608
|
# Check existence again, especially for error_file which might have been created
|
|
509
609
|
if not path.exists():
|
|
510
|
-
#
|
|
511
|
-
|
|
512
|
-
|
|
610
|
+
# For sync command, be more tolerant of non-existent files since we're just determining paths
|
|
611
|
+
if command == "sync":
|
|
612
|
+
# Skip reading content for non-existent files in sync mode
|
|
613
|
+
continue
|
|
614
|
+
else:
|
|
615
|
+
# This case should ideally be caught by resolve(strict=True) earlier for non-error files
|
|
616
|
+
# Raise standard FileNotFoundError
|
|
617
|
+
raise FileNotFoundError(f"{path}")
|
|
513
618
|
|
|
514
619
|
if path.is_file(): # Read only if it's a file
|
|
515
620
|
try:
|
|
@@ -574,7 +679,25 @@ def construct_paths(
|
|
|
574
679
|
style="warning"
|
|
575
680
|
)
|
|
576
681
|
|
|
577
|
-
|
|
682
|
+
|
|
683
|
+
# Try to get extension from CSV; fallback to built-in mapping if PDD_PATH/CSV unavailable
|
|
684
|
+
try:
|
|
685
|
+
file_extension = get_extension(language) # Pass determined language
|
|
686
|
+
if not file_extension and (language or '').lower() != 'prompt':
|
|
687
|
+
raise ValueError('empty extension')
|
|
688
|
+
except Exception:
|
|
689
|
+
builtin_ext_map = {
|
|
690
|
+
'python': '.py', 'javascript': '.js', 'typescript': '.ts', 'java': '.java',
|
|
691
|
+
'cpp': '.cpp', 'c': '.c', 'go': '.go', 'ruby': '.rb', 'rust': '.rs',
|
|
692
|
+
'kotlin': '.kt', 'swift': '.swift', 'csharp': '.cs', 'php': '.php',
|
|
693
|
+
'scala': '.scala', 'r': '.r', 'lua': '.lua', 'perl': '.pl', 'bash': '.sh',
|
|
694
|
+
'shell': '.sh', 'powershell': '.ps1', 'sql': '.sql', 'html': '.html', 'css': '.css',
|
|
695
|
+
'prompt': '.prompt', 'makefile': '',
|
|
696
|
+
# Common data/config formats
|
|
697
|
+
'json': '.json', 'jsonl': '.jsonl', 'yaml': '.yaml', 'yml': '.yml', 'toml': '.toml', 'ini': '.ini'
|
|
698
|
+
}
|
|
699
|
+
file_extension = builtin_ext_map.get(language.lower(), f".{language.lower()}" if language else '')
|
|
700
|
+
|
|
578
701
|
|
|
579
702
|
|
|
580
703
|
# ------------- Step 3b: build output paths ---------------
|
|
@@ -584,10 +707,52 @@ def construct_paths(
|
|
|
584
707
|
if k.startswith("output") and v is not None # Ensure value is not None
|
|
585
708
|
}
|
|
586
709
|
|
|
710
|
+
# Determine input file directory for default output path generation
|
|
711
|
+
# Only apply for commands that generate/update files based on specific input files
|
|
712
|
+
# Commands like sync, generate, test, example have their own directory management
|
|
713
|
+
commands_using_input_dir = {'fix', 'crash', 'verify', 'split', 'change', 'update'}
|
|
714
|
+
input_file_dir: Optional[str] = None
|
|
715
|
+
input_file_dirs: Dict[str, Optional[str]] = {}
|
|
716
|
+
if input_paths and command in commands_using_input_dir:
|
|
717
|
+
try:
|
|
718
|
+
# For fix/crash/verify commands, use specific file directories for each output
|
|
719
|
+
if command in {'fix', 'crash', 'verify'}:
|
|
720
|
+
# Map output keys to their corresponding input file keys
|
|
721
|
+
input_key_map = {
|
|
722
|
+
'fix': {'output_code': 'code_file', 'output_test': 'unit_test_file', 'output_results': 'code_file'},
|
|
723
|
+
'crash': {'output': 'code_file', 'output_program': 'program_file'},
|
|
724
|
+
'verify': {'output_code': 'code_file', 'output_program': 'verification_program', 'output_results': 'code_file'},
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
for output_key, input_key in input_key_map.get(command, {}).items():
|
|
728
|
+
if input_key in input_paths:
|
|
729
|
+
input_file_dirs[output_key] = str(input_paths[input_key].parent)
|
|
730
|
+
|
|
731
|
+
# Set default input_file_dir to code_file directory as fallback
|
|
732
|
+
if 'code_file' in input_paths:
|
|
733
|
+
input_file_dir = str(input_paths['code_file'].parent)
|
|
734
|
+
else:
|
|
735
|
+
first_input_path = next(iter(input_paths.values()))
|
|
736
|
+
input_file_dir = str(first_input_path.parent)
|
|
737
|
+
else:
|
|
738
|
+
# For other commands, use first input path
|
|
739
|
+
first_input_path = next(iter(input_paths.values()))
|
|
740
|
+
input_file_dir = str(first_input_path.parent)
|
|
741
|
+
except (StopIteration, AttributeError):
|
|
742
|
+
# If no input paths or path doesn't have parent, use None (falls back to CWD)
|
|
743
|
+
pass
|
|
744
|
+
|
|
587
745
|
try:
|
|
588
746
|
# generate_output_paths might return Dict[str, str] or Dict[str, Path]
|
|
589
747
|
# Let's assume it returns Dict[str, str] based on verification error,
|
|
590
748
|
# and convert them to Path objects here.
|
|
749
|
+
# Determine path resolution mode:
|
|
750
|
+
# - If explicitly provided, use it
|
|
751
|
+
# - Otherwise: sync uses "cwd", other commands use "config_base"
|
|
752
|
+
effective_path_resolution_mode = path_resolution_mode
|
|
753
|
+
if effective_path_resolution_mode is None:
|
|
754
|
+
effective_path_resolution_mode = "cwd" if command == "sync" else "config_base"
|
|
755
|
+
|
|
591
756
|
output_paths_str: Dict[str, str] = generate_output_paths(
|
|
592
757
|
command=command,
|
|
593
758
|
output_locations=output_location_opts,
|
|
@@ -595,7 +760,12 @@ def construct_paths(
|
|
|
595
760
|
language=language,
|
|
596
761
|
file_extension=file_extension,
|
|
597
762
|
context_config=context_config,
|
|
763
|
+
input_file_dir=input_file_dir,
|
|
764
|
+
input_file_dirs=input_file_dirs,
|
|
765
|
+
config_base_dir=str(pddrc_path.parent) if pddrc_path else None,
|
|
766
|
+
path_resolution_mode=effective_path_resolution_mode,
|
|
598
767
|
)
|
|
768
|
+
|
|
599
769
|
# Convert to Path objects for internal use
|
|
600
770
|
output_paths_resolved: Dict[str, Path] = {k: Path(v) for k, v in output_paths_str.items()}
|
|
601
771
|
|
|
@@ -604,36 +774,59 @@ def construct_paths(
|
|
|
604
774
|
raise # Re-raise the ValueError
|
|
605
775
|
|
|
606
776
|
# ------------- Step 4: overwrite confirmation ------------
|
|
607
|
-
#
|
|
777
|
+
# Initialize existing_files before the conditional to avoid UnboundLocalError
|
|
608
778
|
existing_files: Dict[str, Path] = {}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
779
|
+
|
|
780
|
+
if command in ["test", "bug"] and not force:
|
|
781
|
+
# For test/bug commands without --force, create numbered files instead of overwriting
|
|
782
|
+
for key, path in output_paths_resolved.items():
|
|
783
|
+
if path.is_file():
|
|
784
|
+
base, ext = os.path.splitext(path)
|
|
785
|
+
i = 1
|
|
786
|
+
new_path = Path(f"{base}_{i}{ext}")
|
|
787
|
+
while new_path.exists():
|
|
788
|
+
i += 1
|
|
789
|
+
new_path = Path(f"{base}_{i}{ext}")
|
|
790
|
+
output_paths_resolved[key] = new_path
|
|
791
|
+
else:
|
|
792
|
+
# Check if any output *file* exists (operate on Path objects)
|
|
793
|
+
for k, p_obj in output_paths_resolved.items():
|
|
794
|
+
if p_obj.is_file():
|
|
795
|
+
existing_files[k] = p_obj # Store the Path object
|
|
613
796
|
|
|
614
797
|
if existing_files and not force:
|
|
798
|
+
paths_list = "\n".join(f" • {p.resolve()}" for p in existing_files.values())
|
|
615
799
|
if not quiet:
|
|
616
800
|
# Use the Path objects stored in existing_files for resolve()
|
|
617
801
|
# Print without Rich tags for easier testing
|
|
618
|
-
paths_list = "\n".join(f" • {p.resolve()}" for p in existing_files.values())
|
|
619
802
|
console.print(
|
|
620
803
|
f"Warning: The following output files already exist and may be overwritten:\n{paths_list}",
|
|
621
804
|
style="warning"
|
|
622
805
|
)
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
click.
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
806
|
+
|
|
807
|
+
# Use confirm_callback if provided (for TUI environments), otherwise use click.confirm
|
|
808
|
+
if confirm_callback is not None:
|
|
809
|
+
# Use the provided callback for confirmation (e.g., from Textual TUI)
|
|
810
|
+
confirm_message = f"The following files will be overwritten:\n{paths_list}\n\nOverwrite existing files?"
|
|
811
|
+
if not confirm_callback(confirm_message, "Overwrite Confirmation"):
|
|
812
|
+
raise click.Abort()
|
|
813
|
+
else:
|
|
814
|
+
# Use click.confirm for CLI interaction
|
|
815
|
+
try:
|
|
816
|
+
if not click.confirm(
|
|
817
|
+
click.style("Overwrite existing files?", fg="yellow"), default=True, show_default=True
|
|
818
|
+
):
|
|
819
|
+
click.secho("Operation cancelled.", fg="red", err=True)
|
|
820
|
+
raise click.Abort()
|
|
821
|
+
except click.Abort:
|
|
822
|
+
raise # Let Abort propagate to be handled by PDDCLI.invoke()
|
|
823
|
+
except Exception as e: # Catch potential errors during confirm (like EOFError in non-interactive)
|
|
824
|
+
if 'EOF' in str(e) or 'end-of-file' in str(e).lower():
|
|
825
|
+
# Non-interactive environment, default to not overwriting
|
|
826
|
+
click.secho("Non-interactive environment detected. Use --force to overwrite existing files.", fg="yellow", err=True)
|
|
827
|
+
else:
|
|
828
|
+
click.secho(f"Confirmation failed: {e}. Aborting.", fg="red", err=True)
|
|
829
|
+
raise click.Abort()
|
|
637
830
|
|
|
638
831
|
|
|
639
832
|
# ------------- Final reporting ---------------------------
|
|
@@ -661,7 +854,14 @@ def construct_paths(
|
|
|
661
854
|
resolved_config["prompts_dir"] = str(next(iter(input_paths.values())).parent)
|
|
662
855
|
resolved_config["code_dir"] = str(gen_path.parent)
|
|
663
856
|
resolved_config["tests_dir"] = str(Path(resolved_config.get("test_output_path", "tests")).parent)
|
|
664
|
-
|
|
857
|
+
# example_output_path can be a directory (e.g., "context/") or a file path (e.g., "examples/foo.py")
|
|
858
|
+
# If it ends with / or has no file extension, treat as directory; otherwise use parent
|
|
859
|
+
example_path_str = resolved_config.get("example_output_path", "examples")
|
|
860
|
+
example_path = Path(example_path_str)
|
|
861
|
+
if example_path_str.endswith('/') or '.' not in example_path.name:
|
|
862
|
+
resolved_config["examples_dir"] = example_path_str.rstrip('/')
|
|
863
|
+
else:
|
|
864
|
+
resolved_config["examples_dir"] = str(example_path.parent)
|
|
665
865
|
|
|
666
866
|
|
|
667
|
-
return resolved_config, input_strings, output_file_paths_str_return, language
|
|
867
|
+
return resolved_config, input_strings, output_file_paths_str_return, language
|
pdd/context_generator.py
CHANGED
|
@@ -16,6 +16,9 @@ def context_generator(
|
|
|
16
16
|
temperature: float = 0,
|
|
17
17
|
time: Optional[float] = DEFAULT_TIME,
|
|
18
18
|
verbose: bool = False,
|
|
19
|
+
source_file_path: str = None,
|
|
20
|
+
example_file_path: str = None,
|
|
21
|
+
module_name: str = None,
|
|
19
22
|
) -> tuple:
|
|
20
23
|
"""
|
|
21
24
|
Generates a concise example on how to use a given code module properly.
|
|
@@ -79,7 +82,10 @@ def context_generator(
|
|
|
79
82
|
input_json={
|
|
80
83
|
"code_module": code_module,
|
|
81
84
|
"processed_prompt": processed_prompt,
|
|
82
|
-
"language": language
|
|
85
|
+
"language": language,
|
|
86
|
+
"source_file_path": source_file_path or "",
|
|
87
|
+
"example_file_path": example_file_path or "",
|
|
88
|
+
"module_name": module_name or ""
|
|
83
89
|
},
|
|
84
90
|
strength=strength,
|
|
85
91
|
temperature=temperature,
|
|
@@ -95,6 +101,7 @@ def context_generator(
|
|
|
95
101
|
strength=0.5,
|
|
96
102
|
temperature=temperature,
|
|
97
103
|
time=time,
|
|
104
|
+
language=language,
|
|
98
105
|
verbose=verbose
|
|
99
106
|
)
|
|
100
107
|
except Exception as e:
|
|
@@ -112,6 +119,7 @@ def context_generator(
|
|
|
112
119
|
strength=strength,
|
|
113
120
|
temperature=temperature,
|
|
114
121
|
time=time,
|
|
122
|
+
language=language,
|
|
115
123
|
verbose=verbose
|
|
116
124
|
)
|
|
117
125
|
total_cost = llm_response['cost'] + unfinished_cost + continue_cost
|
|
@@ -149,4 +157,4 @@ if __name__ == "__main__":
|
|
|
149
157
|
print("[bold green]Generated Example Code:[/bold green]")
|
|
150
158
|
print(example_code)
|
|
151
159
|
print(f"[bold blue]Total Cost: ${total_cost:.6f}[/bold blue]")
|
|
152
|
-
print(f"[bold blue]Model Name: {model_name}[/bold blue]")
|
|
160
|
+
print(f"[bold blue]Model Name: {model_name}[/bold blue]")
|