syncable_cli/agent/tools/platform/
list_deployment_capabilities.rs1use rig::completion::ToolDefinition;
7use rig::tool::Tool;
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10
11use crate::agent::tools::error::{ErrorCategory, format_error_for_llm};
12use crate::platform::api::{PlatformApiClient, PlatformApiError};
13use crate::wizard::get_provider_deployment_statuses;
14
15#[derive(Debug, Deserialize)]
17pub struct ListDeploymentCapabilitiesArgs {
18 pub project_id: String,
20}
21
22#[derive(Debug, thiserror::Error)]
24#[error("List deployment capabilities error: {0}")]
25pub struct ListDeploymentCapabilitiesError(String);
26
27#[derive(Debug, Clone, Serialize, Deserialize, Default)]
32pub struct ListDeploymentCapabilitiesTool;
33
34impl ListDeploymentCapabilitiesTool {
35 pub fn new() -> Self {
37 Self
38 }
39}
40
41impl Tool for ListDeploymentCapabilitiesTool {
42 const NAME: &'static str = "list_deployment_capabilities";
43
44 type Error = ListDeploymentCapabilitiesError;
45 type Args = ListDeploymentCapabilitiesArgs;
46 type Output = String;
47
48 async fn definition(&self, _prompt: String) -> ToolDefinition {
49 ToolDefinition {
50 name: Self::NAME.to_string(),
51 description: r#"List available deployment capabilities for a project.
52
53Returns information about which cloud providers are connected and what deployment
54targets are available (clusters, registries, Cloud Run).
55
56**Parameters:**
57- project_id: The UUID of the project to check
58
59**Prerequisites:**
60- User must be authenticated via `sync-ctl auth login`
61- User must have access to the project
62
63**What it returns:**
64- providers: Array of provider status objects with:
65 - provider: Provider name (Gcp, Hetzner, Aws, Azure, Scaleway, Cyso)
66 - is_available: Whether the provider is currently supported (false = coming soon)
67 - is_connected: Whether the provider has cloud credentials
68 - cloud_runner_available: Whether Cloud Run/serverless is available
69 - clusters: Array of available Kubernetes clusters
70 - registries: Array of available container registries
71 - summary: Human-readable status
72
73**Provider Availability:**
74- Available now: GCP, Hetzner, Azure
75- Coming soon: AWS, Scaleway, Cyso Cloud
76
77**Use Cases:**
78- Before creating a deployment, check what options are available
79- Verify a provider is connected before attempting deployment
80- Find cluster and registry IDs for deployment configuration"#
81 .to_string(),
82 parameters: json!({
83 "type": "object",
84 "properties": {
85 "project_id": {
86 "type": "string",
87 "description": "The UUID of the project"
88 }
89 },
90 "required": ["project_id"]
91 }),
92 }
93 }
94
95 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
96 if args.project_id.trim().is_empty() {
98 return Ok(format_error_for_llm(
99 "list_deployment_capabilities",
100 ErrorCategory::ValidationFailed,
101 "project_id cannot be empty",
102 Some(vec![
103 "Use list_projects to find valid project IDs",
104 "Use current_context to get the currently selected project",
105 ]),
106 ));
107 }
108
109 let client = match PlatformApiClient::new() {
111 Ok(c) => c,
112 Err(e) => {
113 return Ok(format_api_error("list_deployment_capabilities", e));
114 }
115 };
116
117 match get_provider_deployment_statuses(&client, &args.project_id).await {
119 Ok(statuses) => {
120 let available_connected_count = statuses
122 .iter()
123 .filter(|s| s.provider.is_available() && s.is_connected)
124 .count();
125 let total_clusters: usize = statuses.iter().map(|s| s.clusters.len()).sum();
126 let total_registries: usize = statuses.iter().map(|s| s.registries.len()).sum();
127
128 let provider_data: Vec<serde_json::Value> = statuses
130 .iter()
131 .map(|s| {
132 let clusters: Vec<serde_json::Value> = s
133 .clusters
134 .iter()
135 .map(|c| {
136 json!({
137 "id": c.id,
138 "name": c.name,
139 "region": c.region,
140 "is_healthy": c.is_healthy,
141 })
142 })
143 .collect();
144
145 let registries: Vec<serde_json::Value> = s
146 .registries
147 .iter()
148 .map(|r| {
149 json!({
150 "id": r.id,
151 "name": r.name,
152 "region": r.region,
153 "is_ready": r.is_ready,
154 })
155 })
156 .collect();
157
158 json!({
159 "provider": format!("{:?}", s.provider),
160 "is_available": s.provider.is_available(),
161 "is_connected": s.is_connected,
162 "cloud_runner_available": s.cloud_runner_available,
163 "clusters": clusters,
164 "registries": registries,
165 "summary": if s.provider.is_available() {
166 s.summary.clone()
167 } else {
168 "Coming soon".to_string()
169 },
170 })
171 })
172 .collect();
173
174 let summary = if available_connected_count == 0 {
176 "No available providers connected. Connect GCP, Hetzner, or Azure in platform settings.".to_string()
177 } else {
178 let mut parts = vec![format!(
179 "{} provider{} ready",
180 available_connected_count,
181 if available_connected_count == 1 {
182 ""
183 } else {
184 "s"
185 }
186 )];
187 if total_clusters > 0 {
188 parts.push(format!(
189 "{} cluster{}",
190 total_clusters,
191 if total_clusters == 1 { "" } else { "s" }
192 ));
193 }
194 if total_registries > 0 {
195 parts.push(format!(
196 "{} registr{}",
197 total_registries,
198 if total_registries == 1 { "y" } else { "ies" }
199 ));
200 }
201 parts.join(", ")
202 };
203
204 let result = json!({
205 "success": true,
206 "project_id": args.project_id,
207 "providers": provider_data,
208 "summary": summary,
209 "available_connected_count": available_connected_count,
210 "total_clusters": total_clusters,
211 "total_registries": total_registries,
212 "coming_soon_providers": ["AWS", "Scaleway", "Cyso Cloud"],
213 "next_steps": if available_connected_count > 0 {
214 vec![
215 "Use analyze_project to discover Dockerfiles in the project",
216 "Use create_deployment_config to create a deployment configuration",
217 "For Cloud Run deployments, no cluster is needed",
218 "Note: AWS, Scaleway, and Cyso Cloud are coming soon"
219 ]
220 } else {
221 vec![
222 "Use open_provider_settings to connect GCP, Hetzner, or Azure",
223 "After connecting, run this tool again to see available options",
224 "Note: AWS, Scaleway, and Cyso Cloud are coming soon"
225 ]
226 }
227 });
228
229 serde_json::to_string_pretty(&result).map_err(|e| {
230 ListDeploymentCapabilitiesError(format!("Failed to serialize: {}", e))
231 })
232 }
233 Err(e) => Ok(format_api_error("list_deployment_capabilities", e)),
234 }
235 }
236}
237
238fn format_api_error(tool_name: &str, error: PlatformApiError) -> String {
240 match error {
241 PlatformApiError::Unauthorized => format_error_for_llm(
242 tool_name,
243 ErrorCategory::PermissionDenied,
244 "Not authenticated - please run `sync-ctl auth login` first",
245 Some(vec![
246 "The user needs to authenticate with the Syncable platform",
247 "Run: sync-ctl auth login",
248 ]),
249 ),
250 PlatformApiError::NotFound(msg) => format_error_for_llm(
251 tool_name,
252 ErrorCategory::ResourceUnavailable,
253 &format!("Resource not found: {}", msg),
254 Some(vec![
255 "The project ID may be incorrect",
256 "Use list_projects to find valid project IDs",
257 ]),
258 ),
259 PlatformApiError::PermissionDenied(msg) => format_error_for_llm(
260 tool_name,
261 ErrorCategory::PermissionDenied,
262 &format!("Permission denied: {}", msg),
263 Some(vec![
264 "The user does not have access to this project",
265 "Contact the project admin for access",
266 ]),
267 ),
268 PlatformApiError::RateLimited => format_error_for_llm(
269 tool_name,
270 ErrorCategory::ResourceUnavailable,
271 "Rate limit exceeded - please try again later",
272 Some(vec!["Wait a moment before retrying"]),
273 ),
274 PlatformApiError::HttpError(e) => format_error_for_llm(
275 tool_name,
276 ErrorCategory::NetworkError,
277 &format!("Network error: {}", e),
278 Some(vec![
279 "Check network connectivity",
280 "The Syncable API may be temporarily unavailable",
281 ]),
282 ),
283 PlatformApiError::ParseError(msg) => format_error_for_llm(
284 tool_name,
285 ErrorCategory::InternalError,
286 &format!("Failed to parse API response: {}", msg),
287 Some(vec!["This may be a temporary API issue"]),
288 ),
289 PlatformApiError::ApiError { status, message } => format_error_for_llm(
290 tool_name,
291 ErrorCategory::ExternalCommandFailed,
292 &format!("API error ({}): {}", status, message),
293 Some(vec!["Check the error message for details"]),
294 ),
295 PlatformApiError::ServerError { status, message } => format_error_for_llm(
296 tool_name,
297 ErrorCategory::ExternalCommandFailed,
298 &format!("Server error ({}): {}", status, message),
299 Some(vec![
300 "The Syncable API is experiencing issues",
301 "Try again later",
302 ]),
303 ),
304 PlatformApiError::ConnectionFailed => format_error_for_llm(
305 tool_name,
306 ErrorCategory::NetworkError,
307 "Could not connect to Syncable API",
308 Some(vec![
309 "Check your internet connection",
310 "The Syncable API may be temporarily unavailable",
311 ]),
312 ),
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn test_tool_name() {
322 assert_eq!(
323 ListDeploymentCapabilitiesTool::NAME,
324 "list_deployment_capabilities"
325 );
326 }
327
328 #[test]
329 fn test_tool_creation() {
330 let tool = ListDeploymentCapabilitiesTool::new();
331 assert!(format!("{:?}", tool).contains("ListDeploymentCapabilitiesTool"));
332 }
333}