Files
planer/backend/cli.py
2026-07-22 16:12:23 +02:00

120 lines
4.0 KiB
Python

"""CLI des Planers (Phase 1, kein Server):
python -m backend scan <pfad> Erst-Scan eines Projekt-Repos
python -m backend rescan <pfad> 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 <pid>` 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()