✨ feat(actions): add install_skills and format_md
This commit is contained in:
+114
-1
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user