1use clap::{Parser, Subcommand, ValueEnum};
5
6use crate::commands::{api, auth, backup, issue, org, pr, release, repo, run};
7
8#[derive(Debug, Parser)]
9#[command(
10 name = "runtime",
11 version,
12 about = "Runtime CLI — speak the Runtime API from your shell"
13)]
14pub struct Cli {
15 #[command(subcommand)]
16 pub command: Command,
17}
18
19#[derive(Debug, Subcommand)]
20pub enum Command {
21 #[command(subcommand)]
23 Auth(auth::AuthCmd),
24 #[command(subcommand)]
26 Repo(repo::RepoCmd),
27 #[command(subcommand)]
29 Issue(issue::IssueCmd),
30 #[command(subcommand)]
32 Pr(pr::PrCmd),
33 #[command(subcommand)]
35 Run(run::RunCmd),
36 #[command(subcommand)]
38 Org(org::OrgCmd),
39 #[command(subcommand)]
41 Release(release::ReleaseCmd),
42 Api(api::ApiArgs),
44 #[command(subcommand)]
46 Backup(backup::BackupCmd),
47 Completion {
52 shell: Shell,
54 },
55}
56
57#[derive(Debug, Clone, Copy, ValueEnum)]
61pub enum Shell {
62 Bash,
63 Zsh,
64 Fish,
65 Elvish,
66 PowerShell,
67}
68
69impl From<Shell> for clap_complete::Shell {
70 fn from(s: Shell) -> Self {
71 match s {
72 Shell::Bash => clap_complete::Shell::Bash,
73 Shell::Zsh => clap_complete::Shell::Zsh,
74 Shell::Fish => clap_complete::Shell::Fish,
75 Shell::Elvish => clap_complete::Shell::Elvish,
76 Shell::PowerShell => clap_complete::Shell::PowerShell,
77 }
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
87 fn top_level_parses() {
88 let cases = [
89 vec!["runtime", "auth", "status"],
90 vec![
91 "runtime",
92 "auth",
93 "login",
94 "--username",
95 "alice",
96 "--password-stdin",
97 ],
98 vec!["runtime", "repo", "list"],
99 vec!["runtime", "repo", "view", "alice/r1"],
100 vec!["runtime", "repo", "clone", "alice/r1"],
101 vec!["runtime", "issue", "list", "alice/r1"],
102 vec!["runtime", "issue", "view", "alice/r1", "1"],
103 vec![
104 "runtime", "issue", "create", "alice/r1", "--title", "T", "--body", "B",
105 ],
106 vec![
107 "runtime", "issue", "comment", "alice/r1", "1", "--body", "hi",
108 ],
109 vec!["runtime", "issue", "close", "alice/r1", "1"],
110 vec!["runtime", "issue", "reopen", "alice/r1", "1"],
111 vec!["runtime", "pr", "list", "alice/r1"],
112 vec!["runtime", "pr", "view", "alice/r1", "1"],
113 vec!["runtime", "pr", "checkout", "alice/r1", "1"],
114 vec![
115 "runtime",
116 "pr",
117 "merge",
118 "alice/r1",
119 "1",
120 "--strategy",
121 "squash",
122 ],
123 vec!["runtime", "pr", "review", "alice/r1", "1", "--approve"],
124 vec!["runtime", "run", "list", "alice/r1"],
125 vec![
126 "runtime",
127 "run",
128 "view",
129 "00000000-0000-0000-0000-000000000000",
130 ],
131 vec![
132 "runtime",
133 "run",
134 "cancel",
135 "00000000-0000-0000-0000-000000000000",
136 ],
137 vec!["runtime", "org", "list"],
138 vec!["runtime", "org", "members", "acme"],
139 vec!["runtime", "org", "teams", "acme"],
140 vec!["runtime", "release", "list", "alice/r1"],
141 vec!["runtime", "api", "{ viewer { username } }"],
142 vec!["runtime", "completion", "bash"],
144 vec!["runtime", "completion", "zsh"],
145 vec!["runtime", "completion", "fish"],
146 vec!["runtime", "completion", "power-shell"],
147 vec!["runtime", "backup", "create"],
148 ];
149 for argv in cases {
150 Cli::try_parse_from(argv.iter().copied()).unwrap_or_else(|e| panic!("{argv:?}: {e}"));
151 }
152 }
153
154 #[test]
155 fn auth_login_defaults_to_public_remote() {
156 let cli = Cli::try_parse_from([
157 "runtime",
158 "auth",
159 "login",
160 "--username",
161 "alice",
162 "--password-stdin",
163 ])
164 .unwrap();
165 let Command::Auth(auth::AuthCmd::Login(args)) = cli.command else {
166 panic!("expected auth login command");
167 };
168 assert_eq!(args.host.as_deref(), Some("https://gitruntime.com"));
169 }
170
171 #[test]
172 fn issue_command_options_parse_into_expected_structs() {
173 let cli = Cli::try_parse_from([
174 "runtime",
175 "issue",
176 "list",
177 "alice/r1",
178 "--state",
179 "all",
180 "--label",
181 "bug",
182 "--label",
183 "ui",
184 "--milestone",
185 "M1",
186 "--json",
187 ])
188 .unwrap();
189 let Command::Issue(issue::IssueCmd::List(args)) = cli.command else {
190 panic!("expected issue list command");
191 };
192 assert_eq!(args.repo, "alice/r1");
193 assert_eq!(args.state, "all");
194 assert_eq!(args.label, ["bug", "ui"]);
195 assert_eq!(args.milestone.as_deref(), Some("M1"));
196 assert!(args.json);
197 assert!(!args.tsv);
198
199 let cli = Cli::try_parse_from([
200 "runtime",
201 "issue",
202 "set-state",
203 "alice/r1",
204 "5",
205 "--state",
206 "In progress",
207 ])
208 .unwrap();
209 let Command::Issue(issue::IssueCmd::SetState(args)) = cli.command else {
210 panic!("expected issue set-state command");
211 };
212 assert_eq!(args.repo, "alice/r1");
213 assert_eq!(args.number, 5);
214 assert_eq!(args.state, "In progress");
215
216 assert!(
217 Cli::try_parse_from(["runtime", "issue", "create", "alice/r1", "--title", ""]).is_err()
218 );
219 }
220
221 #[test]
222 fn pr_command_options_and_conflicts_parse_as_expected() {
223 let cli = Cli::try_parse_from([
224 "runtime", "pr", "create", "alice/r1", "--title", "Ship it", "--body", "Ready",
225 "--head", "feature", "--base", "main",
226 ])
227 .unwrap();
228 let Command::Pr(pr::PrCmd::Create(args)) = cli.command else {
229 panic!("expected pr create command");
230 };
231 assert_eq!(args.repo, "alice/r1");
232 assert_eq!(args.title, "Ship it");
233 assert_eq!(args.body.as_deref(), Some("Ready"));
234 assert_eq!(args.head, "feature");
235 assert_eq!(args.base, "main");
236
237 let cli = Cli::try_parse_from([
238 "runtime",
239 "pr",
240 "comment",
241 "alice/r1",
242 "3",
243 "--body",
244 "Looks good",
245 ])
246 .unwrap();
247 let Command::Pr(pr::PrCmd::Comment(args)) = cli.command else {
248 panic!("expected pr comment command");
249 };
250 assert_eq!(args.repo, "alice/r1");
251 assert_eq!(args.number, 3);
252 assert_eq!(args.body.as_deref(), Some("Looks good"));
253
254 assert!(Cli::try_parse_from([
255 "runtime",
256 "pr",
257 "review",
258 "alice/r1",
259 "3",
260 "--approve",
261 "--comment",
262 ])
263 .is_err());
264 }
265
266 #[test]
267 fn run_org_and_release_options_parse_into_expected_structs() {
268 let cli = Cli::try_parse_from(["runtime", "run", "list", "alice/r1", "--tsv"]).unwrap();
269 let Command::Run(run::RunCmd::List(args)) = cli.command else {
270 panic!("expected run list command");
271 };
272 assert_eq!(args.repo, "alice/r1");
273 assert!(args.tsv);
274
275 let cli = Cli::try_parse_from([
276 "runtime",
277 "run",
278 "logs",
279 "00000000-0000-0000-0000-000000000000",
280 "--follow",
281 ])
282 .unwrap();
283 let Command::Run(run::RunCmd::Logs(args)) = cli.command else {
284 panic!("expected run logs command");
285 };
286 assert_eq!(args.id, "00000000-0000-0000-0000-000000000000");
287 assert!(args.follow);
288
289 let cli = Cli::try_parse_from(["runtime", "org", "members", "acme", "--json"]).unwrap();
290 let Command::Org(org::OrgCmd::Members(args)) = cli.command else {
291 panic!("expected org members command");
292 };
293 assert_eq!(args.login, "acme");
294 assert!(args.json);
295
296 let cli = Cli::try_parse_from(["runtime", "org", "teams", "acme", "--tsv"]).unwrap();
297 let Command::Org(org::OrgCmd::Teams(args)) = cli.command else {
298 panic!("expected org teams command");
299 };
300 assert_eq!(args.login, "acme");
301 assert!(args.tsv);
302
303 let cli =
304 Cli::try_parse_from(["runtime", "release", "view", "alice/r1", "v1.0.0", "--json"])
305 .unwrap();
306 let Command::Release(release::ReleaseCmd::View(args)) = cli.command else {
307 panic!("expected release view command");
308 };
309 assert_eq!(args.repo, "alice/r1");
310 assert_eq!(args.tag, "v1.0.0");
311 assert!(args.json);
312
313 let cli = Cli::try_parse_from([
314 "runtime",
315 "release",
316 "create",
317 "alice/r1",
318 "--tag",
319 "v1.0.0",
320 "--name",
321 "Runtime 1",
322 "--body",
323 "Notes",
324 "--draft",
325 "--prerelease",
326 ])
327 .unwrap();
328 let Command::Release(release::ReleaseCmd::Create(args)) = cli.command else {
329 panic!("expected release create command");
330 };
331 assert_eq!(args.repo, "alice/r1");
332 assert_eq!(args.tag, "v1.0.0");
333 assert_eq!(args.name, "Runtime 1");
334 assert_eq!(args.body.as_deref(), Some("Notes"));
335 assert!(args.draft);
336 assert!(args.prerelease);
337 }
338}