diff --git a/src/cursor_flasher/cli.py b/src/cursor_flasher/cli.py new file mode 100644 index 0000000..a6c719d --- /dev/null +++ b/src/cursor_flasher/cli.py @@ -0,0 +1,103 @@ +"""CLI for starting/stopping the cursor-flasher daemon.""" +import argparse +import os +import signal +import sys +from pathlib import Path + +from cursor_flasher.daemon import run_daemon + +PID_FILE = Path.home() / ".cursor-flasher" / "daemon.pid" + + +def _write_pid() -> None: + PID_FILE.parent.mkdir(parents=True, exist_ok=True) + PID_FILE.write_text(str(os.getpid())) + + +def _read_pid() -> int | None: + if not PID_FILE.exists(): + return None + try: + pid = int(PID_FILE.read_text().strip()) + os.kill(pid, 0) + return pid + except (ValueError, ProcessLookupError, PermissionError): + PID_FILE.unlink(missing_ok=True) + return None + + +def _remove_pid() -> None: + PID_FILE.unlink(missing_ok=True) + + +def cmd_start(args: argparse.Namespace) -> None: + existing = _read_pid() + if existing is not None: + print(f"Daemon already running (PID {existing})") + sys.exit(1) + + if args.foreground: + _write_pid() + try: + run_daemon() + finally: + _remove_pid() + else: + pid = os.fork() + if pid > 0: + print(f"Daemon started (PID {pid})") + return + os.setsid() + _write_pid() + try: + run_daemon() + finally: + _remove_pid() + + +def cmd_stop(args: argparse.Namespace) -> None: + pid = _read_pid() + if pid is None: + print("Daemon is not running") + sys.exit(1) + + os.kill(pid, signal.SIGTERM) + _remove_pid() + print(f"Daemon stopped (PID {pid})") + + +def cmd_status(args: argparse.Namespace) -> None: + pid = _read_pid() + if pid is None: + print("Daemon is not running") + else: + print(f"Daemon is running (PID {pid})") + + +def main() -> None: + parser = argparse.ArgumentParser( + prog="cursor-flasher", + description="Flash the Cursor window when the AI agent is waiting for input", + ) + sub = parser.add_subparsers(dest="command") + + start_parser = sub.add_parser("start", help="Start the daemon") + start_parser.add_argument( + "--foreground", "-f", action="store_true", + help="Run in the foreground (don't daemonize)", + ) + start_parser.set_defaults(func=cmd_start) + + stop_parser = sub.add_parser("stop", help="Stop the daemon") + stop_parser.set_defaults(func=cmd_stop) + + status_parser = sub.add_parser("status", help="Check daemon status") + status_parser.set_defaults(func=cmd_status) + + args = parser.parse_args() + if not hasattr(args, "func"): + parser.print_help() + sys.exit(1) + + args.func(args)