intent_engine/project.rs
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
9/// Project root markers in priority order (highest priority first)
10/// These are used to identify the root directory of a project
11const PROJECT_ROOT_MARKERS: &[&str] = &[
12 ".git", // Git (highest priority)
13 ".hg", // Mercurial
14 "package.json", // Node.js
15 "Cargo.toml", // Rust
16 "pyproject.toml", // Python (PEP 518)
17 "go.mod", // Go Modules
18 "pom.xml", // Maven (Java)
19 "build.gradle", // Gradle (Java/Kotlin)
20];
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 /// Find the project root by searching upwards for .intent-engine directory
31 pub fn find_project_root() -> Option<PathBuf> {
32 let mut current = std::env::current_dir().ok()?;
33
34 loop {
35 let intent_dir = current.join(INTENT_DIR);
36 if intent_dir.exists() && intent_dir.is_dir() {
37 return Some(current);
38 }
39
40 if !current.pop() {
41 break;
42 }
43 }
44
45 None
46 }
47
48 /// Infer the project root directory based on common project markers
49 ///
50 /// This function implements a smart algorithm to find the project root:
51 /// 1. Start from current directory and traverse upwards
52 /// 2. Check each directory for project markers (in priority order)
53 /// 3. Return the first directory that contains any marker
54 /// 4. If no marker found, return None (fallback to CWD handled by caller)
55 fn infer_project_root() -> Option<PathBuf> {
56 let cwd = std::env::current_dir().ok()?;
57 let mut current = cwd.clone();
58
59 loop {
60 // Check if any marker exists in current directory
61 for marker in PROJECT_ROOT_MARKERS {
62 let marker_path = current.join(marker);
63 if marker_path.exists() {
64 return Some(current);
65 }
66 }
67
68 // Try to move up to parent directory
69 if !current.pop() {
70 // Reached filesystem root without finding any marker
71 break;
72 }
73 }
74
75 None
76 }
77
78 /// Initialize a new Intent-Engine project using smart root inference
79 ///
80 /// This function implements the smart lazy initialization algorithm:
81 /// 1. Try to infer project root based on common markers
82 /// 2. If inference succeeds, initialize in the inferred root
83 /// 3. If inference fails, fallback to CWD and print warning to stderr
84 pub async fn initialize_project() -> Result<Self> {
85 let cwd = std::env::current_dir()?;
86
87 // Try to infer the project root
88 let root = match Self::infer_project_root() {
89 Some(inferred_root) => {
90 // Successfully inferred project root
91 inferred_root
92 },
93 None => {
94 // Fallback: use current working directory
95 // Print warning to stderr
96 eprintln!(
97 "Warning: Could not determine a project root based on common markers (e.g., .git, package.json).\n\
98 Initialized Intent-Engine in the current directory '{}'.\n\
99 For predictable behavior, it's recommended to initialize from a directory containing a root marker.",
100 cwd.display()
101 );
102 cwd
103 },
104 };
105
106 let intent_dir = root.join(INTENT_DIR);
107 let db_path = intent_dir.join(DB_FILE);
108
109 // Create .intent-engine directory if it doesn't exist
110 if !intent_dir.exists() {
111 std::fs::create_dir_all(&intent_dir)?;
112 }
113
114 // Create database connection
115 let pool = create_pool(&db_path).await?;
116
117 // Run migrations
118 run_migrations(&pool).await?;
119
120 Ok(ProjectContext {
121 root,
122 db_path,
123 pool,
124 })
125 }
126
127 /// Load an existing project context
128 pub async fn load() -> Result<Self> {
129 let root = Self::find_project_root().ok_or(IntentError::NotAProject)?;
130 let db_path = root.join(INTENT_DIR).join(DB_FILE);
131
132 let pool = create_pool(&db_path).await?;
133
134 Ok(ProjectContext {
135 root,
136 db_path,
137 pool,
138 })
139 }
140
141 /// Load project context, initializing if necessary (for write commands)
142 pub async fn load_or_init() -> Result<Self> {
143 match Self::load().await {
144 Ok(ctx) => Ok(ctx),
145 Err(IntentError::NotAProject) => Self::initialize_project().await,
146 Err(e) => Err(e),
147 }
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 // Note: Tests that modify the current directory are intentionally limited
156 // because they can interfere with other tests running in parallel.
157 // These functionalities are thoroughly tested by integration tests.
158
159 #[test]
160 fn test_constants() {
161 assert_eq!(INTENT_DIR, ".intent-engine");
162 assert_eq!(DB_FILE, "project.db");
163 }
164
165 #[test]
166 fn test_project_context_debug() {
167 // Just verify that ProjectContext implements Debug
168 // We can't easily create one without side effects in a unit test
169 let _type_check = |ctx: ProjectContext| {
170 let _ = format!("{:?}", ctx);
171 };
172 }
173
174 #[test]
175 fn test_project_root_markers_list() {
176 // Verify that the markers list contains expected markers
177 assert!(PROJECT_ROOT_MARKERS.contains(&".git"));
178 assert!(PROJECT_ROOT_MARKERS.contains(&"Cargo.toml"));
179 assert!(PROJECT_ROOT_MARKERS.contains(&"package.json"));
180 }
181
182 #[test]
183 fn test_project_root_markers_priority() {
184 // Verify that .git has highest priority (comes first)
185 assert_eq!(PROJECT_ROOT_MARKERS[0], ".git");
186 }
187
188 /// Test infer_project_root in an isolated environment
189 /// Note: This test creates a temporary directory structure but doesn't change CWD
190 #[test]
191 fn test_infer_project_root_with_git() {
192 // This test is limited because we can't easily change CWD in unit tests
193 // The actual behavior is tested in integration tests
194 // Here we just verify the marker list is correct
195 assert!(PROJECT_ROOT_MARKERS.contains(&".git"));
196 }
197
198 /// Test that markers list includes all major project types
199 #[test]
200 fn test_all_major_project_types_covered() {
201 let markers = PROJECT_ROOT_MARKERS;
202
203 // Git version control
204 assert!(markers.contains(&".git"));
205 assert!(markers.contains(&".hg"));
206
207 // Programming languages
208 assert!(markers.contains(&"Cargo.toml")); // Rust
209 assert!(markers.contains(&"package.json")); // Node.js
210 assert!(markers.contains(&"pyproject.toml")); // Python
211 assert!(markers.contains(&"go.mod")); // Go
212 assert!(markers.contains(&"pom.xml")); // Java (Maven)
213 assert!(markers.contains(&"build.gradle")); // Java/Kotlin (Gradle)
214 }
215}