feat(actions): add install_skills and format_md

This commit is contained in:
csh 2026-01-23 14:40:08 +08:00
parent 3d1582ce9e
commit 0c4cd0e037
2 changed files with 135 additions and 1 deletions

View File

@ -2,7 +2,7 @@
import sys
from datetime import datetime, timezone
from pathlib import Path
from shutil import copy2, copytree
from shutil import copy2, copytree, which
import subprocess
import tomllib
@ -640,6 +640,115 @@ def sync_standards_action(config: dict, context: dict) -> int:
return 0
def normalize_names(raw: object, label: str) -> list[str]:
if raw is None:
raise ValueError(f"{label} is required")
if isinstance(raw, str):
items = [raw]
else:
items = list(raw)
cleaned: list[str] = []
for item in items:
name = str(item).strip()
if not name:
continue
if "/" in name or "\\" in name or ".." in name:
raise ValueError(f"invalid {label}: {name}")
cleaned.append(name)
if not cleaned:
raise ValueError(f"{label} is empty")
return cleaned
def normalize_globs(raw: object) -> list[str]:
if raw is None:
return ["**/*.md"]
if isinstance(raw, str):
items = [raw]
else:
items = list(raw)
cleaned = [str(item).strip() for item in items if str(item).strip()]
return cleaned or ["**/*.md"]
def install_skills_action(config: dict, context: dict) -> int:
mode = str(config.get("mode", "list")).lower()
codex_home = Path(config.get("codex_home", "~/.codex")).expanduser()
if not codex_home.is_absolute():
codex_home = (context["project_root"] / codex_home).resolve()
skills_src_root = PLAYBOOK_ROOT / "codex/skills"
if not skills_src_root.is_dir():
print(f"ERROR: skills source not found: {skills_src_root}", file=sys.stderr)
return 2
skills_dst_root = codex_home / "skills"
ensure_dir(skills_dst_root)
if mode == "all":
skills = [
path.name
for path in skills_src_root.iterdir()
if path.is_dir() and not path.name.startswith(".")
]
elif mode == "list":
try:
skills = normalize_names(config.get("skills"), "skills")
except ValueError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
else:
print("ERROR: mode must be list or all", file=sys.stderr)
return 2
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
for name in skills:
src = skills_src_root / name
if not src.is_dir():
print(f"ERROR: skill not found: {name}", file=sys.stderr)
return 2
dst = skills_dst_root / name
if dst.exists():
backup = skills_dst_root / f"{name}.bak.{timestamp}"
dst.rename(backup)
log(f"Backed up existing skill: {name} -> {backup.name}")
copytree(src, dst)
log(f"Installed: {name}")
return 0
def format_md_action(config: dict, context: dict) -> int:
tool = str(config.get("tool", "prettier")).lower()
if tool != "prettier":
print("ERROR: format_md.tool only supports prettier", file=sys.stderr)
return 2
project_root: Path = context["project_root"]
prettier = project_root / "node_modules/.bin/prettier"
if not prettier.is_file():
prettier = PLAYBOOK_ROOT / "node_modules/.bin/prettier"
if not prettier.is_file():
resolved = which("prettier")
if resolved:
prettier = Path(resolved)
else:
log("Skip: prettier not found.")
return 0
globs_raw = config.get("globs", ["**/*.md"])
globs = normalize_globs(globs_raw)
result = subprocess.run(
[str(prettier), "-w", *globs],
cwd=project_root,
capture_output=True,
text=True,
)
if result.returncode != 0:
sys.stderr.write(result.stderr)
return result.returncode
def run_action(name: str, config: dict, context: dict) -> int:
print(f"[action] {name}")
if name == "vendor":
@ -648,6 +757,10 @@ def run_action(name: str, config: dict, context: dict) -> int:
return sync_templates_action(config, context)
if name == "sync_standards":
return sync_standards_action(config, context)
if name == "install_skills":
return install_skills_action(config, context)
if name == "format_md":
return format_md_action(config, context)
return 0

View File

@ -101,5 +101,26 @@ langs = ["tsl"]
self.assertEqual(result.returncode, 0)
self.assertTrue(agents_index.is_file())
def test_install_skills(self):
with tempfile.TemporaryDirectory() as tmp_dir:
target = Path(tmp_dir) / "codex"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
[install_skills]
codex_home = "{target}"
mode = "list"
skills = ["brainstorming"]
"""
config_path = Path(tmp_dir) / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
result = run_cli("-config", str(config_path))
skill_file = target / "skills/brainstorming/SKILL.md"
self.assertEqual(result.returncode, 0)
self.assertTrue(skill_file.is_file())
if __name__ == "__main__":
unittest.main()