120 lines
4.0 KiB
Python
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()
|