1use std::fs::{self, OpenOptions};
10use std::io::{Read, Write};
11use std::path::{Path, PathBuf};
12
13use anyhow::{Context, Result};
14use fs2::FileExt;
15use indexmap::IndexMap;
16use serde::{Deserialize, Serialize};
17
18#[derive(Clone)]
20pub struct MarkdownStorage {
21 storage_dir: PathBuf,
22}
23
24impl MarkdownStorage {
25 pub fn new(storage_dir: PathBuf) -> Self {
27 Self { storage_dir }
28 }
29
30 pub fn init(&self) -> Result<()> {
32 fs::create_dir_all(&self.storage_dir)?;
33 Ok(())
34 }
35
36 pub fn store<T: Serialize>(&self, key: &str, data: &T, title: &str) -> Result<()> {
38 let file_path = self.storage_dir.join(format!("{key}.md"));
39 let markdown = self.serialize_to_markdown(data, title)?;
40 write_with_lock(&file_path, markdown.as_bytes())
41 }
42
43 pub fn load<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Result<T> {
45 let file_path = self.storage_dir.join(format!("{key}.md"));
46 let content = read_with_shared_lock(&file_path)?;
47 self.deserialize_from_markdown(&content)
48 }
49
50 pub fn list(&self) -> Result<Vec<String>> {
52 let mut items = Vec::new();
53
54 for entry in fs::read_dir(&self.storage_dir)? {
55 let entry = entry?;
56 if let Some(name) = entry.path().file_stem().and_then(|file_name| file_name.to_str()) {
57 items.push(name.to_string());
58 }
59 }
60
61 Ok(items)
62 }
63
64 pub fn delete(&self, key: &str) -> Result<()> {
66 let file_path = self.storage_dir.join(format!("{key}.md"));
67 if file_path.exists() {
68 if let Ok(file) = OpenOptions::new().read(true).write(true).open(&file_path) {
71 let _ = file.lock_exclusive();
72 drop(file);
74 }
75
76 match fs::remove_file(&file_path) {
79 Ok(_) => {}
80 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
81 Err(err) => {
82 return Err(err).with_context(|| {
83 format!("Failed to delete markdown file at {}", file_path.display())
84 });
85 }
86 }
87 }
88 Ok(())
89 }
90
91 pub fn exists(&self, key: &str) -> bool {
93 let file_path = self.storage_dir.join(format!("{key}.md"));
94 file_path.exists()
95 }
96
97 fn serialize_to_markdown<T: Serialize>(&self, data: &T, title: &str) -> Result<String> {
98 let json = serde_json::to_string_pretty(data)?;
99 let yaml = serde_saphyr::to_string(data)?;
100
101 let markdown = format!(
102 "# {}\n\n\
103 ## JSON\n\n\
104 ```json\n\
105 {}\n\
106 ```\n\n\
107 ## YAML\n\n\
108 ```yaml\n\
109 {}\n\
110 ```\n\n\
111 ## Raw Data\n\n\
112 {}\n",
113 title,
114 json,
115 yaml,
116 self.format_raw_data(data)
117 );
118
119 Ok(markdown)
120 }
121
122 fn deserialize_from_markdown<T: for<'de> Deserialize<'de>>(&self, content: &str) -> Result<T> {
123 if let Some(json_block) = self.extract_code_block(content, "json") {
124 return serde_json::from_str(json_block).context("Failed to parse JSON from markdown");
125 }
126
127 if let Some(yaml_block) = self.extract_code_block(content, "yaml") {
128 return serde_saphyr::from_str(yaml_block)
129 .context("Failed to parse YAML from markdown");
130 }
131
132 Err(anyhow::anyhow!("No valid JSON or YAML found in markdown"))
133 }
134
135 fn extract_code_block<'a>(&self, content: &'a str, language: &str) -> Option<&'a str> {
136 let start_pattern = format!("```{language}");
137 let end_pattern = "```";
138
139 if let Some(start_idx) = content.find(&start_pattern) {
140 let code_start = start_idx + start_pattern.len();
141 if let Some(end_idx) = content[code_start..].find(end_pattern) {
142 let code_end = code_start + end_idx;
143 return Some(content[code_start..code_end].trim());
144 }
145 }
146
147 None
148 }
149
150 fn format_raw_data<T: Serialize>(&self, data: &T) -> String {
151 match serde_json::to_value(data) {
152 Ok(serde_json::Value::Object(map)) => {
153 let mut lines = Vec::with_capacity(map.len());
154 for (key, value) in map {
155 lines.push(format!("- **{}**: {}", key, self.format_value(&value)));
156 }
157 lines.join("\n")
158 }
159 _ => "Complex data structure".to_string(),
160 }
161 }
162
163 fn format_value(&self, value: &serde_json::Value) -> String {
164 match value {
165 serde_json::Value::String(s) => format!("\"{s}\""),
166 serde_json::Value::Number(n) => n.to_string(),
167 serde_json::Value::Bool(b) => b.to_string(),
168 serde_json::Value::Array(arr) => format!("[{} items]", arr.len()),
169 serde_json::Value::Object(obj) => format!("{{{} fields}}", obj.len()),
170 serde_json::Value::Null => "null".to_string(),
171 }
172 }
173}
174
175fn write_with_lock(path: &Path, data: &[u8]) -> Result<()> {
176 if let Some(parent) = path.parent() {
177 fs::create_dir_all(parent).with_context(|| {
178 format!("Failed to ensure parent directory exists for {}", path.display())
179 })?;
180 }
181
182 let mut file = OpenOptions::new()
183 .create(true)
184 .write(true)
185 .truncate(false)
186 .open(path)
187 .with_context(|| format!("Failed to open file at {}", path.display()))?;
188
189 FileExt::lock_exclusive(&file)
190 .with_context(|| format!("Failed to acquire exclusive lock for {}", path.display()))?;
191
192 file.set_len(0).with_context(|| {
193 format!("Failed to truncate file at {} while holding exclusive lock", path.display())
194 })?;
195
196 file.write_all(data).with_context(|| {
197 format!("Failed to write file content to {} while holding exclusive lock", path.display())
198 })?;
199
200 file.sync_all().with_context(|| {
201 format!("Failed to sync file at {} after writing with exclusive lock", path.display())
202 })?;
203
204 FileExt::unlock(&file)
205 .with_context(|| format!("Failed to release exclusive lock for {}", path.display()))
206}
207
208fn read_with_shared_lock(path: &Path) -> Result<String> {
209 let mut file = OpenOptions::new()
210 .read(true)
211 .open(path)
212 .with_context(|| format!("Failed to open file at {}", path.display()))?;
213
214 FileExt::lock_shared(&file)
215 .with_context(|| format!("Failed to acquire shared lock for {}", path.display()))?;
216
217 let mut content = String::new();
218 file.read_to_string(&mut content).with_context(|| {
219 format!("Failed to read file content from {} while holding shared lock", path.display())
220 })?;
221
222 FileExt::unlock(&file)
223 .with_context(|| format!("Failed to release shared lock for {}", path.display()))?;
224
225 Ok(content)
226}
227
228pub struct SimpleKVStorage {
230 storage: MarkdownStorage,
231}
232
233impl SimpleKVStorage {
234 pub fn new(storage_dir: PathBuf) -> Self {
235 Self { storage: MarkdownStorage::new(storage_dir) }
236 }
237
238 pub fn init(&self) -> Result<()> {
239 self.storage.init()
240 }
241
242 pub fn put(&self, key: &str, value: &str) -> Result<()> {
243 let data = IndexMap::from([("value".to_string(), value.to_string())]);
244 self.storage.store(key, &data, &format!("Key-Value: {key}"))
245 }
246
247 pub fn get(&self, key: &str) -> Result<String> {
248 let data: IndexMap<String, String> = self.storage.load(key)?;
249 data.get("value")
250 .cloned()
251 .ok_or_else(|| anyhow::anyhow!("Value not found for key: {key}"))
252 }
253
254 pub fn delete(&self, key: &str) -> Result<()> {
255 self.storage.delete(key)
256 }
257
258 pub fn list_keys(&self) -> Result<Vec<String>> {
259 self.storage.list()
260 }
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct ProjectData {
266 pub name: String,
267 pub description: Option<String>,
268 pub version: String,
269 pub tags: Vec<String>,
270 pub metadata: IndexMap<String, String>,
271}
272
273impl ProjectData {
274 pub fn new(name: &str) -> Self {
275 Self {
276 name: name.to_string(),
277 description: None,
278 version: "1.0.0".to_string(),
279 tags: vec![],
280 metadata: IndexMap::new(),
281 }
282 }
283}
284
285#[derive(Clone)]
287pub struct ProjectStorage {
288 storage: MarkdownStorage,
289}
290
291impl ProjectStorage {
292 pub fn new(storage_dir: PathBuf) -> Self {
293 Self { storage: MarkdownStorage::new(storage_dir) }
294 }
295
296 pub fn init(&self) -> Result<()> {
297 self.storage.init()
298 }
299
300 pub fn save_project(&self, project: &ProjectData) -> Result<()> {
301 self.storage
302 .store(&project.name, project, &format!("Project: {}", project.name))
303 }
304
305 pub fn load_project(&self, name: &str) -> Result<ProjectData> {
306 self.storage.load(name)
307 }
308
309 pub fn list_projects(&self) -> Result<Vec<String>> {
310 self.storage.list()
311 }
312
313 pub fn delete_project(&self, name: &str) -> Result<()> {
314 self.storage.delete(name)
315 }
316
317 pub fn storage_dir(&self) -> &Path {
318 &self.storage.storage_dir
319 }
320}
321
322#[derive(Clone)]
324pub struct SimpleProjectManager {
325 storage: ProjectStorage,
326 workspace_root: PathBuf,
327 project_root: PathBuf,
328}
329
330impl SimpleProjectManager {
331 pub fn new(workspace_root: PathBuf) -> Self {
334 let project_root = workspace_root.join(".vtcode").join("projects");
335 Self::with_project_root(workspace_root, project_root)
336 }
337
338 pub fn with_project_root(workspace_root: PathBuf, project_root: PathBuf) -> Self {
340 let storage = ProjectStorage::new(project_root.clone());
341 Self { storage, workspace_root, project_root }
342 }
343
344 pub fn init(&self) -> Result<()> {
346 self.storage.init()
347 }
348
349 pub fn create_project(&self, name: &str, description: Option<&str>) -> Result<()> {
351 let mut project = ProjectData::new(name);
352 project.description = description.map(|s| s.to_string());
353
354 self.storage.save_project(&project)?;
355 Ok(())
356 }
357
358 pub fn load_project(&self, name: &str) -> Result<ProjectData> {
360 self.storage.load_project(name)
361 }
362
363 pub fn list_projects(&self) -> Result<Vec<String>> {
365 self.storage.list_projects()
366 }
367
368 pub fn delete_project(&self, name: &str) -> Result<()> {
370 self.storage.delete_project(name)
371 }
372
373 pub fn update_project(&self, project: &ProjectData) -> Result<()> {
375 self.storage.save_project(project)
376 }
377
378 pub fn project_data_dir(&self, project_name: &str) -> PathBuf {
380 self.project_root.join(project_name)
381 }
382
383 pub fn config_dir(&self, project_name: &str) -> PathBuf {
385 self.project_data_dir(project_name).join("config")
386 }
387
388 pub fn cache_dir(&self, project_name: &str) -> PathBuf {
390 self.project_data_dir(project_name).join("cache")
391 }
392
393 pub fn workspace_root(&self) -> &Path {
395 &self.workspace_root
396 }
397
398 pub fn project_root(&self) -> &Path {
400 &self.project_root
401 }
402
403 pub fn project_exists(&self, name: &str) -> bool {
405 self.storage
406 .list_projects()
407 .map(|projects| projects.contains(&name.to_string()))
408 .unwrap_or(false)
409 }
410
411 pub fn get_project_info(&self, name: &str) -> Result<String> {
413 let project = self.load_project(name)?;
414
415 let mut info = format!("Project: {}\n", project.name);
416 if let Some(desc) = &project.description {
417 info.push_str(&format!("Description: {desc}\n"));
418 }
419 info.push_str(&format!("Version: {}\n", project.version));
420 info.push_str(&format!("Tags: {}\n", project.tags.join(", ")));
421
422 if !project.metadata.is_empty() {
423 info.push_str("\nMetadata:\n");
424 for (key, value) in &project.metadata {
425 info.push_str(&format!(" {key}: {value}\n"));
426 }
427 }
428
429 Ok(info)
430 }
431
432 pub fn identify_current_project(&self) -> Result<String> {
434 let project_file = self.workspace_root.join(".vtcode-project");
435 if project_file.exists() {
436 let content = fs::read_to_string(&project_file)?;
437 return Ok(content.trim().to_string());
438 }
439
440 self.workspace_root
441 .file_name()
442 .and_then(|name| name.to_str())
443 .map(|name| name.to_string())
444 .ok_or_else(|| anyhow::anyhow!("Could not determine project name from directory"))
445 }
446
447 pub fn set_current_project(&self, name: &str) -> Result<()> {
449 let project_file = self.workspace_root.join(".vtcode-project");
450 fs::write(project_file, name)?;
451 Ok(())
452 }
453}
454
455pub struct SimpleCache {
457 cache_dir: PathBuf,
458}
459
460impl SimpleCache {
461 pub fn new(cache_dir: PathBuf) -> Self {
463 Self { cache_dir }
464 }
465
466 pub fn init(&self) -> Result<()> {
468 fs::create_dir_all(&self.cache_dir)?;
469 Ok(())
470 }
471
472 pub fn store(&self, key: &str, data: &str) -> Result<()> {
474 let file_path = self.cache_dir.join(format!("{key}.txt"));
475 write_with_lock(&file_path, data.as_bytes())
476 }
477
478 pub fn load(&self, key: &str) -> Result<String> {
480 let file_path = self.cache_dir.join(format!("{key}.txt"));
481 read_with_shared_lock(&file_path).map_err(|err| {
482 if err
483 .downcast_ref::<std::io::Error>()
484 .is_some_and(|io_err| io_err.kind() == std::io::ErrorKind::NotFound)
485 {
486 anyhow::anyhow!("Cache key '{key}' not found")
487 } else {
488 err
489 }
490 })
491 }
492
493 pub fn exists(&self, key: &str) -> bool {
495 let file_path = self.cache_dir.join(format!("{key}.txt"));
496 file_path.exists()
497 }
498
499 pub fn clear(&self) -> Result<()> {
501 for entry in fs::read_dir(&self.cache_dir)? {
502 let entry = entry?;
503 if entry.path().is_file() {
504 fs::remove_file(entry.path())?;
505 }
506 }
507 Ok(())
508 }
509
510 pub fn list(&self) -> Result<Vec<String>> {
512 let mut entries = Vec::new();
513 for entry in fs::read_dir(&self.cache_dir)? {
514 let entry = entry?;
515 if let Some(name) = entry.path().file_stem().and_then(|file_name| file_name.to_str()) {
516 entries.push(name.to_string());
517 }
518 }
519 Ok(entries)
520 }
521}