use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Default)]
pub struct ProjectProfile {
pub language: String,
pub framework: String,
pub package_manager: String,
pub test_framework: String,
pub ci_type: String,
}
#[derive(Debug, Clone, Default)]
pub struct ProjectContext {
pub root: Option<PathBuf>,
pub agent_context: Option<String>,
pub project_config: Option<String>,
pub cwd: PathBuf,
pub profile: ProjectProfile,
}
const CONTEXT_FILES: &[&str] = &[
"AGENTS.md",
"CLAUDE.md",
"GEMINI.md",
".cursorrules",
".cortex/config.yaml",
];
pub fn discover_project_root(cwd: &Path) -> Option<PathBuf> {
let mut current = Some(cwd.to_path_buf());
while let Some(dir) = current {
for filename in CONTEXT_FILES {
let candidate = dir.join(filename);
if candidate.exists() {
return Some(dir.clone());
}
}
current = dir.parent().map(|p| p.to_path_buf());
}
None
}
pub fn load_project_context(cwd: &Path) -> ProjectContext {
let mut ctx = ProjectContext {
cwd: cwd.to_path_buf(),
..Default::default()
};
let root = discover_project_root(cwd);
ctx.root = root.clone();
if let Some(ref root_dir) = root {
for filename in &CONTEXT_FILES[..4] {
let path = root_dir.join(filename);
if path.exists() {
if let Ok(content) = std::fs::read_to_string(&path) {
let trimmed = content.trim().to_string();
if !trimmed.is_empty() {
ctx.agent_context = Some(trimmed);
break; }
}
}
}
let proj_config = root_dir.join(".cortex/config.yaml");
if proj_config.exists() {
if let Ok(content) = std::fs::read_to_string(&proj_config) {
ctx.project_config = Some(content);
}
}
}
ctx.profile = detect_project_profile(root.as_deref().unwrap_or(cwd));
ctx
}
fn detect_project_profile(project_dir: &Path) -> ProjectProfile {
let mut p = ProjectProfile::default();
if project_dir.join("Cargo.toml").exists() {
p.language = "Rust".into();
p.package_manager = "cargo".into();
p.test_framework = "cargo test".into();
if let Ok(content) = std::fs::read_to_string(project_dir.join("Cargo.toml")) {
if content.contains("actix") { p.framework = "Actix Web".into(); }
else if content.contains("axum") { p.framework = "Axum".into(); }
else if content.contains("rocket") { p.framework = "Rocket".into(); }
else if content.contains("clap") { p.framework = "CLI (clap)".into(); }
}
}
else if project_dir.join("package.json").exists() {
p.language = "JavaScript".into();
p.package_manager = if project_dir.join("yarn.lock").exists() { "yarn".into() }
else if project_dir.join("pnpm-lock.yaml").exists() { "pnpm".into() }
else { "npm".into() };
if let Ok(content) = std::fs::read_to_string(project_dir.join("package.json")) {
if content.contains("\"next\"") { p.framework = "Next.js".into(); p.language = "TypeScript".into(); }
else if content.contains("\"react\"") { p.framework = "React".into(); }
else if content.contains("\"vue\"") { p.framework = "Vue".into(); }
else if content.contains("\"express\"") { p.framework = "Express".into(); }
if content.contains("\"jest\"") { p.test_framework = "Jest".into(); }
else if content.contains("\"vitest\"") { p.test_framework = "Vitest".into(); }
else if content.contains("\"mocha\"") { p.test_framework = "Mocha".into(); }
}
if p.test_framework.is_empty() { p.test_framework = "npm test".into(); }
}
else if project_dir.join("pyproject.toml").exists() || project_dir.join("requirements.txt").exists() || project_dir.join("setup.py").exists() {
p.language = "Python".into();
p.package_manager = if project_dir.join("uv.lock").exists() || project_dir.join("pyproject.toml").exists() { "uv/pip".into() }
else if project_dir.join("Pipfile").exists() { "pipenv".into() }
else { "pip".into() };
if project_dir.join("pyproject.toml").exists() {
if let Ok(content) = std::fs::read_to_string(project_dir.join("pyproject.toml")) {
if content.contains("django") { p.framework = "Django".into(); }
else if content.contains("flask") { p.framework = "Flask".into(); }
else if content.contains("fastapi") { p.framework = "FastAPI".into(); }
if content.contains("pytest") { p.test_framework = "pytest".into(); }
}
}
if p.test_framework.is_empty() { p.test_framework = "pytest".into(); }
}
else if project_dir.join("go.mod").exists() {
p.language = "Go".into();
p.package_manager = "go mod".into();
p.test_framework = "go test".into();
if let Ok(content) = std::fs::read_to_string(project_dir.join("go.mod")) {
if content.contains("gin-gonic") { p.framework = "Gin".into(); }
else if content.contains("fiber") { p.framework = "Fiber".into(); }
else if content.contains("echo") { p.framework = "Echo".into(); }
}
}
else if project_dir.join("pom.xml").exists() {
p.language = "Java".into(); p.package_manager = "maven".into(); p.test_framework = "JUnit".into();
}
else if project_dir.join("build.gradle").exists() || project_dir.join("build.gradle.kts").exists() {
p.language = if project_dir.join("build.gradle.kts").exists() { "Kotlin".into() } else { "Java".into() };
p.package_manager = "gradle".into(); p.test_framework = "JUnit".into();
}
if project_dir.join(".github/workflows").exists() { p.ci_type = "GitHub Actions".into(); }
else if project_dir.join(".gitlab-ci.yml").exists() { p.ci_type = "GitLab CI".into(); }
else if project_dir.join("Jenkinsfile").exists() { p.ci_type = "Jenkins".into(); }
else if project_dir.join("Dockerfile").exists() { p.ci_type = "Docker".into(); }
p
}
pub fn format_project_label(profile: &ProjectProfile) -> String {
let mut parts: Vec<&str> = Vec::new();
if !profile.language.is_empty() { parts.push(&profile.language); }
if !profile.framework.is_empty() { parts.push(&profile.framework); }
if !profile.package_manager.is_empty() { parts.push(&profile.package_manager); }
if parts.is_empty() { return "no project".into(); }
parts.join(" ยท ")
}
pub fn format_project_context(ctx: &ProjectContext) -> String {
let mut parts: Vec<String> = Vec::new();
if let Some(ref root) = ctx.root {
parts.push(format!("## Project Context\n- Project root: {}", root.display()));
}
if let Some(ref agent_ctx) = ctx.agent_context {
parts.push(format!("\n### Project Guidelines\n{}", agent_ctx));
}
if ctx.project_config.is_some() {
parts.push("\n*Project config overrides active*".to_string());
}
parts.join("\n")
}