Skip to main content

actr_cli/
cli.rs

1//! CLI surface: `Cli`/`Commands` enum + unified dispatch.
2//!
3//! Guiding principles (see `cli/README.md` for the long form):
4//!
5//! - **High-frequency commands are flat**: `init`, `gen`, `build`, `run`,
6//!   `ps`, `logs`, `start/stop/restart/rm`, `check`, `doc`.
7//! - **Low-frequency / fine-grained operations are grouped**:
8//!   `deps`, `pkg`, `registry`, `dlq`.
9//! - **Meta commands** sit at the top level: `config`, `version`, `completion`.
10//! - **Every subcommand implements [`crate::core::Command`]**; main dispatches
11//!   through a single `cmd.execute(&ctx)` call and only builds a
12//!   `ServiceContainer` when `cmd.required_components()` is non-empty.
13
14use 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/// Top-level `actr` CLI.
34#[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    /// Verbosity level (currently unused; -v is reserved for future telemetry).
49    #[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    // ── development (flat, high-frequency) ──
59    /// Initialize a new Actor project
60    Init(InitCommand),
61    /// Generate code from proto files
62    Gen(GenCommand),
63    /// Build source artifact and package a signed .actr workload
64    Build(BuildCommand),
65    /// Validate project dependencies
66    Check(CheckCommand),
67    /// Generate project documentation
68    Doc(DocCommand),
69
70    // ── runtime (flat, docker-style) ──
71    /// Run a packaged workload
72    Run(RunCommand),
73    /// List detached runtime instances
74    Ps(PsCommand),
75    /// Show logs for a detached runtime instance
76    Logs(LogsCommand),
77    /// Start a stopped detached runtime instance
78    Start(StartCommand),
79    /// Stop a detached runtime instance
80    Stop(StopCommand),
81    /// Restart a detached runtime instance
82    Restart(RestartCommand),
83    /// Remove a detached runtime instance record
84    Rm(RmCommand),
85
86    // ── resources (grouped) ──
87    /// Local dependency management (install)
88    Deps(DepsArgs),
89    /// Local package operations (sign, verify, keygen)
90    Pkg(PkgArgs),
91    /// Remote service registry (discover, publish, fingerprint)
92    Registry(RegistryArgs),
93    /// Dead Letter Queue inspection and remediation
94    Dlq(DlqArgs),
95
96    // ── meta ──
97    /// Manage project configuration
98    Config(ConfigCommand),
99    /// Print version, git hash, and build date
100    Version(VersionCommand),
101    /// Generate shell completion script
102    Completion(CompletionCommand),
103}
104
105impl Commands {
106    /// Cast the parsed subcommand to its [`Command`] trait object.
107    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
132/// Build the raw clap [`clap::Command`] for completion-script generation.
133pub fn build_cli() -> clap::Command {
134    Cli::command()
135}
136
137/// Entry point for `main.rs`.
138pub 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}