use crate::http::errors::{HttpError, HttpResult};
use crate::http::handlers::AppState;
use crate::http::models::*;
use axum::{
extract::{Path, State},
Json,
};
use validator::Validate;
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpgradeRequest {
skill_id: Option<String>,
}
fn skill_metadata_json(skill: &crate::core::skill_manager::SkillDefinition) -> serde_json::Value {
let source_type = skill
.source_type
.as_ref()
.map(|t| serde_json::to_value(t).unwrap_or(serde_json::Value::Null));
serde_json::json!({
"id": skill.id,
"name": skill.name,
"description": skill.description,
"version": skill.version,
"author": skill.author,
"enabled": skill.enabled,
"created_at": skill.created_at.to_rfc3339(),
"updated_at": skill.updated_at.to_rfc3339(),
"skill_file": skill.skill_file,
"reference_files": skill.reference_files,
"script_files": skill.script_files,
"asset_files": skill.asset_files,
"source_url": skill.source_url,
"source_type": source_type
})
}
pub async fn list_skills(
State(state): State<AppState>,
) -> HttpResult<axum::Json<ApiResponse<SkillsListResponse>>> {
let skills = state.service.skill_manager().list_skills(None).await?;
let skill_responses: Vec<SkillResponse> = skills
.clone()
.into_iter()
.map(|skill| SkillResponse {
id: skill.id.to_string(),
name: skill.name.clone(),
description: skill.description.clone(),
metadata: skill_metadata_json(&skill),
created_at: Some(skill.created_at.to_rfc3339()),
updated_at: Some(skill.updated_at.to_rfc3339()),
})
.collect();
let response = SkillsListResponse {
skills: skill_responses,
count: skills.len(),
total: skills.len(),
};
Ok(axum::Json(ApiResponse::success(response)))
}
pub async fn get_skill(
State(state): State<AppState>,
Path(skill_id): Path<String>,
) -> HttpResult<axum::Json<ApiResponse<SkillResponse>>> {
let skills = state.service.skill_manager().list_skills(None).await?;
let skill_id_parsed = crate::core::service::SkillId::new(skill_id.clone())
.map_err(|_| HttpError::BadRequest("Invalid skill ID format".to_string()))?;
let skill = skills
.into_iter()
.find(|s| s.id == skill_id_parsed)
.ok_or_else(|| HttpError::NotFound(format!("Skill not found: {}", skill_id)))?;
let metadata = skill_metadata_json(&skill);
let response = SkillResponse {
id: skill.id.to_string(),
name: skill.name.clone(),
description: skill.description.clone(),
metadata,
created_at: Some(skill.created_at.to_rfc3339()),
updated_at: Some(skill.updated_at.to_rfc3339()),
};
Ok(axum::Json(ApiResponse::success(response)))
}
pub async fn create_skill(
State(_state): State<AppState>,
Json(_request): Json<SkillRequest>,
) -> HttpResult<axum::Json<ApiResponse<SkillResponse>>> {
_request.validate().map_err(|e| {
HttpError::ValidationError(
e.field_errors()
.into_iter()
.map(|(field, errors)| {
(
field.to_string(),
errors
.iter()
.map(|e| e.message.clone().unwrap_or_default().to_string())
.collect(),
)
})
.collect(),
)
})?;
let _skill_def = serde_json::json!({
"id": format!("skill_{}", chrono::Utc::now().timestamp()),
"name": _request.name,
"description": _request.description,
});
Err(HttpError::InternalServerError(
"Skill creation not yet implemented".to_string(),
))
}
pub async fn update_skill(
State(_state): State<AppState>,
Path(_skill_id): Path<String>,
Json(request): Json<SkillRequest>,
) -> HttpResult<axum::Json<ApiResponse<SkillResponse>>> {
request.validate().map_err(|e| {
HttpError::ValidationError(
e.field_errors()
.into_iter()
.map(|(field, errors)| {
(
field.to_string(),
errors
.iter()
.map(|e| e.message.clone().unwrap_or_default().to_string())
.collect(),
)
})
.collect(),
)
})?;
Err(HttpError::InternalServerError(
"Skill update not yet implemented".to_string(),
))
}
pub async fn delete_skill(
State(state): State<AppState>,
Path(skill_id): Path<String>,
) -> HttpResult<axum::Json<ApiResponse<serde_json::Value>>> {
let skill_id_parsed = crate::core::service::SkillId::new(skill_id.clone())
.map_err(|_| HttpError::BadRequest("Invalid skill ID format".to_string()))?;
let skills = state.service.skill_manager().list_skills(None).await?;
let skill = skills
.into_iter()
.find(|s| s.id == skill_id_parsed)
.ok_or_else(|| HttpError::NotFound(format!("Skill not found: {}", skill_id)))?;
let project_path = &state.project_file_path;
let lock_path = if let Some(parent) = project_path.parent() {
let safe_parent = if parent.exists() {
parent.canonicalize().map_err(|e| {
HttpError::InternalServerError(format!("Failed to resolve parent path: {}", e))
})?
} else {
parent.to_path_buf()
};
safe_parent.join("skills.lock")
} else {
std::path::PathBuf::from("skills.lock")
};
if project_path.exists() {
let mut project = crate::core::manifest::SkillProjectToml::load_from_file(project_path)
.map_err(|e| {
HttpError::InternalServerError(format!("Failed to load project: {}", e))
})?;
if let Some(ref mut deps) = project.dependencies {
deps.dependencies.remove(&skill_id);
}
project.save_to_file(project_path).map_err(|e| {
HttpError::InternalServerError(format!("Failed to save project: {}", e))
})?;
if lock_path.exists() {
let mut lock =
crate::core::lock::SkillsLock::load_from_file(&lock_path).map_err(|e| {
HttpError::InternalServerError(format!("Failed to load lock: {}", e))
})?;
lock.remove_skill(&skill_id);
lock.save_to_file(&lock_path).map_err(|e| {
HttpError::InternalServerError(format!("Failed to save lock: {}", e))
})?;
}
}
let skill_dir = skill.skill_file.parent().ok_or_else(|| {
HttpError::InternalServerError("Skill file has no parent dir".to_string())
})?;
if skill_dir.exists() {
tokio::fs::remove_dir_all(skill_dir).await.map_err(|e| {
HttpError::InternalServerError(format!("Failed to remove skill dir: {}", e))
})?;
}
state
.service
.skill_manager()
.unregister_skill(&skill_id_parsed)
.await
.map_err(|e| HttpError::InternalServerError(e.to_string()))?;
Ok(axum::Json(ApiResponse::success(serde_json::json!({
"message": "Skill removed"
}))))
}
pub async fn upgrade_skills(
State(state): State<AppState>,
Json(payload): Json<Option<UpgradeRequest>>,
) -> HttpResult<axum::Json<ApiResponse<serde_json::Value>>> {
let project_path = state.project_file_path.clone();
let filter_id = payload
.and_then(|p| p.skill_id)
.filter(|s| !s.is_empty() && s != "all");
let output = tokio::task::spawn_blocking(move || {
let exe = std::env::current_exe().map_err(|e| {
HttpError::InternalServerError(format!("Failed to get executable path: {}", e))
})?;
let mut cmd = std::process::Command::new(exe);
cmd.arg("update");
if let Some(ref id) = filter_id {
cmd.arg(id);
}
if let Some(parent) = project_path.parent() {
cmd.current_dir(parent);
}
let output = cmd
.output()
.map_err(|e| HttpError::InternalServerError(format!("Failed to run update: {}", e)))?;
Ok::<_, HttpError>(output)
})
.await
.map_err(|e| HttpError::InternalServerError(format!("Upgrade task failed: {}", e)))??;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(HttpError::InternalServerError(format!(
"Upgrade failed: {}",
stderr.trim()
)));
}
Ok(axum::Json(ApiResponse::success(serde_json::json!({
"message": "Upgrade completed",
"upgraded": true
}))))
}