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.
- pdd/__init__.py +40 -8
- pdd/agentic_bug.py +323 -0
- pdd/agentic_bug_orchestrator.py +497 -0
- pdd/agentic_change.py +231 -0
- pdd/agentic_change_orchestrator.py +526 -0
- pdd/agentic_common.py +598 -0
- pdd/agentic_crash.py +534 -0
- pdd/agentic_e2e_fix.py +319 -0
- pdd/agentic_e2e_fix_orchestrator.py +426 -0
- pdd/agentic_fix.py +1294 -0
- pdd/agentic_langtest.py +162 -0
- pdd/agentic_update.py +387 -0
- pdd/agentic_verify.py +183 -0
- pdd/architecture_sync.py +565 -0
- pdd/auth_service.py +210 -0
- pdd/auto_deps_main.py +71 -51
- pdd/auto_include.py +245 -5
- pdd/auto_update.py +125 -47
- pdd/bug_main.py +196 -23
- pdd/bug_to_unit_test.py +2 -0
- pdd/change_main.py +11 -4
- pdd/cli.py +22 -1181
- pdd/cmd_test_main.py +350 -150
- pdd/code_generator.py +60 -18
- pdd/code_generator_main.py +790 -57
- pdd/commands/__init__.py +48 -0
- pdd/commands/analysis.py +306 -0
- pdd/commands/auth.py +309 -0
- pdd/commands/connect.py +290 -0
- pdd/commands/fix.py +163 -0
- pdd/commands/generate.py +257 -0
- pdd/commands/maintenance.py +175 -0
- pdd/commands/misc.py +87 -0
- pdd/commands/modify.py +256 -0
- pdd/commands/report.py +144 -0
- pdd/commands/sessions.py +284 -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 +589 -111
- pdd/context_generator.py +10 -2
- pdd/context_generator_main.py +175 -76
- pdd/continue_generation.py +53 -10
- pdd/core/__init__.py +33 -0
- pdd/core/cli.py +527 -0
- pdd/core/cloud.py +237 -0
- pdd/core/dump.py +554 -0
- pdd/core/errors.py +67 -0
- pdd/core/remote_session.py +61 -0
- pdd/core/utils.py +90 -0
- pdd/crash_main.py +262 -33
- pdd/data/language_format.csv +71 -63
- pdd/data/llm_model.csv +20 -18
- pdd/detect_change_main.py +5 -4
- pdd/docs/prompting_guide.md +864 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
- pdd/fix_code_loop.py +523 -95
- pdd/fix_code_module_errors.py +6 -2
- pdd/fix_error_loop.py +491 -92
- pdd/fix_errors_from_unit_tests.py +4 -3
- pdd/fix_main.py +278 -21
- pdd/fix_verification_errors.py +12 -100
- pdd/fix_verification_errors_loop.py +529 -286
- pdd/fix_verification_main.py +294 -89
- pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
- pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
- pdd/frontend/dist/index.html +376 -0
- pdd/frontend/dist/logo.svg +33 -0
- pdd/generate_output_paths.py +139 -15
- pdd/generate_test.py +218 -146
- pdd/get_comment.py +19 -44
- pdd/get_extension.py +8 -9
- pdd/get_jwt_token.py +318 -22
- pdd/get_language.py +8 -7
- pdd/get_run_command.py +75 -0
- pdd/get_test_command.py +68 -0
- pdd/git_update.py +70 -19
- pdd/incremental_code_generator.py +2 -2
- pdd/insert_includes.py +13 -4
- pdd/llm_invoke.py +1711 -181
- pdd/load_prompt_template.py +19 -12
- pdd/path_resolution.py +140 -0
- pdd/pdd_completion.fish +25 -2
- pdd/pdd_completion.sh +30 -4
- pdd/pdd_completion.zsh +79 -4
- pdd/postprocess.py +14 -4
- pdd/preprocess.py +293 -24
- pdd/preprocess_main.py +41 -6
- pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
- pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
- pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
- pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
- pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
- pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
- pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
- pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
- pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
- pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
- pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
- pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
- pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
- pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
- pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
- pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
- pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
- pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
- pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
- pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
- pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
- pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
- pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
- pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
- pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
- pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
- pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
- pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -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 +925 -0
- pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
- pdd/prompts/auto_include_LLM.prompt +122 -905
- pdd/prompts/change_LLM.prompt +3093 -1
- pdd/prompts/detect_change_LLM.prompt +686 -27
- pdd/prompts/example_generator_LLM.prompt +22 -1
- pdd/prompts/extract_code_LLM.prompt +5 -1
- pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
- 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 +12 -2
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +9 -0
- pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
- pdd/prompts/generate_test_LLM.prompt +41 -7
- pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
- pdd/prompts/increase_tests_LLM.prompt +1 -5
- pdd/prompts/insert_includes_LLM.prompt +316 -186
- pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
- pdd/prompts/prompt_diff_LLM.prompt +82 -0
- pdd/prompts/trace_LLM.prompt +25 -22
- pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
- pdd/prompts/update_prompt_LLM.prompt +22 -1
- pdd/pytest_output.py +127 -12
- pdd/remote_session.py +876 -0
- pdd/render_mermaid.py +236 -0
- pdd/server/__init__.py +52 -0
- pdd/server/app.py +335 -0
- pdd/server/click_executor.py +587 -0
- pdd/server/executor.py +338 -0
- pdd/server/jobs.py +661 -0
- pdd/server/models.py +241 -0
- pdd/server/routes/__init__.py +31 -0
- pdd/server/routes/architecture.py +451 -0
- pdd/server/routes/auth.py +364 -0
- pdd/server/routes/commands.py +929 -0
- pdd/server/routes/config.py +42 -0
- pdd/server/routes/files.py +603 -0
- pdd/server/routes/prompts.py +1322 -0
- pdd/server/routes/websocket.py +473 -0
- pdd/server/security.py +243 -0
- pdd/server/terminal_spawner.py +209 -0
- pdd/server/token_counter.py +222 -0
- pdd/setup_tool.py +648 -0
- pdd/simple_math.py +2 -0
- pdd/split_main.py +3 -2
- pdd/summarize_directory.py +237 -195
- pdd/sync_animation.py +8 -4
- pdd/sync_determine_operation.py +839 -112
- pdd/sync_main.py +351 -57
- pdd/sync_orchestration.py +1400 -756
- pdd/sync_tui.py +848 -0
- pdd/template_expander.py +161 -0
- pdd/template_registry.py +264 -0
- pdd/templates/architecture/architecture_json.prompt +237 -0
- pdd/templates/generic/generate_prompt.prompt +174 -0
- pdd/trace.py +168 -12
- pdd/trace_main.py +4 -3
- pdd/track_cost.py +140 -63
- pdd/unfinished_prompt.py +51 -4
- pdd/update_main.py +567 -67
- pdd/update_model_costs.py +2 -2
- pdd/update_prompt.py +19 -4
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +29 -11
- pdd_cli-0.0.118.dist-info/RECORD +227 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +1 -1
- pdd_cli-0.0.45.dist-info/RECORD +0 -116
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.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,163 @@ 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
|
+
|
|
76
|
+
def _match_path_to_contexts(
|
|
77
|
+
path_str: str,
|
|
78
|
+
contexts: Dict[str, Any],
|
|
79
|
+
use_specificity: bool = False,
|
|
80
|
+
is_absolute: bool = False
|
|
81
|
+
) -> Optional[str]:
|
|
82
|
+
"""
|
|
83
|
+
Core pattern matching logic - matches a path against context patterns.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
path_str: Path to match (can be relative or absolute)
|
|
87
|
+
contexts: The contexts dict from .pddrc config
|
|
88
|
+
use_specificity: If True, return most specific match; else first match
|
|
89
|
+
is_absolute: If True, use absolute path matching with "*/" prefix
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Context name or None
|
|
93
|
+
"""
|
|
94
|
+
matches = []
|
|
95
|
+
for context_name, context_config in contexts.items():
|
|
96
|
+
if context_name == 'default':
|
|
97
|
+
continue
|
|
98
|
+
for path_pattern in context_config.get('paths', []):
|
|
99
|
+
pattern_base = path_pattern.rstrip('/**').rstrip('/*')
|
|
100
|
+
|
|
101
|
+
# Check for match - handle both absolute and relative paths
|
|
102
|
+
matched = False
|
|
103
|
+
if is_absolute:
|
|
104
|
+
# For absolute paths (CWD-based detection), use existing logic
|
|
105
|
+
if fnmatch.fnmatch(path_str, f"*/{path_pattern}") or \
|
|
106
|
+
fnmatch.fnmatch(path_str, path_pattern) or \
|
|
107
|
+
path_str.endswith(f"/{pattern_base}"):
|
|
108
|
+
matched = True
|
|
109
|
+
else:
|
|
110
|
+
# For relative paths (file-based detection)
|
|
111
|
+
if fnmatch.fnmatch(path_str, path_pattern) or \
|
|
112
|
+
path_str.startswith(pattern_base + '/') or \
|
|
113
|
+
path_str.startswith(pattern_base):
|
|
114
|
+
matched = True
|
|
115
|
+
|
|
116
|
+
if matched:
|
|
117
|
+
if not use_specificity:
|
|
118
|
+
return context_name # First match wins
|
|
119
|
+
matches.append((context_name, len(pattern_base)))
|
|
120
|
+
|
|
121
|
+
if matches:
|
|
122
|
+
matches.sort(key=lambda x: x[1], reverse=True)
|
|
123
|
+
return matches[0][0]
|
|
124
|
+
|
|
125
|
+
return 'default' if 'default' in contexts else None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _detect_context_from_basename(basename: str, config: Dict[str, Any]) -> Optional[str]:
|
|
129
|
+
"""Detect context by matching a sync basename against prompts_dir prefixes or paths patterns."""
|
|
130
|
+
if not basename:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
contexts = config.get('contexts', {})
|
|
134
|
+
matches = []
|
|
135
|
+
|
|
136
|
+
for context_name, context_config in contexts.items():
|
|
137
|
+
if context_name == 'default':
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
defaults = context_config.get('defaults', {})
|
|
141
|
+
prompts_dir = defaults.get('prompts_dir', '')
|
|
142
|
+
if prompts_dir:
|
|
143
|
+
normalized = prompts_dir.rstrip('/')
|
|
144
|
+
prefix = normalized
|
|
145
|
+
if normalized == 'prompts':
|
|
146
|
+
prefix = ''
|
|
147
|
+
elif normalized.startswith('prompts/'):
|
|
148
|
+
prefix = normalized[len('prompts/'):]
|
|
149
|
+
|
|
150
|
+
if prefix and (basename == prefix or basename.startswith(prefix + '/')):
|
|
151
|
+
matches.append((context_name, len(prefix)))
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
for path_pattern in context_config.get('paths', []):
|
|
155
|
+
pattern_base = path_pattern.rstrip('/**').rstrip('/*')
|
|
156
|
+
if fnmatch.fnmatch(basename, path_pattern) or \
|
|
157
|
+
basename.startswith(pattern_base + '/') or \
|
|
158
|
+
basename == pattern_base:
|
|
159
|
+
matches.append((context_name, len(pattern_base)))
|
|
160
|
+
|
|
161
|
+
if not matches:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
matches.sort(key=lambda item: item[1], reverse=True)
|
|
165
|
+
return matches[0][0]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _get_relative_basename(input_path: str, pattern: str) -> str:
|
|
169
|
+
"""
|
|
170
|
+
Compute basename relative to the matched pattern base.
|
|
171
|
+
|
|
172
|
+
This is critical for Issue #237: when a context pattern like
|
|
173
|
+
'frontend/components/**' matches 'frontend/components/marketplace/AssetCard',
|
|
174
|
+
we need to return 'marketplace/AssetCard' (relative to pattern base),
|
|
175
|
+
not the full path which would cause double-pathing in output.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
input_path: The full input path (e.g., 'frontend/components/marketplace/AssetCard')
|
|
179
|
+
pattern: The matching pattern (e.g., 'frontend/components/**')
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Path relative to the pattern base (e.g., 'marketplace/AssetCard')
|
|
183
|
+
|
|
184
|
+
Examples:
|
|
185
|
+
>>> _get_relative_basename('frontend/components/marketplace/AssetCard', 'frontend/components/**')
|
|
186
|
+
'marketplace/AssetCard'
|
|
187
|
+
>>> _get_relative_basename('backend/utils/credit_helpers', 'backend/utils/**')
|
|
188
|
+
'credit_helpers'
|
|
189
|
+
>>> _get_relative_basename('unknown/path', 'other/**')
|
|
190
|
+
'unknown/path' # No match, return as-is
|
|
191
|
+
"""
|
|
192
|
+
# Strip glob patterns to get the base directory
|
|
193
|
+
pattern_base = pattern.rstrip('/**').rstrip('/*').rstrip('*')
|
|
194
|
+
|
|
195
|
+
# Remove trailing slash from pattern base if present
|
|
196
|
+
pattern_base = pattern_base.rstrip('/')
|
|
197
|
+
|
|
198
|
+
# Check if input path starts with pattern base
|
|
199
|
+
if input_path.startswith(pattern_base + '/'):
|
|
200
|
+
# Return the portion after pattern_base/
|
|
201
|
+
return input_path[len(pattern_base) + 1:]
|
|
202
|
+
elif input_path.startswith(pattern_base) and len(input_path) > len(pattern_base):
|
|
203
|
+
# Handle case where pattern_base has no trailing content
|
|
204
|
+
remainder = input_path[len(pattern_base):]
|
|
205
|
+
if remainder.startswith('/'):
|
|
206
|
+
return remainder[1:]
|
|
207
|
+
return remainder
|
|
208
|
+
elif input_path == pattern_base:
|
|
209
|
+
# Exact match - return just the last component
|
|
210
|
+
return input_path.split('/')[-1] if '/' in input_path else input_path
|
|
211
|
+
|
|
212
|
+
# No match - return as-is (fallback for default context)
|
|
213
|
+
return input_path
|
|
214
|
+
|
|
215
|
+
|
|
59
216
|
def _detect_context(current_dir: Path, config: Dict[str, Any], context_override: Optional[str] = None) -> Optional[str]:
|
|
60
217
|
"""Detect the appropriate context based on current directory path."""
|
|
61
218
|
if context_override:
|
|
@@ -65,28 +222,82 @@ def _detect_context(current_dir: Path, config: Dict[str, Any], context_override:
|
|
|
65
222
|
available = list(contexts.keys())
|
|
66
223
|
raise ValueError(f"Unknown context '{context_override}'. Available contexts: {available}")
|
|
67
224
|
return context_override
|
|
68
|
-
|
|
225
|
+
|
|
69
226
|
contexts = config.get('contexts', {})
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
227
|
+
return _match_path_to_contexts(str(current_dir), contexts, use_specificity=False, is_absolute=True)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def detect_context_for_file(file_path: str, repo_root: Optional[str] = None) -> Tuple[Optional[str], Dict[str, Any]]:
|
|
231
|
+
"""
|
|
232
|
+
Detect the appropriate context for a file path based on .pddrc configuration.
|
|
233
|
+
|
|
234
|
+
This function finds the most specific matching context by comparing pattern lengths.
|
|
235
|
+
For example, 'backend/functions/utils/**' is more specific than 'backend/**'.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
file_path: Path to the file (can be absolute or relative)
|
|
239
|
+
repo_root: Optional repository root path. If not provided, will be detected.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Tuple of (context_name, context_config_defaults) or (None, {}) if no match.
|
|
243
|
+
"""
|
|
244
|
+
# Find repo root if not provided
|
|
245
|
+
if repo_root is None:
|
|
246
|
+
pddrc_path = _find_pddrc_file(Path(file_path).parent)
|
|
247
|
+
if pddrc_path:
|
|
248
|
+
repo_root = str(pddrc_path.parent)
|
|
249
|
+
else:
|
|
250
|
+
try:
|
|
251
|
+
import git
|
|
252
|
+
repo = git.Repo(file_path, search_parent_directories=True)
|
|
253
|
+
repo_root = repo.working_tree_dir
|
|
254
|
+
except:
|
|
255
|
+
repo_root = os.getcwd()
|
|
256
|
+
|
|
257
|
+
# Make file_path relative to repo_root for matching
|
|
258
|
+
file_path_abs = os.path.abspath(file_path)
|
|
259
|
+
repo_root_abs = os.path.abspath(repo_root)
|
|
260
|
+
|
|
261
|
+
if file_path_abs.startswith(repo_root_abs):
|
|
262
|
+
relative_path = os.path.relpath(file_path_abs, repo_root_abs)
|
|
263
|
+
else:
|
|
264
|
+
relative_path = file_path
|
|
265
|
+
|
|
266
|
+
# Find and load .pddrc
|
|
267
|
+
pddrc_path = _find_pddrc_file(Path(repo_root))
|
|
268
|
+
if not pddrc_path:
|
|
269
|
+
return None, {}
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
config = _load_pddrc_config(pddrc_path)
|
|
273
|
+
except ValueError:
|
|
274
|
+
return None, {}
|
|
275
|
+
|
|
276
|
+
contexts = config.get('contexts', {})
|
|
277
|
+
|
|
278
|
+
# First, try to match against prompts_dir for each context
|
|
279
|
+
# This allows prompt files to be detected even when paths pattern only matches code files
|
|
280
|
+
prompts_dir_matches = []
|
|
73
281
|
for context_name, context_config in contexts.items():
|
|
74
282
|
if context_name == 'default':
|
|
75
|
-
continue
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return
|
|
88
|
-
|
|
89
|
-
|
|
283
|
+
continue
|
|
284
|
+
prompts_dir = context_config.get('defaults', {}).get('prompts_dir', '')
|
|
285
|
+
if prompts_dir:
|
|
286
|
+
prompts_dir_normalized = prompts_dir.rstrip('/')
|
|
287
|
+
if relative_path.startswith(prompts_dir_normalized + '/') or relative_path == prompts_dir_normalized:
|
|
288
|
+
# Track match with specificity (length of prompts_dir)
|
|
289
|
+
prompts_dir_matches.append((context_name, len(prompts_dir_normalized)))
|
|
290
|
+
|
|
291
|
+
# Return most specific prompts_dir match if any
|
|
292
|
+
if prompts_dir_matches:
|
|
293
|
+
prompts_dir_matches.sort(key=lambda x: x[1], reverse=True)
|
|
294
|
+
matched_context = prompts_dir_matches[0][0]
|
|
295
|
+
return matched_context, _get_context_config(config, matched_context)
|
|
296
|
+
|
|
297
|
+
# Fall back to existing paths pattern matching
|
|
298
|
+
context_name = _match_path_to_contexts(relative_path, contexts, use_specificity=True, is_absolute=False)
|
|
299
|
+
return context_name, _get_context_config(config, context_name)
|
|
300
|
+
|
|
90
301
|
|
|
91
302
|
def _get_context_config(config: Dict[str, Any], context_name: Optional[str]) -> Dict[str, Any]:
|
|
92
303
|
"""Get configuration settings for the specified context."""
|
|
@@ -104,12 +315,13 @@ def _resolve_config_hierarchy(
|
|
|
104
315
|
) -> Dict[str, Any]:
|
|
105
316
|
"""Apply configuration hierarchy: CLI > context > environment > defaults."""
|
|
106
317
|
resolved = {}
|
|
107
|
-
|
|
318
|
+
|
|
108
319
|
# Configuration keys to resolve
|
|
109
320
|
config_keys = {
|
|
110
321
|
'generate_output_path': 'PDD_GENERATE_OUTPUT_PATH',
|
|
111
|
-
'test_output_path': 'PDD_TEST_OUTPUT_PATH',
|
|
322
|
+
'test_output_path': 'PDD_TEST_OUTPUT_PATH',
|
|
112
323
|
'example_output_path': 'PDD_EXAMPLE_OUTPUT_PATH',
|
|
324
|
+
'prompts_dir': 'PDD_PROMPTS_DIR',
|
|
113
325
|
'default_language': 'PDD_DEFAULT_LANGUAGE',
|
|
114
326
|
'target_coverage': 'PDD_TEST_COVERAGE_TARGET',
|
|
115
327
|
'strength': None,
|
|
@@ -117,7 +329,7 @@ def _resolve_config_hierarchy(
|
|
|
117
329
|
'budget': None,
|
|
118
330
|
'max_attempts': None,
|
|
119
331
|
}
|
|
120
|
-
|
|
332
|
+
|
|
121
333
|
for config_key, env_var in config_keys.items():
|
|
122
334
|
# 1. CLI options (highest priority)
|
|
123
335
|
if config_key in cli_options and cli_options[config_key] is not None:
|
|
@@ -129,10 +341,56 @@ def _resolve_config_hierarchy(
|
|
|
129
341
|
elif env_var and env_var in env_vars:
|
|
130
342
|
resolved[config_key] = env_vars[env_var]
|
|
131
343
|
# 4. Defaults are handled elsewhere
|
|
132
|
-
|
|
344
|
+
|
|
345
|
+
# Issue #237: Pass through 'outputs' config for template-based path generation
|
|
346
|
+
# This enables extensible project layouts (Next.js, Vue, Python, Go, etc.)
|
|
347
|
+
if 'outputs' in context_config:
|
|
348
|
+
resolved['outputs'] = context_config['outputs']
|
|
349
|
+
|
|
133
350
|
return resolved
|
|
134
351
|
|
|
135
352
|
|
|
353
|
+
def get_tests_dir_from_config(start_path: Optional[Path] = None) -> Optional[Path]:
|
|
354
|
+
"""
|
|
355
|
+
Get the tests directory from .pddrc configuration.
|
|
356
|
+
|
|
357
|
+
Searches for .pddrc, detects the appropriate context, and returns the
|
|
358
|
+
configured test_output_path as a Path object.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
start_path: Starting directory for .pddrc search. Defaults to CWD.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Path to tests directory if configured, None otherwise.
|
|
365
|
+
"""
|
|
366
|
+
if start_path is None:
|
|
367
|
+
start_path = Path.cwd()
|
|
368
|
+
|
|
369
|
+
# Find and load .pddrc
|
|
370
|
+
pddrc_path = _find_pddrc_file(start_path)
|
|
371
|
+
if not pddrc_path:
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
config = _load_pddrc_config(pddrc_path)
|
|
376
|
+
except ValueError:
|
|
377
|
+
return None
|
|
378
|
+
|
|
379
|
+
# Detect context and get its config
|
|
380
|
+
context_name = _detect_context(start_path, config)
|
|
381
|
+
context_config = _get_context_config(config, context_name)
|
|
382
|
+
|
|
383
|
+
# Check context config first, then env var
|
|
384
|
+
test_output_path = context_config.get('test_output_path')
|
|
385
|
+
if not test_output_path:
|
|
386
|
+
test_output_path = os.environ.get('PDD_TEST_OUTPUT_PATH')
|
|
387
|
+
|
|
388
|
+
if test_output_path:
|
|
389
|
+
return Path(test_output_path)
|
|
390
|
+
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
|
|
136
394
|
def _read_file(path: Path) -> str:
|
|
137
395
|
"""Read a text file safely and return its contents."""
|
|
138
396
|
try:
|
|
@@ -176,52 +434,41 @@ def _candidate_prompt_path(input_files: Dict[str, Path]) -> Path | None:
|
|
|
176
434
|
for p in input_files.values():
|
|
177
435
|
if p.suffix == ".prompt":
|
|
178
436
|
return p
|
|
437
|
+
|
|
438
|
+
# Final fallback: Return the first file path available (e.g. for pdd update <code_file>)
|
|
439
|
+
if input_files:
|
|
440
|
+
return next(iter(input_files.values()))
|
|
441
|
+
|
|
179
442
|
return None
|
|
180
443
|
|
|
181
444
|
|
|
182
445
|
# New helper function to check if a language is known
|
|
183
446
|
def _is_known_language(language_name: str) -> bool:
|
|
184
|
-
"""
|
|
447
|
+
"""Return True if the language is recognized.
|
|
448
|
+
|
|
449
|
+
Prefer CSV in PDD_PATH if available; otherwise fall back to a built-in set
|
|
450
|
+
so basename/language inference does not fail when PDD_PATH is unset.
|
|
451
|
+
"""
|
|
452
|
+
language_name_lower = (language_name or "").lower()
|
|
453
|
+
if not language_name_lower:
|
|
454
|
+
return False
|
|
455
|
+
|
|
456
|
+
builtin_languages = {
|
|
457
|
+
'python', 'javascript', 'typescript', 'java', 'cpp', 'c', 'go', 'ruby', 'rust',
|
|
458
|
+
'kotlin', 'swift', 'csharp', 'php', 'scala', 'r', 'lua', 'perl', 'bash', 'shell',
|
|
459
|
+
'powershell', 'sql', 'prompt', 'html', 'css', 'makefile',
|
|
460
|
+
# Common data and config formats for architecture prompts and configs
|
|
461
|
+
'json', 'jsonl', 'yaml', 'yml', 'toml', 'ini'
|
|
462
|
+
}
|
|
463
|
+
|
|
185
464
|
pdd_path_str = os.getenv('PDD_PATH')
|
|
186
465
|
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.")
|
|
466
|
+
return language_name_lower in builtin_languages
|
|
216
467
|
|
|
217
468
|
csv_file_path = Path(pdd_path_str) / 'data' / 'language_format.csv'
|
|
218
|
-
|
|
219
469
|
if not csv_file_path.is_file():
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
language_name_lower = language_name.lower()
|
|
224
|
-
|
|
470
|
+
return language_name_lower in builtin_languages
|
|
471
|
+
|
|
225
472
|
try:
|
|
226
473
|
with open(csv_file_path, mode='r', encoding='utf-8', newline='') as csvfile:
|
|
227
474
|
reader = csv.DictReader(csvfile)
|
|
@@ -229,10 +476,10 @@ def _is_known_language(language_name: str) -> bool:
|
|
|
229
476
|
if row.get('language', '').lower() == language_name_lower:
|
|
230
477
|
return True
|
|
231
478
|
except csv.Error as e:
|
|
232
|
-
# Log and return False or raise a custom error
|
|
233
479
|
console.print(f"[error]CSV Error reading {csv_file_path}: {e}", style="error")
|
|
234
|
-
return
|
|
235
|
-
|
|
480
|
+
return language_name_lower in builtin_languages
|
|
481
|
+
|
|
482
|
+
return language_name_lower in builtin_languages
|
|
236
483
|
|
|
237
484
|
|
|
238
485
|
def _strip_language_suffix(path_like: os.PathLike[str]) -> str:
|
|
@@ -264,6 +511,24 @@ def _extract_basename(
|
|
|
264
511
|
"""
|
|
265
512
|
Deduce the project basename according to the rules explained in *Step A*.
|
|
266
513
|
"""
|
|
514
|
+
# Handle 'fix' command specifically to create a unique basename per test file
|
|
515
|
+
if command == "fix":
|
|
516
|
+
prompt_path = _candidate_prompt_path(input_file_paths)
|
|
517
|
+
if not prompt_path:
|
|
518
|
+
raise ValueError("Could not determine prompt file for 'fix' command.")
|
|
519
|
+
|
|
520
|
+
prompt_basename = _strip_language_suffix(prompt_path)
|
|
521
|
+
|
|
522
|
+
unit_test_path = input_file_paths.get("unit_test_file")
|
|
523
|
+
if not unit_test_path:
|
|
524
|
+
# Fallback to just the prompt basename if no unit test file is provided
|
|
525
|
+
# This might happen in some edge cases, but 'fix' command structure requires it
|
|
526
|
+
return prompt_basename
|
|
527
|
+
|
|
528
|
+
# Use the stem of the unit test file to make the basename unique
|
|
529
|
+
test_basename = Path(unit_test_path).stem
|
|
530
|
+
return f"{prompt_basename}_{test_basename}"
|
|
531
|
+
|
|
267
532
|
# Handle conflicts first due to its unique structure
|
|
268
533
|
if command == "conflicts":
|
|
269
534
|
key1 = "prompt1"
|
|
@@ -331,14 +596,48 @@ def _determine_language(
|
|
|
331
596
|
ext = path_obj.suffix
|
|
332
597
|
# Prioritize non-prompt code files
|
|
333
598
|
if ext and ext != ".prompt":
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
599
|
+
try:
|
|
600
|
+
language = get_language(ext)
|
|
601
|
+
if language:
|
|
602
|
+
return language.lower()
|
|
603
|
+
except ValueError:
|
|
604
|
+
# Fallback: load language CSV file directly when PDD_PATH is not set
|
|
605
|
+
try:
|
|
606
|
+
import csv
|
|
607
|
+
import os
|
|
608
|
+
# Try to find the CSV file relative to this script
|
|
609
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
610
|
+
csv_path = os.path.join(script_dir, 'data', 'language_format.csv')
|
|
611
|
+
if os.path.exists(csv_path):
|
|
612
|
+
with open(csv_path, 'r') as csvfile:
|
|
613
|
+
reader = csv.DictReader(csvfile)
|
|
614
|
+
for row in reader:
|
|
615
|
+
if row['extension'].lower() == ext.lower():
|
|
616
|
+
return row['language'].lower()
|
|
617
|
+
except (FileNotFoundError, csv.Error):
|
|
618
|
+
pass
|
|
337
619
|
# Handle files without extension like Makefile
|
|
338
620
|
elif not ext and path_obj.is_file(): # Check it's actually a file
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
621
|
+
try:
|
|
622
|
+
language = get_language(path_obj.name) # Check name (e.g., 'Makefile')
|
|
623
|
+
if language:
|
|
624
|
+
return language.lower()
|
|
625
|
+
except ValueError:
|
|
626
|
+
# Fallback: load language CSV file directly for files without extension
|
|
627
|
+
try:
|
|
628
|
+
import csv
|
|
629
|
+
import os
|
|
630
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
631
|
+
csv_path = os.path.join(script_dir, 'data', 'language_format.csv')
|
|
632
|
+
if os.path.exists(csv_path):
|
|
633
|
+
with open(csv_path, 'r') as csvfile:
|
|
634
|
+
reader = csv.DictReader(csvfile)
|
|
635
|
+
for row in reader:
|
|
636
|
+
# Check if the filename matches (for files without extension)
|
|
637
|
+
if not row['extension'] and path_obj.name.lower() == row['language'].lower():
|
|
638
|
+
return row['language'].lower()
|
|
639
|
+
except (FileNotFoundError, csv.Error):
|
|
640
|
+
pass
|
|
342
641
|
|
|
343
642
|
# 3 – parse from prompt filename suffix
|
|
344
643
|
prompt_path = _candidate_prompt_path(input_file_paths)
|
|
@@ -354,7 +653,7 @@ def _determine_language(
|
|
|
354
653
|
|
|
355
654
|
# 4 - Special handling for detect command - default to prompt for LLM prompts
|
|
356
655
|
if command == "detect" and "change_file" in input_file_paths:
|
|
357
|
-
return "prompt"
|
|
656
|
+
return "prompt"
|
|
358
657
|
|
|
359
658
|
# 5 - If no language determined, raise error
|
|
360
659
|
raise ValueError("Could not determine language from input files or options.")
|
|
@@ -374,6 +673,8 @@ def construct_paths(
|
|
|
374
673
|
command_options: Optional[Dict[str, Any]], # Allow None
|
|
375
674
|
create_error_file: bool = True, # Added parameter to control error file creation
|
|
376
675
|
context_override: Optional[str] = None, # Added parameter for context override
|
|
676
|
+
confirm_callback: Optional[Callable[[str, str], bool]] = None, # Callback for interactive confirmation
|
|
677
|
+
path_resolution_mode: Optional[str] = None, # "cwd" or "config_base" - if None, use command default
|
|
377
678
|
) -> Tuple[Dict[str, Any], Dict[str, str], Dict[str, str], str]:
|
|
378
679
|
"""
|
|
379
680
|
High‑level orchestrator that loads inputs, determines basename/language,
|
|
@@ -390,6 +691,7 @@ def construct_paths(
|
|
|
390
691
|
|
|
391
692
|
# ------------- Load .pddrc configuration -----------------
|
|
392
693
|
pddrc_config = {}
|
|
694
|
+
pddrc_path: Optional[Path] = None
|
|
393
695
|
context = None
|
|
394
696
|
context_config = {}
|
|
395
697
|
original_context_config = {} # Keep track of original context config for sync discovery
|
|
@@ -401,9 +703,30 @@ def construct_paths(
|
|
|
401
703
|
pddrc_config = _load_pddrc_config(pddrc_path)
|
|
402
704
|
|
|
403
705
|
# Detect appropriate context
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
706
|
+
# Priority: context_override > file-based detection > CWD-based detection
|
|
707
|
+
if context_override:
|
|
708
|
+
# Delegate validation to _detect_context to avoid duplicate validation logic
|
|
709
|
+
context = _detect_context(Path.cwd(), pddrc_config, context_override)
|
|
710
|
+
else:
|
|
711
|
+
# Try file-based detection when prompt file is provided
|
|
712
|
+
prompt_file_str = input_file_paths.get('prompt_file') if input_file_paths else None
|
|
713
|
+
if prompt_file_str and Path(prompt_file_str).exists():
|
|
714
|
+
detected_context, _ = detect_context_for_file(prompt_file_str)
|
|
715
|
+
if detected_context:
|
|
716
|
+
context = detected_context
|
|
717
|
+
else:
|
|
718
|
+
context = _detect_context(Path.cwd(), pddrc_config, None)
|
|
719
|
+
else:
|
|
720
|
+
basename_hint = command_options.get("basename")
|
|
721
|
+
if basename_hint:
|
|
722
|
+
detected_context = _detect_context_from_basename(basename_hint, pddrc_config)
|
|
723
|
+
if detected_context:
|
|
724
|
+
context = detected_context
|
|
725
|
+
else:
|
|
726
|
+
context = _detect_context(Path.cwd(), pddrc_config, None)
|
|
727
|
+
else:
|
|
728
|
+
context = _detect_context(Path.cwd(), pddrc_config, None)
|
|
729
|
+
|
|
407
730
|
# Get context-specific configuration
|
|
408
731
|
context_config = _get_context_config(pddrc_config, context)
|
|
409
732
|
original_context_config = context_config.copy() # Store original before modifications
|
|
@@ -414,9 +737,15 @@ def construct_paths(
|
|
|
414
737
|
# Apply configuration hierarchy
|
|
415
738
|
env_vars = dict(os.environ)
|
|
416
739
|
resolved_config = _resolve_config_hierarchy(command_options, context_config, env_vars)
|
|
417
|
-
|
|
740
|
+
|
|
741
|
+
# Issue #237: Track matched context for debugging
|
|
742
|
+
resolved_config['_matched_context'] = context or 'default'
|
|
743
|
+
|
|
418
744
|
# Update command_options with resolved configuration for internal use
|
|
745
|
+
# Exclude internal metadata keys (prefixed with _) from command_options
|
|
419
746
|
for key, value in resolved_config.items():
|
|
747
|
+
if key.startswith('_'):
|
|
748
|
+
continue # Skip internal metadata like _matched_context
|
|
420
749
|
if key not in command_options or command_options[key] is None:
|
|
421
750
|
command_options[key] = value
|
|
422
751
|
|
|
@@ -450,7 +779,10 @@ def construct_paths(
|
|
|
450
779
|
language="python", # Dummy language
|
|
451
780
|
file_extension=".py", # Dummy extension
|
|
452
781
|
context_config=context_config,
|
|
782
|
+
config_base_dir=str(pddrc_path.parent) if pddrc_path else None,
|
|
783
|
+
path_resolution_mode="cwd", # Sync resolves paths relative to CWD
|
|
453
784
|
)
|
|
785
|
+
|
|
454
786
|
# Infer base directories from a sample output path
|
|
455
787
|
gen_path = Path(output_paths_str.get("generate_output_path", "src"))
|
|
456
788
|
|
|
@@ -466,10 +798,13 @@ def construct_paths(
|
|
|
466
798
|
else:
|
|
467
799
|
# Fall back to context-aware logic
|
|
468
800
|
# Use original_context_config to avoid checking augmented config with env vars
|
|
469
|
-
if original_context_config and
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
801
|
+
if original_context_config and (
|
|
802
|
+
'prompts_dir' in original_context_config or
|
|
803
|
+
any(key.endswith('_output_path') for key in original_context_config)
|
|
804
|
+
):
|
|
805
|
+
# For configured contexts, use prompts_dir from config if provided,
|
|
806
|
+
# otherwise default to "prompts" at the same level as output dirs
|
|
807
|
+
resolved_config["prompts_dir"] = original_context_config.get("prompts_dir", "prompts")
|
|
473
808
|
resolved_config["code_dir"] = str(gen_path.parent)
|
|
474
809
|
else:
|
|
475
810
|
# For default contexts, maintain relative relationship
|
|
@@ -478,7 +813,27 @@ def construct_paths(
|
|
|
478
813
|
resolved_config["code_dir"] = str(gen_path.parent)
|
|
479
814
|
|
|
480
815
|
resolved_config["tests_dir"] = str(Path(output_paths_str.get("test_output_path", "tests")).parent)
|
|
481
|
-
|
|
816
|
+
|
|
817
|
+
# Determine examples_dir for auto-deps scanning
|
|
818
|
+
# NOTE: outputs.example.path is for OUTPUT only (where to write examples),
|
|
819
|
+
# NOT for determining scan scope. Using it caused CSV row deletion issues.
|
|
820
|
+
# Check RAW context config for example_output_path, or default to "context".
|
|
821
|
+
# Do NOT use output_paths_str since generate_output_paths always returns absolute paths.
|
|
822
|
+
example_path_str = None
|
|
823
|
+
if original_context_config:
|
|
824
|
+
example_path_str = original_context_config.get("example_output_path")
|
|
825
|
+
|
|
826
|
+
# Final fallback to "context" (sensible default for this project)
|
|
827
|
+
if not example_path_str:
|
|
828
|
+
example_path_str = "context"
|
|
829
|
+
|
|
830
|
+
# example_path_str can be a directory (e.g., "context/") or a file path (e.g., "examples/foo.py")
|
|
831
|
+
# If it ends with / or has no file extension, treat as directory; otherwise use parent
|
|
832
|
+
example_path = Path(example_path_str)
|
|
833
|
+
if example_path_str.endswith('/') or '.' not in example_path.name:
|
|
834
|
+
resolved_config["examples_dir"] = example_path_str.rstrip('/')
|
|
835
|
+
else:
|
|
836
|
+
resolved_config["examples_dir"] = str(example_path.parent)
|
|
482
837
|
|
|
483
838
|
except Exception as e:
|
|
484
839
|
console.print(f"[error]Failed to determine initial paths for sync: {e}", style="error")
|
|
@@ -497,11 +852,15 @@ def construct_paths(
|
|
|
497
852
|
for key, path_str in input_file_paths.items():
|
|
498
853
|
try:
|
|
499
854
|
path = Path(path_str).expanduser()
|
|
500
|
-
# Resolve non-error files strictly first
|
|
855
|
+
# Resolve non-error files strictly first, but be more lenient for sync command
|
|
501
856
|
if key != "error_file":
|
|
502
|
-
#
|
|
503
|
-
|
|
504
|
-
|
|
857
|
+
# For sync command, be more tolerant of non-existent files since we're just determining paths
|
|
858
|
+
if command == "sync":
|
|
859
|
+
input_paths[key] = path.resolve()
|
|
860
|
+
else:
|
|
861
|
+
# Let FileNotFoundError propagate naturally if path doesn't exist
|
|
862
|
+
resolved_path = path.resolve(strict=True)
|
|
863
|
+
input_paths[key] = resolved_path
|
|
505
864
|
else:
|
|
506
865
|
# Resolve error file non-strictly, existence checked later
|
|
507
866
|
input_paths[key] = path.resolve()
|
|
@@ -531,9 +890,14 @@ def construct_paths(
|
|
|
531
890
|
|
|
532
891
|
# Check existence again, especially for error_file which might have been created
|
|
533
892
|
if not path.exists():
|
|
534
|
-
#
|
|
535
|
-
|
|
536
|
-
|
|
893
|
+
# For sync command, be more tolerant of non-existent files since we're just determining paths
|
|
894
|
+
if command == "sync":
|
|
895
|
+
# Skip reading content for non-existent files in sync mode
|
|
896
|
+
continue
|
|
897
|
+
else:
|
|
898
|
+
# This case should ideally be caught by resolve(strict=True) earlier for non-error files
|
|
899
|
+
# Raise standard FileNotFoundError
|
|
900
|
+
raise FileNotFoundError(f"{path}")
|
|
537
901
|
|
|
538
902
|
if path.is_file(): # Read only if it's a file
|
|
539
903
|
try:
|
|
@@ -552,7 +916,13 @@ def construct_paths(
|
|
|
552
916
|
|
|
553
917
|
# ------------- Step 2: basename --------------------------
|
|
554
918
|
try:
|
|
555
|
-
|
|
919
|
+
# For sync, example, and test commands, prefer the basename from command_options if provided.
|
|
920
|
+
# This preserves subdirectory paths like 'core/cloud' which would otherwise
|
|
921
|
+
# be lost when extracting from the prompt file path.
|
|
922
|
+
if command in ("sync", "example", "test") and command_options.get("basename"):
|
|
923
|
+
basename = command_options["basename"]
|
|
924
|
+
else:
|
|
925
|
+
basename = _extract_basename(command, input_paths)
|
|
556
926
|
except ValueError as exc:
|
|
557
927
|
# Check if it's the specific error from the initial check (now done at start)
|
|
558
928
|
# This try/except might not be needed if initial check is robust
|
|
@@ -598,7 +968,25 @@ def construct_paths(
|
|
|
598
968
|
style="warning"
|
|
599
969
|
)
|
|
600
970
|
|
|
601
|
-
|
|
971
|
+
|
|
972
|
+
# Try to get extension from CSV; fallback to built-in mapping if PDD_PATH/CSV unavailable
|
|
973
|
+
try:
|
|
974
|
+
file_extension = get_extension(language) # Pass determined language
|
|
975
|
+
if not file_extension and (language or '').lower() != 'prompt':
|
|
976
|
+
raise ValueError('empty extension')
|
|
977
|
+
except Exception:
|
|
978
|
+
builtin_ext_map = {
|
|
979
|
+
'python': '.py', 'javascript': '.js', 'typescript': '.ts', 'java': '.java',
|
|
980
|
+
'cpp': '.cpp', 'c': '.c', 'go': '.go', 'ruby': '.rb', 'rust': '.rs',
|
|
981
|
+
'kotlin': '.kt', 'swift': '.swift', 'csharp': '.cs', 'php': '.php',
|
|
982
|
+
'scala': '.scala', 'r': '.r', 'lua': '.lua', 'perl': '.pl', 'bash': '.sh',
|
|
983
|
+
'shell': '.sh', 'powershell': '.ps1', 'sql': '.sql', 'html': '.html', 'css': '.css',
|
|
984
|
+
'prompt': '.prompt', 'makefile': '',
|
|
985
|
+
# Common data/config formats
|
|
986
|
+
'json': '.json', 'jsonl': '.jsonl', 'yaml': '.yaml', 'yml': '.yml', 'toml': '.toml', 'ini': '.ini'
|
|
987
|
+
}
|
|
988
|
+
file_extension = builtin_ext_map.get(language.lower(), f".{language.lower()}" if language else '')
|
|
989
|
+
|
|
602
990
|
|
|
603
991
|
|
|
604
992
|
# ------------- Step 3b: build output paths ---------------
|
|
@@ -608,10 +996,52 @@ def construct_paths(
|
|
|
608
996
|
if k.startswith("output") and v is not None # Ensure value is not None
|
|
609
997
|
}
|
|
610
998
|
|
|
999
|
+
# Determine input file directory for default output path generation
|
|
1000
|
+
# Only apply for commands that generate/update files based on specific input files
|
|
1001
|
+
# Commands like sync, generate, test, example have their own directory management
|
|
1002
|
+
commands_using_input_dir = {'fix', 'crash', 'verify', 'split', 'change', 'update'}
|
|
1003
|
+
input_file_dir: Optional[str] = None
|
|
1004
|
+
input_file_dirs: Dict[str, Optional[str]] = {}
|
|
1005
|
+
if input_paths and command in commands_using_input_dir:
|
|
1006
|
+
try:
|
|
1007
|
+
# For fix/crash/verify commands, use specific file directories for each output
|
|
1008
|
+
if command in {'fix', 'crash', 'verify'}:
|
|
1009
|
+
# Map output keys to their corresponding input file keys
|
|
1010
|
+
input_key_map = {
|
|
1011
|
+
'fix': {'output_code': 'code_file', 'output_test': 'unit_test_file', 'output_results': 'code_file'},
|
|
1012
|
+
'crash': {'output': 'code_file', 'output_program': 'program_file'},
|
|
1013
|
+
'verify': {'output_code': 'code_file', 'output_program': 'verification_program', 'output_results': 'code_file'},
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
for output_key, input_key in input_key_map.get(command, {}).items():
|
|
1017
|
+
if input_key in input_paths:
|
|
1018
|
+
input_file_dirs[output_key] = str(input_paths[input_key].parent)
|
|
1019
|
+
|
|
1020
|
+
# Set default input_file_dir to code_file directory as fallback
|
|
1021
|
+
if 'code_file' in input_paths:
|
|
1022
|
+
input_file_dir = str(input_paths['code_file'].parent)
|
|
1023
|
+
else:
|
|
1024
|
+
first_input_path = next(iter(input_paths.values()))
|
|
1025
|
+
input_file_dir = str(first_input_path.parent)
|
|
1026
|
+
else:
|
|
1027
|
+
# For other commands, use first input path
|
|
1028
|
+
first_input_path = next(iter(input_paths.values()))
|
|
1029
|
+
input_file_dir = str(first_input_path.parent)
|
|
1030
|
+
except (StopIteration, AttributeError):
|
|
1031
|
+
# If no input paths or path doesn't have parent, use None (falls back to CWD)
|
|
1032
|
+
pass
|
|
1033
|
+
|
|
611
1034
|
try:
|
|
612
1035
|
# generate_output_paths might return Dict[str, str] or Dict[str, Path]
|
|
613
1036
|
# Let's assume it returns Dict[str, str] based on verification error,
|
|
614
1037
|
# and convert them to Path objects here.
|
|
1038
|
+
# Determine path resolution mode:
|
|
1039
|
+
# - If explicitly provided, use it
|
|
1040
|
+
# - Otherwise: sync uses "cwd", other commands use "config_base"
|
|
1041
|
+
effective_path_resolution_mode = path_resolution_mode
|
|
1042
|
+
if effective_path_resolution_mode is None:
|
|
1043
|
+
effective_path_resolution_mode = "cwd" if command == "sync" else "config_base"
|
|
1044
|
+
|
|
615
1045
|
output_paths_str: Dict[str, str] = generate_output_paths(
|
|
616
1046
|
command=command,
|
|
617
1047
|
output_locations=output_location_opts,
|
|
@@ -619,7 +1049,12 @@ def construct_paths(
|
|
|
619
1049
|
language=language,
|
|
620
1050
|
file_extension=file_extension,
|
|
621
1051
|
context_config=context_config,
|
|
1052
|
+
input_file_dir=input_file_dir,
|
|
1053
|
+
input_file_dirs=input_file_dirs,
|
|
1054
|
+
config_base_dir=str(pddrc_path.parent) if pddrc_path else None,
|
|
1055
|
+
path_resolution_mode=effective_path_resolution_mode,
|
|
622
1056
|
)
|
|
1057
|
+
|
|
623
1058
|
# Convert to Path objects for internal use
|
|
624
1059
|
output_paths_resolved: Dict[str, Path] = {k: Path(v) for k, v in output_paths_str.items()}
|
|
625
1060
|
|
|
@@ -628,36 +1063,59 @@ def construct_paths(
|
|
|
628
1063
|
raise # Re-raise the ValueError
|
|
629
1064
|
|
|
630
1065
|
# ------------- Step 4: overwrite confirmation ------------
|
|
631
|
-
#
|
|
1066
|
+
# Initialize existing_files before the conditional to avoid UnboundLocalError
|
|
632
1067
|
existing_files: Dict[str, Path] = {}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1068
|
+
|
|
1069
|
+
if command in ["test", "bug"] and not force:
|
|
1070
|
+
# For test/bug commands without --force, create numbered files instead of overwriting
|
|
1071
|
+
for key, path in output_paths_resolved.items():
|
|
1072
|
+
if path.is_file():
|
|
1073
|
+
base, ext = os.path.splitext(path)
|
|
1074
|
+
i = 1
|
|
1075
|
+
new_path = Path(f"{base}_{i}{ext}")
|
|
1076
|
+
while new_path.exists():
|
|
1077
|
+
i += 1
|
|
1078
|
+
new_path = Path(f"{base}_{i}{ext}")
|
|
1079
|
+
output_paths_resolved[key] = new_path
|
|
1080
|
+
else:
|
|
1081
|
+
# Check if any output *file* exists (operate on Path objects)
|
|
1082
|
+
for k, p_obj in output_paths_resolved.items():
|
|
1083
|
+
if p_obj.is_file():
|
|
1084
|
+
existing_files[k] = p_obj # Store the Path object
|
|
637
1085
|
|
|
638
1086
|
if existing_files and not force:
|
|
1087
|
+
paths_list = "\n".join(f" • {p.resolve()}" for p in existing_files.values())
|
|
639
1088
|
if not quiet:
|
|
640
1089
|
# Use the Path objects stored in existing_files for resolve()
|
|
641
1090
|
# Print without Rich tags for easier testing
|
|
642
|
-
paths_list = "\n".join(f" • {p.resolve()}" for p in existing_files.values())
|
|
643
1091
|
console.print(
|
|
644
1092
|
f"Warning: The following output files already exist and may be overwritten:\n{paths_list}",
|
|
645
1093
|
style="warning"
|
|
646
1094
|
)
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
click.
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
1095
|
+
|
|
1096
|
+
# Use confirm_callback if provided (for TUI environments), otherwise use click.confirm
|
|
1097
|
+
if confirm_callback is not None:
|
|
1098
|
+
# Use the provided callback for confirmation (e.g., from Textual TUI)
|
|
1099
|
+
confirm_message = f"The following files will be overwritten:\n{paths_list}\n\nOverwrite existing files?"
|
|
1100
|
+
if not confirm_callback(confirm_message, "Overwrite Confirmation"):
|
|
1101
|
+
raise click.Abort()
|
|
1102
|
+
else:
|
|
1103
|
+
# Use click.confirm for CLI interaction
|
|
1104
|
+
try:
|
|
1105
|
+
if not click.confirm(
|
|
1106
|
+
click.style("Overwrite existing files?", fg="yellow"), default=True, show_default=True
|
|
1107
|
+
):
|
|
1108
|
+
click.secho("Operation cancelled.", fg="red", err=True)
|
|
1109
|
+
raise click.Abort()
|
|
1110
|
+
except click.Abort:
|
|
1111
|
+
raise # Let Abort propagate to be handled by PDDCLI.invoke()
|
|
1112
|
+
except Exception as e: # Catch potential errors during confirm (like EOFError in non-interactive)
|
|
1113
|
+
if 'EOF' in str(e) or 'end-of-file' in str(e).lower():
|
|
1114
|
+
# Non-interactive environment, default to not overwriting
|
|
1115
|
+
click.secho("Non-interactive environment detected. Use --force to overwrite existing files.", fg="yellow", err=True)
|
|
1116
|
+
else:
|
|
1117
|
+
click.secho(f"Confirmation failed: {e}. Aborting.", fg="red", err=True)
|
|
1118
|
+
raise click.Abort()
|
|
661
1119
|
|
|
662
1120
|
|
|
663
1121
|
# ------------- Final reporting ---------------------------
|
|
@@ -685,7 +1143,27 @@ def construct_paths(
|
|
|
685
1143
|
resolved_config["prompts_dir"] = str(next(iter(input_paths.values())).parent)
|
|
686
1144
|
resolved_config["code_dir"] = str(gen_path.parent)
|
|
687
1145
|
resolved_config["tests_dir"] = str(Path(resolved_config.get("test_output_path", "tests")).parent)
|
|
688
|
-
resolved_config["examples_dir"] = str(Path(resolved_config.get("example_output_path", "examples")).parent)
|
|
689
|
-
|
|
690
1146
|
|
|
691
|
-
|
|
1147
|
+
# Determine examples_dir for auto-deps scanning
|
|
1148
|
+
# NOTE: outputs.example.path is for OUTPUT only (where to write examples),
|
|
1149
|
+
# NOT for determining scan scope. Using it caused CSV row deletion issues.
|
|
1150
|
+
# Check RAW context config for example_output_path, or default to "context".
|
|
1151
|
+
# Do NOT use resolved_config since generate_output_paths sets it to absolute paths.
|
|
1152
|
+
example_path_str = None
|
|
1153
|
+
if original_context_config:
|
|
1154
|
+
example_path_str = original_context_config.get("example_output_path")
|
|
1155
|
+
|
|
1156
|
+
# Final fallback to "context" (sensible default for this project)
|
|
1157
|
+
if not example_path_str:
|
|
1158
|
+
example_path_str = "context"
|
|
1159
|
+
|
|
1160
|
+
# example_path_str can be a directory (e.g., "context/") or a file path (e.g., "examples/foo.py")
|
|
1161
|
+
# If it ends with / or has no file extension, treat as directory; otherwise use parent
|
|
1162
|
+
example_path = Path(example_path_str)
|
|
1163
|
+
if example_path_str.endswith('/') or '.' not in example_path.name:
|
|
1164
|
+
resolved_config["examples_dir"] = example_path_str.rstrip('/')
|
|
1165
|
+
else:
|
|
1166
|
+
resolved_config["examples_dir"] = str(example_path.parent)
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
return resolved_config, input_strings, output_file_paths_str_return, language
|