2026-09-04 11:57:26 +02:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
"""Richtet den AG_QGIS-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]
|
2026-09-14 11:36:55 +02:00
|
|
|
|
LOKALE_ÜBERSCHREIBUNG = ROOT / "workspace.local.toml"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def repos_root() -> Path:
|
|
|
|
|
|
"""Ordner für die Plugin-/Daten-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
|
2026-09-04 11:57:26 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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']}"
|
2026-09-14 11:36:55 +02:00
|
|
|
|
rroot = repos_root()
|
2026-09-04 11:57:26 +02:00
|
|
|
|
print(f"Workspace: {ROOT}\nGitea: {basis_url}\n")
|
2026-09-14 11:36:55 +02:00
|
|
|
|
if rroot != ROOT:
|
|
|
|
|
|
print(f"Repositories liegen laut workspace.local.toml unter: {rroot}\n")
|
|
|
|
|
|
rroot.mkdir(parents=True, exist_ok=True)
|
2026-09-04 11:57:26 +02:00
|
|
|
|
for repo in manifest["repos"]:
|
|
|
|
|
|
name, branch = repo["name"], repo["branch"]
|
2026-09-14 11:36:55 +02:00
|
|
|
|
pfad = rroot / name
|
2026-09-04 11:57:26 +02:00
|
|
|
|
if not (pfad / ".git").is_dir():
|
|
|
|
|
|
print(f"[{name}] klone Branch {branch} ...")
|
2026-09-14 11:36:55 +02:00
|
|
|
|
rc, out = git("clone", "--branch", branch, f"{basis_url}/{name}.git", name, cwd=rroot)
|
2026-09-04 11:57:26 +02:00
|
|
|
|
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())
|