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