diff options
| author | soryu <soryu@soryu.co> | 2026-02-01 01:35:18 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-01 01:35:18 +0000 |
| commit | c2750f86ebd6ac5c04b70dd8249501262d6dd07c (patch) | |
| tree | ecd4a32d74adc3479cdc30c94843a6447bf44649 /makima/src/daemon/api | |
| parent | 7567153e6281b94e39e52be5d060b381ed69597d (diff) | |
| download | soryu-c2750f86ebd6ac5c04b70dd8249501262d6dd07c.tar.gz soryu-c2750f86ebd6ac5c04b70dd8249501262d6dd07c.zip | |
[WIP] Heartbeat checkpoint - 2026-02-01 01:35:18 UTC
Diffstat (limited to 'makima/src/daemon/api')
| -rw-r--r-- | makima/src/daemon/api/contract.rs | 178 |
1 files changed, 177 insertions, 1 deletions
diff --git a/makima/src/daemon/api/contract.rs b/makima/src/daemon/api/contract.rs index 7c76b40..119c0ba 100644 --- a/makima/src/daemon/api/contract.rs +++ b/makima/src/daemon/api/contract.rs @@ -1,6 +1,6 @@ //! Contract API methods. -use serde::Serialize; +use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::client::{ApiClient, ApiError}; @@ -281,3 +281,179 @@ struct AddRemoteRepositoryRequest { repository_url: String, is_primary: bool, } + +// ============================================================================ +// Contracts cleanup types +// ============================================================================ + +/// Request for batch contract operations (cleanup, archive, etc.). +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BatchOperationRequest { + /// The operation to perform: "archive", "delete_archived", "cleanup_worktrees" + pub operation: String, + /// Age threshold in seconds (for archive and delete operations) + #[serde(skip_serializing_if = "Option::is_none")] + pub older_than_seconds: Option<u64>, + /// Status filter for the operation + #[serde(skip_serializing_if = "Option::is_none")] + pub status_filter: Option<Vec<String>>, + /// If true, only show what would be affected without making changes + #[serde(skip_serializing_if = "Option::is_none")] + pub dry_run: Option<bool>, +} + +/// Response from a batch operation. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BatchOperationResponse { + /// The operation that was performed + pub operation: String, + /// Number of items affected + pub affected_count: usize, + /// IDs of affected items + #[serde(default)] + pub affected_ids: Vec<Uuid>, + /// Any errors that occurred + #[serde(default)] + pub errors: Vec<String>, + /// Whether this was a dry run + #[serde(default)] + pub dry_run: bool, +} + +/// Summary of contracts that would be affected by cleanup. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CleanupPreviewResponse { + /// Contracts that would be archived + #[serde(default)] + pub to_archive: Vec<ContractSummary>, + /// Archived contracts that would be deleted + #[serde(default)] + pub to_delete: Vec<ContractSummary>, + /// Orphaned worktrees that would be cleaned up + #[serde(default)] + pub orphaned_worktrees: Vec<String>, +} + +/// Brief summary of a contract for cleanup operations. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ContractSummary { + pub id: Uuid, + pub name: String, + pub status: String, + #[serde(default)] + pub updated_at: Option<String>, +} + +impl ApiClient { + // ======================================================================== + // Contracts cleanup operations + // ======================================================================== + + /// Preview cleanup operations (dry run). + /// Returns what would be affected by archive, delete, and worktree cleanup. + pub async fn cleanup_preview( + &self, + older_than_seconds: u64, + archive: bool, + delete_archived: bool, + worktrees: bool, + ) -> Result<CleanupPreviewResponse, ApiError> { + let mut params = vec![ + format!("older_than_seconds={}", older_than_seconds), + "dry_run=true".to_string(), + ]; + if archive { + params.push("archive=true".to_string()); + } + if delete_archived { + params.push("delete_archived=true".to_string()); + } + if worktrees { + params.push("worktrees=true".to_string()); + } + let query = params.join("&"); + let result = self.get(&format!("/api/v1/contracts/cleanup?{}", query)).await?; + serde_json::from_value(result.0) + .map_err(|e| ApiError::Other(format!("Failed to parse cleanup preview: {}", e))) + } + + /// Archive completed/failed contracts older than the threshold. + pub async fn archive_contracts( + &self, + older_than_seconds: u64, + dry_run: bool, + ) -> Result<BatchOperationResponse, ApiError> { + let req = BatchOperationRequest { + operation: "archive".to_string(), + older_than_seconds: Some(older_than_seconds), + status_filter: Some(vec!["completed".to_string(), "failed".to_string()]), + dry_run: Some(dry_run), + }; + let result = self.post("/api/v1/contracts/batch", &req).await?; + serde_json::from_value(result.0) + .map_err(|e| ApiError::Other(format!("Failed to parse archive response: {}", e))) + } + + /// Delete archived contracts older than the threshold. + pub async fn delete_archived_contracts( + &self, + older_than_seconds: u64, + dry_run: bool, + ) -> Result<BatchOperationResponse, ApiError> { + let req = BatchOperationRequest { + operation: "delete_archived".to_string(), + older_than_seconds: Some(older_than_seconds), + status_filter: Some(vec!["archived".to_string()]), + dry_run: Some(dry_run), + }; + let result = self.post("/api/v1/contracts/batch", &req).await?; + serde_json::from_value(result.0) + .map_err(|e| ApiError::Other(format!("Failed to parse delete response: {}", e))) + } + + /// Clean up orphaned worktrees. + pub async fn cleanup_worktrees(&self, dry_run: bool) -> Result<BatchOperationResponse, ApiError> { + let req = BatchOperationRequest { + operation: "cleanup_worktrees".to_string(), + older_than_seconds: None, + status_filter: None, + dry_run: Some(dry_run), + }; + let result = self.post("/api/v1/contracts/batch", &req).await?; + serde_json::from_value(result.0) + .map_err(|e| ApiError::Other(format!("Failed to parse worktree cleanup response: {}", e))) + } + + /// List contracts with filtering options. + pub async fn list_contracts_filtered( + &self, + status: Option<&str>, + phase: Option<&str>, + stale: bool, + stale_threshold_seconds: Option<u64>, + ) -> Result<JsonValue, ApiError> { + let mut params = Vec::new(); + if let Some(s) = status { + params.push(format!("status={}", s)); + } + if let Some(p) = phase { + params.push(format!("phase={}", p)); + } + if stale { + params.push("stale=true".to_string()); + if let Some(threshold) = stale_threshold_seconds { + params.push(format!("stale_threshold={}", threshold)); + } + } + let query_string = if params.is_empty() { + String::new() + } else { + format!("?{}", params.join("&")) + }; + self.get(&format!("/api/v1/contracts{}", query_string)).await + } +} |
