lazyspec 0.8.0

A little TUI & CLI for project documentation.
Documentation
use anyhow::{bail, Result};
use std::path::Path;
use std::process::Command;

pub fn resolve_agent_id(root: &Path) -> Result<String> {
    resolve_agent_id_with_env(
        root,
        std::env::var("LAZYSPEC_AGENT_ID").ok(),
        std::env::var("CLAUDE_SESSION_ID").ok(),
    )
}

pub fn resolve_agent_id_with_env(
    root: &Path,
    lazyspec_agent_id: Option<String>,
    claude_session_id: Option<String>,
) -> Result<String> {
    if let Some(id) = lazyspec_agent_id.filter(|s| !s.is_empty()) {
        return Ok(id);
    }

    if let Some(id) = claude_session_id.filter(|s| !s.is_empty()) {
        return Ok(id);
    }

    let output = Command::new("git")
        .args(["config", "user.name"])
        .current_dir(root)
        .output()?;

    if !output.status.success() {
        bail!("git config user.name failed; set $LAZYSPEC_AGENT_ID or configure git user.name");
    }

    let user_name = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if user_name.is_empty() {
        bail!("git config user.name is empty; set $LAZYSPEC_AGENT_ID or configure git user.name");
    }

    Ok(user_name)
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;

    #[test]
    fn uses_lazyspec_agent_id_when_set() {
        let result =
            resolve_agent_id_with_env(Path::new("/tmp/fake"), Some("my-agent".into()), None)
                .unwrap();
        assert_eq!(result, "my-agent");
    }

    #[test]
    fn uses_claude_session_id_when_lazyspec_unset() {
        let result =
            resolve_agent_id_with_env(Path::new("/tmp/fake"), None, Some("sess-123".into()))
                .unwrap();
        assert_eq!(result, "sess-123");
    }

    #[test]
    fn lazyspec_agent_id_takes_priority_over_claude_session_id() {
        let result = resolve_agent_id_with_env(
            Path::new("/tmp/fake"),
            Some("agent-1".into()),
            Some("sess-123".into()),
        )
        .unwrap();
        assert_eq!(result, "agent-1");
    }

    #[test]
    fn empty_strings_treated_as_unset() {
        let result =
            resolve_agent_id_with_env(Path::new("/tmp/fake"), Some("".into()), Some("sess".into()))
                .unwrap();
        assert_eq!(result, "sess");
    }

    #[test]
    fn falls_back_to_git_config_username() {
        let dir = TempDir::new().unwrap();
        let root = dir.path();

        Command::new("git")
            .args(["init"])
            .current_dir(root)
            .output()
            .unwrap();
        Command::new("git")
            .args(["config", "user.name", "TestUser"])
            .current_dir(root)
            .output()
            .unwrap();

        let result = resolve_agent_id_with_env(root, None, None).unwrap();
        assert_eq!(result, "TestUser");
    }

    #[test]
    fn both_empty_strings_falls_back_to_git() {
        let dir = TempDir::new().unwrap();
        let root = dir.path();

        Command::new("git")
            .args(["init"])
            .current_dir(root)
            .output()
            .unwrap();
        Command::new("git")
            .args(["config", "user.name", "GitUser"])
            .current_dir(root)
            .output()
            .unwrap();

        let result = resolve_agent_id_with_env(root, Some("".into()), Some("".into())).unwrap();
        assert_eq!(result, "GitUser");
    }
}