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}