use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser)]
#[command(
name = "lific",
version,
about = "Local-first, lightweight issue tracker"
)]
pub struct Cli {
#[arg(long, global = true)]
pub config: Option<PathBuf>,
#[arg(long, global = true)]
pub db: Option<PathBuf>,
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand)]
pub enum Command {
Start {
#[arg(short, long)]
port: Option<u16>,
#[arg(long)]
host: Option<String>,
},
Mcp,
Init,
Key {
#[command(subcommand)]
action: KeyAction,
},
User {
#[command(subcommand)]
action: UserAction,
},
}
#[derive(Subcommand)]
pub enum KeyAction {
Create {
#[arg(short, long)]
name: String,
#[arg(short, long)]
user: Option<String>,
},
Assign {
#[arg(short, long)]
name: String,
#[arg(short, long)]
user: String,
},
List,
Revoke {
#[arg(short, long)]
name: String,
},
Rotate {
#[arg(short, long)]
name: String,
},
}
#[derive(Subcommand)]
pub enum UserAction {
Create {
#[arg(short, long)]
username: String,
#[arg(short, long)]
email: String,
#[arg(short, long)]
password: Option<String>,
#[arg(long)]
admin: bool,
#[arg(long)]
bot: bool,
},
List,
Promote {
#[arg(short, long)]
username: String,
},
Demote {
#[arg(short, long)]
username: String,
},
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn parse_start_defaults() {
let cli = Cli::try_parse_from(["lific", "start"]).unwrap();
assert!(cli.config.is_none());
assert!(cli.db.is_none());
match cli.command {
Command::Start { port, host } => {
assert!(port.is_none());
assert!(host.is_none());
}
_ => panic!("expected Start"),
}
}
#[test]
fn parse_start_with_overrides() {
let cli = Cli::try_parse_from([
"lific",
"--db",
"/tmp/test.db",
"start",
"--port",
"8080",
"--host",
"127.0.0.1",
])
.unwrap();
assert_eq!(cli.db, Some(PathBuf::from("/tmp/test.db")));
match cli.command {
Command::Start { port, host } => {
assert_eq!(port, Some(8080));
assert_eq!(host, Some("127.0.0.1".into()));
}
_ => panic!("expected Start"),
}
}
#[test]
fn parse_mcp() {
let cli = Cli::try_parse_from(["lific", "mcp"]).unwrap();
assert!(matches!(cli.command, Command::Mcp));
}
#[test]
fn parse_init() {
let cli = Cli::try_parse_from(["lific", "init"]).unwrap();
assert!(matches!(cli.command, Command::Init));
}
#[test]
fn parse_key_create() {
let cli = Cli::try_parse_from(["lific", "key", "create", "--name", "test-key"]).unwrap();
match cli.command {
Command::Key {
action: KeyAction::Create { name, user },
} => {
assert_eq!(name, "test-key");
assert!(user.is_none());
}
_ => panic!("expected Key Create"),
}
}
#[test]
fn parse_key_create_with_user() {
let cli = Cli::try_parse_from([
"lific", "key", "create", "--name", "my-key", "--user", "blake",
])
.unwrap();
match cli.command {
Command::Key {
action: KeyAction::Create { name, user },
} => {
assert_eq!(name, "my-key");
assert_eq!(user, Some("blake".into()));
}
_ => panic!("expected Key Create"),
}
}
#[test]
fn parse_key_assign() {
let cli = Cli::try_parse_from([
"lific", "key", "assign", "--name", "opencode", "--user", "blake",
])
.unwrap();
match cli.command {
Command::Key {
action: KeyAction::Assign { name, user },
} => {
assert_eq!(name, "opencode");
assert_eq!(user, "blake");
}
_ => panic!("expected Key Assign"),
}
}
#[test]
fn parse_key_revoke() {
let cli = Cli::try_parse_from(["lific", "key", "revoke", "--name", "old"]).unwrap();
match cli.command {
Command::Key {
action: KeyAction::Revoke { name },
} => assert_eq!(name, "old"),
_ => panic!("expected Key Revoke"),
}
}
#[test]
fn parse_user_create() {
let cli = Cli::try_parse_from([
"lific",
"user",
"create",
"--username",
"blake",
"--email",
"b@test.com",
"--password",
"secret123",
"--admin",
])
.unwrap();
match cli.command {
Command::User {
action:
UserAction::Create {
username,
email,
password,
admin,
bot,
},
} => {
assert_eq!(username, "blake");
assert_eq!(email, "b@test.com");
assert_eq!(password, Some("secret123".into()));
assert!(admin);
assert!(!bot);
}
_ => panic!("expected User Create"),
}
}
#[test]
fn parse_user_list() {
let cli = Cli::try_parse_from(["lific", "user", "list"]).unwrap();
assert!(matches!(
cli.command,
Command::User {
action: UserAction::List,
}
));
}
#[test]
fn parse_global_config_flag() {
let cli = Cli::try_parse_from(["lific", "--config", "/etc/lific.toml", "start"]).unwrap();
assert_eq!(cli.config, Some(PathBuf::from("/etc/lific.toml")));
}
#[test]
fn missing_subcommand_errors() {
assert!(Cli::try_parse_from(["lific"]).is_err());
}
}