diff --git a/src/reyna_cli/cli.py b/src/reyna_cli/cli.py index ebd2174..9315d33 100644 --- a/src/reyna_cli/cli.py +++ b/src/reyna_cli/cli.py @@ -209,6 +209,20 @@ def gitea_repo(name: str, json_output: bool = typer.Option(False, "--json")): fail(str(exc), json_output) +def run_remindctl(args: List[str]) -> Any: + """Execute the supported Reminders CLI without invoking a shell.""" + result = subprocess.run(["remindctl", *args], capture_output=True, text=True, check=False) + if result.returncode: + raise RuntimeError(result.stderr.strip() or result.stdout.strip() or "remindctl command failed") + output = result.stdout.strip() + if not output: + return {} + try: + return json.loads(output) + except json.JSONDecodeError: + return {"output": output} + + def get_required_device(name: str, json_output: bool = False) -> Device: device = get_device(name) if not device: @@ -2139,6 +2153,56 @@ def macmini_reminders_create( fail(str(exc), json_output) +@macmini_reminders_app.command("edit") +def macmini_reminders_edit( + reminder_id: str, + title: Optional[str] = typer.Option(None, "--title"), + list_name: Optional[str] = typer.Option(None, "--list"), + due: Optional[str] = typer.Option(None, "--due"), + notes: Optional[str] = typer.Option(None, "--notes"), + priority: Optional[str] = typer.Option(None, "--priority"), + clear_due: bool = typer.Option(False, "--clear-due"), + complete: bool = typer.Option(False, "--complete"), + incomplete: bool = typer.Option(False, "--incomplete"), + json_output: bool = typer.Option(False, "--json"), +): + """Edit a reminder by its ID or ID prefix returned by ``reminders list``.""" + if complete and incomplete: + fail("Use only one of --complete or --incomplete", json_output) + if not any((title is not None, list_name is not None, due is not None, notes is not None, priority is not None, clear_due, complete, incomplete)): + fail("Specify at least one field to edit", json_output) + args = ["edit", reminder_id] + for flag, value in (("--title", title), ("--list", list_name), ("--due", due), ("--notes", notes), ("--priority", priority)): + if value is not None: + args.extend([flag, value]) + if clear_due: + args.append("--clear-due") + if complete: + args.append("--complete") + if incomplete: + args.append("--incomplete") + args.extend(["--json", "--no-input"]) + try: + emit({"ok": True, "source": "remindctl", "result": run_remindctl(args)}, json_output) + except Exception as exc: + fail(str(exc), json_output) + + +@macmini_reminders_app.command("delete") +def macmini_reminders_delete( + reminder_ids: List[str] = typer.Argument(..., help="Reminder IDs or ID prefixes returned by reminders list."), + force: bool = typer.Option(False, "--force", help="Required: permanently delete without an interactive prompt."), + json_output: bool = typer.Option(False, "--json"), +): + """Permanently delete one or more reminders; requires explicit --force.""" + if not force: + fail("Deletion requires --force", json_output) + try: + emit({"ok": True, "source": "remindctl", "result": run_remindctl(["delete", *reminder_ids, "--force", "--json", "--no-input"])}, json_output) + except Exception as exc: + fail(str(exc), json_output) + + def emit_deco_result(tool: str, result: Dict[str, Any], json_output: bool = False) -> None: emit({"ok": True, "source": "direct", "tool": tool, "result": result}, json_output) diff --git a/tests/test_reminders.py b/tests/test_reminders.py index 5a76ceb..befd2d4 100644 --- a/tests/test_reminders.py +++ b/tests/test_reminders.py @@ -248,6 +248,42 @@ def test_cli_reminders_no_mcp_tool_call_remaining(): assert 'call_macmini_tool("reminders_create"' not in src +def test_cli_reminders_edit_uses_remindctl(monkeypatch): + captured = {} + + def fake_run(args): + captured["args"] = args + return {"updated": {"id": "r1", "title": "New title"}} + + monkeypatch.setattr("reyna_cli.cli.run_remindctl", fake_run) + res = runner.invoke(app, ["macmini", "reminders", "edit", "r1", "--title", "New title", "--due", "tomorrow", "--json"]) + + assert res.exit_code == 0 + assert captured["args"] == ["edit", "r1", "--title", "New title", "--due", "tomorrow", "--json", "--no-input"] + assert json.loads(res.stdout)["source"] == "remindctl" + + +def test_cli_reminders_delete_requires_force(monkeypatch): + res = runner.invoke(app, ["macmini", "reminders", "delete", "r1", "--json"]) + + assert res.exit_code == 1 + assert "--force" in res.stdout + + +def test_cli_reminders_delete_uses_remindctl_with_force(monkeypatch): + captured = {} + + def fake_run(args): + captured["args"] = args + return {"deleted": ["r1"]} + + monkeypatch.setattr("reyna_cli.cli.run_remindctl", fake_run) + res = runner.invoke(app, ["macmini", "reminders", "delete", "r1", "--force", "--json"]) + + assert res.exit_code == 0 + assert captured["args"] == ["delete", "r1", "--force", "--json", "--no-input"] + + def test_privacy_host_cli_has_reminders_authorize(): res = runner.invoke(app, ["privacy-host", "--help"]) assert res.exit_code == 0