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