Skip to main content

theater_cli/
lib.rs

1pub mod commands;
2pub mod config;
3pub mod error;
4pub mod output;
5pub mod templates;
6pub mod utils;
7
8use clap::{Parser, Subcommand, ValueEnum};
9
10/// Log level for runtime/system logs
11#[derive(Debug, Clone, Copy, ValueEnum, Default)]
12pub enum LogLevel {
13    /// Show error logs only
14    Error,
15    /// Show warning and error logs
16    #[default]
17    Warn,
18    /// Show info, warning, and error logs
19    Info,
20    /// Show debug and above
21    Debug,
22    /// Show all logs including trace
23    Trace,
24}
25
26impl From<LogLevel> for tracing::Level {
27    fn from(level: LogLevel) -> Self {
28        match level {
29            LogLevel::Error => tracing::Level::ERROR,
30            LogLevel::Warn => tracing::Level::WARN,
31            LogLevel::Info => tracing::Level::INFO,
32            LogLevel::Debug => tracing::Level::DEBUG,
33            LogLevel::Trace => tracing::Level::TRACE,
34        }
35    }
36}
37
38/// Theater CLI - A WebAssembly actor system that enables state management,
39/// verification, and flexible interaction patterns.
40#[derive(Debug, Parser)]
41#[command(name = "theater")]
42#[command(author, version, about, long_about = None)]
43pub struct Cli {
44    /// Set the log level for runtime/system logs
45    #[arg(short, long, global = true, value_enum, default_value = "warn")]
46    pub log_level: LogLevel,
47
48    /// Display output in JSON format
49    #[arg(long, global = true)]
50    pub json: bool,
51
52    #[command(subcommand)]
53    pub command: Commands,
54}
55
56#[derive(Debug, Subcommand)]
57pub enum Commands {
58    /// Create a new Theater actor project
59    #[command(name = "create")]
60    Create(commands::create::CreateArgs),
61
62    /// Build a Theater actor to WebAssembly
63    #[command(name = "build")]
64    Build(commands::build::BuildArgs),
65
66    /// Spawn an actor with a local runtime (setup + init)
67    #[command(name = "spawn")]
68    Spawn(commands::spawn::SpawnArgs),
69
70    /// Set up an actor with a local runtime, but do not call its init
71    /// export. The replay path uses this; otherwise drive init yourself.
72    #[command(name = "setup")]
73    Setup(commands::setup::SetupArgs),
74
75    /// Generate shell completion scripts
76    #[command(name = "completion")]
77    Completion(commands::completion::CompletionArgs),
78
79    /// Generate dynamic completions (internal use)
80    #[command(name = "dynamic-completion", hide = true)]
81    DynamicCompletion(commands::dynamic_completion::DynamicCompletionArgs),
82}
83
84/// Run the Theater CLI asynchronously with cancellation support
85pub async fn run(
86    cli: Cli,
87    config: config::Config,
88    shutdown_token: tokio_util::sync::CancellationToken,
89) -> anyhow::Result<()> {
90    // Create output manager
91    let output = output::OutputManager::new(config.output.clone());
92
93    // Create a context that contains shared resources
94    let ctx = CommandContext {
95        config,
96        output,
97        log_level: cli.log_level,
98        json: cli.json,
99        shutdown_token: shutdown_token.clone(),
100    };
101
102    // Execute the command with cancellation support
103    let command_future = async {
104        match &cli.command {
105            Commands::Create(args) => commands::create::execute_async(args, &ctx)
106                .await
107                .map_err(anyhow::Error::from),
108            Commands::Build(args) => commands::build::execute_async(args, &ctx)
109                .await
110                .map_err(anyhow::Error::from),
111            Commands::Spawn(args) => commands::spawn::execute_spawn(args, &ctx)
112                .await
113                .map_err(anyhow::Error::from),
114            Commands::Setup(args) => commands::setup::execute_async(args, &ctx)
115                .await
116                .map_err(anyhow::Error::from),
117            Commands::Completion(args) => commands::completion::execute_async(args, &ctx)
118                .await
119                .map_err(anyhow::Error::from),
120            Commands::DynamicCompletion(args) => {
121                commands::dynamic_completion::execute_async(args, &ctx)
122                    .await
123                    .map_err(anyhow::Error::from)
124            }
125        }
126    };
127
128    // Race the command execution against cancellation
129    let result = tokio::select! {
130        result = command_future => result,
131        _ = shutdown_token.cancelled() => {
132            return Ok(());
133        }
134    };
135
136    // Handle the result
137    match result {
138        Ok(()) => Ok(()),
139        Err(e) => {
140            // Use our enhanced error handling
141            if let Some(cli_error) = e.downcast_ref::<error::CliError>() {
142                ctx.output.error(&cli_error.user_message())?;
143                if ctx.is_verbose() {
144                    eprintln!("\nDebug info: {:?}", cli_error);
145                }
146            } else {
147                ctx.output.error(&format!("Error: {}", e))?;
148                if ctx.is_verbose() {
149                    eprintln!("\nDebug info: {:?}", e);
150                }
151            }
152            std::process::exit(1);
153        }
154    }
155}
156
157/// Shared context for command execution
158pub struct CommandContext {
159    pub config: config::Config,
160    pub output: output::OutputManager,
161    pub log_level: LogLevel,
162    pub json: bool,
163    pub shutdown_token: tokio_util::sync::CancellationToken,
164}
165
166impl CommandContext {
167    /// Returns true if log level is debug or higher (more verbose)
168    pub fn is_verbose(&self) -> bool {
169        matches!(self.log_level, LogLevel::Debug | LogLevel::Trace)
170    }
171}