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
75- Coming soon: AWS, Azure, 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 or Hetzner in platform settings.".to_string()
177 } else {
178 let mut parts = vec![format!("{} provider{} ready", available_connected_count, if available_connected_count == 1 { "" } else { "s" })];
179 if total_clusters > 0 {
180 parts.push(format!("{} cluster{}", total_clusters, if total_clusters == 1 { "" } else { "s" }));
181 }
182 if total_registries > 0 {
183 parts.push(format!("{} registr{}", total_registries, if total_registries == 1 { "y" } else { "ies" }));
184 }
185 parts.join(", ")
186 };
187
188 let result = json!({
189 "success": true,
190 "project_id": args.project_id,
191 "providers": provider_data,
192 "summary": summary,
193 "available_connected_count": available_connected_count,
194 "total_clusters": total_clusters,
195 "total_registries": total_registries,
196 "coming_soon_providers": ["AWS", "Azure", "Scaleway", "Cyso Cloud"],
197 "next_steps": if available_connected_count > 0 {
198 vec![
199 "Use analyze_project to discover Dockerfiles in the project",
200 "Use create_deployment_config to create a deployment configuration",
201 "For Cloud Run deployments, no cluster is needed",
202 "Note: AWS, Azure, Scaleway, and Cyso Cloud are coming soon"
203 ]
204 } else {
205 vec![
206 "Use open_provider_settings to connect GCP or Hetzner",
207 "After connecting, run this tool again to see available options",
208 "Note: AWS, Azure, Scaleway, and Cyso Cloud are coming soon"
209 ]
210 }
211 });
212
213 serde_json::to_string_pretty(&result)
214 .map_err(|e| ListDeploymentCapabilitiesError(format!("Failed to serialize: {}", e)))
215 }
216 Err(e) => Ok(format_api_error("list_deployment_capabilities", e)),
217 }
218 }
219}
220
221fn format_api_error(tool_name: &str, error: PlatformApiError) -> String {
223 match error {
224 PlatformApiError::Unauthorized => format_error_for_llm(
225 tool_name,
226 ErrorCategory::PermissionDenied,
227 "Not authenticated - please run `sync-ctl auth login` first",
228 Some(vec![
229 "The user needs to authenticate with the Syncable platform",
230 "Run: sync-ctl auth login",
231 ]),
232 ),
233 PlatformApiError::NotFound(msg) => format_error_for_llm(
234 tool_name,
235 ErrorCategory::ResourceUnavailable,
236 &format!("Resource not found: {}", msg),
237 Some(vec![
238 "The project ID may be incorrect",
239 "Use list_projects to find valid project IDs",
240 ]),
241 ),
242 PlatformApiError::PermissionDenied(msg) => format_error_for_llm(
243 tool_name,
244 ErrorCategory::PermissionDenied,
245 &format!("Permission denied: {}", msg),
246 Some(vec![
247 "The user does not have access to this project",
248 "Contact the project admin for access",
249 ]),
250 ),
251 PlatformApiError::RateLimited => format_error_for_llm(
252 tool_name,
253 ErrorCategory::ResourceUnavailable,
254 "Rate limit exceeded - please try again later",
255 Some(vec!["Wait a moment before retrying"]),
256 ),
257 PlatformApiError::HttpError(e) => format_error_for_llm(
258 tool_name,
259 ErrorCategory::NetworkError,
260 &format!("Network error: {}", e),
261 Some(vec![
262 "Check network connectivity",
263 "The Syncable API may be temporarily unavailable",
264 ]),
265 ),
266 PlatformApiError::ParseError(msg) => format_error_for_llm(
267 tool_name,
268 ErrorCategory::InternalError,
269 &format!("Failed to parse API response: {}", msg),
270 Some(vec!["This may be a temporary API issue"]),
271 ),
272 PlatformApiError::ApiError { status, message } => format_error_for_llm(
273 tool_name,
274 ErrorCategory::ExternalCommandFailed,
275 &format!("API error ({}): {}", status, message),
276 Some(vec!["Check the error message for details"]),
277 ),
278 PlatformApiError::ServerError { status, message } => format_error_for_llm(
279 tool_name,
280 ErrorCategory::ExternalCommandFailed,
281 &format!("Server error ({}): {}", status, message),
282 Some(vec![
283 "The Syncable API is experiencing issues",
284 "Try again later",
285 ]),
286 ),
287 PlatformApiError::ConnectionFailed => format_error_for_llm(
288 tool_name,
289 ErrorCategory::NetworkError,
290 "Could not connect to Syncable API",
291 Some(vec![
292 "Check your internet connection",
293 "The Syncable API may be temporarily unavailable",
294 ]),
295 ),
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn test_tool_name() {
305 assert_eq!(ListDeploymentCapabilitiesTool::NAME, "list_deployment_capabilities");
306 }
307
308 #[test]
309 fn test_tool_creation() {
310 let tool = ListDeploymentCapabilitiesTool::new();
311 assert!(format!("{:?}", tool).contains("ListDeploymentCapabilitiesTool"));
312 }
313}