complior-cli 0.9.3

AI Act Compliance Scanner & Fixer — CLI
use std::path::PathBuf;

use serde::{Deserialize, Serialize};

use crate::types::{ChatMessage, ScanResult};

#[derive(Debug, Serialize, Deserialize)]
pub struct SessionData {
    pub messages: Vec<ChatMessage>,
    pub score_history: Vec<f64>,
    pub open_file_path: Option<String>,
    pub terminal_output: Vec<String>,
    pub last_scan: Option<ScanResult>,
}

fn sessions_dir() -> PathBuf {
    dirs::data_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join("complior")
        .join("sessions")
}

pub async fn save_session(data: &SessionData, name: &str) -> Result<(), String> {
    let dir = sessions_dir();
    tokio::fs::create_dir_all(&dir)
        .await
        .map_err(|e| format!("mkdir: {e}"))?;

    let path = dir.join(format!("{name}.json"));
    let json = serde_json::to_string_pretty(data).map_err(|e| format!("serialize: {e}"))?;
    tokio::fs::write(&path, json)
        .await
        .map_err(|e| format!("write: {e}"))?;
    Ok(())
}

pub async fn load_session(name: &str) -> Result<SessionData, String> {
    let path = sessions_dir().join(format!("{name}.json"));
    let content = tokio::fs::read_to_string(&path)
        .await
        .map_err(|e| format!("read: {e}"))?;
    serde_json::from_str(&content).map_err(|e| format!("parse: {e}"))
}

pub async fn list_sessions() -> Vec<String> {
    let dir = sessions_dir();
    let Ok(mut entries) = tokio::fs::read_dir(&dir).await else {
        return Vec::new();
    };

    let mut names = Vec::new();
    while let Ok(Some(entry)) = entries.next_entry().await {
        let path = entry.path();
        if path.extension().is_some_and(|ext| ext == "json")
            && let Some(stem) = path.file_stem() {
                names.push(stem.to_string_lossy().to_string());
            }
    }

    names.sort();
    names
}

pub async fn mark_first_run_done() {
    let dir = sessions_dir();
    let _ = tokio::fs::create_dir_all(&dir).await;
    let marker = dir.join(".first_run_done");
    let _ = tokio::fs::write(marker, "done").await;
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::MessageRole;

    #[test]
    fn test_session_roundtrip() {
        let data = SessionData {
            messages: vec![ChatMessage::new(
                MessageRole::System,
                "test".to_string(),
            )],
            score_history: vec![42.0, 65.0],
            open_file_path: Some("src/main.rs".to_string()),
            terminal_output: vec!["$ ls".to_string()],
            last_scan: None,
        };

        let json = serde_json::to_string(&data).expect("serialize");
        let loaded: SessionData = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(loaded.messages.len(), 1);
        assert_eq!(loaded.score_history.len(), 2);
    }
}