Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Repository Guidelines

## Project Structure & Module Organization
- `cli/go/`: Go CLI sources in `cmd/prompd`; keep `_test.go` beside implementations.
- `cli/python/`: Installable Python package with `.pdpkg` fixtures and pytest suites in `cli/python/tests`.
- `cli/npm/`: TypeScript CLI in `src/`, compiled to `dist/`, executable shim under `bin/`.
- `examples/` + root `.prmd` bundles: canonical prompt references for integration and packaging checks.
- `vscode-extension/`: VS Code tooling aligned with prompt bundles; update alongside CLI changes.

## Build, Test, and Development Commands
- `cd cli/go && go build -o prompd ./cmd/prompd`: build Go binary for local/manual testing.
- `./build.sh`: emit cross-platform artifacts to `dist/`.
- `pip install -e "cli/python/[dev]"`: install editable Python CLI with dev tooling.
- `python -m pytest cli/python/tests`: run Python suite; append `--cov=prompd` after core edits.
- `cd cli/npm && npm install && npm run build`: compile Node CLI; follow with `npm test` for Jest specs.

## Coding Style & Naming Conventions
- Python: 4-space indent, run `black --line-length 100` and `ruff check`; modules stay snake_case.
- Go: format with `gofmt`, keep package names concise, commands under `cmd/prompd`.
- TypeScript: camelCase functions, PascalCase classes, document exports; keep `dist/` lint-clean.
- Prompt bundles: kebab-case filenames ending in `.prmd` or `.pdpkg`.

## Testing Guidelines
- Co-locate tests (`test_*.py`, `_test.go`, `*.test.ts`) with targets and fixtures.
- `python cli/python/run_tests.py`: smoke validates CLI interactions.
- `go test ./...` plus `go vet ./...`: mandatory before shipping parser/registry changes.
- `npm test -- --coverage`: capture metrics after TypeScript updates.
- `python test_all_production.py` and `validate_production_prompts.py`: required before packaging prompts.

## Commit & Pull Request Guidelines
- Write imperative, scoped commit subjects (e.g., “Add prompt validation smoke tests”).
- In PRs, spell out affected CLIs, manual test commands, linked issues, and UX diffs.
- Update docs, examples, or bundles with code changes; call out new binaries or scripts.
- Verify all relevant build/test commands locally and flag follow-up work when needed.
2,869 changes: 64 additions & 2,805 deletions cli/python/prompd/cli.py

Large diffs are not rendered by default.

81 changes: 81 additions & 0 deletions cli/python/prompd/commands/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Cache management commands for Prompd."""
from __future__ import annotations

from typing import Optional

import click

from prompd.commands.common import console


@click.group(name="cache")
def cache():
"""Package cache management commands."""
pass


@cache.command("list")
@click.option("--global-only", is_flag=True, help="Show only global cache")
@click.option("--local-only", is_flag=True, help="Show only local cache")
def list_cache(global_only: bool, local_only: bool):
"""List cached packages."""
try:
from prompd.package_resolver import PackageResolver

resolver = PackageResolver()

if local_only:
packages_dict = {"local": resolver.project_cache.list_packages(), "global": []}
elif global_only:
packages_dict = {"local": [], "global": resolver.global_cache.list_packages()}
else:
packages_dict = resolver.list_cached_packages()

if packages_dict["local"]:
console.print("\n[bold cyan]Local Project Cache (./.prompd/cache/):[/bold cyan]")
for pkg in packages_dict["local"]:
console.print(f" {pkg.to_string()}")

if packages_dict["global"]:
console.print("\n[bold green]Global Cache (~/.cache/prompd/):[/bold green]")
for pkg in packages_dict["global"]:
console.print(f" {pkg.to_string()}")

if not packages_dict["local"] and not packages_dict["global"]:
console.print("[yellow]No cached packages found[/yellow]")
except Exception as exc:
console.print(f"[red]Failed to list cache:[/red] {exc}")
raise SystemExit(1)


@cache.command("clear")
@click.option("--global", "clear_global", is_flag=True, help="Clear global cache")
@click.option("--local", "clear_local", is_flag=True, help="Clear local cache")
@click.option("--all", "clear_all", is_flag=True, help="Clear both caches")
def clear_cache(clear_global: bool, clear_local: bool, clear_all: bool):
"""Clear package cache."""
try:
from prompd.package_resolver import PackageResolver

resolver = PackageResolver()

if clear_all:
clear_global = clear_local = True
elif not clear_global and not clear_local:
clear_local = True

resolver.clear_cache(clear_global=clear_global, clear_local=clear_local)

cleared = []
if clear_local:
cleared.append("local")
if clear_global:
cleared.append("global")

console.print(f"[green]Success:[/green] Cleared {' and '.join(cleared)} cache(s)")
except Exception as exc:
console.print(f"[red]Failed to clear cache:[/red] {exc}")
raise SystemExit(1)


__all__ = ["cache"]
26 changes: 26 additions & 0 deletions cli/python/prompd/commands/chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Chat-focused shell command for the Prompd CLI."""
from __future__ import annotations

