1use anyhow::Result;
6use async_trait::async_trait;
7use clap::Args;
8
9use crate::core::{
10 ActrCliError, Command, CommandContext, CommandResult, ComponentType, DependencySpec,
11 ErrorReporter, InstallResult,
12};
13
14#[derive(Args, Debug)]
16#[command(
17 about = "Install service dependencies",
18 long_about = "Install service dependencies. You can install specific service packages, or install all dependencies configured in Actr.toml"
19)]
20pub struct InstallCommand {
21 #[arg(value_name = "PACKAGE")]
23 pub packages: Vec<String>,
24
25 #[arg(long)]
27 pub force: bool,
28
29 #[arg(long)]
31 pub force_update: bool,
32
33 #[arg(long)]
35 pub skip_verification: bool,
36}
37
38#[async_trait]
39impl Command for InstallCommand {
40 async fn execute(&self, context: &CommandContext) -> Result<CommandResult> {
41 if !self.is_actr_project() {
43 return Err(ActrCliError::InvalidProject {
44 message: "Not an Actor-RTC project. Run 'actr init' to initialize.".to_string(),
45 }
46 .into());
47 }
48
49 let dependency_specs = if !self.packages.is_empty() {
51 println!("đĻ Adding {} new service dependencies", self.packages.len());
53 self.parse_new_packages()?
54 } else {
55 if self.force_update {
57 println!("đĻ Force updating all service dependencies in configuration");
58 } else {
59 println!("đĻ Installing service dependencies in configuration");
60 }
61 self.load_dependencies_from_config(context).await?
62 };
63
64 if dependency_specs.is_empty() {
65 println!("âšī¸ No dependencies to install");
66 return Ok(CommandResult::Success(
67 "No dependencies to install".to_string(),
68 ));
69 }
70
71 let install_pipeline = {
73 let mut container = context.container.lock().unwrap();
74 container.get_install_pipeline()?
75 };
76
77 match install_pipeline
79 .install_dependencies(&dependency_specs)
80 .await
81 {
82 Ok(install_result) => {
83 self.display_install_success(&install_result);
84 Ok(CommandResult::Install(install_result))
85 }
86 Err(e) => {
87 let cli_error = ActrCliError::InstallFailed {
89 reason: e.to_string(),
90 };
91 eprintln!("{}", ErrorReporter::format_error(&cli_error));
92 Err(e)
93 }
94 }
95 }
96
97 fn required_components(&self) -> Vec<ComponentType> {
98 vec![
100 ComponentType::ConfigManager,
101 ComponentType::DependencyResolver,
102 ComponentType::ServiceDiscovery,
103 ComponentType::NetworkValidator,
104 ComponentType::FingerprintValidator,
105 ComponentType::ProtoProcessor,
106 ComponentType::CacheManager,
107 ]
108 }
109
110 fn name(&self) -> &str {
111 "install"
112 }
113
114 fn description(&self) -> &str {
115 "npm-style service-level dependency management (check-first architecture)"
116 }
117}
118
119impl InstallCommand {
120 pub fn new(
121 packages: Vec<String>,
122 force: bool,
123 force_update: bool,
124 skip_verification: bool,
125 ) -> Self {
126 Self {
127 packages,
128 force,
129 force_update,
130 skip_verification,
131 }
132 }
133
134 pub fn from_args(args: &InstallCommand) -> Self {
136 InstallCommand {
137 packages: args.packages.clone(),
138 force: args.force,
139 force_update: args.force_update,
140 skip_verification: args.skip_verification,
141 }
142 }
143
144 fn is_actr_project(&self) -> bool {
146 std::path::Path::new("Actr.toml").exists()
147 }
148
149 fn parse_new_packages(&self) -> Result<Vec<DependencySpec>> {
151 let mut specs = Vec::new();
152
153 for package_spec in &self.packages {
154 let spec = self.parse_package_spec(package_spec)?;
155 specs.push(spec);
156 }
157
158 Ok(specs)
159 }
160
161 fn parse_package_spec(&self, package_spec: &str) -> Result<DependencySpec> {
163 if package_spec.starts_with("actr://") {
164 self.parse_actr_uri(package_spec)
166 } else if package_spec.contains('@') {
167 self.parse_versioned_spec(package_spec)
169 } else {
170 self.parse_simple_spec(package_spec)
172 }
173 }
174
175 fn parse_actr_uri(&self, uri: &str) -> Result<DependencySpec> {
177 if !uri.starts_with("actr://") {
179 return Err(anyhow::anyhow!("Invalid actr:// URI: {uri}"));
180 }
181
182 let uri_part = &uri[7..]; let service_name = if let Some(pos) = uri_part.find('/') {
184 uri_part[..pos].to_string()
185 } else {
186 uri_part.to_string()
187 };
188
189 let (version, fingerprint) = if uri.contains('?') {
191 self.parse_query_params(uri)?
192 } else {
193 (None, None)
194 };
195
196 Ok(DependencySpec {
197 name: service_name,
198 uri: uri.to_string(),
199 version,
200 fingerprint,
201 })
202 }
203
204 fn parse_query_params(&self, uri: &str) -> Result<(Option<String>, Option<String>)> {
206 if let Some(query_start) = uri.find('?') {
207 let query = &uri[query_start + 1..];
208 let mut version = None;
209 let mut fingerprint = None;
210
211 for param in query.split('&') {
212 if let Some((key, value)) = param.split_once('=') {
213 match key {
214 "version" => version = Some(value.to_string()),
215 "fingerprint" => fingerprint = Some(value.to_string()),
216 _ => {} }
218 }
219 }
220
221 Ok((version, fingerprint))
222 } else {
223 Ok((None, None))
224 }
225 }
226
227 fn parse_versioned_spec(&self, spec: &str) -> Result<DependencySpec> {
229 let parts: Vec<&str> = spec.split('@').collect();
230 if parts.len() != 2 {
231 return Err(anyhow::anyhow!(
232 "Invalid package specification: {spec}. Use 'service-name@version'"
233 ));
234 }
235
236 let service_name = parts[0].to_string();
237 let version = parts[1].to_string();
238 let uri = format!("actr://{service_name}/?version={version}");
239
240 Ok(DependencySpec {
241 name: service_name,
242 uri,
243 version: Some(version),
244 fingerprint: None,
245 })
246 }
247
248 fn parse_simple_spec(&self, spec: &str) -> Result<DependencySpec> {
250 let service_name = spec.to_string();
251 let uri = format!("actr://{service_name}/");
252
253 Ok(DependencySpec {
254 name: service_name,
255 uri,
256 version: None,
257 fingerprint: None,
258 })
259 }
260
261 async fn load_dependencies_from_config(
263 &self,
264 context: &CommandContext,
265 ) -> Result<Vec<DependencySpec>> {
266 let config_manager = {
267 let container = context.container.lock().unwrap();
268 container.get_config_manager()?
269 };
270 let config = config_manager
271 .load_config(
272 config_manager
273 .get_project_root()
274 .join("Actr.toml")
275 .as_path(),
276 )
277 .await?;
278
279 let mut specs = Vec::new();
280
281 for dependency in &config.dependencies {
282 let uri = format!(
283 "actr://{}:{}+{}@v1/",
284 dependency.realm.realm_id,
285 dependency.actr_type.manufacturer,
286 dependency.actr_type.name
287 );
288 specs.push(DependencySpec {
289 name: dependency.alias.clone(),
290 uri,
291 version: None,
292 fingerprint: dependency.fingerprint.clone(),
293 });
294 }
295
296 Ok(specs)
297 }
298
299 fn display_install_success(&self, result: &InstallResult) {
301 println!();
302 println!("â
Installation successful!");
303 println!(
304 " đĻ Installed dependencies: {}",
305 result.installed_dependencies.len()
306 );
307 println!(" đī¸ Cache updates: {}", result.cache_updates);
308
309 if result.updated_config {
310 println!(" đ Configuration file updated");
311 }
312
313 if result.updated_lock_file {
314 println!(" đ Lock file updated");
315 }
316
317 if !result.warnings.is_empty() {
318 println!();
319 println!("â ī¸ Warnings:");
320 for warning in &result.warnings {
321 println!(" âĸ {warning}");
322 }
323 }
324
325 println!();
326 println!("đĄ Tip: Run 'actr gen' to generate the latest code");
327 }
328}
329
330impl Default for InstallCommand {
331 fn default() -> Self {
332 Self::new(Vec::new(), false, false, false)
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
341 fn test_parse_simple_spec() {
342 let cmd = InstallCommand::default();
343 let spec = cmd.parse_simple_spec("user-service").unwrap();
344
345 assert_eq!(spec.name, "user-service");
346 assert_eq!(spec.uri, "actr://user-service/");
347 assert_eq!(spec.version, None);
348 assert_eq!(spec.fingerprint, None);
349 }
350
351 #[test]
352 fn test_parse_versioned_spec() {
353 let cmd = InstallCommand::default();
354 let spec = cmd.parse_versioned_spec("user-service@1.2.0").unwrap();
355
356 assert_eq!(spec.name, "user-service");
357 assert_eq!(spec.uri, "actr://user-service/?version=1.2.0");
358 assert_eq!(spec.version, Some("1.2.0".to_string()));
359 assert_eq!(spec.fingerprint, None);
360 }
361
362 #[test]
363 fn test_parse_actr_uri_simple() {
364 let cmd = InstallCommand::default();
365 let spec = cmd.parse_actr_uri("actr://user-service/").unwrap();
366
367 assert_eq!(spec.name, "user-service");
368 assert_eq!(spec.uri, "actr://user-service/");
369 assert_eq!(spec.version, None);
370 assert_eq!(spec.fingerprint, None);
371 }
372
373 #[test]
374 fn test_parse_actr_uri_with_params() {
375 let cmd = InstallCommand::default();
376 let spec = cmd
377 .parse_actr_uri("actr://user-service/?version=1.2.0&fingerprint=sha256:abc123")
378 .unwrap();
379
380 assert_eq!(spec.name, "user-service");
381 assert_eq!(
382 spec.uri,
383 "actr://user-service/?version=1.2.0&fingerprint=sha256:abc123"
384 );
385 assert_eq!(spec.version, Some("1.2.0".to_string()));
386 assert_eq!(spec.fingerprint, Some("sha256:abc123".to_string()));
387 }
388}