pdd-cli 0.0.59__py3-none-any.whl → 0.0.61__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pdd-cli might be problematic. Click here for more details.

pdd/setup_tool.py CHANGED
@@ -1,16 +1,22 @@
1
- """Interactive post-install setup utility for PDD."""
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
+ """
2
6
 
3
7
  import os
4
8
  import sys
5
- from pathlib import Path
6
- from typing import Dict, List, Optional, Tuple
7
-
9
+ import subprocess
10
+ import json
8
11
  import requests
9
-
12
+ import csv
13
+ import importlib.resources
14
+ from pathlib import Path
15
+ from typing import Dict, Optional, Tuple, List
10
16
 
11
17
  # Global variables for non-ASCII characters and colors
12
18
  HEAVY_HORIZONTAL = "━"
13
- LIGHT_HORIZONTAL = "─"
19
+ LIGHT_HORIZONTAL = "─"
14
20
  HEAVY_VERTICAL = "┃"
15
21
  LIGHT_VERTICAL = "│"
16
22
  TOP_LEFT_CORNER = "┏"
@@ -34,52 +40,44 @@ CYAN = "\033[96m"
34
40
  YELLOW = "\033[93m"
35
41
  BOLD = "\033[1m"
36
42
 
37
-
38
43
  # Template content inline
39
- HELLO_PYTHON_TEMPLATE = """Create a Python program that prints "Hello <username>" in ASCII art.
40
-
41
- Requirements:
42
- - <username> is the username of the current session, using the "whoami" command
43
- - in the code, generate the full english 26 character alphabet in ascii art as a map of character values to ascii art strings, then use those to render
44
- - Use only the Python standard library (no external dependencies)
45
- - Create large, bold ASCII art text, using ASCII drawing characters, symbols, or any other characters that are useful
46
- - The drawn text should be at least 10 rows in height
47
- - Make it visually appealing with simple characters like #, *, or =
48
- - Keep the code clean and readable
49
- - Add a brief comment explaining what the program does
50
-
51
- The program should be self-contained and runnable with just `python3 filename.py`."""
52
-
53
- LLM_MODEL_CSV_TEMPLATE = """provider,model,input,output,coding_arena_elo,base_url,api_key,max_reasoning_tokens,structured_output,reasoning_type
54
- OpenAI,gpt-5-nano,0.05,0.4,1249,,OPENAI_API_KEY,0,True,none
55
- Google,gemini/gemini-2.5-pro,1.25,10.0,1360,,GOOGLE_API_KEY,0,True,none
56
- OpenAI,gpt-5-mini,0.25,2.0,1325,,OPENAI_API_KEY,0,True,effort
57
- OpenAI,gpt-5,1.25,10.0,1482,,OPENAI_API_KEY,0,True,effort
58
- OpenAI,gpt-4.1,2.0,8.0,1253,,OPENAI_API_KEY,0,True,none"""
44
+ SUCCESS_PYTHON_TEMPLATE = """
45
+ Write a python script to print "You did it, <Username>!!!" to the console.
46
+ Do not write anything except that message.
47
+ Capitalize the username."""
48
+
49
+ def _read_packaged_llm_model_csv() -> Tuple[List[str], List[Dict[str, str]]]:
50
+ """Load the packaged CSV (pdd/data/llm_model.csv) and return header + rows.
51
+
52
+ Returns:
53
+ (header_fields, rows) where header_fields is the list of column names
54
+ and rows is a list of dictionaries for each CSV row.
55
+ """
56
+ try:
57
+ csv_text = importlib.resources.files('pdd').joinpath('data/llm_model.csv').read_text()
58
+ except Exception as e:
59
+ raise FileNotFoundError(f"Failed to load default LLM model CSV from package: {e}")
59
60
 
61
+ reader = csv.DictReader(csv_text.splitlines())
62
+ header = reader.fieldnames or []
63
+ rows = [row for row in reader]
64
+ return header, rows
60
65
 
61
66
  def print_colored(text: str, color: str = WHITE, bold: bool = False) -> None:
62
- """Print colored text to console."""
63
-
67
+ """Print colored text to console"""
64
68
  style = BOLD + color if bold else color
65
69
  print(f"{style}{text}{RESET}")
66
70
 
67
-
68
71
  def create_divider(char: str = LIGHT_HORIZONTAL, width: int = 80) -> str:
69
- """Create a horizontal divider line."""
70
-
72
+ """Create a horizontal divider line"""
71
73
  return char * width
72
74
 
73
-
74
75
  def create_fat_divider(width: int = 80) -> str:
75
- """Create a fat horizontal divider line."""
76
-
76
+ """Create a fat horizontal divider line"""
77
77
  return HEAVY_HORIZONTAL * width
78
78
 
79
-
80
- def print_pdd_logo() -> None:
81
- """Print the PDD logo in ASCII art."""
82
-
79
+ def print_pdd_logo():
80
+ """Print the PDD logo in ASCII art"""
83
81
  logo = "\n".join(
84
82
  [
85
83
  " +xxxxxxxxxxxxxxx+",
@@ -100,77 +98,135 @@ def print_pdd_logo() -> None:
100
98
  ]
101
99
  )
102
100
  print(f"{CYAN}{logo}{RESET}")
103
- print_colored("Supported: OpenAI and Google Gemini (non-Vertex)", WHITE)
101
+ print()
102
+ print_colored("Let's get set up quickly with a solid basic configuration!", WHITE, bold=True)
103
+ print()
104
+ print_colored("Supported: OpenAI, Google Gemini, and Anthropic Claude", WHITE)
104
105
  print_colored("from their respective API endpoints (no third-parties, such as Azure)", WHITE)
105
106
  print()
106
107
 
108
+ def get_csv_variable_names() -> Dict[str, str]:
109
+ """Inspect packaged CSV to determine API key variable names per provider.
110
+
111
+ Focus on direct providers only: OpenAI GPT models (model startswith 'gpt-'),
112
+ Google Gemini (model startswith 'gemini/'), and Anthropic (model startswith 'anthropic/').
113
+ """
114
+ header, rows = _read_packaged_llm_model_csv()
115
+ variable_names: Dict[str, str] = {}
116
+
117
+ for row in rows:
118
+ model = (row.get('model') or '').strip()
119
+ api_key = (row.get('api_key') or '').strip()
120
+ provider = (row.get('provider') or '').strip().upper()
121
+
122
+ if not api_key:
123
+ continue
124
+
125
+ if model.startswith('gpt-') and provider == 'OPENAI':
126
+ variable_names['OPENAI'] = api_key
127
+ elif model.startswith('gemini/') and provider == 'GOOGLE':
128
+ # Prefer direct Gemini key, not Vertex
129
+ variable_names['GOOGLE'] = api_key
130
+ elif model.startswith('anthropic/') and provider == 'ANTHROPIC':
131
+ variable_names['ANTHROPIC'] = api_key
132
+
133
+ # Fallbacks if not detected (keep prior behavior)
134
+ variable_names.setdefault('OPENAI', 'OPENAI_API_KEY')
135
+ # Prefer GEMINI_API_KEY name for Google if present
136
+ variable_names.setdefault('GOOGLE', 'GEMINI_API_KEY')
137
+ variable_names.setdefault('ANTHROPIC', 'ANTHROPIC_API_KEY')
138
+ return variable_names
107
139
 
108
140
  def discover_api_keys() -> Dict[str, Optional[str]]:
109
- """Discover API keys from environment variables."""
110
-
141
+ """Discover API keys from environment variables"""
142
+ # Get the variable names actually used in CSV template
143
+ csv_vars = get_csv_variable_names()
144
+
111
145
  keys = {
112
- "OPENAI_API_KEY": os.getenv("OPENAI_API_KEY"),
113
- "GOOGLE_API_KEY": os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY"),
146
+ 'OPENAI_API_KEY': os.getenv('OPENAI_API_KEY'),
147
+ 'ANTHROPIC_API_KEY': os.getenv('ANTHROPIC_API_KEY'),
114
148
  }
149
+
150
+ # For Google, check both possible environment variables but use CSV template's variable name
151
+ google_var_name = csv_vars.get('GOOGLE', 'GEMINI_API_KEY') # Default to GEMINI_API_KEY
152
+ google_api_key = os.getenv('GEMINI_API_KEY') or os.getenv('GOOGLE_API_KEY')
153
+ keys[google_var_name] = google_api_key
154
+
115
155
  return keys
116
156
 
117
-
118
157
  def test_openai_key(api_key: str) -> bool:
119
- """Test OpenAI API key validity."""
120
-
158
+ """Test OpenAI API key validity"""
121
159
  if not api_key or not api_key.strip():
122
160
  return False
123
-
161
+
124
162
  try:
125
163
  headers = {
126
- "Authorization": f"Bearer {api_key.strip()}",
127
- "Content-Type": "application/json",
164
+ 'Authorization': f'Bearer {api_key.strip()}',
165
+ 'Content-Type': 'application/json'
128
166
  }
129
167
  response = requests.get(
130
- "https://api.openai.com/v1/models",
168
+ 'https://api.openai.com/v1/models',
131
169
  headers=headers,
132
- timeout=10,
170
+ timeout=10
133
171
  )
134
172
  return response.status_code == 200
135
173
  except Exception:
136
174
  return False
137
175
 
138
-
139
176
  def test_google_key(api_key: str) -> bool:
140
- """Test Google Gemini API key validity."""
141
-
177
+ """Test Google Gemini API key validity"""
142
178
  if not api_key or not api_key.strip():
143
179
  return False
144
-
180
+
145
181
  try:
146
182
  response = requests.get(
147
- f"https://generativelanguage.googleapis.com/v1beta/models?key={api_key.strip()}",
148
- timeout=10,
183
+ f'https://generativelanguage.googleapis.com/v1beta/models?key={api_key.strip()}',
184
+ timeout=10
149
185
  )
150
186
  return response.status_code == 200
151
187
  except Exception:
152
188
  return False
153
189
 
190
+ def test_anthropic_key(api_key: str) -> bool:
191
+ """Test Anthropic API key validity"""
192
+ if not api_key or not api_key.strip():
193
+ return False
194
+
195
+ try:
196
+ headers = {
197
+ 'x-api-key': api_key.strip(),
198
+ 'Content-Type': 'application/json'
199
+ }
200
+ response = requests.get(
201
+ 'https://api.anthropic.com/v1/messages',
202
+ headers=headers,
203
+ timeout=10
204
+ )
205
+ # Anthropic returns 400 for invalid request structure but 401/403 for bad keys
206
+ return response.status_code != 401 and response.status_code != 403
207
+ except Exception:
208
+ return False
154
209
 
155
210
  def test_api_keys(keys: Dict[str, Optional[str]]) -> Dict[str, bool]:
156
- """Test all discovered API keys."""
157
-
158
- results: Dict[str, bool] = {}
159
-
211
+ """Test all discovered API keys"""
212
+ results = {}
213
+
160
214
  print_colored(f"\n{LIGHT_HORIZONTAL * 40}", CYAN)
161
215
  print_colored("Testing discovered API keys...", CYAN, bold=True)
162
216
  print_colored(f"{LIGHT_HORIZONTAL * 40}", CYAN)
163
-
217
+
164
218
  for key_name, key_value in keys.items():
165
219
  if key_value:
166
220
  print(f"Testing {key_name}...", end=" ", flush=True)
167
- if key_name == "OPENAI_API_KEY":
221
+ if key_name == 'OPENAI_API_KEY':
168
222
  valid = test_openai_key(key_value)
169
- elif key_name in ["GOOGLE_API_KEY"]:
223
+ elif key_name in ['GEMINI_API_KEY', 'GOOGLE_API_KEY']:
170
224
  valid = test_google_key(key_value)
225
+ elif key_name == 'ANTHROPIC_API_KEY':
226
+ valid = test_anthropic_key(key_value)
171
227
  else:
172
228
  valid = False
173
-
229
+
174
230
  if valid:
175
231
  print_colored(f"{CHECK_MARK} Valid", CYAN)
176
232
  results[key_name] = True
@@ -180,40 +236,41 @@ def test_api_keys(keys: Dict[str, Optional[str]]) -> Dict[str, bool]:
180
236
  else:
181
237
  print_colored(f"{key_name}: Not found", YELLOW)
182
238
  results[key_name] = False
183
-
239
+
184
240
  return results
185
241
 
186
-
187
242
  def get_user_keys(current_keys: Dict[str, Optional[str]]) -> Dict[str, Optional[str]]:
188
- """Interactive key entry/modification."""
189
-
243
+ """Interactive key entry/modification"""
190
244
  print_colored(f"\n{create_fat_divider()}", YELLOW)
191
245
  print_colored("API Key Configuration", YELLOW, bold=True)
192
246
  print_colored(f"{create_fat_divider()}", YELLOW)
193
-
247
+
194
248
  print_colored("You need only one API key to get started", WHITE)
195
249
  print()
196
250
  print_colored("Get API keys here:", WHITE)
197
251
  print_colored(f" OpenAI {ARROW_RIGHT} https://platform.openai.com/api-keys", CYAN)
198
252
  print_colored(f" Google Gemini {ARROW_RIGHT} https://aistudio.google.com/app/apikey", CYAN)
253
+ print_colored(f" Anthropic {ARROW_RIGHT} https://console.anthropic.com/settings/keys", CYAN)
199
254
  print()
200
255
  print_colored("A free instant starter key is available from Google Gemini (above)", CYAN)
201
256
  print()
202
-
257
+
203
258
  new_keys = current_keys.copy()
204
-
205
- for key_name in ["OPENAI_API_KEY", "GOOGLE_API_KEY"]:
259
+
260
+ # Get the actual key names from discovered keys
261
+ key_names = list(current_keys.keys())
262
+ for key_name in key_names:
206
263
  current_value = current_keys.get(key_name, "")
207
264
  status = "found" if current_value else "not found"
208
-
265
+
209
266
  print_colored(f"{LIGHT_HORIZONTAL * 60}", CYAN)
210
267
  print_colored(f"{key_name} (currently: {status})", WHITE, bold=True)
211
-
268
+
212
269
  if current_value:
213
- prompt = "Enter new key or press ENTER to keep existing: "
270
+ prompt = f"Enter new key or press ENTER to keep existing: "
214
271
  else:
215
- prompt = "Enter API key (or press ENTER to skip): "
216
-
272
+ prompt = f"Enter API key (or press ENTER to skip): "
273
+
217
274
  try:
218
275
  user_input = input(f"{WHITE}{prompt}{RESET}").strip()
219
276
  if user_input:
@@ -223,325 +280,367 @@ def get_user_keys(current_keys: Dict[str, Optional[str]]) -> Dict[str, Optional[
223
280
  except KeyboardInterrupt:
224
281
  print_colored("\n\nSetup cancelled.", YELLOW)
225
282
  sys.exit(0)
226
-
283
+
227
284
  return new_keys
228
285
 
229
-
230
286
  def detect_shell() -> str:
231
- """Detect user's default shell."""
232
-
287
+ """Detect user's default shell"""
233
288
  try:
