use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "kx", version, about = "CLI dev assistant powered by Kernex")]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
#[arg(long, global = true, default_value = "claude-code")]
pub provider: String,
#[arg(long, global = true)]
pub model: Option<String>,
#[arg(long, global = true)]
pub api_key: Option<String>,
#[arg(long, global = true)]
pub base_url: Option<String>,
pub message: Option<String>,
}
#[derive(Subcommand)]
pub enum Command {
Dev {
message: Option<String>,
},
Audit,
Docs,
Init,
Pipeline {
#[command(subcommand)]
action: PipelineAction,
},
Skills {
#[command(subcommand)]
action: SkillsAction,
},
Cron {
#[command(subcommand)]
action: CronAction,
},
}
#[derive(Subcommand)]
pub enum PipelineAction {
Run {
name: String,
},
List,
}
#[derive(Subcommand)]
pub enum SkillsAction {
List,
Add {
source: String,
#[arg(short, long, default_value = "sandboxed")]
trust: String,
},
Remove {
name: String,
},
Verify,
}
#[derive(Subcommand)]
pub enum CronAction {
Create {
description: String,
#[arg(long)]
at: String,
#[arg(long)]
repeat: Option<String>,
},
List,
Delete {
id: String,
},
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn cli_parses_no_args() {
let cli = Cli::try_parse_from(["kx"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
assert!(cli.command.is_none());
assert!(cli.message.is_none());
assert_eq!(cli.provider, "claude-code");
}
#[test]
fn cli_parses_oneshot_message() {
let cli = Cli::try_parse_from(["kx", "fix the bug"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
assert!(cli.command.is_none());
assert_eq!(cli.message, Some("fix the bug".to_string()));
}
#[test]
fn cli_parses_dev_subcommand() {
let cli = Cli::try_parse_from(["kx", "dev"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
assert!(matches!(cli.command, Some(Command::Dev { message: None })));
}
#[test]
fn cli_parses_dev_with_message() {
let cli = Cli::try_parse_from(["kx", "dev", "write tests"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
if let Some(Command::Dev { message }) = cli.command {
assert_eq!(message, Some("write tests".to_string()));
} else {
panic!("Expected Dev command");
}
}
#[test]
fn cli_parses_audit() {
let cli = Cli::try_parse_from(["kx", "audit"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
assert!(matches!(cli.command, Some(Command::Audit)));
}
#[test]
fn cli_parses_docs() {
let cli = Cli::try_parse_from(["kx", "docs"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
assert!(matches!(cli.command, Some(Command::Docs)));
}
#[test]
fn cli_parses_init() {
let cli = Cli::try_parse_from(["kx", "init"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
assert!(matches!(cli.command, Some(Command::Init)));
}
#[test]
fn cli_parses_pipeline_run() {
let cli = Cli::try_parse_from(["kx", "pipeline", "run", "code-review"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
if let Some(Command::Pipeline { action }) = cli.command {
if let PipelineAction::Run { name } = action {
assert_eq!(name, "code-review");
} else {
panic!("Expected Run action");
}
} else {
panic!("Expected Pipeline command");
}
}
#[test]
fn cli_parses_pipeline_list() {
let cli = Cli::try_parse_from(["kx", "pipeline", "list"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
if let Some(Command::Pipeline { action }) = cli.command {
assert!(matches!(action, PipelineAction::List));
} else {
panic!("Expected Pipeline command");
}
}
#[test]
fn cli_parses_provider_flag() {
let cli = Cli::try_parse_from(["kx", "--provider", "ollama", "dev"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
assert_eq!(cli.provider, "ollama");
}
#[test]
fn cli_parses_model_flag() {
let cli = Cli::try_parse_from(["kx", "--model", "gpt-4o", "dev"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
assert_eq!(cli.model, Some("gpt-4o".to_string()));
}
#[test]
fn cli_parses_api_key_flag() {
let cli = Cli::try_parse_from(["kx", "--api-key", "sk-test", "dev"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
assert_eq!(cli.api_key, Some("sk-test".to_string()));
}
#[test]
fn cli_parses_base_url_flag() {
let cli = Cli::try_parse_from(["kx", "--base-url", "http://localhost:11434", "dev"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
assert_eq!(cli.base_url, Some("http://localhost:11434".to_string()));
}
#[test]
fn cli_provider_default_is_claude_code() {
let cli = Cli::try_parse_from(["kx", "dev"]);
assert!(cli.is_ok());
assert_eq!(cli.unwrap().provider, "claude-code");
}
#[test]
fn cli_parses_skills_list() {
let cli = Cli::try_parse_from(["kx", "skills", "list"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
if let Some(Command::Skills { action }) = cli.command {
assert!(matches!(action, SkillsAction::List));
} else {
panic!("Expected Skills command");
}
}
#[test]
fn cli_parses_skills_add() {
let cli = Cli::try_parse_from(["kx", "skills", "add", "acme/repo"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
if let Some(Command::Skills { action }) = cli.command {
if let SkillsAction::Add { source, trust } = action {
assert_eq!(source, "acme/repo");
assert_eq!(trust, "sandboxed");
} else {
panic!("Expected Add action");
}
} else {
panic!("Expected Skills command");
}
}
#[test]
fn cli_parses_skills_add_with_trust() {
let cli = Cli::try_parse_from(["kx", "skills", "add", "acme/repo", "-t", "trusted"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
if let Some(Command::Skills { action }) = cli.command {
if let SkillsAction::Add { source, trust } = action {
assert_eq!(source, "acme/repo");
assert_eq!(trust, "trusted");
} else {
panic!("Expected Add action");
}
} else {
panic!("Expected Skills command");
}
}
#[test]
fn cli_parses_skills_remove() {
let cli = Cli::try_parse_from(["kx", "skills", "remove", "my-skill"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
if let Some(Command::Skills { action }) = cli.command {
if let SkillsAction::Remove { name } = action {
assert_eq!(name, "my-skill");
} else {
panic!("Expected Remove action");
}
} else {
panic!("Expected Skills command");
}
}
#[test]
fn cli_parses_skills_verify() {
let cli = Cli::try_parse_from(["kx", "skills", "verify"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
if let Some(Command::Skills { action }) = cli.command {
assert!(matches!(action, SkillsAction::Verify));
} else {
panic!("Expected Skills command");
}
}
#[test]
fn cli_parses_cron_list() {
let cli = Cli::try_parse_from(["kx", "cron", "list"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
if let Some(Command::Cron { action }) = cli.command {
assert!(matches!(action, CronAction::List));
} else {
panic!("Expected Cron command");
}
}
#[test]
fn cli_parses_cron_create() {
let cli = Cli::try_parse_from([
"kx",
"cron",
"create",
"run the test suite",
"--at",
"2026-04-03T09:00:00",
]);
assert!(cli.is_ok());
let cli = cli.unwrap();
if let Some(Command::Cron { action }) = cli.command {
if let CronAction::Create {
description,
at,
repeat,
} = action
{
assert_eq!(description, "run the test suite");
assert_eq!(at, "2026-04-03T09:00:00");
assert!(repeat.is_none());
} else {
panic!("Expected Create action");
}
} else {
panic!("Expected Cron command");
}
}
#[test]
fn cli_parses_cron_create_with_repeat() {
let cli = Cli::try_parse_from([
"kx",
"cron",
"create",
"run lints",
"--at",
"2026-04-03T08:00:00",
"--repeat",
"daily",
]);
assert!(cli.is_ok());
let cli = cli.unwrap();
if let Some(Command::Cron { action }) = cli.command {
if let CronAction::Create { repeat, .. } = action {
assert_eq!(repeat, Some("daily".to_string()));
} else {
panic!("Expected Create action");
}
} else {
panic!("Expected Cron command");
}
}
#[test]
fn cli_parses_cron_delete() {
let cli = Cli::try_parse_from(["kx", "cron", "delete", "abc12345"]);
assert!(cli.is_ok());
let cli = cli.unwrap();
if let Some(Command::Cron { action }) = cli.command {
if let CronAction::Delete { id } = action {
assert_eq!(id, "abc12345");
} else {
panic!("Expected Delete action");
}
} else {
panic!("Expected Cron command");
}
}
#[test]
fn cli_has_valid_structure() {
Cli::command().debug_assert();
}
#[test]
fn cli_version_flag() {
let result = Cli::try_parse_from(["kx", "--version"]);
assert!(result.is_err());
}
#[test]
fn cli_help_flag() {
let result = Cli::try_parse_from(["kx", "--help"]);
assert!(result.is_err());
}
}