spool 1.2.0

Git-native, event-sourced task management
Documentation
use anyhow::{anyhow, Context, Result};
use std::fs::{self, File};
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};

use crate::event::Event;
use crate::migration;

pub struct SpoolContext {
    pub root: PathBuf,
    pub events_dir: PathBuf,
    pub archive_dir: PathBuf,
}

impl SpoolContext {
    /// Create a new SpoolContext with the given root directory
    pub fn new(root: PathBuf) -> Self {
        Self {
            events_dir: root.join("events"),
            archive_dir: root.join("archive"),
            root,
        }
    }

    pub fn discover() -> Result<Self> {
        let mut current = std::env::current_dir()?;
        loop {
            let spool_dir = current.join(".spool");
            if spool_dir.is_dir() {
                let ctx = Self::new(spool_dir);
                // Check and run any needed migrations
                migration::check_and_migrate(&ctx)?;
                return Ok(ctx);
            }
            if !current.pop() {
                return Err(anyhow!(
                    "Not in a spool directory. Run 'spool init' to create one."
                ));
            }
        }
    }

    pub fn index_path(&self) -> PathBuf {
        self.root.join(".index.json")
    }

    pub fn state_path(&self) -> PathBuf {
        self.root.join(".state.json")
    }

    pub fn get_event_files(&self) -> Result<Vec<PathBuf>> {
        let mut files = Vec::new();
        if self.events_dir.is_dir() {
            for entry in fs::read_dir(&self.events_dir)? {
                let entry = entry?;
                let path = entry.path();
                if path.extension().is_some_and(|ext| ext == "jsonl") {
                    files.push(path);
                }
            }
        }
        files.sort();
        Ok(files)
    }

    pub fn get_archive_files(&self) -> Result<Vec<PathBuf>> {
        let mut files = Vec::new();
        if self.archive_dir.is_dir() {
            for entry in fs::read_dir(&self.archive_dir)? {
                let entry = entry?;
                let path = entry.path();
                if path.extension().is_some_and(|ext| ext == "jsonl") {
                    files.push(path);
                }
            }
        }
        files.sort();
        Ok(files)
    }

    pub fn parse_events_from_file(&self, path: &Path) -> Result<Vec<Event>> {
        let file = File::open(path).with_context(|| format!("Failed to open {:?}", path))?;
        let reader = BufReader::new(file);
        let mut events = Vec::new();
        for (line_num, line) in reader.lines().enumerate() {
            let line = line?;
            if line.trim().is_empty() {
                continue;
            }
            let event: Event = serde_json::from_str(&line)
                .with_context(|| format!("Failed to parse line {} in {:?}", line_num + 1, path))?;
            events.push(event);
        }
        Ok(events)
    }
}

pub fn init() -> Result<()> {
    let spool_dir = PathBuf::from(".spool");

    if spool_dir.exists() {
        return Err(anyhow!(".spool directory already exists"));
    }

    fs::create_dir_all(spool_dir.join("events"))?;
    fs::create_dir_all(spool_dir.join("archive"))?;

    let gitignore = r#"# Derived files - rebuilt from events on checkout/merge
# These are caches for fast queries, not source of truth

# Task index: maps task_id -> status, date range, file locations
.index.json

# Materialized state: current snapshot of all tasks
.state.json

# Any temporary files from tooling
*.tmp
*.bak
"#;
    fs::write(spool_dir.join(".gitignore"), gitignore)?;

    // Write initial version file
    let version = migration::VersionInfo::default();
    let version_json = serde_json::to_string_pretty(&version)?;
    fs::write(spool_dir.join("version.json"), version_json)?;

    println!("Created .spool/");
    println!("  .spool/events/     - Daily event logs");
    println!("  .spool/archive/    - Monthly rollups");
    println!("  .spool/.gitignore  - Ignores derived files");
    println!("  .spool/version.json - Format version tracking");

    Ok(())
}