1use axum::{
10 Json,
11 extract::{Path, State},
12 http::{HeaderMap, StatusCode},
13 response::{IntoResponse, Response},
14};
15use serde::Deserialize;
16use zeroclaw_runtime::rpc::types::{
17 SkillBundleEntry, SkillListEntry, SkillsBundlesResult, SkillsListResult, SkillsReadResult,
18};
19use zeroclaw_runtime::skills::{
20 RemoveMode, ScaffoldOptions, ServiceError, SkillFrontmatter, SkillsService,
21};
22
23use super::AppState;
24use super::api::require_auth;
25
26#[derive(Debug, Deserialize)]
29pub struct SkillCreateBody {
30 pub name: String,
31 pub frontmatter: SkillFrontmatter,
32 #[serde(default)]
35 pub body: String,
36 #[serde(default)]
39 pub no_scaffold: bool,
40}
41
42#[derive(Debug, Deserialize)]
43pub struct SkillWriteBody {
44 pub frontmatter: SkillFrontmatter,
45 #[serde(default)]
46 pub body: String,
47}
48
49#[derive(Debug, Deserialize, Default)]
50pub struct DeleteQuery {
51 #[serde(default)]
54 pub purge: bool,
55}
56
57pub async fn handle_list_bundles(State(state): State<AppState>, headers: HeaderMap) -> Response {
61 if let Err(e) = require_auth(&state, &headers) {
62 return e.into_response();
63 }
64 let config = state.config.read().clone();
65 let install_root = config.install_root_dir();
66 let service = SkillsService::new(&config, install_root);
67
68 match service.list_bundles() {
69 Ok(bundles) => Json(SkillsBundlesResult {
70 bundles: bundles
71 .into_iter()
72 .map(|b| SkillBundleEntry {
73 alias: b.alias,
74 directory: b.directory.display().to_string(),
75 include: b.include,
76 exclude: b.exclude,
77 })
78 .collect(),
79 })
80 .into_response(),
81 Err(e) => service_error_response(e),
82 }
83}
84
85pub async fn handle_list_skills(
87 State(state): State<AppState>,
88 headers: HeaderMap,
89 Path(alias): Path<String>,
90) -> Response {
91 if let Err(e) = require_auth(&state, &headers) {
92 return e.into_response();
93 }
94 let config = state.config.read().clone();
95 let install_root = config.install_root_dir();
96 let service = SkillsService::new(&config, install_root);
97
98 match service.list_skills(Some(&alias)) {
99 Ok(skills) => Json(SkillsListResult {
100 skills: skills
101 .into_iter()
102 .map(|s| SkillListEntry {
103 bundle: s.r#ref.bundle().to_string(),
104 name: s.r#ref.name().to_string(),
105 directory: s.directory.display().to_string(),
106 frontmatter: s.frontmatter,
107 })
108 .collect(),
109 })
110 .into_response(),
111 Err(e) => service_error_response(e),
112 }
113}
114
115pub async fn handle_create_skill(
117 State(state): State<AppState>,
118 headers: HeaderMap,
119 Path(alias): Path<String>,
120 Json(body): Json<SkillCreateBody>,
121) -> Response {
122 if let Err(e) = require_auth(&state, &headers) {
123 return e.into_response();
124 }
125 let config = state.config.read().clone();
126 let install_root = config.install_root_dir();
127 let service = SkillsService::new(&config, install_root);
128
129 let target = match service.resolve_ref(&body.name, Some(&alias)) {
130 Ok(r) => r,
131 Err(e) => return service_error_response(e),
132 };
133 match service.scaffold_skill(
134 &target,
135 body.frontmatter,
136 ScaffoldOptions {
137 create_optional_subdirs: !body.no_scaffold,
138 body: body.body,
139 },
140 ) {
141 Ok(path) => (
142 StatusCode::CREATED,
143 Json(serde_json::json!({
144 "bundle": target.bundle(),
145 "name": target.name(),
146 "directory": path.display().to_string(),
147 })),
148 )
149 .into_response(),
150 Err(e) => service_error_response(e),
151 }
152}
153
154pub async fn handle_read_skill(
156 State(state): State<AppState>,
157 headers: HeaderMap,
158 Path((alias, name)): Path<(String, String)>,
159) -> Response {
160 if let Err(e) = require_auth(&state, &headers) {
161 return e.into_response();
162 }
163 let config = state.config.read().clone();
164 let install_root = config.install_root_dir();
165 let service = SkillsService::new(&config, install_root);
166
167 let target = match service.resolve_ref(&name, Some(&alias)) {
168 Ok(r) => r,
169 Err(e) => return service_error_response(e),
170 };
171 match service.read_skill(&target) {
172 Ok(doc) => Json(SkillsReadResult {
173 bundle: target.bundle().to_string(),
174 name: target.name().to_string(),
175 frontmatter: doc.frontmatter,
176 body: doc.body,
177 })
178 .into_response(),
179 Err(e) => service_error_response(e),
180 }
181}
182
183pub async fn handle_write_skill(
185 State(state): State<AppState>,
186 headers: HeaderMap,
187 Path((alias, name)): Path<(String, String)>,
188 Json(body): Json<SkillWriteBody>,
189) -> Response {
190 if let Err(e) = require_auth(&state, &headers) {
191 return e.into_response();
192 }
193 let config = state.config.read().clone();
194 let install_root = config.install_root_dir();
195 let service = SkillsService::new(&config, install_root);
196
197 let target = match service.resolve_ref(&name, Some(&alias)) {
198 Ok(r) => r,
199 Err(e) => return service_error_response(e),
200 };
201 let doc = zeroclaw_runtime::skills::SkillDocument {
202 frontmatter: body.frontmatter,
203 body: body.body,
204 };
205 match service.write_skill(&target, &doc) {
206 Ok(()) => StatusCode::NO_CONTENT.into_response(),
207 Err(e) => service_error_response(e),
208 }
209}
210
211pub async fn handle_delete_skill(
213 State(state): State<AppState>,
214 headers: HeaderMap,
215 Path((alias, name)): Path<(String, String)>,
216 axum::extract::Query(q): axum::extract::Query<DeleteQuery>,
217) -> Response {
218 if let Err(e) = require_auth(&state, &headers) {
219 return e.into_response();
220 }
221 let config = state.config.read().clone();
222 let install_root = config.install_root_dir();
223 let service = SkillsService::new(&config, install_root);
224
225 let target = match service.resolve_ref(&name, Some(&alias)) {
226 Ok(r) => r,
227 Err(e) => return service_error_response(e),
228 };
229 let mode = if q.purge {
230 RemoveMode::Purge
231 } else {
232 RemoveMode::Archive
233 };
234 match service.remove_skill(&target, mode) {
235 Ok(()) => StatusCode::NO_CONTENT.into_response(),
236 Err(e) => service_error_response(e),
237 }
238}
239
240fn service_error_response(err: ServiceError) -> Response {
243 let status = match &err {
244 ServiceError::Ref(_) => StatusCode::BAD_REQUEST,
245 ServiceError::Bundle(_) => StatusCode::BAD_REQUEST,
246 ServiceError::Scaffold(_) => StatusCode::BAD_REQUEST,
247 ServiceError::DocumentParse(_) => StatusCode::UNPROCESSABLE_ENTITY,
248 ServiceError::NotFound(_) => StatusCode::NOT_FOUND,
249 ServiceError::Io(_) => StatusCode::INTERNAL_SERVER_ERROR,
250 };
251 (
252 status,
253 Json(serde_json::json!({
254 "error": format!("{}", err),
255 })),
256 )
257 .into_response()
258}