1use crate::db::{create_pool, run_migrations};
2use crate::error::{IntentError, Result};
3use sqlx::SqlitePool;
4use std::path::PathBuf;
5
6const INTENT_DIR: &str = ".intent-engine";
7const DB_FILE: &str = "project.db";
8
9const PROJECT_ROOT_MARKERS: &[&str] = &[
12 ".git", ".hg", "package.json", "Cargo.toml", "pyproject.toml", "go.mod", "pom.xml", "build.gradle", ];
21
22#[derive(Debug)]
23pub struct ProjectContext {
24 pub root: PathBuf,
25 pub db_path: PathBuf,
26 pub pool: SqlitePool,
27}
28
29impl ProjectContext {
30 pub fn find_project_root() -> Option<PathBuf> {
37 if let Ok(env_path) = std::env::var("INTENT_ENGINE_PROJECT_DIR") {
39 let path = PathBuf::from(env_path);
40 let intent_dir = path.join(INTENT_DIR);
41 if intent_dir.exists() && intent_dir.is_dir() {
42 eprintln!(
43 "✓ Using project from INTENT_ENGINE_PROJECT_DIR: {}",
44 path.display()
45 );
46 return Some(path);
47 } else {
48 eprintln!(
49 "⚠ INTENT_ENGINE_PROJECT_DIR set but no .intent-engine found: {}",
50 path.display()
51 );
52 }
53 }
54
55 if let Ok(mut current) = std::env::current_dir() {
57 let start_dir = current.clone();
58 loop {
59 let intent_dir = current.join(INTENT_DIR);
60 if intent_dir.exists() && intent_dir.is_dir() {
61 if current != start_dir {
62 eprintln!("✓ Found project: {}", current.display());
63 }
64 return Some(current);
65 }
66
67 if !current.pop() {
68 break;
69 }
70 }
71 }
72
73 if let Ok(home) = std::env::var("HOME") {
75 let home_path = PathBuf::from(home);
76 let intent_dir = home_path.join(INTENT_DIR);
77 if intent_dir.exists() && intent_dir.is_dir() {
78 eprintln!("✓ Using home project: {}", home_path.display());
79 return Some(home_path);
80 }
81 }
82
83 #[cfg(target_os = "windows")]
85 if let Ok(userprofile) = std::env::var("USERPROFILE") {
86 let home_path = PathBuf::from(userprofile);
87 let intent_dir = home_path.join(INTENT_DIR);
88 if intent_dir.exists() && intent_dir.is_dir() {
89 eprintln!("✓ Using home project: {}", home_path.display());
90 return Some(home_path);
91 }
92 }
93
94 None
95 }
96
97 fn infer_project_root() -> Option<PathBuf> {
105 let cwd = std::env::current_dir().ok()?;
106 let mut current = cwd.clone();
107
108 loop {
109 for marker in PROJECT_ROOT_MARKERS {
111 let marker_path = current.join(marker);
112 if marker_path.exists() {
113 return Some(current);
114 }
115 }
116
117 if !current.pop() {
119 break;
121 }
122 }
123
124 None
125 }
126
127 pub async fn initialize_project() -> Result<Self> {
134 let cwd = std::env::current_dir()?;
135
136 let root = match Self::infer_project_root() {
138 Some(inferred_root) => {
139 inferred_root
141 },
142 None => {
143 eprintln!(
146 "Warning: Could not determine a project root based on common markers (e.g., .git, package.json).\n\
147 Initialized Intent-Engine in the current directory '{}'.\n\
148 For predictable behavior, it's recommended to initialize from a directory containing a root marker.",
149 cwd.display()
150 );
151 cwd
152 },
153 };
154
155 let intent_dir = root.join(INTENT_DIR);
156 let db_path = intent_dir.join(DB_FILE);
157
158 if !intent_dir.exists() {
160 std::fs::create_dir_all(&intent_dir)?;
161 }
162
163 let pool = create_pool(&db_path).await?;
165
166 run_migrations(&pool).await?;
168
169 Ok(ProjectContext {
170 root,
171 db_path,
172 pool,
173 })
174 }
175
176 pub async fn load() -> Result<Self> {
178 let root = Self::find_project_root().ok_or(IntentError::NotAProject)?;
179 let db_path = root.join(INTENT_DIR).join(DB_FILE);
180
181 let pool = create_pool(&db_path).await?;
182
183 Ok(ProjectContext {
184 root,
185 db_path,
186 pool,
187 })
188 }
189
190 pub async fn load_or_init() -> Result<Self> {
192 match Self::load().await {
193 Ok(ctx) => Ok(ctx),
194 Err(IntentError::NotAProject) => Self::initialize_project().await,
195 Err(e) => Err(e),
196 }
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
209 fn test_constants() {
210 assert_eq!(INTENT_DIR, ".intent-engine");
211 assert_eq!(DB_FILE, "project.db");
212 }
213
214 #[test]
215 fn test_project_context_debug() {
216 let _type_check = |ctx: ProjectContext| {
219 let _ = format!("{:?}", ctx);
220 };
221 }
222
223 #[test]
224 fn test_project_root_markers_list() {
225 assert!(PROJECT_ROOT_MARKERS.contains(&".git"));
227 assert!(PROJECT_ROOT_MARKERS.contains(&"Cargo.toml"));
228 assert!(PROJECT_ROOT_MARKERS.contains(&"package.json"));
229 }
230
231 #[test]
232 fn test_project_root_markers_priority() {
233 assert_eq!(PROJECT_ROOT_MARKERS[0], ".git");
235 }
236
237 #[test]
240 fn test_infer_project_root_with_git() {
241 assert!(PROJECT_ROOT_MARKERS.contains(&".git"));
245 }
246
247 #[test]
249 fn test_all_major_project_types_covered() {
250 let markers = PROJECT_ROOT_MARKERS;
251
252 assert!(markers.contains(&".git"));
254 assert!(markers.contains(&".hg"));
255
256 assert!(markers.contains(&"Cargo.toml")); assert!(markers.contains(&"package.json")); assert!(markers.contains(&"pyproject.toml")); assert!(markers.contains(&"go.mod")); assert!(markers.contains(&"pom.xml")); assert!(markers.contains(&"build.gradle")); }
264}