164 lines
6.1 KiB
Python
164 lines
6.1 KiB
Python
"""CLI contracts for the capture utility."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from collections.abc import Sequence
|
|
from dataclasses import replace
|
|
from pathlib import Path
|
|
|
|
from game_data_capture.config import AppConfig, ConfigError, ServerConfig, load_config
|
|
from game_data_capture.doctor import doctor_as_dict, run_doctor
|
|
from game_data_capture.paths import PathConfigurationError, RuntimePaths, resolve_runtime_paths
|
|
from game_data_capture.runtime import CaptureRuntime, RuntimeFailure
|
|
|
|
EXIT_OK = 0
|
|
EXIT_CONFIG_ERROR = 2
|
|
EXIT_UNIMPLEMENTED = 3
|
|
EXIT_RUNTIME_ERROR = 4
|
|
|
|
|
|
def _add_config_argument(parser: argparse.ArgumentParser) -> None:
|
|
parser.add_argument("--config", type=Path, help="absolute path to a TOML configuration file")
|
|
|
|
|
|
def _port_argument(raw_value: str) -> int:
|
|
try:
|
|
port = int(raw_value)
|
|
except ValueError as error:
|
|
raise argparse.ArgumentTypeError("port must be an integer from 1024 to 65535") from error
|
|
if not 1024 <= port <= 65535:
|
|
raise argparse.ArgumentTypeError("port must be an integer from 1024 to 65535")
|
|
return port
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
"""Build the stable command surface without initializing runtime resources."""
|
|
|
|
parser = argparse.ArgumentParser(
|
|
prog="shd-capture", description="Local SHD.build game-data capture"
|
|
)
|
|
subcommands = parser.add_subparsers(dest="command", required=True)
|
|
|
|
doctor = subcommands.add_parser("doctor", help="report local prerequisite status")
|
|
_add_config_argument(doctor)
|
|
doctor.add_argument("--json", action="store_true", help="emit JSON diagnostics")
|
|
|
|
serve = subcommands.add_parser("serve", help="start the localhost review service")
|
|
_add_config_argument(serve)
|
|
serve.add_argument("--host", help="loopback IP literal override")
|
|
serve.add_argument("--port", type=_port_argument, help="TCP port override")
|
|
|
|
watch = subcommands.add_parser("watch", help="watch the Wayland clipboard for images")
|
|
_add_config_argument(watch)
|
|
watch_modes = watch.add_mutually_exclusive_group()
|
|
watch_modes.add_argument("--arm", action="store_true", help="arm live clipboard capture")
|
|
watch_modes.add_argument(
|
|
"--dry-run", action="store_true", help="validate the capture runtime plan"
|
|
)
|
|
|
|
ingest_image = subcommands.add_parser("ingest-image", help="stage one image for capture")
|
|
_add_config_argument(ingest_image)
|
|
ingest_image.add_argument("image", type=Path)
|
|
|
|
import_video = subcommands.add_parser("import-video", help="stage a video for frame extraction")
|
|
_add_config_argument(import_video)
|
|
import_video.add_argument("video", type=Path)
|
|
|
|
export = subcommands.add_parser("export", help="write a versioned ISAC handoff")
|
|
_add_config_argument(export)
|
|
export.add_argument("destination", type=Path)
|
|
|
|
verify = subcommands.add_parser("verify", help="verify an evidence archive")
|
|
_add_config_argument(verify)
|
|
verify.add_argument("archive", type=Path)
|
|
|
|
evaluate = subcommands.add_parser("eval", help="evaluate local OCR and classification")
|
|
_add_config_argument(evaluate)
|
|
evaluate.add_argument("corpus", type=Path)
|
|
|
|
purge = subcommands.add_parser("purge", help="purge retained local data")
|
|
_add_config_argument(purge)
|
|
purge.add_argument("--confirm", action="store_true", help="confirm a future destructive purge")
|
|
|
|
return parser
|
|
|
|
|
|
def _load_cli_config(config_path: Path | None) -> tuple[AppConfig, RuntimePaths]:
|
|
paths = resolve_runtime_paths()
|
|
return load_config(config_path=config_path, paths=paths), paths
|
|
|
|
|
|
def _doctor(arguments: argparse.Namespace) -> int:
|
|
config, paths = _load_cli_config(arguments.config)
|
|
diagnostics = doctor_as_dict(run_doctor(config, paths))
|
|
if arguments.json:
|
|
print(json.dumps(diagnostics, separators=(",", ":"), sort_keys=True))
|
|
else:
|
|
for diagnostic in diagnostics:
|
|
print(f"{diagnostic['name']}: {diagnostic['status']} ({diagnostic['detail']})")
|
|
return EXIT_OK
|
|
|
|
|
|
def _serve_contract(arguments: argparse.Namespace) -> int:
|
|
config, paths = _load_cli_config(arguments.config)
|
|
host = arguments.host if arguments.host is not None else config.server.host
|
|
port = arguments.port if arguments.port is not None else config.server.port
|
|
configured = replace(config, server=ServerConfig(host=host, port=port))
|
|
CaptureRuntime(configured, paths).serve()
|
|
return EXIT_OK
|
|
|
|
|
|
def _unimplemented(command: str) -> int:
|
|
print(
|
|
f"{command}: subsystem is not implemented; requires gdc-runtime-integration",
|
|
file=sys.stderr,
|
|
)
|
|
return EXIT_UNIMPLEMENTED
|
|
|
|
|
|
def _watch(arguments: argparse.Namespace) -> int:
|
|
config, paths = _load_cli_config(arguments.config)
|
|
if not arguments.arm and not arguments.dry_run:
|
|
return _unimplemented("watch")
|
|
runtime = CaptureRuntime(config, paths)
|
|
if arguments.dry_run:
|
|
runtime.validate_plan()
|
|
print("watch dry-run: capture runtime plan is valid")
|
|
return EXIT_OK
|
|
runtime.watch()
|
|
return EXIT_OK
|
|
|
|
|
|
def _ingest_image(arguments: argparse.Namespace) -> int:
|
|
config, paths = _load_cli_config(arguments.config)
|
|
CaptureRuntime(config, paths).ingest_image(arguments.image)
|
|
print("image capture stored")
|
|
return EXIT_OK
|
|
|
|
|
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
"""Run one CLI command and return a stable process exit status."""
|
|
|
|
parser = build_parser()
|
|
arguments = parser.parse_args(argv)
|
|
try:
|
|
if arguments.command == "doctor":
|
|
return _doctor(arguments)
|
|
if arguments.command == "serve":
|
|
return _serve_contract(arguments)
|
|
if arguments.command == "watch":
|
|
return _watch(arguments)
|
|
if arguments.command == "ingest-image":
|
|
return _ingest_image(arguments)
|
|
_load_cli_config(arguments.config)
|
|
return _unimplemented(arguments.command)
|
|
except (ConfigError, PathConfigurationError) as error:
|
|
print(f"configuration error: {error}", file=sys.stderr)
|
|
return EXIT_CONFIG_ERROR
|
|
except RuntimeFailure:
|
|
print("capture runtime failed", file=sys.stderr)
|
|
return EXIT_RUNTIME_ERROR
|