✨ feat(actions): add install_skills and format_md
This commit is contained in:
parent
3d1582ce9e
commit
0c4cd0e037
|
|
@ -2,7 +2,7 @@
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from shutil import copy2, copytree
|
from shutil import copy2, copytree, which
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import tomllib
|
import tomllib
|
||||||
|
|
@ -640,6 +640,115 @@ def sync_standards_action(config: dict, context: dict) -> int:
|
||||||
return 0
|
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:
|
def run_action(name: str, config: dict, context: dict) -> int:
|
||||||
print(f"[action] {name}")
|
print(f"[action] {name}")
|
||||||
if name == "vendor":
|
if name == "vendor":
|
||||||
|
|
@ -648,6 +757,10 @@ def run_action(name: str, config: dict, context: dict) -> int:
|
||||||
return sync_templates_action(config, context)
|
return sync_templates_action(config, context)
|
||||||
if name == "sync_standards":
|
if name == "sync_standards":
|
||||||
return sync_standards_action(config, context)
|
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
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,5 +101,26 @@ langs = ["tsl"]
|
||||||
self.assertEqual(result.returncode, 0)
|
self.assertEqual(result.returncode, 0)
|
||||||
self.assertTrue(agents_index.is_file())
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue