mecha10_cli/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 mut current = path.canonicalize().context("Failed to canonicalize path")?;
53
54 loop {
55 let config_path = current.join(paths::PROJECT_CONFIG);
56 if config_path.exists() {
57 return Ok(Self { root: current });
58 }
59
60 match current.parent() {
62 Some(parent) => current = parent.to_path_buf(),
63 None => {
64 return Err(anyhow!(
65 "No mecha10.json found in {} or any parent directory.\n\
66 Run 'mecha10 init' to create a new project.",
67 path.display()
68 ))
69 }
70 }
71 }
72 }
73
74 pub fn new(path: PathBuf) -> Self {
84 Self { root: path }
85 }
86
87 pub fn root(&self) -> &Path {
89 &self.root
90 }
91
92 pub fn config_path(&self) -> PathBuf {
94 self.root.join(paths::PROJECT_CONFIG)
95 }
96
97 pub fn is_initialized(&self) -> bool {
99 self.config_path().exists()
100 }
101
102 pub fn name(&self) -> Result<String> {
106 let (name, _) = self.load_metadata()?;
107 Ok(name)
108 }
109
110 pub fn version(&self) -> Result<String> {
114 let (_, version) = self.load_metadata()?;
115 Ok(version)
116 }
117
118 pub fn load_metadata(&self) -> Result<(String, String)> {
122 let mecha10_json = self.config_path();
124 if mecha10_json.exists() {
125 let content = fs::read_to_string(&mecha10_json).context("Failed to read mecha10.json")?;
126 let json: serde_json::Value = serde_json::from_str(&content).context("Failed to parse mecha10.json")?;
127
128 let name = json["name"]
129 .as_str()
130 .ok_or_else(|| anyhow!("Missing 'name' field in mecha10.json"))?
131 .to_string();
132
133 let version = json["version"]
134 .as_str()
135 .ok_or_else(|| anyhow!("Missing 'version' field in mecha10.json"))?
136 .to_string();
137
138 return Ok((name, version));
139 }
140
141 let cargo_toml = self.root.join(paths::rust::CARGO_TOML);
143 if cargo_toml.exists() {
144 let content = fs::read_to_string(&cargo_toml).context("Failed to read Cargo.toml")?;
145
146 let toml: toml::Value = content.parse().context("Failed to parse Cargo.toml")?;
148
149 let name = toml
150 .get("package")
151 .and_then(|p| p.get("name"))
152 .and_then(|n| n.as_str())
153 .ok_or_else(|| anyhow!("Missing 'package.name' in Cargo.toml"))?
154 .to_string();
155
156 let version = toml
157 .get("package")
158 .and_then(|p| p.get("version"))
159 .and_then(|v| v.as_str())
160 .ok_or_else(|| anyhow!("Missing 'package.version' in Cargo.toml"))?
161 .to_string();
162
163 return Ok((name, version));
164 }
165
166 Err(anyhow!(
167 "No mecha10.json or Cargo.toml found in project root: {}",
168 self.root.display()
169 ))
170 }
171
172 pub fn validate(&self) -> Result<()> {
176 if !self.config_path().exists() {
178 return Err(anyhow!(
179 "Project not initialized: mecha10.json not found at {}",
180 self.root.display()
181 ));
182 }
183
184 let required_dirs = vec!["nodes", "drivers", "types"];
186 for dir in required_dirs {
187 let dir_path = self.root.join(dir);
188 if !dir_path.exists() {
189 return Err(anyhow!("Invalid project structure: missing '{}' directory", dir));
190 }
191 }
192
193 Ok(())
194 }
195
196 pub fn list_nodes(&self) -> Result<Vec<String>> {
200 let nodes_dir = self.root.join(paths::project::NODES_DIR);
201 self.list_directories(&nodes_dir)
202 }
203
204 pub fn list_drivers(&self) -> Result<Vec<String>> {
208 let drivers_dir = self.root.join("drivers");
209 self.list_directories(&drivers_dir)
210 }
211
212 pub fn list_types(&self) -> Result<Vec<String>> {
216 let types_dir = self.root.join("types");
217 self.list_directories(&types_dir)
218 }
219
220 pub async fn list_enabled_nodes(&self) -> Result<Vec<String>> {
226 use crate::services::ConfigService;
227
228 let config = ConfigService::load_from(&self.config_path()).await?;
229 Ok(config.nodes.get_node_names())
230 }
231
232 fn list_directories(&self, dir: &Path) -> Result<Vec<String>> {
234 if !dir.exists() {
235 return Ok(Vec::new());
236 }
237
238 let mut names = Vec::new();
239 for entry in fs::read_dir(dir).with_context(|| format!("Failed to read directory: {}", dir.display()))? {
240 let entry = entry?;
241 if entry.file_type()?.is_dir() {
242 if let Some(name) = entry.file_name().to_str() {
243 names.push(name.to_string());
244 }
245 }
246 }
247
248 names.sort();
249 Ok(names)
250 }
251
252 pub fn path(&self, relative: &str) -> PathBuf {
254 self.root.join(relative)
255 }
256}