234
- shell_path = os.getenv("SHELL", "/bin/bash")
289
+ shell_path = os.getenv('SHELL', '/bin/bash')
235
290
  shell_name = os.path.basename(shell_path)
236
291
  return shell_name
237
- except Exception:
238
- return "bash"
239
-
292
+ except:
293
+ return 'bash'
240
294
 
241
295
  def get_shell_init_file(shell: str) -> str:
242
- """Get the appropriate shell initialization file."""
243
-
296
+ """Get the appropriate shell initialization file"""
244
297
  home = Path.home()
245
-
298
+
246
299
  shell_files = {
247
- "bash": home / ".bashrc",
248
- "zsh": home / ".zshrc",
249
- "fish": home / ".config/fish/config.fish",
250
- "csh": home / ".cshrc",
251
- "tcsh": home / ".tcshrc",
252
- "ksh": home / ".kshrc",
300
+ 'bash': home / '.bashrc',
301
+ 'zsh': home / '.zshrc',
302
+ 'fish': home / '.config/fish/config.fish',
303
+ 'csh': home / '.cshrc',
304
+ 'tcsh': home / '.tcshrc',
305
+ 'ksh': home / '.kshrc',
306
+ 'sh': home / '.profile'
253
307
  }
254
-
255
- return str(shell_files.get(shell, home / ".bashrc"))
256
-
308
+
309
+ return str(shell_files.get(shell, home / '.bashrc'))
257
310
 
