Artikel

24. CLI-Werkzeuge

Allgemein

Die CLI ist die zentrale Bedienoberfläche für das MVP. Sie verbindet Generator, Review-Queue, Publisher, Logs und Reports.

Ziel:

ein Einstiegspunkt
klare Kommandos
keine versteckte Magie
Dry-Run als Standard
Live-Betrieb nur explizit

Empfohlener Aufruf:

python -m src.main <command>

24.1 Beiträge generieren

Ein Beitrag wird aus einer Content-Quelle erzeugt und als Entwurf gespeichert.

python -m src.main generate --platform linkedin --type workshop

Mit Varianten:

python -m src.main generate --platform linkedin --type workshop --variants 3

Mit konkreter Quelle:

python -m src.main generate --platform linkedin --type book_intro --source-id book_001

Erwartete Ausgabe:

DRAFT_CREATED
draft_20260512_093000_123456

24.2 Entwürfe anzeigen

Alle Entwürfe:

python -m src.main queue list

Nur offene Entwürfe:

python -m src.main queue list --status draft

Einzelnen Entwurf anzeigen:

python -m src.main queue show draft_20260512_093000_123456

Ausgabe:

ID:       draft_20260512_093000_123456
Platform: linkedin
Type:     workshop
Status:   draft

Text:
Gemeinfreie Texte sind frei verfügbar, aber nicht automatisch verlagsfertig.

24.3 Beitrag freigeben

python -m src.main queue approve draft_20260512_093000_123456

Ergebnis:

APPROVED
draft_20260512_093000_123456

Die Freigabe ändert nur den Status.

draft → approved

Veröffentlicht wird dadurch noch nichts.

24.4 Beitrag ablehnen

python -m src.main queue reject draft_20260512_093000_123456

Ergebnis:

REJECTED
draft_20260512_093000_123456

Statuswechsel:

draft → rejected

Abgelehnte Beiträge bleiben im Archiv erhalten.

24.5 Posting ausführen

Dry-Run ist Standard:

python -m src.main publish draft_20260512_093000_123456

Live-Veröffentlichung:

python -m src.main publish draft_20260512_093000_123456 --live

Nächsten freigegebenen Beitrag veröffentlichen:

python -m src.main publish-next

Live:

python -m src.main publish-next --live

Ergebnis bei Dry-Run:

DRY_RUN
draft_20260512_093000_123456

Ergebnis bei Erfolg:

PUBLISHED
draft_20260512_093000_123456

Ergebnis bei Fehler:

FAILED
draft_20260512_093000_123456
Post button not found.

Ergebnis bei kritischem Plattformzustand:

BLOCKED
draft_20260512_093000_123456
Login required.

24.6 Queue prüfen

Queue-Status zusammenfassen:

python -m src.main queue status

Beispielausgabe:

QUEUE STATUS

draft:      4
approved:   2
published: 12
rejected:   3
failed:     1
blocked:    0

Offene Veröffentlichungen:

python -m src.main queue list --status approved

Fehlerhafte Beiträge:

python -m src.main queue list --status failed

Blockierte Beiträge:

python -m src.main queue list --status blocked

24.7 Logs anzeigen

Letzte Events:

python -m src.main logs events --limit 20

Letzte Fehler:

python -m src.main logs errors --limit 20

OpenAI-Nutzung:

python -m src.main logs openai --limit 20

Plattformantworten:

python -m src.main logs platform --limit 20

Beispielausgabe:

2026-05-12T09:30:00+02:00  info   post_published   linkedin   draft_20260512_001
2026-05-12T09:31:00+02:00  error  post_failed      instagram  draft_20260512_002

24.8 Report erzeugen

Tagesreport:

python -m src.main report daily --date 2026-05-12

Wochenreport:

python -m src.main report weekly --start 2026-05-11 --end 2026-05-17

Ausgabe:

TAGESREPORT 2026-05-12 bis 2026-05-12

Beiträge:
- veröffentlicht: 1
- fehlgeschlagen: 0
- blockiert: 0
- offen: 3

OpenAI:
- Requests: 6
- Total-Tokens: 3390

24.9 Beispiel: main.py mit argparse

Datei:

src/main.py

import argparse
import json
from pathlib import Path
from typing import Any


class MainCliError(Exception):
    pass


