use clap::{Parser, Subcommand, ValueEnum};
use crate::commands::{api, auth, backup, issue, org, pr, release, repo, run};
#[derive(Debug, Parser)]
#[command(
name = "runtime",
version,
about = "Runtime CLI — speak the Runtime API from your shell"
)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
#[command(subcommand)]
Auth(auth::AuthCmd),
#[command(subcommand)]
Repo(repo::RepoCmd),
#[command(subcommand)]
Issue(issue::IssueCmd),
#[command(subcommand)]
Pr(pr::PrCmd),
#[command(subcommand)]
Run(run::RunCmd),
#[command(subcommand)]
Org(org::OrgCmd),
#[command(subcommand)]
Release(release::ReleaseCmd),
Api(api::ApiArgs),
#[command(subcommand)]
Backup(backup::BackupCmd),
Completion {
shell: Shell,
},
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum Shell {
Bash,
Zsh,
Fish,
Elvish,
PowerShell,
}
impl From<Shell> for clap_complete::Shell {
fn from(s: Shell) -> Self {
match s {
Shell::Bash => clap_complete::Shell::Bash,
Shell::Zsh => clap_complete::Shell::Zsh,
Shell::Fish => clap_complete::Shell::Fish,
Shell::Elvish => clap_complete::Shell::Elvish,
Shell::PowerShell => clap_complete::Shell::PowerShell,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn top_level_parses() {
let cases = [
vec!["runtime", "auth", "status"],
vec![
"runtime",
"auth",
"login",
"--username",
"alice",
"--password-stdin",
],
vec!["runtime", "repo", "list"],
vec!["runtime", "repo", "view", "alice/r1"],
vec!["runtime", "repo", "clone", "alice/r1"],
vec!["runtime", "issue", "list", "alice/r1"],
vec!["runtime", "issue", "view", "alice/r1", "1"],
vec![
"runtime", "issue", "create", "alice/r1", "--title", "T", "--body", "B",
],
vec![
"runtime", "issue", "comment", "alice/r1", "1", "--body", "hi",
],
vec!["runtime", "issue", "close", "alice/r1", "1"],
vec!["runtime", "issue", "reopen", "alice/r1", "1"],
vec!["runtime", "pr", "list", "alice/r1"],
vec!["runtime", "pr", "view", "alice/r1", "1"],
vec!["runtime", "pr", "checkout", "alice/r1", "1"],
vec![
"runtime",
"pr",
"merge",
"alice/r1",
"1",
"--strategy",
"squash",
],
vec!["runtime", "pr", "review", "alice/r1", "1", "--approve"],
vec!["runtime", "run", "list", "alice/r1"],
vec![
"runtime",
"run",
"view",
"00000000-0000-0000-0000-000000000000",
],
vec![
"runtime",
"run",
"cancel",
"00000000-0000-0000-0000-000000000000",
],
vec!["runtime", "org", "list"],
vec!["runtime", "org", "members", "acme"],
vec!["runtime", "org", "teams", "acme"],
vec!["runtime", "release", "list", "alice/r1"],
vec!["runtime", "api", "{ viewer { username } }"],
vec!["runtime", "completion", "bash"],
vec!["runtime", "completion", "zsh"],
vec!["runtime", "completion", "fish"],
vec!["runtime", "completion", "power-shell"],
vec!["runtime", "backup", "create"],
];
for argv in cases {
Cli::try_parse_from(argv.iter().copied()).unwrap_or_else(|e| panic!("{argv:?}: {e}"));
}
}
#[test]
fn auth_login_defaults_to_public_remote() {
let cli = Cli::try_parse_from([
"runtime",
"auth",
"login",
"--username",
"alice",
"--password-stdin",
])
.unwrap();
let Command::Auth(auth::AuthCmd::Login(args)) = cli.command else {
panic!("expected auth login command");
};
assert_eq!(args.host.as_deref(), Some("https://gitruntime.com"));
}
#[test]
fn issue_command_options_parse_into_expected_structs() {
let cli = Cli::try_parse_from([
"runtime",
"issue",
"list",
"alice/r1",
"--state",
"all",
"--label",
"bug",
"--label",
"ui",
"--milestone",
"M1",
"--json",
])
.unwrap();
let Command::Issue(issue::IssueCmd::List(args)) = cli.command else {
panic!("expected issue list command");
};
assert_eq!(args.repo, "alice/r1");
assert_eq!(args.state, "all");
assert_eq!(args.label, ["bug", "ui"]);
assert_eq!(args.milestone.as_deref(), Some("M1"));
assert!(args.json);
assert!(!args.tsv);
let cli = Cli::try_parse_from([
"runtime",
"issue",
"set-state",
"alice/r1",
"5",
"--state",
"In progress",
])
.unwrap();
let Command::Issue(issue::IssueCmd::SetState(args)) = cli.command else {
panic!("expected issue set-state command");
};
assert_eq!(args.repo, "alice/r1");
assert_eq!(args.number, 5);
assert_eq!(args.state, "In progress");
assert!(
Cli::try_parse_from(["runtime", "issue", "create", "alice/r1", "--title", ""]).is_err()
);
}
#[test]
fn pr_command_options_and_conflicts_parse_as_expected() {
let cli = Cli::try_parse_from([
"runtime", "pr", "create", "alice/r1", "--title", "Ship it", "--body", "Ready",
"--head", "feature", "--base", "main",
])
.unwrap();
let Command::Pr(pr::PrCmd::Create(args)) = cli.command else {
panic!("expected pr create command");
};
assert_eq!(args.repo, "alice/r1");
assert_eq!(args.title, "Ship it");
assert_eq!(args.body.as_deref(), Some("Ready"));
assert_eq!(args.head, "feature");
assert_eq!(args.base, "main");
let cli = Cli::try_parse_from([
"runtime",
"pr",
"comment",
"alice/r1",
"3",
"--body",
"Looks good",
])
.unwrap();
let Command::Pr(pr::PrCmd::Comment(args)) = cli.command else {
panic!("expected pr comment command");
};
assert_eq!(args.repo, "alice/r1");
assert_eq!(args.number, 3);
assert_eq!(args.body.as_deref(), Some("Looks good"));
assert!(Cli::try_parse_from([
"runtime",
"pr",
"review",
"alice/r1",
"3",
"--approve",
"--comment",
])
.is_err());
}
#[test]
fn run_org_and_release_options_parse_into_expected_structs() {
let cli = Cli::try_parse_from(["runtime", "run", "list", "alice/r1", "--tsv"]).unwrap();
let Command::Run(run::RunCmd::List(args)) = cli.command else {
panic!("expected run list command");
};
assert_eq!(args.repo, "alice/r1");
assert!(args.tsv);
let cli = Cli::try_parse_from([
"runtime",
"run",
"logs",
"00000000-0000-0000-0000-000000000000",
"--follow",
])
.unwrap();
let Command::Run(run::RunCmd::Logs(args)) = cli.command else {
panic!("expected run logs command");
};
assert_eq!(args.id, "00000000-0000-0000-0000-000000000000");
assert!(args.follow);
let cli = Cli::try_parse_from(["runtime", "org", "members", "acme", "--json"]).unwrap();
let Command::Org(org::OrgCmd::Members(args)) = cli.command else {
panic!("expected org members command");
};
assert_eq!(args.login, "acme");
assert!(args.json);
let cli = Cli::try_parse_from(["runtime", "org", "teams", "acme", "--tsv"]).unwrap();
let Command::Org(org::OrgCmd::Teams(args)) = cli.command else {
panic!("expected org teams command");
};
assert_eq!(args.login, "acme");
assert!(args.tsv);
let cli =
Cli::try_parse_from(["runtime", "release", "view", "alice/r1", "v1.0.0", "--json"])
.unwrap();
let Command::Release(release::ReleaseCmd::View(args)) = cli.command else {
panic!("expected release view command");
};
assert_eq!(args.repo, "alice/r1");
assert_eq!(args.tag, "v1.0.0");
assert!(args.json);
let cli = Cli::try_parse_from([
"runtime",
"release",
"create",
"alice/r1",
"--tag",
"v1.0.0",
"--name",
"Runtime 1",
"--body",
"Notes",
"--draft",
"--prerelease",
])
.unwrap();
let Command::Release(release::ReleaseCmd::Create(args)) = cli.command else {
panic!("expected release create command");
};
assert_eq!(args.repo, "alice/r1");
assert_eq!(args.tag, "v1.0.0");
assert_eq!(args.name, "Runtime 1");
assert_eq!(args.body.as_deref(), Some("Notes"));
assert!(args.draft);
assert!(args.prerelease);
}
}