Skip to main content

runtime_cli/
cli.rs

1//! clap derive surface for `runtime`. Top-level subcommands map 1:1 to
2//! the modules under `commands/`.
3
4use 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    /// Manage credentials for the configured Runtime instance.
22    #[command(subcommand)]
23    Auth(auth::AuthCmd),
24    /// Repository operations.
25    #[command(subcommand)]
26    Repo(repo::RepoCmd),
27    /// Issue operations.
28    #[command(subcommand)]
29    Issue(issue::IssueCmd),
30    /// Pull-request operations.
31    #[command(subcommand)]
32    Pr(pr::PrCmd),
33    /// Action-run operations.
34    #[command(subcommand)]
35    Run(run::RunCmd),
36    /// Organization operations.
37    #[command(subcommand)]
38    Org(org::OrgCmd),
39    /// Release operations.
40    #[command(subcommand)]
41    Release(release::ReleaseCmd),
42    /// Run a raw GraphQL query against the configured instance.
43    Api(api::ApiArgs),
44    /// Snapshot / restore the instance state.
45    #[command(subcommand)]
46    Backup(backup::BackupCmd),
47    /// Issue #31 — print a shell completion script for `runtime`.
48    /// Pipe the output into the shell's completion directory, e.g.
49    /// `runtime completion bash > /usr/local/etc/bash_completion.d/runtime`
50    /// or `runtime completion zsh > ~/.zsh/completions/_runtime`.
51    Completion {
52        /// Target shell: `bash`, `zsh`, `fish`, `elvish`, or `powershell`.
53        shell: Shell,
54    },
55}
56
57/// Re-export of `clap_complete::Shell` as a `ValueEnum` so the `runtime
58/// completion <shell>` subcommand can use it directly without a custom
59/// parser. Kept in lockstep with `clap_complete::Shell` variants.
60#[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    /// Smoke-test that every documented top-level command parses.
86    #[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            // Issue #31 — completion subcommand parses for each shell.
143            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}