mecha10_cli/services/
project.rs1#![allow(dead_code)]
2
3use anyhow::{anyhow, Context, Result};
9use std::fs;
10use std::path::{Path, PathBuf};
11
12pub struct ProjectService {
34 root: PathBuf,
36}
37
38impl ProjectService {
39 pub fn detect(path: &Path) -> Result<Self> {
51 let mut current = path.canonicalize().context("Failed to canonicalize path")?;
52
53 loop {
54 let config_path = current.join("mecha10.json");
55 if config_path.exists() {
56 return Ok(Self { root: current });
57 }
58
59 match current.parent() {
61 Some(parent) => current = parent.to_path_buf(),
62 None => {
63 return Err(anyhow!(
64 "No mecha10.json found in {} or any parent directory.\n\
65 Run 'mecha10 init' to create a new project.",
66 path.display()
67 ))
68 }
69 }
70 }
71 }
72
73 pub fn new(path: PathBuf) -> Self {
83 Self { root: path }
84 }
85
86 pub fn root(&self) -> &Path {
88 &self.root
89 }
90
91 pub fn config_path(&self) -> PathBuf {
93 self.root.join("mecha10.json")
94 }
95
96 pub fn is_initialized(&self) -> bool {
98 self.config_path().exists()
99 }
100
101 pub fn name(&self) -> Result<String> {
105 let (name, _) = self.load_metadata()?;
106 Ok(name)
107 }
108
109 pub fn version(&self) -> Result<String> {
113 let (_, version) = self.load_metadata()?;
114 Ok(version)
115 }
116
117 pub fn load_metadata(&self) -> Result<(String, String)> {
121 let mecha10_json = self.config_path();
123 if mecha10_json.exists() {
124 let content = fs::read_to_string(&mecha10_json).context("Failed to read mecha10.json")?;
125 let json: serde_json::Value = serde_json::from_str(&content).context("Failed to parse mecha10.json")?;
126
127 let name = json["name"]
128 .as_str()
129 .ok_or_else(|| anyhow!("Missing 'name' field in mecha10.json"))?
130 .to_string();
131
132 let version = json["version"]
133 .as_str()
134 .ok_or_else(|| anyhow!("Missing 'version' field in mecha10.json"))?
135 .to_string();
136
137 return Ok((name, version));
138 }
139
140 let cargo_toml = self.root.join("Cargo.toml");
142 if cargo_toml.exists() {
143 let content = fs::read_to_string(&cargo_toml).context("Failed to read Cargo.toml")?;
144
145 let toml: toml::Value = content.parse().context("Failed to parse Cargo.toml")?;
147
148 let name = toml
149 .get("package")
150 .and_then(|p| p.get("name"))
151 .and_then(|n| n.as_str())
152 .ok_or_else(|| anyhow!("Missing 'package.name' in Cargo.toml"))?
153 .to_string();
154
155 let version = toml
156 .get("package")
157 .and_then(|p| p.get("version"))
158 .and_then(|v| v.as_str())
159 .ok_or_else(|| anyhow!("Missing 'package.version' in Cargo.toml"))?
160 .to_string();
161
162 return Ok((name, version));
163 }
164
165 Err(anyhow!(
166 "No mecha10.json or Cargo.toml found in project root: {}",
167 self.root.display()
168 ))
169 }
170
171 pub fn validate(&self) -> Result<()> {
175 if !self.config_path().exists() {
177 return Err(anyhow!(
178 "Project not initialized: mecha10.json not found at {}",
179 self.root.display()
180 ));
181 }
182
183 let required_dirs = vec!["nodes", "drivers", "types"];
185 for dir in required_dirs {
186 let dir_path = self.root.join(dir);
187 if !dir_path.exists() {
188 return Err(anyhow!("Invalid project structure: missing '{}' directory", dir));
189 }
190 }
191
192 Ok(())
193 }
194
195 pub fn list_nodes(&self) -> Result<Vec<String>> {
199 let nodes_dir = self.root.join("nodes");
200 self.list_directories(&nodes_dir)
201 }
202
203 pub fn list_drivers(&self) -> Result<Vec<String>> {
207 let drivers_dir = self.root.join("drivers");
208 self.list_directories(&drivers_dir)
209 }
210
211 pub fn list_types(&self) -> Result<Vec<String>> {
215 let types_dir = self.root.join("types");
216 self.list_directories(&types_dir)
217 }
218
219 pub async fn list_enabled_nodes(&self) -> Result<Vec<String>> {
225 use crate::services::ConfigService;
226
227 let config = ConfigService::load_from(&self.config_path()).await?;
228 Ok(config.nodes.get_node_names())
229 }
230
231 fn list_directories(&self, dir: &Path) -> Result<Vec<String>> {
233 if !dir.exists() {
234 return Ok(Vec::new());
235 }
236
237 let mut names = Vec::new();
238 for entry in fs::read_dir(dir).with_context(|| format!("Failed to read directory: {}", dir.display()))? {
239 let entry = entry?;
240 if entry.file_type()?.is_dir() {
241 if let Some(name) = entry.file_name().to_str() {
242 names.push(name.to_string());
243 }
244 }
245 }
246
247 names.sort();
248 Ok(names)
249 }
250
251 pub fn path(&self, relative: &str) -> PathBuf {
253 self.root.join(relative)
254 }
255}