next_plaid_cli/index/
paths.rs1use std::fs;
9use std::path::{Path, PathBuf};
10
11use anyhow::{Context, Result};
12use serde::{Deserialize, Serialize};
13use xxhash_rust::xxh3::xxh3_64;
14
15const STATE_FILE: &str = "state.json";
16const PROJECT_FILE: &str = "project.json";
17const INDEX_SUBDIR: &str = "index";
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ProjectMetadata {
22 pub project_path: PathBuf,
24 pub project_name: String,
26}
27
28impl ProjectMetadata {
29 pub fn new(project_path: &Path) -> Self {
30 let project_name = project_path
31 .file_name()
32 .map(|n| n.to_string_lossy().to_string())
33 .unwrap_or_else(|| "project".to_string());
34
35 Self {
36 project_path: project_path.to_path_buf(),
37 project_name,
38 }
39 }
40
41 pub fn load(index_dir: &Path) -> Result<Self> {
42 let path = index_dir.join(PROJECT_FILE);
43 let content = fs::read_to_string(&path)
44 .with_context(|| format!("Failed to read {}", path.display()))?;
45 Ok(serde_json::from_str(&content)?)
46 }
47
48 pub fn save(&self, index_dir: &Path) -> Result<()> {
49 fs::create_dir_all(index_dir)?;
50 let path = index_dir.join(PROJECT_FILE);
51 let content = serde_json::to_string_pretty(self)?;
52 fs::write(&path, content)?;
53 Ok(())
54 }
55}
56
57pub fn get_plaid_data_dir() -> Result<PathBuf> {
59 let data_dir = dirs::data_dir().context("Could not determine data directory")?;
60 Ok(data_dir.join("plaid").join("indices"))
61}
62
63fn compute_index_dir_name(project_path: &Path) -> String {
66 let path_str = project_path.to_string_lossy();
67 let hash = xxh3_64(path_str.as_bytes());
68 let hash_prefix = format!("{:08x}", hash).chars().take(8).collect::<String>();
69
70 let project_name = project_path
71 .file_name()
72 .map(|n| n.to_string_lossy().to_string())
73 .unwrap_or_else(|| "project".to_string());
74
75 let sanitized_name: String = project_name
77 .chars()
78 .map(|c| {
79 if c.is_alphanumeric() || c == '-' || c == '_' {
80 c
81 } else {
82 '_'
83 }
84 })
85 .collect();
86
87 format!("{}-{}", sanitized_name, hash_prefix)
88}
89
90pub fn get_index_dir_for_project(project_path: &Path) -> Result<PathBuf> {
93 let base_dir = get_plaid_data_dir()?;
94 let dir_name = compute_index_dir_name(project_path);
95 Ok(base_dir.join(dir_name))
96}
97
98pub fn find_index_for_project(project_path: &Path) -> Result<Option<PathBuf>> {
101 let index_dir = get_index_dir_for_project(project_path)?;
102
103 let metadata_path = index_dir.join(INDEX_SUBDIR).join("metadata.json");
105 if metadata_path.exists() {
106 if let Ok(meta) = ProjectMetadata::load(&index_dir) {
108 if meta.project_path == project_path {
109 return Ok(Some(index_dir));
110 }
111 }
112 return Ok(Some(index_dir));
115 }
116
117 Ok(None)
118}
119
120pub fn index_exists(project_path: &Path) -> bool {
122 matches!(find_index_for_project(project_path), Ok(Some(_)))
123}
124
125#[derive(Debug, Clone)]
127pub struct ParentIndexInfo {
128 pub index_dir: PathBuf,
130 pub project_path: PathBuf,
132 pub relative_subdir: PathBuf,
134}
135
136pub fn find_parent_index(search_path: &Path) -> Result<Option<ParentIndexInfo>> {
139 let data_dir = get_plaid_data_dir()?;
140
141 if !data_dir.exists() {
142 return Ok(None);
143 }
144
145 let mut best_match: Option<ParentIndexInfo> = None;
146 let mut best_depth = 0;
147
148 for entry in fs::read_dir(&data_dir)?.filter_map(|e| e.ok()) {
149 let index_dir = entry.path();
150 if !index_dir.is_dir() {
151 continue;
152 }
153
154 if let Ok(meta) = ProjectMetadata::load(&index_dir) {
156 if search_path != meta.project_path {
159 if let Ok(relative) = search_path.strip_prefix(&meta.project_path) {
160 let depth = meta.project_path.components().count();
162 if depth > best_depth {
163 best_depth = depth;
164 best_match = Some(ParentIndexInfo {
165 index_dir,
166 project_path: meta.project_path,
167 relative_subdir: relative.to_path_buf(),
168 });
169 }
170 }
171 }
172 }
173 }
174
175 Ok(best_match)
176}
177
178pub fn get_state_path(index_dir: &Path) -> PathBuf {
180 index_dir.join(STATE_FILE)
181}
182
183pub fn get_vector_index_path(index_dir: &Path) -> PathBuf {
185 index_dir.join(INDEX_SUBDIR)
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn test_compute_index_dir_name() {
194 let path = PathBuf::from("/Users/foo/myproject");
195 let name = compute_index_dir_name(&path);
196 assert!(name.starts_with("myproject-"));
198 assert_eq!(name.len(), "myproject-".len() + 8);
199 }
200
201 #[test]
202 fn test_compute_index_dir_name_with_special_chars() {
203 let path = PathBuf::from("/Users/foo/my project (1)");
204 let name = compute_index_dir_name(&path);
205 assert!(name.starts_with("my_project__1_-"));
207 }
208
209 #[test]
210 fn test_different_paths_different_hashes() {
211 let path1 = PathBuf::from("/Users/foo/project1");
212 let path2 = PathBuf::from("/Users/foo/project2");
213 let name1 = compute_index_dir_name(&path1);
214 let name2 = compute_index_dir_name(&path2);
215 assert_ne!(name1, name2);
216 }
217}