Skip to main content

codebridge_cli/
handlers.rs

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    // Execute actions
67    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        // Pass config to actions (for script execution)
77        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    // Decide whether to run build (smart detection)
115    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        // Determine build command using smart detection
123        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
157// NEW: GET /worktrees?projectId=<id> endpoint
158pub 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    // Execute git worktree list action
173    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}
197/// GET /projects – list all project directories under workspace
198pub 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
220/// GET /list-files?projectId=<id> – recursive file listing for a project, respecting .gitignore
221pub 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    // Use ignore::WalkBuilder to respect .gitignore and skip hidden files
241    let walker = ignore::WalkBuilder::new(&project_path)
242        .hidden(true) // skip hidden files (those starting with '.')
243        .git_ignore(true) // respect .gitignore
244        .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; // skip problematic entries, but continue walking
252            }
253        };
254
255        // Skip the root directory itself
256        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}
297/// GET /file-content?projectId=<id>&path=<path> – return file content as plain text
298pub 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}