aman/src/aman_cli.py
Thales Maciel 4d0081d1d0 Split aman.py into focused CLI and runtime modules
Break the old god module into flat siblings for CLI parsing, run lifecycle, daemon state, shared processing helpers, benchmark tooling, and maintainer-only model sync so changes stop sharing one giant import graph.

Keep aman as a thin shim over aman_cli, move sync-default-model behind the hidden aman-maint entrypoint plus Make wrappers, and update packaging metadata plus maintainer docs to reflect the new surface.

Retarget the tests to the new seams with dedicated runtime, run, benchmark, maintainer, and entrypoint suites, and verify with python3 -m unittest discover -s tests -p "test_*.py", python3 -m py_compile src/*.py tests/*.py, PYTHONPATH=src python3 -m aman --help, PYTHONPATH=src python3 -m aman version, and PYTHONPATH=src python3 -m aman_maint --help.
2026-03-14 14:54:57 -03:00

328 lines
10 KiB
Python

from __future__ import annotations
import argparse
import importlib.metadata
import json
import logging
import sys
from pathlib import Path
from config import Config, ConfigValidationError, save
from constants import DEFAULT_CONFIG_PATH
from diagnostics import (
format_diagnostic_line,
run_doctor,
run_self_check,
)
LEGACY_MAINT_COMMANDS = {"sync-default-model"}
def _local_project_version() -> str | None:
pyproject_path = Path(__file__).resolve().parents[1] / "pyproject.toml"
if not pyproject_path.exists():
return None
for line in pyproject_path.read_text(encoding="utf-8").splitlines():
stripped = line.strip()
if stripped.startswith('version = "'):
return stripped.split('"')[1]
return None
def app_version() -> str:
local_version = _local_project_version()
if local_version:
return local_version
try:
return importlib.metadata.version("aman")
except importlib.metadata.PackageNotFoundError:
return "0.0.0-dev"
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description=(
"Aman is an X11 dictation daemon for Linux desktops. "
"Use `run` for foreground setup/support, `doctor` for fast preflight "
"checks, and `self-check` for deeper installed-system readiness."
),
epilog=(
"Supported daily use is the systemd --user service. "
"For recovery: doctor -> self-check -> journalctl -> "
"aman run --verbose."
),
)
subparsers = parser.add_subparsers(dest="command")
run_parser = subparsers.add_parser(
"run",
help="run Aman in the foreground for setup, support, or debugging",
description="Run Aman in the foreground for setup, support, or debugging.",
)
run_parser.add_argument("--config", default="", help="path to config.json")
run_parser.add_argument("--dry-run", action="store_true", help="log hotkey only")
run_parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="enable verbose logs",
)
doctor_parser = subparsers.add_parser(
"doctor",
help="run fast preflight diagnostics for config and local environment",
description="Run fast preflight diagnostics for config and the local environment.",
)
doctor_parser.add_argument("--config", default="", help="path to config.json")
doctor_parser.add_argument("--json", action="store_true", help="print JSON output")
doctor_parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="enable verbose logs",
)
self_check_parser = subparsers.add_parser(
"self-check",
help="run deeper installed-system readiness diagnostics without modifying local state",
description=(
"Run deeper installed-system readiness diagnostics without modifying "
"local state."
),
)
self_check_parser.add_argument("--config", default="", help="path to config.json")
self_check_parser.add_argument("--json", action="store_true", help="print JSON output")
self_check_parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="enable verbose logs",
)
bench_parser = subparsers.add_parser(
"bench",
help="run the processing flow from input text without stt or injection",
)
bench_parser.add_argument("--config", default="", help="path to config.json")
bench_input = bench_parser.add_mutually_exclusive_group(required=True)
bench_input.add_argument("--text", default="", help="input transcript text")
bench_input.add_argument(
"--text-file",
default="",
help="path to transcript text file",
)
bench_parser.add_argument(
"--repeat",
type=int,
default=1,
help="number of measured runs",
)
bench_parser.add_argument(
"--warmup",
type=int,
default=1,
help="number of warmup runs",
)
bench_parser.add_argument("--json", action="store_true", help="print JSON output")
bench_parser.add_argument(
"--print-output",
action="store_true",
help="print final processed output text",
)
bench_parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="enable verbose logs",
)
eval_parser = subparsers.add_parser(
"eval-models",
help="evaluate model/parameter matrices against expected outputs",
)
eval_parser.add_argument(
"--dataset",
required=True,
help="path to evaluation dataset (.jsonl)",
)
eval_parser.add_argument(
"--matrix",
required=True,
help="path to model matrix (.json)",
)
eval_parser.add_argument(
"--heuristic-dataset",
default="",
help="optional path to heuristic alignment dataset (.jsonl)",
)
eval_parser.add_argument(
"--heuristic-weight",
type=float,
default=0.25,
help="weight for heuristic score contribution to combined ranking (0.0-1.0)",
)
eval_parser.add_argument(
"--report-version",
type=int,
default=2,
help="report schema version to emit",
)
eval_parser.add_argument(
"--output",
default="",
help="optional path to write full JSON report",
)
eval_parser.add_argument("--json", action="store_true", help="print JSON output")
eval_parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="enable verbose logs",
)
heuristic_builder = subparsers.add_parser(
"build-heuristic-dataset",
help="build a canonical heuristic dataset from a raw JSONL source",
)
heuristic_builder.add_argument(
"--input",
required=True,
help="path to raw heuristic dataset (.jsonl)",
)
heuristic_builder.add_argument(
"--output",
required=True,
help="path to canonical heuristic dataset (.jsonl)",
)
heuristic_builder.add_argument(
"--json",
action="store_true",
help="print JSON summary output",
)
heuristic_builder.add_argument(
"-v",
"--verbose",
action="store_true",
help="enable verbose logs",
)
subparsers.add_parser("version", help="print aman version")
init_parser = subparsers.add_parser("init", help="write a default config")
init_parser.add_argument("--config", default="", help="path to config.json")
init_parser.add_argument(
"--force",
action="store_true",
help="overwrite existing config",
)
return parser
def parse_cli_args(argv: list[str]) -> argparse.Namespace:
parser = build_parser()
normalized_argv = list(argv)
known_commands = {
"run",
"doctor",
"self-check",
"bench",
"eval-models",
"build-heuristic-dataset",
"version",
"init",
}
if normalized_argv and normalized_argv[0] in {"-h", "--help"}:
return parser.parse_args(normalized_argv)
if normalized_argv and normalized_argv[0] in LEGACY_MAINT_COMMANDS:
parser.error(
"`sync-default-model` moved to `aman-maint sync-default-model` "
"(or use `make sync-default-model`)."
)
if not normalized_argv or normalized_argv[0] not in known_commands:
normalized_argv = ["run", *normalized_argv]
return parser.parse_args(normalized_argv)
def configure_logging(verbose: bool) -> None:
logging.basicConfig(
stream=sys.stderr,
level=logging.DEBUG if verbose else logging.INFO,
format="aman: %(asctime)s %(levelname)s %(message)s",
)
def diagnostic_command(args, runner) -> int:
report = runner(args.config)
if args.json:
print(report.to_json())
else:
for check in report.checks:
print(format_diagnostic_line(check))
print(f"overall: {report.status}")
return 0 if report.ok else 2
def doctor_command(args) -> int:
return diagnostic_command(args, run_doctor)
def self_check_command(args) -> int:
return diagnostic_command(args, run_self_check)
def version_command(_args) -> int:
print(app_version())
return 0
def init_command(args) -> int:
config_path = Path(args.config) if args.config else DEFAULT_CONFIG_PATH
if config_path.exists() and not args.force:
logging.error(
"init failed: config already exists at %s (use --force to overwrite)",
config_path,
)
return 1
cfg = Config()
save(config_path, cfg)
logging.info("wrote default config to %s", config_path)
return 0
def main(argv: list[str] | None = None) -> int:
args = parse_cli_args(list(argv) if argv is not None else sys.argv[1:])
if args.command == "run":
configure_logging(args.verbose)
from aman_run import run_command
return run_command(args)
if args.command == "doctor":
configure_logging(args.verbose)
return diagnostic_command(args, run_doctor)
if args.command == "self-check":
configure_logging(args.verbose)
return diagnostic_command(args, run_self_check)
if args.command == "bench":
configure_logging(args.verbose)
from aman_benchmarks import bench_command
return bench_command(args)
if args.command == "eval-models":
configure_logging(args.verbose)
from aman_benchmarks import eval_models_command
return eval_models_command(args)
if args.command == "build-heuristic-dataset":
configure_logging(args.verbose)
from aman_benchmarks import build_heuristic_dataset_command
return build_heuristic_dataset_command(args)
if args.command == "version":
configure_logging(False)
return version_command(args)
if args.command == "init":
configure_logging(False)
return init_command(args)
raise RuntimeError(f"unsupported command: {args.command}")