vtcode-core 0.103.1

Core library for VT Code - a Rust-based terminal coding agent
use anyhow::{Context, Result};
use serde::Serialize;
use serde_json::Value;
use std::path::PathBuf;
use std::time::SystemTime;

/// Tracks files generated by code execution
#[derive(Debug, Clone)]
pub struct FileTracker {
    workspace_root: PathBuf,
    tracked_patterns: Vec<String>,
}

impl FileTracker {
    pub fn new(workspace_root: PathBuf) -> Self {
        Self {
            workspace_root,
            tracked_patterns: vec![
                "*.pdf".to_string(),
                "*.xlsx".to_string(),
                "*.csv".to_string(),
                "*.docx".to_string(),
                "*.png".to_string(),
                "*.jpg".to_string(),
                "*.json".to_string(),
                "*.xml".to_string(),
            ],
        }
    }

    /// Add a custom file pattern to track
    pub fn add_pattern(&mut self, pattern: String) {
        self.tracked_patterns.push(pattern);
    }

    /// Scan for newly created or modified files matching tracked patterns
    pub async fn detect_new_files(&self, since: SystemTime) -> Result<Vec<TrackedFile>> {
        let mut new_files = Vec::new();

        for pattern in &self.tracked_patterns {
            let matches = self.find_files_matching_pattern(pattern).await?;

            for file_path in matches {
                if let Ok(metadata) = tokio::fs::metadata(&file_path).await
                    && let Ok(modified) = metadata.modified()
                    && modified >= since
                    && metadata.is_file()
                {
                    new_files.push(TrackedFile {
                        absolute_path: file_path,
                        size: metadata.len(),
                        modified,
                    });
                }
            }
        }

        Ok(new_files)
    }

    /// Check if a specific file exists and return its info
    pub async fn verify_file_exists(&self, filename: &str) -> Result<Option<TrackedFile>> {
        let file_path = self.workspace_root.join(filename);

        match tokio::fs::metadata(&file_path).await {
            Ok(metadata) if metadata.is_file() => {
                let modified = metadata.modified().unwrap_or_else(|_| SystemTime::now());

                Ok(Some(TrackedFile {
                    absolute_path: file_path,
                    size: metadata.len(),
                    modified,
                }))
            }
            Ok(_) => Ok(None),  // Not a file
            Err(_) => Ok(None), // Doesn't exist
        }
    }

    /// Get the absolute path of a file
    pub fn get_absolute_path(&self, filename: &str) -> PathBuf {
        self.workspace_root.join(filename)
    }

    /// Find all files matching a glob pattern
    pub async fn find_files_matching_pattern(&self, pattern: &str) -> Result<Vec<PathBuf>> {
        use tokio::task;
        let workspace = self.workspace_root.clone();
        let pattern = pattern.to_string();

        task::spawn_blocking(move || {
            let mut files = Vec::new();

            if let Ok(glob_pattern) = glob::glob(&format!("{}/{}", workspace.display(), pattern)) {
                for path in glob_pattern.flatten() {
                    files.push(path);
                }
            }

            Ok(files)
        })
        .await
        .context("Failed to spawn file search task")?
    }

    /// Generate a verification code snippet
    pub fn generate_verification_code(&self, filename: &str) -> String {
        format!(
            r#"
import os
import sys

file_path = os.path.abspath('{filename}')
exists = os.path.exists(file_path)
is_file = os.path.isfile(file_path) if exists else False
size = os.path.getsize(file_path) if exists else 0

print(f"FILE_VERIFICATION: {{file_path}} {{exists}} {{is_file}} {{size}}")
sys.exit(0 if exists and is_file else 1)
"#,
            filename = filename
        )
    }

    /// Generate a summary of tracked files
    pub fn generate_file_summary(&self, files: &[TrackedFile]) -> String {
        if files.is_empty() {
            return "No generated files detected.".to_string();
        }

        let mut summary = String::from("Generated files:\n");
        for file in files {
            summary.push_str(&format!(
                "  - {} ({} bytes)\n",
                file.absolute_path.display(),
                file.size
            ));
        }
        summary
    }
}

/// Represents a tracked file with metadata
#[derive(Debug, Clone, Serialize)]
pub struct TrackedFile {
    pub absolute_path: PathBuf,
    pub size: u64,
    #[serde(with = "system_time_serde")]
    pub modified: SystemTime,
}

mod system_time_serde {
    use serde::Serializer;
    use std::time::{SystemTime, UNIX_EPOCH};

    pub fn serialize<S>(time: &SystemTime, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let duration = time
            .duration_since(UNIX_EPOCH)
            .unwrap_or_else(|_| std::time::Duration::from_secs(0));
        serializer.serialize_u64(duration.as_secs())
    }
}

impl TrackedFile {
    pub fn to_json(&self) -> Value {
        serde_json::json!({
            "absolute_path": self.absolute_path.display().to_string(),
            "size": self.size,
            "modified": format!("{:?}", self.modified),
        })
    }
}

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

    #[tokio::test]
    async fn test_file_tracker_basic() {
        let tracker = FileTracker::new(PathBuf::from("/test"));
        assert!(!tracker.tracked_patterns.is_empty());
    }

    #[tokio::test]
    async fn test_verification_code_generation() {
        let tracker = FileTracker::new(PathBuf::from("/workspace"));
        let code = tracker.generate_verification_code("test.pdf");
        assert!(code.contains("test.pdf"));
        assert!(code.contains("FILE_VERIFICATION"));
    }

    #[test]
    fn test_file_summary_generation() {
        let tracker = FileTracker::new(PathBuf::from("/workspace"));
        let files = vec![TrackedFile {
            absolute_path: PathBuf::from("/workspace/test.pdf"),
            size: 1024,
            modified: SystemTime::now(),
        }];

        let summary = tracker.generate_file_summary(&files);
        assert!(summary.contains("test.pdf"));
        assert!(summary.contains("1024 bytes"));
    }
}