Skip to main content

forgekit_core/project/
mod.rs

1use std::path::{Path, PathBuf};
2
3use crate::edit::EditModule;
4use crate::error::{ForgeError, Result};
5use crate::storage::UnifiedGraphStore;
6use crate::types::Language;
7
8#[derive(Debug, Clone)]
9pub struct ProjectInfo {
10    pub root: PathBuf,
11    pub language: Language,
12    pub entry_point: PathBuf,
13    pub manifest: Option<PathBuf>,
14    pub source_dir: PathBuf,
15}
16
17pub struct ProjectModule {
18    store: std::sync::Arc<UnifiedGraphStore>,
19}
20
21impl ProjectModule {
22    pub fn new(store: std::sync::Arc<UnifiedGraphStore>) -> Self {
23        Self { store }
24    }
25
26    pub async fn scaffold(&self, name: &str, language: Language) -> Result<ProjectInfo> {
27        let project_root = self.store.codebase_path.join(name);
28        if project_root.exists() {
29            return Err(ForgeError::FileAlreadyExists(project_root));
30        }
31
32        let template = project_template(&language, name);
33        let edit = EditModule::new(self.store.clone());
34
35        for (rel_path, content) in &template.files {
36            edit.create_file(Path::new(&format!("{}/{}", name, rel_path)), content)
37                .await?;
38        }
39
40        Ok(ProjectInfo {
41            root: project_root.clone(),
42            language,
43            entry_point: project_root.join(&template.entry_point),
44            manifest: template.manifest.map(|m| project_root.join(m)),
45            source_dir: project_root.join(&template.source_dir),
46        })
47    }
48
49    pub fn detect(&self) -> Option<ProjectInfo> {
50        let root = &self.store.codebase_path;
51        detect_project(root)
52    }
53}
54
55fn detect_project(root: &Path) -> Option<ProjectInfo> {
56    let lang_and_manifest: Option<(Language, PathBuf)> = if root.join("Cargo.toml").exists() {
57        Some((Language::Rust, root.join("Cargo.toml")))
58    } else if root.join("go.mod").exists() {
59        Some((Language::Go, root.join("go.mod")))
60    } else if root.join("pom.xml").exists() {
61        Some((Language::Java, root.join("pom.xml")))
62    } else if root.join("package.json").exists() {
63        let ext = if root.join("tsconfig.json").exists() {
64            Language::TypeScript
65        } else {
66            Language::JavaScript
67        };
68        Some((ext, root.join("package.json")))
69    } else if root.join("Makefile").exists() || root.join("makefile").exists() {
70        Some((Language::C, root.join("Makefile")))
71    } else if root.join("pyproject.toml").exists() || root.join("setup.py").exists() {
72        Some((Language::Python, root.join("pyproject.toml")))
73    } else {
74        None
75    };
76
77    let (language, manifest) = lang_and_manifest?;
78
79    let (source_dir, entry_point) = match &language {
80        Language::Rust => ("src".to_string(), "src/main.rs".to_string()),
81        Language::Python => ("src".to_string(), "src/main.py".to_string()),
82        Language::Java => (
83            "src/main/java".to_string(),
84            "src/main/java/Main.java".to_string(),
85        ),
86        Language::C => ("src".to_string(), "src/main.c".to_string()),
87        Language::TypeScript | Language::JavaScript => {
88            ("src".to_string(), "src/index.ts".to_string())
89        }
90        Language::Go => (".".to_string(), "main.go".to_string()),
91        _ => ("src".to_string(), "src/main".to_string()),
92    };
93
94    Some(ProjectInfo {
95        root: root.to_path_buf(),
96        language,
97        entry_point: root.join(&entry_point),
98        manifest: Some(manifest),
99        source_dir: root.join(&source_dir),
100    })
101}
102
103struct ProjectTemplate {
104    files: Vec<(String, String)>,
105    entry_point: String,
106    manifest: Option<String>,
107    source_dir: String,
108}
109
110fn project_template(lang: &Language, name: &str) -> ProjectTemplate {
111    match lang {
112        Language::Rust => ProjectTemplate {
113            files: vec![
114                (
115                    "Cargo.toml".to_string(),
116                    format!(
117                        "[package]\nname = \"{}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\n",
118                        name
119                    ),
120                ),
121                ("src/main.rs".to_string(), "fn main() {\n    println!(\"Hello from {}!\");\n}\n".replace("{}", name)),
122            ],
123            entry_point: "src/main.rs".to_string(),
124            manifest: Some("Cargo.toml".to_string()),
125            source_dir: "src".to_string(),
126        },
127        Language::Python => ProjectTemplate {
128            files: vec![
129                (
130                    "pyproject.toml".to_string(),
131                    format!(
132                        "[project]\nname = \"{}\"\nversion = \"0.1.0\"\nrequires-python = \">=3.8\"\n",
133                        name
134                    ),
135                ),
136                ("src/__init__.py".to_string(), String::new()),
137                (
138                    "src/main.py".to_string(),
139                    "def main():\n    print(\"Hello!\")\n\nif __name__ == \"__main__\":\n    main()\n".to_string(),
140                ),
141            ],
142            entry_point: "src/main.py".to_string(),
143            manifest: Some("pyproject.toml".to_string()),
144            source_dir: "src".to_string(),
145        },
146        Language::Java => ProjectTemplate {
147            files: vec![
148                (
149                    "pom.xml".to_string(),
150                    format!(
151                        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project>\n  <modelVersion>4.0.0</modelVersion>\n  <groupId>com.example</groupId>\n  <artifactId>{}</artifactId>\n  <version>0.1.0</version>\n</project>\n",
152                        name
153                    ),
154                ),
155                (
156                    "src/main/java/Main.java".to_string(),
157                    "public class Main {\n    public static void main(String[] args) {\n        System.out.println(\"Hello!\");\n    }\n}\n".to_string(),
158                ),
159            ],
160            entry_point: "src/main/java/Main.java".to_string(),
161            manifest: Some("pom.xml".to_string()),
162            source_dir: "src/main/java".to_string(),
163        },
164        Language::C => ProjectTemplate {
165            files: vec![
166                (
167                    "Makefile".to_string(),
168                    format!("CC = gcc\nCFLAGS = -Wall -Wextra\n\n{}.out: src/main.o\n\t$(CC) $(CFLAGS) -o $@ $^\n\nsrc/main.o: src/main.c\n\t$(CC) $(CFLAGS) -c -o $@ $<\n\nclean:\n\trm -f *.out src/*.o\n", name),
169                ),
170                (
171                    "src/main.c".to_string(),
172                    "#include <stdio.h>\n\nint main(void) {\n    printf(\"Hello!\\n\");\n    return 0;\n}\n".to_string(),
173                ),
174                (
175                    "include/.gitkeep".to_string(),
176                    String::new(),
177                ),
178            ],
179            entry_point: "src/main.c".to_string(),
180            manifest: Some("Makefile".to_string()),
181            source_dir: "src".to_string(),
182        },
183        Language::TypeScript => ProjectTemplate {
184            files: vec![
185                (
186                    "package.json".to_string(),
187                    format!(
188                        "{{\"name\": \"{}\", \"version\": \"0.1.0\", \"main\": \"src/index.ts\", \"scripts\": {{\"build\": \"tsc\", \"test\": \"echo \\\"no tests\\\"\"}}}}\n",
189                        name
190                    ),
191                ),
192                (
193                    "tsconfig.json".to_string(),
194                    "{{\"compilerOptions\": {{\"target\": \"ES2020\", \"module\": \"commonjs\", \"outDir\": \"./dist\", \"strict\": true}}, \"include\": [\"src/**/*\"]}}\n".to_string(),
195                ),
196                (
197                    "src/index.ts".to_string(),
198                    "console.log(\"Hello!\");\n".to_string(),
199                ),
200            ],
201            entry_point: "src/index.ts".to_string(),
202            manifest: Some("package.json".to_string()),
203            source_dir: "src".to_string(),
204        },
205        _ => ProjectTemplate {
206            files: vec![("README.md".to_string(), format!("# {}\n", name))],
207            entry_point: "README.md".to_string(),
208            manifest: None,
209            source_dir: ".".to_string(),
210        },
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use crate::storage::BackendKind;
218
219    async fn make_store(dir: &Path) -> std::sync::Arc<UnifiedGraphStore> {
220        std::sync::Arc::new(
221            UnifiedGraphStore::open_with_path(dir, dir.join("test.db"), BackendKind::default())
222                .await
223                .unwrap(),
224        )
225    }
226
227    #[tokio::test]
228    async fn test_scaffold_rust() {
229        let temp = tempfile::tempdir().unwrap();
230        let store = make_store(temp.path()).await;
231        let module = ProjectModule::new(store);
232
233        let info = module.scaffold("my-lib", Language::Rust).await.unwrap();
234        assert!(info.root.ends_with("my-lib"));
235        assert_eq!(info.language, Language::Rust);
236        assert!(info.entry_point.ends_with("src/main.rs"));
237        assert!(info.manifest.is_some());
238        assert!(temp.path().join("my-lib/Cargo.toml").exists());
239        assert!(temp.path().join("my-lib/src/main.rs").exists());
240    }
241
242    #[tokio::test]
243    async fn test_scaffold_python() {
244        let temp = tempfile::tempdir().unwrap();
245        let store = make_store(temp.path()).await;
246        let module = ProjectModule::new(store);
247
248        let info = module.scaffold("my-py", Language::Python).await.unwrap();
249        assert_eq!(info.language, Language::Python);
250        assert!(temp.path().join("my-py/pyproject.toml").exists());
251        assert!(temp.path().join("my-py/src/__init__.py").exists());
252        assert!(temp.path().join("my-py/src/main.py").exists());
253    }
254
255    #[tokio::test]
256    async fn test_scaffold_java() {
257        let temp = tempfile::tempdir().unwrap();
258        let store = make_store(temp.path()).await;
259        let module = ProjectModule::new(store);
260
261        let info = module.scaffold("my-java", Language::Java).await.unwrap();
262        assert_eq!(info.language, Language::Java);
263        assert!(temp.path().join("my-java/pom.xml").exists());
264        assert!(temp.path().join("my-java/src/main/java/Main.java").exists());
265    }
266
267    #[tokio::test]
268    async fn test_scaffold_c() {
269        let temp = tempfile::tempdir().unwrap();
270        let store = make_store(temp.path()).await;
271        let module = ProjectModule::new(store);
272
273        let info = module.scaffold("my-c", Language::C).await.unwrap();
274        assert_eq!(info.language, Language::C);
275        assert!(temp.path().join("my-c/Makefile").exists());
276        assert!(temp.path().join("my-c/src/main.c").exists());
277    }
278
279    #[tokio::test]
280    async fn test_scaffold_typescript() {
281        let temp = tempfile::tempdir().unwrap();
282        let store = make_store(temp.path()).await;
283        let module = ProjectModule::new(store);
284
285        let info = module
286            .scaffold("my-ts", Language::TypeScript)
287            .await
288            .unwrap();
289        assert_eq!(info.language, Language::TypeScript);
290        assert!(temp.path().join("my-ts/package.json").exists());
291        assert!(temp.path().join("my-ts/tsconfig.json").exists());
292        assert!(temp.path().join("my-ts/src/index.ts").exists());
293    }
294
295    #[tokio::test]
296    async fn test_scaffold_rejects_existing() {
297        let temp = tempfile::tempdir().unwrap();
298        tokio::fs::create_dir(temp.path().join("already-here"))
299            .await
300            .unwrap();
301        let store = make_store(temp.path()).await;
302        let module = ProjectModule::new(store);
303
304        let result = module.scaffold("already-here", Language::Rust).await;
305        assert!(result.is_err());
306    }
307
308    #[test]
309    fn test_detect_rust_project() {
310        let temp = tempfile::tempdir().unwrap();
311        std::fs::write(
312            temp.path().join("Cargo.toml"),
313            "[package]\nname = \"test\"\n",
314        )
315        .unwrap();
316        let info = detect_project(temp.path()).unwrap();
317        assert_eq!(info.language, Language::Rust);
318    }
319
320    #[test]
321    fn test_detect_python_project() {
322        let temp = tempfile::tempdir().unwrap();
323        std::fs::write(temp.path().join("pyproject.toml"), "[project]\n").unwrap();
324        let info = detect_project(temp.path()).unwrap();
325        assert_eq!(info.language, Language::Python);
326    }
327
328    #[test]
329    fn test_detect_typescript_over_js() {
330        let temp = tempfile::tempdir().unwrap();
331        std::fs::write(temp.path().join("package.json"), "{}").unwrap();
332        std::fs::write(temp.path().join("tsconfig.json"), "{}").unwrap();
333        let info = detect_project(temp.path()).unwrap();
334        assert_eq!(info.language, Language::TypeScript);
335    }
336
337    #[test]
338    fn test_detect_nothing() {
339        let temp = tempfile::tempdir().unwrap();
340        assert!(detect_project(temp.path()).is_none());
341    }
342}