fastskill_core/http/handlers/
skills.rs1use crate::http::errors::{HttpError, HttpResult};
4use crate::http::handlers::AppState;
5use crate::http::models::*;
6use axum::{
7 extract::{Path, State},
8 Json,
9};
10use validator::Validate;
11
12#[derive(serde::Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct UpgradeRequest {
15 skill_id: Option<String>,
16}
17
18fn skill_metadata_json(skill: &crate::core::skill_manager::SkillDefinition) -> serde_json::Value {
19 let source_type = skill
20 .source_type
21 .as_ref()
22 .map(|t| serde_json::to_value(t).unwrap_or(serde_json::Value::Null));
23 serde_json::json!({
24 "id": skill.id,
25 "name": skill.name,
26 "description": skill.description,
27 "version": skill.version,
28 "author": skill.author,
29 "enabled": skill.enabled,
30 "created_at": skill.created_at.to_rfc3339(),
31 "updated_at": skill.updated_at.to_rfc3339(),
32 "skill_file": skill.skill_file,
33 "reference_files": skill.reference_files,
34 "script_files": skill.script_files,
35 "asset_files": skill.asset_files,
36 "source_url": skill.source_url,
37 "source_type": source_type
38 })
39}
40
41pub async fn list_skills(
43 State(state): State<AppState>,
44) -> HttpResult<axum::Json<ApiResponse<SkillsListResponse>>> {
45 let skills = state.service.skill_manager().list_skills(None).await?;
46
47 let skill_responses: Vec<SkillResponse> = skills
48 .clone()
49 .into_iter()
50 .map(|skill| SkillResponse {
51 id: skill.id.to_string(),
52 name: skill.name.clone(),
53 description: skill.description.clone(),
54 metadata: skill_metadata_json(&skill),
55 created_at: Some(skill.created_at.to_rfc3339()),
56 updated_at: Some(skill.updated_at.to_rfc3339()),
57 })
58 .collect();
59
60 let response = SkillsListResponse {
61 skills: skill_responses,
62 count: skills.len(),
63 total: skills.len(),
64 };
65
66 Ok(axum::Json(ApiResponse::success(response)))
67}
68
69pub async fn get_skill(
71 State(state): State<AppState>,
72 Path(skill_id): Path<String>,
73) -> HttpResult<axum::Json<ApiResponse<SkillResponse>>> {
74 let skills = state.service.skill_manager().list_skills(None).await?;
77 let skill_id_parsed = crate::core::service::SkillId::new(skill_id.clone())
78 .map_err(|_| HttpError::BadRequest("Invalid skill ID format".to_string()))?;
79 let skill = skills
80 .into_iter()
81 .find(|s| s.id == skill_id_parsed)
82 .ok_or_else(|| HttpError::NotFound(format!("Skill not found: {}", skill_id)))?;
83
84 let metadata = skill_metadata_json(&skill);
85
86 let response = SkillResponse {
87 id: skill.id.to_string(),
88 name: skill.name.clone(),
89 description: skill.description.clone(),
90 metadata,
91 created_at: Some(skill.created_at.to_rfc3339()),
92 updated_at: Some(skill.updated_at.to_rfc3339()),
93 };
94
95 Ok(axum::Json(ApiResponse::success(response)))
96}
97
98pub async fn create_skill(
100 State(_state): State<AppState>,
101 Json(_request): Json<SkillRequest>,
102) -> HttpResult<axum::Json<ApiResponse<SkillResponse>>> {
103 _request.validate().map_err(|e| {
107 HttpError::ValidationError(
108 e.field_errors()
109 .into_iter()
110 .map(|(field, errors)| {
111 (
112 field.to_string(),
113 errors
114 .iter()
115 .map(|e| e.message.clone().unwrap_or_default().to_string())
116 .collect(),
117 )
118 })
119 .collect(),
120 )
121 })?;
122
123 let _skill_def = serde_json::json!({
125 "id": format!("skill_{}", chrono::Utc::now().timestamp()),
126 "name": _request.name,
127 "description": _request.description,
128 });
129
130 Err(HttpError::InternalServerError(
133 "Skill creation not yet implemented".to_string(),
134 ))
135}
136
137pub async fn update_skill(
139 State(_state): State<AppState>,
140 Path(_skill_id): Path<String>,
141 Json(request): Json<SkillRequest>,
142) -> HttpResult<axum::Json<ApiResponse<SkillResponse>>> {
143 request.validate().map_err(|e| {
147 HttpError::ValidationError(
148 e.field_errors()
149 .into_iter()
150 .map(|(field, errors)| {
151 (
152 field.to_string(),
153 errors
154 .iter()
155 .map(|e| e.message.clone().unwrap_or_default().to_string())
156 .collect(),
157 )
158 })
159 .collect(),
160 )
161 })?;
162
163 Err(HttpError::InternalServerError(
165 "Skill update not yet implemented".to_string(),
166 ))
167}
168
169pub async fn delete_skill(
171 State(state): State<AppState>,
172 Path(skill_id): Path<String>,
173) -> HttpResult<axum::Json<ApiResponse<serde_json::Value>>> {
174 let skill_id_parsed = crate::core::service::SkillId::new(skill_id.clone())
175 .map_err(|_| HttpError::BadRequest("Invalid skill ID format".to_string()))?;
176
177 let skills = state.service.skill_manager().list_skills(None).await?;
178 let skill = skills
179 .into_iter()
180 .find(|s| s.id == skill_id_parsed)
181 .ok_or_else(|| HttpError::NotFound(format!("Skill not found: {}", skill_id)))?;
182
183 let project_path = &state.project_file_path;
184 let lock_path = if let Some(parent) = project_path.parent() {
185 let safe_parent = if parent.exists() {
186 parent.canonicalize().map_err(|e| {
187 HttpError::InternalServerError(format!("Failed to resolve parent path: {}", e))
188 })?
189 } else {
190 parent.to_path_buf()
191 };
192 safe_parent.join("skills.lock")
193 } else {
194 std::path::PathBuf::from("skills.lock")
195 };
196
197 if project_path.exists() {
198 let mut project = crate::core::manifest::SkillProjectToml::load_from_file(project_path)
199 .map_err(|e| {
200 HttpError::InternalServerError(format!("Failed to load project: {}", e))
201 })?;
202 if let Some(ref mut deps) = project.dependencies {
203 deps.dependencies.remove(&skill_id);
204 }
205 project.save_to_file(project_path).map_err(|e| {
206 HttpError::InternalServerError(format!("Failed to save project: {}", e))
207 })?;
208 if lock_path.exists() {
209 let mut lock =
210 crate::core::lock::SkillsLock::load_from_file(&lock_path).map_err(|e| {
211 HttpError::InternalServerError(format!("Failed to load lock: {}", e))
212 })?;
213 lock.remove_skill(&skill_id);
214 lock.save_to_file(&lock_path).map_err(|e| {
215 HttpError::InternalServerError(format!("Failed to save lock: {}", e))
216 })?;
217 }
218 }
219
220 let skill_dir = skill.skill_file.parent().ok_or_else(|| {
221 HttpError::InternalServerError("Skill file has no parent dir".to_string())
222 })?;
223 if skill_dir.exists() {
224 tokio::fs::remove_dir_all(skill_dir).await.map_err(|e| {
225 HttpError::InternalServerError(format!("Failed to remove skill dir: {}", e))
226 })?;
227 }
228
229 state
230 .service
231 .skill_manager()
232 .unregister_skill(&skill_id_parsed)
233 .await
234 .map_err(|e| HttpError::InternalServerError(e.to_string()))?;
235
236 Ok(axum::Json(ApiResponse::success(serde_json::json!({
237 "message": "Skill removed"
238 }))))
239}
240
241pub async fn upgrade_skills(
243 State(state): State<AppState>,
244 Json(payload): Json<Option<UpgradeRequest>>,
245) -> HttpResult<axum::Json<ApiResponse<serde_json::Value>>> {
246 let project_path = state.project_file_path.clone();
247 let filter_id = payload
248 .and_then(|p| p.skill_id)
249 .filter(|s| !s.is_empty() && s != "all");
250
251 let output = tokio::task::spawn_blocking(move || {
252 let exe = std::env::current_exe().map_err(|e| {
253 HttpError::InternalServerError(format!("Failed to get executable path: {}", e))
254 })?;
255 let mut cmd = std::process::Command::new(exe);
256 cmd.arg("update");
257 if let Some(ref id) = filter_id {
258 cmd.arg(id);
259 }
260 if let Some(parent) = project_path.parent() {
261 cmd.current_dir(parent);
262 }
263 let output = cmd
264 .output()
265 .map_err(|e| HttpError::InternalServerError(format!("Failed to run update: {}", e)))?;
266 Ok::<_, HttpError>(output)
267 })
268 .await
269 .map_err(|e| HttpError::InternalServerError(format!("Upgrade task failed: {}", e)))??;
270
271 if !output.status.success() {
272 let stderr = String::from_utf8_lossy(&output.stderr);
273 return Err(HttpError::InternalServerError(format!(
274 "Upgrade failed: {}",
275 stderr.trim()
276 )));
277 }
278
279 Ok(axum::Json(ApiResponse::success(serde_json::json!({
280 "message": "Upgrade completed",
281 "upgraded": true
282 }))))
283}