258
311
  def create_api_env_script(keys: Dict[str, str], shell: str) -> str:
259
- """Create shell-appropriate environment script."""
260
-
312
+ """Create shell-appropriate environment script"""
261
313
  valid_keys = {k: v for k, v in keys.items() if v}
262
-
263
- if shell == "fish":
314
+
315
+ if shell == 'fish':
264
316
  lines = []
265
317
  for key, value in valid_keys.items():
266
- lines.append(f"set -gx {key} \"{value}\"")
267
- return "\n".join(lines) + "\n"
268
- if shell in ["csh", "tcsh"]:
318
+ lines.append(f'set -gx {key} "{value}"')
319
+ return '\n'.join(lines) + '\n'
320
+ elif shell in ['csh', 'tcsh']:
269
321
  lines = []
270
322
  for key, value in valid_keys.items():
271
- lines.append(f"setenv {key} \"{value}\"")
272
- return "\n".join(lines) + "\n"
273
-
274
- lines = []
275
- for key, value in valid_keys.items():
276
- lines.append(f"export {key}=\"{value}\"")
277
- return "\n".join(lines) + "\n"
278
-
279
-
280
- def save_configuration(valid_keys: Dict[str, str]) -> Tuple[List[str], bool]:
281
- """Save configuration to ~/.pdd/ directory."""
323
+ lines.append(f'setenv {key} "{value}"')
324
+ return '\n'.join(lines) + '\n'
325
+ else: # bash, zsh, ksh, sh and others
326
+ lines = []
327
+ for key, value in valid_keys.items():
328
+ lines.append(f'export {key}="{value}"')
329
+ return '\n'.join(lines) + '\n'
282
330
 
