csusage-adapter-antigravity 0.0.2

Coding agent CLI usage reports (ccusage fork with Claude Science support)
use std::{collections::HashSet, env, fs, path::PathBuf};

use csusage_adapter_common::collect_files_with_extension;

use crate::Result;

pub(super) const ANTIGRAVITY_DATA_DIR_ENV: &str = "ANTIGRAVITY_DATA_DIR";

const DEFAULT_ANTIGRAVITY_ROOTS: [&str; 5] = [
    ".gemini/antigravity",
    ".gemini/antigravity-cli",
    ".gemini/antigravity-ide",
    ".gemini/antigravity-backup",
    ".config/antigravity",
];

fn roots() -> Vec<PathBuf> {
    if let Ok(value) = env::var(ANTIGRAVITY_DATA_DIR_ENV) {
        return value
            .split(',')
            .map(str::trim)
            .filter(|path| !path.is_empty())
            .map(PathBuf::from)
            .collect();
    }

    crate::home::home_dir()
        .into_iter()
        .flat_map(|home| {
            DEFAULT_ANTIGRAVITY_ROOTS
                .into_iter()
                .map(move |root| home.join(root))
        })
        .collect()
}

fn conversation_dir(root: PathBuf) -> PathBuf {
    let nested = root.join("conversations");
    if nested.is_dir() { nested } else { root }
}

pub(super) fn conversation_db_paths() -> Result<Vec<PathBuf>> {
    let mut paths = Vec::new();
    let mut seen = HashSet::new();
    for root in roots().into_iter().map(conversation_dir) {
        let mut files = Vec::new();
        collect_files_with_extension(&root, "db", &mut files);
        for path in files {
            let canonical_path = fs::canonicalize(&path).unwrap_or_else(|_| path.clone());
            if seen.insert(canonical_path) {
                paths.push(path);
            }
        }
    }
    paths.sort();
    Ok(paths)
}

#[cfg(test)]
mod tests {
    use std::ffi::OsString;

    use csusage_test_support::{EnvVarsGuard, fs_fixture};

    use super::*;

    #[test]
    fn discovers_databases_from_independent_roots() {
        let fixture = fs_fixture!({
            ".gemini/antigravity/conversations/ide.db": "",
            ".gemini/antigravity-cli/conversations/cli.db": "",
            ".gemini/antigravity-ide/conversations/ide-alt.db": "",
            ".gemini/antigravity-backup/conversations/backup.db": "",
            ".config/antigravity/conversations/config.db": "",
        });
        let _guard = EnvVarsGuard::set_many([
            (ANTIGRAVITY_DATA_DIR_ENV, None),
            ("HOME", Some(OsString::from(fixture.root()))),
            ("USERPROFILE", Some(OsString::from(fixture.root()))),
        ]);

        let paths = conversation_db_paths().unwrap();

        assert_eq!(paths.len(), 5);
        assert!(paths.iter().all(|path| path.extension().unwrap() == "db"));
    }

    #[test]
    fn accepts_conversation_directory_and_parent_directory_overrides() {
        let fixture = fs_fixture!({
            "parent/conversations/parent.db": "",
            "direct/conversation.db": "",
        });
        let override_value = format!(
            "{},{}",
            fixture.path("parent").display(),
            fixture.path("direct").display()
        );
        let _guard = EnvVarsGuard::set_many([(
            ANTIGRAVITY_DATA_DIR_ENV,
            Some(OsString::from(override_value)),
        )]);

        let paths = conversation_db_paths().unwrap();

        assert_eq!(
            paths,
            vec![
                fixture.path("direct/conversation.db"),
                fixture.path("parent/conversations/parent.db"),
            ]
        );
    }

    #[test]
    fn deduplicates_canonicalized_override_aliases() {
        let fixture = fs_fixture!({
            "conversations/session.db": "",
        });
        let override_value = format!(
            "{},{}",
            fixture.root().display(),
            fixture.path("conversations/..").display()
        );
        let _guard = EnvVarsGuard::set_many([(
            ANTIGRAVITY_DATA_DIR_ENV,
            Some(OsString::from(override_value)),
        )]);

        assert_eq!(
            conversation_db_paths().unwrap(),
            vec![fixture.path("conversations/session.db")]
        );
    }
}