theater_cli/commands/
start.rs

1use anyhow::Result;
2use clap::Parser;
3use std::net::SocketAddr;
4use tokio::sync::mpsc;
5use tracing::debug;
6
7use crate::client::ManagementResponse;
8use crate::tui;
9use crate::utils::event_display::{display_events_header, display_single_event};
10use crate::{error::CliError, output::formatters::ActorStarted, CommandContext};
11use theater::utils::resolve_reference;
12
13#[derive(Debug, Parser)]
14pub struct StartArgs {
15    /// Path or URL to the actor manifest file
16    #[arg(required = true)]
17    pub manifest: String,
18
19    /// Address of the theater server
20    #[arg(short, long)]
21    pub address: Option<SocketAddr>,
22
23    /// Initial state as JSON string or path to JSON file
24    #[arg(short, long)]
25    pub initial_state: Option<String>,
26
27    /// Subscribe to actor events
28    #[arg(short, long)]
29    pub subscribe: bool,
30
31    /// Act as the actor's parent
32    #[arg(short, long)]
33    pub parent: bool,
34
35    /// Output only the actor ID (useful for piping to other commands)
36    #[arg(long)]
37    pub id_only: bool,
38
39    /// Event format (pretty, compact, json)
40    #[arg(short, long, default_value = "compact")]
41    pub format: String,
42}
43
44/// Execute the start command asynchronously (modernized)
45pub async fn execute_async(args: &StartArgs, ctx: &CommandContext) -> Result<(), CliError> {
46    debug!("Starting actor from manifest: {}", args.manifest);
47
48    // Get server address from args or config
49    let address = ctx.server_address(args.address);
50    debug!("Connecting to server at: {}", address);
51
52    // Resolve the manifest reference (could be file path, URL, or store path)
53    let manifest_bytes = resolve_reference(&args.manifest).await.map_err(|e| {
54        CliError::invalid_manifest(format!(
55            "Failed to resolve manifest reference '{}': {}",
56            args.manifest, e
57        ))
58    })?;
59
60    // Convert bytes to string
61    let manifest_content = String::from_utf8(manifest_bytes).map_err(|e| {
62        CliError::invalid_manifest(format!("Manifest content is not valid UTF-8: {}", e))
63    })?;
64
65    // Handle the initial state parameter
66    let initial_state = if let Some(state_str) = &args.initial_state {
67        // Try to resolve as reference first (file path, URL, or store path)
68        match resolve_reference(state_str).await {
69            Ok(bytes) => {
70                debug!("Resolved initial state from reference: {}", state_str);
71                Some(bytes)
72            }
73            Err(_) => {
74                // If resolution fails, assume it's a JSON string
75                debug!("Using provided string as JSON initial state");
76                Some(state_str.as_bytes().to_vec())
77            }
78        }
79    } else {
80        None
81    };
82
83    // Create client and connect
84    let client = ctx.create_client();
85    client
86        .connect()
87        .await
88        .map_err(|e| CliError::connection_failed(address, e))?;
89
90    // Start the actor with initial state
91    debug!("Calling start actor on client");
92    client
93        .start_actor(manifest_content, initial_state, args.parent, args.subscribe)
94        .await
95        .map_err(|e| CliError::actor_not_found(format!("Failed to start actor: {}", e)))?;
96    debug!("Actor start request sent successfully");
97
98    // Check if we should use TUI mode
99    let use_tui = args.subscribe && args.parent && !ctx.json && !args.id_only;
100
101    if use_tui {
102        // Use TUI mode
103        return run_with_tui(args, client).await;
104    } else if args.subscribe && !ctx.json {
105        println!("");
106        display_events_header(&args.format);
107    }
108
109    let mut actor_started = false;
110
111    // Add a timeout for actor startup
112    let timeout_duration = tokio::time::Duration::from_secs(30);
113
114    debug!("Entering response loop, waiting for actor start confirmation or events");
115    loop {
116        tokio::select! {
117            data = client.next_response() => {
118                debug!("Received response from client");
119                debug!("Response data: {:?}", data);
120                if let Ok(data) = data {
121                    match data {
122                        ManagementResponse::ActorStarted { id } => {
123                            debug!("Management response received: Actor started with ID: {}", id);
124                            actor_started = true;
125
126                            if args.id_only {
127                                println!("{}", id);
128                                break;
129                            } else {
130                                let result = ActorStarted {
131                                    actor_id: id.to_string(),
132                                    manifest_path: args.manifest.clone(),
133                                    address: address.to_string(),
134                                    subscribing: args.subscribe,
135                                    acting_as_parent: args.parent,
136                                };
137                                debug!("Outputting result: {:?}", result);
138                                ctx.output.output(&result, None)?;
139
140                                // if we are not subscribing or acting as a parent, break the loop
141                                if !args.subscribe && !args.parent {
142                                    break;
143                                }
144                            }
145                        }
146                        ManagementResponse::ActorEvent { event } => {
147                            if args.subscribe {
148                                display_single_event(&event, &args.format)
149                                    .map_err(|e| CliError::invalid_input("event_display", "event", e.to_string()))?;
150                            }
151                        }
152                        ManagementResponse::ActorError { error } => {
153                            if args.subscribe {
154                                println!("-----[actor error]-----------------");
155                                println!("     {}", error);
156                                println!("-----------------------------------");
157                            }
158                        }
159                        ManagementResponse::ActorStopped { id } => {
160                            println!("-----[actor stopped]-----------------");
161                            println!("{}", id);
162                            println!("-------------------------------------");
163                            break;
164                        }
165                        ManagementResponse::ActorResult(actor_result) => {
166                            if args.parent {
167                                println!("-----[actor result]-----------------");
168                                println!("     {}", actor_result);
169                                println!("------------------------------------");
170                            }
171                        }
172                        _ => {
173                            println!("Unknown response received");
174                            break;
175                        }
176                    }
177                }
178            }
179            _ = tokio::time::sleep(timeout_duration) => {
180                if !actor_started {
181                    return Err(CliError::operation_timeout("Actor startup", timeout_duration.as_secs()));
182                }
183            }
184            _ = tokio::signal::ctrl_c() => {
185                debug!("Received Ctrl-C, stopping");
186                if !ctx.json {
187                    println!("\n{}\n", "Interrupted by user");
188                }
189                break;
190            }
191        }
192    }
193
194    Ok(())
195}
196
197/// Run the start command with TUI interface
198async fn run_with_tui(
199    args: &StartArgs,
200    client: crate::client::TheaterClient,
201) -> Result<(), CliError> {
202    debug!("Starting TUI mode for actor monitoring");
203
204    // Create channel for communication with TUI
205    let (response_tx, response_rx) = mpsc::unbounded_channel();
206
207    // We'll need to get the actor ID from the first ActorStarted response
208    let mut _actor_id: Option<String> = None;
209    let mut tui_started = false;
210    let mut tui_completed = false;
211
212    // Add a timeout for actor startup
213    let timeout_duration = tokio::time::Duration::from_secs(30);
214
215    // Start TUI task early and wait for first actor started event
216    let mut tui_handle = {
217        let manifest_path = args.manifest.clone();
218        tokio::spawn(async move {
219            // Use a placeholder actor ID initially
220            if let Err(e) =
221                tui::run_tui("Starting...".to_string(), manifest_path, response_rx).await
222            {
223                eprintln!("TUI error: {}", e);
224            }
225        })
226    };
227
228    loop {
229        tokio::select! {
230            data = client.next_response() => {
231                if let Ok(response) = data {
232                    match &response {
233                        ManagementResponse::ActorStarted { id } => {
234                            _actor_id = Some(id.to_string());
235                            debug!("Actor started with ID: {}", id);
236                            tui_started = true;
237                        }
238                        ManagementResponse::ActorStopped { .. } => {
239                            // Send to TUI and break
240                            let _ = response_tx.send(response);
241                            break;
242                        }
243                        _ => {}
244                    }
245
246                    // Send all responses to TUI
247                    if let Err(_) = response_tx.send(response) {
248                        // TUI channel closed, probably user quit
249                        debug!("TUI channel closed, stopping");
250                        break;
251                    }
252                }
253            }
254            _ = tokio::time::sleep(timeout_duration) => {
255                if !tui_started {
256                    return Err(CliError::operation_timeout("Actor startup", timeout_duration.as_secs()));
257                }
258            }
259            _ = tokio::signal::ctrl_c() => {
260                debug!("Received Ctrl-C, stopping TUI mode");
261                break;
262            }
263            result = &mut tui_handle, if !tui_completed => {
264                match result {
265                    Ok(_) => debug!("TUI task completed"),
266                    Err(e) => debug!("TUI task error: {}", e),
267                }
268                tui_completed = true;
269                break;
270            }
271        }
272    }
273
274    // Clean up - wait for TUI to finish if it hasn't already
275    if !tui_completed {
276        let _ = tokio::time::timeout(tokio::time::Duration::from_millis(500), tui_handle).await;
277    }
278
279    Ok(())
280}