Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421use clap::{Parser, Subcommand};use clap_complete::Shell;
#[derive(Parser, Debug)]#[command(name = "vein", about = "Agent-focused issue tracker backed by Vikunja")]#[command(subcommand_required = true, arg_required_else_help = true)]pub struct Cli { #[command(subcommand)] pub command: Option<Command>,}
#[derive(Subcommand, Debug)]pub enum PromptCommand { /// Agent orientation: available tools, workflow guidance, and ready tasks Orient,}
#[derive(Subcommand, Debug)]pub enum Command { /// Discover Vikunja projects and buckets, print env var configuration Init, /// List available Vikunja projects ListProjects, /// List views for a Vikunja project ListProjectViews { /// Project ID project_id: i64, }, /// List buckets for a project view ListProjectViewBuckets { /// Project ID project_id: i64, /// View ID view_id: i64, }, /// Interactive kanban board TUI Board, /// Run as MCP stdio server Serve, /// Generate shell completion script Completions { /// Shell to generate completions for shell: Shell, }, /// List tasks ready to be worked on (Todo bucket) ListReady, /// List and search tasks across all buckets ListTasks { /// Filter expression (e.g. "done = false", "priority >= 3") #[arg(short, long)] filter: Option<String>, /// Search text to match against task titles #[arg(short, long)] search: Option<String>, }, /// Get full details of a task by index (e.g. 3 or #3) or identifier (e.g. VEIN-3) GetTask { /// Task index (e.g. 3 or #3) or identifier (e.g. VEIN-3) task_id: String, }, /// List tasks currently in progress ListInProgress, /// List completed tasks ListDone, /// Claim a task (move to In Progress) Claim { /// Task index (e.g. 3 or #3) or identifier (e.g. VEIN-3) task_id: String, }, /// Mark a task as done Complete { /// Task index (e.g. 3 or #3) or identifier (e.g. VEIN-3) task_id: String, }, /// Add a comment to a task Comment { /// Task index (e.g. 3 or #3) or identifier (e.g. VEIN-3) task_id: String, /// Comment text comment: String, }, /// Update an existing task's title, description, or priority UpdateTask { /// Task index (e.g. 3 or #3) or identifier (e.g. VEIN-3) task_id: String, /// New title #[arg(short, long)] title: Option<String>, /// New description #[arg(short, long)] description: Option<String>, /// Priority: none, low, medium, high, urgent #[arg(short, long)] priority: Option<String>, }, /// Create a new label CreateLabel { /// Label title title: String, }, /// Add a label to a task AddLabel { /// Task index (e.g. 3 or #3) or identifier (e.g. VEIN-3) task_id: String, /// Label ID label_id: i64, }, /// List all available labels ListLabels, /// Add a relation between two tasks AddRelation { /// Task index (e.g. 3 or #3) or identifier (e.g. VEIN-3) task_id: String, /// Other task identifier (e.g. VEIN-3) or numeric ID other_task_id: String, /// Relation kind (blocked, blocking, related, subtask, parenttask, etc.) relation_kind: String, }, /// Print MCP prompt text Prompt { #[command(subcommand)] prompt: PromptCommand, }, /// Create a new task in the project CreateTask { /// Task title title: String, /// Task description #[arg(short, long, default_value = "")] description: String, /// Priority: none, low, medium, high, urgent #[arg(short, long)] priority: Option<String>, },}
#[cfg(test)]mod tests { use super::*;
#[test] fn parses_init_subcommand() { let cli = Cli::parse_from(["vein", "init"]); assert!(matches!(cli.command, Some(Command::Init))); }
#[test] fn parses_list_projects_subcommand() { let cli = Cli::parse_from(["vein", "list-projects"]); assert!(matches!(cli.command, Some(Command::ListProjects))); }
#[test] fn parses_list_project_views_subcommand() { let cli = Cli::parse_from(["vein", "list-project-views", "42"]); assert!(matches!( cli.command, Some(Command::ListProjectViews { project_id: 42 }) )); }
#[test] fn parses_list_project_view_buckets_subcommand() { let cli = Cli::parse_from(["vein", "list-project-view-buckets", "5", "10"]); assert!(matches!( cli.command, Some(Command::ListProjectViewBuckets { project_id: 5, view_id: 10 }) )); }
#[test] fn parses_completions_subcommand() { let cli = Cli::parse_from(["vein", "completions", "fish"]); assert!(matches!( cli.command, Some(Command::Completions { shell: Shell::Fish }) )); }
#[test] fn parses_completions_bash() { let cli = Cli::parse_from(["vein", "completions", "bash"]); assert!(matches!( cli.command, Some(Command::Completions { shell: Shell::Bash }) )); }
#[test] fn parses_completions_zsh() { let cli = Cli::parse_from(["vein", "completions", "zsh"]); assert!(matches!( cli.command, Some(Command::Completions { shell: Shell::Zsh }) )); }
#[test] fn parses_list_ready_subcommand() { let cli = Cli::parse_from(["vein", "list-ready"]); assert!(matches!(cli.command, Some(Command::ListReady))); }
#[test] fn parses_list_tasks_subcommand() { let cli = Cli::parse_from(["vein", "list-tasks"]); assert!(matches!( cli.command, Some(Command::ListTasks { filter: None, search: None }) )); }
#[test] fn parses_list_tasks_with_filter_and_search() { let cli = Cli::parse_from(["vein", "list-tasks", "-f", "done = false", "-s", "login"]); match cli.command { Some(Command::ListTasks { filter: Some(f), search: Some(s), }) => { assert_eq!(f, "done = false"); assert_eq!(s, "login"); } other => panic!("expected ListTasks, got {other:?}"), } }
#[test] fn parses_get_task_subcommand() { let cli = Cli::parse_from(["vein", "get-task", "42"]); match cli.command { Some(Command::GetTask { task_id }) => assert_eq!(task_id, "42"), other => panic!("expected GetTask, got {other:?}"), } }
#[test] fn parses_get_task_with_identifier() { let cli = Cli::parse_from(["vein", "get-task", "VEIN-3"]); match cli.command { Some(Command::GetTask { task_id }) => assert_eq!(task_id, "VEIN-3"), other => panic!("expected GetTask, got {other:?}"), } }
#[test] fn parses_list_in_progress_subcommand() { let cli = Cli::parse_from(["vein", "list-in-progress"]); assert!(matches!(cli.command, Some(Command::ListInProgress))); }
#[test] fn parses_list_done_subcommand() { let cli = Cli::parse_from(["vein", "list-done"]); assert!(matches!(cli.command, Some(Command::ListDone))); }
#[test] fn parses_complete_subcommand() { let cli = Cli::parse_from(["vein", "complete", "42"]); match cli.command { Some(Command::Complete { task_id }) => assert_eq!(task_id, "42"), other => panic!("expected Complete, got {other:?}"), } }
#[test] fn parses_claim_subcommand() { let cli = Cli::parse_from(["vein", "claim", "VEIN-3"]); match cli.command { Some(Command::Claim { task_id }) => assert_eq!(task_id, "VEIN-3"), other => panic!("expected Claim, got {other:?}"), } }
#[test] fn parses_comment_subcommand() { let cli = Cli::parse_from(["vein", "comment", "42", "Work in progress"]); match cli.command { Some(Command::Comment { task_id, comment }) => { assert_eq!(task_id, "42"); assert_eq!(comment, "Work in progress"); } other => panic!("expected Comment, got {other:?}"), } }
#[test] fn parses_update_task_subcommand() { let cli = Cli::parse_from([ "vein", "update-task", "42", "-t", "New title", "-d", "New desc", ]); match cli.command { Some(Command::UpdateTask { task_id, title, description, .. }) => { assert_eq!(task_id, "42"); assert_eq!(title.as_deref(), Some("New title")); assert_eq!(description.as_deref(), Some("New desc")); } other => panic!("expected UpdateTask, got {other:?}"), } }
#[test] fn parses_add_relation_subcommand() { let cli = Cli::parse_from(["vein", "add-relation", "VEIN-1", "VEIN-2", "blocked"]); match cli.command { Some(Command::AddRelation { task_id, other_task_id, relation_kind, }) => { assert_eq!(task_id, "VEIN-1"); assert_eq!(other_task_id, "VEIN-2"); assert_eq!(relation_kind, "blocked"); } other => panic!("expected AddRelation, got {other:?}"), } }
#[test] fn parses_create_task_subcommand() { let cli = Cli::parse_from(["vein", "create-task", "Fix the bug"]); match cli.command { Some(Command::CreateTask { title, description, .. }) => { assert_eq!(title, "Fix the bug"); assert_eq!(description, ""); } other => panic!("expected CreateTask, got {other:?}"), } }
#[test] fn parses_create_task_with_description() { let cli = Cli::parse_from([ "vein", "create-task", "Fix the bug", "-d", "Something is broken", ]); match cli.command { Some(Command::CreateTask { title, description, .. }) => { assert_eq!(title, "Fix the bug"); assert_eq!(description, "Something is broken"); } other => panic!("expected CreateTask, got {other:?}"), } }
#[test] fn parses_create_task_with_priority() { let cli = Cli::parse_from(["vein", "create-task", "Urgent bug", "-p", "high"]); match cli.command { Some(Command::CreateTask { title, priority, .. }) => { assert_eq!(title, "Urgent bug"); assert_eq!(priority.as_deref(), Some("high")); } other => panic!("expected CreateTask, got {other:?}"), } }
#[test] fn parses_prompt_orient_subcommand() { let cli = Cli::parse_from(["vein", "prompt", "orient"]); assert!(matches!( cli.command, Some(Command::Prompt { prompt: PromptCommand::Orient }) )); }
#[test] fn prompt_without_subcommand_fails() { let result = Cli::try_parse_from(["vein", "prompt"]); assert!(result.is_err()); }
#[test] fn parses_board_subcommand() { let cli = Cli::parse_from(["vein", "board"]); assert!(matches!(cli.command, Some(Command::Board))); }
#[test] fn parses_serve_subcommand() { let cli = Cli::parse_from(["vein", "serve"]); assert!(matches!(cli.command, Some(Command::Serve))); }
#[test] fn no_subcommand_prints_help() { // With subcommand_required, parsing with no args should fail (prints help) let result = Cli::try_parse_from(["vein"]); assert!(result.is_err()); }}