tli 0.0.5

Fast file-backed task tracker for humans, hooks, and AI agents.
Documentation
use std::path::{Path, PathBuf};

use anyhow::{Context, Result, anyhow};
use chrono::{DateTime, Local, Utc};

pub(crate) fn resolve_root(root: Option<PathBuf>) -> Result<PathBuf> {
    match root {
        Some(path) => Ok(path),
        None => {
            let cwd = std::env::current_dir().context("failed to resolve current directory")?;
            find_existing_root(&cwd).ok_or_else(|| {
                anyhow!(
                    "could not find '.tli' from '{}' up to filesystem root; pass --root <path> to create or target a store explicitly",
                    display_path(&cwd)
                )
            })
        }
    }
}

pub(crate) fn find_existing_root(start: &Path) -> Option<PathBuf> {
    let mut current = Some(start);
    while let Some(dir) = current {
        let candidate = dir.join(".tli");
        if candidate.is_dir() {
            return Some(candidate);
        }
        current = dir.parent();
    }
    None
}

pub(crate) fn parse_timestamp(value: &str) -> Result<DateTime<Utc>> {
    let parsed = DateTime::parse_from_rfc3339(value)
        .with_context(|| format!("expected RFC3339 timestamp, got '{value}'"))?;
    Ok(parsed.with_timezone(&Utc))
}

pub(crate) fn format_timestamp(value: &DateTime<Utc>) -> String {
    value
        .with_timezone(&Local)
        .format("%Y-%m-%d %H:%M:%S %:z")
        .to_string()
}

pub(crate) fn display_path(path: &Path) -> String {
    path.to_string_lossy().into_owned()
}

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

    #[test]
    fn parse_timestamp_requires_rfc3339() {
        assert!(parse_timestamp("2026-05-02T18:42:57+08:00").is_ok());
        assert!(parse_timestamp("2026-05-02 18:42:57").is_err());
    }

    #[test]
    fn format_timestamp_is_not_empty() {
        let formatted = format_timestamp(&Utc::now());
        assert!(!formatted.is_empty());
    }

    #[test]
    fn find_existing_root_walks_up_parent_directories() {
        let temp = tempfile::TempDir::new().unwrap();
        let repo = temp.path().join("repo");
        let nested = repo.join("a").join("b");
        std::fs::create_dir_all(repo.join(".tli")).unwrap();
        std::fs::create_dir_all(&nested).unwrap();

        let found = find_existing_root(&nested).unwrap();
        assert_eq!(found, repo.join(".tli"));
    }

    #[test]
    fn find_existing_root_returns_none_when_missing() {
        let temp = tempfile::TempDir::new().unwrap();
        let nested = temp.path().join("a").join("b");
        std::fs::create_dir_all(&nested).unwrap();

        assert!(find_existing_root(&nested).is_none());
    }
}