Skip to main content

pitchfork_cli/cli/
mod.rs

1use crate::Result;
2use clap::Parser;
3use std::ffi::OsString;
4
5mod activate;
6mod api_schema;
7mod boot;
8mod cd;
9mod clean;
10mod completion;
11mod daemons;
12mod disable;
13mod enable;
14mod json_output;
15mod list;
16pub mod logs;
17mod mcp;
18mod proxy;
19mod restart;
20mod run;
21mod schema;
22mod settings;
23mod sponsors;
24mod start;
25mod status;
26mod stop;
27mod supervisor;
28mod tui;
29mod usage;
30mod wait;
31
32#[derive(Debug, clap::Parser)]
33#[clap(name = "pitchfork", version = env!("CARGO_PKG_VERSION"), about = env!("CARGO_PKG_DESCRIPTION"))]
34struct Cli {
35    #[clap(subcommand)]
36    command: Commands,
37}
38
39#[derive(Debug, clap::Subcommand)]
40#[allow(clippy::large_enum_variant)]
41enum Commands {
42    Activate(activate::Activate),
43    ApiSchema(api_schema::ApiSchema),
44    Boot(boot::Boot),
45    Cd(cd::Cd),
46    Clean(clean::Clean),
47    Daemons(daemons::Daemons),
48    Completion(completion::Completion),
49    Disable(disable::Disable),
50    Enable(enable::Enable),
51    List(list::List),
52    Logs(logs::Logs),
53    Mcp(mcp::Mcp),
54    Proxy(proxy::Proxy),
55    Restart(restart::Restart),
56    Run(run::Run),
57    Schema(schema::Schema),
58    Settings(settings::Settings),
59    Sponsors(sponsors::Sponsors),
60    Start(start::Start),
61    Status(status::Status),
62    Stop(stop::Stop),
63    Supervisor(supervisor::Supervisor),
64    Tui(tui::Tui),
65    Usage(usage::Usage),
66    Wait(wait::Wait),
67    #[clap(external_subcommand)]
68    Fallback(Vec<OsString>),
69}
70
71/// Parses tokens captured by the implicit subcommand fallback as a
72/// `pitchfork start` invocation, so usage/help/error output reflects that.
73#[derive(Debug, clap::Parser)]
74#[clap(
75    name = "pitchfork",
76    bin_name = "pitchfork start",
77    version = env!("CARGO_PKG_VERSION"),
78    long_about = start::LONG_ABOUT
79)]
80struct StartFallback {
81    #[clap(flatten)]
82    start: start::Start,
83}
84
85pub async fn run() -> Result<()> {
86    let args = Cli::parse();
87    let program = std::env::args_os()
88        .next()
89        .unwrap_or_else(|| "pitchfork".into());
90    match args.command {
91        Commands::Activate(activate) => activate.run().await,
92        Commands::Boot(boot) => boot.run().await,
93        Commands::Cd(cd) => cd.run().await,
94        Commands::Clean(clean) => clean.run().await,
95        Commands::Daemons(daemons) => daemons.run().await,
96        Commands::Completion(completion) => completion.run().await,
97        Commands::Disable(disable) => disable.run().await,
98        Commands::Enable(enable) => enable.run().await,
99        Commands::List(list) => list.run().await,
100        Commands::Logs(logs) => logs.run().await,
101        Commands::Mcp(mcp) => mcp.run().await,
102        Commands::Proxy(proxy) => proxy.run().await,
103        Commands::Restart(restart) => restart.run().await,
104        Commands::Run(run) => run.run().await,
105        Commands::ApiSchema(api_schema) => api_schema.run().await,
106        Commands::Schema(schema) => schema.run().await,
107        Commands::Settings(settings) => settings.run().await,
108        Commands::Sponsors(_) => sponsors::Sponsors::run().await,
109        Commands::Start(start) => start.run().await,
110        Commands::Status(status) => status.run().await,
111        Commands::Stop(stop) => stop.run().await,
112        Commands::Supervisor(supervisor) => supervisor.run().await,
113        Commands::Tui(tui) => tui.run().await,
114        Commands::Usage(usage) => usage.run().await,
115        Commands::Wait(wait) => wait.run().await,
116        Commands::Fallback(tokens) => {
117            let mut argv = vec![program];
118            argv.extend(tokens);
119            StartFallback::parse_from(argv).start.run().await
120        }
121    }
122}
123
124/// Drain and display any pending notifications from the supervisor.
125///
126/// Notifications are queued by the supervisor for events that happen
127/// asynchronously (e.g. proxy bind failure) and would otherwise be invisible
128/// to CLI users.  Call this at the end of user-facing commands that connect
129/// to the supervisor via IPC.
130pub(crate) async fn drain_notifications(ipc: &crate::ipc::client::IpcClient) {
131    use log::LevelFilter;
132    if let Ok(notifications) = ipc.get_notifications().await {
133        for (level, msg) in notifications {
134            match level {
135                LevelFilter::Trace => trace!("{msg}"),
136                LevelFilter::Debug => debug!("{msg}"),
137                LevelFilter::Info => info!("{msg}"),
138                LevelFilter::Warn => warn!("{msg}"),
139                LevelFilter::Error => error!("{msg}"),
140                _ => {}
141            }
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use clap::Parser;
150
151    #[test]
152    fn unknown_subcommand_captured_as_fallback() {
153        let args = Cli::parse_from(["pitchfork", "mydaemon", "--force"]);
154        match args.command {
155            Commands::Fallback(tokens) => {
156                assert_eq!(
157                    tokens,
158                    vec![OsString::from("mydaemon"), OsString::from("--force")]
159                );
160            }
161            _ => panic!("expected Fallback variant, got {:?}", args.command),
162        }
163    }
164
165    #[test]
166    fn unknown_subcommand_captures_multiple_args() {
167        let args = Cli::parse_from(["pitchfork", "api", "worker", "--force"]);
168        match args.command {
169            Commands::Fallback(tokens) => {
170                assert_eq!(
171                    tokens,
172                    vec![
173                        OsString::from("api"),
174                        OsString::from("worker"),
175                        OsString::from("--force")
176                    ]
177                );
178            }
179            _ => panic!("expected Fallback variant, got {:?}", args.command),
180        }
181    }
182
183    #[test]
184    fn known_start_parses_as_start() {
185        let args = Cli::parse_from(["pitchfork", "start", "mydaemon"]);
186        match args.command {
187            Commands::Start(_) => {}
188            _ => panic!("expected Start variant, got {:?}", args.command),
189        }
190    }
191
192    #[test]
193    fn start_alias_still_works() {
194        let args = Cli::parse_from(["pitchfork", "s", "mydaemon"]);
195        match args.command {
196            Commands::Start(_) => {}
197            _ => panic!("expected Start variant, got {:?}", args.command),
198        }
199    }
200
201    #[test]
202    fn fallback_reparse_as_start() {
203        StartFallback::try_parse_from(["pitchfork", "mydaemon", "--force"])
204            .expect("should re-parse captured tokens as Start");
205    }
206
207    #[test]
208    fn fallback_reparse_rejects_invalid_start_flag() {
209        let result = StartFallback::try_parse_from(["pitchfork", "mydaemon", "--not-a-start-flag"]);
210        assert!(
211            result.is_err(),
212            "expected re-parse to fail for invalid Start flag"
213        );
214    }
215
216    #[test]
217    fn fallback_invalid_start_usage_renders_pitchfork_start() {
218        let err = StartFallback::try_parse_from(["pitchfork", "mydaemon", "--not-a-start-flag"])
219            .unwrap_err();
220        let rendered = err.to_string();
221        assert!(
222            rendered.contains("Usage: pitchfork start"),
223            "expected usage to contain 'pitchfork start', got: {rendered}"
224        );
225    }
226
227    #[test]
228    fn fallback_help_shows_start_long_about() {
229        let err = StartFallback::try_parse_from(["pitchfork", "mydaemon", "--help"]).unwrap_err();
230        let rendered = err.to_string();
231        assert!(
232            rendered.contains("Examples:"),
233            "expected help to include Start long_about examples, got: {rendered}"
234        );
235        assert!(
236            rendered.contains("pitchfork start api"),
237            "expected help to reference `pitchfork start api`, got: {rendered}"
238        );
239    }
240}