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