1use std::sync::Arc;
15
16use anyhow::Result;
17use clap::{CommandFactory, Parser, Subcommand};
18use owo_colors::OwoColorize;
19use url::Url;
20
21use crate::commands::discovery::StandaloneDiscoverConfig;
22use crate::commands::{
23 BuildCommand, CheckCommand, CompletionCommand, ConfigCommand, DepsArgs, DlqArgs, DocCommand,
24 GenCommand, InitCommand, LogsCommand, PkgArgs, PsCommand, RegistryArgs, RegistryCommand,
25 RestartCommand, RmCommand, RunCommand, StartCommand, StopCommand, VersionCommand,
26};
27use crate::core::{
28 ActrCliError, Command, CommandContext, CommandResult, ConfigManager, ConsoleUI,
29 ContainerBuilder, DefaultCacheManager, DefaultDependencyResolver, DefaultFingerprintValidator,
30 DefaultNetworkValidator, DefaultProtoProcessor, DiscoveryContext, ErrorReporter,
31 NetworkServiceDiscovery, ServiceContainer, TomlConfigManager,
32};
33
34#[derive(Parser)]
36#[command(name = "actr")]
37#[command(
38 about = "Actor-RTC Command Line Tool",
39 long_about = "Actor-RTC Command Line Tool.\n\n\
40 Commands are grouped by audience:\n \
41 development: init / gen / build / check / doc\n \
42 runtime: run / ps / logs / start / stop / restart / rm\n \
43 resources: deps / pkg / registry / dlq\n \
44 meta: config / version / completion",
45 version,
46 disable_version_flag = true
47)]
48pub struct Cli {
49 #[arg(short, action = clap::ArgAction::Count, hide = true)]
51 pub verbose: u8,
52
53 #[command(subcommand)]
54 pub command: Option<Commands>,
55}
56
57#[derive(Subcommand)]
58pub enum Commands {
59 Init(InitCommand),
62 Gen(GenCommand),
64 Build(BuildCommand),
66 Check(CheckCommand),
68 Doc(DocCommand),
70
71 Run(RunCommand),
74 Ps(PsCommand),
76 Logs(LogsCommand),
78 Start(StartCommand),
80 Stop(StopCommand),
82 Restart(RestartCommand),
84 Rm(RmCommand),
86
87 Deps(DepsArgs),
90 Pkg(PkgArgs),
92 Registry(RegistryArgs),
94 Dlq(DlqArgs),
96
97 Config(ConfigCommand),
100 Version(VersionCommand),
102 Completion(CompletionCommand),
104}
105
106impl Commands {
107 pub fn as_command(&self) -> &dyn Command {
109 match self {
110 Commands::Init(c) => c,
111 Commands::Gen(c) => c,
112 Commands::Build(c) => c,
113 Commands::Check(c) => c,
114 Commands::Doc(c) => c,
115 Commands::Run(c) => c,
116 Commands::Ps(c) => c,
117 Commands::Logs(c) => c,
118 Commands::Start(c) => c,
119 Commands::Stop(c) => c,
120 Commands::Restart(c) => c,
121 Commands::Rm(c) => c,
122 Commands::Deps(c) => c,
123 Commands::Pkg(c) => c,
124 Commands::Registry(c) => c,
125 Commands::Dlq(c) => c,
126 Commands::Config(c) => c,
127 Commands::Version(c) => c,
128 Commands::Completion(c) => c,
129 }
130 }
131}
132
133pub fn build_cli() -> clap::Command {
135 Cli::command()
136}
137
138pub async fn run() -> Result<()> {
140 let cli = Cli::parse();
141
142 let Some(ref cmd_variant) = cli.command else {
143 Cli::command().print_help()?;
144 return Ok(());
145 };
146
147 let standalone_discover = extract_standalone_discover(cmd_variant);
149
150 let command = cmd_variant.as_command();
151 let needs_container = !command.required_components().is_empty();
152 let container = if needs_container {
153 build_container(standalone_discover).await?
154 } else {
155 ContainerBuilder::new().build()?
156 };
157
158 let ctx = CommandContext {
159 container: Arc::new(std::sync::Mutex::new(container)),
160 args: crate::core::CommandArgs {
161 command: String::new(),
162 subcommand: None,
163 flags: std::collections::HashMap::new(),
164 positional: Vec::new(),
165 },
166 working_dir: std::env::current_dir()?,
167 };
168
169 match command.execute(&ctx).await {
170 Ok(result) => {
171 render_result(result);
172 Ok(())
173 }
174 Err(e) => {
175 if let Some(cli_error) = e.downcast_ref::<ActrCliError>() {
176 if matches!(cli_error, ActrCliError::OperationCancelled) {
177 std::process::exit(0);
178 }
179 eprintln!("{}", ErrorReporter::format_error(cli_error));
180 } else {
181 eprintln!("{} {e:?}", "Error:".red());
182 }
183 std::process::exit(1);
184 }
185 }
186}
187
188fn extract_standalone_discover(cmd: &Commands) -> Option<StandaloneDiscoverConfig> {
192 if let Commands::Registry(registry_args) = cmd {
193 if let RegistryCommand::Discover(discover_cmd) = ®istry_args.command {
194 return discover_cmd.standalone_config();
195 }
196 }
197 None
198}
199
200fn render_result(result: CommandResult) {
201 match result {
202 CommandResult::Success(msg) => {
203 if !msg.is_empty() && msg != "Help displayed" {
204 println!("{msg}");
205 }
206 }
207 CommandResult::Install(install_result) => {
208 println!("Installation complete: {}", install_result.summary());
209 }
210 CommandResult::Validation(report) => {
211 let formatted = ErrorReporter::format_validation_report(&report);
212 println!("{formatted}");
213 }
214 CommandResult::Generation(gen_result) => {
215 println!("Generated {} files", gen_result.generated_files.len());
216 }
217 CommandResult::Error(error) => {
218 eprintln!("{} {error}", "Error:".red());
219 std::process::exit(1);
220 }
221 }
222}
223
224async fn build_container(
225 standalone_discover: Option<StandaloneDiscoverConfig>,
226) -> Result<ServiceContainer> {
227 if let Some(cfg) = standalone_discover {
229 let mut builder = ContainerBuilder::new();
230 builder = builder.config_path(std::path::Path::new("."));
231
232 let mut container = builder.build()?;
233 container = container.register_user_interface(Arc::new(ConsoleUI::new()));
234
235 let discovery_context = DiscoveryContext {
236 package_actr_type: actr_protocol::ActrType {
237 manufacturer: "cli".into(),
238 name: "registry-discover".into(),
239 version: "0.0.0".into(),
240 },
241 signaling_url: cfg.endpoint.clone(),
242 ais_endpoint: cfg.endpoint.to_string(),
243 realm: actr_protocol::Realm {
244 realm_id: cfg.realm_id as u32,
245 },
246 realm_secret: Some(cfg.realm_secret),
247 };
248
249 container = container
250 .register_service_discovery(Arc::new(NetworkServiceDiscovery::new(discovery_context)));
251 return Ok(container);
252 }
253
254 let manifest_path = std::path::Path::new("manifest.toml");
256 let actr_path = std::path::Path::new("actr.toml");
257
258 let config_path = if manifest_path.exists() {
259 manifest_path
260 } else if actr_path.exists() {
261 actr_path
262 } else {
263 return build_minimal_container().await;
266 };
267
268 let mut builder = ContainerBuilder::new();
269 builder = builder.config_path(config_path);
270
271 let mut container = builder.build()?;
272 container = container.register_user_interface(Arc::new(ConsoleUI::new()));
273
274 let manager = Arc::new(TomlConfigManager::new(config_path));
275 container = container.register_config_manager(manager.clone());
276
277 let mut container =
278 container.register_dependency_resolver(Arc::new(DefaultDependencyResolver::new()));
279 container = container.register_network_validator(Arc::new(DefaultNetworkValidator::new()));
280 container =
281 container.register_fingerprint_validator(Arc::new(DefaultFingerprintValidator::new()));
282 container = container.register_proto_processor(Arc::new(DefaultProtoProcessor::new()));
283 container = container.register_cache_manager(Arc::new(DefaultCacheManager::new()));
284
285 let config = manager.load_config(config_path).await?;
286 let effective_cli = crate::config::resolver::resolve_effective_cli_config().unwrap_or_default();
287
288 let signaling_url = Url::parse(&effective_cli.network.signaling_url).map_err(|e| {
289 anyhow::anyhow!(
290 "Invalid network.signaling_url '{}': {}",
291 effective_cli.network.signaling_url,
292 e
293 )
294 })?;
295
296 let ais_endpoint = effective_cli.network.ais_endpoint.clone();
297 let realm_id = effective_cli.network.realm_id.unwrap_or(1);
298 let realm_secret = effective_cli.network.realm_secret.clone();
299
300 let discovery_context = DiscoveryContext {
301 package_actr_type: config.package.actr_type.clone(),
302 signaling_url,
303 ais_endpoint,
304 realm: actr_protocol::Realm { realm_id },
305 realm_secret,
306 };
307
308 container = container
309 .register_service_discovery(Arc::new(NetworkServiceDiscovery::new(discovery_context)));
310
311 Ok(container)
312}
313
314async fn build_minimal_container() -> Result<ServiceContainer> {
317 let mut container = ContainerBuilder::new().build()?;
318 container = container.register_user_interface(Arc::new(ConsoleUI::new()));
319 Ok(container)
320}