2025-06-20 23:20:38 +01:00
|
|
|
use async_trait::async_trait;
|
2025-06-27 16:18:35 +01:00
|
|
|
use command_group::{AsyncCommandGroup, AsyncGroupChild};
|
|
|
|
|
use tokio::process::Command;
|
2025-06-20 23:20:38 +01:00
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
2025-06-25 09:27:29 +01:00
|
|
|
use crate::{
|
|
|
|
|
executor::{Executor, ExecutorError},
|
|
|
|
|
models::{project::Project, task::Task},
|
2025-06-27 16:18:35 +01:00
|
|
|
utils::shell::get_shell_command,
|
2025-06-25 09:27:29 +01:00
|
|
|
};
|
2025-06-20 23:20:38 +01:00
|
|
|
|
|
|
|
|
/// Executor for running project setup scripts
|
|
|
|
|
pub struct SetupScriptExecutor {
|
|
|
|
|
pub script: String,
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-14 16:15:29 +01:00
|
|
|
impl SetupScriptExecutor {
|
|
|
|
|
pub fn new(script: String) -> Self {
|
|
|
|
|
Self { script }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-20 23:20:38 +01:00
|
|
|
#[async_trait]
|
|
|
|
|
impl Executor for SetupScriptExecutor {
|
|
|
|
|
async fn spawn(
|
|
|
|
|
&self,
|
|
|
|
|
pool: &sqlx::SqlitePool,
|
|
|
|
|
task_id: Uuid,
|
|
|
|
|
worktree_path: &str,
|
2025-06-27 16:18:35 +01:00
|
|
|
) -> Result<AsyncGroupChild, ExecutorError> {
|
2025-06-20 23:20:38 +01:00
|
|
|
// Validate the task and project exist
|
|
|
|
|
let task = Task::find_by_id(pool, task_id)
|
|
|
|
|
.await?
|
|
|
|
|
.ok_or(ExecutorError::TaskNotFound)?;
|
|
|
|
|
|
|
|
|
|
let _project = Project::find_by_id(pool, task.project_id)
|
|
|
|
|
.await?
|
|
|
|
|
.ok_or(ExecutorError::TaskNotFound)?; // Reuse TaskNotFound for simplicity
|
|
|
|
|
|
2025-06-27 16:18:35 +01:00
|
|
|
let (shell_cmd, shell_arg) = get_shell_command();
|
|
|
|
|
let mut command = Command::new(shell_cmd);
|
|
|
|
|
command
|
2025-06-20 23:20:38 +01:00
|
|
|
.kill_on_drop(true)
|
|
|
|
|
.stdout(std::process::Stdio::piped())
|
|
|
|
|
.stderr(std::process::Stdio::piped())
|
2025-06-27 16:18:35 +01:00
|
|
|
.arg(shell_arg)
|
2025-06-20 23:20:38 +01:00
|
|
|
.arg(&self.script)
|
2025-06-27 16:18:35 +01:00
|
|
|
.current_dir(worktree_path);
|
|
|
|
|
|
|
|
|
|
let child = command.group_spawn().map_err(|e| {
|
|
|
|
|
crate::executor::SpawnContext::from_command(&command, "SetupScript")
|
|
|
|
|
.with_task(task_id, Some(task.title.clone()))
|
|
|
|
|
.with_context("Setup script execution")
|
|
|
|
|
.spawn_error(e)
|
|
|
|
|
})?;
|
2025-06-20 23:20:38 +01:00
|
|
|
|
|
|
|
|
Ok(child)
|
|
|
|
|
}
|
2025-07-14 16:15:29 +01:00
|
|
|
|
|
|
|
|
/// Normalize setup script logs into a readable format
|
|
|
|
|
fn normalize_logs(
|
|
|
|
|
&self,
|
|
|
|
|
logs: &str,
|
|
|
|
|
_worktree_path: &str,
|
|
|
|
|
) -> Result<crate::executor::NormalizedConversation, String> {
|
|
|
|
|
let mut entries = Vec::new();
|
|
|
|
|
|
|
|
|
|
// Add script command as first entry
|
|
|
|
|
entries.push(crate::executor::NormalizedEntry {
|
|
|
|
|
timestamp: None,
|
|
|
|
|
entry_type: crate::executor::NormalizedEntryType::SystemMessage,
|
|
|
|
|
content: format!("Executing setup script:\n{}", self.script),
|
|
|
|
|
metadata: None,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Process the logs - split by lines and create entries
|
|
|
|
|
if !logs.trim().is_empty() {
|
|
|
|
|
let lines: Vec<&str> = logs.lines().collect();
|
|
|
|
|
let mut current_chunk = String::new();
|
|
|
|
|
|
|
|
|
|
for line in lines {
|
|
|
|
|
current_chunk.push_str(line);
|
|
|
|
|
current_chunk.push('\n');
|
|
|
|
|
|
|
|
|
|
// Create entry for every 10 lines or when we encounter an error-like line
|
|
|
|
|
if current_chunk.lines().count() >= 10
|
|
|
|
|
|| line.to_lowercase().contains("error")
|
|
|
|
|
|| line.to_lowercase().contains("failed")
|
|
|
|
|
|| line.to_lowercase().contains("exception")
|
|
|
|
|
{
|
|
|
|
|
let entry_type = if line.to_lowercase().contains("error")
|
|
|
|
|
|| line.to_lowercase().contains("failed")
|
|
|
|
|
|| line.to_lowercase().contains("exception")
|
|
|
|
|
{
|
|
|
|
|
crate::executor::NormalizedEntryType::ErrorMessage
|
|
|
|
|
} else {
|
|
|
|
|
crate::executor::NormalizedEntryType::SystemMessage
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
entries.push(crate::executor::NormalizedEntry {
|
|
|
|
|
timestamp: Some(chrono::Utc::now().to_rfc3339()),
|
|
|
|
|
entry_type,
|
|
|
|
|
content: current_chunk.trim().to_string(),
|
|
|
|
|
metadata: None,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
current_chunk.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add any remaining content
|
|
|
|
|
if !current_chunk.trim().is_empty() {
|
|
|
|
|
entries.push(crate::executor::NormalizedEntry {
|
|
|
|
|
timestamp: Some(chrono::Utc::now().to_rfc3339()),
|
|
|
|
|
entry_type: crate::executor::NormalizedEntryType::SystemMessage,
|
|
|
|
|
content: current_chunk.trim().to_string(),
|
|
|
|
|
metadata: None,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(crate::executor::NormalizedConversation {
|
|
|
|
|
entries,
|
|
|
|
|
session_id: None,
|
2025-07-16 16:33:27 +01:00
|
|
|
executor_type: "setup-script".to_string(),
|
2025-07-14 16:15:29 +01:00
|
|
|
prompt: Some(self.script.clone()),
|
|
|
|
|
summary: None,
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-06-20 23:20:38 +01:00
|
|
|
}
|