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#[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
124pub(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}