Skip to main content

omni_dev/cli/
daemon.rs

1//! `omni-dev daemon` — supervise the long-lived daemon and its services.
2
3pub(crate) mod control;
4pub(crate) mod restart;
5pub(crate) mod run;
6pub(crate) mod start;
7pub(crate) mod status;
8pub(crate) mod stop;
9
10use anyhow::Result;
11use clap::{Parser, Subcommand};
12
13/// Daemon: host long-lived services (e.g. the browser bridge) under one
14/// supervised, menu-bar-controllable process.
15#[derive(Parser)]
16pub struct DaemonCommand {
17    /// The daemon subcommand to execute.
18    #[command(subcommand)]
19    pub command: DaemonSubcommands,
20}
21
22/// Daemon subcommands.
23#[derive(Subcommand)]
24pub enum DaemonSubcommands {
25    /// Runs the daemon in the foreground (the process launchd execs).
26    Run(run::RunCommand),
27    /// Starts the daemon in the background.
28    Start(start::StartCommand),
29    /// Stops the running daemon.
30    Stop(stop::StopCommand),
31    /// Restarts the daemon.
32    Restart(restart::RestartCommand),
33    /// Reports daemon and per-service status.
34    Status(status::StatusCommand),
35}
36
37impl DaemonCommand {
38    /// Executes the daemon command.
39    pub async fn execute(self) -> Result<()> {
40        match self.command {
41            DaemonSubcommands::Run(cmd) => cmd.execute().await,
42            DaemonSubcommands::Start(cmd) => cmd.execute().await,
43            DaemonSubcommands::Stop(cmd) => cmd.execute().await,
44            DaemonSubcommands::Restart(cmd) => cmd.execute().await,
45            DaemonSubcommands::Status(cmd) => cmd.execute().await,
46        }
47    }
48}
49
50#[cfg(test)]
51#[allow(clippy::unwrap_used, clippy::expect_used)]
52mod tests {
53    use super::*;
54    use std::path::Path;
55
56    /// Mirrors the `omni-dev daemon` argv surface for parse tests.
57    #[derive(Parser)]
58    struct Wrapper {
59        #[command(subcommand)]
60        cmd: DaemonSubcommands,
61    }
62
63    fn parse(args: &[&str]) -> DaemonSubcommands {
64        let mut full = vec!["omni-dev"];
65        full.extend_from_slice(args);
66        Wrapper::try_parse_from(full).unwrap().cmd
67    }
68
69    #[test]
70    fn parses_all_subcommands() {
71        assert!(matches!(parse(&["run"]), DaemonSubcommands::Run(_)));
72        assert!(matches!(parse(&["start"]), DaemonSubcommands::Start(_)));
73        assert!(matches!(parse(&["stop"]), DaemonSubcommands::Stop(_)));
74        assert!(matches!(parse(&["restart"]), DaemonSubcommands::Restart(_)));
75        assert!(matches!(parse(&["status"]), DaemonSubcommands::Status(_)));
76    }
77
78    #[test]
79    fn socket_override_parses() {
80        let DaemonSubcommands::Run(cmd) = parse(&["run", "--socket", "/tmp/x.sock"]) else {
81            panic!("expected run");
82        };
83        assert_eq!(cmd.socket.as_deref(), Some(Path::new("/tmp/x.sock")));
84    }
85
86    #[test]
87    fn status_json_flag_parses() {
88        let DaemonSubcommands::Status(cmd) = parse(&["status", "--json"]) else {
89            panic!("expected status");
90        };
91        assert!(cmd.json);
92    }
93
94    #[test]
95    fn socket_defaults_to_none() {
96        let DaemonSubcommands::Status(cmd) = parse(&["status"]) else {
97            panic!("expected status");
98        };
99        assert!(cmd.socket.is_none());
100        assert!(!cmd.json);
101    }
102
103    /// `daemon status` against a socket with no daemon dispatches through to the
104    /// "not running" path (table and `--json`) without erroring or side effects.
105    #[tokio::test]
106    async fn status_dispatch_reports_not_running() {
107        let dir = tempfile::tempdir().unwrap();
108        let socket = dir.path().join("absent.sock");
109        for json in [false, true] {
110            let cmd = DaemonCommand {
111                command: DaemonSubcommands::Status(status::StatusCommand {
112                    socket: Some(socket.clone()),
113                    json,
114                }),
115            };
116            cmd.execute().await.unwrap();
117        }
118    }
119}