1use crate::actions::execute_action;
2use crate::build::run_build;
3use crate::build_detector;
4use crate::config::AppConfig;
5use crate::error::{AppError, AppResult};
6use crate::models::*;
7use crate::security::safe_join;
8use axum::Json;
9use axum::extract::{Query, State};
10use std::sync::Arc;
11use tokio::fs;
12use tokio::process::Command;
13use tracing::{debug, info, warn};
14use uuid::Uuid;
15
16pub async fn ping() -> &'static str {
17 "pong"
18}
19pub async fn open_editor(Json(req): Json<OpenEditorRequest>) -> AppResult<Json<serde_json::Value>> {
20 info!("Opening editor '{}' for path: {}", req.editor, req.path);
21
22 let mut arg = req.path;
23 if let Some(line) = req.line {
24 arg.push_str(&format!(":{}", line));
25 if let Some(col) = req.column {
26 arg.push_str(&format!(":{}", col));
27 }
28 }
29
30 tokio::task::spawn_blocking(move || {
31 Command::new(&req.editor)
32 .arg(&arg)
33 .spawn()
34 .map_err(|e| AppError::Internal(format!("Failed to launch editor: {}", e)))
35 })
36 .await
37 .map_err(|e| AppError::Internal(format!("Task error: {}", e)))??;
38
39 Ok(Json(serde_json::json!({ "success": true })))
40}
41pub async fn execute(
42 State(config): State<Arc<AppConfig>>,
43 Json(req): Json<ExecuteRequest>,
44) -> AppResult<Json<ExecuteResponse>> {
45 let request_id = Uuid::new_v4();
46 info!(
47 request_id = %request_id,
48 project_id = %req.project_id,
49 action_count = req.actions.len(),
50 "Execute request received"
51 );
52
53 let project_root = config.workspace.join(&req.project_id);
54 if !project_root.exists() {
55 warn!(
56 request_id = %request_id,
57 project_id = %req.project_id,
58 "Project directory does not exist"
59 );
60 return Err(AppError::BadRequest(format!(
61 "Project '{}' does not exist",
62 req.project_id
63 )));
64 }
65
66 let mut action_results = Vec::new();
68 let mut any_failed = false;
69 for (idx, action) in req.actions.into_iter().enumerate() {
70 debug!(
71 request_id = %request_id,
72 action_index = idx,
73 action_type = ?action,
74 "Executing action"
75 );
76 match execute_action(action, &project_root, &config).await {
78 Ok(result) => {
79 if result.success {
80 info!(
81 request_id = %request_id,
82 action_index = idx,
83 "Action succeeded"
84 );
85 } else {
86 warn!(
87 request_id = %request_id,
88 action_index = idx,
89 error = ?result.error,
90 "Action failed"
91 );
92 any_failed = true;
93 }
94 action_results.push(result);
95 }
96 Err(e) => {
97 warn!(
98 request_id = %request_id,
99 action_index = idx,
100 error = ?e,
101 "Action execution error"
102 );
103 any_failed = true;
104 action_results.push(ActionResult {
105 success: false,
106 path: None,
107 content: None,
108 error: Some(e.to_string()),
109 });
110 }
111 }
112 }
113
114 let (build_output, continue_loop) = if any_failed {
116 info!(
117 request_id = %request_id,
118 "Skipping build due to action failure"
119 );
120 (None, false)
121 } else {
122 let build_cmd = build_detector::get_build_command(&project_root, &config.build_command);
124 if let Some(cmd) = build_cmd {
125 info!(
126 request_id = %request_id,
127 build_command = %cmd,
128 "Running build"
129 );
130 let build_output = run_build(&project_root, &cmd).await;
131 let continue_loop = !build_output.success;
132 (Some(build_output), continue_loop)
133 } else {
134 info!(
135 request_id = %request_id,
136 "No build command needed for this project"
137 );
138 (None, false)
139 }
140 };
141
142 let response = ExecuteResponse {
143 action_results,
144 build_output,
145 continue_loop,
146 };
147
148 info!(
149 request_id = %request_id,
150 continue_loop = continue_loop,
151 "Sending response"
152 );
153
154 Ok(Json(response))
155}
156
157pub async fn list_worktrees(
159 State(config): State<Arc<AppConfig>>,
160 Query(query): Query<ListFilesQuery>,
161) -> AppResult<Json<Vec<WorktreeInfo>>> {
162 info!("Listing worktrees for project: {}", query.project_id);
163
164 let project_root = config.workspace.join(&query.project_id);
165 if !project_root.exists() {
166 return Err(AppError::NotFound(format!(
167 "Project '{}' not found",
168 query.project_id
169 )));
170 }
171
172 let action = Action::GitWorktreeList;
174 match execute_action(action, &project_root, &config).await {
175 Ok(result) => {
176 if result.success {
177 if let Some(content) = result.content {
178 let worktrees: Vec<WorktreeInfo> =
179 serde_json::from_str(&content).map_err(|e| {
180 AppError::Internal(format!("Failed to parse worktrees: {}", e))
181 })?;
182 Ok(Json(worktrees))
183 } else {
184 Ok(Json(Vec::new()))
185 }
186 } else {
187 Err(AppError::Internal(
188 result
189 .error
190 .unwrap_or_else(|| "Failed to list worktrees".to_string()),
191 ))
192 }
193 }
194 Err(e) => Err(e),
195 }
196}
197pub async fn list_projects(State(config): State<Arc<AppConfig>>) -> AppResult<Json<Vec<String>>> {
199 info!("Listing projects in workspace: {:?}", config.workspace);
200 let mut projects = Vec::new();
201 let mut read_dir = fs::read_dir(&config.workspace)
202 .await
203 .map_err(|e| AppError::Internal(format!("Failed to read workspace: {}", e)))?;
204
205 while let Ok(Some(entry)) = read_dir.next_entry().await {
206 if entry
207 .file_type()
208 .await
209 .map(|ft| ft.is_dir())
210 .unwrap_or(false)
211 && let Ok(name) = entry.file_name().into_string()
212 {
213 projects.push(name);
214 }
215 }
216 info!("Found {} projects", projects.len());
217 Ok(Json(projects))
218}
219
220pub async fn list_files(
222 State(config): State<Arc<AppConfig>>,
223 Query(query): Query<ListFilesQuery>,
224) -> AppResult<Json<Vec<FileEntry>>> {
225 info!(
226 "Listing files for project: {} in workspace: {:?}",
227 query.project_id, config.workspace
228 );
229 let project_path = config.workspace.join(&query.project_id);
230 if !project_path.exists() {
231 warn!("Project '{}' not found", query.project_id);
232 return Err(AppError::NotFound(format!(
233 "Project '{}' not found",
234 query.project_id
235 )));
236 }
237
238 let mut entries = Vec::new();
239
240 let walker = ignore::WalkBuilder::new(&project_path)
242 .hidden(true) .git_ignore(true) .build();
245
246 for result in walker {
247 let entry = match result {
248 Ok(e) => e,
249 Err(e) => {
250 warn!("Failed to read entry: {}", e);
251 continue; }
253 };
254
255 if entry.path() == project_path {
257 continue;
258 }
259
260 let metadata = match entry.metadata() {
261 Ok(m) => m,
262 Err(e) => {
263 warn!("Failed to get metadata for {:?}: {}", entry.path(), e);
264 continue;
265 }
266 };
267
268 let relative_path = match entry.path().strip_prefix(&project_path) {
269 Ok(p) => p,
270 Err(e) => {
271 warn!("Failed to strip prefix for {:?}: {}", entry.path(), e);
272 continue;
273 }
274 };
275 let path_str = relative_path.to_string_lossy().to_string();
276 let name = entry.file_name().to_string_lossy().to_string();
277
278 entries.push(FileEntry {
279 name,
280 path: path_str,
281 is_dir: metadata.is_dir(),
282 size: if metadata.is_file() {
283 Some(metadata.len())
284 } else {
285 None
286 },
287 });
288 }
289
290 info!(
291 "Found {} files in project {}",
292 entries.len(),
293 query.project_id
294 );
295 Ok(Json(entries))
296}
297pub async fn file_content(
299 State(config): State<Arc<AppConfig>>,
300 Query(query): Query<FileContentQuery>,
301) -> AppResult<String> {
302 info!(
303 "Getting file content for project: {}, path: {}",
304 query.project_id, query.path
305 );
306 let project_path = config.workspace.join(&query.project_id);
307 let full_path = safe_join(&project_path, &query.path)?;
308
309 if !full_path.exists() {
310 warn!("File not found: {}", query.path);
311 return Err(AppError::NotFound(format!(
312 "File not found: {}",
313 query.path
314 )));
315 }
316 if full_path.is_dir() {
317 warn!("Path is a directory: {}", query.path);
318 return Err(AppError::BadRequest("Path is a directory".into()));
319 }
320
321 let content = fs::read_to_string(&full_path)
322 .await
323 .map_err(|e| AppError::Internal(format!("Failed to read file: {}", e)))?;
324 info!("Successfully read file: {}", query.path);
325 Ok(content)
326}