1use std::sync::Arc;
15
16use anyhow::Result;
17use clap::{CommandFactory, Parser, Subcommand};
18use owo_colors::OwoColorize;
19use url::Url;
20
21use crate::commands::{
22 BuildCommand, CheckCommand, CompletionCommand, ConfigCommand, DepsArgs, DlqArgs, DocCommand,
23 GenCommand, InitCommand, LogsCommand, PkgArgs, PsCommand, RegistryArgs, RestartCommand,
24 RmCommand, RunCommand, StartCommand, StopCommand, VersionCommand,
25};
26use crate::core::{
27 ActrCliError, Command, CommandContext, CommandResult, ConfigManager, ConsoleUI,
28 ContainerBuilder, DefaultCacheManager, DefaultDependencyResolver, DefaultFingerprintValidator,
29 DefaultNetworkValidator, DefaultProtoProcessor, DiscoveryContext, ErrorReporter,
30 NetworkServiceDiscovery, ServiceContainer, TomlConfigManager,
31};
32
33#[derive(Parser)]
35#[command(name = "actr")]
36#[command(
37 about = "Actor-RTC Command Line Tool",
38 long_about = "Actor-RTC Command Line Tool.\n\n\
39 Commands are grouped by audience:\n \
40 development: init / gen / build / check / doc\n \
41 runtime: run / ps / logs / start / stop / restart / rm\n \
42 resources: deps / pkg / registry / dlq\n \
43 meta: config / version / completion",
44 version,
45 disable_version_flag = true
46)]
47pub struct Cli {
48 #[arg(short, action = clap::ArgAction::Count, hide = true)]
50 pub verbose: u8,
51
52 #[command(subcommand)]
53 pub command: Option<Commands>,
54}
55
56#[derive(Subcommand)]
57pub enum Commands {
58 Init(InitCommand),
61 Gen(GenCommand),
63 Build(BuildCommand),
65 Check(CheckCommand),
67 Doc(DocCommand),
69
70 Run(RunCommand),
73 Ps(PsCommand),
75 Logs(LogsCommand),
77 Start(StartCommand),
79 Stop(StopCommand),
81 Restart(RestartCommand),
83 Rm(RmCommand),
85
86 Deps(DepsArgs),
89 Pkg(PkgArgs),
91 Registry(RegistryArgs),
93 Dlq(DlqArgs),
95
96 Config(ConfigCommand),
99 Version(VersionCommand),
101 Completion(CompletionCommand),
103}
104
105impl Commands {
106 pub fn as_command(&self) -> &dyn Command {
108 match self {
109 Commands::Init(c) => c,
110 Commands::Gen(c) => c,
111 Commands::Build(c) => c,
112 Commands::Check(c) => c,
113 Commands::Doc(c) => c,
114 Commands::Run(c) => c,
115 Commands::Ps(c) => c,
116 Commands::Logs(c) => c,
117 Commands::Start(c) => c,
118 Commands::Stop(c) => c,
119 Commands::Restart(c) => c,
120 Commands::Rm(c) => c,
121 Commands::Deps(c) => c,
122 Commands::Pkg(c) => c,
123 Commands::Registry(c) => c,
124 Commands::Dlq(c) => c,
125 Commands::Config(c) => c,
126 Commands::Version(c) => c,
127 Commands::Completion(c) => c,
128 }
129 }
130}
131
132pub fn build_cli() -> clap::Command {
134 Cli::command()
135}
136
137pub async fn run() -> Result<()> {
139 let cli = Cli::parse();
140
141 let Some(cmd) = cli.command else {
142 Cli::command().print_help()?;
143 return Ok(());
144 };
145
146 let command = cmd.as_command();
147 let needs_container = !command.required_components().is_empty();
148 let container = if needs_container {
149 build_container().await?
150 } else {
151 ContainerBuilder::new().build()?
152 };
153
154 let ctx = CommandContext {
155 container: Arc::new(std::sync::Mutex::new(container)),
156 args: crate::core::CommandArgs {
157 command: String::new(),
158 subcommand: None,
159 flags: std::collections::HashMap::new(),
160 positional: Vec::new(),
161 },
162 working_dir: std::env::current_dir()?,
163 };
164
165 match command.execute(&ctx).await {
166 Ok(result) => {
167 render_result(result);
168 Ok(())
169 }
170 Err(e) => {
171 if let Some(cli_error) = e.downcast_ref::<ActrCliError>() {
172 if matches!(cli_error, ActrCliError::OperationCancelled) {
173 std::process::exit(0);
174 }
175 eprintln!("{}", ErrorReporter::format_error(cli_error));
176 } else {
177 eprintln!("{} {e:?}", "Error:".red());
178 }
179 std::process::exit(1);
180 }
181 }
182}
183
184fn render_result(result: CommandResult) {
185 match result {
186 CommandResult::Success(msg) => {
187 if !msg.is_empty() && msg != "Help displayed" {
188 println!("{msg}");
189 }
190 }
191 CommandResult::Install(install_result) => {
192 println!("Installation complete: {}", install_result.summary());
193 }
194 CommandResult::Validation(report) => {
195 let formatted = ErrorReporter::format_validation_report(&report);
196 println!("{formatted}");
197 }
198 CommandResult::Generation(gen_result) => {
199 println!("Generated {} files", gen_result.generated_files.len());
200 }
201 CommandResult::Error(error) => {
202 eprintln!("{} {error}", "Error:".red());
203 std::process::exit(1);
204 }
205 }
206}
207
208async fn build_container() -> Result<ServiceContainer> {
209 let config_path = std::path::Path::new("manifest.toml");
210 let mut builder = ContainerBuilder::new();
211 let mut config_manager = None;
212
213 if config_path.exists() {
214 builder = builder.config_path(config_path);
215 }
216
217 let mut container = builder.build()?;
218 container = container.register_user_interface(Arc::new(ConsoleUI::new()));
219
220 if config_path.exists() {
221 let manager = Arc::new(TomlConfigManager::new(config_path));
222 container = container.register_config_manager(manager.clone());
223 config_manager = Some(manager);
224 }
225
226 let mut container =
227 container.register_dependency_resolver(Arc::new(DefaultDependencyResolver::new()));
228 container = container.register_network_validator(Arc::new(DefaultNetworkValidator::new()));
229 container =
230 container.register_fingerprint_validator(Arc::new(DefaultFingerprintValidator::new()));
231 container = container.register_proto_processor(Arc::new(DefaultProtoProcessor::new()));
232 container = container.register_cache_manager(Arc::new(DefaultCacheManager::new()));
233
234 if let Some(manager) = config_manager {
235 let config = manager.load_config(config_path).await?;
236 let effective_cli =
237 crate::config::resolver::resolve_effective_cli_config().unwrap_or_default();
238
239 let signaling_url = Url::parse(&effective_cli.network.signaling_url).map_err(|e| {
240 anyhow::anyhow!(
241 "Invalid network.signaling_url '{}': {}",
242 effective_cli.network.signaling_url,
243 e
244 )
245 })?;
246
247 let ais_endpoint = effective_cli.network.ais_endpoint.clone();
248 let realm_id = effective_cli.network.realm_id.unwrap_or(1);
249 let realm_secret = effective_cli.network.realm_secret.clone();
250
251 let discovery_context = DiscoveryContext {
252 package_actr_type: config.package.actr_type.clone(),
253 signaling_url,
254 ais_endpoint,
255 realm: actr_protocol::Realm { realm_id },
256 realm_secret,
257 };
258
259 container = container
260 .register_service_discovery(Arc::new(NetworkServiceDiscovery::new(discovery_context)));
261 }
262 Ok(container)
263}