diff options
| author | soryu <soryu@soryu.co> | 2026-02-01 01:36:40 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-01 01:36:40 +0000 |
| commit | d0b9edbe470f6bee134b5bc52108c77b05f28f87 (patch) | |
| tree | 772c9a9dc6f84e9600c3f14864f75b2dd1c9b963 | |
| parent | c2750f86ebd6ac5c04b70dd8249501262d6dd07c (diff) | |
| download | soryu-d0b9edbe470f6bee134b5bc52108c77b05f28f87.tar.gz soryu-d0b9edbe470f6bee134b5bc52108c77b05f28f87.zip | |
[WIP] Heartbeat checkpoint - 2026-02-01 01:36:40 UTC
| -rw-r--r-- | makima/src/bin/makima.rs | 247 |
1 files changed, 247 insertions, 0 deletions
diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs index 2795e5e..b546428 100644 --- a/makima/src/bin/makima.rs +++ b/makima/src/bin/makima.rs @@ -807,6 +807,253 @@ async fn run_red_team(cmd: RedTeamCommand) -> Result<(), Box<dyn std::error::Err } } +/// Run contracts management commands. +async fn run_contracts(cmd: ContractsCommand) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { + match cmd { + ContractsCommand::List(args) => run_contracts_list(args).await, + ContractsCommand::Cleanup(args) => run_contracts_cleanup(args).await, + } +} + +/// Run the contracts list command. +async fn run_contracts_list(args: ListArgs) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { + // Load config and merge with CLI args + let config = CliConfig::load(); + let api_url = if args.common.api_url.is_empty() || args.common.api_url == "https://api.makima.jp" { + config.get_api_url() + } else { + args.common.api_url.clone() + }; + let api_key = if args.common.api_key.is_empty() { + config.get_api_key().ok_or("API key required. Set via --api-key or MAKIMA_API_KEY")? + } else { + args.common.api_key.clone() + }; + + let client = ApiClient::new(api_url, api_key)?; + + // Parse format + let format: ListOutputFormat = args.parse_format().map_err(|e| { + eprintln!("Error: {}", e); + e + })?; + + // Parse stale threshold + let stale_threshold = if args.stale { + Some(args.parse_threshold().map_err(|e| { + eprintln!("Error parsing threshold: {}", e); + e + })?) + } else { + None + }; + + // Build status filter string + let status_filter = args.status.as_ref().map(|v| v.join(",")); + + // Fetch contracts + let result = client.list_contracts_filtered( + status_filter.as_deref(), + args.phase.as_deref(), + args.stale, + stale_threshold, + ).await?; + + // Extract contracts array + let mut contracts: Vec<serde_json::Value> = result.0.get("contracts") + .and_then(|v| v.as_array()) + .cloned() + .unwrap_or_default(); + + // Apply client-side filtering for "waiting" (if server doesn't support it) + if args.waiting { + // Filter to contracts that have pending questions + // This is a simplification - in practice, we'd need the server to track this + // For now, we can check if status is "active" and filter based on additional criteria + contracts.retain(|c| { + let status = c.get("status").and_then(|v| v.as_str()).unwrap_or(""); + // Contracts that are active and might be waiting for input + // In a more complete implementation, the server would track this + status == "active" + }); + } + + // Apply limit + if let Some(limit) = args.limit { + contracts.truncate(limit); + } + + // Output based on format + match format { + ListOutputFormat::Json => { + // JSON output - output the full contracts array + let output = serde_json::json!({ + "contracts": contracts, + "total": contracts.len() + }); + println!("{}", serde_json::to_string_pretty(&output)?); + } + ListOutputFormat::Compact => { + // Compact output - one line per contract + for contract in &contracts { + let id = contract.get("id").and_then(|v| v.as_str()).unwrap_or("?"); + let name = contract.get("name").and_then(|v| v.as_str()).unwrap_or("Unnamed"); + let status = contract.get("status").and_then(|v| v.as_str()).unwrap_or("?"); + let phase = contract.get("phase").and_then(|v| v.as_str()).unwrap_or("?"); + println!("{}\t{}\t{}\t{}", id, status, phase, name); + } + } + ListOutputFormat::Table => { + // Table output - formatted columns + if contracts.is_empty() { + println!("No contracts found."); + return Ok(()); + } + + // Print header + println!("{:<36} {:<10} {:<10} {:<6} {}", + "ID", "STATUS", "PHASE", "TASKS", "NAME"); + println!("{:-<36} {:-<10} {:-<10} {:-<6} {:-<30}", + "", "", "", "", ""); + + for contract in &contracts { + let id = contract.get("id").and_then(|v| v.as_str()).unwrap_or("?"); + let name = contract.get("name").and_then(|v| v.as_str()).unwrap_or("Unnamed"); + let status = contract.get("status").and_then(|v| v.as_str()).unwrap_or("?"); + let phase = contract.get("phase").and_then(|v| v.as_str()).unwrap_or("?"); + let task_count = contract.get("taskCount") + .or_else(|| contract.get("task_count")) + .and_then(|v| v.as_i64()) + .unwrap_or(0); + + // Truncate name if too long + let display_name = if name.len() > 40 { + format!("{}...", &name[..37]) + } else { + name.to_string() + }; + + println!("{:<36} {:<10} {:<10} {:<6} {}", + id, status, phase, task_count, display_name); + } + + println!(); + println!("Total: {} contract(s)", contracts.len()); + } + } + + Ok(()) +} + +/// Run the contracts cleanup command. +async fn run_contracts_cleanup(args: makima::daemon::cli::contract::CleanupArgs) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { + // Load config and merge with CLI args + let config = CliConfig::load(); + let api_url = if args.common.api_url.is_empty() || args.common.api_url == "https://api.makima.jp" { + config.get_api_url() + } else { + args.common.api_url.clone() + }; + let api_key = if args.common.api_key.is_empty() { + config.get_api_key().ok_or("API key required. Set via --api-key or MAKIMA_API_KEY")? + } else { + args.common.api_key.clone() + }; + + // Check if any operation is selected + if !args.has_any_operation() { + eprintln!("No cleanup operation specified. Use one of:"); + eprintln!(" --archive Archive old completed/failed contracts"); + eprintln!(" --delete-archived Delete old archived contracts"); + eprintln!(" --worktrees Clean up orphaned worktrees"); + eprintln!(" --all Run all cleanup operations"); + return Ok(()); + } + + let client = ApiClient::new(api_url, api_key)?; + let older_than_seconds = args.parse_older_than().map_err(|e| { + eprintln!("Error parsing --older-than: {}", e); + e + })?; + + let archive = args.archive || args.all; + let delete_archived = args.delete_archived || args.all; + let worktrees = args.worktrees || args.all; + + if args.dry_run { + // Preview mode + eprintln!("Previewing cleanup operations (dry run)..."); + let preview = client.cleanup_preview(older_than_seconds, archive, delete_archived, worktrees).await?; + + if archive && !preview.to_archive.is_empty() { + eprintln!("\nContracts to archive ({}):", preview.to_archive.len()); + for c in &preview.to_archive { + eprintln!(" {} - {} ({})", c.id, c.name, c.status); + } + } + + if delete_archived && !preview.to_delete.is_empty() { + eprintln!("\nArchived contracts to delete ({}):", preview.to_delete.len()); + for c in &preview.to_delete { + eprintln!(" {} - {}", c.id, c.name); + } + } + + if worktrees && !preview.orphaned_worktrees.is_empty() { + eprintln!("\nOrphaned worktrees to clean up ({}):", preview.orphaned_worktrees.len()); + for path in &preview.orphaned_worktrees { + eprintln!(" {}", path); + } + } + + if preview.to_archive.is_empty() && preview.to_delete.is_empty() && preview.orphaned_worktrees.is_empty() { + eprintln!("\nNothing to clean up."); + } else { + eprintln!("\nRun without --dry-run to execute these operations."); + } + } else { + // Execute cleanup + if !args.force { + eprintln!("This will perform cleanup operations. Use --force to skip confirmation."); + eprintln!("Operations selected:"); + if archive { eprintln!(" - Archive completed/failed contracts older than {}", args.older_than); } + if delete_archived { eprintln!(" - Delete archived contracts older than {}", args.older_than); } + if worktrees { eprintln!(" - Clean up orphaned worktrees"); } + eprintln!("\nContinue? [y/N] "); + + let mut input = String::new(); + std::io::stdin().read_line(&mut input)?; + if !input.trim().eq_ignore_ascii_case("y") { + eprintln!("Cancelled."); + return Ok(()); + } + } + + // Execute operations + if archive { + eprintln!("Archiving old contracts..."); + let result = client.archive_contracts(older_than_seconds, false).await?; + eprintln!(" Archived {} contract(s)", result.affected_count); + } + + if delete_archived { + eprintln!("Deleting archived contracts..."); + let result = client.delete_archived_contracts(older_than_seconds, false).await?; + eprintln!(" Deleted {} contract(s)", result.affected_count); + } + + if worktrees { + eprintln!("Cleaning up orphaned worktrees..."); + let result = client.cleanup_worktrees(false).await?; + eprintln!(" Cleaned up {} worktree(s)", result.affected_count); + } + + eprintln!("\nCleanup complete."); + } + + Ok(()) +} + /// Load contracts from API async fn load_contracts(client: &ApiClient) -> Result<Vec<ListItem>, Box<dyn std::error::Error + Send + Sync>> { let result = client.list_contracts().await?; |
