summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-01 01:36:40 +0000
committersoryu <soryu@soryu.co>2026-02-01 01:36:40 +0000
commitd0b9edbe470f6bee134b5bc52108c77b05f28f87 (patch)
tree772c9a9dc6f84e9600c3f14864f75b2dd1c9b963
parentc2750f86ebd6ac5c04b70dd8249501262d6dd07c (diff)
downloadsoryu-d0b9edbe470f6bee134b5bc52108c77b05f28f87.tar.gz
soryu-d0b9edbe470f6bee134b5bc52108c77b05f28f87.zip
[WIP] Heartbeat checkpoint - 2026-02-01 01:36:40 UTC
-rw-r--r--makima/src/bin/makima.rs247
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?;