pdd-cli 0.0.42__py3-none-any.whl → 0.0.90__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) 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 +80 -19
  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 +281 -81
  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 -62
  40. pdd/data/llm_model.csv +20 -18
  41. pdd/detect_change_main.py +5 -4
  42. pdd/fix_code_loop.py +331 -77
  43. pdd/fix_error_loop.py +209 -60
  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 +319 -272
  48. pdd/fix_verification_main.py +57 -17
  49. pdd/generate_output_paths.py +93 -10
  50. pdd/generate_test.py +16 -5
  51. pdd/get_jwt_token.py +48 -9
  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/increase_tests.py +7 -0
  56. pdd/incremental_code_generator.py +2 -2
  57. pdd/insert_includes.py +11 -3
  58. pdd/llm_invoke.py +1278 -110
  59. pdd/load_prompt_template.py +36 -10
  60. pdd/pdd_completion.fish +25 -2
  61. pdd/pdd_completion.sh +30 -4
  62. pdd/pdd_completion.zsh +79 -4
  63. pdd/postprocess.py +10 -3
  64. pdd/preprocess.py +228 -15
  65. pdd/preprocess_main.py +8 -5
  66. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  67. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  68. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  69. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  70. pdd/prompts/agentic_update_LLM.prompt +1071 -0
  71. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  72. pdd/prompts/auto_include_LLM.prompt +98 -101
  73. pdd/prompts/change_LLM.prompt +1 -3
  74. pdd/prompts/detect_change_LLM.prompt +562 -3
  75. pdd/prompts/example_generator_LLM.prompt +22 -1
  76. pdd/prompts/extract_code_LLM.prompt +5 -1
  77. pdd/prompts/extract_program_code_fix_LLM.prompt +14 -2
  78. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  79. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  80. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  81. pdd/prompts/fix_code_module_errors_LLM.prompt +16 -4
  82. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +6 -41
  83. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  84. pdd/prompts/generate_test_LLM.prompt +21 -6
  85. pdd/prompts/increase_tests_LLM.prompt +1 -2
  86. pdd/prompts/insert_includes_LLM.prompt +1181 -6
  87. pdd/prompts/split_LLM.prompt +1 -62
  88. pdd/prompts/trace_LLM.prompt +25 -22
  89. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  90. pdd/prompts/update_prompt_LLM.prompt +22 -1
  91. pdd/prompts/xml_convertor_LLM.prompt +3246 -7
  92. pdd/pytest_output.py +188 -21
  93. pdd/python_env_detector.py +151 -0
  94. pdd/render_mermaid.py +236 -0
  95. pdd/setup_tool.py +648 -0
  96. pdd/simple_math.py +2 -0
  97. pdd/split_main.py +3 -2
  98. pdd/summarize_directory.py +56 -7
  99. pdd/sync_determine_operation.py +918 -186
  100. pdd/sync_main.py +82 -32
  101. pdd/sync_orchestration.py +1456 -453
  102. pdd/sync_tui.py +848 -0
  103. pdd/template_registry.py +264 -0
  104. pdd/templates/architecture/architecture_json.prompt +242 -0
  105. pdd/templates/generic/generate_prompt.prompt +174 -0
  106. pdd/trace.py +168 -12
  107. pdd/trace_main.py +4 -3
  108. pdd/track_cost.py +151 -61
  109. pdd/unfinished_prompt.py +49 -3
  110. pdd/update_main.py +549 -67
  111. pdd/update_model_costs.py +2 -2
  112. pdd/update_prompt.py +19 -4
  113. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +20 -7
  114. pdd_cli-0.0.90.dist-info/RECORD +153 -0
  115. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
  116. pdd_cli-0.0.42.dist-info/RECORD +0 -115
  117. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
  118. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
  119. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,174 @@
