✨ feat(actions): add install_skills and format_md
This commit is contained in:
parent
3d1582ce9e
commit
0c4cd0e037
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue