Skip to main content

vtcode_commons/
project.rs

1//! Project-related utilities and structures
2
3use crate::utils::{extract_readme_excerpt, extract_toml_str};
4use std::fmt::Write as _;
5use std::path::{Path, PathBuf};
6use tokio::fs;
7
8/// Lightweight project overview extracted from workspace files
9pub struct ProjectOverview {
10    pub name: Option<String>,
11    pub version: Option<String>,
12    pub description: Option<String>,
13    pub readme_excerpt: Option<String>,
14    pub root: PathBuf,
15}
16
17impl ProjectOverview {
18    pub fn short_for_display(&self) -> String {
19        let mut out = String::new();
20        if let Some(name) = &self.name {
21            let _ = write!(out, "Project: {name}");
22        }
23        if let Some(ver) = &self.version {
24            if !out.is_empty() {
25                out.push(' ');
26            }
27            let _ = write!(out, "v{ver}");
28        }
29        if !out.is_empty() {
30            out.push('\n');
31        }
32        if let Some(desc) = &self.description {
33            out.push_str(desc);
34            out.push('\n');
35        }
36        let _ = write!(out, "Root: {}", self.root.display());
37        out
38    }
39
40    pub fn as_prompt_block(&self) -> String {
41        let mut s = String::new();
42        if let Some(name) = &self.name {
43            let _ = writeln!(s, "- Name: {name}");
44        }
45        if let Some(ver) = &self.version {
46            let _ = writeln!(s, "- Version: {ver}");
47        }
48        if let Some(desc) = &self.description {
49            let _ = writeln!(s, "- Description: {desc}");
50        }
51        let _ = writeln!(s, "- Workspace Root: {}", self.root.display());
52        if let Some(excerpt) = &self.readme_excerpt {
53            s.push_str("- README Excerpt: \n");
54            s.push_str(excerpt);
55            if !excerpt.ends_with('\n') {
56                s.push('\n');
57            }
58        }
59        s
60    }
61}
62
63/// Build a minimal project overview from Cargo.toml and README.md
64pub async fn build_project_overview(root: &Path) -> Option<ProjectOverview> {
65    let cargo_toml_path = root.join("Cargo.toml");
66    let readme_path = root.join("README.md");
67    let (cargo_toml, readme) = tokio::join!(fs::read_to_string(cargo_toml_path), fs::read_to_string(readme_path));
68
69    let metadata = cargo_toml.ok().map(|contents| ProjectMetadata::from_cargo_toml(&contents));
70    let readme_excerpt = match readme {
71        Ok(contents) => Some(extract_readme_excerpt(&contents, 1200)),
72        Err(_) => read_readme_fallback(root).await,
73    };
74
75    let overview = ProjectOverview {
76        name: metadata.as_ref().and_then(|metadata| metadata.name.clone()),
77        version: metadata.as_ref().and_then(|metadata| metadata.version.clone()),
78        description: metadata.and_then(|metadata| metadata.description),
79        readme_excerpt,
80        root: root.to_path_buf(),
81    };
82
83    (overview.name.is_some()
84        || overview.version.is_some()
85        || overview.description.is_some()
86        || overview.readme_excerpt.is_some())
87    .then_some(overview)
88}
89
90struct ProjectMetadata {
91    name: Option<String>,
92    version: Option<String>,
93    description: Option<String>,
94}
95
96impl ProjectMetadata {
97    fn from_cargo_toml(contents: &str) -> Self {
98        Self {
99            name: extract_toml_str(contents, "name"),
100            version: extract_toml_str(contents, "version"),
101            description: extract_toml_str(contents, "description"),
102        }
103    }
104}
105
106async fn read_readme_fallback(root: &Path) -> Option<String> {
107    for alt in ["QUICKSTART.md", "user-context.md"] {
108        let path = root.join(alt);
109        if let Ok(contents) = fs::read_to_string(path).await {
110            return Some(extract_readme_excerpt(&contents, 800));
111        }
112    }
113    None
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use std::fs;
120    use tempfile::tempdir;
121
122    #[tokio::test]
123    async fn overview_keeps_metadata_only_projects() {
124        let root = tempdir().expect("temporary project directory");
125        fs::write(root.path().join("Cargo.toml"), "version = \"1.2.3\"\ndescription = \"metadata\"\n")
126            .expect("write Cargo.toml");
127
128        let overview = build_project_overview(root.path())
129            .await
130            .expect("metadata should produce an overview");
131
132        assert_eq!(overview.version.as_deref(), Some("1.2.3"));
133        assert_eq!(overview.description.as_deref(), Some("metadata"));
134    }
135}