From c2750f86ebd6ac5c04b70dd8249501262d6dd07c Mon Sep 17 00:00:00 2001 From: soryu Date: Sun, 1 Feb 2026 01:35:18 +0000 Subject: [WIP] Heartbeat checkpoint - 2026-02-01 01:35:18 UTC --- makima/src/daemon/cli/contract.rs | 208 +++++++++++++++++++++++++++++++++++++- makima/src/daemon/cli/mod.rs | 21 ++++ 2 files changed, 228 insertions(+), 1 deletion(-) (limited to 'makima/src/daemon/cli') diff --git a/makima/src/daemon/cli/contract.rs b/makima/src/daemon/cli/contract.rs index a443b85..9d6784d 100644 --- a/makima/src/daemon/cli/contract.rs +++ b/makima/src/daemon/cli/contract.rs @@ -1,6 +1,6 @@ //! Contract subcommand - task-contract interaction commands. -use clap::Args; +use clap::{Args, Subcommand}; use uuid::Uuid; /// Common arguments for contract commands. @@ -85,3 +85,209 @@ pub struct CreateFileArgs { /// 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()); + } +} diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs index c848e8e..8538afa 100644 --- a/makima/src/daemon/cli/mod.rs +++ b/makima/src/daemon/cli/mod.rs @@ -65,6 +65,27 @@ pub enum Commands { /// Red team commands for adversarial monitoring #[command(name = "red-team", subcommand)] RedTeam(RedTeamCommand), + + /// Contract management commands (list, cleanup) + #[command(subcommand)] + Contracts(ContractsCommand), +} + +/// Contracts management subcommands for multi-contract operations. +#[derive(Subcommand, Debug)] +pub enum ContractsCommand { + /// List all contracts with optional filters + /// + /// Examples: + /// makima contracts list # List all contracts + /// makima contracts list --status active # List only active contracts + /// makima contracts list --stale # Show stale contracts + /// makima contracts list --waiting # Show contracts waiting for input + /// makima contracts list --format json # Output as JSON + List(contract::ListArgs), + + /// Clean up old contracts, archived contracts, and orphaned worktrees + Cleanup(contract::CleanupArgs), } /// Config subcommands for CLI configuration. -- cgit v1.2.3