mecha10_cli_core/services/
project.rs1#![allow(dead_code)]
2
3use crate::paths;
9use anyhow::{anyhow, Context, Result};
10use std::fs;
11use std::path::{Path, PathBuf};
12
13pub struct ProjectService {
35 root: PathBuf,
37}
38
39impl ProjectService {
40 pub fn detect(path: &Path) -> Result<Self> {
52 let start = path.canonicalize().context("Failed to canonicalize path")?;
53
54 mecha10_core::fs_utils::find_project_root(&start)
55 .map(|root| Self { root })
56 .ok_or_else(|| {
57 anyhow!(
58 "No mecha10.json found in {} or any parent directory.\n\
59 Run 'mecha10 init' to create a new project.",
60 path.display()
61 )
62 })
63 }
64
65 pub fn new(path: PathBuf) -> Self {
75 Self { root: path }
76 }
77
78 pub fn root(&self) -> &Path {
80 &self.root
81 }
82
83 pub fn config_path(&self) -> PathBuf {
85 self.root.join(paths::PROJECT_CONFIG)
86 }
87
88 pub fn is_initialized(&self) -> bool {
90 self.config_path().exists()
91 }
92
93 pub fn name(&self) -> Result<String> {
97 let (name, _) = self.load_metadata()?;
98 Ok(name)
99 }
100
101 pub fn version(&self) -> Result<String> {
105 let (_, version) = self.load_metadata()?;
106 Ok(version)
107 }
108
109 pub fn load_metadata(&self) -> Result<(String, String)> {
113 let mecha10_json = self.config_path();
115 if mecha10_json.exists() {
116 let content = fs::read_to_string(&mecha10_json).context("Failed to read mecha10.json")?;
117 let json: serde_json::Value = serde_json::from_str(&content).context("Failed to parse mecha10.json")?;
118
119 let name = json["name"]
120 .as_str()
121 .ok_or_else(|| anyhow!("Missing 'name' field in mecha10.json"))?
122 .to_string();
123
124 let version = json["version"]
125 .as_str()
126 .ok_or_else(|| anyhow!("Missing 'version' field in mecha10.json"))?
127 .to_string();
128
129 return Ok((name, version));
130 }
131
132 let cargo_toml = self.root.join(paths::rust::CARGO_TOML);
134 if cargo_toml.exists() {
135 let content = fs::read_to_string(&cargo_toml).context("Failed to read Cargo.toml")?;
136
137 let toml: toml::Value = content.parse().context("Failed to parse Cargo.toml")?;
139
140 let name = toml
141 .get("package")
142 .and_then(|p| p.get("name"))
143 .and_then(|n| n.as_str())
144 .ok_or_else(|| anyhow!("Missing 'package.name' in Cargo.toml"))?
145 .to_string();
146
147 let version = toml
148 .get("package")
149 .and_then(|p| p.get("version"))
150 .and_then(|v| v.as_str())
151 .ok_or_else(|| anyhow!("Missing 'package.version' in Cargo.toml"))?
152 .to_string();
153
154 return Ok((name, version));
155 }
156
157 Err(anyhow!(
158 "No mecha10.json or Cargo.toml found in project root: {}",
159 self.root.display()
160 ))
161 }
162
163 pub fn validate(&self) -> Result<()> {
167 if !self.config_path().exists() {
169 return Err(anyhow!(
170 "Project not initialized: mecha10.json not found at {}",
171 self.root.display()
172 ));
173 }
174
175 let required_dirs = vec!["nodes", "drivers", "types"];
177 for dir in required_dirs {
178 let dir_path = self.root.join(dir);
179 if !dir_path.exists() {
180 return Err(anyhow!("Invalid project structure: missing '{}' directory", dir));
181 }
182 }
183
184 Ok(())
185 }
186
187 pub fn list_nodes(&self) -> Result<Vec<String>> {
191 let nodes_dir = self.root.join(paths::project::NODES_DIR);
192 self.list_directories(&nodes_dir)
193 }
194
195 pub fn list_drivers(&self) -> Result<Vec<String>> {
199 let drivers_dir = self.root.join("drivers");
200 self.list_directories(&drivers_dir)
201 }
202
203 pub fn list_types(&self) -> Result<Vec<String>> {
207 let types_dir = self.root.join("types");
208 self.list_directories(&types_dir)
209 }
210
211 pub async fn list_enabled_nodes(&self) -> Result<Vec<String>> {
217 use crate::services::ConfigService;
218
219 let config = ConfigService::load_from(&self.config_path()).await?;
220 Ok(config.nodes.get_node_names())
221 }
222
223 fn list_directories(&self, dir: &Path) -> Result<Vec<String>> {
225 if !dir.exists() {
226 return Ok(Vec::new());
227 }
228
229 let mut names = Vec::new();
230 for entry in fs::read_dir(dir).with_context(|| format!("Failed to read directory: {}", dir.display()))? {
231 let entry = entry?;
232 if entry.file_type()?.is_dir() {
233 if let Some(name) = entry.file_name().to_str() {
234 names.push(name.to_string());
235 }
236 }
237 }
238
239 names.sort();
240 Ok(names)
241 }
242
243 pub fn path(&self, relative: &str) -> PathBuf {
245 self.root.join(relative)
246 }
247}