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!("{}.md", key));
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!("{}.md", key));
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
57 .path()
58 .file_stem()
59 .and_then(|file_name| file_name.to_str())
60 {
61 items.push(name.to_string());
62 }
63 }
64
65 Ok(items)
66 }
67
68 pub fn delete(&self, key: &str) -> Result<()> {
70 let file_path = self.storage_dir.join(format!("{}.md", key));
71 if file_path.exists() {
72 if let Ok(file) = OpenOptions::new().read(true).write(true).open(&file_path) {
75 let _ = file.lock_exclusive();
76 drop(file);
78 }
79
80 match fs::remove_file(&file_path) {
83 Ok(_) => {}
84 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
85 Err(err) => {
86 return Err(err).with_context(|| {
87 format!("Failed to delete markdown file at {}", file_path.display())
88 });
89 }
90 }
91 }
92 Ok(())
93 }
94
95 pub fn exists(&self, key: &str) -> bool {
97 let file_path = self.storage_dir.join(format!("{}.md", key));
98 file_path.exists()
99 }
100
101 fn serialize_to_markdown<T: Serialize>(&self, data: &T, title: &str) -> Result<String> {
102 let json = serde_json::to_string_pretty(data)?;
103 let yaml = serde_yaml::to_string(data)?;
104
105 let markdown = format!(
106 "# {}\n\n\
107 ## JSON\n\n\
108 ```json\n\
109 {}\n\
110 ```\n\n\
111 ## YAML\n\n\
112 ```yaml\n\
113 {}\n\
114 ```\n\n\
115 ## Raw Data\n\n\
116 {}\n",
117 title,
118 json,
119 yaml,
120 self.format_raw_data(data)
121 );
122
123 Ok(markdown)
124 }
125
126 fn deserialize_from_markdown<T: for<'de> Deserialize<'de>>(&self, content: &str) -> Result<T> {
127 if let Some(json_block) = self.extract_code_block(content, "json") {
128 return serde_json::from_str(json_block).context("Failed to parse JSON from markdown");
129 }
130
131 if let Some(yaml_block) = self.extract_code_block(content, "yaml") {
132 return serde_yaml::from_str(yaml_block).context("Failed to parse YAML from markdown");
133 }
134
135 Err(anyhow::anyhow!("No valid JSON or YAML found in markdown"))
136 }
137
138 fn extract_code_block<'a>(&self, content: &'a str, language: &str) -> Option<&'a str> {
139 let start_pattern = format!("```{}", language);
140 let end_pattern = "```";
141
142 if let Some(start_idx) = content.find(&start_pattern) {
143 let code_start = start_idx + start_pattern.len();
144 if let Some(end_idx) = content[code_start..].find(end_pattern) {
145 let code_end = code_start + end_idx;
146 return Some(content[code_start..code_end].trim());
147 }
148 }
149
150 None
151 }
152
153 fn format_raw_data<T: Serialize>(&self, data: &T) -> String {
154 match serde_json::to_value(data) {
155 Ok(serde_json::Value::Object(map)) => {
156 let mut lines = Vec::new();
157 for (key, value) in map {
158 lines.push(format!("- **{}**: {}", key, self.format_value(&value)));
159 }
160 lines.join("\n")
161 }
162 _ => "Complex data structure".to_string(),
163 }
164 }
165
166 fn format_value(&self, value: &serde_json::Value) -> String {
167 match value {
168 serde_json::Value::String(s) => format!("\"{}\"", s),
169 serde_json::Value::Number(n) => n.to_string(),
170 serde_json::Value::Bool(b) => b.to_string(),
171 serde_json::Value::Array(arr) => format!("[{} items]", arr.len()),
172 serde_json::Value::Object(obj) => format!("{{{} fields}}", obj.len()),
173 serde_json::Value::Null => "null".to_string(),
174 }
175 }
176}
177
178fn write_with_lock(path: &Path, data: &[u8]) -> Result<()> {
179 if let Some(parent) = path.parent() {
180 fs::create_dir_all(parent).with_context(|| {
181 format!(
182 "Failed to ensure parent directory exists for {}",
183 path.display()
184 )
185 })?;
186 }
187
188 let mut file = OpenOptions::new()
189 .create(true)
190 .write(true)
191 .truncate(false)
192 .open(path)
193 .with_context(|| format!("Failed to open file at {}", path.display()))?;
194
195 file.lock_exclusive()
196 .with_context(|| format!("Failed to acquire exclusive lock for {}", path.display()))?;
197
198 file.set_len(0).with_context(|| {
199 format!(
200 "Failed to truncate file at {} while holding exclusive lock",
201 path.display()
202 )
203 })?;
204
205 file.write_all(data).with_context(|| {
206 format!(
207 "Failed to write file content to {} while holding exclusive lock",
208 path.display()
209 )
210 })?;
211
212 file.sync_all().with_context(|| {
213 format!(
214 "Failed to sync file at {} after writing with exclusive lock",
215 path.display()
216 )
217 })?;
218
219 file.unlock()
220 .with_context(|| format!("Failed to release exclusive lock for {}", path.display()))
221}
222
223fn read_with_shared_lock(path: &Path) -> Result<String> {
224 let mut file = OpenOptions::new()
225 .read(true)
226 .open(path)
227 .with_context(|| format!("Failed to open file at {}", path.display()))?;
228
229 file.lock_shared()
230 .with_context(|| format!("Failed to acquire shared lock for {}", path.display()))?;
231
232 let mut content = String::new();
233 file.read_to_string(&mut content).with_context(|| {
234 format!(
235 "Failed to read file content from {} while holding shared lock",
236 path.display()
237 )
238 })?;
239
240 file.unlock()
241 .with_context(|| format!("Failed to release shared lock for {}", path.display()))?;
242
243 Ok(content)
244}
245
246#[cfg(feature = "kv")]
248pub struct SimpleKVStorage {
249 storage: MarkdownStorage,
250}
251
252#[cfg(feature = "kv")]
253impl SimpleKVStorage {
254 pub fn new(storage_dir: PathBuf) -> Self {
255 Self {
256 storage: MarkdownStorage::new(storage_dir),
257 }
258 }
259
260 pub fn init(&self) -> Result<()> {
261 self.storage.init()
262 }
263
264 pub fn put(&self, key: &str, value: &str) -> Result<()> {
265 let data = IndexMap::from([("value".to_string(), value.to_string())]);
266 self.storage
267 .store(key, &data, &format!("Key-Value: {}", key))
268 }
269
270 pub fn get(&self, key: &str) -> Result<String> {
271 let data: IndexMap<String, String> = self.storage.load(key)?;
272 data.get("value")
273 .cloned()
274 .ok_or_else(|| anyhow::anyhow!("Value not found for key: {}", key))
275 }
276
277 pub fn delete(&self, key: &str) -> Result<()> {
278 self.storage.delete(key)
279 }
280
281 pub fn list_keys(&self) -> Result<Vec<String>> {
282 self.storage.list()
283 }
284}
285
286#[cfg(feature = "projects")]
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct ProjectData {
290 pub name: String,
291 pub description: Option<String>,
292 pub version: String,
293 pub tags: Vec<String>,
294 pub metadata: IndexMap<String, String>,
295}
296
297#[cfg(feature = "projects")]
298impl ProjectData {
299 pub fn new(name: &str) -> Self {
300 Self {
301 name: name.to_string(),
302 description: None,
303 version: "1.0.0".to_string(),
304 tags: vec![],
305 metadata: IndexMap::new(),
306 }
307 }
308}
309
310#[cfg(feature = "projects")]
312#[derive(Clone)]
313pub struct ProjectStorage {
314 storage: MarkdownStorage,
315}
316
317#[cfg(feature = "projects")]
318impl ProjectStorage {
319 pub fn new(storage_dir: PathBuf) -> Self {
320 Self {
321 storage: MarkdownStorage::new(storage_dir),
322 }
323 }
324
325 pub fn init(&self) -> Result<()> {
326 self.storage.init()
327 }
328
329 pub fn save_project(&self, project: &ProjectData) -> Result<()> {
330 self.storage.store(
331 &project.name,
332 project,
333 &format!("Project: {}", project.name),
334 )
335 }
336
337 pub fn load_project(&self, name: &str) -> Result<ProjectData> {
338 self.storage.load(name)
339 }
340
341 pub fn list_projects(&self) -> Result<Vec<String>> {
342 self.storage.list()
343 }
344
345 pub fn delete_project(&self, name: &str) -> Result<()> {
346 self.storage.delete(name)
347 }
348
349 pub fn storage_dir(&self) -> &Path {
350 &self.storage.storage_dir
351 }
352}
353
354#[cfg(feature = "projects")]
356#[derive(Clone)]
357pub struct SimpleProjectManager {
358 storage: ProjectStorage,
359 workspace_root: PathBuf,
360 project_root: PathBuf,
361}
362
363#[cfg(feature = "projects")]
364impl SimpleProjectManager {
365 pub fn new(workspace_root: PathBuf) -> Self {
368 let project_root = workspace_root.join(".vtcode").join("projects");
369 Self::with_project_root(workspace_root, project_root)
370 }
371
372 pub fn with_project_root(workspace_root: PathBuf, project_root: PathBuf) -> Self {
374 let storage = ProjectStorage::new(project_root.clone());
375 Self {
376 storage,
377 workspace_root,
378 project_root,
379 }
380 }
381
382 pub fn init(&self) -> Result<()> {
384 self.storage.init()
385 }
386
387 pub fn create_project(&self, name: &str, description: Option<&str>) -> Result<()> {
389 let mut project = ProjectData::new(name);
390 project.description = description.map(|s| s.to_string());
391
392 self.storage.save_project(&project)?;
393 Ok(())
394 }
395
396 pub fn load_project(&self, name: &str) -> Result<ProjectData> {
398 self.storage.load_project(name)
399 }
400
401 pub fn list_projects(&self) -> Result<Vec<String>> {
403 self.storage.list_projects()
404 }
405
406 pub fn delete_project(&self, name: &str) -> Result<()> {
408 self.storage.delete_project(name)
409 }
410
411 pub fn update_project(&self, project: &ProjectData) -> Result<()> {
413 self.storage.save_project(project)
414 }
415
416 pub fn project_data_dir(&self, project_name: &str) -> PathBuf {
418 self.project_root.join(project_name)
419 }
420
421 pub fn config_dir(&self, project_name: &str) -> PathBuf {
423 self.project_data_dir(project_name).join("config")
424 }
425
426 pub fn cache_dir(&self, project_name: &str) -> PathBuf {
428 self.project_data_dir(project_name).join("cache")
429 }
430
431 pub fn workspace_root(&self) -> &Path {
433 &self.workspace_root
434 }
435
436 pub fn project_root(&self) -> &Path {
438 &self.project_root
439 }
440
441 pub fn project_exists(&self, name: &str) -> bool {
443 self.storage
444 .list_projects()
445 .map(|projects| projects.contains(&name.to_string()))
446 .unwrap_or(false)
447 }
448
449 pub fn get_project_info(&self, name: &str) -> Result<String> {
451 let project = self.load_project(name)?;
452
453 let mut info = format!("Project: {}\n", project.name);
454 if let Some(desc) = &project.description {
455 info.push_str(&format!("Description: {}\n", desc));
456 }
457 info.push_str(&format!("Version: {}\n", project.version));
458 info.push_str(&format!("Tags: {}\n", project.tags.join(", ")));
459
460 if !project.metadata.is_empty() {
461 info.push_str("\nMetadata:\n");
462 for (key, value) in &project.metadata {
463 info.push_str(&format!(" {}: {}\n", key, value));
464 }
465 }
466
467 Ok(info)
468 }
469
470 pub fn identify_current_project(&self) -> Result<String> {
472 let project_file = self.workspace_root.join(".vtcode-project");
473 if project_file.exists() {
474 let content = fs::read_to_string(&project_file)?;
475 return Ok(content.trim().to_string());
476 }
477
478 self.workspace_root
479 .file_name()
480 .and_then(|name| name.to_str())
481 .map(|name| name.to_string())
482 .ok_or_else(|| anyhow::anyhow!("Could not determine project name from directory"))
483 }
484
485 pub fn set_current_project(&self, name: &str) -> Result<()> {
487 let project_file = self.workspace_root.join(".vtcode-project");
488 fs::write(project_file, name)?;
489 Ok(())
490 }
491}
492
493#[cfg(feature = "cache")]
495pub struct SimpleCache {
496 cache_dir: PathBuf,
497}
498
499#[cfg(feature = "cache")]
500impl SimpleCache {
501 pub fn new(cache_dir: PathBuf) -> Self {
503 Self { cache_dir }
504 }
505
506 pub fn init(&self) -> Result<()> {
508 fs::create_dir_all(&self.cache_dir)?;
509 Ok(())
510 }
511
512 pub fn store(&self, key: &str, data: &str) -> Result<()> {
514 let file_path = self.cache_dir.join(format!("{}.txt", key));
515 write_with_lock(&file_path, data.as_bytes())
516 }
517
518 pub fn load(&self, key: &str) -> Result<String> {
520 let file_path = self.cache_dir.join(format!("{}.txt", key));
521 read_with_shared_lock(&file_path).map_err(|err| {
522 if err
523 .downcast_ref::<std::io::Error>()
524 .is_some_and(|io_err| io_err.kind() == std::io::ErrorKind::NotFound)
525 {
526 anyhow::anyhow!("Cache key '{}' not found", key)
527 } else {
528 err
529 }
530 })
531 }
532
533 pub fn exists(&self, key: &str) -> bool {
535 let file_path = self.cache_dir.join(format!("{}.txt", key));
536 file_path.exists()
537 }
538
539 pub fn clear(&self) -> Result<()> {
541 for entry in fs::read_dir(&self.cache_dir)? {
542 let entry = entry?;
543 if entry.path().is_file() {
544 fs::remove_file(entry.path())?;
545 }
546 }
547 Ok(())
548 }
549
550 pub fn list(&self) -> Result<Vec<String>> {
552 let mut entries = Vec::new();
553 for entry in fs::read_dir(&self.cache_dir)? {
554 let entry = entry?;
555 if let Some(name) = entry
556 .path()
557 .file_stem()
558 .and_then(|file_name| file_name.to_str())
559 {
560 entries.push(name.to_string());
561 }
562 }
563 Ok(entries)
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570 use serial_test::serial;
571 use std::sync::{Arc, Barrier};
572 use std::thread;
573 use tempfile::TempDir;
574
575 #[test]
576 fn markdown_storage_roundtrip() {
577 let dir = TempDir::new().expect("temp dir");
578 let storage = MarkdownStorage::new(dir.path().to_path_buf());
579 storage.init().expect("init storage");
580
581 #[derive(Serialize, Deserialize, PartialEq, Debug)]
582 struct Sample {
583 name: String,
584 value: u32,
585 }
586
587 let data = Sample {
588 name: "example".to_string(),
589 value: 42,
590 };
591
592 storage
593 .store("sample", &data, "Sample Data")
594 .expect("store");
595 let loaded: Sample = storage.load("sample").expect("load");
596 assert_eq!(loaded, data);
597 }
598
599 #[test]
600 #[serial]
601 fn concurrent_writes_preserve_integrity() {
602 let dir = TempDir::new().expect("temp dir");
603 let storage = MarkdownStorage::new(dir.path().to_path_buf());
604 storage.init().expect("init storage");
605
606 #[derive(Serialize, Deserialize, PartialEq, Debug)]
607 struct Sample {
608 name: String,
609 value: u32,
610 }
611
612 let barrier = Arc::new(Barrier::new(3));
613 let shared = Arc::new(storage);
614 let key = "concurrent";
615
616 let mut handles = Vec::new();
617 for (name, value) in [("first", 1u32), ("second", 2u32)] {
618 let barrier = barrier.clone();
619 let storage = shared.clone();
620 let key = key.to_string();
621 handles.push(thread::spawn(move || {
622 let data = Sample {
623 name: name.to_string(),
624 value,
625 };
626
627 barrier.wait();
628 storage
629 .store(&key, &data, "Concurrent Sample")
630 .expect("store concurrently");
631 }));
632 }
633
634 barrier.wait();
636
637 for handle in handles {
638 handle.join().expect("join thread");
639 }
640
641 let final_value: Sample = shared
642 .load(key)
643 .expect("load value after concurrent writes");
644
645 assert!(
646 (final_value.name == "first" && final_value.value == 1)
647 || (final_value.name == "second" && final_value.value == 2),
648 "final value should match one of the writers, got {:?}",
649 final_value
650 );
651 }
652}