1use clap::{Args, CommandFactory, Parser, Subcommand};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub(crate) enum CommandKind {
5 Version,
6 Check {
7 project: String,
8 },
9 Fmt {
10 project: String,
11 check: bool,
12 },
13 Install {
14 package: Option<String>,
15 project: String,
16 },
17 Build {
18 project: String,
19 mode: BuildMode,
20 },
21 Run {
22 project: String,
23 mode: BuildMode,
24 },
25 Clean {
26 project: String,
27 },
28 Init {
29 kind: InitKind,
30 path: Option<String>,
31 },
32 Xtazy {
33 subcommand: XtazySubcommand,
34 },
35 SelfUpdate,
36 Doctor,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub(crate) enum BuildMode {
41 Dev,
42 Prod,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub(crate) enum InitKind {
47 App,
48 Package,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub(crate) enum XtazySubcommand {
53 Install { version: Option<String> },
54 Update,
55 AutoUpdate { action: Option<AutoUpdateAction> },
56 UseVersion { version: String },
57 Active,
58 List,
59 Remove { version: String },
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub(crate) enum AutoUpdateAction {
64 Off,
65 Status,
66}
67
68#[derive(Parser, Debug)]
69#[command(
70 name = "dealer",
71 about = "Xtazy project orchestrator",
72 disable_version_flag = true
73)]
74struct DealerCli {
75 #[command(subcommand)]
76 command: DealerCommand,
77}
78
79#[derive(Subcommand, Debug)]
80enum DealerCommand {
81 Check(ProjectArg),
82 Fmt(FmtArgs),
83 Install(InstallArgs),
84 Build(BuildArgs),
85 Run(BuildArgs),
86 Clean(ProjectArg),
87 Init(InitArgs),
88 Xtazy(XtazyArgs),
89 #[command(name = "self")]
90 SelfCommand(SelfArgs),
91 Doctor,
92}
93
94#[derive(Args, Debug)]
95struct ProjectArg {
96 #[arg(default_value = ".")]
97 project: String,
98}
99
100#[derive(Args, Debug)]
101struct FmtArgs {
102 #[arg(long)]
103 check: bool,
104 #[arg(default_value = ".")]
105 project: String,
106}
107
108#[derive(Args, Debug)]
109struct InstallArgs {
110 package: Option<String>,
111 project: Option<String>,
112}
113
114#[derive(Args, Debug)]
115struct BuildArgs {
116 #[arg(long, conflicts_with = "prod")]
117 dev: bool,
118 #[arg(long, conflicts_with = "dev")]
119 prod: bool,
120 #[arg(default_value = ".")]
121 project: String,
122}
123
124#[derive(Args, Debug)]
125struct InitArgs {
126 #[command(subcommand)]
127 kind: InitCommand,
128}
129
130#[derive(Subcommand, Debug)]
131enum InitCommand {
132 App(InitPath),
133 Package(InitPath),
134}
135
136#[derive(Args, Debug)]
137struct InitPath {
138 path: Option<String>,
139}
140
141#[derive(Args, Debug)]
142struct XtazyArgs {
143 #[command(subcommand)]
144 command: XtazyCommand,
145}
146
147#[derive(Subcommand, Debug)]
148enum XtazyCommand {
149 Install(VersionArg),
150 Update,
151 AutoUpdate(AutoUpdateArgs),
152 Use(RequiredVersionArg),
153 Active,
154 List,
155 Remove(RequiredVersionArg),
156}
157
158#[derive(Args, Debug)]
159struct VersionArg {
160 version: Option<String>,
161}
162
163#[derive(Args, Debug)]
164struct RequiredVersionArg {
165 version: String,
166}
167
168#[derive(Args, Debug)]
169struct AutoUpdateArgs {
170 #[command(subcommand)]
171 action: Option<AutoUpdateCommand>,
172}
173
174#[derive(Subcommand, Debug)]
175enum AutoUpdateCommand {
176 Off,
177 Status,
178}
179
180#[derive(Args, Debug)]
181struct SelfArgs {
182 #[command(subcommand)]
183 command: SelfCommand,
184}
185
186#[derive(Subcommand, Debug)]
187enum SelfCommand {
188 Update,
189}
190
191pub(crate) fn parse_args(args: &[String]) -> Result<CommandKind, String> {
192 if args.len() == 1 {
193 return Err(DealerCli::command().render_usage().to_string());
194 }
195
196 if args.get(1).map(String::as_str) == Some("--version")
197 || args.get(1).map(String::as_str) == Some("-V")
198 {
199 return Ok(CommandKind::Version);
200 }
201
202 if args.len() == 2 && !args[1].starts_with('-') && !is_known_command(&args[1]) {
204 return Ok(CommandKind::Check {
205 project: args[1].clone(),
206 });
207 }
208
209 let cli = DealerCli::try_parse_from(args).map_err(|error| error.to_string())?;
210 Ok(command_from_cli(cli.command))
211}
212
213fn is_known_command(value: &str) -> bool {
214 matches!(
215 value,
216 "check"
217 | "fmt"
218 | "install"
219 | "build"
220 | "run"
221 | "clean"
222 | "init"
223 | "xtazy"
224 | "self"
225 | "doctor"
226 )
227}
228
229fn command_from_cli(command: DealerCommand) -> CommandKind {
230 match command {
231 DealerCommand::Check(args) => CommandKind::Check {
232 project: args.project,
233 },
234 DealerCommand::Fmt(args) => CommandKind::Fmt {
235 project: args.project,
236 check: args.check,
237 },
238 DealerCommand::Install(args) => install_command(args),
239 DealerCommand::Build(args) => CommandKind::Build {
240 project: args.project,
241 mode: mode_from_args(args.dev, args.prod),
242 },
243 DealerCommand::Run(args) => CommandKind::Run {
244 project: args.project,
245 mode: mode_from_args(args.dev, args.prod),
246 },
247 DealerCommand::Clean(args) => CommandKind::Clean {
248 project: args.project,
249 },
250 DealerCommand::Init(args) => init_command(args.kind),
251 DealerCommand::Xtazy(args) => CommandKind::Xtazy {
252 subcommand: xtazy_command(args.command),
253 },
254 DealerCommand::SelfCommand(args) => match args.command {
255 SelfCommand::Update => CommandKind::SelfUpdate,
256 },
257 DealerCommand::Doctor => CommandKind::Doctor,
258 }
259}
260
261fn install_command(args: InstallArgs) -> CommandKind {
262 match (args.package, args.project) {
263 (None, None) => CommandKind::Install {
264 package: None,
265 project: ".".to_string(),
266 },
267 (Some(package), None) if looks_like_project_path(&package) => CommandKind::Install {
268 package: None,
269 project: package,
270 },
271 (Some(package), None) => CommandKind::Install {
272 package: Some(package),
273 project: ".".to_string(),
274 },
275 (package, Some(project)) => CommandKind::Install { package, project },
276 }
277}
278
279fn looks_like_project_path(value: &str) -> bool {
280 value == "." || value.contains('/') || std::path::Path::new(value).is_dir()
281}
282
283fn mode_from_args(_dev: bool, prod: bool) -> BuildMode {
284 if prod {
285 BuildMode::Prod
286 } else {
287 BuildMode::Dev
288 }
289}
290
291fn init_command(command: InitCommand) -> CommandKind {
292 match command {
293 InitCommand::App(path) => CommandKind::Init {
294 kind: InitKind::App,
295 path: path.path,
296 },
297 InitCommand::Package(path) => CommandKind::Init {
298 kind: InitKind::Package,
299 path: path.path,
300 },
301 }
302}
303
304fn xtazy_command(command: XtazyCommand) -> XtazySubcommand {
305 match command {
306 XtazyCommand::Install(args) => XtazySubcommand::Install {
307 version: args.version,
308 },
309 XtazyCommand::Update => XtazySubcommand::Update,
310 XtazyCommand::AutoUpdate(args) => XtazySubcommand::AutoUpdate {
311 action: args.action.map(|action| match action {
312 AutoUpdateCommand::Off => AutoUpdateAction::Off,
313 AutoUpdateCommand::Status => AutoUpdateAction::Status,
314 }),
315 },
316 XtazyCommand::Use(args) => XtazySubcommand::UseVersion {
317 version: args.version,
318 },
319 XtazyCommand::Active => XtazySubcommand::Active,
320 XtazyCommand::List => XtazySubcommand::List,
321 XtazyCommand::Remove(args) => XtazySubcommand::Remove {
322 version: args.version,
323 },
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 fn args(values: &[&str]) -> Vec<String> {
332 values.iter().map(|value| value.to_string()).collect()
333 }
334
335 #[test]
336 fn parse_version_flag() {
337 let cmd = parse_args(&args(&["dealer", "--version"])).expect("version should parse");
338
339 assert_eq!(cmd, CommandKind::Version);
340 }
341
342 #[test]
343 fn parse_defaults_to_check_for_single_project_argument() {
344 let cmd =
345 parse_args(&args(&["dealer", "project"])).expect("single project arg should parse");
346
347 assert_eq!(
348 cmd,
349 CommandKind::Check {
350 project: "project".to_string()
351 }
352 );
353 }
354
355 #[test]
356 fn parse_check_defaults_to_current_directory() {
357 let cmd = parse_args(&args(&["dealer", "check"])).expect("check should parse");
358
359 assert_eq!(
360 cmd,
361 CommandKind::Check {
362 project: ".".to_string()
363 }
364 );
365 }
366
367 #[test]
368 fn parse_build_prod_command() {
369 let cmd = parse_args(&args(&["dealer", "build", "--prod", "project"]))
370 .expect("build command should parse");
371
372 assert_eq!(
373 cmd,
374 CommandKind::Build {
375 project: "project".to_string(),
376 mode: BuildMode::Prod
377 }
378 );
379 }
380
381 #[test]
382 fn parse_install_package_and_project() {
383 let cmd = parse_args(&args(&["dealer", "install", "foo", "project"]))
384 .expect("install command should parse");
385
386 assert_eq!(
387 cmd,
388 CommandKind::Install {
389 package: Some("foo".to_string()),
390 project: "project".to_string()
391 }
392 );
393 }
394
395 #[test]
396 fn parse_xtazy_auto_update_status() {
397 let cmd = parse_args(&args(&["dealer", "xtazy", "auto-update", "status"]))
398 .expect("auto-update status should parse");
399
400 assert_eq!(
401 cmd,
402 CommandKind::Xtazy {
403 subcommand: XtazySubcommand::AutoUpdate {
404 action: Some(AutoUpdateAction::Status)
405 }
406 }
407 );
408 }
409
410 #[test]
411 fn parse_self_update() {
412 let cmd =
413 parse_args(&args(&["dealer", "self", "update"])).expect("self update should parse");
414
415 assert_eq!(cmd, CommandKind::SelfUpdate);
416 }
417}