import click

from prompd.commands.common import console


@click.command(name="chat")
def chat_command():
"""Start the Prompd shell directly in chat mode. [BETA FEATURE]"""
try:
from prompd.shell import PrompdShell

shell = PrompdShell()
shell.enter_chat_mode()
shell.start()
except Exception as exc:
try:
console.print(f"[red]Error launching chat:[/red] {exc}")
except Exception:
print(f"Error launching chat: {exc}")
raise SystemExit(1)


__all__ = ["chat_command"]
15 changes: 15 additions & 0 deletions cli/python/prompd/commands/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Shared helpers used across CLI command modules."""
from __future__ import annotations

import re
from prompd.console import console

SEMVER_PATTERN = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")


def is_valid_semver(version: str) -> bool:
"""Return True if the provided version string matches semantic versioning."""
return bool(SEMVER_PATTERN.match(version))


__all__ = ["console", "is_valid_semver"]
185 changes: 185 additions & 0 deletions cli/python/prompd/commands/compile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
"""Implementation of the `prompd compile` command."""
from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any, Dict, Optional, Tuple

import click

from prompd.commands.common import console


@click.command(name="compile", context_settings=dict(ignore_unknown_options=True, allow_extra_args=True))
@click.argument("source", type=str)
@click.option(
"--to",
"output_format",
default="markdown",
help="Output format (markdown | provider-json [openai|anthropic] | provider-json:openai)",
)
@click.option("--to-markdown", is_flag=True, help="Shorthand for --to markdown")
@click.option(
"--to-provider-json",
type=click.Choice(["openai", "anthropic"]),
help="Shorthand for --to provider-json <provider>",
)
@click.option("-p", "--param", multiple=True, help="Parameter in format key=value (repeat for multiple)")
@click.option(
"-f",
"--params-file",
type=click.Path(exists=True, path_type=Path),
multiple=True,
help="Load parameters from JSON file (repeatable)",
)
@click.option("-o", "--output", type=click.Path(), help="Write compiled output to file")
@click.option("-v", "--verbose", is_flag=True, help="Verbose output")
@click.pass_context
def compile_command(
ctx,
source: str,
output_format: str,
to_markdown: bool,
to_provider_json: Optional[str],
param: Tuple[str, ...],
params_file: Tuple[Path, ...],
output: Optional[str],
verbose: bool,
):
"""Compile a .prmd file or package reference to a target format."""
try:
source_path = Path(source)
package_pattern = r"^(@[\w.-]+/[\w.-]+|[\w.-]+)@([\w.-]+)/(.+\.prmd)$"
match = re.match(package_pattern, source)

if match:
package_ref = f"{match.group(1)}@{match.group(2)}"
file_path_in_package = match.group(3)

if verbose:
console.print(f"[cyan]Resolving package:[/cyan] {package_ref}")
console.print(f"[cyan]File path:[/cyan] {file_path_in_package}")

from prompd.package_resolver import PackageResolver

resolver = PackageResolver()
try:
package_path = resolver.resolve_package(package_ref)
source_path = package_path / file_path_in_package

if not source_path.exists():
console.print(f"[red]File not found in package:[/red] {file_path_in_package}")
console.print(f"[yellow]Package location:[/yellow] {package_path}")
raise SystemExit(1)

if verbose:
console.print(f"[green]Resolved to:[/green] {source_path}")
except Exception as exc:
console.print(f"[red]Failed to resolve package:[/red] {exc}")
raise SystemExit(1)
elif not source_path.exists():
if "@" in source and "/" not in source.split("@")[-1]:
from prompd.package_resolver import PackageResolver

resolver = PackageResolver()
try:
package_path = resolver.resolve_package(source)
manifest_file = package_path / "manifest.json"
if manifest_file.exists():
with open(manifest_file, "r", encoding="utf-8") as mf:
manifest = json.load(mf)
main_file = manifest.get("main")
if main_file:
source_path = package_path / main_file
if verbose:
console.print(f"[green]Using main file:[/green] {main_file}")
except Exception:
pass

if not source_path.exists():
console.print(f"[red]File not found:[/red] {source}")
raise SystemExit(1)

parameters: Dict[str, Any] = {}
if params_file:
for pf in params_file:
try:
data = json.loads(Path(pf).read_text(encoding="utf-8"))
if isinstance(data, dict):
parameters.update(data)
except Exception as exc:
console.print(f"[red]Error loading params file {pf}:[/red] {exc}")
raise SystemExit(1)

if param:
for kv in param:
if "=" not in kv:
console.print(f"[red]Invalid parameter:[/red] {kv}. Use key=value")
raise SystemExit(1)
key, value = kv.split("=", 1)

try:
if value.strip().startswith(("{", "[")):
parameters[key] = json.loads(value)
elif value.lower() in ("true", "false"):
parameters[key] = value.lower() == "true"
elif value.replace(".", "").replace("-", "").isdigit() and value.count(".") <= 1:
parameters[key] = float(value) if "." in value else int(value)
else:
parameters[key] = value
except json.JSONDecodeError:
parameters[key] = value

if to_markdown:
output_format = "markdown"
elif to_provider_json:
output_format = f"provider-json:{to_provider_json}"
else:
extra = list(getattr(ctx, "args", []) or [])
if output_format.strip().lower() == "provider-json" and extra:
next_tok = extra[0]
if next_tok and not next_tok.startswith("-"):
output_format = f"provider-json:{next_tok}"
try:
ctx.args = extra[1:]
except Exception:
pass

if verbose:
try:
console.print(
f"[dim]Compiling {source} -> {output_format} with params: {list(parameters.keys())}[/dim]"
)
except Exception:
pass

from prompd.compiler import PrompdCompiler

compiler = PrompdCompiler()
result = compiler.compile(
source=source,
output_format=output_format,
parameters=parameters,
output_file=Path(output) if output else None,
verbose=verbose,
)

if output:
try:
console.print(f"[green]OK[/green] Compiled output written to {output}")
except Exception:
print(f"OK - Compiled output written to {output}")
else:
print(result)
except SystemExit:
raise
except Exception as exc:
try:
console.print(f"[red]Error compiling:[/red] {exc}")
except Exception:
print(f"Error compiling: {exc}")
raise SystemExit(1)


__all__ = ["compile_command"]
49 changes: 48 additions & 1 deletion cli/python/prompd/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,4 +328,51 @@ def get_template_body(template: Optional[str], name: str, description: str, para
# Get template or use basic
template_body = templates.get(template, templates['basic'])

return template_body
return template_body


@click.command(name="create")
@click.argument("file", type=click.Path(path_type=Path))
@click.option("-i", "--interactive", is_flag=True, help="Interactive mode with prompts")
@click.option("-n", "--name", help="Prompt name")
@click.option("-d", "--description", help="Prompt description")
@click.option("-a", "--author", help="Author name")
@click.option("-v", "--version", default="1.0.0", help="Version (default: 1.0.0)")
@click.option(
"-t",
"--template",
type=click.Choice(["basic", "analysis", "security", "code-review", "creative"]),
help="Use a predefined template",
)
def create_command(
file: Path,
interactive: bool,
name: Optional[str],
description: Optional[str],
author: Optional[str],
version: str,
template: Optional[str],
):
"""Create a new .prmd file."""
try:
create_prmd_file(
file_path=file,
interactive=interactive,
name=name,
description=description,
author=author,
version=version,
template=template,
)
console.print(f"[green]OK[/green] Created {file}")
except Exception as exc:
console.print(f"[red]Error:[/red] {exc}")
raise SystemExit(1)


__all__ = [
"create_prmd_file",
"generate_prmd_content",
"get_template_body",
"create_command",
]
Loading