331
+ def save_configuration(valid_keys: Dict[str, str]) -> Tuple[List[str], bool, Optional[str]]:
332
+ """Save configuration to ~/.pdd/ directory"""
283
333
  home = Path.home()
284
- pdd_dir = home / ".pdd"
334
+ pdd_dir = home / '.pdd'
285
335
  created_pdd_dir = False
286
- saved_files: List[str] = []
287
-
336
+ saved_files = []
337
+
338
+ # Create .pdd directory if it doesn't exist
288
339
  if not pdd_dir.exists():
289
340
  pdd_dir.mkdir(mode=0o755)
290
341
  created_pdd_dir = True
291
-
342
+
343
+ # Detect shell and create api-env script
292
344
  shell = detect_shell()
293
345
  api_env_content = create_api_env_script(valid_keys, shell)
294
-
295
- api_env_file = pdd_dir / "api-env"
346
+
347
+ # Write shell-specific api-env file
348
+ api_env_file = pdd_dir / f'api-env.{shell}'
296
349
  api_env_file.write_text(api_env_content)
297
350
  api_env_file.chmod(0o755)
298
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
299
366
 
300
- csv_lines = LLM_MODEL_CSV_TEMPLATE.strip().split("\n")
301
- header = csv_lines[0]
302
- valid_lines = [header]
303
-
304
- for line in csv_lines[1:]:
305
- if "OPENAI_API_KEY" in line and "OPENAI_API_KEY" in valid_keys:
306
- valid_lines.append(line)
307
- elif "GOOGLE_API_KEY" in line and "GOOGLE_API_KEY" in valid_keys:
308
- valid_lines.append(line)
309
-
310
- llm_model_file = pdd_dir / "llm_model.csv"
311
- llm_model_file.write_text("\n".join(valid_lines) + "\n")
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})
312
384
  saved_files.append(str(llm_model_file))