class LogPrinter:
    def __init__(self, root_dir: Path) -> None:
        self.root_dir = root_dir

    def print_events(self, log_name: str, limit: int) -> None:
        log_file = self._get_log_file(log_name)

        if log_file.exists() is False:
            print("NO_LOG_FILE")
            print(str(log_file))
            return

        lines = log_file.read_text(encoding="utf-8").splitlines()
        selected_lines = lines[-limit:]

        for line in selected_lines:
            payload = json.loads(line)

            if isinstance(payload, dict) is False:
                continue

            timestamp = str(payload.get("timestamp", ""))
            level = str(payload.get("level", ""))
            event = str(payload.get("event", ""))
            platform = str(payload.get("platform", ""))
            reference_id = str(payload.get("reference_id", ""))

            print(
                timestamp
                + "  "
                + level.ljust(8)
                + event.ljust(28)
                + platform.ljust(12)
                + reference_id
            )

    def _get_log_file(self, log_name: str) -> Path:
        if log_name == "events":
            return self.root_dir / "storage" / "logs" / "events.jsonl"

        if log_name == "errors":
            return self.root_dir / "storage" / "logs" / "errors.jsonl"

        if log_name == "openai":
            return self.root_dir / "storage" / "logs" / "openai_usage.jsonl"

        if log_name == "platform":
            return self.root_dir / "storage" / "logs" / "platform_responses.jsonl"

        raise MainCliError("Unsupported log name: " + log_name)


class QueueStatusPrinter:
    def __init__(self, root_dir: Path) -> None:
        self.root_dir = root_dir

    def print_status(self) -> None:
        queue_file = self.root_dir / "data" / "post_drafts.json"

        if queue_file.exists() is False:
            print("NO_QUEUE")
            return

        with queue_file.open("r", encoding="utf-8") as file:
            payload = json.load(file)

        if isinstance(payload, list) is False:
            raise MainCliError("Queue must contain a list.")

        counts: dict[str, int] = {
            "draft": 0,
            "approved": 0,
            "published": 0,
            "rejected": 0,
            "failed": 0,
            "blocked": 0,
            "dry_run": 0,
        }

        for item in payload:
            if isinstance(item, dict) is False:
                continue

            status = item.get("status")

            if isinstance(status, str) is False:
                continue

            current_count = counts.get(status)

            if current_count is None:
                counts[status] = 1
            else:
                counts[status] = current_count + 1

        print("QUEUE STATUS")
        print("")

        for status, count in counts.items():
            print(status.ljust(10) + str(count))


def run_generate(args: argparse.Namespace) -> None:
    from src.generate_posts import main as generate_main

    generate_main()


def run_queue(args: argparse.Namespace) -> None:
    from src.review_queue import build_service

    root_dir = Path(__file__).resolve().parent.parent
    service = build_service(root_dir)

    if args.queue_command == "list":
        service.list_drafts(args.status)
        return

    if args.queue_command == "show":
        service.show(args.draft_id)
        return

    if args.queue_command == "approve":
        service.approve(args.draft_id)
        return

    if args.queue_command == "reject":
        service.reject(args.draft_id)
        return

    if args.queue_command == "edit":
        service.edit(args.draft_id)
        return

    if args.queue_command == "auto-approve":
        service.auto_approve()
        return

    if args.queue_command == "status":
        QueueStatusPrinter(root_dir).print_status()
        return

    raise MainCliError("Unsupported queue command: " + str(args.queue_command))


def run_publish(args: argparse.Namespace) -> None:
    from src.publisher import build_service

    root_dir = Path(__file__).resolve().parent.parent

    dry_run = True

    if args.live is True:
        dry_run = False

    service = build_service(root_dir, dry_run)
    service.publish_one(args.post_id)


def run_publish_next(args: argparse.Namespace) -> None:
    from src.publisher import build_service

    root_dir = Path(__file__).resolve().parent.parent

    dry_run = True

    if args.live is True:
        dry_run = False

    service = build_service(root_dir, dry_run)
    service.publish_next()


def run_logs(args: argparse.Namespace) -> None:
    root_dir = Path(__file__).resolve().parent.parent

    printer = LogPrinter(root_dir)
    printer.print_events(
        log_name=args.log_name,
        limit=args.limit,
    )


