diff --git a/tools/version_manager.py b/tools/version_manager.py new file mode 100644 index 0000000..1ac7247 --- /dev/null +++ b/tools/version_manager.py @@ -0,0 +1,88 @@ +""" +Version Manager — archives and rolls back teacher skill versions. +Adapted from colleague-skill's version_manager.py. +""" + +import json +import os +import shutil +from datetime import datetime + +MAX_VERSIONS = 10 + + +def list_versions(teacher_dir: str) -> list: + """List all archived versions of a teacher skill.""" + versions_dir = os.path.join(teacher_dir, "versions") + if not os.path.exists(versions_dir): + return [] + + versions = [] + for entry in sorted(os.listdir(versions_dir)): + entry_path = os.path.join(versions_dir, entry) + if os.path.isdir(entry_path) and entry.startswith("v"): + files = os.listdir(entry_path) + mtime = os.path.getmtime(entry_path) + versions.append({ + "version": entry[1:], + "archived_at": datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M"), + "files": files, + }) + return versions + + +def rollback(teacher_dir: str, target_version: str) -> bool: + """Roll back a teacher skill to a previous version. Backs up current state first.""" + target_dir = os.path.join(teacher_dir, "versions", f"v{target_version}") + if not os.path.isdir(target_dir): + return False + + meta_path = os.path.join(teacher_dir, "meta.json") + current_version = "unknown" + if os.path.exists(meta_path): + with open(meta_path, "r", encoding="utf-8") as f: + meta = json.load(f) + current_version = meta.get("version", "unknown") + backup_dir = os.path.join(teacher_dir, "versions", f"v{current_version}_before_rollback") + os.makedirs(backup_dir, exist_ok=True) + for fname in ["SKILL.md", "teaching.md", "voice.md", "meta.json"]: + src = os.path.join(teacher_dir, fname) + if os.path.exists(src): + shutil.copy2(src, backup_dir) + + for fname in ["SKILL.md", "teaching.md", "voice.md", "meta.json"]: + src = os.path.join(target_dir, fname) + if os.path.exists(src): + shutil.copy2(src, os.path.join(teacher_dir, fname)) + + if os.path.exists(meta_path): + with open(meta_path, "r", encoding="utf-8") as f: + meta = json.load(f) + meta["rollback_from"] = current_version + meta["updated_at"] = datetime.now().strftime("%Y-%m-%d") + with open(meta_path, "w", encoding="utf-8") as f: + json.dump(meta, f, ensure_ascii=False, indent=2) + + return True + + +def cleanup_old_versions(teacher_dir: str) -> int: + """Remove old versions beyond MAX_VERSIONS limit.""" + versions_dir = os.path.join(teacher_dir, "versions") + if not os.path.exists(versions_dir): + return 0 + + entries = [] + for entry in os.listdir(versions_dir): + entry_path = os.path.join(versions_dir, entry) + if os.path.isdir(entry_path): + entries.append((entry_path, os.path.getmtime(entry_path))) + + entries.sort(key=lambda x: x[1], reverse=True) + + removed = 0 + for path, _ in entries[MAX_VERSIONS:]: + shutil.rmtree(path) + removed += 1 + + return removed