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                        ManagementResponse::Error { error } => {
173                            return Err(CliError::management_error(error));
174                        }
175                        _ => {
176                            println!("Unknown response received");
177                            break;
178                        }
179                    }
180                }
181            }
182            _ = tokio::time::sleep(timeout_duration) => {
183                if !actor_started {
184                    return Err(CliError::operation_timeout("Actor startup", timeout_duration.as_secs()));
185                }
186            }
187            _ = tokio::signal::ctrl_c() => {
188                debug!("Received Ctrl-C, stopping");
189                if !ctx.json {
190                    println!("\n{}\n", "Interrupted by user");
191                }
192                break;
193            }
194        }
195    }
196
197    Ok(())
198}
199
200/// Run the start command with TUI interface
201async fn run_with_tui(
202    args: &StartArgs,
203    client: crate::client::TheaterClient,
204) -> Result<(), CliError> {
205    debug!("Starting TUI mode for actor monitoring");
206
207    // Create channel for communication with TUI
208    let (response_tx, response_rx) = mpsc::unbounded_channel();
209
210    // We'll need to get the actor ID from the first ActorStarted response
211    let mut _actor_id: Option<String> = None;
212    let mut tui_started = false;
213    let mut tui_completed = false;
214
215    // Add a timeout for actor startup
216    let timeout_duration = tokio::time::Duration::from_secs(30);
217
218    // Start TUI task early and wait for first actor started event
219    let mut tui_handle = {
220        let manifest_path = args.manifest.clone();
221        tokio::spawn(async move {
222            // Use a placeholder actor ID initially
223            if let Err(e) =
224                tui::run_tui("Starting...".to_string(), manifest_path, response_rx).await
225            {
226                eprintln!("TUI error: {}", e);
227            }
228        })
229    };
230
231    loop {
232        tokio::select! {
233            data = client.next_response() => {
234                if let Ok(response) = data {
235                    match &response {
236                        ManagementResponse::ActorStarted { id } => {
237                            _actor_id = Some(id.to_string());
238                            debug!("Actor started with ID: {}", id);
239                            tui_started = true;
240                        }
241                        ManagementResponse::ActorStopped { .. } => {
242                            // Send to TUI and break
243                            let _ = response_tx.send(response);
244                            break;
245                        }
246                        _ => {}
247                    }
248
249                    // Send all responses to TUI
250                    if let Err(_) = response_tx.send(response) {
251                        // TUI channel closed, probably user quit
252                        debug!("TUI channel closed, stopping");
253                        break;
254                    }
255                }
256            }
257            _ = tokio::time::sleep(timeout_duration) => {
258                if !tui_started {
259                    return Err(CliError::operation_timeout("Actor startup", timeout_duration.as_secs()));
260                }
261            }
262            _ = tokio::signal::ctrl_c() => {
263                debug!("Received Ctrl-C, stopping TUI mode");
264                break;
265            }
266            result = &mut tui_handle, if !tui_completed => {
267                match result {
268                    Ok(_) => debug!("TUI task completed"),
269                    Err(e) => debug!("TUI task error: {}", e),
270                }
271                tui_completed = true;
272                break;
273            }
274        }
275    }
276
277    // Clean up - wait for TUI to finish if it hasn't already
278    if !tui_completed {
279        let _ = tokio::time::timeout(tokio::time::Duration::from_millis(500), tui_handle).await;
280    }
281
282    Ok(())
283}