gigi-cli 1.0.0

Gigi — A Claude Code-like AI coding assistant CLI in Rust
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
use tokio::process::Child;
use anyhow::{Result, Context};
use chrono::{DateTime, Utc};

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
pub enum TaskStatus {
    Running,
    Finished { exit_code: i32 },
    Failed { error: String },
    Killed,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TaskInfo {
    pub id: String,
    pub command: String,
    pub status: TaskStatus,
    pub log_path: String,
    pub started_at: DateTime<Utc>,
}

pub struct BackgroundTask {
    pub info: TaskInfo,
    pub child: Arc<Mutex<Option<Child>>>,
}

pub struct TaskManager {
    tasks: Mutex<HashMap<String, BackgroundTask>>,
}

impl TaskManager {
    pub fn global() -> &'static Self {
        static INSTANCE: OnceLock<TaskManager> = OnceLock::new();
        INSTANCE.get_or_init(|| Self {
            tasks: Mutex::new(HashMap::new()),
        })
    }

    pub fn spawn_task(&self, command: String, session_dir: &Path) -> Result<String> {
        let task_id = format!("task_{}", &uuid::Uuid::new_v4().to_string()[..8]);
        
        let logs_dir = session_dir.join("logs");
        std::fs::create_dir_all(&logs_dir)?;
        let log_path = logs_dir.join(format!("{}.log", task_id));

        // Create log file
        let log_file = std::fs::File::create(&log_path)?;

        let (shell, flag) = if cfg!(target_os = "windows") {
            ("cmd", "/C")
        } else {
            ("sh", "-c")
        };

        // Spawn child and redirect stdout/stderr to the log file
        let child = tokio::process::Command::new(shell)
            .arg(flag)
            .arg(&command)
            .stdin(std::process::Stdio::null())
            .stdout(log_file.try_clone()?)
            .stderr(log_file)
            .spawn()
            .context("Failed to spawn background task")?;

        let info = TaskInfo {
            id: task_id.clone(),
            command: command.clone(),
            status: TaskStatus::Running,
            log_path: log_path.to_string_lossy().to_string(),
            started_at: Utc::now(),
        };

        let mut tasks = self.tasks.lock().unwrap();
        tasks.insert(task_id.clone(), BackgroundTask {
            info,
            child: Arc::new(Mutex::new(Some(child))),
        });

        Ok(task_id)
    }

    /// Refresh status of all running tasks by calling try_wait()
    pub fn refresh_tasks(&self) {
        let mut tasks = self.tasks.lock().unwrap();
        for task in tasks.values_mut() {
            if let TaskStatus::Running = task.info.status {
                let mut child_guard = task.child.lock().unwrap();
                if let Some(child) = child_guard.as_mut() {
                    match child.try_wait() {
                        Ok(Some(status)) => {
                            task.info.status = TaskStatus::Finished {
                                exit_code: status.code().unwrap_or(0),
                            };
                            *child_guard = None;
                        }
                        Ok(None) => {
                            // Still running
                        }
                        Err(e) => {
                            task.info.status = TaskStatus::Failed {
                                error: e.to_string(),
                            };
                            *child_guard = None;
                        }
                    }
                } else {
                    // Child exited or failed already but status was not updated
                    task.info.status = TaskStatus::Finished { exit_code: 0 };
                }
            }
        }
    }

    pub fn list_tasks(&self) -> Vec<TaskInfo> {
        self.refresh_tasks();
        let tasks = self.tasks.lock().unwrap();
        let mut list: Vec<TaskInfo> = tasks.values().map(|t| t.info.clone()).collect();
        list.sort_by_key(|t| t.started_at);
        list
    }

    pub fn kill_task(&self, task_id: &str) -> Result<bool> {
        self.refresh_tasks();
        let mut tasks = self.tasks.lock().unwrap();
        if let Some(task) = tasks.get_mut(task_id) {
            if let TaskStatus::Running = task.info.status {
                let mut child_guard = task.child.lock().unwrap();
                if let Some(mut child) = child_guard.take() {
                    let _ = child.start_kill();
                    task.info.status = TaskStatus::Killed;
                    return Ok(true);
                }
            }
        }
        Ok(false)
    }
}