#!/usr/bin/env python3 """Richtet den AG_Prozesse-Workspace ein bzw. aktualisiert ihn. 1. Klont fehlende Repositories aus workspace.toml in Unterordner (Default-Branch). 2. Holt für vorhandene Klone den Stand vom Server (git fetch) und zeigt Branch, Vorsprung/Rückstand und ungesicherte Änderungen an. Ein Pull passiert nur mit --pull und nur bei sauberem Arbeitsverzeichnis (fast-forward). 3. Spiegelt die Skills aus .agents/skills/ nach .claude/skills/ (Claude Code liest nur dort; der Agent-Skills-Standard und andere Werkzeuge lesen .agents/skills/). Aufrufe: python scripts/setup_workspace.py # klonen + Status + Skills python scripts/setup_workspace.py --pull # zusätzlich fast-forward pull python scripts/setup_workspace.py --nur-skills """ from __future__ import annotations import argparse import os import shutil import subprocess import tomllib from pathlib import Path ROOT = Path(__file__).resolve().parents[1] LOKALE_ÜBERSCHREIBUNG = ROOT / "workspace.local.toml" def repos_root() -> Path: """Ordner für die weiteren Repository-Klone, siehe workspace.local.toml (nicht versioniert).""" if not LOKALE_ÜBERSCHREIBUNG.is_file(): return ROOT with LOKALE_ÜBERSCHREIBUNG.open("rb") as f: lokal = tomllib.load(f) pfad = lokal.get("workspace", {}).get("repos_root") return Path(pfad).expanduser() if pfad else ROOT def git(*args: str, cwd: Path) -> tuple[int, str]: p = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True, errors="replace") return p.returncode, (p.stdout + p.stderr).strip() def sync_skills() -> None: quelle = ROOT / ".agents" / "skills" ziel = ROOT / ".claude" / "skills" if not quelle.is_dir(): print("Keine Skills in .agents/skills/ gefunden.") return ziel.parent.mkdir(exist_ok=True) # Alten Spiegel entfernen (Symlink, Junction oder Kopie) if ziel.is_symlink() or (ziel.exists() and not ziel.is_dir()): ziel.unlink() elif ziel.is_dir(): try: os.rmdir(ziel) # leere Junction/Ordner except OSError: shutil.rmtree(ziel) art = "symlink" try: os.symlink(quelle, ziel, target_is_directory=True) except (OSError, NotImplementedError): art = "kopie" if os.name == "nt": try: import _winapi # type: ignore[attr-defined] _winapi.CreateJunction(str(quelle), str(ziel)) art = "junction" except Exception: pass if art == "kopie": shutil.copytree(quelle, ziel, ignore=shutil.ignore_patterns("__pycache__")) anzahl = len([p for p in quelle.iterdir() if (p / "SKILL.md").is_file()]) print(f"Skills: {anzahl} aus .agents/skills/ nach .claude/skills/ gespiegelt ({art}).") if art == "kopie": print(" Hinweis: Kopie statt Link – nach Änderungen an .agents/skills/ dieses Skript erneut ausführen.") def main() -> int: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--pull", action="store_true", help="saubere Klone per fast-forward aktualisieren") ap.add_argument("--nur-skills", action="store_true", help="nur Skills spiegeln, kein Git") args = ap.parse_args() if not args.nur_skills: with (ROOT / "workspace.toml").open("rb") as f: manifest = tomllib.load(f) ws = manifest["workspace"] basis_url = f"{ws['gitea']}/{ws['org']}" rroot = repos_root() print(f"Workspace: {ROOT}\nGitea: {basis_url}\n") if rroot != ROOT: print(f"Repositories liegen laut workspace.local.toml unter: {rroot}\n") rroot.mkdir(parents=True, exist_ok=True) for repo in manifest.get("repos", []): name, branch = repo["name"], repo["branch"] pfad = rroot / name if not (pfad / ".git").is_dir(): print(f"[{name}] klone Branch {branch} ...") rc, out = git("clone", "--branch", branch, f"{basis_url}/{name}.git", name, cwd=rroot) print(" " + ("ok" if rc == 0 else f"FEHLER: {out}")) continue git("fetch", "--quiet", "--prune", cwd=pfad) _, aktuell = git("rev-parse", "--abbrev-ref", "HEAD", cwd=pfad) _, dirty = git("status", "--porcelain", cwd=pfad) n_dirty = len([z for z in dirty.splitlines() if z.strip()]) _, ab = git("rev-list", "--left-right", "--count", f"HEAD...origin/{aktuell}", cwd=pfad) vor, _, zurueck = ab.partition("\t") if "\t" in ab else ("?", "", "?") meldung = f"[{name}] Branch {aktuell} (+{vor}/-{zurueck.strip()} zu origin), {n_dirty} ungesicherte Änderungen" if aktuell != branch: meldung += f" <- Hinweis: Manifest erwartet {branch}" if args.pull and n_dirty == 0 and aktuell == branch: rc, out = git("pull", "--ff-only", "--quiet", cwd=pfad) meldung += " -> pull " + ("ok" if rc == 0 else f"FEHLER: {out.splitlines()[-1] if out else rc}") print(meldung) print() sync_skills() return 0 if __name__ == "__main__": raise SystemExit(main())