Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
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
5 changes: 3 additions & 2 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,9 @@ def _install_shared_infra(

Copies ``.specify/scripts/<variant>/`` and ``.specify/templates/`` from
the bundled core_pack or source checkout, where ``<variant>`` is
``bash`` when *script_type* is ``"sh"`` and ``powershell`` when it is
``"ps"``. Tracks all installed files in ``speckit.manifest.json``.
``bash`` when *script_type* is ``"sh"``, ``python`` when it is ``"py"``,
and ``powershell`` when it is ``"ps"``. Tracks all installed files in
``speckit.manifest.json``.

Shared scripts and page templates are processed to resolve
``__SPECKIT_COMMAND_<NAME>__`` placeholders using *invoke_separator*
Expand Down
6 changes: 5 additions & 1 deletion src/specify_cli/shared_infra.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ def _is_managed(rel: str, dst: Path) -> bool:
# manifest entries the core no longer ships (stale-script cleanup, #3076).
seen_rels: set[str] = set()
scripts_scanned = False
variant_dir = "bash" if script_type == "sh" else "powershell"
variant_dir = {"sh": "bash", "py": "python"}.get(script_type, "powershell")

def _decide_overwrite(rel: str, dst: Path) -> tuple[bool, str | None]:
"""Return (write, bucket) where bucket is 'skip', 'preserved', or None."""
Expand Down Expand Up @@ -462,6 +462,10 @@ def _ensure_or_bucket_dir(directory: Path) -> bool:
for src_path in variant_src.rglob("*"):
if not src_path.is_file():
continue
# Python bytecode caches are local artifacts, not
# workflow scripts — never install them.
if "__pycache__" in src_path.parts:
continue
# Mark scanned only once a real source file is seen. An
# empty (or symlink-skipped) variant keeps this False, so
# stale-cleanup is skipped — otherwise it would treat every
Expand Down
1 change: 1 addition & 0 deletions templates/commands/analyze.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ description: Perform a non-destructive cross-artifact consistency and quality an
scripts:
sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks
ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks
py: scripts/python/check_prerequisites.py --json --require-tasks --include-tasks
---

## User Input
Expand Down
1 change: 1 addition & 0 deletions templates/commands/checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ description: Generate a custom checklist for the current feature based on user r
scripts:
sh: scripts/bash/check-prerequisites.sh --json
ps: scripts/powershell/check-prerequisites.ps1 -Json
py: scripts/python/check_prerequisites.py --json
---

## Checklist Purpose: "Unit Tests for English"
Expand Down
1 change: 1 addition & 0 deletions templates/commands/clarify.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ handoffs:
scripts:
sh: scripts/bash/check-prerequisites.sh --json --paths-only
ps: scripts/powershell/check-prerequisites.ps1 -Json -PathsOnly
py: scripts/python/check_prerequisites.py --json --paths-only
---

## User Input
Expand Down
1 change: 1 addition & 0 deletions templates/commands/converge.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ description: Assess the current codebase against the feature's spec, plan, and t
scripts:
sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks
ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks
py: scripts/python/check_prerequisites.py --json --require-tasks --include-tasks
---

## User Input
Expand Down
1 change: 1 addition & 0 deletions templates/commands/implement.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ description: Execute the implementation plan by processing and executing all tas
scripts:
sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks
ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks
py: scripts/python/check_prerequisites.py --json --require-tasks --include-tasks

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, thanks. install_shared_infra mapped every non-sh type to powershell. Fixed in aa7e627: py now maps to the python variant dir (with __pycache__ excluded from the copy), plus a test that runs the installer with --script py and asserts check_prerequisites.py lands in .specify/scripts/python/.

---

## User Input
Expand Down
1 change: 1 addition & 0 deletions templates/commands/taskstoissues.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ tools: ['github/github-mcp-server/list_issues', 'github/github-mcp-server/issue_
scripts:
sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks
ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks
py: scripts/python/check_prerequisites.py --json --require-tasks --include-tasks
---

## User Input
Expand Down
96 changes: 96 additions & 0 deletions tests/test_command_template_py_scripts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Command templates with a py: script line must render for --script py.

Covers #3283: ``py:`` lines in the ``scripts:`` frontmatter of
``templates/commands/*.md`` reference Python scripts that exist in the repo,
and ``process_template`` turns them into a valid Python invocation
(interpreter-prefixed, path rewritten to the ``.specify`` tree).

``plan.md`` and ``tasks.md`` gain their ``py:`` lines together with
``setup_plan.py``/``setup_tasks.py`` in the core-scripts port (#3280); the
existence check below enforces that ordering.
"""

import re
from pathlib import Path

import pytest

from specify_cli.integrations.base import IntegrationBase

REPO_ROOT = Path(__file__).parent.parent
TEMPLATES_DIR = REPO_ROOT / "templates" / "commands"

_PY_LINE = re.compile(r"^\s*py: (scripts/python/\S+\.py)", re.MULTILINE)


def _py_script(name: str) -> str | None:
match = _PY_LINE.search((TEMPLATES_DIR / name).read_text(encoding="utf-8"))
return match.group(1) if match else None


PY_TEMPLATES = sorted(
p.name for p in TEMPLATES_DIR.glob("*.md") if _py_script(p.name)
)


@pytest.fixture(autouse=True)
def _pin_interpreter(monkeypatch):
monkeypatch.setattr(
"specify_cli.integrations.base.shutil.which",
lambda name: "/usr/bin/python3" if name == "python3" else None,
)


def test_py_templates_discovered():
# Guard: the glob must find the known py-scripted templates, otherwise
# the parametrized tests below would silently pass on an empty set.
assert "implement.md" in PY_TEMPLATES
assert "clarify.md" in PY_TEMPLATES


@pytest.mark.parametrize("name", PY_TEMPLATES)
def test_referenced_python_script_exists(name: str):
# A py: line must never point at a script the repo does not ship —
# rendering would produce a broken invocation at runtime.
script = _py_script(name)
assert (REPO_ROOT / script).is_file(), f"{name} references missing {script}"


@pytest.mark.parametrize("name", PY_TEMPLATES)
def test_template_renders_python_invocation(name: str):
content = (TEMPLATES_DIR / name).read_text(encoding="utf-8")
result = IntegrationBase.process_template(content, "agent", "py")
assert "{SCRIPT}" not in result
assert re.search(
r"python3 \.specify/scripts/python/\w+\.py(?: --[\w-]+)*", result
), f"{name} did not render a Python invocation"


@pytest.mark.parametrize("name", PY_TEMPLATES)
def test_sh_rendering_unchanged(name: str):
# Negative: adding py: lines must not leak into sh rendering.
content = (TEMPLATES_DIR / name).read_text(encoding="utf-8")
result = IntegrationBase.process_template(content, "agent", "sh")
assert "{SCRIPT}" not in result
assert "scripts/python" not in result


def test_install_shared_infra_copies_python_scripts(tmp_path):
# --script py must install scripts/python/ into .specify/scripts/python/
# so the rendered invocations point at files that exist.
from rich.console import Console

from specify_cli.shared_infra import install_shared_infra

install_shared_infra(
tmp_path,
"py",
version="0.0.0",
core_pack=None,
repo_root=REPO_ROOT,
console=Console(quiet=True),
force=False,
)
dest = tmp_path / ".specify" / "scripts" / "python"
assert (dest / "check_prerequisites.py").is_file()
assert not (tmp_path / ".specify" / "scripts" / "powershell").exists()