mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
fix(bundle): escape catalog metadata in discovery output (#3774)
* fix(bundle): escape catalog metadata in discovery output Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(bundle): escape provides fallback values Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import re
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
from rich.markup import escape as _escape_markup
|
||||
|
||||
from ..._console import console, err_console
|
||||
from ..._download_security import MAX_DOWNLOAD_BYTES, read_response_limited
|
||||
@@ -185,11 +186,16 @@ def bundle_search(
|
||||
else ""
|
||||
)
|
||||
console.print(
|
||||
f" [bold]{r.entry.id}[/bold] v{r.entry.version} — {r.entry.name} "
|
||||
f"[dim]({r.entry.role})[/dim] {_trust_badge(r.entry.verified)} {policy}"
|
||||
f" [bold]{_escape_markup(str(r.entry.id))}[/bold] "
|
||||
f"v{_escape_markup(str(r.entry.version))} — "
|
||||
f"{_escape_markup(str(r.entry.name))} "
|
||||
f"[dim]({_escape_markup(str(r.entry.role))})[/dim] "
|
||||
f"{_trust_badge(r.entry.verified)} {policy}"
|
||||
)
|
||||
console.print(f" {_escape_markup(str(r.entry.description))}")
|
||||
console.print(
|
||||
f" [dim]source: {_escape_markup(str(r.source.id))}[/dim]"
|
||||
)
|
||||
console.print(f" {r.entry.description}")
|
||||
console.print(f" [dim]source: {r.source.id}[/dim]")
|
||||
|
||||
|
||||
@bundle_app.command("info")
|
||||
@@ -242,16 +248,31 @@ def bundle_info(
|
||||
print(_json.dumps(payload, indent=2))
|
||||
return
|
||||
|
||||
console.print(f"\n[bold cyan]{entry.id}[/bold cyan] v{entry.version} — {entry.name}")
|
||||
console.print(f" Role: {entry.role}")
|
||||
console.print(f" {entry.description}")
|
||||
console.print(f" Author: {entry.author} License: {entry.license}")
|
||||
console.print(f" Source: {resolved.source.id} ({resolved.source.install_policy.value})")
|
||||
console.print(
|
||||
f"\n[bold cyan]{_escape_markup(str(entry.id))}[/bold cyan] "
|
||||
f"v{_escape_markup(str(entry.version))} — "
|
||||
f"{_escape_markup(str(entry.name))}"
|
||||
)
|
||||
console.print(f" Role: {_escape_markup(str(entry.role))}")
|
||||
console.print(f" {_escape_markup(str(entry.description))}")
|
||||
console.print(
|
||||
f" Author: {_escape_markup(str(entry.author))} "
|
||||
f"License: {_escape_markup(str(entry.license))}"
|
||||
)
|
||||
console.print(
|
||||
f" Source: {_escape_markup(str(resolved.source.id))} "
|
||||
f"({resolved.source.install_policy.value})"
|
||||
)
|
||||
console.print(f" Trust: {_trust_badge(entry.verified)}")
|
||||
if entry.requires_speckit_version:
|
||||
console.print(f" Requires Spec Kit: {entry.requires_speckit_version}")
|
||||
console.print(
|
||||
f" Requires Spec Kit: "
|
||||
f"{_escape_markup(str(entry.requires_speckit_version))}"
|
||||
)
|
||||
if manifest and manifest.integration:
|
||||
console.print(f" Integration: {manifest.integration.id}")
|
||||
console.print(
|
||||
f" Integration: {_escape_markup(str(manifest.integration.id))}"
|
||||
)
|
||||
|
||||
if components:
|
||||
console.print("\n [bold]Components[/bold] (added on install):")
|
||||
@@ -261,18 +282,22 @@ def bundle_info(
|
||||
continue
|
||||
console.print(f" [bold]{kind}:[/bold]")
|
||||
for item in items:
|
||||
console.print(f" - {_format_component(item)}")
|
||||
console.print(
|
||||
f" - {_escape_markup(_format_component(item))}"
|
||||
)
|
||||
else:
|
||||
console.print("\n [bold]Provides:[/bold]")
|
||||
for kind in ("extensions", "presets", "steps", "workflows"):
|
||||
count = entry.provides.get(kind, 0)
|
||||
if count:
|
||||
console.print(f" {kind}: {count}")
|
||||
console.print(f" {kind}: {_escape_markup(str(count))}")
|
||||
|
||||
if overlaps:
|
||||
console.print("\n [yellow]Overlaps with already-installed bundles:[/yellow]")
|
||||
for overlap in overlaps:
|
||||
console.print(f" [yellow]-[/yellow] {overlap}")
|
||||
console.print(
|
||||
f" [yellow]-[/yellow] {_escape_markup(str(overlap))}"
|
||||
)
|
||||
|
||||
if not resolved.install_allowed:
|
||||
console.print(
|
||||
|
||||
@@ -17,6 +17,7 @@ from typer.testing import CliRunner
|
||||
|
||||
from specify_cli import app
|
||||
from specify_cli.bundler.services.packager import build_bundle
|
||||
from tests.conftest import strip_ansi
|
||||
from tests.bundler_helpers import (
|
||||
catalog_entry_dict,
|
||||
valid_manifest_dict,
|
||||
@@ -25,6 +26,42 @@ from tests.bundler_helpers import (
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
MARKUP_BUNDLE_ID = "[red]markup-id[/red]"
|
||||
MARKUP_SOURCE_ID = "[underline]markup-source[/underline]"
|
||||
|
||||
|
||||
def _configure_markup_catalog(project: Path, **overrides: object) -> dict:
|
||||
entry = catalog_entry_dict(
|
||||
MARKUP_BUNDLE_ID,
|
||||
name="[green]Markup Name[/green]",
|
||||
version="[blue]1.0.0[/blue]",
|
||||
role="[magenta]Markup Role[/magenta]",
|
||||
description="[yellow]Markup Description[/yellow]",
|
||||
author="[cyan]Markup Author[/cyan]",
|
||||
license="[bold]Markup License[/bold]",
|
||||
download_url="https://example.com/markup-bundle.zip",
|
||||
requires={"speckit_version": "[italic]>=0.1.0[/italic]"},
|
||||
**overrides,
|
||||
)
|
||||
catalog = project / "markup-catalog.json"
|
||||
write_catalog_file(catalog, {MARKUP_BUNDLE_ID: entry})
|
||||
config = {
|
||||
"schema_version": "1.0",
|
||||
"catalogs": [
|
||||
{
|
||||
"id": MARKUP_SOURCE_ID,
|
||||
"url": str(catalog),
|
||||
"priority": 1,
|
||||
"install_policy": "install-allowed",
|
||||
}
|
||||
],
|
||||
}
|
||||
(project / ".specify" / "bundle-catalogs.yml").write_text(
|
||||
yaml.safe_dump(config),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return entry
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def project(tmp_path: Path, monkeypatch) -> Path:
|
||||
@@ -124,6 +161,24 @@ def test_search_works_without_a_project(tmp_path: Path, monkeypatch):
|
||||
assert result.output.strip().startswith("[")
|
||||
|
||||
|
||||
def test_search_escapes_catalog_markup(project: Path):
|
||||
entry = _configure_markup_catalog(project)
|
||||
|
||||
result = runner.invoke(app, ["bundle", "search", "--offline"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
output = " ".join(strip_ansi(result.output).split())
|
||||
for value in (
|
||||
entry["id"],
|
||||
entry["name"],
|
||||
entry["version"],
|
||||
entry["role"],
|
||||
entry["description"],
|
||||
MARKUP_SOURCE_ID,
|
||||
):
|
||||
assert value in output
|
||||
|
||||
|
||||
def test_info_unknown_bundle_without_project_reports_not_found(tmp_path: Path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path) # no .specify/
|
||||
result = runner.invoke(app, ["bundle", "info", "does-not-exist", "--offline"])
|
||||
@@ -261,6 +316,83 @@ def test_info_expands_full_component_set(project: Path, monkeypatch):
|
||||
assert "Trust" in text.output
|
||||
|
||||
|
||||
def test_info_escapes_catalog_markup(project: Path, monkeypatch):
|
||||
entry = _configure_markup_catalog(project)
|
||||
bundle_dir = project / "markup-bundle"
|
||||
bundle_dir.mkdir()
|
||||
manifest_data = valid_manifest_dict()
|
||||
manifest_data["bundle"]["id"] = MARKUP_BUNDLE_ID
|
||||
manifest_data["integration"] = {
|
||||
"id": "[conceal]markup-integration[/conceal]"
|
||||
}
|
||||
manifest_path = bundle_dir / "bundle.yml"
|
||||
manifest_path.write_text(yaml.safe_dump(manifest_data), encoding="utf-8")
|
||||
_mock_manifest_download(monkeypatch, manifest_path)
|
||||
monkeypatch.setattr(
|
||||
"specify_cli.commands.bundle._manifest_component_view",
|
||||
lambda manifest: [
|
||||
{
|
||||
"kind": "extensions",
|
||||
"id": "[reverse]markup-component[/reverse]",
|
||||
"version": "[strike]2.0.0[/strike]",
|
||||
}
|
||||
],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"specify_cli.commands.bundle._bundle_overlaps",
|
||||
lambda project_root, manifest, *, offline: [
|
||||
"[blink]markup-overlap[/blink]"
|
||||
],
|
||||
)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["bundle", "info", MARKUP_BUNDLE_ID, "--offline"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
output = " ".join(strip_ansi(result.output).split())
|
||||
for value in (
|
||||
entry["id"],
|
||||
entry["name"],
|
||||
entry["version"],
|
||||
entry["role"],
|
||||
entry["description"],
|
||||
entry["author"],
|
||||
entry["license"],
|
||||
entry["requires"]["speckit_version"],
|
||||
MARKUP_SOURCE_ID,
|
||||
"[conceal]markup-integration[/conceal]",
|
||||
"[reverse]markup-component[/reverse]",
|
||||
"[strike]2.0.0[/strike]",
|
||||
"[blink]markup-overlap[/blink]",
|
||||
):
|
||||
assert value in output
|
||||
|
||||
|
||||
def test_info_escapes_catalog_provides_fallback_markup(project: Path, monkeypatch):
|
||||
markup_count = "[bold]markup-count[/bold]"
|
||||
_configure_markup_catalog(
|
||||
project,
|
||||
provides={"extensions": markup_count},
|
||||
)
|
||||
bundle_dir = project / "markup-bundle"
|
||||
bundle_dir.mkdir()
|
||||
manifest_data = valid_manifest_dict(provides={})
|
||||
manifest_data["bundle"]["id"] = MARKUP_BUNDLE_ID
|
||||
manifest_path = bundle_dir / "bundle.yml"
|
||||
manifest_path.write_text(yaml.safe_dump(manifest_data), encoding="utf-8")
|
||||
_mock_manifest_download(monkeypatch, manifest_path)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["bundle", "info", MARKUP_BUNDLE_ID, "--offline"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert markup_count in strip_ansi(result.output)
|
||||
|
||||
|
||||
def test_info_expands_discovery_only_bundle(project: Path, monkeypatch):
|
||||
# Discovery-only bundles must still be fully inspectable via `info`;
|
||||
# only `install` is refused for them.
|
||||
|
||||
Reference in New Issue
Block a user