313
-
385
+
386
+ # Update shell init file
314
387
  init_file_path = get_shell_init_file(shell)
315
388
  init_file = Path(init_file_path)
316
-
389
+ init_file_updated = None
390
+
317
391
  source_line = f'[ -f "{api_env_file}" ] && source "{api_env_file}"'
318
- if shell == "fish":
392
+ if shell == 'fish':
319
393
  source_line = f'test -f "{api_env_file}"; and source "{api_env_file}"'
320
-
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
321
403
  if init_file.exists():
322
404
  content = init_file.read_text()
323
405
  if str(api_env_file) not in content:
324
- with init_file.open("a", encoding="utf-8") as file_handle:
325
- file_handle.write(f"\n# PDD API environment\n{source_line}\n")
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)
326
409
  else:
327
- init_file.write_text(f"# PDD API environment\n{source_line}\n")
328
-
329
- return saved_files, created_pdd_dir
330
-
331
-
332
- def create_sample_prompt() -> str:
333
- """Create the sample prompt file."""
334
-
335
- prompt_file = Path("hello_you_python.prompt")
336
- prompt_file.write_text(HELLO_PYTHON_TEMPLATE)
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)
337
419
  return str(prompt_file)
338
420
 
339
-
340
421
  def show_menu(keys: Dict[str, Optional[str]], test_results: Dict[str, bool]) -> str:
