syncable_cli/agent/tools/platform/
list_deployments.rs1use 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#[derive(Debug, Deserialize)]
15pub struct ListDeploymentsArgs {
16 pub project_id: String,
18 pub limit: Option<i32>,
20}
21
22#[derive(Debug, thiserror::Error)]
24#[error("List deployments error: {0}")]
25pub struct ListDeploymentsError(String);
26
27#[derive(Debug, Clone, Serialize, Deserialize, Default)]
31pub struct ListDeploymentsTool;
32
33impl ListDeploymentsTool {
34 pub fn new() -> Self {
36 Self
37 }
38}
39
40impl Tool for ListDeploymentsTool {
41 const NAME: &'static str = "list_deployments";
42
43 type Error = ListDeploymentsError;
44 type Args = ListDeploymentsArgs;
45 type Output = String;
46
47 async fn definition(&self, _prompt: String) -> ToolDefinition {
48 ToolDefinition {
49 name: Self::NAME.to_string(),
50 description: r#"List recent deployments for a project.
51
52Returns a list of deployments with their status, commit SHA, public URLs,
53and creation timestamps.
54
55**Parameters:**
56- project_id: The project UUID
57- limit: Optional number of deployments to return (default 10)
58
59**Prerequisites:**
60- User must be authenticated via `sync-ctl auth login`
61
62**Use Cases:**
63- View deployment history for a project
64- Find the public URL of a deployed service
65- Check the status of recent deployments
66- Get task IDs for checking deployment status"#
67 .to_string(),
68 parameters: json!({
69 "type": "object",
70 "properties": {
71 "project_id": {
72 "type": "string",
73 "description": "The UUID of the project to list deployments for"
74 },
75 "limit": {
76 "type": "integer",
77 "description": "Optional: number of deployments to return (default 10)"
78 }
79 },
80 "required": ["project_id"]
81 }),
82 }
83 }
84
85 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
86 if args.project_id.trim().is_empty() {
88 return Ok(format_error_for_llm(
89 "list_deployments",
90 ErrorCategory::ValidationFailed,
91 "project_id cannot be empty",
92 Some(vec![
93 "Use list_projects to find valid project IDs",
94 "Use select_project to set the current project context",
95 ]),
96 ));
97 }
98
99 let client = match PlatformApiClient::new() {
101 Ok(c) => c,
102 Err(e) => {
103 return Ok(format_api_error("list_deployments", e));
104 }
105 };
106
107 match client.list_deployments(&args.project_id, args.limit).await {
109 Ok(paginated) => {
110 if paginated.data.is_empty() {
111 return Ok(json!({
112 "success": true,
113 "deployments": [],
114 "count": 0,
115 "has_more": false,
116 "message": "No deployments found for this project. Use trigger_deployment to start a deployment."
117 })
118 .to_string());
119 }
120
121 let deployment_list: Vec<serde_json::Value> = paginated
122 .data
123 .iter()
124 .map(|deployment| {
125 json!({
126 "id": deployment.id,
127 "service_name": deployment.service_name,
128 "repository": deployment.repository_full_name,
129 "status": deployment.status,
130 "task_id": deployment.backstage_task_id,
131 "commit_sha": deployment.commit_sha,
132 "public_url": deployment.public_url,
133 "created_at": deployment.created_at.to_rfc3339()
134 })
135 })
136 .collect();
137
138 let result = json!({
139 "success": true,
140 "deployments": deployment_list,
141 "count": paginated.data.len(),
142 "has_more": paginated.pagination.has_more,
143 "next_cursor": paginated.pagination.next_cursor,
144 "message": format!("Found {} deployment(s)", paginated.data.len())
145 });
146
147 serde_json::to_string_pretty(&result)
148 .map_err(|e| ListDeploymentsError(format!("Failed to serialize: {}", e)))
149 }
150 Err(e) => Ok(format_api_error("list_deployments", e)),
151 }
152 }
153}
154
155fn format_api_error(tool_name: &str, error: PlatformApiError) -> String {
157 match error {
158 PlatformApiError::Unauthorized => format_error_for_llm(
159 tool_name,
160 ErrorCategory::PermissionDenied,
161 "Not authenticated - please run `sync-ctl auth login` first",
162 Some(vec![
163 "The user needs to authenticate with the Syncable platform",
164 "Run: sync-ctl auth login",
165 ]),
166 ),
167 PlatformApiError::NotFound(msg) => format_error_for_llm(
168 tool_name,
169 ErrorCategory::ResourceUnavailable,
170 &format!("Resource not found: {}", msg),
171 Some(vec![
172 "The project ID may be incorrect",
173 "Use list_projects to find valid project IDs",
174 ]),
175 ),
176 PlatformApiError::PermissionDenied(msg) => format_error_for_llm(
177 tool_name,
178 ErrorCategory::PermissionDenied,
179 &format!("Permission denied: {}", msg),
180 Some(vec![
181 "The user does not have access to this project",
182 "Contact the project admin for access",
183 ]),
184 ),
185 PlatformApiError::RateLimited => format_error_for_llm(
186 tool_name,
187 ErrorCategory::ResourceUnavailable,
188 "Rate limit exceeded - please try again later",
189 Some(vec!["Wait a moment before retrying"]),
190 ),
191 PlatformApiError::HttpError(e) => format_error_for_llm(
192 tool_name,
193 ErrorCategory::NetworkError,
194 &format!("Network error: {}", e),
195 Some(vec![
196 "Check network connectivity",
197 "The Syncable API may be temporarily unavailable",
198 ]),
199 ),
200 PlatformApiError::ParseError(msg) => format_error_for_llm(
201 tool_name,
202 ErrorCategory::InternalError,
203 &format!("Failed to parse API response: {}", msg),
204 Some(vec!["This may be a temporary API issue"]),
205 ),
206 PlatformApiError::ApiError { status, message } => format_error_for_llm(
207 tool_name,
208 ErrorCategory::ExternalCommandFailed,
209 &format!("API error ({}): {}", status, message),
210 Some(vec!["Check the error message for details"]),
211 ),
212 PlatformApiError::ServerError { status, message } => format_error_for_llm(
213 tool_name,
214 ErrorCategory::ExternalCommandFailed,
215 &format!("Server error ({}): {}", status, message),
216 Some(vec![
217 "The Syncable API is experiencing issues",
218 "Try again later",
219 ]),
220 ),
221 PlatformApiError::ConnectionFailed => format_error_for_llm(
222 tool_name,
223 ErrorCategory::NetworkError,
224 "Could not connect to Syncable API",
225 Some(vec![
226 "Check your internet connection",
227 "The Syncable API may be temporarily unavailable",
228 ]),
229 ),
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn test_tool_name() {
239 assert_eq!(ListDeploymentsTool::NAME, "list_deployments");
240 }
241
242 #[test]
243 fn test_tool_creation() {
244 let tool = ListDeploymentsTool::new();
245 assert!(format!("{:?}", tool).contains("ListDeploymentsTool"));
246 }
247}