#![allow(dead_code)]
use crate::paths;
use anyhow::{anyhow, Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
pub struct ProjectService {
root: PathBuf,
}
impl ProjectService {
pub fn detect(path: &Path) -> Result<Self> {
let start = path.canonicalize().context("Failed to canonicalize path")?;
mecha10_core::fs_utils::find_project_root(&start)
.map(|root| Self { root })
.ok_or_else(|| {
anyhow!(
"No mecha10.json found in {} or any parent directory.\n\
Run 'mecha10 init' to create a new project.",
path.display()
)
})
}
pub fn new(path: PathBuf) -> Self {
Self { root: path }
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn config_path(&self) -> PathBuf {
self.root.join(paths::PROJECT_CONFIG)
}
pub fn is_initialized(&self) -> bool {
self.config_path().exists()
}
pub fn name(&self) -> Result<String> {
let (name, _) = self.load_metadata()?;
Ok(name)
}
pub fn version(&self) -> Result<String> {
let (_, version) = self.load_metadata()?;
Ok(version)
}
pub fn load_metadata(&self) -> Result<(String, String)> {
let mecha10_json = self.config_path();
if mecha10_json.exists() {
let content = fs::read_to_string(&mecha10_json).context("Failed to read mecha10.json")?;
let json: serde_json::Value = serde_json::from_str(&content).context("Failed to parse mecha10.json")?;
let name = json["name"]
.as_str()
.ok_or_else(|| anyhow!("Missing 'name' field in mecha10.json"))?
.to_string();
let version = json["version"]
.as_str()
.ok_or_else(|| anyhow!("Missing 'version' field in mecha10.json"))?
.to_string();
return Ok((name, version));
}
let cargo_toml = self.root.join(paths::rust::CARGO_TOML);
if cargo_toml.exists() {
let content = fs::read_to_string(&cargo_toml).context("Failed to read Cargo.toml")?;
let toml: toml::Value = content.parse().context("Failed to parse Cargo.toml")?;
let name = toml
.get("package")
.and_then(|p| p.get("name"))
.and_then(|n| n.as_str())
.ok_or_else(|| anyhow!("Missing 'package.name' in Cargo.toml"))?
.to_string();
let version = toml
.get("package")
.and_then(|p| p.get("version"))
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("Missing 'package.version' in Cargo.toml"))?
.to_string();
return Ok((name, version));
}
Err(anyhow!(
"No mecha10.json or Cargo.toml found in project root: {}",
self.root.display()
))
}
pub fn validate(&self) -> Result<()> {
if !self.config_path().exists() {
return Err(anyhow!(
"Project not initialized: mecha10.json not found at {}",
self.root.display()
));
}
let required_dirs = vec!["nodes", "drivers", "types"];
for dir in required_dirs {
let dir_path = self.root.join(dir);
if !dir_path.exists() {
return Err(anyhow!("Invalid project structure: missing '{}' directory", dir));
}
}
Ok(())
}
pub fn list_nodes(&self) -> Result<Vec<String>> {
let nodes_dir = self.root.join(paths::project::NODES_DIR);
self.list_directories(&nodes_dir)
}
pub fn list_drivers(&self) -> Result<Vec<String>> {
let drivers_dir = self.root.join("drivers");
self.list_directories(&drivers_dir)
}
pub fn list_types(&self) -> Result<Vec<String>> {
let types_dir = self.root.join("types");
self.list_directories(&types_dir)
}
pub async fn list_enabled_nodes(&self) -> Result<Vec<String>> {
use crate::services::ConfigService;
let config = ConfigService::load_from(&self.config_path()).await?;
Ok(config.nodes.get_node_names())
}
fn list_directories(&self, dir: &Path) -> Result<Vec<String>> {
if !dir.exists() {
return Ok(Vec::new());
}
let mut names = Vec::new();
for entry in fs::read_dir(dir).with_context(|| format!("Failed to read directory: {}", dir.display()))? {
let entry = entry?;
if entry.file_type()?.is_dir() {
if let Some(name) = entry.file_name().to_str() {
names.push(name.to_string());
}
}
}
names.sort();
Ok(names)
}
pub fn path(&self, relative: &str) -> PathBuf {
self.root.join(relative)
}
}