use anyhow::Result;
use ignore::WalkBuilder;
use std::path::{Path, PathBuf};
use super::profile::{Language, LanguageInfo, RepoProfile, Service, Tool, ToolInfo};
pub struct Scanner {
root_path: PathBuf,
}
impl Scanner {
pub fn new(path: &Path) -> Self {
Self {
root_path: path.to_path_buf(),
}
}
pub fn scan(&self) -> Result<RepoProfile> {
let mut profile = RepoProfile::default();
self.scan_manifests(&mut profile)?;
self.scan_configs(&mut profile)?;
self.scan_source_files(&mut profile)?;
self.scan_services(&mut profile)?;
self.apply_smart_inference(&mut profile)?;
Ok(profile)
}
fn scan_manifests(&self, profile: &mut RepoProfile) -> Result<()> {
if self.root_path.join("package.json").exists() {
let detected_by = vec!["package.json".to_string()];
let version = self.read_node_version()?;
profile.add_language(
Language::JavaScript,
LanguageInfo {
detected_by,
version,
file_count: 0, },
);
if self.root_path.join("pnpm-lock.yaml").exists() {
profile.add_tool(
Tool::Pnpm,
ToolInfo {
detected_by: vec!["pnpm-lock.yaml".to_string()],
version: self.extract_pnpm_version()?,
},
);
} else if self.root_path.join("yarn.lock").exists() {
profile.add_tool(
Tool::Yarn,
ToolInfo {
detected_by: vec!["yarn.lock".to_string()],
version: None,
},
);
} else if self.root_path.join("package-lock.json").exists() {
profile.add_tool(
Tool::Npm,
ToolInfo {
detected_by: vec!["package-lock.json".to_string()],
version: None,
},
);
}
}
if self.root_path.join("Cargo.toml").exists() {
let detected_by = vec!["Cargo.toml".to_string()];
let version = self.read_rust_version()?;
profile.add_language(
Language::Rust,
LanguageInfo {
detected_by,
version,
file_count: 0,
},
);
}
if self.root_path.join("requirements.txt").exists()
|| self.root_path.join("pyproject.toml").exists()
|| self.root_path.join("setup.py").exists()
{
let mut detected_by = vec![];
if self.root_path.join("requirements.txt").exists() {
detected_by.push("requirements.txt".to_string());
}
if self.root_path.join("pyproject.toml").exists() {
detected_by.push("pyproject.toml".to_string());
}
if self.root_path.join("setup.py").exists() {
detected_by.push("setup.py".to_string());
}
let version = self.read_python_version()?;
profile.add_language(
Language::Python,
LanguageInfo {
detected_by,
version,
file_count: 0,
},
);
}
if self.root_path.join("go.mod").exists() {
let detected_by = vec!["go.mod".to_string()];
let version = self.read_go_version()?;
profile.add_language(
Language::Go,
LanguageInfo {
detected_by,
version,
file_count: 0,
},
);
}
Ok(())
}
fn scan_configs(&self, profile: &mut RepoProfile) -> Result<()> {
if self.root_path.join("foundry.toml").exists() {
profile.add_tool(
Tool::Foundry,
ToolInfo {
detected_by: vec!["foundry.toml".to_string()],
version: self.extract_foundry_version()?,
},
);
profile.add_language(
Language::Solidity,
LanguageInfo {
detected_by: vec!["foundry.toml".to_string()],
version: self.extract_solc_version()?,
file_count: 0,
},
);
}
if self.root_path.join("hardhat.config.js").exists()
|| self.root_path.join("hardhat.config.ts").exists()
{
profile.add_tool(
Tool::Hardhat,
ToolInfo {
detected_by: vec!["hardhat.config.*".to_string()],
version: None,
},
);
}
if self.root_path.join("mud.config.ts").exists() {
profile.add_tool(
Tool::MudFramework,
ToolInfo {
detected_by: vec!["mud.config.ts".to_string()],
version: None,
},
);
}
if self.root_path.join("tsconfig.json").exists() {
profile.add_language(
Language::TypeScript,
LanguageInfo {
detected_by: vec!["tsconfig.json".to_string()],
version: None,
file_count: 0,
},
);
}
let dojo_configs = [
"dojo_dev.toml",
"dojo_sepolia.toml",
"dojo_mainnet.toml",
"dojo.toml",
];
let has_dojo_config = dojo_configs.iter().any(|config| {
self.root_path.join(config).exists()
|| self.root_path.join("contracts").join(config).exists()
});
if has_dojo_config {
profile.add_tool(
Tool::Dojo,
ToolInfo {
detected_by: vec!["dojo_*.toml".to_string()],
version: None,
},
);
if self.root_path.join("Scarb.toml").exists()
|| self.root_path.join("contracts/Scarb.toml").exists()
{
profile.add_tool(
Tool::Scarb,
ToolInfo {
detected_by: vec!["Scarb.toml (Dojo project)".to_string()],
version: self.extract_scarb_version()?,
},
);
}
if !profile.languages.contains_key(&Language::Cairo) {
profile.add_language(
Language::Cairo,
LanguageInfo {
detected_by: vec!["dojo_*.toml".to_string()],
version: self.extract_cairo_version()?,
file_count: 0,
},
);
}
}
Ok(())
}
fn scan_source_files(&self, profile: &mut RepoProfile) -> Result<()> {
let sol_files = self.count_files_with_extension("sol")?;
if sol_files > 0 && !profile.languages.contains_key(&Language::Solidity) {
profile.add_language(
Language::Solidity,
LanguageInfo {
detected_by: vec![format!("{} .sol files", sol_files)],
version: None,
file_count: sol_files,
},
);
}
let cairo_files = self.count_files_with_extension("cairo")?;
if cairo_files > 0 {
profile.add_language(
Language::Cairo,
LanguageInfo {
detected_by: vec![format!("{} .cairo files", cairo_files)],
version: None,
file_count: cairo_files,
},
);
}
if profile.languages.contains_key(&Language::JavaScript) {
let js_count =
self.count_files_with_extension("js")? + self.count_files_with_extension("jsx")?;
if let Some(info) = profile.languages.get_mut(&Language::JavaScript) {
info.file_count = js_count;
}
}
if profile.languages.contains_key(&Language::TypeScript) {
let ts_count =
self.count_files_with_extension("ts")? + self.count_files_with_extension("tsx")?;
if let Some(info) = profile.languages.get_mut(&Language::TypeScript) {
info.file_count = ts_count;
}
}
Ok(())
}
fn scan_services(&self, profile: &mut RepoProfile) -> Result<()> {
if self.root_path.join("docker-compose.yml").exists()
|| self.root_path.join("docker-compose.yaml").exists()
{
}
if self.root_path.join("mprocs.yaml").exists() {
profile.add_service(Service {
name: "mprocs".to_string(),
image: None,
ports: vec![],
});
}
Ok(())
}
fn apply_smart_inference(&self, profile: &mut RepoProfile) -> Result<()> {
if profile.tools.contains_key(&Tool::Foundry) {
profile.add_service(Service {
name: "anvil".to_string(),
image: Some("ghcr.io/foundry-rs/foundry:latest".to_string()),
ports: vec![8545],
});
}
if profile.tools.contains_key(&Tool::MudFramework) {
profile.add_service(Service {
name: "indexer".to_string(),
image: Some("ghcr.io/latticexyz/store-indexer:latest".to_string()),
ports: vec![],
});
}
Ok(())
}
fn count_files_with_extension(&self, ext: &str) -> Result<usize> {
let count = WalkBuilder::new(&self.root_path)
.hidden(false) .git_ignore(true) .git_global(true) .git_exclude(true) .require_git(false) .max_depth(Some(10)) .build()
.filter_map(Result::ok)
.filter(|entry| {
entry.file_type().map(|ft| ft.is_file()).unwrap_or(false)
&& entry.path().extension().and_then(|e| e.to_str()) == Some(ext)
})
.count();
Ok(count)
}
fn read_node_version(&self) -> Result<Option<String>> {
let nvmrc_path = self.root_path.join(".nvmrc");
if nvmrc_path.exists() {
let content = std::fs::read_to_string(nvmrc_path)?;
return Ok(Some(content.trim().to_string()));
}
Ok(None)
}
fn read_rust_version(&self) -> Result<Option<String>> {
let toolchain_path = self.root_path.join("rust-toolchain.toml");
if toolchain_path.exists() {
}
Ok(None)
}
fn read_python_version(&self) -> Result<Option<String>> {
let version_path = self.root_path.join(".python-version");
if version_path.exists() {
let content = std::fs::read_to_string(version_path)?;
return Ok(Some(content.trim().to_string()));
}
Ok(None)
}
fn read_go_version(&self) -> Result<Option<String>> {
Ok(None)
}
fn extract_pnpm_version(&self) -> Result<Option<String>> {
Ok(None)
}
fn extract_foundry_version(&self) -> Result<Option<String>> {
Ok(None)
}
fn extract_solc_version(&self) -> Result<Option<String>> {
Ok(None)
}
fn extract_scarb_version(&self) -> Result<Option<String>> {
Ok(None)
}
fn extract_cairo_version(&self) -> Result<Option<String>> {
Ok(None)
}
}