Files
vibe-kanban/backend/src/executors/setup_script.rs

131 lines
4.4 KiB
Rust
Raw Normal View History

2025-06-20 23:20:38 +01:00
use async_trait::async_trait;
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},
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,
}
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,
) -> 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
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())
.arg(shell_arg)
2025-06-20 23:20:38 +01:00
.arg(&self.script)
.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)
}
/// 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,
executor_type: "setup-script".to_string(),
prompt: Some(self.script.clone()),
summary: None,
})
}
2025-06-20 23:20:38 +01:00
}