1use std::path::PathBuf;
9
10use clap::{Args, Parser, Subcommand};
11
12#[derive(Debug, Parser)]
14#[command(name = "frame", version, about = "Frame application tooling")]
15pub struct Cli {
16 #[command(subcommand)]
18 pub command: Command,
19}
20
21#[derive(Debug, Subcommand)]
23pub enum Command {
24 New {
26 name: String,
28 },
29 Run,
31 Dev {
35 #[arg(long, default_value_t = 100)]
38 quiet_ms: u64,
39 },
40 Test(TestScope),
43 Check,
46 Doctor,
49 Build,
51 Host {
54 #[arg(long)]
56 config: PathBuf,
57 },
58}
59
60#[derive(Args, Clone, Copy, Debug, Default)]
64pub struct TestScope {
65 #[arg(long)]
67 pub component: bool,
68 #[arg(long)]
70 pub host: bool,
71 #[arg(long)]
73 pub browser: bool,
74}
75
76impl TestScope {
77 #[must_use]
79 pub fn is_everything(self) -> bool {
80 !(self.component || self.host || self.browser)
81 }
82
83 #[must_use]
85 pub fn component_selected(self) -> bool {
86 self.component || self.is_everything()
87 }
88
89 #[must_use]
91 pub fn host_selected(self) -> bool {
92 self.host || self.is_everything()
93 }
94
95 #[must_use]
97 pub fn browser_selected(self) -> bool {
98 self.browser || self.is_everything()
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::{Cli, Command, TestScope};
105 use clap::Parser;
106
107 #[test]
108 fn clap_takes_bare_name_and_rejects_removed_and_unknown_flags() {
109 assert!(Cli::try_parse_from(["frame", "new", "valid_app"]).is_ok());
110 assert!(Cli::try_parse_from(["frame", "new", "valid_app", "--frame-root", "."]).is_err());
111 assert!(Cli::try_parse_from(["frame", "new", "valid_app", "--force"]).is_err());
112 }
113
114 #[test]
115 fn test_scope_defaults_to_everything_and_flags_narrow_to_a_union() -> Result<(), String> {
116 let default = TestScope::default();
117 assert!(default.is_everything());
118 assert!(default.component_selected() && default.host_selected());
119 assert!(default.browser_selected());
120
121 let parsed = Cli::try_parse_from(["frame", "test", "--component", "--browser"])
122 .map_err(|error| error.to_string())?;
123 let Command::Test(scope) = parsed.command else {
124 return Err("frame test --component --browser parsed to the wrong verb".to_owned());
125 };
126 assert!(!scope.is_everything());
127 assert!(scope.component_selected());
128 assert!(scope.browser_selected());
129 assert!(!scope.host_selected());
130 Ok(())
131 }
132
133 #[test]
134 fn every_verb_parses_and_host_requires_its_config() {
135 for verb in ["run", "check", "doctor", "build"] {
136 assert!(Cli::try_parse_from(["frame", verb]).is_ok(), "{verb}");
137 }
138 assert!(Cli::try_parse_from(["frame", "host", "--config", "frame.toml"]).is_ok());
139 assert!(
140 Cli::try_parse_from(["frame", "host"]).is_err(),
141 "frame host without --config must be a parse error"
142 );
143 assert!(
144 Cli::try_parse_from(["frame", "e2e"]).is_err(),
145 "e2e is dead as a command name"
146 );
147 }
148}