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