1
+ """
2
+ Maintenance commands (sync, auto_deps, setup).
3
+ """
4
+ import click
5
+ from typing import Optional, Tuple
6
+ from pathlib import Path
7
+
8
+ from ..sync_main import sync_main
9
+ from ..auto_deps_main import auto_deps_main
10
+ from ..track_cost import track_cost
11
+ from ..core.errors import handle_error
12
+ from ..core.utils import _run_setup_utility
13
+
14
+ @click.command("sync")
15
+ @click.argument("basename", required=True)
16
+ @click.option(
17
+ "--max-attempts",
18
+ type=int,
19
+ default=None,
20
+ help="Maximum number of fix attempts. Default: 3 or .pddrc value.",
21
+ )
22
+ @click.option(
23
+ "--budget",
24
+ type=float,
25
+ default=None,
26
+ help="Maximum total cost for the sync process. Default: 20.0 or .pddrc value.",
27
+ )
28
+ @click.option(
29
+ "--skip-verify",
30
+ is_flag=True,
31
+ default=False,
32
+ help="Skip the functional verification step.",
33
+ )
34
+ @click.option(
35
+ "--skip-tests",
36
+ is_flag=True,
37
+ default=False,
38
+ help="Skip unit test generation and fixing.",
39
+ )
40
+ @click.option(
41
+ "--target-coverage",
42
+ default=None,
43
+ help="Desired code coverage percentage. Default: 10.0 or .pddrc value.",
44
+ )
45
+ @click.option(
46
+ "--dry-run",
47
+ is_flag=True,
48
+ default=False,
49
+ help="Analyze sync state without executing operations. Shows what sync would do.",
50
+ )
51
+ @click.option(
52
+ "--log",
53
+ is_flag=True,
54
+ default=False,
55
+ hidden=True,
56
+ help="Deprecated: Use --dry-run instead.",
57
+ )
58
+ @click.pass_context
59
+ @track_cost
60
+ def sync(
61
+ ctx: click.Context,
62
+ basename: str,
63
+ max_attempts: Optional[int],
64
+ budget: Optional[float],
65
+ skip_verify: bool,
66
+ skip_tests: bool,
67
+ target_coverage: float,
68
+ dry_run: bool,
69
+ log: bool,
70
+ ) -> Optional[Tuple[str, float, str]]:
71
+ """
72
+ Synchronize prompts with code and tests.
73
+
74
+ BASENAME is the base name of the prompt file (e.g., 'my_module' for 'prompts/my_module_python.prompt').
75
+ """
76
+ # Handle deprecated --log flag
77
+ if log:
78
+ click.echo(
79
+ click.style(
80
+ "Warning: --log is deprecated, use --dry-run instead.",
81
+ fg="yellow"
82
+ ),
83
+ err=True
84
+ )
85
+ dry_run = True
86
+
87
+ try:
88
+ result, total_cost, model_name = sync_main(
89
+ ctx=ctx,
90
+ basename=basename,
91
+ max_attempts=max_attempts,
92
+ budget=budget,
93
+ skip_verify=skip_verify,
94
+ skip_tests=skip_tests,
95
+ target_coverage=target_coverage,
96
+ dry_run=dry_run,
97
+ )
98
+ return str(result), total_cost, model_name
99
+ except click.Abort:
100
+ raise
101
+ except Exception as exception:
102
+ handle_error(exception, "sync", ctx.obj.get("quiet", False))
103
+ return None
104
+
105
+
106
+ @click.command("auto-deps")
107
+ @click.argument("prompt_file", type=click.Path(exists=True, dir_okay=False))
108
+ # exists=False to allow manual handling of quoted paths or paths with globs that shell didn't expand
109
+ @click.argument("directory_path", type=click.Path(exists=False, file_okay=False))
110
+ @click.option(
111
+ "--output",
112
+ type=click.Path(writable=True),
113
+ default=None,
114
+ help="Specify where to save the modified prompt (file or directory).",
115
+ )
116
+ @click.option(
117
+ "--csv",
118
+ type=click.Path(writable=True),
119
+ default=None,
120
+ help="Specify the CSV file that contains or will contain dependency information.",
121
+ )
122
+ @click.option(
123
+ "--force-scan",
124
+ is_flag=True,
125
+ default=False,
126
+ help="Force rescanning of all potential dependency files even if they exist in the CSV file.",
127
+ )
128
+ @click.pass_context
129
+ @track_cost
130
+ def auto_deps(
131
+ ctx: click.Context,
132
+ prompt_file: str,
133
+ directory_path: str,
134
+ output: Optional[str],
135
+ csv: Optional[str],
136
+ force_scan: bool,
137
+ ) -> Optional[Tuple[str, float, str]]:
138
+ """Analyze project dependencies and update the prompt file."""
139
+ try:
140
+ # Strip quotes from directory_path if present (e.g. passed incorrectly)
141
+ if directory_path:
142
+ directory_path = directory_path.strip('"').strip("'")
143
+
144
+ # auto_deps_main signature: (ctx, prompt_file, directory_path, auto_deps_csv_path, output, force_scan)
145
+ result, total_cost, model_name = auto_deps_main(
146
+ ctx=ctx,
147
+ prompt_file=prompt_file,
148
+ directory_path=directory_path,
149
+ auto_deps_csv_path=csv,
150
+ output=output,
151
+ force_scan=force_scan
152
+ )
153
+ return result, total_cost, model_name
154
+ except click.Abort:
155
+ raise
156
+ except Exception as exception:
157
+ handle_error(exception, "auto-deps", ctx.obj.get("quiet", False))
158
+ return None
159
+
160
+
161
+ @click.command("setup")
162
+ @click.pass_context
163
+ def setup(ctx: click.Context):
164
+ """Run the interactive setup utility."""
165
+ try:
166
+ # Import here to allow proper mocking
167
+ from .. import cli as cli_module
168
+ quiet = ctx.obj.get("quiet", False) if ctx.obj else False
169
+ # First install completion
170
+ cli_module.install_completion(quiet=quiet)
171
+ # Then run setup utility
172
+ _run_setup_utility()
173
+ except Exception as e:
174
+ handle_error(e, "setup", False)
pdd/commands/misc.py ADDED
@@ -0,0 +1,79 @@
1
+ """
2
+ Miscellaneous commands (preprocess).
3
+ """
4
+ import click
5
+ from typing import Optional, Tuple
6
+
7
+ from ..preprocess_main import preprocess_main
8
+ from ..core.errors import handle_error
9
+
10
+ @click.command("preprocess")
11
+ @click.argument("prompt_file", type=click.Path(exists=True, dir_okay=False))
12
+ @click.option(
13
+ "--output",
14
+ type=click.Path(writable=True),
15
+ default=None,
16
+ help="Specify where to save the preprocessed prompt file (file or directory).",
17
+ )
18
+ @click.option(
19
+ "--xml",
20
+ is_flag=True,
21
+ default=False,
22
+ help="Insert XML delimiters for structure (minimal preprocessing).",
23
+ )
24
+ @click.option(
25
+ "--recursive",
26
+ is_flag=True,
27
+ default=False,
28
+ help="Recursively preprocess includes.",
29
+ )
30
+ @click.option(
31
+ "--double",
32
+ is_flag=True,
33
+ default=False,
34
+ help="Double curly brackets.",
35
+ )
36
+ @click.option(
37
+ "--exclude",
38
+ multiple=True,
39
+ default=None,
40
+ help="List of keys to exclude from curly bracket doubling.",
41
+ )
42
+ @click.pass_context
43
+ # No @track_cost as preprocessing is local, but return dummy tuple for callback
44
+ def preprocess(
45
+ ctx: click.Context,
46
+ prompt_file: str,
47
+ output: Optional[str],
48
+ xml: bool,
49
+ recursive: bool,
50
+ double: bool,
51
+ exclude: Optional[Tuple[str, ...]],
52
+ ) -> Optional[Tuple[str, float, str]]:
53
+ """Preprocess a prompt file to prepare it for LLM use."""
54
+ try:
55
+ # Since preprocess is a local operation, we don't track cost
56
+ # But we need to return a tuple in the expected format for result callback
57
+ result = preprocess_main(
58
+ ctx=ctx,
59
+ prompt_file=prompt_file,
60
+ output=output,
61
+ xml=xml,
62
+ recursive=recursive,
63
+ double=double,
64
+ exclude=list(exclude) if exclude else [],
65
+ )
66
+
67
+ # Handle the result from preprocess_main
68
+ if result is None:
69
+ # If preprocess_main returns None, still return a dummy tuple for the callback
70
+ return "", 0.0, "local"
71
+ else:
72
+ # Unpack the return value from preprocess_main
73
+ processed_prompt, total_cost, model_name = result
74
+ return processed_prompt, total_cost, model_name
75
+ except click.Abort:
76
+ raise
77
+ except Exception as exception:
78
+ handle_error(exception, "preprocess", ctx.obj.get("quiet", False))
79
+ return None
pdd/commands/modify.py ADDED
@@ -0,0 +1,230 @@
1
+ """
2
+ Modify commands (change, split, update).
3
+ """
4
+ import click
5
+ from pathlib import Path
6
+ from typing import Dict, Optional, Tuple, Union
7
+
8
+ from ..split_main import split_main
9
+ from ..change_main import change_main
10
+ from ..update_main import update_main
11
+ from ..track_cost import track_cost
12
+ from ..core.errors import handle_error
13
+
14
+ @click.command("split")
15
+ @click.argument("input_prompt", type=click.Path(exists=True, dir_okay=False))
16
+ @click.argument("input_code", type=click.Path(exists=True, dir_okay=False))
17
+ @click.argument("example_code", type=click.Path(exists=True, dir_okay=False))
18
+ @click.option(
19
+ "--output-sub",
20
+ type=click.Path(writable=True),
21
+ default=None,
22
+ help="Specify where to save the generated sub-prompt file (file or directory).",
23
+ )
24
+ @click.option(
25
+ "--output-modified",
26
+ type=click.Path(writable=True),
27
+ default=None,
28
+ help="Specify where to save the modified prompt file (file or directory).",
29
+ )
30
+ @click.pass_context
31
+ @track_cost
32
+ def split(
33
+ ctx: click.Context,
34
+ input_prompt: str,
35
+ input_code: str,
36
+ example_code: str,
37
+ output_sub: Optional[str],
38
+ output_modified: Optional[str],
39
+ ) -> Optional[Tuple[Dict[str, str], float, str]]:
40
+ """Split large complex prompt files into smaller ones."""
41
+ quiet = ctx.obj.get("quiet", False)
42
+ command_name = "split"
43
+ try:
44
+ result_data, total_cost, model_name = split_main(
45
+ ctx=ctx,
46
+ input_prompt_file=input_prompt,
47
+ input_code_file=input_code,
48
+ example_code_file=example_code,
49
+ output_sub=output_sub,
50
+ output_modified=output_modified,
51
+ )
52
+ return result_data, total_cost, model_name
53
+ except click.Abort:
54
+ raise
55
+ except Exception as e:
56
+ handle_error(e, command_name, quiet)
57
+ return None
58
+
59
+
60
+ @click.command("change")
61
+ @click.argument("change_prompt_file", type=click.Path(exists=True, dir_okay=False))
62
+ @click.argument("input_code", type=click.Path(exists=True)) # Can be file or dir
63
+ @click.argument("input_prompt_file", type=click.Path(exists=True, dir_okay=False), required=False)
64
+ @click.option(
65
+ "--budget",
66
+ type=float,
67
+ default=5.0,
68
+ show_default=True,
69
+ help="Maximum cost allowed for the change process.",
70
+ )
71
+ @click.option(
72
+ "--output",
73
+ type=click.Path(writable=True),
74
+ default=None,
75
+ help="Specify where to save the modified prompt file (file or directory).",
76
+ )
77
+ @click.option(
78
+ "--csv",
79
+ "use_csv",
80
+ is_flag=True,
81
+ default=False,
82
+ help="Use a CSV file for batch change prompts.",
83
+ )
84
+ @click.pass_context
85
+ @track_cost
86
+ def change(
87
+ ctx: click.Context,
88
+ change_prompt_file: str,
89
+ input_code: str,
90
+ input_prompt_file: Optional[str],
91
+ output: Optional[str],
92
+ use_csv: bool,
93
+ budget: float,
94
+ ) -> Optional[Tuple[Union[str, Dict], float, str]]:
95
+ """Modify prompt(s) based on change instructions."""
96
+ quiet = ctx.obj.get("quiet", False)
97
+ command_name = "change"
98
+ try:
99
+ # --- ADD VALIDATION LOGIC HERE ---
100
+ input_code_path = Path(input_code) # Convert to Path object
101
+ if use_csv:
102
+ if not input_code_path.is_dir():
103
+ raise click.UsageError("INPUT_CODE must be a directory when using --csv.")
104
+ if input_prompt_file:
105
+ raise click.UsageError("Cannot use --csv and specify an INPUT_PROMPT_FILE simultaneously.")
106
+ else: # Not using CSV
107
+ if not input_prompt_file:
108
+ # This check might be better inside change_main, but can be here too
109
+ raise click.UsageError("INPUT_PROMPT_FILE is required when not using --csv.")
110
+ if not input_code_path.is_file():
111
+ # This check might be better inside change_main, but can be here too
112
+ raise click.UsageError("INPUT_CODE must be a file when not using --csv.")
113
+ # --- END VALIDATION LOGIC ---
114
+
115
+ result_data, total_cost, model_name = change_main(
116
+ ctx=ctx,
117
+ change_prompt_file=change_prompt_file,
118
+ input_code=input_code,
119
+ input_prompt_file=input_prompt_file,
120
+ output=output,
121
+ use_csv=use_csv,
122
+ budget=budget,
123
+ )
124
+ return result_data, total_cost, model_name
125
+ except click.Abort:
126
+ raise
127
+ except (click.UsageError, Exception) as e: # Catch specific and general exceptions
128
+ handle_error(e, command_name, quiet)
129
+ return None
130
+
131
+
132
+ @click.command("update")
133
+ @click.argument("input_prompt_file", type=click.Path(exists=True, dir_okay=False), required=False)
134
+ @click.argument("modified_code_file", type=click.Path(exists=True, dir_okay=False), required=False)
135
+ @click.argument("input_code_file", type=click.Path(exists=True, dir_okay=False), required=False)
136
+ @click.option(
137
+ "--output",
138
+ type=click.Path(writable=True),
139
+ default=None,
140
+ help="Specify where to save the updated prompt file(s). For single files: saves to this specific path or directory. For repository mode: saves all prompts to this directory. If not specified, uses the original prompt location (single file) or 'prompts' directory (repository mode).",
141
+ )
142
+ @click.option(
143
+ "--git",
144
+ "use_git",
145
+ is_flag=True,
146
+ default=False,
147
+ help="Use git history to find the original code file.",
148
+ )
149
+ @click.option(
150
+ "--extensions",
151
+ type=str,
152
+ default=None,
153
+ help="Comma-separated list of file extensions to update in repo mode (e.g., 'py,js,ts').",
154
+ )
155
+ @click.option(
156
+ "--simple",
157
+ is_flag=True,
158
+ default=False,
159
+ help="Use legacy 2-stage LLM update instead of agentic mode.",
160
+ )
161
+ @click.pass_context
162
+ @track_cost
163
+ def update(
164
+ ctx: click.Context,
165
+ input_prompt_file: Optional[str],
166
+ modified_code_file: Optional[str],
167
+ input_code_file: Optional[str],
168
+ output: Optional[str],
169
+ use_git: bool,
170
+ extensions: Optional[str],
171
+ simple: bool,
172
+ ) -> Optional[Tuple[str, float, str]]:
173
+ """
174
+ Update prompts based on code changes.
175
+
176
+ This command operates in two modes:
177
+
178
+ 1. **Single-File Mode:** When you provide at least a code file, it updates
179
+ or generates a single prompt.
180
+ - `pdd update <CODE_FILE>`: Generates a new prompt for the code.
181
+ - `pdd update [PROMPT_FILE] <CODE_FILE>`: Updates prompt based on code.
182
+ - `pdd update [PROMPT_FILE] <CODE_FILE> <ORIGINAL_CODE_FILE>`: Updates prompt using explicit original code.
183
+
184
+ 2. **Repository-Wide Mode:** When you provide no file arguments, it scans the
185
+ entire repository, finds all code/prompt pairs, creates missing prompts,
186
+ and updates them all based on the latest git changes.
187
+ - `pdd update`: Updates all prompts for modified files in the repo.
188
+ """
189
+ quiet = ctx.obj.get("quiet", False)
190
+ command_name = "update"
191
+ try:
192
+ # In single-file generation mode, when only one positional argument is provided,
193
+ # it is treated as the code file (not the prompt file). This enables the workflow:
194
+ # `pdd update <CODE_FILE>` to generate a new prompt for the given code file.
195
+ # So if input_prompt_file has a value but modified_code_file is None,
196
+ # we reassign input_prompt_file to actual_modified_code_file.
197
+ if input_prompt_file is not None and modified_code_file is None:
198
+ actual_modified_code_file = input_prompt_file
199
+ actual_input_prompt_file = None
200
+ else:
201
+ actual_modified_code_file = modified_code_file
202
+ actual_input_prompt_file = input_prompt_file
203
+
204
+ is_repo_mode = actual_input_prompt_file is None and actual_modified_code_file is None
205
+
206
+ if is_repo_mode:
207
+ if any([input_code_file, use_git]):
208
+ raise click.UsageError(
209
+ "Cannot use file-specific arguments or flags like --git or --input-code in repository-wide mode (when no files are provided)."
210
+ )
211
+ elif extensions:
212
+ raise click.UsageError("--extensions can only be used in repository-wide mode (when no files are provided).")
213
+
214
+ result, total_cost, model_name = update_main(
215
+ ctx=ctx,
216
+ input_prompt_file=actual_input_prompt_file,
217
+ modified_code_file=actual_modified_code_file,
218
+ input_code_file=input_code_file,
219
+ output=output,
220
+ use_git=use_git,
221
+ repo=is_repo_mode,
222
+ extensions=extensions,
223
+ simple=simple,
224
+ )
225
+ return result, total_cost, model_name
226
+ except click.Abort:
227
+ raise
228
+ except (click.UsageError, Exception) as exception:
229
+ handle_error(exception, command_name, quiet)
230
+ return None
pdd/commands/report.py ADDED
@@ -0,0 +1,144 @@
1
+ """
2
+ Report commands (report-core).
3
+ """
4
+ import os
5
+ import click
6
+ import json
7
+ import webbrowser
8
+ import urllib.parse
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ from ..core.errors import handle_error, console
13
+ from ..core.dump import _build_issue_markdown, _github_config, _post_issue_to_github, _create_gist_with_files
14
+
15
+ @click.command("report-core")
16
+ @click.argument("core_file", type=click.Path(exists=True, dir_okay=False), required=False)
17
+ @click.option(
18
+ "--api",
19
+ is_flag=True,
20
+ default=False,
21
+ help="Create issue directly via GitHub API instead of opening browser. Requires authentication."
22
+ )
23
+ @click.option(
24
+ "--repo",
25
+ default=None,
26
+ help="GitHub repository in format 'owner/repo'. Defaults to 'promptdriven/pdd' or PDD_GITHUB_REPO env var."
27
+ )
28
+ @click.option(
29
+ "--description",
30
+ "-d",
31
+ default="",
32
+ help="Optional description of what happened to include in the issue."
33
+ )
34
+ @click.pass_context
35
+ def report_core(ctx: click.Context, core_file: Optional[str], api: bool, repo: Optional[str], description: str):
36
+ """Report a bug by creating a GitHub issue with the core dump file.
37
+
38
+ If CORE_FILE is not provided, the most recent core dump in .pdd/core_dumps is used.
39
+
40
+ By default, opens a browser with a pre-filled issue template. Use --api to create
41
+ the issue directly via GitHub API (requires authentication via gh CLI or GITHUB_TOKEN).
42
+ """
43
+ try:
44
+ if not core_file:
45
+ # Find latest core dump
46
+ core_dump_dir = Path.cwd() / ".pdd" / "core_dumps"
47
+ if not core_dump_dir.exists():
48
+ raise click.UsageError("No core dumps found in .pdd/core_dumps.")
49
+
50
+ dumps = sorted(core_dump_dir.glob("pdd-core-*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
51
+ if not dumps:
52
+ raise click.UsageError("No core dumps found in .pdd/core_dumps.")
53
+ core_file = str(dumps[0])
54
+ console.print(f"[info]Using most recent core dump: {core_file}[/info]")
55
+
56
+ core_path = Path(core_file)
57
+ try:
58
+ payload = json.loads(core_path.read_text(encoding="utf-8"))
59
+ except Exception as e:
60
+ raise click.UsageError(f"Failed to parse core dump: {e}")
61
+
62
+ # Determine repository
63
+ target_repo = repo or os.getenv("PDD_GITHUB_REPO", "promptdriven/pdd")
64
+
65
+ # For API submission, create a gist with all files
66
+ gist_url = None
67
+ if api:
68
+ console.print("[info]Attempting to create issue via GitHub API...[/info]")
69
+
70
+ github_config = _github_config(target_repo)
71
+ if not github_config:
72
+ console.print(
73
+ "[error]No GitHub authentication found. Please either:[/error]\n"
74
+ " 1. Install and authenticate with GitHub CLI: gh auth login\n"
75
+ " 2. Set GITHUB_TOKEN or GH_TOKEN environment variable\n"
76
+ " 3. Set PDD_GITHUB_TOKEN environment variable\n"
77
+ "\n"
78
+ "[info]Falling back to browser-based submission...[/info]"
79
+ )
80
+ api = False
81
+ else:
82
+ token, resolved_repo = github_config
83
+
84
+ # Create gist with all files
85
+ console.print(f"[info]Creating Gist with all files...[/info]")
86
+ gist_url = _create_gist_with_files(token, payload, core_path)
87
+
88
+ if gist_url:
89
+ console.print(f"[success]Gist created: {gist_url}[/success]")
90
+ else:
91
+ console.print("[warning]Failed to create Gist, including files in issue body...[/warning]")
92
+
93
+ # Build issue with gist link
94
+ title, body = _build_issue_markdown(
95
+ payload=payload,
96
+ description=description,
97
+ core_path=core_path,
98
+ replay_path=None,
99
+ attachments=[],
100
+ truncate_files=False,
101
+ gist_url=gist_url
102
+ )
103
+
104
+ console.print(f"[info]Creating issue in {resolved_repo}...[/info]")
105
+ issue_url = _post_issue_to_github(token, resolved_repo, title, body)
106
+ if issue_url:
107
+ console.print(f"[success]Issue created successfully: {issue_url}[/success]")
108
+ return
109
+ else:
110
+ console.print(
111
+ "[warning]Failed to create issue via API. Falling back to browser...[/warning]"
112
+ )
113
+ api = False
114
+
115
+ # Build issue content for browser mode (if not already built for API)
116
+ if not api:
117
+ # For browser-based submission, we'll truncate files to avoid URL length limits
118
+ title, body = _build_issue_markdown(
119
+ payload=payload,
120
+ description=description,
121
+ core_path=core_path,
122
+ replay_path=None,
123
+ attachments=[],
124
+ truncate_files=True # Truncate for browser
125
+ )
126
+
127
+ # Browser-based submission (default or fallback)
128
+ if not api:
129
+ # URL encode
130
+ encoded_title = urllib.parse.quote(title)
131
+ encoded_body = urllib.parse.quote(body)
132
+
133
+ url = f"https://github.com/{target_repo}/issues/new?title={encoded_title}&body={encoded_body}"
134
+
135
+ console.print(f"[info]Opening GitHub issue creation page for {target_repo}...[/info]")
136
+ console.print("[info]Note: File contents are truncated for browser submission. Use --api for full contents.[/info]")
137
+
138
+ if len(url) > 8000:
139
+ console.print("[warning]The issue body is large. Browser might truncate it.[/warning]")
140
+
141
+ webbrowser.open(url)
142
+
143
+ except Exception as e:
144
+ handle_error(e, "report-core", ctx.obj.get("quiet", False))