341
- """Show main menu and get user choice."""
342
-
422
+ """Show main menu and get user choice"""
343
423
  print_colored(f"\n{create_divider()}", CYAN)
344
424
  print_colored("Main Menu", CYAN, bold=True)
345
425
  print_colored(f"{create_divider()}", CYAN)
346
-
426
+
427
+ # Show current status
347
428
  print_colored("Current API Key Status:", WHITE, bold=True)
348
- for key_name in ["OPENAI_API_KEY", "GOOGLE_API_KEY"]:
429
+ # Get the actual key names from discovered keys
430
+ key_names = list(keys.keys())
431
+ for key_name in key_names:
349
432
  key_value = keys.get(key_name)
350
433
  if key_value:
351
- status = (
352
- f"{CHECK_MARK} Valid"
353
- if test_results.get(key_name)
354
- else f"{CROSS_MARK} Invalid"
355
- )
434
+ status = f"{CHECK_MARK} Valid" if test_results.get(key_name) else f"{CROSS_MARK} Invalid"
356
435
  status_color = CYAN if test_results.get(key_name) else YELLOW
357
436
  else:
358
437
  status = "Not configured"
359
438
  status_color = YELLOW
360
-
439
+
361
440
  print(f" {key_name}: ", end="")
362
441
  print_colored(status, status_color)
363
-
442
+
364
443
  print()
365
444
  print_colored("Options:", WHITE, bold=True)
366
- print(" 1. Re-enter API keys")
367
- print(" 2. Re-test current keys")
368
- print(" 3. Save configuration and exit")
369
- print(" 4. Exit without saving")
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")
370
449
  print()
371
-
450
+
372
451
  while True:
373
452
  try:
374
453
  choice = input(f"{WHITE}Choose an option (1-4): {RESET}").strip()
