134 lines
4.4 KiB
Python
Executable file
134 lines
4.4 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""ACS menu bar app with local commit agent.
|
|
|
|
Manages a lightweight commit agent that discovers local repos, asks the
|
|
remote ACS daemon for LLM-generated commit messages, and commits+pushes.
|
|
Runs without the full auto_commit_service package — only needs httpx + rumps.
|
|
|
|
Usage:
|
|
./commits-tray --url http://apricot.local:8200 --cycle 300
|
|
./commits-tray --url http://apricot.local:8200 --commit-local --dry-run
|
|
./commits-tray --url http://apricot.local:8200 --commit-local # for real
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import logging
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Add the tray module directory so we can import directly
|
|
_script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
_tray_dir = os.path.join(_script_dir, "src", "auto_commit_service", "tray")
|
|
sys.path.insert(0, _tray_dir)
|
|
|
|
|
|
def _load_ignore_repos_from_config() -> list[str]:
|
|
"""Read ignore_repos from ~/.config/commits/startup-config.json.
|
|
|
|
Mirrors the daemon's config loader (see auto_commit_service.__main__).
|
|
Supports either a top-level `ignore_repos` array (shared) or a per-daemon
|
|
entry whose `directory` matches the current working dir. Missing or
|
|
unreadable files yield an empty list — never raises.
|
|
"""
|
|
config_path = Path.home() / ".config" / "commits" / "startup-config.json"
|
|
if not config_path.exists():
|
|
return []
|
|
try:
|
|
data = json.loads(config_path.read_text())
|
|
except (OSError, json.JSONDecodeError) as exc:
|
|
logging.warning(f"commits-tray: failed to read {config_path}: {exc}")
|
|
return []
|
|
|
|
ignore: list[str] = []
|
|
if isinstance(data.get("ignore_repos"), list):
|
|
ignore.extend(str(p) for p in data["ignore_repos"] if p)
|
|
|
|
cwd = str(Path.cwd())
|
|
for daemon in data.get("daemons", []) or []:
|
|
if isinstance(daemon, dict) and daemon.get("directory") == cwd:
|
|
entries = daemon.get("ignore_repos")
|
|
if isinstance(entries, list):
|
|
ignore.extend(str(p) for p in entries if p)
|
|
break
|
|
|
|
# De-dup while preserving order
|
|
seen: set[str] = set()
|
|
deduped: list[str] = []
|
|
for p in ignore:
|
|
if p not in seen:
|
|
seen.add(p)
|
|
deduped.append(p)
|
|
return deduped
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="ACS menu bar app + local commit agent")
|
|
parser.add_argument(
|
|
"--url", "-u",
|
|
default="http://apricot.local:8200",
|
|
help="Remote ACS daemon URL for LLM + recording (default: http://apricot.local:8200)",
|
|
)
|
|
parser.add_argument(
|
|
"--repos", "-r",
|
|
nargs="+",
|
|
default=None,
|
|
help="Base paths to scan for local git repos (default: ~/Code)",
|
|
)
|
|
parser.add_argument(
|
|
"--cycle", "-c",
|
|
type=int,
|
|
default=300,
|
|
help="Seconds between commit cycles (default: 300)",
|
|
)
|
|
parser.add_argument(
|
|
"--commit-local",
|
|
action="store_true",
|
|
default=False,
|
|
help="Enable local commit loop on this host (proxy mode). Default OFF.",
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
default=False,
|
|
help="Scan and generate messages but skip git commit/push. Useful for validation.",
|
|
)
|
|
parser.add_argument(
|
|
"--max-diff-bytes",
|
|
type=int,
|
|
default=131072,
|
|
help="Per-repo diff size cap in bytes before truncation (default: 131072)",
|
|
)
|
|
parser.add_argument(
|
|
"--ignore-repo",
|
|
action="append",
|
|
default=[],
|
|
dest="ignore_repos",
|
|
help=(
|
|
"Substring matched against absolute repo paths; matching repos "
|
|
"are excluded from discovery. Repeat for multiple. Merged with "
|
|
"ignore_repos from ~/.config/commits/startup-config.json."
|
|
),
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
ignore_repos = list(_load_ignore_repos_from_config()) + list(args.ignore_repos or [])
|
|
|
|
# Import after argparse so --help exits cleanly before rumps is required
|
|
from app import run_tray # noqa: E402
|
|
|
|
repos_paths = [Path(p).expanduser() for p in args.repos] if args.repos else None
|
|
run_tray(
|
|
daemon_url=args.url,
|
|
repos_paths=repos_paths,
|
|
commit_local=args.commit_local,
|
|
dry_run=args.dry_run,
|
|
max_diff_bytes=args.max_diff_bytes,
|
|
cycle_seconds=args.cycle,
|
|
ignore_repos=ignore_repos,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|