pdd-cli 0.0.56__py3-none-any.whl → 0.0.58__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/cli.py +244 -14
- pdd/code_generator_main.py +151 -3
- pdd/construct_paths.py +6 -2
- pdd/data/language_format.csv +2 -0
- pdd/preprocess.py +45 -7
- pdd/prompts/extract_promptline_LLM.prompt +17 -11
- pdd/prompts/trace_LLM.prompt +25 -22
- pdd/template_registry.py +264 -0
- pdd/templates/architecture/architecture_json.prompt +183 -0
- pdd/trace.py +49 -13
- pdd_cli-0.0.58.data/data/utils/pdd-setup.py +524 -0
- {pdd_cli-0.0.56.dist-info → pdd_cli-0.0.58.dist-info}/METADATA +3 -3
- {pdd_cli-0.0.56.dist-info → pdd_cli-0.0.58.dist-info}/RECORD +18 -15
- {pdd_cli-0.0.56.dist-info → pdd_cli-0.0.58.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.56.dist-info → pdd_cli-0.0.58.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.56.dist-info → pdd_cli-0.0.58.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.56.dist-info → pdd_cli-0.0.58.dist-info}/top_level.txt +0 -0
pdd/__init__.py
CHANGED
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
|
|
|
@@ -32,7 +34,12 @@ 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,
|
|
@@ -166,7 +255,6 @@ def cli(
|
|
|
166
255
|
):
|
|
167
256
|
"""
|
|
168
257
|
Main entry point for the PDD CLI. Handles global options and initializes context.
|
|
169
|
-
Supports multi-command chaining.
|
|
170
258
|
"""
|
|
171
259
|
# Ensure PDD_PATH is set before any commands run
|
|
172
260
|
get_local_pdd_path()
|
|
@@ -189,6 +277,13 @@ def cli(
|
|
|
189
277
|
if quiet:
|
|
190
278
|
ctx.obj["verbose"] = False
|
|
191
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
|
+
|
|
192
287
|
# If --list-contexts is provided, print and exit before any other actions
|
|
193
288
|
if list_contexts:
|
|
194
289
|
try:
|
|
@@ -227,17 +322,23 @@ def cli(
|
|
|
227
322
|
style="warning"
|
|
228
323
|
)
|
|
229
324
|
|
|
230
|
-
#
|
|
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 ---
|
|
231
333
|
@cli.result_callback()
|
|
232
334
|
@click.pass_context
|
|
233
335
|
def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float, str]]], **kwargs):
|
|
234
336
|
"""
|
|
235
|
-
Processes
|
|
236
|
-
|
|
337
|
+
Processes results returned by executed commands and prints a summary.
|
|
237
338
|
Receives a list of tuples, typically (result, cost, model_name),
|
|
238
339
|
or None from each command function.
|
|
239
340
|
"""
|
|
240
|
-
|
|
341
|
+
total_cost = 0.0
|
|
241
342
|
# Get Click's invoked subcommands attribute first
|
|
242
343
|
invoked_subcommands = getattr(ctx, 'invoked_subcommands', [])
|
|
243
344
|
# If Click didn't provide it (common in real runs), fall back to the list
|
|
@@ -251,11 +352,12 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
|
|
|
251
352
|
invoked_subcommands = ctx.obj.get('invoked_subcommands', []) or []
|
|
252
353
|
except Exception:
|
|
253
354
|
invoked_subcommands = []
|
|
355
|
+
results = results or []
|
|
254
356
|
num_commands = len(invoked_subcommands)
|
|
255
357
|
num_results = len(results) # Number of results actually received
|
|
256
358
|
|
|
257
359
|
if not ctx.obj.get("quiet"):
|
|
258
|
-
console.print("\n[info]--- Command
|
|
360
|
+
console.print("\n[info]--- Command Execution Summary ---[/info]")
|
|
259
361
|
|
|
260
362
|
for i, result_tuple in enumerate(results):
|
|
261
363
|
# Use the retrieved subcommand name (might be "Unknown Command X" in tests)
|
|
@@ -279,7 +381,7 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
|
|
|
279
381
|
# Check if the result is the expected tuple structure from @track_cost or preprocess success
|
|
280
382
|
elif isinstance(result_tuple, tuple) and len(result_tuple) == 3:
|
|
281
383
|
_result_data, cost, model_name = result_tuple
|
|
282
|
-
|
|
384
|
+
total_cost += cost
|
|
283
385
|
if not ctx.obj.get("quiet"):
|
|
284
386
|
# Special handling for preprocess success message (check actual command name)
|
|
285
387
|
actual_command_name = invoked_subcommands[i] if i < num_commands else None # Get actual name if possible
|
|
@@ -298,17 +400,107 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
|
|
|
298
400
|
if not ctx.obj.get("quiet"):
|
|
299
401
|
# Only print total cost if at least one command potentially contributed cost
|
|
300
402
|
if any(res is not None and isinstance(res, tuple) and len(res) == 3 for res in results):
|
|
301
|
-
console.print(f"[info]Total Estimated Cost
|
|
403
|
+
console.print(f"[info]Total Estimated Cost:[/info] ${total_cost:.6f}")
|
|
302
404
|
# Indicate if the chain might have been incomplete due to errors
|
|
303
405
|
if num_results < num_commands and not all(res is None for res in results): # Avoid printing if all failed
|
|
304
406
|
console.print("[warning]Note: Chain may have terminated early due to errors.[/warning]")
|
|
305
407
|
console.print("[info]-------------------------------------[/info]")
|
|
306
408
|
|
|
307
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
|
+
|
|
308
500
|
# --- Command Definitions ---
|
|
309
501
|
|
|
310
|
-
@cli.command("generate")
|
|
311
|
-
@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))
|
|
312
504
|
@click.option(
|
|
313
505
|
"--output",
|
|
314
506
|
type=click.Path(writable=True),
|
|
@@ -336,18 +528,40 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
|
|
|
336
528
|
multiple=True,
|
|
337
529
|
help="Set template variable (KEY=VALUE) or read KEY from env",
|
|
338
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
|
+
)
|
|
339
538
|
@click.pass_context
|
|
340
539
|
@track_cost
|
|
341
540
|
def generate(
|
|
342
541
|
ctx: click.Context,
|
|
343
|
-
prompt_file: str,
|
|
542
|
+
prompt_file: Optional[str],
|
|
344
543
|
output: Optional[str],
|
|
345
544
|
original_prompt_file_path: Optional[str],
|
|
346
545
|
incremental_flag: bool,
|
|
347
546
|
env_kv: Tuple[str, ...],
|
|
547
|
+
template_name: Optional[str],
|
|
348
548
|
) -> Optional[Tuple[str, float, str]]:
|
|
349
549
|
"""Generate code from a prompt file."""
|
|
350
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.")
|
|
351
565
|
# Parse -e/--env arguments into a dict
|
|
352
566
|
env_vars: Dict[str, str] = {}
|
|
353
567
|
import os as _os
|
|
@@ -368,7 +582,7 @@ def generate(
|
|
|
368
582
|
console.print(f"[warning]-e {key} not found in environment; skipping[/warning]")
|
|
369
583
|
generated_code, incremental, total_cost, model_name = code_generator_main(
|
|
370
584
|
ctx=ctx,
|
|
371
|
-
prompt_file=prompt_file,
|
|
585
|
+
prompt_file=prompt_file, # resolved template path or user path
|
|
372
586
|
output=output,
|
|
373
587
|
original_prompt_file_path=original_prompt_file_path,
|
|
374
588
|
force_incremental_flag=incremental_flag,
|
|
@@ -1330,6 +1544,22 @@ def install_completion_cmd(ctx: click.Context) -> None: # Return type remains No
|
|
|
1330
1544
|
# Do not return anything, as the callback expects None or a tuple
|
|
1331
1545
|
|
|
1332
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
|
+
|
|
1333
1563
|
# --- Entry Point ---
|
|
1334
1564
|
if __name__ == "__main__":
|
|
1335
1565
|
cli()
|
pdd/code_generator_main.py
CHANGED
|
@@ -79,6 +79,27 @@ def _expand_vars(text: str, vars_map: Optional[Dict[str, str]]) -> str:
|
|
|
79
79
|
return text
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def _parse_front_matter(text: str) -> Tuple[Optional[Dict[str, Any]], str]:
|
|
83
|
+
"""Parse YAML front matter at the start of a prompt and return (meta, body)."""
|
|
84
|
+
try:
|
|
85
|
+
if not text.startswith("---\n"):
|
|
86
|
+
return None, text
|
|
87
|
+
end_idx = text.find("\n---", 4)
|
|
88
|
+
if end_idx == -1:
|
|
89
|
+
return None, text
|
|
90
|
+
fm_body = text[4:end_idx]
|
|
91
|
+
rest = text[end_idx + len("\n---"):]
|
|
92
|
+
if rest.startswith("\n"):
|
|
93
|
+
rest = rest[1:]
|
|
94
|
+
import yaml as _yaml
|
|
95
|
+
meta = _yaml.safe_load(fm_body) or {}
|
|
96
|
+
if not isinstance(meta, dict):
|
|
97
|
+
meta = {}
|
|
98
|
+
return meta, rest
|
|
99
|
+
except Exception:
|
|
100
|
+
return None, text
|
|
101
|
+
|
|
102
|
+
|
|
82
103
|
def get_git_content_at_ref(file_path: str, git_ref: str = "HEAD") -> Optional[str]:
|
|
83
104
|
"""Gets the content of the file as it was at the specified git_ref."""
|
|
84
105
|
abs_file_path = pathlib.Path(file_path).resolve()
|
|
@@ -187,6 +208,10 @@ def code_generator_main(
|
|
|
187
208
|
context_override=ctx.obj.get('context')
|
|
188
209
|
)
|
|
189
210
|
prompt_content = input_strings["prompt_file"]
|
|
211
|
+
# Phase-2 templates: parse front matter metadata
|
|
212
|
+
fm_meta, body = _parse_front_matter(prompt_content)
|
|
213
|
+
if fm_meta:
|
|
214
|
+
prompt_content = body
|
|
190
215
|
# Determine final output path: if user passed a directory, use resolved file path
|
|
191
216
|
resolved_output = output_file_paths.get("output")
|
|
192
217
|
if output is None:
|
|
@@ -212,10 +237,108 @@ def code_generator_main(
|
|
|
212
237
|
existing_code_content: Optional[str] = None
|
|
213
238
|
original_prompt_content_for_incremental: Optional[str] = None
|
|
214
239
|
|
|
240
|
+
# Merge -e vars with front-matter defaults; validate required
|
|
241
|
+
if env_vars is None:
|
|
242
|
+
env_vars = {}
|
|
243
|
+
if fm_meta and isinstance(fm_meta.get("variables"), dict):
|
|
244
|
+
for k, spec in (fm_meta["variables"].items()):
|
|
245
|
+
if isinstance(spec, dict):
|
|
246
|
+
if k not in env_vars and "default" in spec:
|
|
247
|
+
env_vars[k] = str(spec["default"])
|
|
248
|
+
# if scalar default allowed, ignore for now
|
|
249
|
+
missing = [k for k, spec in fm_meta["variables"].items() if isinstance(spec, dict) and spec.get("required") and k not in env_vars]
|
|
250
|
+
if missing:
|
|
251
|
+
console.print(f"[error]Missing required variables: {', '.join(missing)}")
|
|
252
|
+
return "", False, 0.0, "error"
|
|
253
|
+
|
|
254
|
+
# Execute optional discovery from front matter to populate env_vars without overriding explicit -e values
|
|
255
|
+
def _run_discovery(discover_cfg: Dict[str, Any]) -> Dict[str, str]:
|
|
256
|
+
results: Dict[str, str] = {}
|
|
257
|
+
try:
|
|
258
|
+
if not discover_cfg:
|
|
259
|
+
return results
|
|
260
|
+
enabled = discover_cfg.get("enabled", False)
|
|
261
|
+
if not enabled:
|
|
262
|
+
return results
|
|
263
|
+
root = discover_cfg.get("root", ".")
|
|
264
|
+
patterns = discover_cfg.get("patterns", []) or []
|
|
265
|
+
exclude = discover_cfg.get("exclude", []) or []
|
|
266
|
+
max_per = int(discover_cfg.get("max_per_pattern", 0) or 0)
|
|
267
|
+
max_total = int(discover_cfg.get("max_total", 0) or 0)
|
|
268
|
+
root_path = pathlib.Path(root).resolve()
|
|
269
|
+
seen: List[str] = []
|
|
270
|
+
def _match_one(patterns_list: List[str]) -> List[str]:
|
|
271
|
+
matches: List[str] = []
|
|
272
|
+
for pat in patterns_list:
|
|
273
|
+
globbed = list(root_path.rglob(pat))
|
|
274
|
+
for p in globbed:
|
|
275
|
+
if any(p.match(ex) for ex in exclude):
|
|
276
|
+
continue
|
|
277
|
+
sp = str(p.resolve())
|
|
278
|
+
if sp not in matches:
|
|
279
|
+
matches.append(sp)
|
|
280
|
+
if max_per and len(matches) >= max_per:
|
|
281
|
+
matches = matches[:max_per]
|
|
282
|
+
break
|
|
283
|
+
return matches
|
|
284
|
+
# If a mapping 'set' is provided, compute per-variable results
|
|
285
|
+
set_map = discover_cfg.get("set") or {}
|
|
286
|
+
if isinstance(set_map, dict) and set_map:
|
|
287
|
+
for var_name, spec in set_map.items():
|
|
288
|
+
if var_name in env_vars:
|
|
289
|
+
continue # don't override explicit -e
|
|
290
|
+
v_patterns = spec.get("patterns", []) if isinstance(spec, dict) else []
|
|
291
|
+
v_exclude = spec.get("exclude", []) if isinstance(spec, dict) else []
|
|
292
|
+
save_exclude = exclude
|
|
293
|
+
try:
|
|
294
|
+
if v_exclude:
|
|
295
|
+
exclude = v_exclude
|
|
296
|
+
matches = _match_one(v_patterns or patterns)
|
|
297
|
+
finally:
|
|
298
|
+
exclude = save_exclude
|
|
299
|
+
if matches:
|
|
300
|
+
results[var_name] = ",".join(matches)
|
|
301
|
+
seen.extend(matches)
|
|
302
|
+
# Fallback: populate SCAN_FILES and SCAN metadata
|
|
303
|
+
if not results:
|
|
304
|
+
files = _match_one(patterns)
|
|
305
|
+
if max_total and len(files) > max_total:
|
|
306
|
+
files = files[:max_total]
|
|
307
|
+
if files:
|
|
308
|
+
results["SCAN_FILES"] = ",".join(files)
|
|
309
|
+
# Always set root/patterns helpers
|
|
310
|
+
if root:
|
|
311
|
+
results.setdefault("SCAN_ROOT", str(root_path))
|
|
312
|
+
if patterns:
|
|
313
|
+
results.setdefault("SCAN_PATTERNS", ",".join(patterns))
|
|
314
|
+
except Exception as e:
|
|
315
|
+
if verbose and not quiet:
|
|
316
|
+
console.print(f"[yellow]Discovery skipped due to error: {e}[/yellow]")
|
|
317
|
+
return results
|
|
318
|
+
|
|
319
|
+
if fm_meta and isinstance(fm_meta.get("discover"), dict):
|
|
320
|
+
discovered = _run_discovery(fm_meta.get("discover") or {})
|
|
321
|
+
for k, v in discovered.items():
|
|
322
|
+
if k not in env_vars:
|
|
323
|
+
env_vars[k] = v
|
|
324
|
+
|
|
215
325
|
# Expand variables in output path if provided
|
|
216
326
|
if output_path:
|
|
217
327
|
output_path = _expand_vars(output_path, env_vars)
|
|
218
328
|
|
|
329
|
+
# Honor front-matter output when CLI did not pass --output
|
|
330
|
+
if output is None and fm_meta and isinstance(fm_meta.get("output"), str):
|
|
331
|
+
try:
|
|
332
|
+
meta_out = _expand_vars(fm_meta["output"], env_vars)
|
|
333
|
+
if meta_out:
|
|
334
|
+
output_path = str(pathlib.Path(meta_out).resolve())
|
|
335
|
+
except Exception:
|
|
336
|
+
pass
|
|
337
|
+
|
|
338
|
+
# Honor front-matter language if provided (overrides detection for both local and cloud)
|
|
339
|
+
if fm_meta and isinstance(fm_meta.get("language"), str) and fm_meta.get("language"):
|
|
340
|
+
language = fm_meta.get("language")
|
|
341
|
+
|
|
219
342
|
if output_path and pathlib.Path(output_path).exists():
|
|
220
343
|
try:
|
|
221
344
|
existing_code_content = pathlib.Path(output_path).read_text(encoding="utf-8")
|
|
@@ -462,9 +585,11 @@ def code_generator_main(
|
|
|
462
585
|
local_prompt = pdd_preprocess(prompt_content, recursive=True, double_curly_brackets=False, exclude_keys=[])
|
|
463
586
|
local_prompt = _expand_vars(local_prompt, env_vars)
|
|
464
587
|
local_prompt = pdd_preprocess(local_prompt, recursive=False, double_curly_brackets=True, exclude_keys=[])
|
|
588
|
+
# Language already resolved (front matter overrides detection if present)
|
|
589
|
+
gen_language = language
|
|
465
590
|
generated_code_content, total_cost, model_name = local_code_generator_func(
|
|
466
591
|
prompt=local_prompt,
|
|
467
|
-
language=
|
|
592
|
+
language=gen_language,
|
|
468
593
|
strength=strength,
|
|
469
594
|
temperature=temperature,
|
|
470
595
|
time=time_budget,
|
|
@@ -476,14 +601,37 @@ def code_generator_main(
|
|
|
476
601
|
console.print(Panel(f"Full generation successful. Model: {model_name}, Cost: ${total_cost:.6f}", title="[green]Local Success[/green]", expand=False))
|
|
477
602
|
|
|
478
603
|
if generated_code_content is not None:
|
|
604
|
+
# Optional output_schema JSON validation before writing
|
|
605
|
+
try:
|
|
606
|
+
if fm_meta and isinstance(fm_meta.get("output_schema"), dict):
|
|
607
|
+
is_json_output = False
|
|
608
|
+
if isinstance(language, str) and str(language).lower().strip() == "json":
|
|
609
|
+
is_json_output = True
|
|
610
|
+
elif output_path and str(output_path).lower().endswith(".json"):
|
|
611
|
+
is_json_output = True
|
|
612
|
+
if is_json_output:
|
|
613
|
+
parsed = json.loads(generated_code_content)
|
|
614
|
+
try:
|
|
615
|
+
import jsonschema # type: ignore
|
|
616
|
+
jsonschema.validate(instance=parsed, schema=fm_meta.get("output_schema"))
|
|
617
|
+
except ModuleNotFoundError:
|
|
618
|
+
if verbose and not quiet:
|
|
619
|
+
console.print("[yellow]jsonschema not installed; skipping schema validation.[/yellow]")
|
|
620
|
+
except Exception as ve:
|
|
621
|
+
raise click.UsageError(f"Generated JSON does not match output_schema: {ve}")
|
|
622
|
+
except json.JSONDecodeError as jde:
|
|
623
|
+
raise click.UsageError(f"Generated output is not valid JSON: {jde}")
|
|
624
|
+
|
|
479
625
|
if output_path:
|
|
480
626
|
p_output = pathlib.Path(output_path)
|
|
481
627
|
p_output.parent.mkdir(parents=True, exist_ok=True)
|
|
482
628
|
p_output.write_text(generated_code_content, encoding="utf-8")
|
|
483
629
|
if verbose or not quiet:
|
|
484
630
|
console.print(f"Generated code saved to: [green]{p_output.resolve()}[/green]")
|
|
485
|
-
elif not quiet:
|
|
486
|
-
|
|
631
|
+
elif not quiet:
|
|
632
|
+
# No destination resolved; surface the generated code directly to the console.
|
|
633
|
+
console.print(Panel(Text(generated_code_content, overflow="fold"), title="[cyan]Generated Code[/cyan]", expand=False))
|
|
634
|
+
console.print("[yellow]No output path resolved; skipping file write and stdout print.[/yellow]")
|
|
487
635
|
else:
|
|
488
636
|
console.print("[red]Error: Code generation failed. No code was produced.[/red]")
|
|
489
637
|
return "", was_incremental_operation, total_cost, model_name or "error"
|
pdd/construct_paths.py
CHANGED
|
@@ -210,7 +210,9 @@ def _is_known_language(language_name: str) -> bool:
|
|
|
210
210
|
builtin_languages = {
|
|
211
211
|
'python', 'javascript', 'typescript', 'java', 'cpp', 'c', 'go', 'ruby', 'rust',
|
|
212
212
|
'kotlin', 'swift', 'csharp', 'php', 'scala', 'r', 'lua', 'perl', 'bash', 'shell',
|
|
213
|
-
'powershell', 'sql', 'prompt', 'html', 'css', 'makefile'
|
|
213
|
+
'powershell', 'sql', 'prompt', 'html', 'css', 'makefile',
|
|
214
|
+
# Common data and config formats for architecture prompts and configs
|
|
215
|
+
'json', 'jsonl', 'yaml', 'yml', 'toml', 'ini'
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
pdd_path_str = os.getenv('PDD_PATH')
|
|
@@ -636,7 +638,9 @@ def construct_paths(
|
|
|
636
638
|
'kotlin': '.kt', 'swift': '.swift', 'csharp': '.cs', 'php': '.php',
|
|
637
639
|
'scala': '.scala', 'r': '.r', 'lua': '.lua', 'perl': '.pl', 'bash': '.sh',
|
|
638
640
|
'shell': '.sh', 'powershell': '.ps1', 'sql': '.sql', 'html': '.html', 'css': '.css',
|
|
639
|
-
'prompt': '.prompt', 'makefile': ''
|
|
641
|
+
'prompt': '.prompt', 'makefile': '',
|
|
642
|
+
# Common data/config formats
|
|
643
|
+
'json': '.json', 'jsonl': '.jsonl', 'yaml': '.yaml', 'yml': '.yml', 'toml': '.toml', 'ini': '.ini'
|
|
640
644
|
}
|
|
641
645
|
file_extension = builtin_ext_map.get(language.lower(), f".{language.lower()}" if language else '')
|
|
642
646
|
|
pdd/data/language_format.csv
CHANGED