pdd-cli 0.0.45__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.
Files changed (114) hide show
  1. pdd/__init__.py +4 -4
  2. pdd/agentic_common.py +863 -0
  3. pdd/agentic_crash.py +534 -0
  4. pdd/agentic_fix.py +1179 -0
  5. pdd/agentic_langtest.py +162 -0
  6. pdd/agentic_update.py +370 -0
  7. pdd/agentic_verify.py +183 -0
  8. pdd/auto_deps_main.py +15 -5
  9. pdd/auto_include.py +63 -5
  10. pdd/bug_main.py +3 -2
  11. pdd/bug_to_unit_test.py +2 -0
  12. pdd/change_main.py +11 -4
  13. pdd/cli.py +22 -1181
  14. pdd/cmd_test_main.py +73 -21
  15. pdd/code_generator.py +58 -18
  16. pdd/code_generator_main.py +672 -25
  17. pdd/commands/__init__.py +42 -0
  18. pdd/commands/analysis.py +248 -0
  19. pdd/commands/fix.py +140 -0
  20. pdd/commands/generate.py +257 -0
  21. pdd/commands/maintenance.py +174 -0
  22. pdd/commands/misc.py +79 -0
  23. pdd/commands/modify.py +230 -0
  24. pdd/commands/report.py +144 -0
  25. pdd/commands/templates.py +215 -0
  26. pdd/commands/utility.py +110 -0
  27. pdd/config_resolution.py +58 -0
  28. pdd/conflicts_main.py +8 -3
  29. pdd/construct_paths.py +258 -82
  30. pdd/context_generator.py +10 -2
  31. pdd/context_generator_main.py +113 -11
  32. pdd/continue_generation.py +47 -7
  33. pdd/core/__init__.py +0 -0
  34. pdd/core/cli.py +503 -0
  35. pdd/core/dump.py +554 -0
  36. pdd/core/errors.py +63 -0
  37. pdd/core/utils.py +90 -0
  38. pdd/crash_main.py +44 -11
  39. pdd/data/language_format.csv +71 -63
  40. pdd/data/llm_model.csv +20 -18
  41. pdd/detect_change_main.py +5 -4
  42. pdd/fix_code_loop.py +330 -76
  43. pdd/fix_error_loop.py +207 -61
  44. pdd/fix_errors_from_unit_tests.py +4 -3
  45. pdd/fix_main.py +75 -18
  46. pdd/fix_verification_errors.py +12 -100
  47. pdd/fix_verification_errors_loop.py +306 -272
  48. pdd/fix_verification_main.py +28 -9
  49. pdd/generate_output_paths.py +93 -10
  50. pdd/generate_test.py +16 -5
  51. pdd/get_jwt_token.py +9 -2
  52. pdd/get_run_command.py +73 -0
  53. pdd/get_test_command.py +68 -0
  54. pdd/git_update.py +70 -19
  55. pdd/incremental_code_generator.py +2 -2
  56. pdd/insert_includes.py +11 -3
  57. pdd/llm_invoke.py +1269 -103
  58. pdd/load_prompt_template.py +36 -10
  59. pdd/pdd_completion.fish +25 -2
  60. pdd/pdd_completion.sh +30 -4
  61. pdd/pdd_completion.zsh +79 -4
  62. pdd/postprocess.py +10 -3
  63. pdd/preprocess.py +228 -15
  64. pdd/preprocess_main.py +8 -5
  65. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  66. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  67. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  68. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  69. pdd/prompts/agentic_update_LLM.prompt +1071 -0
  70. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  71. pdd/prompts/auto_include_LLM.prompt +100 -905
  72. pdd/prompts/detect_change_LLM.prompt +122 -20
  73. pdd/prompts/example_generator_LLM.prompt +22 -1
  74. pdd/prompts/extract_code_LLM.prompt +5 -1
  75. pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
  76. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  77. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  78. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  79. pdd/prompts/fix_code_module_errors_LLM.prompt +4 -2
  80. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +8 -0
  81. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  82. pdd/prompts/generate_test_LLM.prompt +21 -6
  83. pdd/prompts/increase_tests_LLM.prompt +1 -5
  84. pdd/prompts/insert_includes_LLM.prompt +228 -108
  85. pdd/prompts/trace_LLM.prompt +25 -22
  86. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  87. pdd/prompts/update_prompt_LLM.prompt +22 -1
  88. pdd/pytest_output.py +127 -12
  89. pdd/render_mermaid.py +236 -0
  90. pdd/setup_tool.py +648 -0
  91. pdd/simple_math.py +2 -0
  92. pdd/split_main.py +3 -2
  93. pdd/summarize_directory.py +49 -6
  94. pdd/sync_determine_operation.py +543 -98
  95. pdd/sync_main.py +81 -31
  96. pdd/sync_orchestration.py +1334 -751
  97. pdd/sync_tui.py +848 -0
  98. pdd/template_registry.py +264 -0
  99. pdd/templates/architecture/architecture_json.prompt +242 -0
  100. pdd/templates/generic/generate_prompt.prompt +174 -0
  101. pdd/trace.py +168 -12
  102. pdd/trace_main.py +4 -3
  103. pdd/track_cost.py +151 -61
  104. pdd/unfinished_prompt.py +49 -3
  105. pdd/update_main.py +549 -67
  106. pdd/update_model_costs.py +2 -2
  107. pdd/update_prompt.py +19 -4
  108. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +19 -6
  109. pdd_cli-0.0.90.dist-info/RECORD +153 -0
  110. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
  111. pdd_cli-0.0.45.dist-info/RECORD +0 -116
  112. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
  113. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
  114. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/top_level.txt +0 -0
