syncable_cli/agent/tools/platform/
list_deployment_configs.rs

1//! List deployment configs tool for the agent
2//!
3//! Allows the agent to list deployment configurations for a project.
4
5use rig::completion::ToolDefinition;
6use rig::tool::Tool;
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9
10use crate::agent::tools::error::{ErrorCategory, format_error_for_llm};
11use crate::platform::api::{PlatformApiClient, PlatformApiError};
12
13/// Arguments for the list deployment configs tool
14#[derive(Debug, Deserialize)]
15pub struct ListDeploymentConfigsArgs {
16    /// The project ID to list deployment configs for
17    pub project_id: String,
18}
19
20/// Error type for list deployment configs operations
21#[derive(Debug, thiserror::Error)]
22#[error("List deployment configs error: {0}")]
23pub struct ListDeploymentConfigsError(String);
24
25/// Tool to list deployment configurations for a project
26///
27/// Returns all deployment configs with service names, branches, target types,
28/// and auto-deploy settings.
29#[derive(Debug, Clone, Serialize, Deserialize, Default)]
30pub struct ListDeploymentConfigsTool;
31
32impl ListDeploymentConfigsTool {
33    /// Create a new ListDeploymentConfigsTool
34    pub fn new() -> Self {
35        Self
36    }
37}
38
39impl Tool for ListDeploymentConfigsTool {
40    const NAME: &'static str = "list_deployment_configs";
41
42    type Error = ListDeploymentConfigsError;
43    type Args = ListDeploymentConfigsArgs;
44    type Output = String;
45
46    async fn definition(&self, _prompt: String) -> ToolDefinition {
47        ToolDefinition {
48            name: Self::NAME.to_string(),
49            description: r#"List deployment configurations for a project.
50
51Returns all deployment configs associated with the project, including:
52- Service name and branch
53- Target type (kubernetes or cloud_runner)
54- Auto-deploy status
55- Port configuration
56
57**Prerequisites:**
58- User must be authenticated via `sync-ctl auth login`
59- A project must be selected (use select_project first)
60
61**Use Cases:**
62- View available deployment configurations before triggering a deployment
63- Check auto-deploy settings for services
64- Find the config_id needed to trigger a deployment"#
65                .to_string(),
66            parameters: json!({
67                "type": "object",
68                "properties": {
69                    "project_id": {
70                        "type": "string",
71                        "description": "The UUID of the project to list deployment configs for"
72                    }
73                },
74                "required": ["project_id"]
75            }),
76        }
77    }
78
79    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
80        // Validate project_id
81        if args.project_id.trim().is_empty() {
82            return Ok(format_error_for_llm(
83                "list_deployment_configs",
84                ErrorCategory::ValidationFailed,
85                "project_id cannot be empty",
86                Some(vec![
87                    "Use list_projects to find valid project IDs",
88                    "Use select_project to set the current project context",
89                ]),
90            ));
91        }
92
93        // Create the API client
94        let client = match PlatformApiClient::new() {
95            Ok(c) => c,
96            Err(e) => {
97                return Ok(format_api_error("list_deployment_configs", e));
98            }
99        };
100
101        // Fetch deployment configs
102        match client.list_deployment_configs(&args.project_id).await {
103            Ok(configs) => {
104                if configs.is_empty() {
105                    return Ok(json!({
106                        "success": true,
107                        "configs": [],
108                        "count": 0,
109                        "message": "No deployment configs found for this project. You may need to create a deployment configuration first."
110                    })
111                    .to_string());
112                }
113
114                let config_list: Vec<serde_json::Value> = configs
115                    .iter()
116                    .map(|config| {
117                        json!({
118                            "id": config.id,
119                            "service_name": config.service_name,
120                            "repository": config.repository_full_name,
121                            "branch": config.branch,
122                            "target_type": config.target_type,
123                            "port": config.port,
124                            "auto_deploy_enabled": config.auto_deploy_enabled,
125                            "deployment_strategy": config.deployment_strategy,
126                            "environment_id": config.environment_id,
127                            "created_at": config.created_at.to_rfc3339()
128                        })
129                    })
130                    .collect();
131
132                let result = json!({
133                    "success": true,
134                    "configs": config_list,
135                    "count": configs.len(),
136                    "message": format!("Found {} deployment configuration(s)", configs.len())
137                });
138
139                serde_json::to_string_pretty(&result)
140                    .map_err(|e| ListDeploymentConfigsError(format!("Failed to serialize: {}", e)))
141            }
142            Err(e) => Ok(format_api_error("list_deployment_configs", e)),
143        }
144    }
145}
146
147/// Format a PlatformApiError for LLM consumption
148fn format_api_error(tool_name: &str, error: PlatformApiError) -> String {
149    match error {
150        PlatformApiError::Unauthorized => format_error_for_llm(
151            tool_name,
152            ErrorCategory::PermissionDenied,
153            "Not authenticated - please run `sync-ctl auth login` first",
154            Some(vec![
155                "The user needs to authenticate with the Syncable platform",
156                "Run: sync-ctl auth login",
157            ]),
158        ),
159        PlatformApiError::NotFound(msg) => format_error_for_llm(
160            tool_name,
161            ErrorCategory::ResourceUnavailable,
162            &format!("Resource not found: {}", msg),
163            Some(vec![
164                "The project ID may be incorrect",
165                "Use list_projects to find valid project IDs",
166            ]),
167        ),
168        PlatformApiError::PermissionDenied(msg) => format_error_for_llm(
169            tool_name,
170            ErrorCategory::PermissionDenied,
171            &format!("Permission denied: {}", msg),
172            Some(vec![
173                "The user does not have access to this project",
174                "Contact the project admin for access",
175            ]),
176        ),
177        PlatformApiError::RateLimited => format_error_for_llm(
178            tool_name,
179            ErrorCategory::ResourceUnavailable,
180            "Rate limit exceeded - please try again later",
181            Some(vec!["Wait a moment before retrying"]),
182        ),
183        PlatformApiError::HttpError(e) => format_error_for_llm(
184            tool_name,
185            ErrorCategory::NetworkError,
186            &format!("Network error: {}", e),
187            Some(vec![
188                "Check network connectivity",
189                "The Syncable API may be temporarily unavailable",
190            ]),
191        ),
192        PlatformApiError::ParseError(msg) => format_error_for_llm(
193            tool_name,
194            ErrorCategory::InternalError,
195            &format!("Failed to parse API response: {}", msg),
196            Some(vec!["This may be a temporary API issue"]),
197        ),
198        PlatformApiError::ApiError { status, message } => format_error_for_llm(
199            tool_name,
200            ErrorCategory::ExternalCommandFailed,
201            &format!("API error ({}): {}", status, message),
202            Some(vec!["Check the error message for details"]),
203        ),
204        PlatformApiError::ServerError { status, message } => format_error_for_llm(
205            tool_name,
206            ErrorCategory::ExternalCommandFailed,
207            &format!("Server error ({}): {}", status, message),
208            Some(vec![
209                "The Syncable API is experiencing issues",
210                "Try again later",
211            ]),
212        ),
213        PlatformApiError::ConnectionFailed => format_error_for_llm(
214            tool_name,
215            ErrorCategory::NetworkError,
216            "Could not connect to Syncable API",
217            Some(vec![
218                "Check your internet connection",
219                "The Syncable API may be temporarily unavailable",
220            ]),
221        ),
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_tool_name() {
231        assert_eq!(ListDeploymentConfigsTool::NAME, "list_deployment_configs");
232    }
233
234    #[test]
235    fn test_tool_creation() {
236        let tool = ListDeploymentConfigsTool::new();
237        assert!(format!("{:?}", tool).contains("ListDeploymentConfigsTool"));
238    }
239}