375
- if choice in ["1", "2", "3", "4"]:
454
+ if choice in ['1', '2', '3', '4']:
376
455
  return choice
377
- print_colored("Please enter 1, 2, 3, or 4", YELLOW)
456
+ else:
457
+ print_colored("Please enter 1, 2, 3, or 4", YELLOW)
378
458
  except KeyboardInterrupt:
379
459
  print_colored("\n\nSetup cancelled.", YELLOW)
380
460
  sys.exit(0)
381
461
 
382
-
383
- def create_exit_summary(
384
- saved_files: List[str],
385
- created_pdd_dir: bool,
386
- sample_prompt_file: str,
387
- shell: str,
388
- ) -> str:
389
- """Create comprehensive exit summary."""
390
-
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"""
391
464
  summary_lines = [
392
465
  "\n\n\n\n\n",
393
466
  create_fat_divider(),
394
467
  "PDD Setup Complete!",
395
468
  create_fat_divider(),
396
469
  "",
397
- "Files created and configured:",
398
- "",
470
+ "API Keys Configured:",
471
+ ""
399
472
  ]
400
-
401
- file_descriptions: List[Tuple[str, str]] = []
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 = []
402
486
  if created_pdd_dir:
403
487
  file_descriptions.append(("~/.pdd/", "PDD configuration directory"))
404
-
488
+
405
489
  for file_path in saved_files:
406
- if "api-env" in file_path:
407
- file_descriptions.append((file_path, "API environment variables"))
408
- elif "llm_model.csv" in file_path:
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:
409
493
  file_descriptions.append((file_path, "LLM model configuration"))
410
-
494
+
411
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
+
412
501
  file_descriptions.append(("PDD-SETUP-SUMMARY.txt", "This summary"))
413
-
502
+
503
+ # Find max file path length for alignment
414
504
  max_path_len = max(len(path) for path, _ in file_descriptions)
415
-
505
+
416
506
  for file_path, description in file_descriptions:
417
507
  summary_lines.append(f"{file_path:<{max_path_len + 2}}{description}")
418
-
419
- summary_lines.extend(
420
- [
421
- "",
422
- create_divider(),
423
- "",
424
- "QUICK START:",
425
- "",
426
- "1. Reload your shell environment:",
427
- ]
428
- )
429
-
430
- api_env_path = f"{Path.home()}/.pdd/api-env"
431
- if shell == "fish":
432
- source_cmd = f"source {api_env_path}"
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}"
433
523
  else:
434
524
  source_cmd = f"source {api_env_path}"
435
-
436
- summary_lines.extend(
437
- [
438
- f" {source_cmd}",
439
- "",
440
- "2. Generate code from the sample prompt:",
441
- " pdd generate hello_you_python.prompt",
442
- "",
443
- create_divider(),
444
- "",
445
- "LEARN MORE:",
446
- "",
447
- f"{BULLET} PDD documentation: pdd --help",
448
- f"{BULLET} PDD website: https://promptdriven.ai/",
449
- f"{BULLET} Discord community: https://discord.gg/Yp4RTh8bG7",
450
- "",
451
- "TIPS:",
452
- "",
453
- f"{BULLET} IMPORTANT: Reload your shell environment using the source command above",
454
- "",
455
- f"{BULLET} Start with simple prompts and gradually increase complexity",
456
- 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",
457
- f"{BULLET} Try out 'pdd example' with your prompt+code to create examples which help pdd do better",
458
- "",
459
- f"{BULLET} As you get comfortable, learn configuration settings, including the .pddrc file, PDD_GENERATE_OUTPUT_PATH, and PDD_TEST_OUTPUT_PATH",
460
- f"{BULLET} For larger projects, use Makefiles and/or 'pdd sync'",
461
- f"{BULLET} For ongoing substantial projects, learn about llm_model.csv to optimize model cost, latency, and output quality",
462
- "",
463
- f"{BULLET} Use 'pdd --help' to explore all available commands",
464
- "",
465
- "Problems? Shout out on our Discord for help! https://discord.gg/Yp4RTh8bG7",
466
- ]
467
- )
468
-
469
- return "\n".join(summary_lines)
470
-
471
-
472
- def main() -> None:
473
- """Main setup workflow."""
474
-
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
475
563
  print_pdd_logo()
476
-
564
+
565
+ # Discover environment
477
566
  print_colored(f"{create_divider()}", CYAN)
478
567
  print_colored("Discovering local configuration...", CYAN, bold=True)
479
568
  print_colored(f"{create_divider()}", CYAN)
480
-
569
+
481
570
  keys = discover_api_keys()
571
+
572
+ # Test discovered keys
482
573
  test_results = test_api_keys(keys)
483
-
574
+
575
+ # Main interaction loop
484
576
  while True:
485
577
  choice = show_menu(keys, test_results)
486
-
487
- if choice == "1":
578
+
579
+ if choice == '1':
580
+ # Re-enter keys
488
581
  keys = get_user_keys(keys)
489
582
  test_results = test_api_keys(keys)
490
- elif choice == "2":
583
+
584
+ elif choice == '2':
585
+ # Re-test keys
491
586
  test_results = test_api_keys(keys)
492
- elif choice == "3":
587
+
588
+ elif choice == '3':
589
+ # Save and exit
493
590
  valid_keys = {k: v for k, v in keys.items() if v and test_results.get(k)}
494
-
591
+
495
592
  if not valid_keys:
496
593
  print_colored("\nNo valid API keys to save!", YELLOW)
497
594
  continue
498
-
595
+
499
596
  print_colored(f"\n{create_divider()}", CYAN)
500
597
  print_colored("Saving configuration...", CYAN, bold=True)
501
598
  print_colored(f"{create_divider()}", CYAN)
502
-
599
+
503
600
  try:
504
- saved_files, created_pdd_dir = save_configuration(valid_keys)
601
+ saved_files, created_pdd_dir, init_file_updated = save_configuration(valid_keys)
505
602
  sample_prompt_file = create_sample_prompt()
506
603
  shell = detect_shell()
507
-
508
- summary = create_exit_summary(
509
- saved_files,
510
- created_pdd_dir,
511
- sample_prompt_file,
512
- shell,
513
- )
514
-
515
- summary_file = Path("PDD-SETUP-SUMMARY.txt")
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')
516
610
  summary_file.write_text(summary)
517
-
518
- for line in summary.split("\n"):
611
+
612
+ # Display summary with colors
613
+ lines = summary.split('\n')
614
+ for line in lines:
519
615
  if line == create_fat_divider():
520
616
  print_colored(line, YELLOW, bold=True)
521
617
  elif line == "PDD Setup Complete!":
522
618
  print_colored(line, YELLOW, bold=True)
523
619
  elif line == create_divider():
524
620
  print_colored(line, CYAN)
525
- elif any(
526
- line.startswith(prefix)
527
- for prefix in ["QUICK START:", "LEARN MORE:", "TIPS:"]
528
- ):
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:"):
529
626
  print_colored(line, CYAN, bold=True)
530
627
  elif "IMPORTANT:" in line or "Problems?" in line:
531
628
  print_colored(line, YELLOW, bold=True)
532
629
  else:
533
630
  print(line)
534
-
631
+
535
632
  break
536
- except Exception as exc: # noqa: BLE001
537
- print_colored(f"Error saving configuration: {exc}", YELLOW)
633
+
634
+ except Exception as e:
635
+ print_colored(f"Error saving configuration: {e}", YELLOW)
538
636
  continue
539
- elif choice == "4":
637
+
638
+ elif choice == '4':
639
+ # Exit without saving
540
640
  print_colored("\nExiting without saving configuration.", YELLOW)
541
641
  break
542
642
 
543
-
544
- if __name__ == "__main__":
643
+ if __name__ == '__main__':
545
644
  try:
546
645
  main()
547
646
  except KeyboardInterrupt: