"""CLI des Planers (Phase 1, kein Server): python -m backend scan Erst-Scan eines Projekt-Repos python -m backend rescan Re-Scan über git-Diff gegen die Anker-Stände python -m backend status Karten-Stände aller bekannten Läufe """ import argparse import asyncio import logging import os import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) # `python -m backend` von der Wurzel import agents # noqa: E402 import database as db import scan_dienst log = logging.getLogger("planer") def _abbruch(meldung: str) -> "NoReturn": # noqa: F821 print(f"ABBRUCH: {meldung}", file=sys.stderr) sys.exit(1) def _vorflug(wurzel: Path) -> str: try: return scan_dienst.vorflug(wurzel) except scan_dienst.VorflugFehler as e: _abbruch(str(e)) def _fortschritt(text: str) -> None: print(f" {text}", flush=True) def _bericht(ergebnis: dict) -> None: print(f"Status: {ergebnis['status']}") if ergebnis.get("infra_error"): print(f" Infra: {ergebnis['infra_error']}") for pfad in ergebnis.get("geschrieben", []): print(f" geschrieben: .planer/{pfad}") for card in ergebnis.get("dead", []): print(f" DEAD: {card} (requeue über erneuten Lauf oder DB)") if ergebnis["status"] == "ok": print("Artefakte aktualisiert — jetzt committen (Code + .planer/ atomar).") async def _scan(wurzel: Path, fortsetzen: bool = False) -> int: stand = _vorflug(wurzel) await db.init_db() agents.on_event = db.add_event import scan_board ergebnis = await scan_board.run_scan(wurzel, wurzel.name, stand, set_p=_fortschritt, fortsetzen=fortsetzen) _bericht(ergebnis) return 0 if ergebnis["status"] == "ok" else 1 async def _rescan(wurzel: Path) -> int: stand_neu = _vorflug(wurzel) await db.init_db() agents.on_event = db.add_event try: ergebnis = await scan_dienst.rescan_lauf(wurzel, wurzel.name, stand_neu, set_p=_fortschritt) except scan_dienst.VorflugFehler as e: _abbruch(str(e)) if ergebnis["status"] == "unveraendert": print("Keine Änderungen seit dem letzten Scan — nichts zu tun.") return 0 _bericht(ergebnis) return 0 if ergebnis["status"] == "ok" else 1 async def _status() -> None: await db.init_db() verbindung = await db.get_db() cursor = await verbindung.execute("SELECT DISTINCT topic FROM kanban_cards ORDER BY topic") topics = [r[0] for r in await cursor.fetchall()] if not topics: print("Keine Läufe in der Planer-DB.") return for topic in topics: counts = await db.kanban_stage_counts(topic) dead = await db.kanban_dead(topic) print(f"{topic}: " + ", ".join(f"{s}={n}" for b in counts.values() for s, n in b.items()) + (f" — DEAD: {[c['card_id'] for c in dead]}" if dead else "")) def main() -> None: logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s") # Hänger-Diagnose (offener Befund: Prozess lebt nach Board-Ende weiter): # `kill -USR1 ` schreibt alle Python-Stacks nach stderr. import faulthandler import signal as _signal faulthandler.register(_signal.SIGUSR1) parser = argparse.ArgumentParser(prog="planer") sub = parser.add_subparsers(dest="kommando", required=True) for name in ("scan", "rescan"): p = sub.add_parser(name) p.add_argument("pfad", type=Path) if name == "scan": p.add_argument("--fortsetzen", action="store_true", help="abgebrochenen Lauf fortsetzen (nur tote Chunks neu)") sub.add_parser("status") args = parser.parse_args() if args.kommando == "scan": sys.exit(asyncio.run(_scan(args.pfad.resolve(), fortsetzen=args.fortsetzen))) elif args.kommando == "rescan": sys.exit(asyncio.run(_rescan(args.pfad.resolve()))) else: asyncio.run(_status()) if __name__ == "__main__": main()