whoseportisitanyway 1.1.2

Cross-platform TUI for discovering which ports are in use, who owns them, and what they're for
Documentation
use std::path::Path;

use crate::model::Project;

const PROJECT_MARKERS: &[&str] = &[
    "package.json",
    "Cargo.toml",
    "go.mod",
    "Gemfile",
    "requirements.txt",
    "pyproject.toml",
    "setup.py",
    "setup.cfg",
    "Pipfile",
    "mix.exs",
    "build.gradle",
    "build.gradle.kts",
    "pom.xml",
    "composer.json",
    "pubspec.yaml",
    "deno.json",
    "deno.jsonc",
    "bun.lockb",
    "Makefile",
    ".git",
    "CMakeLists.txt",
    "meson.build",
    "flake.nix",
    "cabal.project",
    "stack.yaml",
    "shard.yml",
    "Project.toml",
    "rebar.config",
];

pub fn detect_project(cwd: &Path) -> Option<Project> {
    let mut dir = cwd.to_path_buf();
    loop {
        for marker in PROJECT_MARKERS {
            if dir.join(marker).exists() {
                let name = dir
                    .file_name()
                    .map(|n| n.to_string_lossy().to_string())
                    .unwrap_or_else(|| "unknown".to_string());
                return Some(Project {
                    name,
                    root: dir,
                    framework: None,
                });
            }
        }
        if !dir.pop() {
            return None;
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::tempdir;

    #[test]
    fn detects_cargo_project() {
        let dir = tempdir().unwrap();
        std::fs::write(dir.path().join("Cargo.toml"), "").unwrap();
        let project = detect_project(dir.path()).unwrap();
        assert_eq!(project.root, dir.path());
        assert!(project.framework.is_none());
    }

    #[test]
    fn detects_node_project() {
        let dir = tempdir().unwrap();
        std::fs::write(dir.path().join("package.json"), "{}").unwrap();
        let project = detect_project(dir.path()).unwrap();
        assert_eq!(project.root, dir.path());
    }

    #[test]
    fn detects_go_project() {
        let dir = tempdir().unwrap();
        std::fs::write(dir.path().join("go.mod"), "").unwrap();
        let project = detect_project(dir.path()).unwrap();
        assert_eq!(project.root, dir.path());
    }

    #[test]
    fn detects_python_project() {
        let dir = tempdir().unwrap();
        std::fs::write(dir.path().join("requirements.txt"), "").unwrap();
        let project = detect_project(dir.path()).unwrap();
        assert_eq!(project.root, dir.path());
    }

    #[test]
    fn detects_git_repo() {
        let dir = tempdir().unwrap();
        std::fs::create_dir(dir.path().join(".git")).unwrap();
        let project = detect_project(dir.path()).unwrap();
        assert_eq!(project.root, dir.path());
    }

    #[test]
    fn walks_up_to_parent() {
        let dir = tempdir().unwrap();
        std::fs::write(dir.path().join("Cargo.toml"), "").unwrap();
        let sub = dir.path().join("src");
        std::fs::create_dir(&sub).unwrap();
        let project = detect_project(&sub).unwrap();
        assert_eq!(project.root, dir.path());
    }

    #[test]
    fn project_name_from_dir_name() {
        let dir = tempdir().unwrap();
        std::fs::write(dir.path().join("package.json"), "").unwrap();
        let project = detect_project(dir.path()).unwrap();
        assert!(!project.name.is_empty());
    }

    #[test]
    fn returns_none_when_no_markers() {
        let dir = tempdir().unwrap();
        assert!(detect_project(dir.path()).is_none());
    }

    #[test]
    fn all_markers_recognized() {
        for marker in PROJECT_MARKERS {
            let dir = tempdir().unwrap();
            let path = dir.path().join(marker);
            if marker.contains('.') || !marker.starts_with('.') {
                std::fs::write(&path, "").unwrap();
            } else {
                std::fs::create_dir(&path).unwrap();
            }
            assert!(
                detect_project(dir.path()).is_some(),
                "marker {} not detected",
                marker
            );
        }
    }
}