//! Contract subcommand - task-contract interaction commands. use clap::{Args, Subcommand}; use uuid::Uuid; /// Common arguments for contract commands. #[derive(Args, Debug, Clone)] pub struct ContractArgs { /// API URL #[arg(long, env = "MAKIMA_API_URL", default_value = "https://api.makima.jp", global = true)] pub api_url: String, /// API key for authentication #[arg(long, env = "MAKIMA_API_KEY", global = true)] pub api_key: String, /// Current task ID (optional) #[arg(long, env = "MAKIMA_TASK_ID", global = true)] pub task_id: Option, /// Contract ID #[arg(long, env = "MAKIMA_CONTRACT_ID", global = true)] pub contract_id: Uuid, } /// Arguments for file command (get specific file). #[derive(Args, Debug)] pub struct FileArgs { #[command(flatten)] pub common: ContractArgs, /// File ID to retrieve pub file_id: Uuid, } /// Arguments for report command. #[derive(Args, Debug)] pub struct ReportArgs { #[command(flatten)] pub common: ContractArgs, /// Progress message pub message: String, } /// Arguments for completion-action command. #[derive(Args, Debug)] pub struct CompletionActionArgs { #[command(flatten)] pub common: ContractArgs, /// Comma-separated list of modified files #[arg(long)] pub files: Option, /// Number of lines added #[arg(long, default_value = "0")] pub lines_added: i32, /// Number of lines removed #[arg(long, default_value = "0")] pub lines_removed: i32, /// Whether there are code changes #[arg(long)] pub code: bool, } /// Arguments for update-file command. #[derive(Args, Debug)] pub struct UpdateFileArgs { #[command(flatten)] pub common: ContractArgs, /// File ID to update pub file_id: Uuid, } /// Arguments for create-file command. #[derive(Args, Debug)] pub struct CreateFileArgs { #[command(flatten)] pub common: ContractArgs, /// Name of the new file pub name: String, } // ============================================================================ // Contracts management commands (makima contracts ...) // ============================================================================ /// Common arguments for contracts management commands. #[derive(Args, Debug, Clone)] pub struct ContractsCommonArgs { /// API URL #[arg(long, env = "MAKIMA_API_URL", default_value = "https://api.makima.jp", global = true)] pub api_url: String, /// API key for authentication #[arg(long, env = "MAKIMA_API_KEY", global = true)] pub api_key: String, } /// Contracts management subcommands. #[derive(Subcommand, Debug)] pub enum ContractsSubcommand { /// List all contracts with optional filters List(ListArgs), /// Clean up old contracts, archived contracts, and orphaned worktrees Cleanup(CleanupArgs), } /// Output format for list command. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum ListOutputFormat { #[default] Table, Json, Compact, } impl std::str::FromStr for ListOutputFormat { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "table" => Ok(ListOutputFormat::Table), "json" => Ok(ListOutputFormat::Json), "compact" => Ok(ListOutputFormat::Compact), _ => Err(format!("Invalid format '{}'. Valid options: table, json, compact", s)), } } } impl std::fmt::Display for ListOutputFormat { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ListOutputFormat::Table => write!(f, "table"), ListOutputFormat::Json => write!(f, "json"), ListOutputFormat::Compact => write!(f, "compact"), } } } /// Arguments for the list subcommand. #[derive(Args, Debug)] pub struct ListArgs { #[command(flatten)] pub common: ContractsCommonArgs, /// Filter by status (active, completed, failed) #[arg(long, value_delimiter = ',')] pub status: Option>, /// Show only stale contracts (no activity within threshold) #[arg(long)] pub stale: bool, /// Stale threshold duration (e.g., "30m", "1h", "2d"). Default: 30m #[arg(long, default_value = "30m")] pub threshold: String, /// Show contracts waiting for user input #[arg(long)] pub waiting: bool, /// Filter by phase (e.g., "plan", "execute", "review") #[arg(long)] pub phase: Option, /// Output format: table, json, or compact #[arg(long, short = 'f', default_value = "table")] pub format: String, /// Limit number of results #[arg(long, short = 'n')] pub limit: Option, } impl ListArgs { /// Parse the format string into a ListOutputFormat enum. pub fn parse_format(&self) -> Result { self.format.parse() } /// Parse the threshold duration into seconds. pub fn parse_threshold(&self) -> Result { parse_duration(&self.threshold) } } /// Arguments for the cleanup subcommand. #[derive(Args, Debug)] pub struct CleanupArgs { #[command(flatten)] pub common: ContractsCommonArgs, /// Archive completed/failed contracts older than the threshold #[arg(long)] pub archive: bool, /// Delete archived contracts older than the threshold #[arg(long)] pub delete_archived: bool, /// Clean up orphaned worktrees (worktrees without associated contracts) #[arg(long)] pub worktrees: bool, /// Run all cleanup operations (archive, delete-archived, worktrees) #[arg(long)] pub all: bool, /// Age threshold for cleanup operations (e.g., "7d", "24h", "30d") /// Default: 7d (7 days) #[arg(long, default_value = "7d")] pub older_than: String, /// Show what would be affected without making changes #[arg(long)] pub dry_run: bool, /// Skip confirmation prompts for destructive operations #[arg(long)] pub force: bool, } impl CleanupArgs { /// Parse the older_than duration string into seconds. /// Supports formats like "7d", "24h", "30m", "60s". pub fn parse_older_than(&self) -> Result { parse_duration(&self.older_than) } /// Returns true if any cleanup operation is selected. pub fn has_any_operation(&self) -> bool { self.archive || self.delete_archived || self.worktrees || self.all } } /// Parse a duration string like "7d", "24h", "30m", "60s" into seconds. pub fn parse_duration(s: &str) -> Result { let s = s.trim(); if s.is_empty() { return Err("Empty duration string".to_string()); } let (num_str, unit) = if s.ends_with('d') || s.ends_with('D') { (&s[..s.len() - 1], 'd') } else if s.ends_with('h') || s.ends_with('H') { (&s[..s.len() - 1], 'h') } else if s.ends_with('m') || s.ends_with('M') { (&s[..s.len() - 1], 'm') } else if s.ends_with('s') || s.ends_with('S') { (&s[..s.len() - 1], 's') } else { // Default to days if no unit (s, 'd') }; let num: u64 = num_str .parse() .map_err(|_| format!("Invalid number in duration: {}", num_str))?; let seconds = match unit { 'd' => num * 24 * 60 * 60, 'h' => num * 60 * 60, 'm' => num * 60, 's' => num, _ => unreachable!(), }; Ok(seconds) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_duration() { assert_eq!(parse_duration("7d").unwrap(), 7 * 24 * 60 * 60); assert_eq!(parse_duration("24h").unwrap(), 24 * 60 * 60); assert_eq!(parse_duration("30m").unwrap(), 30 * 60); assert_eq!(parse_duration("60s").unwrap(), 60); assert_eq!(parse_duration("7D").unwrap(), 7 * 24 * 60 * 60); assert_eq!(parse_duration("7").unwrap(), 7 * 24 * 60 * 60); // defaults to days assert!(parse_duration("").is_err()); assert!(parse_duration("abc").is_err()); } }