vtcode_commons/
project.rs1use crate::utils::{extract_readme_excerpt, extract_toml_str};
4use std::fmt::Write as _;
5use std::path::{Path, PathBuf};
6use tokio::fs;
7
8pub 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
63pub 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}