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::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/// Top-level `actr` CLI.
35#[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    /// Verbosity level (currently unused; -v is reserved for future telemetry).
50    #[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    // ── development (flat, high-frequency) ──
60    /// Initialize a new Actor project
61    Init(InitCommand),
62    /// Generate code from proto files
63    Gen(GenCommand),
64    /// Build source artifact and package a signed .actr workload
65    Build(BuildCommand),
66    /// Validate project dependencies
67    Check(CheckCommand),
68    /// Generate project documentation
69    Doc(DocCommand),
70
71    // ── runtime (flat, docker-style) ──
72    /// Run a packaged workload
73    Run(RunCommand),
74    /// List detached runtime instances
75    Ps(PsCommand),
76    /// Show logs for a detached runtime instance
77    Logs(LogsCommand),
78    /// Start a stopped detached runtime instance
79    Start(StartCommand),
80    /// Stop a detached runtime instance
81    Stop(StopCommand),
82    /// Restart a detached runtime instance
83    Restart(RestartCommand),
84    /// Remove a detached runtime instance record
85    Rm(RmCommand),
86
87    // ── resources (grouped) ──
88    /// Local dependency management (install)
89    Deps(DepsArgs),
90    /// Local package operations (sign, verify, keygen)
91    Pkg(PkgArgs),
92    /// Remote service registry (discover, publish, fingerprint)
93    Registry(RegistryArgs),
94    /// Dead Letter Queue inspection and remediation
95    Dlq(DlqArgs),
96
97    // ── meta ──
98    /// Manage project configuration
99    Config(ConfigCommand),
100    /// Print version, git hash, and build date
101    Version(VersionCommand),
102    /// Generate shell completion script
103    Completion(CompletionCommand),
104}
105
106impl Commands {
107    /// Cast the parsed subcommand to its [`Command`] trait object.
108    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
133/// Build the raw clap [`clap::Command`] for completion-script generation.
134pub fn build_cli() -> clap::Command {
135    Cli::command()
136}
137
138/// Entry point for `main.rs`.
139pub 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    // Extract standalone discover config early (before building container).
148    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
188/// If the user ran `actr registry discover` with standalone flags,
189/// extract them before the container is built so we can register
190/// ServiceDiscovery without needing a local `manifest.toml`.
191fn extract_standalone_discover(cmd: &Commands) -> Option<StandaloneDiscoverConfig> {
192    if let Commands::Registry(registry_args) = cmd {
193        if let RegistryCommand::Discover(discover_cmd) = &registry_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    // ── Standalone mode (--endpoint --realm-id --realm-secret) ──
228    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    // ── Project mode (needs manifest.toml or actr.toml in cwd) ──
255    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        // Neither file exists — register minimal container;
264        // ServiceDiscovery will fail validation with a clear error.
265        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
314/// Build a container with only shell-level components (UI),
315/// for when neither manifest.toml nor actr.toml exists.
316async 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}