pdd/setup_tool.py ADDED
@@ -0,0 +1,648 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PDD Setup Script - Post-install configuration tool for PDD (Prompt Driven Development)
4
+ Helps new users bootstrap their PDD configuration with LLM API keys and basic settings.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import subprocess
10
+ import json
11
+ import requests
12
+ import csv
13
+ import importlib.resources
14
+ import shlex
15
+ from pathlib import Path
16
+ from typing import Dict, Optional, Tuple, List
17
+
18
+ # Global variables for non-ASCII characters and colors
19
+ HEAVY_HORIZONTAL = "━"
20
+ LIGHT_HORIZONTAL = "─"
21
+ HEAVY_VERTICAL = "┃"
22
+ LIGHT_VERTICAL = "│"
23
+ TOP_LEFT_CORNER = "┏"
24
+ TOP_RIGHT_CORNER = "┓"
25
+ BOTTOM_LEFT_CORNER = "┗"
26
+ BOTTOM_RIGHT_CORNER = "┛"
27
+ CROSS = "┼"
28
+ TEE_DOWN = "┬"
29
+ TEE_UP = "┴"
30
+ TEE_RIGHT = "├"
31
+ TEE_LEFT = "┤"
32
+ BULLET = "•"
33
+ ARROW_RIGHT = "→"
34
+ CHECK_MARK = "✓"
35
+ CROSS_MARK = "✗"
36
+
37
+ # Color codes
38
+ RESET = "\033[0m"
39
+ WHITE = "\033[97m"
40
+ CYAN = "\033[96m"
41
+ YELLOW = "\033[93m"
42
+ BOLD = "\033[1m"
43
+
44
+ # Template content inline
45
+ SUCCESS_PYTHON_TEMPLATE = """
46
+ Write a python script to print "You did it, <Username>!!!" to the console.
47
+ Do not write anything except that message.
48
+ Capitalize the username."""
49
+
50
+ def _read_packaged_llm_model_csv() -> Tuple[List[str], List[Dict[str, str]]]:
51
+ """Load the packaged CSV (pdd/data/llm_model.csv) and return header + rows.
52
+
53
+ Returns:
54
+ (header_fields, rows) where header_fields is the list of column names
55
+ and rows is a list of dictionaries for each CSV row.
56
+ """
57
+ try:
58
+ csv_text = importlib.resources.files('pdd').joinpath('data/llm_model.csv').read_text()
59
+ except Exception as e:
60
+ raise FileNotFoundError(f"Failed to load default LLM model CSV from package: {e}")
61
+
62
+ reader = csv.DictReader(csv_text.splitlines())
63
+ header = reader.fieldnames or []
64
+ rows = [row for row in reader]
65
+ return header, rows
66
+
67
+ def print_colored(text: str, color: str = WHITE, bold: bool = False) -> None:
68
+ """Print colored text to console"""
69
+ style = BOLD + color if bold else color
70
+ print(f"{style}{text}{RESET}")
71
+
72
+ def create_divider(char: str = LIGHT_HORIZONTAL, width: int = 80) -> str:
73
+ """Create a horizontal divider line"""
74
+ return char * width
75
+
76
+ def create_fat_divider(width: int = 80) -> str:
77
+ """Create a fat horizontal divider line"""
78
+ return HEAVY_HORIZONTAL * width
79
+
80
+ def print_pdd_logo():
81
+ """Print the PDD logo in ASCII art"""
82
+ logo = "\n".join(
83
+ [
84
+ " +xxxxxxxxxxxxxxx+",
85
+ "xxxxxxxxxxxxxxxxxxxxx+",
86
+ "xxx +xx+ PROMPT",
87
+ "xxx x+ xx+ DRIVEN",
88
+ "xxx x+ xxx DEVELOPMENT©",
89
+ "xxx x+ xx+",
90
+ "xxx x+ xx+ COMMAND LINE INTERFACE",
91
+ "xxx x+ xxx",
92
+ "xxx +xx+ ",
93
+ "xxx +xxxxxxxxxxx+",
94
+ "xxx +xx+",
95
+ "xxx +xx+",
96
+ "xxx+xx+ WWW.PROMPTDRIVEN.AI",
97
+ "xxxx+",
98
+ "xx+",
99
+ ]
100
+ )
101
+ print(f"{CYAN}{logo}{RESET}")
102
+ print()
103
+ print_colored("Let's get set up quickly with a solid basic configuration!", WHITE, bold=True)
104
+ print()
105
+ print_colored("Supported: OpenAI, Google Gemini, and Anthropic Claude", WHITE)
106
+ print_colored("from their respective API endpoints (no third-parties, such as Azure)", WHITE)
107
+ print()
108
+
109
+ def get_csv_variable_names() -> Dict[str, str]:
110
+ """Inspect packaged CSV to determine API key variable names per provider.
111
+
112
+ Focus on direct providers only: OpenAI GPT models (model startswith 'gpt-'),
113
+ Google Gemini (model startswith 'gemini/'), and Anthropic (model startswith 'anthropic/').
114
+ """
115
+ header, rows = _read_packaged_llm_model_csv()
116
+ variable_names: Dict[str, str] = {}
117
+
118
+ for row in rows:
119
+ model = (row.get('model') or '').strip()
120
+ api_key = (row.get('api_key') or '').strip()
121
+ provider = (row.get('provider') or '').strip().upper()
122
+
123
+ if not api_key:
124
+ continue
125
+
126
+ if model.startswith('gpt-') and provider == 'OPENAI':
127
+ variable_names['OPENAI'] = api_key
128
+ elif model.startswith('gemini/') and provider == 'GOOGLE':
129
+ # Prefer direct Gemini key, not Vertex
130
+ variable_names['GOOGLE'] = api_key
131
+ elif model.startswith('anthropic/') and provider == 'ANTHROPIC':
132
+ variable_names['ANTHROPIC'] = api_key
133
+
134
+ # Fallbacks if not detected (keep prior behavior)
135
+ variable_names.setdefault('OPENAI', 'OPENAI_API_KEY')
136
+ # Prefer GEMINI_API_KEY name for Google if present
137
+ variable_names.setdefault('GOOGLE', 'GEMINI_API_KEY')
138
+ variable_names.setdefault('ANTHROPIC', 'ANTHROPIC_API_KEY')
139
+ return variable_names
140
+
141
+ def discover_api_keys() -> Dict[str, Optional[str]]:
142
+ """Discover API keys from environment variables"""
143
+ # Get the variable names actually used in CSV template
144
+ csv_vars = get_csv_variable_names()
145
+
146
+ keys = {
147
+ 'OPENAI_API_KEY': os.getenv('OPENAI_API_KEY'),
148
+ 'ANTHROPIC_API_KEY': os.getenv('ANTHROPIC_API_KEY'),
149
+ }
150
+
151
+ # For Google, check both possible environment variables but use CSV template's variable name
152
+ google_var_name = csv_vars.get('GOOGLE', 'GEMINI_API_KEY') # Default to GEMINI_API_KEY
153
+ google_api_key = os.getenv('GEMINI_API_KEY') or os.getenv('GOOGLE_API_KEY')
154
+ keys[google_var_name] = google_api_key
155
+
156
+ return keys
157
+
158
+ def test_openai_key(api_key: str) -> bool:
159
+ """Test OpenAI API key validity"""
160
+ if not api_key or not api_key.strip():
161
+ return False
162
+
163
+ try:
164
+ headers = {
165
+ 'Authorization': f'Bearer {api_key.strip()}',
166
+ 'Content-Type': 'application/json'
167
+ }
168
+ response = requests.get(
169
+ 'https://api.openai.com/v1/models',
170
+ headers=headers,
171
+ timeout=10
172
+ )
173
+ return response.status_code == 200
174
+ except Exception:
175
+ return False
176
+
177
+ def test_google_key(api_key: str) -> bool:
178
+ """Test Google Gemini API key validity"""
179
+ if not api_key or not api_key.strip():
180
+ return False
181
+
182
+ try:
183
+ response = requests.get(
184
+ f'https://generativelanguage.googleapis.com/v1beta/models?key={api_key.strip()}',
185
+ timeout=10
186
+ )
187
+ return response.status_code == 200
188
+ except Exception:
189
+ return False
190
+
191
+ def test_anthropic_key(api_key: str) -> bool:
192
+ """Test Anthropic API key validity"""
193
+ if not api_key or not api_key.strip():
194
+ return False
195
+
196
+ try:
197
+ headers = {
198
+ 'x-api-key': api_key.strip(),
199
+ 'Content-Type': 'application/json'
200
+ }
201
+ response = requests.get(
202
+ 'https://api.anthropic.com/v1/messages',
203
+ headers=headers,
204
+ timeout=10
205
+ )
206
+ # Anthropic returns 400 for invalid request structure but 401/403 for bad keys
207
+ return response.status_code != 401 and response.status_code != 403
208
+ except Exception:
209
+ return False
210
+
211
+ def test_api_keys(keys: Dict[str, Optional[str]]) -> Dict[str, bool]:
212
+ """Test all discovered API keys"""
213
+ results = {}
214
+
215
+ print_colored(f"\n{LIGHT_HORIZONTAL * 40}", CYAN)
216
+ print_colored("Testing discovered API keys...", CYAN, bold=True)
217
+ print_colored(f"{LIGHT_HORIZONTAL * 40}", CYAN)
218
+
219
+ for key_name, key_value in keys.items():
220
+ if key_value:
221
+ print(f"Testing {key_name}...", end=" ", flush=True)
222
+ if key_name == 'OPENAI_API_KEY':
223
+ valid = test_openai_key(key_value)
224
+ elif key_name in ['GEMINI_API_KEY', 'GOOGLE_API_KEY']:
225
+ valid = test_google_key(key_value)
226
+ elif key_name == 'ANTHROPIC_API_KEY':
227
+ valid = test_anthropic_key(key_value)
228
+ else:
229
+ valid = False
230
+
231
+ if valid:
232
+ print_colored(f"{CHECK_MARK} Valid", CYAN)
233
+ results[key_name] = True
234
+ else:
235
+ print_colored(f"{CROSS_MARK} Invalid", YELLOW)
236
+ results[key_name] = False
237
+ else:
238
+ print_colored(f"{key_name}: Not found", YELLOW)
239
+ results[key_name] = False
240
+
241
+ return results
242
+
243
+ def get_user_keys(current_keys: Dict[str, Optional[str]]) -> Dict[str, Optional[str]]:
244
+ """Interactive key entry/modification"""
245
+ print_colored(f"\n{create_fat_divider()}", YELLOW)
246
+ print_colored("API Key Configuration", YELLOW, bold=True)
247
+ print_colored(f"{create_fat_divider()}", YELLOW)
248
+
249
+ print_colored("You need only one API key to get started", WHITE)
250
+ print()
251
+ print_colored("Get API keys here:", WHITE)
252
+ print_colored(f" OpenAI {ARROW_RIGHT} https://platform.openai.com/api-keys", CYAN)
253
+ print_colored(f" Google Gemini {ARROW_RIGHT} https://aistudio.google.com/app/apikey", CYAN)
254
+ print_colored(f" Anthropic {ARROW_RIGHT} https://console.anthropic.com/settings/keys", CYAN)
255
+ print()
256
+ print_colored("A free instant starter key is available from Google Gemini (above)", CYAN)
257
+ print()
258
+
259
+ new_keys = current_keys.copy()
260
+
261
+ # Get the actual key names from discovered keys
262
+ key_names = list(current_keys.keys())
263
+ for key_name in key_names:
264
+ current_value = current_keys.get(key_name, "")
265
+ status = "found" if current_value else "not found"
266
+
267
+ print_colored(f"{LIGHT_HORIZONTAL * 60}", CYAN)
268
+ print_colored(f"{key_name} (currently: {status})", WHITE, bold=True)
269
+
270
+ if current_value:
271
+ prompt = f"Enter new key or press ENTER to keep existing: "
272
+ else:
273
+ prompt = f"Enter API key (or press ENTER to skip): "
274
+
275
+ try:
276
+ user_input = input(f"{WHITE}{prompt}{RESET}").strip()
277
+ if user_input:
278
+ new_keys[key_name] = user_input
279
+ elif not current_value:
280
+ new_keys[key_name] = None
281
+ except KeyboardInterrupt:
282
+ print_colored("\n\nSetup cancelled.", YELLOW)
283
+ sys.exit(0)
284
+
285
+ return new_keys
286
+
287
+ def detect_shell() -> str:
288
+ """Detect user's default shell"""
289
+ try:
290
+ shell_path = os.getenv('SHELL', '/bin/bash')
291
+ shell_name = os.path.basename(shell_path)
292
+ return shell_name
293
+ except:
294
+ return 'bash'
295
+
296
+ def get_shell_init_file(shell: str) -> str:
297
+ """Get the appropriate shell initialization file"""
298
+ home = Path.home()
299
+
300
+ shell_files = {
301
+ 'bash': home / '.bashrc',
302
+ 'zsh': home / '.zshrc',
303
+ 'fish': home / '.config/fish/config.fish',
304
+ 'csh': home / '.cshrc',
305
+ 'tcsh': home / '.tcshrc',
306
+ 'ksh': home / '.kshrc',
307
+ 'sh': home / '.profile'
308
+ }
309
+
310
+ return str(shell_files.get(shell, home / '.bashrc'))
311
+
312
+ def create_api_env_script(keys: Dict[str, str], shell: str) -> str:
313
+ """Create shell-appropriate environment script with proper escaping"""
314
+ valid_keys = {k: v for k, v in keys.items() if v}
315
+ lines = []
316
+
317
+ for key, value in valid_keys.items():
318
+ # shlex.quote is designed for POSIX shells (sh, bash, zsh, ksh)
319
+ # It also works reasonably well for fish and csh for simple assignments
320
+ quoted_val = shlex.quote(value)
321
+
322
+ if shell == 'fish':
323
+ lines.append(f'set -gx {key} {quoted_val}')
324
+ elif shell in ['csh', 'tcsh']:
325
+ lines.append(f'setenv {key} {quoted_val}')
326
+ else: # bash, zsh, ksh, sh and others
327
+ lines.append(f'export {key}={quoted_val}')
328
+
329
+ return '\n'.join(lines) + '\n'
330
+
331
+ def save_configuration(valid_keys: Dict[str, str]) -> Tuple[List[str], bool, Optional[str]]:
332
+ """Save configuration to ~/.pdd/ directory"""
333
+ home = Path.home()
334
+ pdd_dir = home / '.pdd'
335
+ created_pdd_dir = False
336
+ saved_files = []
337
+
338
+ # Create .pdd directory if it doesn't exist
339
+ if not pdd_dir.exists():
340
+ pdd_dir.mkdir(mode=0o755)
341
+ created_pdd_dir = True
342
+
343
+ # Detect shell and create api-env script
344
+ shell = detect_shell()
345
+ api_env_content = create_api_env_script(valid_keys, shell)
346
+
347
+ # Write shell-specific api-env file
348
+ api_env_file = pdd_dir / f'api-env.{shell}'
349
+ api_env_file.write_text(api_env_content)
350
+ api_env_file.chmod(0o755)
351
+ saved_files.append(str(api_env_file))
352
+
353
+ # Create llm_model.csv with models from packaged CSV filtered by provider and available keys
354
+ header_fields, rows = _read_packaged_llm_model_csv()
355
+
356
+ # Keep only direct Google Gemini (model startswith 'gemini/'), OpenAI GPT (gpt-*) and Anthropic (anthropic/*)
357
+ def _is_supported_model(row: Dict[str, str]) -> bool:
358
+ model = (row.get('model') or '').strip()
359
+ if model.startswith('gpt-'):
360
+ return True
361
+ if model.startswith('gemini/'):
362
+ return True
363
+ if model.startswith('anthropic/'):
364
+ return True
365
+ return False
366
+
367
+ # Filter rows by supported models and by api_key presence in valid_keys
368
+ filtered_rows: List[Dict[str, str]] = []
369
+ for row in rows:
370
+ if not _is_supported_model(row):
371
+ continue
372
+ api_key_name = (row.get('api_key') or '').strip()
373
+ # Include only if we have a validated key for this row
374
+ if api_key_name and api_key_name in valid_keys:
375
+ filtered_rows.append(row)
376
+
377
+ # Write out the filtered CSV to ~/.pdd/llm_model.csv preserving column order
378
+ llm_model_file = pdd_dir / 'llm_model.csv'
379
+ with llm_model_file.open('w', newline='') as f:
380
+ writer = csv.DictWriter(f, fieldnames=header_fields)
381
+ writer.writeheader()
382
+ for row in filtered_rows:
383
+ writer.writerow({k: row.get(k, '') for k in header_fields})
384
+ saved_files.append(str(llm_model_file))
385
+
386
+ # Update shell init file
387
+ init_file_path = get_shell_init_file(shell)
388
+ init_file = Path(init_file_path)
389
+ init_file_updated = None
390
+
391
+ source_line = f'[ -f "{api_env_file}" ] && source "{api_env_file}"'
392
+ if shell == 'fish':
393
+ source_line = f'test -f "{api_env_file}"; and source "{api_env_file}"'
394
+ elif shell in ['csh', 'tcsh']:
395
+ source_line = f'if ( -f "{api_env_file}" ) source "{api_env_file}"'
396
+ elif shell == 'sh':
397
+ source_line = f'[ -f "{api_env_file}" ] && . "{api_env_file}"'
398
+
399
+ # Ensure parent directory exists (important for fish shell)
400
+ init_file.parent.mkdir(parents=True, exist_ok=True)
401
+
402
+ # Check if source line already exists
403
+ if init_file.exists():
404
+ content = init_file.read_text()
405
+ if str(api_env_file) not in content:
406
+ with init_file.open('a') as f:
407
+ f.write(f'\n# PDD API environment\n{source_line}\n')
408
+ init_file_updated = str(init_file)
409
+ else:
410
+ init_file.write_text(f'# PDD API environment\n{source_line}\n')
411
+ init_file_updated = str(init_file)
412
+
413
+ return saved_files, created_pdd_dir, init_file_updated
414
+
415
+ def create_sample_prompt():
416
+ """Create the sample prompt file"""
417
+ prompt_file = Path('success_python.prompt')
418
+ prompt_file.write_text(SUCCESS_PYTHON_TEMPLATE)
419
+ return str(prompt_file)
420
+
421
+ def show_menu(keys: Dict[str, Optional[str]], test_results: Dict[str, bool]) -> str:
422
+ """Show main menu and get user choice"""
423
+ print_colored(f"\n{create_divider()}", CYAN)
424
+ print_colored("Main Menu", CYAN, bold=True)
425
+ print_colored(f"{create_divider()}", CYAN)
426
+
427
+ # Show current status
428
+ print_colored("Current API Key Status:", WHITE, bold=True)
429
+ # Get the actual key names from discovered keys
430
+ key_names = list(keys.keys())
431
+ for key_name in key_names:
432
+ key_value = keys.get(key_name)
433
+ if key_value:
434
+ status = f"{CHECK_MARK} Valid" if test_results.get(key_name) else f"{CROSS_MARK} Invalid"
435
+ status_color = CYAN if test_results.get(key_name) else YELLOW
436
+ else:
437
+ status = "Not configured"
438
+ status_color = YELLOW
439
+
440
+ print(f" {key_name}: ", end="")
441
+ print_colored(status, status_color)
442
+
443
+ print()
444
+ print_colored("Options:", WHITE, bold=True)
445
+ print(f" 1. Re-enter API keys")
446
+ print(f" 2. Re-test current keys")
447
+ print(f" 3. Save configuration and exit")
448
+ print(f" 4. Exit without saving")
449
+ print()
450
+
451
+ while True:
452
+ try:
453
+ choice = input(f"{WHITE}Choose an option (1-4): {RESET}").strip()
454
+ if choice in ['1', '2', '3', '4']:
455
+ return choice
456
+ else:
457
+ print_colored("Please enter 1, 2, 3, or 4", YELLOW)
458
+ except KeyboardInterrupt:
459
+ print_colored("\n\nSetup cancelled.", YELLOW)
460
+ sys.exit(0)
461
+
462
+ def create_exit_summary(saved_files: List[str], created_pdd_dir: bool, sample_prompt_file: str, shell: str, valid_keys: Dict[str, str], init_file_updated: Optional[str] = None) -> str:
463
+ """Create comprehensive exit summary"""
464
+ summary_lines = [
465
+ "\n\n\n\n\n",
466
+ create_fat_divider(),
467
+ "PDD Setup Complete!",
468
+ create_fat_divider(),
469
+ "",
470
+ "API Keys Configured:",
471
+ ""
472
+ ]
473
+
474
+ # Add configured API keys information
475
+ if valid_keys:
476
+ for key_name, key_value in valid_keys.items():
477
+ # Show just the first and last few characters for security
478
+ masked_key = f"{key_value[:8]}...{key_value[-4:]}" if len(key_value) > 12 else "***"
479
+ summary_lines.append(f" {key_name}: {masked_key}")
480
+ summary_lines.extend(["", "Files created and configured:", ""])
481
+ else:
482
+ summary_lines.extend([" None", "", "Files created and configured:", ""])
483
+
484
+ # File descriptions with alignment
485
+ file_descriptions = []
486
+ if created_pdd_dir:
487
+ file_descriptions.append(("~/.pdd/", "PDD configuration directory"))
488
+
489
+ for file_path in saved_files:
490
+ if 'api-env.' in file_path:
491
+ file_descriptions.append((file_path, f"API environment variables ({shell} shell)"))
492
+ elif 'llm_model.csv' in file_path:
493
+ file_descriptions.append((file_path, "LLM model configuration"))
494
+
495
+ file_descriptions.append((sample_prompt_file, "Sample prompt for testing"))
496
+
497
+ # Add shell init file if it was updated
498
+ if init_file_updated:
499
+ file_descriptions.append((init_file_updated, f"Shell startup file (updated to source API environment)"))
500
+
501
+ file_descriptions.append(("PDD-SETUP-SUMMARY.txt", "This summary"))
502
+
503
+ # Find max file path length for alignment
504
+ max_path_len = max(len(path) for path, _ in file_descriptions)
505
+
506
+ for file_path, description in file_descriptions:
507
+ summary_lines.append(f"{file_path:<{max_path_len + 2}}{description}")
508
+
509
+ summary_lines.extend([
510
+ "",
511
+ create_divider(),
512
+ "",
513
+ "QUICK START:",
514
+ "",
515
+ f"1. Reload your shell environment:"
516
+ ])
517
+
518
+ # Shell-specific source command for manual reloading
519
+ api_env_path = f"{Path.home()}/.pdd/api-env.{shell}"
520
+ # Use dot command for sh shell, source for others
521
+ if shell == 'sh':
522
+ source_cmd = f". {api_env_path}"
523
+ else:
524
+ source_cmd = f"source {api_env_path}"
525
+
526
+ summary_lines.extend([
527
+ f" {source_cmd}",
528
+ "",
529
+ f"2. Generate code from the sample prompt:",
530
+ f" pdd generate success_python.prompt",
531
+ "",
532
+ create_divider(),
533
+ "",
534
+ "LEARN MORE:",
535
+ "",
536
+ f"{BULLET} PDD documentation: pdd --help",
537
+ f"{BULLET} PDD website: https://promptdriven.ai/",
538
+ f"{BULLET} Discord community: https://discord.gg/Yp4RTh8bG7",
539
+ "",
540
+ "TIPS:",
541
+ "",
542
+ f"{BULLET} IMPORTANT: Reload your shell environment using the source command above",
543
+ "",
544
+ f"{BULLET} Start with simple prompts and gradually increase complexity",
545
+ f"{BULLET} Try out 'pdd test' with your prompt+code to create test(s) pdd can use to automatically verify and fix your output code",
546
+ f"{BULLET} Try out 'pdd example' with your prompt+code to create examples which help pdd do better",
547
+ "",
548
+ f"{BULLET} As you get comfortable, learn configuration settings, including the .pddrc file, PDD_GENERATE_OUTPUT_PATH, and PDD_TEST_OUTPUT_PATH",
549
+ f"{BULLET} For larger projects, use Makefiles and/or 'pdd sync'",
550
+ f"{BULLET} For ongoing substantial projects, learn about llm_model.csv and the --strength,",
551
+ f" --temperature, and --time options to optimize model cost, latency, and output quality",
552
+ "",
553
+ f"{BULLET} Use 'pdd --help' to explore all available commands",
554
+ "",
555
+ "Problems? Shout out on our Discord for help! https://discord.gg/Yp4RTh8bG7"
556
+ ])
557
+
558
+ return '\n'.join(summary_lines)
559
+
560
+ def main():
561
+ """Main setup workflow"""
562
+ # Initial greeting
563
+ print_pdd_logo()
564
+
565
+ # Discover environment
566
+ print_colored(f"{create_divider()}", CYAN)
567
+ print_colored("Discovering local configuration...", CYAN, bold=True)
568
+ print_colored(f"{create_divider()}", CYAN)
569
+
570
+ keys = discover_api_keys()
571
+
572
+ # Test discovered keys
573
+ test_results = test_api_keys(keys)
574
+
575
+ # Main interaction loop
576
+ while True:
577
+ choice = show_menu(keys, test_results)
578
+
579
+ if choice == '1':
580
+ # Re-enter keys
581
+ keys = get_user_keys(keys)
582
+ test_results = test_api_keys(keys)
583
+
584
+ elif choice == '2':
585
+ # Re-test keys
586
+ test_results = test_api_keys(keys)
587
+
588
+ elif choice == '3':
589
+ # Save and exit
590
+ valid_keys = {k: v for k, v in keys.items() if v and test_results.get(k)}
591
+
592
+ if not valid_keys:
593
+ print_colored("\nNo valid API keys to save!", YELLOW)
594
+ continue
595
+
596
+ print_colored(f"\n{create_divider()}", CYAN)
597
+ print_colored("Saving configuration...", CYAN, bold=True)
598
+ print_colored(f"{create_divider()}", CYAN)
599
+
600
+ try:
601
+ saved_files, created_pdd_dir, init_file_updated = save_configuration(valid_keys)
602
+ sample_prompt_file = create_sample_prompt()
603
+ shell = detect_shell()
604
+
605
+ # Create and display summary
606
+ summary = create_exit_summary(saved_files, created_pdd_dir, sample_prompt_file, shell, valid_keys, init_file_updated)
607
+
608
+ # Write summary to file
609
+ summary_file = Path('PDD-SETUP-SUMMARY.txt')
610
+ summary_file.write_text(summary)
611
+
612
+ # Display summary with colors
613
+ lines = summary.split('\n')
614
+ for line in lines:
615
+ if line == create_fat_divider():
616
+ print_colored(line, YELLOW, bold=True)
617
+ elif line == "PDD Setup Complete!":
618
+ print_colored(line, YELLOW, bold=True)
619
+ elif line == create_divider():
620
+ print_colored(line, CYAN)
621
+ elif line.startswith("API Keys Configured:") or line.startswith("Files created and configured:"):
622
+ print_colored(line, CYAN, bold=True)
623
+ elif line.startswith("QUICK START:"):
624
+ print_colored(line, YELLOW, bold=True)
625
+ elif line.startswith("LEARN MORE:") or line.startswith("TIPS:"):
626
+ print_colored(line, CYAN, bold=True)
627
+ elif "IMPORTANT:" in line or "Problems?" in line:
628
+ print_colored(line, YELLOW, bold=True)
629
+ else:
630
+ print(line)
631
+
632
+ break
633
+
634
+ except Exception as e:
635
+ print_colored(f"Error saving configuration: {e}", YELLOW)
636
+ continue
637
+
638
+ elif choice == '4':
639
+ # Exit without saving
640
+ print_colored("\nExiting without saving configuration.", YELLOW)
641
+ break
642
+
643
+ if __name__ == '__main__':
644
+ try:
645
+ main()
646
+ except KeyboardInterrupt:
647
+ print_colored("\n\nSetup cancelled.", YELLOW)
648
+ sys.exit(0)
pdd/simple_math.py ADDED
@@ -0,0 +1,2 @@
1
+ def add(a, b):
2
+ return a + b
pdd/split_main.py CHANGED
@@ -53,7 +53,8 @@ def split_main(
53
53
  force=ctx.obj.get('force', False),
54
54
  quiet=ctx.obj.get('quiet', False),
55
55
  command="split",
56
- command_options=command_options
56
+ command_options=command_options,
57
+ context_override=ctx.obj.get('context')
57
58
  )
58
59
 
59
60
  # Get parameters from context
@@ -113,4 +114,4 @@ def split_main(
113
114
  elif isinstance(e, ValueError):
114
115
  rprint("[yellow]Hint: Check if input files have valid content.[/yellow]")
115
116
 
116
- sys.exit(1)
117
+ sys.exit(1)