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.
- pdd/__init__.py +4 -4
- pdd/agentic_common.py +863 -0
- pdd/agentic_crash.py +534 -0
- pdd/agentic_fix.py +1179 -0
- pdd/agentic_langtest.py +162 -0
- pdd/agentic_update.py +370 -0
- pdd/agentic_verify.py +183 -0
- pdd/auto_deps_main.py +15 -5
- pdd/auto_include.py +63 -5
- pdd/bug_main.py +3 -2
- pdd/bug_to_unit_test.py +2 -0
- pdd/change_main.py +11 -4
- pdd/cli.py +22 -1181
- pdd/cmd_test_main.py +73 -21
- pdd/code_generator.py +58 -18
- pdd/code_generator_main.py +672 -25
- pdd/commands/__init__.py +42 -0
- pdd/commands/analysis.py +248 -0
- pdd/commands/fix.py +140 -0
- pdd/commands/generate.py +257 -0
- pdd/commands/maintenance.py +174 -0
- pdd/commands/misc.py +79 -0
- pdd/commands/modify.py +230 -0
- pdd/commands/report.py +144 -0
- pdd/commands/templates.py +215 -0
- pdd/commands/utility.py +110 -0
- pdd/config_resolution.py +58 -0
- pdd/conflicts_main.py +8 -3
- pdd/construct_paths.py +258 -82
- pdd/context_generator.py +10 -2
- pdd/context_generator_main.py +113 -11
- pdd/continue_generation.py +47 -7
- pdd/core/__init__.py +0 -0
- pdd/core/cli.py +503 -0
- pdd/core/dump.py +554 -0
- pdd/core/errors.py +63 -0
- pdd/core/utils.py +90 -0
- pdd/crash_main.py +44 -11
- pdd/data/language_format.csv +71 -63
- pdd/data/llm_model.csv +20 -18
- pdd/detect_change_main.py +5 -4
- pdd/fix_code_loop.py +330 -76
- pdd/fix_error_loop.py +207 -61
- pdd/fix_errors_from_unit_tests.py +4 -3
- pdd/fix_main.py +75 -18
- pdd/fix_verification_errors.py +12 -100
- pdd/fix_verification_errors_loop.py +306 -272
- pdd/fix_verification_main.py +28 -9
- pdd/generate_output_paths.py +93 -10
- pdd/generate_test.py +16 -5
- pdd/get_jwt_token.py +9 -2
- pdd/get_run_command.py +73 -0
- pdd/get_test_command.py +68 -0
- pdd/git_update.py +70 -19
- pdd/incremental_code_generator.py +2 -2
- pdd/insert_includes.py +11 -3
- pdd/llm_invoke.py +1269 -103
- pdd/load_prompt_template.py +36 -10
- pdd/pdd_completion.fish +25 -2
- pdd/pdd_completion.sh +30 -4
- pdd/pdd_completion.zsh +79 -4
- pdd/postprocess.py +10 -3
- pdd/preprocess.py +228 -15
- pdd/preprocess_main.py +8 -5
- pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
- pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
- pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
- pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
- pdd/prompts/agentic_update_LLM.prompt +1071 -0
- pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
- pdd/prompts/auto_include_LLM.prompt +100 -905
- pdd/prompts/detect_change_LLM.prompt +122 -20
- pdd/prompts/example_generator_LLM.prompt +22 -1
- pdd/prompts/extract_code_LLM.prompt +5 -1
- pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
- pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
- pdd/prompts/extract_promptline_LLM.prompt +17 -11
- pdd/prompts/find_verification_errors_LLM.prompt +6 -0
- pdd/prompts/fix_code_module_errors_LLM.prompt +4 -2
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +8 -0
- pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
- pdd/prompts/generate_test_LLM.prompt +21 -6
- pdd/prompts/increase_tests_LLM.prompt +1 -5
- pdd/prompts/insert_includes_LLM.prompt +228 -108
- pdd/prompts/trace_LLM.prompt +25 -22
- pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
- pdd/prompts/update_prompt_LLM.prompt +22 -1
- pdd/pytest_output.py +127 -12
- pdd/render_mermaid.py +236 -0
- pdd/setup_tool.py +648 -0
- pdd/simple_math.py +2 -0
- pdd/split_main.py +3 -2
- pdd/summarize_directory.py +49 -6
- pdd/sync_determine_operation.py +543 -98
- pdd/sync_main.py +81 -31
- pdd/sync_orchestration.py +1334 -751
- pdd/sync_tui.py +848 -0
- pdd/template_registry.py +264 -0
- pdd/templates/architecture/architecture_json.prompt +242 -0
- pdd/templates/generic/generate_prompt.prompt +174 -0
- pdd/trace.py +168 -12
- pdd/trace_main.py +4 -3
- pdd/track_cost.py +151 -61
- pdd/unfinished_prompt.py +49 -3
- pdd/update_main.py +549 -67
- pdd/update_model_costs.py +2 -2
- pdd/update_prompt.py +19 -4
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +19 -6
- pdd_cli-0.0.90.dist-info/RECORD +153 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
- pdd_cli-0.0.45.dist-info/RECORD +0 -116
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.45.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))
|