use clap::{ArgAction, Parser, Subcommand, ValueEnum};
use clap_complete::Shell;
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(name = "mino")]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(short, long, global = true, action = ArgAction::Count)]
pub verbose: u8,
#[arg(short, long, global = true, env = "MINO_CONFIG")]
pub config: Option<PathBuf>,
#[arg(long, global = true)]
pub no_local: bool,
#[arg(long, global = true, env = "MINO_TRUST_LOCAL")]
pub trust_local: bool,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Run(RunArgs),
Exec(ExecArgs),
Init(InitArgs),
List(ListArgs),
Stop(StopArgs),
Logs(LogsArgs),
Status,
Setup(SetupArgs),
Config(ConfigArgs),
Cache(CacheArgs),
Completions(CompletionsArgs),
}
#[derive(Parser, Debug)]
pub struct ExecArgs {
pub session: Option<String>,
#[arg(last = true)]
pub command: Vec<String>,
}
#[derive(Parser, Debug)]
pub struct SetupArgs {
#[arg(short, long)]
pub yes: bool,
#[arg(long)]
pub check: bool,
#[arg(long)]
pub upgrade: bool,
}
#[derive(Parser, Debug)]
pub struct InitArgs {
#[arg(short, long)]
pub force: bool,
#[arg(short, long)]
pub path: Option<PathBuf>,
}
#[derive(Parser, Debug)]
pub struct RunArgs {
#[arg(short, long)]
pub name: Option<String>,
#[arg(short, long)]
pub project: Option<PathBuf>,
#[arg(long)]
pub aws: bool,
#[arg(long)]
pub gcp: bool,
#[arg(long)]
pub azure: bool,
#[arg(long, conflicts_with_all = ["aws", "gcp", "azure"])]
pub all_clouds: bool,
#[arg(long = "no-ssh-agent")]
pub no_ssh_agent: bool,
#[arg(long = "no-github")]
pub no_github: bool,
#[arg(long)]
pub strict_credentials: bool,
#[arg(long)]
pub image: Option<String>,
#[arg(long, value_delimiter = ',', conflicts_with = "image")]
pub layers: Vec<String>,
#[arg(short, long, value_parser = parse_env_var)]
pub env: Vec<(String, String)>,
#[arg(long)]
pub volume: Vec<String>,
#[arg(short, long)]
pub detach: bool,
#[arg(long)]
pub read_only: bool,
#[arg(long)]
pub no_cache: bool,
#[arg(long)]
pub no_home: bool,
#[arg(long, conflicts_with = "no_cache")]
pub cache_fresh: bool,
#[arg(long)]
pub network: Option<String>,
#[arg(long, value_delimiter = ',')]
pub network_allow: Vec<String>,
#[arg(long, value_parser = clap::builder::PossibleValuesParser::new(["dev", "registries"]), conflicts_with = "network_allow")]
pub network_preset: Option<String>,
#[arg(last = true)]
pub command: Vec<String>,
}
#[derive(Parser, Debug)]
pub struct ListArgs {
#[arg(short, long)]
pub all: bool,
#[arg(short, long, default_value = "table")]
pub format: OutputFormat,
}
#[derive(Parser, Debug)]
pub struct StopArgs {
pub session: String,
#[arg(short, long)]
pub force: bool,
}
#[derive(Parser, Debug)]
pub struct LogsArgs {
pub session: String,
#[arg(short, long)]
pub follow: bool,
#[arg(short, long, default_value = "100")]
pub lines: u32,
}
#[derive(Parser, Debug)]
pub struct ConfigArgs {
#[command(subcommand)]
pub action: Option<ConfigAction>,
}
#[derive(Subcommand, Debug)]
pub enum ConfigAction {
Show,
Path,
Init {
#[arg(short, long)]
force: bool,
},
Set {
key: String,
value: String,
#[arg(long)]
local: bool,
},
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum OutputFormat {
Table,
Json,
Plain,
}
#[derive(Parser, Debug)]
pub struct CacheArgs {
#[command(subcommand)]
pub action: CacheAction,
}
#[derive(Subcommand, Debug)]
pub enum CacheAction {
List {
#[arg(short, long, default_value = "table")]
format: OutputFormat,
},
Info {
#[arg(short, long)]
project: Option<PathBuf>,
},
Gc {
#[arg(long)]
days: Option<u32>,
#[arg(long)]
dry_run: bool,
},
#[command(group(clap::ArgGroup::new("target").required(true).args(["volumes", "images", "home", "all"])))]
Clear {
#[arg(long)]
volumes: bool,
#[arg(long)]
images: bool,
#[arg(long)]
home: bool,
#[arg(long, conflicts_with_all = ["volumes", "images", "home"])]
all: bool,
#[arg(short, long)]
yes: bool,
},
}
#[derive(Parser, Debug)]
pub struct CompletionsArgs {
pub shell: Shell,
}
fn parse_env_var(s: &str) -> Result<(String, String), String> {
let pos = s
.find('=')
.ok_or_else(|| format!("invalid KEY=VALUE format: no '=' found in '{s}'"))?;
Ok((s[..pos].to_string(), s[pos + 1..].to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_env_var_valid() {
let (k, v) = parse_env_var("FOO=bar").unwrap();
assert_eq!(k, "FOO");
assert_eq!(v, "bar");
}
#[test]
fn parse_env_var_with_equals() {
let (k, v) = parse_env_var("FOO=bar=baz").unwrap();
assert_eq!(k, "FOO");
assert_eq!(v, "bar=baz");
}
#[test]
fn parse_env_var_invalid() {
assert!(parse_env_var("FOO").is_err());
}
#[test]
fn cli_parses_run() {
let cli = Cli::parse_from(["mino", "run", "--aws", "--", "bash"]);
match cli.command {
Commands::Run(args) => {
assert!(args.aws);
assert_eq!(args.command, vec!["bash"]);
}
_ => panic!("expected Run command"),
}
}
#[test]
fn cli_parses_status() {
let cli = Cli::parse_from(["mino", "status"]);
assert!(matches!(cli.command, Commands::Status));
}
#[test]
fn cli_parses_setup() {
let cli = Cli::parse_from(["mino", "setup"]);
match cli.command {
Commands::Setup(args) => {
assert!(!args.yes);
assert!(!args.check);
}
_ => panic!("expected Setup command"),
}
}
#[test]
fn cli_parses_setup_with_flags() {
let cli = Cli::parse_from(["mino", "setup", "--yes", "--check"]);
match cli.command {
Commands::Setup(args) => {
assert!(args.yes);
assert!(args.check);
assert!(!args.upgrade);
}
_ => panic!("expected Setup command"),
}
}
#[test]
fn cli_parses_setup_upgrade() {
let cli = Cli::parse_from(["mino", "setup", "--upgrade"]);
match cli.command {
Commands::Setup(args) => {
assert!(!args.yes);
assert!(!args.check);
assert!(args.upgrade);
}
_ => panic!("expected Setup command"),
}
}
#[test]
fn cli_parses_init() {
let cli = Cli::parse_from(["mino", "init"]);
assert!(matches!(cli.command, Commands::Init(_)));
}
#[test]
fn cli_parses_init_force() {
let cli = Cli::parse_from(["mino", "init", "--force"]);
match cli.command {
Commands::Init(args) => assert!(args.force),
_ => panic!("expected Init command"),
}
}
#[test]
fn cli_no_local_flag() {
let cli = Cli::parse_from(["mino", "--no-local", "status"]);
assert!(cli.no_local);
}
#[test]
fn cli_trust_local_flag() {
let cli = Cli::parse_from(["mino", "--trust-local", "status"]);
assert!(cli.trust_local);
assert!(!cli.no_local);
}
#[test]
fn cli_parses_network_flags() {
let cli = Cli::parse_from(["mino", "run", "--network", "none", "--", "bash"]);
match cli.command {
Commands::Run(args) => {
assert_eq!(args.network.as_deref(), Some("none"));
assert!(args.network_allow.is_empty());
}
_ => panic!("expected Run command"),
}
}
#[test]
fn cli_parses_network_allow() {
let cli = Cli::parse_from([
"mino",
"run",
"--network-allow",
"github.com:443,npmjs.org:443",
"--",
"bash",
]);
match cli.command {
Commands::Run(args) => {
assert_eq!(args.network_allow, vec!["github.com:443", "npmjs.org:443"]);
}
_ => panic!("expected Run command"),
}
}
#[test]
fn cli_verbose_levels() {
let cli = Cli::parse_from(["mino", "status"]);
assert_eq!(cli.verbose, 0);
let cli = Cli::parse_from(["mino", "-v", "status"]);
assert_eq!(cli.verbose, 1);
let cli = Cli::parse_from(["mino", "-vv", "status"]);
assert_eq!(cli.verbose, 2);
}
#[test]
fn cli_no_ssh_agent_flag() {
let cli = Cli::parse_from(["mino", "run", "--no-ssh-agent", "--", "bash"]);
match cli.command {
Commands::Run(args) => assert!(args.no_ssh_agent),
_ => panic!("expected Run command"),
}
}
#[test]
fn cli_no_github_flag() {
let cli = Cli::parse_from(["mino", "run", "--no-github", "--", "bash"]);
match cli.command {
Commands::Run(args) => assert!(args.no_github),
_ => panic!("expected Run command"),
}
}
#[test]
fn cli_ssh_github_default_enabled() {
let cli = Cli::parse_from(["mino", "run", "--", "bash"]);
match cli.command {
Commands::Run(args) => {
assert!(!args.no_ssh_agent);
assert!(!args.no_github);
}
_ => panic!("expected Run command"),
}
}
#[test]
fn cli_strict_credentials_flag() {
let cli = Cli::parse_from(["mino", "run", "--strict-credentials", "--", "bash"]);
match cli.command {
Commands::Run(args) => {
assert!(args.strict_credentials);
}
_ => panic!("expected Run command"),
}
}
#[test]
fn cli_strict_credentials_default_false() {
let cli = Cli::parse_from(["mino", "run", "--", "bash"]);
match cli.command {
Commands::Run(args) => {
assert!(!args.strict_credentials);
}
_ => panic!("expected Run command"),
}
}
#[test]
fn cli_parses_read_only() {
let cli = Cli::parse_from(["mino", "run", "--read-only", "--", "bash"]);
match cli.command {
Commands::Run(args) => assert!(args.read_only),
_ => panic!("expected Run command"),
}
}
#[test]
fn cli_read_only_default_false() {
let cli = Cli::parse_from(["mino", "run", "--", "bash"]);
match cli.command {
Commands::Run(args) => assert!(!args.read_only),
_ => panic!("expected Run command"),
}
}
#[test]
fn cli_parses_completions_bash() {
let cli = Cli::parse_from(["mino", "completions", "bash"]);
match cli.command {
Commands::Completions(args) => assert_eq!(args.shell, Shell::Bash),
_ => panic!("expected Completions command"),
}
}
#[test]
fn cli_parses_completions_zsh() {
let cli = Cli::parse_from(["mino", "completions", "zsh"]);
match cli.command {
Commands::Completions(args) => assert_eq!(args.shell, Shell::Zsh),
_ => panic!("expected Completions command"),
}
}
#[test]
fn cli_parses_exec_no_args() {
let cli = Cli::parse_from(["mino", "exec"]);
match cli.command {
Commands::Exec(args) => {
assert!(args.session.is_none());
assert!(args.command.is_empty());
}
_ => panic!("expected Exec command"),
}
}
#[test]
fn cli_parses_exec_with_session() {
let cli = Cli::parse_from(["mino", "exec", "my-session"]);
match cli.command {
Commands::Exec(args) => {
assert_eq!(args.session.as_deref(), Some("my-session"));
assert!(args.command.is_empty());
}
_ => panic!("expected Exec command"),
}
}
#[test]
fn cli_parses_exec_with_command() {
let cli = Cli::parse_from(["mino", "exec", "--", "ls", "-la"]);
match cli.command {
Commands::Exec(args) => {
assert!(args.session.is_none());
assert_eq!(args.command, vec!["ls", "-la"]);
}
_ => panic!("expected Exec command"),
}
}
#[test]
fn cli_parses_exec_with_session_and_command() {
let cli = Cli::parse_from(["mino", "exec", "my-session", "--", "ls", "-la"]);
match cli.command {
Commands::Exec(args) => {
assert_eq!(args.session.as_deref(), Some("my-session"));
assert_eq!(args.command, vec!["ls", "-la"]);
}
_ => panic!("expected Exec command"),
}
}
#[test]
fn cli_parses_no_home() {
let cli = Cli::parse_from(["mino", "run", "--no-home", "--", "bash"]);
match cli.command {
Commands::Run(args) => assert!(args.no_home),
_ => panic!("expected Run command"),
}
}
#[test]
fn cli_no_home_default_false() {
let cli = Cli::parse_from(["mino", "run", "--", "bash"]);
match cli.command {
Commands::Run(args) => assert!(!args.no_home),
_ => panic!("expected Run command"),
}
}
#[test]
fn cli_parses_cache_clear_home() {
let cli = Cli::parse_from(["mino", "cache", "clear", "--home"]);
match cli.command {
Commands::Cache(args) => match args.action {
CacheAction::Clear {
home,
volumes,
images,
all,
..
} => {
assert!(home);
assert!(!volumes);
assert!(!images);
assert!(!all);
}
_ => panic!("expected Clear action"),
},
_ => panic!("expected Cache command"),
}
}
#[test]
fn cli_parses_completions_fish() {
let cli = Cli::parse_from(["mino", "completions", "fish"]);
match cli.command {
Commands::Completions(args) => assert_eq!(args.shell, Shell::Fish),
_ => panic!("expected Completions command"),
}
}
}