Skip to main content

theater_cli/commands/
start.rs

1use anyhow::Result;
2use clap::{Parser, ValueEnum};
3use std::io::Write;
4use std::path::PathBuf;
5use tokio::sync::mpsc;
6use tracing::{debug, error};
7
8use crate::{error::CliError, CommandContext};
9use theater::chain::ChainEvent;
10use theater::config::actor_manifest::{
11    RuntimeHostConfig, StoreHandlerConfig, SupervisorHostConfig, TcpHandlerConfig,
12    TerminalHandlerConfig, TimerHandlerConfig,
13};
14use theater::handler::HandlerRegistry;
15use theater::messages::TheaterCommand;
16use theater::pack_bridge::Value;
17use theater::theater_runtime::TheaterRuntime;
18use theater::utils::resolve_reference;
19use theater::ManifestConfig;
20use theater::TheaterId;
21use theater_handler_loop::LoopHandler;
22use theater_handler_message_server::{MessageRouter, MessageServerHandler};
23use theater_handler_rpc::RpcHandler;
24use theater_handler_runtime::RuntimeHandler;
25use theater_handler_store::StoreHandler;
26use theater_handler_supervisor::SupervisorHandler;
27use theater_handler_tcp::TcpHandler;
28use theater_handler_terminal::TerminalHandler;
29use theater_handler_timer::TimerHandler;
30
31/// Output format for chain events
32#[derive(Debug, Clone, Copy, ValueEnum, Default)]
33pub enum EventFormat {
34    /// JSON format (one JSON object per line)
35    Json,
36    /// Short format (compact, one line per event)
37    #[default]
38    Short,
39    /// Full format (complete event data, multi-line)
40    Full,
41}
42
43#[derive(Debug, Parser)]
44pub struct StartArgs {
45    /// Path or URL to the actor manifest file
46    #[arg(default_value = "manifest.toml")]
47    pub manifest: String,
48
49    /// Output chain events from all actors
50    #[arg(long)]
51    pub events: bool,
52
53    /// Format for event output (used with --events)
54    #[arg(long, value_enum, default_value = "short")]
55    pub events_format: EventFormat,
56
57    /// Save the chain to a local directory after the actor exits.
58    /// Defaults to `.chains/` in the current directory.
59    #[arg(long, default_missing_value = ".chains", num_args = 0..=1)]
60    pub save: Option<PathBuf>,
61
62    /// Skip calling the actor's init function after spawning
63    #[arg(long)]
64    pub no_init: bool,
65
66    /// Disable actor log output to stdout
67    #[arg(long)]
68    pub no_actor_logs: bool,
69}
70
71/// Format a chain event with actor ID prefix using ChainEvent's Display impl (short)
72fn format_event_short(event: &ChainEvent, actor_id: &TheaterId) -> String {
73    let id_str = actor_id.to_string();
74    let short_id = &id_str[..8.min(id_str.len())];
75    format!("[{}] {}\n", short_id, event)
76}
77
78/// Format a chain event with full data (multi-line, complete)
79fn format_event_full(event: &ChainEvent, actor_id: &TheaterId) -> String {
80    let id_str = actor_id.to_string();
81    let short_id = &id_str[..8.min(id_str.len())];
82    let hash_hex = hex::encode(&event.hash);
83    let parent_hex = event
84        .parent_hash
85        .as_ref()
86        .map(hex::encode)
87        .unwrap_or_else(|| "none".to_string());
88    let data_str = String::from_utf8_lossy(&event.data);
89
90    format!(
91        "EVENT [{}] {}\nparent: {}\ntype: {}\nsize: {}\n{}\n\n",
92        short_id,
93        hash_hex,
94        parent_hex,
95        event.event_type,
96        event.data.len(),
97        data_str
98    )
99}
100
101/// Format a chain event as JSON for stdout
102fn format_event_json(event: &ChainEvent, actor_id: &TheaterId) -> String {
103    let json = serde_json::json!({
104        "actor_id": actor_id.to_string(),
105        "hash": hex::encode(&event.hash),
106        "parent_hash": event.parent_hash.as_ref().map(hex::encode),
107        "event_type": event.event_type,
108        "data": format!("{} bytes (pack-encoded)", event.data.len())
109    });
110    serde_json::to_string(&json).unwrap_or_else(|_| "{}".to_string())
111}
112
113/// Create a handler registry with all Theater handlers
114fn create_handler_registry(
115    theater_tx: mpsc::Sender<TheaterCommand>,
116    show_actor_logs: bool,
117) -> Result<HandlerRegistry, CliError> {
118    let mut registry = HandlerRegistry::new();
119
120    // Runtime handler - provides log, get-chain, shutdown
121    let runtime_config = RuntimeHostConfig {};
122    registry.register(
123        RuntimeHandler::new(runtime_config, theater_tx.clone(), None)
124            .with_show_logs(show_actor_logs),
125    );
126
127    // Store handler - provides content storage
128    let store_config = StoreHandlerConfig::default();
129    registry.register(StoreHandler::new(store_config, None));
130
131    // Supervisor handler - allows spawning/managing child actors
132    let supervisor_config = SupervisorHostConfig {};
133    registry.register(SupervisorHandler::new(supervisor_config, None));
134
135    // Message server handler - inter-actor messaging
136    let message_router = MessageRouter::new();
137    registry.register(MessageServerHandler::new(None, message_router.clone()));
138
139    // RPC handler - direct actor-to-actor function calls
140    registry.register(RpcHandler::new(theater_tx.clone()));
141
142    // TCP handler - TCP server/client functionality
143    let tcp_config = TcpHandlerConfig {
144        listen: None,
145        max_connections: None,
146        ..Default::default()
147    };
148    registry.register(TcpHandler::new(tcp_config));
149
150    // Terminal handler - stdin/stdout/stderr for interactive CLI apps
151    let terminal_config = TerminalHandlerConfig::default();
152    registry.register(TerminalHandler::new(terminal_config));
153
154    // Timer handler - periodic tick callbacks for game loops, polling, etc.
155    let timer_config = TimerHandlerConfig::default();
156    registry.register(TimerHandler::new(timer_config));
157
158    // Loop handler - cooperative looping with yield points
159    registry.register(LoopHandler::new());
160
161    Ok(registry)
162}
163
164/// Execute the start command - spin up a local runtime and run the actor
165pub async fn execute_async(args: &StartArgs, ctx: &CommandContext) -> Result<(), CliError> {
166    debug!("Starting actor from manifest: {}", args.manifest);
167
168    // Resolve the manifest reference (file path, URL, or store path)
169    let manifest_bytes = resolve_reference(&args.manifest).await.map_err(|e| {
170        CliError::invalid_manifest(format!(
171            "Failed to resolve manifest reference '{}': {}",
172            args.manifest, e
173        ))
174    })?;
175
176    let manifest_content = String::from_utf8(manifest_bytes).map_err(|e| {
177        CliError::invalid_manifest(format!("Manifest content is not valid UTF-8: {}", e))
178    })?;
179
180    // (Chain writing is handled by the runtime's ChainWriter via runtime.chain_dir)
181
182    // Parse the manifest first (needed to check for replay handler)
183    let manifest = ManifestConfig::from_toml_str(&manifest_content)
184        .map_err(|e| CliError::invalid_manifest(format!("Failed to parse manifest: {}", e)))?;
185
186    // Create the TheaterRuntime in-process
187    let (theater_tx, theater_rx) = mpsc::channel::<TheaterCommand>(32);
188    let handler_registry = create_handler_registry(theater_tx.clone(), !args.no_actor_logs)?;
189
190    let mut runtime = TheaterRuntime::new(
191        theater_tx.clone(),
192        theater_rx,
193        None, // no channel events forwarding needed
194        handler_registry,
195    )
196    .await
197    .map_err(|e| CliError::server_error(format!("Failed to create runtime: {}", e)))?;
198
199    // Set chain output directory if --save is specified
200    if let Some(ref dir) = args.save {
201        runtime.chain_dir = Some(dir.clone());
202    }
203
204    // Set up global event subscription (receives events from ALL actors)
205    let (global_events_tx, mut global_events_rx) = mpsc::channel(256);
206    runtime.add_global_subscription(global_events_tx);
207
208    // Spawn the runtime event loop in a background task
209    let runtime_handle = tokio::spawn(async move {
210        if let Err(e) = runtime.run().await {
211            error!("Theater runtime error: {}", e);
212        }
213    });
214
215    // Resolve WASM path relative to manifest directory
216    let wasm_path = if manifest.package.starts_with('/') || manifest.package.contains("://") {
217        // Absolute path or URL - use as is
218        manifest.package.clone()
219    } else {
220        // Relative path - resolve relative to manifest's directory
221        let manifest_path = std::path::Path::new(&args.manifest);
222        if let Some(manifest_dir) = manifest_path.parent() {
223            manifest_dir
224                .join(&manifest.package)
225                .to_string_lossy()
226                .to_string()
227        } else {
228            manifest.package.clone()
229        }
230    };
231
232    // Load WASM bytes
233    let wasm_bytes = resolve_reference(&wasm_path).await.map_err(|e| {
234        CliError::server_error(format!("Failed to load WASM from '{}': {}", wasm_path, e))
235    })?;
236
237    // Spawn the actor
238    let (response_tx, response_rx) = tokio::sync::oneshot::channel();
239
240    // Set up a supervisor channel so we get notified when the actor exits
241    let (supervisor_tx, mut supervisor_rx) = mpsc::channel(32);
242
243    theater_tx
244        .send(TheaterCommand::SpawnActor {
245            wasm_bytes,
246            name: Some(manifest.name.clone()),
247            manifest: Some(manifest),
248            init_bytes: None,
249            response_tx,
250            supervisor_tx: Some(supervisor_tx),
251            subscription_tx: None, // Using global subscription instead
252        })
253        .await
254        .map_err(|e| CliError::server_error(format!("Failed to send spawn command: {}", e)))?;
255
256    // Wait for the actor to start
257    let actor_id = match response_rx.await {
258        Ok(Ok(id)) => {
259            debug!("Actor started: {}", id);
260            id
261        }
262        Ok(Err(e)) => {
263            return Err(CliError::server_error(format!(
264                "Failed to start actor: {}",
265                e
266            )));
267        }
268        Err(e) => {
269            return Err(CliError::server_error(format!(
270                "Failed to receive spawn response: {}",
271                e
272            )));
273        }
274    };
275
276    // Call init unless --no-init flag is set
277    if !args.no_init {
278        // Get the actor handle
279        let (handle_tx, handle_rx) = tokio::sync::oneshot::channel();
280        theater_tx
281            .send(TheaterCommand::GetActorHandle {
282                actor_id,
283                response_tx: handle_tx,
284            })
285            .await
286            .map_err(|e| CliError::server_error(format!("Failed to get actor handle: {}", e)))?;
287
288        let actor_handle = match handle_rx.await {
289            Ok(Some(handle)) => handle,
290            Ok(None) => {
291                return Err(CliError::server_error("Actor handle not found".to_string()));
292            }
293            Err(e) => {
294                return Err(CliError::server_error(format!(
295                    "Failed to receive actor handle: {}",
296                    e
297                )));
298            }
299        };
300
301        // Call init — state is injected by execute_call_pack from the actor store
302        let init_params = Value::Tuple(vec![]);
303        debug!("Calling init on actor {}", actor_id);
304        let _init_result = actor_handle
305            .call_function("theater:simple/actor.init".to_string(), init_params)
306            .await
307            .map_err(|e| CliError::server_error(format!("Failed to call init: {}", e)))?;
308        debug!("Init completed");
309    }
310
311    // Now wait for either:
312    // - The actor to exit (supervisor notification)
313    // - Ctrl+C
314    // - Shutdown token cancellation
315    //
316    // Output modes:
317    // - Default: print only log messages as [actor-id] message
318    // - --events: print all chain events as JSON
319    // - --chain-dir: also persist events to files
320    loop {
321        tokio::select! {
322            // Actor result (exit/error)
323            result = supervisor_rx.recv() => {
324                match result {
325                    Some(actor_result) => {
326                        debug!("Actor exited: {:?}", actor_result);
327                        match actor_result {
328                            theater::messages::ActorResult::Success(success) => {
329                                if let Some(output) = success.result {
330                                    // Write actor result to stdout
331                                    let _ = std::io::stdout().write_all(&output);
332                                    let _ = std::io::stdout().flush();
333                                }
334                            }
335                            theater::messages::ActorResult::Error(err) => {
336                                eprintln!("Actor error: {}", err.error);
337                                std::process::exit(1);
338                            }
339                            theater::messages::ActorResult::ExternalStop(_) => {
340                                debug!("Actor stopped externally");
341                            }
342                        }
343                        break;
344                    }
345                    None => {
346                        // Supervisor channel closed, actor is done
347                        debug!("Supervisor channel closed");
348                        break;
349                    }
350                }
351            }
352
353            // Global event subscription (all actors)
354            event = global_events_rx.recv() => {
355                if let Some((event_actor_id, event_result)) = event {
356                    match event_result {
357                        Ok(chain_event) => {
358                            // Output events if --events mode is enabled
359                            // (Actor logs are printed directly by RuntimeHandler, not extracted here)
360                            if args.events {
361                                match args.events_format {
362                                    EventFormat::Json => {
363                                        println!("{}", format_event_json(&chain_event, &event_actor_id));
364                                    }
365                                    EventFormat::Short => {
366                                        print!("{}", format_event_short(&chain_event, &event_actor_id));
367                                    }
368                                    EventFormat::Full => {
369                                        print!("{}", format_event_full(&chain_event, &event_actor_id));
370                                    }
371                                }
372                            }
373
374                            // Check for root actor shutdown
375                            if event_actor_id == actor_id && chain_event.event_type == "shutdown" {
376                                break;
377                            }
378                        }
379                        Err(e) => {
380                            debug!("Actor error event: {:?}", e);
381                        }
382                    }
383                }
384            }
385
386            // Ctrl+C
387            _ = tokio::signal::ctrl_c() => {
388                debug!("Received Ctrl+C, stopping actor {}", actor_id);
389                eprintln!("\nStopping actor...");
390
391                let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
392                let _ = theater_tx.send(TheaterCommand::StopActor {
393                    actor_id,
394                    response_tx: stop_tx,
395                }).await;
396
397                // Wait briefly for graceful shutdown
398                match tokio::time::timeout(
399                    tokio::time::Duration::from_secs(5),
400                    stop_rx,
401                ).await {
402                    Ok(Ok(Ok(()))) => debug!("Actor stopped gracefully"),
403                    _ => debug!("Actor stop timed out or failed"),
404                }
405                break;
406            }
407
408            // Shutdown token
409            _ = ctx.shutdown_token.cancelled() => {
410                debug!("Shutdown token cancelled");
411                break;
412            }
413        }
414    }
415
416    // Drop the theater_tx to signal the runtime to stop
417    drop(theater_tx);
418
419    // Wait for runtime to finish (with timeout)
420    let _ = tokio::time::timeout(tokio::time::Duration::from_secs(5), runtime_handle).await;
421
422    Ok(())
423}