pdd-cli 0.0.41__py3-none-any.whl → 0.0.43__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/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """PDD - Prompt Driven Development"""
2
2
 
3
- __version__ = "0.0.41"
3
+ __version__ = "0.0.43"
4
4
 
5
5
  # Strength parameter used for LLM extraction across the codebase
6
6
  # Used in postprocessing, XML tagging, code generation, and other extraction
pdd/auto_deps_main.py CHANGED
@@ -44,7 +44,7 @@ def auto_deps_main( # pylint: disable=too-many-arguments, too-many-locals
44
44
  "csv": auto_deps_csv_path
45
45
  }
46
46
 
47
- input_strings, output_file_paths, _ = construct_paths(
47
+ resolved_config, input_strings, output_file_paths, _ = construct_paths(
48
48
  input_file_paths=input_file_paths,
49
49
  force=ctx.obj.get('force', False),
50
50
  quiet=ctx.obj.get('quiet', False),
pdd/bug_main.py CHANGED
@@ -45,7 +45,7 @@ def bug_main(
45
45
  "output": output,
46
46
  "language": language
47
47
  }
48
- input_strings, output_file_paths, detected_language = construct_paths(
48
+ resolved_config, input_strings, output_file_paths, detected_language = construct_paths(
49
49
  input_file_paths=input_file_paths,
50
50
  force=ctx.obj.get('force', False),
51
51
  quiet=ctx.obj.get('quiet', False),
pdd/change_main.py CHANGED
@@ -197,7 +197,7 @@ def change_main(
197
197
  logger.debug("Calling construct_paths with inputs: %s and options: %s",
198
198
  input_file_paths, command_options)
199
199
  try:
200
- input_strings, output_file_paths, language = construct_paths(
200
+ resolved_config, input_strings, output_file_paths, language = construct_paths(
201
201
  input_file_paths=input_file_paths,
202
202
  force=force,
203
203
  quiet=quiet,
pdd/cli.py CHANGED
@@ -35,6 +35,7 @@ from .fix_verification_main import fix_verification_main
35
35
  from .install_completion import install_completion, get_local_pdd_path
36
36
  from .preprocess_main import preprocess_main
37
37
  from .split_main import split_main
38
+ from .sync_main import sync_main
38
39
  from .trace_main import trace_main
39
40
  from .track_cost import track_cost
40
41
  from .update_main import update_main
@@ -695,7 +696,7 @@ def change(
695
696
  "--output",
696
697
  type=click.Path(writable=True),
697
698
  default=None,
698
- help="Specify where to save the updated prompt file (file or directory).",
699
+ help="Specify where to save the updated prompt file. If not specified, overwrites the original prompt file to maintain it as the source of truth.",
699
700
  )
700
701
  @click.option(
701
702
  "--git",
@@ -870,8 +871,8 @@ def crash(
870
871
  result_data = {
871
872
  "success": success,
872
873
  "attempts": attempts,
873
- "fixed_code_path": output,
874
- "fixed_program_path": output_program,
874
+ "fixed_code": fixed_code,
875
+ "fixed_program": fixed_program,
875
876
  }
876
877
  return result_data, cost, model
877
878
  except Exception as e:
@@ -1098,6 +1099,83 @@ def verify(
1098
1099
  return None # Return None on failure
1099
1100
 
1100
1101
 
1102
+ @cli.command("sync")
1103
+ @click.argument("basename", type=str)
1104
+ @click.option(
1105
+ "--max-attempts",
1106
+ type=int,
1107
+ default=3,
1108
+ show_default=True,
1109
+ help="Maximum number of sync attempts.",
1110
+ )
1111
+ @click.option(
1112
+ "--budget",
1113
+ type=float,
1114
+ default=10.0,
1115
+ show_default=True,
1116
+ help="Maximum total cost allowed for the entire sync process.",
1117
+ )
1118
+ @click.option(
1119
+ "--skip-verify",
1120
+ is_flag=True,
1121
+ default=False,
1122
+ help="Skip verification step during sync.",
1123
+ )
1124
+ @click.option(
1125
+ "--skip-tests",
1126
+ is_flag=True,
1127
+ default=False,
1128
+ help="Skip test generation during sync.",
1129
+ )
1130
+ @click.option(
1131
+ "--target-coverage",
1132
+ type=click.FloatRange(0.0, 100.0),
1133
+ default=90.0,
1134
+ show_default=True,
1135
+ help="Target code coverage percentage for generated tests.",
1136
+ )
1137
+ @click.option(
1138
+ "--log",
1139
+ is_flag=True,
1140
+ default=False,
1141
+ help="Enable detailed logging during sync.",
1142
+ )
1143
+ @click.pass_context
1144
+ @track_cost
1145
+ def sync(
1146
+ ctx: click.Context,
1147
+ basename: str,
1148
+ max_attempts: int,
1149
+ budget: float,
1150
+ skip_verify: bool,
1151
+ skip_tests: bool,
1152
+ target_coverage: float,
1153
+ log: bool,
1154
+ ) -> Optional[Tuple[Dict[str, Any], float, str]]:
1155
+ """Automatically execute the complete PDD workflow loop for a given basename.
1156
+
1157
+ This command implements the entire synchronized cycle, intelligently determining
1158
+ what steps are needed and executing them in the correct order. It detects
1159
+ programming languages by scanning for prompt files matching the pattern
1160
+ {basename}_{language}.prompt in the prompts directory.
1161
+ """
1162
+ try:
1163
+ results, total_cost, model = sync_main(
1164
+ ctx=ctx,
1165
+ basename=basename,
1166
+ max_attempts=max_attempts,
1167
+ budget=budget,
1168
+ skip_verify=skip_verify,
1169
+ skip_tests=skip_tests,
1170
+ target_coverage=target_coverage,
1171
+ log=log,
1172
+ )
1173
+ return results, total_cost, model
1174
+ except Exception as exception:
1175
+ handle_error(exception, "sync", ctx.obj.get("quiet", False))
1176
+ return None
1177
+
1178
+
1101
1179
  @cli.command("install_completion")
1102
1180
  @click.pass_context
1103
1181
  # No @track_cost
pdd/cmd_test_main.py CHANGED
@@ -81,7 +81,7 @@ def cmd_test_main(
81
81
  "target_coverage": target_coverage,
82
82
  }
83
83
 
84
- input_strings, output_file_paths, language = construct_paths(
84
+ resolved_config, input_strings, output_file_paths, language = construct_paths(
85
85
  input_file_paths=input_file_paths,
86
86
  force=ctx.obj["force"],
87
87
  quiet=ctx.obj["quiet"],
@@ -146,8 +146,8 @@ def cmd_test_main(
146
146
  ctx.exit(1)
147
147
  return "", 0.0, ""
148
148
 
149
- # Handle output
150
- output_file = output_file_paths["output"]
149
+ # Handle output - prioritize orchestration output path over construct_paths result
150
+ output_file = output or output_file_paths["output"]
151
151
  if merge and existing_tests:
152
152
  output_file = existing_tests
153
153
 
@@ -157,7 +157,7 @@ def code_generator_main(
157
157
  command_options: Dict[str, Any] = {"output": output}
158
158
 
159
159
  try:
160
- input_strings, output_file_paths, language = construct_paths(
160
+ resolved_config, input_strings, output_file_paths, language = construct_paths(
161
161
  input_file_paths=input_file_paths_dict,
162
162
  force=force_overwrite,
163
163
  quiet=quiet,
@@ -165,7 +165,8 @@ def code_generator_main(
165
165
  command_options=command_options,
166
166
  )
167
167
  prompt_content = input_strings["prompt_file"]
168
- output_path = output_file_paths.get("output")
168
+ # Prioritize orchestration output path over construct_paths result
169
+ output_path = output or output_file_paths.get("output")
169
170
 
170
171
  except FileNotFoundError as e:
171
172
  console.print(f"[red]Error: Input file not found: {e.filename}[/red]")
pdd/conflicts_main.py CHANGED
@@ -28,7 +28,7 @@ def conflicts_main(ctx: click.Context, prompt1: str, prompt2: str, output: Optio
28
28
  command_options = {
29
29
  "output": output
30
30
  }
31
- input_strings, output_file_paths, _ = construct_paths(
31
+ resolved_config, input_strings, output_file_paths, _ = construct_paths(
32
32
  input_file_paths=input_file_paths,
33
33
  force=ctx.obj.get('force', False),
34
34
  quiet=ctx.obj.get('quiet', False),
pdd/construct_paths.py CHANGED
@@ -4,9 +4,12 @@ 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 # Added Optional
7
+ from typing import Dict, Tuple, Any, Optional, List
8
+ import fnmatch
9
+ import logging
8
10
 
9
11
  import click
12
+ import yaml
10
13
  from rich.console import Console
11
14
  from rich.theme import Theme
12
15
 
@@ -21,6 +24,114 @@ import csv
21
24
 
22
25
  console = Console(theme=Theme({"info": "cyan", "warning": "yellow", "error": "bold red"}))
23
26
 
27
+ # Configuration loading functions
28
+ def _find_pddrc_file(start_path: Optional[Path] = None) -> Optional[Path]:
29
+ """Find .pddrc file by searching upward from the given path."""
30
+ if start_path is None:
31
+ start_path = Path.cwd()
32
+
33
+ # Search upward through parent directories
34
+ for path in [start_path] + list(start_path.parents):
35
+ pddrc_file = path / ".pddrc"
36
+ if pddrc_file.is_file():
37
+ return pddrc_file
38
+ return None
39
+
40
+ def _load_pddrc_config(pddrc_path: Path) -> Dict[str, Any]:
41
+ """Load and parse .pddrc configuration file."""
42
+ try:
43
+ with open(pddrc_path, 'r', encoding='utf-8') as f:
44
+ config = yaml.safe_load(f)
45
+
46
+ if not isinstance(config, dict):
47
+ raise ValueError(f"Invalid .pddrc format: expected dictionary at root level")
48
+
49
+ # Validate basic structure
50
+ if 'contexts' not in config:
51
+ raise ValueError(f"Invalid .pddrc format: missing 'contexts' section")
52
+
53
+ return config
54
+ except yaml.YAMLError as e:
55
+ raise ValueError(f"YAML syntax error in .pddrc: {e}")
56
+ except Exception as e:
57
+ raise ValueError(f"Error loading .pddrc: {e}")
58
+
59
+ def _detect_context(current_dir: Path, config: Dict[str, Any], context_override: Optional[str] = None) -> Optional[str]:
60
+ """Detect the appropriate context based on current directory path."""
61
+ if context_override:
62
+ # Validate that the override context exists
63
+ contexts = config.get('contexts', {})
64
+ if context_override not in contexts:
65
+ available = list(contexts.keys())
66
+ raise ValueError(f"Unknown context '{context_override}'. Available contexts: {available}")
67
+ return context_override
68
+
69
+ contexts = config.get('contexts', {})
70
+ current_path_str = str(current_dir)
71
+
72
+ # Try to match against each context's paths
73
+ for context_name, context_config in contexts.items():
74
+ if context_name == 'default':
75
+ continue # Handle default as fallback
76
+
77
+ paths = context_config.get('paths', [])
78
+ for path_pattern in paths:
79
+ # Convert glob pattern to match current directory
80
+ if fnmatch.fnmatch(current_path_str, f"*/{path_pattern}") or \
81
+ fnmatch.fnmatch(current_path_str, path_pattern) or \
82
+ current_path_str.endswith(f"/{path_pattern.rstrip('/**')}"):
83
+ return context_name
84
+
85
+ # Return default context if available
86
+ if 'default' in contexts:
87
+ return 'default'
88
+
89
+ return None
90
+
91
+ def _get_context_config(config: Dict[str, Any], context_name: Optional[str]) -> Dict[str, Any]:
92
+ """Get configuration settings for the specified context."""
93
+ if not context_name:
94
+ return {}
95
+
96
+ contexts = config.get('contexts', {})
97
+ context_config = contexts.get(context_name, {})
98
+ return context_config.get('defaults', {})
99
+
100
+ def _resolve_config_hierarchy(
101
+ cli_options: Dict[str, Any],
102
+ context_config: Dict[str, Any],
103
+ env_vars: Dict[str, str]
104
+ ) -> Dict[str, Any]:
105
+ """Apply configuration hierarchy: CLI > context > environment > defaults."""
106
+ resolved = {}
107
+
108
+ # Configuration keys to resolve
109
+ config_keys = {
110
+ 'generate_output_path': 'PDD_GENERATE_OUTPUT_PATH',
111
+ 'test_output_path': 'PDD_TEST_OUTPUT_PATH',
112
+ 'example_output_path': 'PDD_EXAMPLE_OUTPUT_PATH',
113
+ 'default_language': 'PDD_DEFAULT_LANGUAGE',
114
+ 'target_coverage': 'PDD_TEST_COVERAGE_TARGET',
115
+ 'strength': None,
116
+ 'temperature': None,
117
+ 'budget': None,
118
+ 'max_attempts': None,
119
+ }
120
+
121
+ for config_key, env_var in config_keys.items():
122
+ # 1. CLI options (highest priority)
123
+ if config_key in cli_options and cli_options[config_key] is not None:
124
+ resolved[config_key] = cli_options[config_key]
125
+ # 2. Context configuration
126
+ elif config_key in context_config:
127
+ resolved[config_key] = context_config[config_key]
128
+ # 3. Environment variables
129
+ elif env_var and env_var in env_vars:
130
+ resolved[config_key] = env_vars[env_var]
131
+ # 4. Defaults are handled elsewhere
132
+
133
+ return resolved
134
+
24
135
 
25
136
  def _read_file(path: Path) -> str:
26
137
  """Read a text file safely and return its contents."""
@@ -126,29 +237,24 @@ def _is_known_language(language_name: str) -> bool:
126
237
 
127
238
  def _strip_language_suffix(path_like: os.PathLike[str]) -> str:
128
239
  """
129
- Remove trailing '_<language>.prompt' or '_<language>' from a filename stem
130
- if it matches a known language.
240
+ Remove trailing '_<language>' from a filename stem if it matches a known language.
131
241
  """
132
242
  p = Path(path_like)
133
- stem = p.stem # removes last extension (e.g. '.prompt', '.py')
243
+ stem = p.stem # removes last extension (e.g., '.prompt', '.py')
134
244
 
135
- if "_" not in stem: # No underscore, nothing to strip
245
+ if "_" not in stem:
136
246
  return stem
137
247
 
138
248
  parts = stem.split("_")
139
- # Avoid splitting single-word stems like "Makefile_" if that's possible
140
- if len(parts) < 2:
141
- return stem
142
-
143
249
  candidate_lang = parts[-1]
144
250
 
145
- # Check if the last part is a known language
146
251
  if _is_known_language(candidate_lang):
147
- # If the last part is a language, strip it
252
+ # Do not strip '_prompt' from a non-.prompt file (e.g., 'test_prompt.txt')
253
+ if candidate_lang == 'prompt' and p.suffix != '.prompt':
254
+ return stem
148
255
  return "_".join(parts[:-1])
149
- else:
150
- # Last part is not a language, return original stem
151
- return stem
256
+
257
+ return stem
152
258
 
153
259
 
154
260
  def _extract_basename(
@@ -267,20 +373,125 @@ def construct_paths(
267
373
  command: str,
268
374
  command_options: Optional[Dict[str, Any]], # Allow None
269
375
  create_error_file: bool = True, # Added parameter to control error file creation
270
- ) -> Tuple[Dict[str, str], Dict[str, str], str]:
376
+ context_override: Optional[str] = None, # Added parameter for context override
377
+ ) -> Tuple[Dict[str, Any], Dict[str, str], Dict[str, str], str]:
271
378
  """
272
379
  High‑level orchestrator that loads inputs, determines basename/language,
273
380
  computes output locations, and verifies overwrite rules.
381
+
382
+ Supports .pddrc configuration with context-aware settings and configuration hierarchy:
383
+ CLI options > .pddrc context > environment variables > defaults
274
384
 
275
385
  Returns
276
386
  -------
277
- (input_strings, output_file_paths, language)
387
+ (resolved_config, input_strings, output_file_paths, language)
278
388
  """
279
389
  command_options = command_options or {} # Ensure command_options is a dict
280
390
 
391
+ # ------------- Load .pddrc configuration -----------------
392
+ pddrc_config = {}
393
+ context = None
394
+ context_config = {}
395
+ original_context_config = {} # Keep track of original context config for sync discovery
396
+
397
+ try:
398
+ # Find and load .pddrc file
399
+ pddrc_path = _find_pddrc_file()
400
+ if pddrc_path:
401
+ pddrc_config = _load_pddrc_config(pddrc_path)
402
+
403
+ # Detect appropriate context
404
+ current_dir = Path.cwd()
405
+ context = _detect_context(current_dir, pddrc_config, context_override)
406
+
407
+ # Get context-specific configuration
408
+ context_config = _get_context_config(pddrc_config, context)
409
+ original_context_config = context_config.copy() # Store original before modifications
410
+
411
+ if not quiet and context:
412
+ console.print(f"[info]Using .pddrc context:[/info] {context}")
413
+
414
+ # Apply configuration hierarchy
415
+ env_vars = dict(os.environ)
416
+ resolved_config = _resolve_config_hierarchy(command_options, context_config, env_vars)
417
+
418
+ # Update command_options with resolved configuration for internal use
419
+ for key, value in resolved_config.items():
420
+ if key not in command_options or command_options[key] is None:
421
+ command_options[key] = value
422
+
423
+ # Also update context_config with resolved environment variables for generate_output_paths
424
+ # This ensures environment variables are available when context config doesn't override them
425
+ for key, value in resolved_config.items():
426
+ if key.endswith('_output_path') and key not in context_config:
427
+ context_config[key] = value
428
+
429
+ except Exception as e:
430
+ error_msg = f"Configuration error: {e}"
431
+ console.print(f"[error]{error_msg}[/error]", style="error")
432
+ if not quiet:
433
+ console.print("[warning]Continuing with default configuration...[/warning]", style="warning")
434
+ # Initialize resolved_config on error to avoid downstream issues
435
+ resolved_config = command_options.copy()
436
+
437
+
438
+ # ------------- Handle sync discovery mode ----------------
439
+ if command == "sync" and not input_file_paths:
440
+ basename = command_options.get("basename")
441
+ if not basename:
442
+ raise ValueError("Basename must be provided in command_options for sync discovery mode.")
443
+
444
+ # For discovery, we only need directory paths. Call generate_output_paths with dummy values.
445
+ try:
446
+ output_paths_str = generate_output_paths(
447
+ command="sync",
448
+ output_locations={},
449
+ basename=basename,
450
+ language="python", # Dummy language
451
+ file_extension=".py", # Dummy extension
452
+ context_config=context_config,
453
+ )
454
+ # Infer base directories from a sample output path
455
+ gen_path = Path(output_paths_str.get("generate_output_path", "src"))
456
+
457
+ # First, check current working directory for prompt files matching the basename pattern
458
+ current_dir = Path.cwd()
459
+ prompt_pattern = f"{basename}_*.prompt"
460
+ if list(current_dir.glob(prompt_pattern)):
461
+ # Found prompt files in current working directory
462
+ resolved_config["prompts_dir"] = str(current_dir)
463
+ resolved_config["code_dir"] = str(current_dir)
464
+ if not quiet:
465
+ console.print(f"[info]Found prompt files in current directory:[/info] {current_dir}")
466
+ else:
467
+ # Fall back to context-aware logic
468
+ # Use original_context_config to avoid checking augmented config with env vars
469
+ if original_context_config and any(key.endswith('_output_path') for key in original_context_config):
470
+ # For configured contexts, prompts are typically at the same level as output dirs
471
+ # e.g., if code goes to "pdd/", prompts should be at "prompts/" (siblings)
472
+ resolved_config["prompts_dir"] = "prompts"
473
+ resolved_config["code_dir"] = str(gen_path.parent)
474
+ else:
475
+ # For default contexts, maintain relative relationship
476
+ # e.g., if code goes to "pi.py", prompts should be at "prompts/" (siblings)
477
+ resolved_config["prompts_dir"] = str(gen_path.parent / "prompts")
478
+ resolved_config["code_dir"] = str(gen_path.parent)
479
+
480
+ resolved_config["tests_dir"] = str(Path(output_paths_str.get("test_output_path", "tests")).parent)
481
+ resolved_config["examples_dir"] = str(Path(output_paths_str.get("example_output_path", "examples")).parent)
482
+
483
+ except Exception as e:
484
+ console.print(f"[error]Failed to determine initial paths for sync: {e}", style="error")
485
+ raise
486
+
487
+ # Return early for discovery mode
488
+ return resolved_config, {}, {}, ""
489
+
490
+
281
491
  if not input_file_paths:
282
492
  raise ValueError("No input files provided")
283
493
 
494
+
284
495
  # ------------- normalise & resolve Paths -----------------
285
496
  input_paths: Dict[str, Path] = {}
286
497
  for key, path_str in input_file_paths.items():
@@ -407,6 +618,7 @@ def construct_paths(
407
618
  basename=basename,
408
619
  language=language,
409
620
  file_extension=file_extension,
621
+ context_config=context_config,
410
622
  )
411
623
  # Convert to Path objects for internal use
412
624
  output_paths_resolved: Dict[str, Path] = {k: Path(v) for k, v in output_paths_str.items()}
@@ -440,8 +652,12 @@ def construct_paths(
440
652
  click.secho("Operation cancelled.", fg="red", err=True)
441
653
  sys.exit(1) # Exit if user chooses not to overwrite
442
654
  except Exception as e: # Catch potential errors during confirm (like EOFError in non-interactive)
443
- click.secho(f"Confirmation failed: {e}. Aborting.", fg="red", err=True)
444
- sys.exit(1)
655
+ if 'EOF' in str(e) or 'end-of-file' in str(e).lower():
656
+ # Non-interactive environment, default to not overwriting
657
+ click.secho("Non-interactive environment detected. Use --force to overwrite existing files.", fg="yellow", err=True)
658
+ else:
659
+ click.secho(f"Confirmation failed: {e}. Aborting.", fg="red", err=True)
660
+ sys.exit(1)
445
661
 
446
662
 
447
663
  # ------------- Final reporting ---------------------------
@@ -462,4 +678,14 @@ def construct_paths(
462
678
  # Since we converted to Path, convert back now.
463
679
  output_file_paths_str_return = {k: str(v) for k, v in output_paths_resolved.items()}
464
680
 
465
- return input_strings, output_file_paths_str_return, language
681
+ # Add resolved paths to the config that gets returned
682
+ resolved_config.update(output_file_paths_str_return)
683
+ # Also add inferred directory paths
684
+ gen_path = Path(resolved_config.get("generate_output_path", "src"))
685
+ resolved_config["prompts_dir"] = str(next(iter(input_paths.values())).parent)
686
+ resolved_config["code_dir"] = str(gen_path.parent)
687
+ 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
+
691
+ return resolved_config, input_strings, output_file_paths_str_return, language
@@ -25,7 +25,7 @@ def context_generator_main(ctx: click.Context, prompt_file: str, code_file: str,
25
25
  command_options = {
26
26
  "output": output
27
27
  }
28
- input_strings, output_file_paths, language = construct_paths(
28
+ resolved_config, input_strings, output_file_paths, language = construct_paths(
29
29
  input_file_paths=input_file_paths,
30
30
  force=ctx.obj.get('force', False),
31
31
  quiet=ctx.obj.get('quiet', False),
@@ -51,22 +51,37 @@ def context_generator_main(ctx: click.Context, prompt_file: str, code_file: str,
51
51
  verbose=ctx.obj.get('verbose', False)
52
52
  )
53
53
 
54
- # Save results
55
- if output_file_paths["output"]:
56
- with open(output_file_paths["output"], 'w') as f:
54
+ # Save results - prioritize orchestration output path over construct_paths result
55
+ final_output_path = output or output_file_paths["output"]
56
+ print(f"DEBUG: output param = {output}")
57
+ print(f"DEBUG: output_file_paths['output'] = {output_file_paths['output']}")
58
+ print(f"DEBUG: final_output_path = {final_output_path}")
59
+ if final_output_path and example_code is not None:
60
+ with open(final_output_path, 'w') as f:
57
61
  f.write(example_code)
62
+ elif final_output_path and example_code is None:
63
+ # Log the error but don't crash
64
+ if not ctx.obj.get('quiet', False):
65
+ rprint("[bold red]Warning:[/bold red] Example generation failed, skipping file write")
58
66
 
59
67
  # Provide user feedback
60
68
  if not ctx.obj.get('quiet', False):
61
- rprint("[bold green]Example code generated successfully.[/bold green]")
62
- rprint(f"[bold]Model used:[/bold] {model_name}")
63
- rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
64
- if output:
65
- rprint(f"[bold]Example code saved to:[/bold] {output_file_paths['output']}")
69
+ if example_code is not None:
70
+ rprint("[bold green]Example code generated successfully.[/bold green]")
71
+ rprint(f"[bold]Model used:[/bold] {model_name}")
72
+ rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
73
+ if final_output_path and example_code is not None:
74
+ rprint(f"[bold]Example code saved to:[/bold] {final_output_path}")
75
+ else:
76
+ rprint("[bold red]Example code generation failed.[/bold red]")
77
+ rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
66
78
 
67
- # Always print example code, even in quiet mode
68
- rprint("[bold]Generated Example Code:[/bold]")
69
- rprint(example_code)
79
+ # Always print example code, even in quiet mode (if it exists)
80
+ if example_code is not None:
81
+ rprint("[bold]Generated Example Code:[/bold]")
82
+ rprint(example_code)
83
+ else:
84
+ rprint("[bold red]No example code generated due to errors.[/bold red]")
70
85
 
71
86
  return example_code, total_cost, model_name
72
87