Skip to content

Commit 010e778

Browse files
committed
feat(skills): add cli skill test command
1 parent 182528e commit 010e778

1 file changed

Lines changed: 70 additions & 1 deletion

File tree

src/qwenpaw/cli/skills_cmd.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
SkillConflictError,
1111
SkillPoolService,
1212
SkillService,
13+
_validate_skill_content,
1314
get_workspace_skills_dir,
1415
list_workspaces,
1516
read_skill_pool_manifest,
@@ -21,9 +22,11 @@
2122
import_pool_skill_from_hub,
2223
install_skill_from_hub,
2324
)
25+
from ..agents.utils.file_handling import read_text_file_with_encoding_fallback
2426
from ..config import load_config
2527
from ..constant import WORKING_DIR
26-
from ..security.skill_scanner import SkillScanError
28+
from ..exceptions import SkillsError
29+
from ..security.skill_scanner import SkillScanError, scan_skill_directory
2730
from .utils import prompt_checkbox, prompt_confirm
2831

2932

@@ -104,6 +107,57 @@ def _print_skill_changes(
104107
)
105108

106109

110+
def _validate_skill_frontmatter(skill_dir: Path) -> None:
111+
"""Validate required skill metadata."""
112+
skill_md = skill_dir / "SKILL.md"
113+
if not skill_md.is_file():
114+
raise click.ClickException(f"Missing SKILL.md: {skill_md}")
115+
116+
content = read_text_file_with_encoding_fallback(skill_md)
117+
try:
118+
_validate_skill_content(content)
119+
except SkillsError as exc:
120+
raise click.ClickException(str(exc))
121+
except Exception as exc:
122+
raise click.ClickException(
123+
f"SKILL.md frontmatter is invalid: {exc}",
124+
) from exc
125+
126+
127+
def _resolve_skill_test_dir(skill: str, agent_id: str) -> Path:
128+
"""Resolve a skill argument as a path first, then workspace skill name."""
129+
candidate = Path(skill).expanduser()
130+
if candidate.exists():
131+
return candidate.resolve()
132+
133+
working_dir = _get_agent_workspace(agent_id)
134+
return get_workspace_skills_dir(working_dir) / skill
135+
136+
137+
def _run_skill_test(skill_dir: Path) -> str:
138+
"""Run local skill validation and security scanning."""
139+
if not skill_dir.is_dir():
140+
raise click.ClickException(f"Skill directory not found: {skill_dir}")
141+
142+
skill_name = skill_dir.name
143+
_validate_skill_frontmatter(skill_dir)
144+
try:
145+
result = scan_skill_directory(
146+
skill_dir,
147+
skill_name=skill_name,
148+
block=True,
149+
)
150+
except SkillScanError as exc:
151+
raise click.ClickException(str(exc)) from exc
152+
153+
if result is not None and not result.is_safe:
154+
raise click.ClickException(
155+
"Security scan found "
156+
f"{len(result.findings)} issue(s) in skill '{skill_name}'.",
157+
)
158+
return skill_name
159+
160+
107161
def _apply_skill_changes(
108162
skill_service: SkillService,
109163
pool_service: SkillPoolService | None,
@@ -489,3 +543,18 @@ def uninstall_cmd(
489543
raise
490544
except Exception as exc:
491545
raise click.ClickException(str(exc)) from exc
546+
547+
548+
@skills_group.command("test")
549+
@click.argument("skill", required=True)
550+
@click.option(
551+
"--agent-id",
552+
default="default",
553+
help="Agent ID (defaults to 'default')",
554+
)
555+
def test_cmd(skill: str, agent_id: str) -> None:
556+
"""Validate a workspace skill or local skill directory."""
557+
skill_dir = _resolve_skill_test_dir(skill, agent_id)
558+
skill_name = _run_skill_test(skill_dir)
559+
click.echo(f"Skill test passed: {skill_name}")
560+
click.echo(f"Path: {skill_dir}")

0 commit comments

Comments
 (0)