pdd-cli 0.0.55__py3-none-any.whl → 0.0.57__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 +1 -1
- pdd/auto_deps_main.py +3 -2
- pdd/bug_main.py +3 -2
- pdd/change_main.py +1 -0
- pdd/cli.py +287 -15
- pdd/cmd_test_main.py +1 -0
- pdd/code_generator_main.py +152 -3
- pdd/conflicts_main.py +3 -2
- pdd/construct_paths.py +23 -2
- pdd/context_generator_main.py +2 -1
- pdd/crash_main.py +3 -2
- pdd/data/language_format.csv +4 -1
- pdd/detect_change_main.py +3 -2
- pdd/fix_main.py +3 -2
- pdd/fix_verification_main.py +1 -0
- pdd/preprocess.py +45 -7
- pdd/preprocess_main.py +2 -1
- pdd/prompts/extract_promptline_LLM.prompt +17 -11
- pdd/prompts/trace_LLM.prompt +25 -22
- pdd/split_main.py +3 -2
- pdd/sync_determine_operation.py +28 -15
- pdd/sync_main.py +3 -1
- pdd/sync_orchestration.py +3 -2
- pdd/template_registry.py +264 -0
- pdd/templates/architecture/architecture_json.prompt +183 -0
- pdd/trace.py +49 -13
- pdd/trace_main.py +2 -1
- pdd/update_main.py +2 -1
- {pdd_cli-0.0.55.dist-info → pdd_cli-0.0.57.dist-info}/METADATA +3 -3
- {pdd_cli-0.0.55.dist-info → pdd_cli-0.0.57.dist-info}/RECORD +34 -32
- {pdd_cli-0.0.55.dist-info → pdd_cli-0.0.57.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.55.dist-info → pdd_cli-0.0.57.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.55.dist-info → pdd_cli-0.0.57.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.55.dist-info → pdd_cli-0.0.57.dist-info}/top_level.txt +0 -0
pdd/__init__.py
CHANGED
pdd/auto_deps_main.py
CHANGED
|
@@ -49,7 +49,8 @@ def auto_deps_main( # pylint: disable=too-many-arguments, too-many-locals
|
|
|
49
49
|
force=ctx.obj.get('force', False),
|
|
50
50
|
quiet=ctx.obj.get('quiet', False),
|
|
51
51
|
command="auto-deps",
|
|
52
|
-
command_options=command_options
|
|
52
|
+
command_options=command_options,
|
|
53
|
+
context_override=ctx.obj.get('context')
|
|
53
54
|
)
|
|
54
55
|
|
|
55
56
|
# Get the CSV file path
|
|
@@ -101,4 +102,4 @@ def auto_deps_main( # pylint: disable=too-many-arguments, too-many-locals
|
|
|
101
102
|
except Exception as exc:
|
|
102
103
|
if not ctx.obj.get('quiet', False):
|
|
103
104
|
rprint(f"[bold red]Error:[/bold red] {str(exc)}")
|
|
104
|
-
sys.exit(1)
|
|
105
|
+
sys.exit(1)
|
pdd/bug_main.py
CHANGED
|
@@ -50,7 +50,8 @@ def bug_main(
|
|
|
50
50
|
force=ctx.obj.get('force', False),
|
|
51
51
|
quiet=ctx.obj.get('quiet', False),
|
|
52
52
|
command="bug",
|
|
53
|
-
command_options=command_options
|
|
53
|
+
command_options=command_options,
|
|
54
|
+
context_override=ctx.obj.get('context')
|
|
54
55
|
)
|
|
55
56
|
|
|
56
57
|
# Use the language detected by construct_paths if none was explicitly provided
|
|
@@ -117,4 +118,4 @@ def bug_main(
|
|
|
117
118
|
except Exception as e:
|
|
118
119
|
if not ctx.obj.get('quiet', False):
|
|
119
120
|
rprint(f"[bold red]Error:[/bold red] {str(e)}")
|
|
120
|
-
sys.exit(1)
|
|
121
|
+
sys.exit(1)
|
pdd/change_main.py
CHANGED
|
@@ -203,6 +203,7 @@ def change_main(
|
|
|
203
203
|
quiet=quiet,
|
|
204
204
|
command="change",
|
|
205
205
|
command_options=command_options,
|
|
206
|
+
context_override=ctx.obj.get('context')
|
|
206
207
|
)
|
|
207
208
|
logger.debug("construct_paths returned:")
|
|
208
209
|
logger.debug(" input_strings keys: %s", list(input_strings.keys()))
|
pdd/cli.py
CHANGED
|
@@ -8,6 +8,8 @@ generating code, tests, fixing issues, and managing prompts.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
11
13
|
from typing import Any, Dict, List, Optional, Tuple
|
|
12
14
|
from pathlib import Path # Import Path
|
|
13
15
|
|
|
@@ -26,13 +28,18 @@ from .cmd_test_main import cmd_test_main
|
|
|
26
28
|
from .code_generator_main import code_generator_main
|
|
27
29
|
from .conflicts_main import conflicts_main
|
|
28
30
|
# Need to import construct_paths for tests patching pdd.cli.construct_paths
|
|
29
|
-
from .construct_paths import construct_paths
|
|
31
|
+
from .construct_paths import construct_paths, list_available_contexts
|
|
30
32
|
from .context_generator_main import context_generator_main
|
|
31
33
|
from .crash_main import crash_main
|
|
32
34
|
from .detect_change_main import detect_change_main
|
|
33
35
|
from .fix_main import fix_main
|
|
34
36
|
from .fix_verification_main import fix_verification_main
|
|
35
|
-
from .install_completion import
|
|
37
|
+
from .install_completion import (
|
|
38
|
+
install_completion,
|
|
39
|
+
get_local_pdd_path,
|
|
40
|
+
get_current_shell,
|
|
41
|
+
get_shell_rc_path,
|
|
42
|
+
)
|
|
36
43
|
from .preprocess_main import preprocess_main
|
|
37
44
|
from .pytest_output import run_pytest_and_capture_output
|
|
38
45
|
from .split_main import split_main
|
|
@@ -40,6 +47,7 @@ from .sync_main import sync_main
|
|
|
40
47
|
from .trace_main import trace_main
|
|
41
48
|
from .track_cost import track_cost
|
|
42
49
|
from .update_main import update_main
|
|
50
|
+
from . import template_registry
|
|
43
51
|
|
|
44
52
|
|
|
45
53
|
# --- Initialize Rich Console ---
|
|
@@ -75,8 +83,89 @@ def handle_error(exception: Exception, command_name: str, quiet: bool):
|
|
|
75
83
|
# Do NOT re-raise e here. Let the command function return None.
|
|
76
84
|
|
|
77
85
|
|
|
86
|
+
def _first_pending_command(ctx: click.Context) -> Optional[str]:
|
|
87
|
+
"""Return the first subcommand scheduled for this invocation."""
|
|
88
|
+
for arg in ctx.protected_args:
|
|
89
|
+
if not arg.startswith("-"):
|
|
90
|
+
return arg
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _api_env_exists() -> bool:
|
|
95
|
+
"""Check whether the ~/.pdd/api-env file exists."""
|
|
96
|
+
return (Path.home() / ".pdd" / "api-env").exists()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _completion_installed() -> bool:
|
|
100
|
+
"""Check if the shell RC file already sources the PDD completion script."""
|
|
101
|
+
shell = get_current_shell()
|
|
102
|
+
rc_path = get_shell_rc_path(shell) if shell else None
|
|
103
|
+
if not rc_path:
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
content = Path(rc_path).read_text(encoding="utf-8")
|
|
108
|
+
except (OSError, UnicodeDecodeError):
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
return "PDD CLI completion" in content or "pdd_completion" in content
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _project_has_local_configuration() -> bool:
|
|
115
|
+
"""Detect project-level env configuration that should suppress reminders."""
|
|
116
|
+
cwd = Path.cwd()
|
|
117
|
+
|
|
118
|
+
env_file = cwd / ".env"
|
|
119
|
+
if env_file.exists():
|
|
120
|
+
try:
|
|
121
|
+
env_content = env_file.read_text(encoding="utf-8")
|
|
122
|
+
except (OSError, UnicodeDecodeError):
|
|
123
|
+
env_content = ""
|
|
124
|
+
if any(token in env_content for token in ("OPENAI_API_KEY=", "GOOGLE_API_KEY=", "ANTHROPIC_API_KEY=")):
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
project_pdd_dir = cwd / ".pdd"
|
|
128
|
+
if project_pdd_dir.exists():
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _should_show_onboarding_reminder(ctx: click.Context) -> bool:
|
|
135
|
+
"""Determine whether to display the onboarding reminder banner."""
|
|
136
|
+
suppress = os.getenv("PDD_SUPPRESS_SETUP_REMINDER", "").lower()
|
|
137
|
+
if suppress in {"1", "true", "yes"}:
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
first_command = _first_pending_command(ctx)
|
|
141
|
+
if first_command == "setup":
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
if _api_env_exists():
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
if _project_has_local_configuration():
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
if _completion_installed():
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _run_setup_utility() -> None:
|
|
157
|
+
"""Execute the interactive setup utility script."""
|
|
158
|
+
setup_script = Path(__file__).resolve().parent.parent / "utils" / "pdd-setup.py"
|
|
159
|
+
if not setup_script.exists():
|
|
160
|
+
raise FileNotFoundError(f"Setup utility not found at {setup_script}")
|
|
161
|
+
|
|
162
|
+
result = subprocess.run([sys.executable, str(setup_script)])
|
|
163
|
+
if result.returncode not in (0, None):
|
|
164
|
+
raise RuntimeError(f"Setup utility exited with status {result.returncode}")
|
|
165
|
+
|
|
166
|
+
|
|
78
167
|
# --- Main CLI Group ---
|
|
79
|
-
@click.group(
|
|
168
|
+
@click.group(invoke_without_command=True, help="PDD (Prompt-Driven Development) Command Line Interface.")
|
|
80
169
|
@click.option(
|
|
81
170
|
"--force",
|
|
82
171
|
is_flag=True,
|
|
@@ -134,6 +223,20 @@ def handle_error(exception: Exception, command_name: str, quiet: bool):
|
|
|
134
223
|
default=False,
|
|
135
224
|
help="Run commands locally instead of in the cloud.",
|
|
136
225
|
)
|
|
226
|
+
@click.option(
|
|
227
|
+
"--context",
|
|
228
|
+
"context_override",
|
|
229
|
+
type=str,
|
|
230
|
+
default=None,
|
|
231
|
+
help="Override automatic context detection and use the specified .pddrc context.",
|
|
232
|
+
)
|
|
233
|
+
@click.option(
|
|
234
|
+
"--list-contexts",
|
|
235
|
+
"list_contexts",
|
|
236
|
+
is_flag=True,
|
|
237
|
+
default=False,
|
|
238
|
+
help="List available contexts from .pddrc and exit.",
|
|
239
|
+
)
|
|
137
240
|
@click.version_option(version=__version__, package_name="pdd-cli")
|
|
138
241
|
@click.pass_context
|
|
139
242
|
def cli(
|
|
@@ -147,10 +250,11 @@ def cli(
|
|
|
147
250
|
review_examples: bool,
|
|
148
251
|
local: bool,
|
|
149
252
|
time: Optional[float], # Type hint is Optional[float]
|
|
253
|
+
context_override: Optional[str],
|
|
254
|
+
list_contexts: bool,
|
|
150
255
|
):
|
|
151
256
|
"""
|
|
152
257
|
Main entry point for the PDD CLI. Handles global options and initializes context.
|
|
153
|
-
Supports multi-command chaining.
|
|
154
258
|
"""
|
|
155
259
|
# Ensure PDD_PATH is set before any commands run
|
|
156
260
|
get_local_pdd_path()
|
|
@@ -166,11 +270,44 @@ def cli(
|
|
|
166
270
|
ctx.obj["local"] = local
|
|
167
271
|
# Use DEFAULT_TIME if time is not provided
|
|
168
272
|
ctx.obj["time"] = time if time is not None else DEFAULT_TIME
|
|
273
|
+
# Persist context override for downstream calls
|
|
274
|
+
ctx.obj["context"] = context_override
|
|
169
275
|
|
|
170
276
|
# Suppress verbose if quiet is enabled
|
|
171
277
|
if quiet:
|
|
172
278
|
ctx.obj["verbose"] = False
|
|
173
279
|
|
|
280
|
+
# Warn users who have not completed interactive setup unless they are running it now
|
|
281
|
+
if _should_show_onboarding_reminder(ctx):
|
|
282
|
+
console.print(
|
|
283
|
+
"[warning]Complete onboarding with `pdd setup` to install tab completion and configure API keys.[/warning]"
|
|
284
|
+
)
|
|
285
|
+
ctx.obj["reminder_shown"] = True
|
|
286
|
+
|
|
287
|
+
# If --list-contexts is provided, print and exit before any other actions
|
|
288
|
+
if list_contexts:
|
|
289
|
+
try:
|
|
290
|
+
names = list_available_contexts()
|
|
291
|
+
except Exception as exc:
|
|
292
|
+
# Surface config errors as usage errors
|
|
293
|
+
raise click.UsageError(f"Failed to load .pddrc: {exc}")
|
|
294
|
+
# Print one per line; avoid Rich formatting for portability
|
|
295
|
+
for name in names:
|
|
296
|
+
click.echo(name)
|
|
297
|
+
ctx.exit(0)
|
|
298
|
+
|
|
299
|
+
# Optional early validation for --context
|
|
300
|
+
if context_override:
|
|
301
|
+
try:
|
|
302
|
+
names = list_available_contexts()
|
|
303
|
+
except Exception as exc:
|
|
304
|
+
# If .pddrc is malformed, propagate as usage error
|
|
305
|
+
raise click.UsageError(f"Failed to load .pddrc: {exc}")
|
|
306
|
+
if context_override not in names:
|
|
307
|
+
raise click.UsageError(
|
|
308
|
+
f"Unknown context '{context_override}'. Available contexts: {', '.join(names)}"
|
|
309
|
+
)
|
|
310
|
+
|
|
174
311
|
# Perform auto-update check unless disabled
|
|
175
312
|
if os.getenv("PDD_AUTO_UPDATE", "true").lower() != "false":
|
|
176
313
|
try:
|
|
@@ -185,17 +322,23 @@ def cli(
|
|
|
185
322
|
style="warning"
|
|
186
323
|
)
|
|
187
324
|
|
|
188
|
-
#
|
|
325
|
+
# If no subcommands were provided, show help and exit gracefully
|
|
326
|
+
if ctx.invoked_subcommand is None and not ctx.protected_args:
|
|
327
|
+
if not quiet:
|
|
328
|
+
console.print("[info]Run `pdd --help` for usage or `pdd setup` to finish onboarding.[/info]")
|
|
329
|
+
click.echo(ctx.get_help())
|
|
330
|
+
ctx.exit(0)
|
|
331
|
+
|
|
332
|
+
# --- Result Callback for Command Execution Summary ---
|
|
189
333
|
@cli.result_callback()
|
|
190
334
|
@click.pass_context
|
|
191
335
|
def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float, str]]], **kwargs):
|
|
192
336
|
"""
|
|
193
|
-
Processes
|
|
194
|
-
|
|
337
|
+
Processes results returned by executed commands and prints a summary.
|
|
195
338
|
Receives a list of tuples, typically (result, cost, model_name),
|
|
196
339
|
or None from each command function.
|
|
197
340
|
"""
|
|
198
|
-
|
|
341
|
+
total_cost = 0.0
|
|
199
342
|
# Get Click's invoked subcommands attribute first
|
|
200
343
|
invoked_subcommands = getattr(ctx, 'invoked_subcommands', [])
|
|
201
344
|
# If Click didn't provide it (common in real runs), fall back to the list
|
|
@@ -209,11 +352,12 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
|
|
|
209
352
|
invoked_subcommands = ctx.obj.get('invoked_subcommands', []) or []
|
|
210
353
|
except Exception:
|
|
211
354
|
invoked_subcommands = []
|
|
355
|
+
results = results or []
|
|
212
356
|
num_commands = len(invoked_subcommands)
|
|
213
357
|
num_results = len(results) # Number of results actually received
|
|
214
358
|
|
|
215
359
|
if not ctx.obj.get("quiet"):
|
|
216
|
-
console.print("\n[info]--- Command
|
|
360
|
+
console.print("\n[info]--- Command Execution Summary ---[/info]")
|
|
217
361
|
|
|
218
362
|
for i, result_tuple in enumerate(results):
|
|
219
363
|
# Use the retrieved subcommand name (might be "Unknown Command X" in tests)
|
|
@@ -237,7 +381,7 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
|
|
|
237
381
|
# Check if the result is the expected tuple structure from @track_cost or preprocess success
|
|
238
382
|
elif isinstance(result_tuple, tuple) and len(result_tuple) == 3:
|
|
239
383
|
_result_data, cost, model_name = result_tuple
|
|
240
|
-
|
|
384
|
+
total_cost += cost
|
|
241
385
|
if not ctx.obj.get("quiet"):
|
|
242
386
|
# Special handling for preprocess success message (check actual command name)
|
|
243
387
|
actual_command_name = invoked_subcommands[i] if i < num_commands else None # Get actual name if possible
|
|
@@ -256,17 +400,107 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
|
|
|
256
400
|
if not ctx.obj.get("quiet"):
|
|
257
401
|
# Only print total cost if at least one command potentially contributed cost
|
|
258
402
|
if any(res is not None and isinstance(res, tuple) and len(res) == 3 for res in results):
|
|
259
|
-
console.print(f"[info]Total Estimated Cost
|
|
403
|
+
console.print(f"[info]Total Estimated Cost:[/info] ${total_cost:.6f}")
|
|
260
404
|
# Indicate if the chain might have been incomplete due to errors
|
|
261
405
|
if num_results < num_commands and not all(res is None for res in results): # Avoid printing if all failed
|
|
262
406
|
console.print("[warning]Note: Chain may have terminated early due to errors.[/warning]")
|
|
263
407
|
console.print("[info]-------------------------------------[/info]")
|
|
264
408
|
|
|
265
409
|
|
|
410
|
+
# --- Templates Command Group ---
|
|
411
|
+
@click.group(name="templates")
|
|
412
|
+
def templates_group():
|
|
413
|
+
"""Manage packaged and project templates."""
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
@templates_group.command("list")
|
|
418
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
419
|
+
@click.option("--filter", "filter_tag", type=str, default=None, help="Filter by tag")
|
|
420
|
+
def templates_list(as_json: bool, filter_tag: Optional[str]):
|
|
421
|
+
try:
|
|
422
|
+
items = template_registry.list_templates(filter_tag)
|
|
423
|
+
if as_json:
|
|
424
|
+
import json as _json
|
|
425
|
+
click.echo(_json.dumps(items, indent=2))
|
|
426
|
+
else:
|
|
427
|
+
if not items:
|
|
428
|
+
console.print("[info]No templates found.[/info]")
|
|
429
|
+
return
|
|
430
|
+
console.print("[info]Available Templates:[/info]")
|
|
431
|
+
for it in items:
|
|
432
|
+
tags = ", ".join(it.get("tags", []))
|
|
433
|
+
console.print(f"- [bold]{it['name']}[/bold] ({it.get('version','')}) — {it.get('description','')} [dim]{tags}[/dim]")
|
|
434
|
+
except Exception as e:
|
|
435
|
+
handle_error(e, "templates list", False)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@templates_group.command("show")
|
|
439
|
+
@click.argument("name", type=str)
|
|
440
|
+
def templates_show(name: str):
|
|
441
|
+
try:
|
|
442
|
+
data = template_registry.show_template(name)
|
|
443
|
+
summary = data.get("summary", {})
|
|
444
|
+
console.print(f"[bold]{summary.get('name','')}[/bold] — {summary.get('description','')}")
|
|
445
|
+
console.print(f"Version: {summary.get('version','')} Tags: {', '.join(summary.get('tags',[]))}")
|
|
446
|
+
console.print(f"Language: {summary.get('language','')} Output: {summary.get('output','')}")
|
|
447
|
+
console.print(f"Path: {summary.get('path','')}")
|
|
448
|
+
if data.get("variables"):
|
|
449
|
+
console.print("\n[info]Variables:[/info]")
|
|
450
|
+
for k, v in data["variables"].items():
|
|
451
|
+
console.print(f"- {k}: {v}")
|
|
452
|
+
if data.get("usage"):
|
|
453
|
+
console.print("\n[info]Usage:[/info]")
|
|
454
|
+
console.print(data["usage"]) # raw; CLI may format later
|
|
455
|
+
if data.get("discover"):
|
|
456
|
+
console.print("\n[info]Discover:[/info]")
|
|
457
|
+
console.print(data["discover"]) # raw dict
|
|
458
|
+
if data.get("output_schema"):
|
|
459
|
+
console.print("\n[info]Output Schema:[/info]")
|
|
460
|
+
try:
|
|
461
|
+
import json as _json
|
|
462
|
+
console.print(_json.dumps(data["output_schema"], indent=2))
|
|
463
|
+
except Exception:
|
|
464
|
+
console.print(str(data["output_schema"]))
|
|
465
|
+
if data.get("notes"):
|
|
466
|
+
console.print("\n[info]Notes:[/info]")
|
|
467
|
+
console.print(data["notes"]) # plain text
|
|
468
|
+
except Exception as e:
|
|
469
|
+
handle_error(e, "templates show", False)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@templates_group.command("copy")
|
|
473
|
+
@click.argument("name", type=str)
|
|
474
|
+
@click.option("--to", "dest_dir", type=click.Path(file_okay=False), required=True)
|
|
475
|
+
def templates_copy(name: str, dest_dir: str):
|
|
476
|
+
try:
|
|
477
|
+
dest = template_registry.copy_template(name, dest_dir)
|
|
478
|
+
console.print(f"[success]Copied to:[/success] {dest}")
|
|
479
|
+
except Exception as e:
|
|
480
|
+
handle_error(e, "templates copy", False)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# Register the templates group with the main CLI.
|
|
484
|
+
# Click disallows attaching a MultiCommand to a chained group via add_command,
|
|
485
|
+
# so insert directly into the commands mapping after cli is defined.
|
|
486
|
+
cli.commands["templates"] = templates_group
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
# --- Custom Click Command Classes ---
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class GenerateCommand(click.Command):
|
|
493
|
+
"""Ensure help shows PROMPT_FILE as required even when validated at runtime."""
|
|
494
|
+
|
|
495
|
+
def collect_usage_pieces(self, ctx: click.Context) -> List[str]:
|
|
496
|
+
pieces = super().collect_usage_pieces(ctx)
|
|
497
|
+
return ["PROMPT_FILE" if piece == "[PROMPT_FILE]" else piece for piece in pieces]
|
|
498
|
+
|
|
499
|
+
|
|
266
500
|
# --- Command Definitions ---
|
|
267
501
|
|
|
268
|
-
@cli.command("generate")
|
|
269
|
-
@click.argument("prompt_file", type=click.Path(exists=True, dir_okay=False))
|
|
502
|
+
@cli.command("generate", cls=GenerateCommand)
|
|
503
|
+
@click.argument("prompt_file", required=False, type=click.Path(exists=True, dir_okay=False))
|
|
270
504
|
@click.option(
|
|
271
505
|
"--output",
|
|
272
506
|
type=click.Path(writable=True),
|
|
@@ -294,18 +528,40 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
|
|
|
294
528
|
multiple=True,
|
|
295
529
|
help="Set template variable (KEY=VALUE) or read KEY from env",
|
|
296
530
|
)
|
|
531
|
+
@click.option(
|
|
532
|
+
"--template",
|
|
533
|
+
"template_name",
|
|
534
|
+
type=str,
|
|
535
|
+
default=None,
|
|
536
|
+
help="Use a packaged/project template by name (e.g., architecture/architecture_json)",
|
|
537
|
+
)
|
|
297
538
|
@click.pass_context
|
|
298
539
|
@track_cost
|
|
299
540
|
def generate(
|
|
300
541
|
ctx: click.Context,
|
|
301
|
-
prompt_file: str,
|
|
542
|
+
prompt_file: Optional[str],
|
|
302
543
|
output: Optional[str],
|
|
303
544
|
original_prompt_file_path: Optional[str],
|
|
304
545
|
incremental_flag: bool,
|
|
305
546
|
env_kv: Tuple[str, ...],
|
|
547
|
+
template_name: Optional[str],
|
|
306
548
|
) -> Optional[Tuple[str, float, str]]:
|
|
307
549
|
"""Generate code from a prompt file."""
|
|
308
550
|
try:
|
|
551
|
+
# Resolve template to a prompt path when requested
|
|
552
|
+
if template_name and prompt_file:
|
|
553
|
+
raise click.UsageError("Provide either --template or a PROMPT_FILE path, not both.")
|
|
554
|
+
if template_name:
|
|
555
|
+
try:
|
|
556
|
+
from . import template_registry as _tpl
|
|
557
|
+
meta = _tpl.load_template(template_name)
|
|
558
|
+
prompt_file = meta.get("path")
|
|
559
|
+
if not prompt_file:
|
|
560
|
+
raise click.UsageError(f"Template '{template_name}' did not return a valid path")
|
|
561
|
+
except Exception as e:
|
|
562
|
+
raise click.UsageError(f"Failed to load template '{template_name}': {e}")
|
|
563
|
+
if not template_name and not prompt_file:
|
|
564
|
+
raise click.UsageError("Missing PROMPT_FILE. To use a template, pass --template NAME instead.")
|
|
309
565
|
# Parse -e/--env arguments into a dict
|
|
310
566
|
env_vars: Dict[str, str] = {}
|
|
311
567
|
import os as _os
|
|
@@ -326,7 +582,7 @@ def generate(
|
|
|
326
582
|
console.print(f"[warning]-e {key} not found in environment; skipping[/warning]")
|
|
327
583
|
generated_code, incremental, total_cost, model_name = code_generator_main(
|
|
328
584
|
ctx=ctx,
|
|
329
|
-
prompt_file=prompt_file,
|
|
585
|
+
prompt_file=prompt_file, # resolved template path or user path
|
|
330
586
|
output=output,
|
|
331
587
|
original_prompt_file_path=original_prompt_file_path,
|
|
332
588
|
force_incremental_flag=incremental_flag,
|
|
@@ -1288,6 +1544,22 @@ def install_completion_cmd(ctx: click.Context) -> None: # Return type remains No
|
|
|
1288
1544
|
# Do not return anything, as the callback expects None or a tuple
|
|
1289
1545
|
|
|
1290
1546
|
|
|
1547
|
+
@cli.command("setup")
|
|
1548
|
+
@click.pass_context
|
|
1549
|
+
def setup_cmd(ctx: click.Context) -> None:
|
|
1550
|
+
"""Run the interactive setup utility and install shell completion."""
|
|
1551
|
+
command_name = "setup"
|
|
1552
|
+
quiet_mode = ctx.obj.get("quiet", False)
|
|
1553
|
+
|
|
1554
|
+
try:
|
|
1555
|
+
install_completion(quiet=quiet_mode)
|
|
1556
|
+
_run_setup_utility()
|
|
1557
|
+
if not quiet_mode:
|
|
1558
|
+
console.print("[success]Setup completed. Restart your shell or source your RC file to apply changes.[/success]")
|
|
1559
|
+
except Exception as exc:
|
|
1560
|
+
handle_error(exc, command_name, quiet_mode)
|
|
1561
|
+
|
|
1562
|
+
|
|
1291
1563
|
# --- Entry Point ---
|
|
1292
1564
|
if __name__ == "__main__":
|
|
1293
1565
|
cli()
|
pdd/cmd_test_main.py
CHANGED