def run_report(args: argparse.Namespace) -> None:
    from src.report import ReportService
    from src.report import parse_date

    root_dir = Path(__file__).resolve().parent.parent
    service = ReportService(root_dir)

    if args.report_command == "daily":
        service.daily(parse_date(args.date))
        return

    if args.report_command == "weekly":
        service.weekly(
            start_date=parse_date(args.start),
            end_date=parse_date(args.end),
        )
        return

    raise MainCliError("Unsupported report command: " + str(args.report_command))


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="social-publisher",
    )

    subparsers = parser.add_subparsers(
        dest="command",
        required=True,
    )

    generate_parser = subparsers.add_parser("generate")
    generate_parser.add_argument("--platform", required=False)
    generate_parser.add_argument("--type", required=False)
    generate_parser.add_argument("--source-id", required=False)
    generate_parser.add_argument("--variants", required=False, type=int)

    queue_parser = subparsers.add_parser("queue")
    queue_subparsers = queue_parser.add_subparsers(
        dest="queue_command",
        required=True,
    )

    queue_list_parser = queue_subparsers.add_parser("list")
    queue_list_parser.add_argument("--status", required=False)

    queue_show_parser = queue_subparsers.add_parser("show")
    queue_show_parser.add_argument("draft_id")

    queue_approve_parser = queue_subparsers.add_parser("approve")
    queue_approve_parser.add_argument("draft_id")

    queue_reject_parser = queue_subparsers.add_parser("reject")
    queue_reject_parser.add_argument("draft_id")

    queue_edit_parser = queue_subparsers.add_parser("edit")
    queue_edit_parser.add_argument("draft_id")

    queue_subparsers.add_parser("auto-approve")
    queue_subparsers.add_parser("status")

    publish_parser = subparsers.add_parser("publish")
    publish_parser.add_argument("post_id")
    publish_parser.add_argument("--live", action="store_true")

    publish_next_parser = subparsers.add_parser("publish-next")
    publish_next_parser.add_argument("--live", action="store_true")

    logs_parser = subparsers.add_parser("logs")
    logs_parser.add_argument(
        "log_name",
        choices=[
            "events",
            "errors",
            "openai",
            "platform",
        ],
    )
    logs_parser.add_argument("--limit", required=False, type=int, default=20)

    report_parser = subparsers.add_parser("report")
    report_subparsers = report_parser.add_subparsers(
        dest="report_command",
        required=True,
    )

    daily_parser = report_subparsers.add_parser("daily")
    daily_parser.add_argument("--date", required=True)

    weekly_parser = report_subparsers.add_parser("weekly")
    weekly_parser.add_argument("--start", required=True)
    weekly_parser.add_argument("--end", required=True)

    return parser


def main() -> None:
    parser = build_parser()
    args = parser.parse_args()

    if args.command == "generate":
        run_generate(args)
        return

    if args.command == "queue":
        run_queue(args)
        return

    if args.command == "publish":
        run_publish(args)
        return

    if args.command == "publish-next":
        run_publish_next(args)
        return

    if args.command == "logs":
        run_logs(args)
        return

    if args.command == "report":
        run_report(args)
        return

    raise MainCliError("Unsupported command: " + str(args.command))


if __name__ == "__main__":
    main()

Kommandoreferenz

python -m src.main generate --platform linkedin --type workshop
python -m src.main queue list
python -m src.main queue list --status draft
python -m src.main queue show draft_20260512_001
python -m src.main queue edit draft_20260512_001
python -m src.main queue approve draft_20260512_001
python -m src.main queue reject draft_20260512_001
python -m src.main queue auto-approve
python -m src.main queue status
python -m src.main publish draft_20260512_001
python -m src.main publish draft_20260512_001 --live
python -m src.main publish-next
python -m src.main publish-next --live
python -m src.main logs events --limit 20
python -m src.main logs errors --limit 20
python -m src.main logs openai --limit 20
python -m src.main report daily --date 2026-05-12
python -m src.main report weekly --start 2026-05-11 --end 2026-05-17

Ergebnis dieses Kapitels

Die CLI stellt jetzt bereit:

Beitragserzeugung
Queue-Verwaltung
Freigabe
Ablehnung
Bearbeitung
Dry-Run
Live-Publishing
Queue-Status
Log-Anzeige
Tagesreport
Wochenreport

Damit ist das System ohne Weboberfläche bedienbar. Für das MVP reicht diese CLI vollständig aus.

0 Kommentare