Skip to main content

netget/cli/
easy_startup.rs

1//! Easy protocol startup logic
2
3use anyhow::{Context, Result};
4use serde_json::Value as JsonValue;
5use std::sync::Arc;
6use tracing::info;
7
8use crate::llm::OllamaClient;
9use crate::protocol::EASY_REGISTRY;
10use crate::state::{AppState, EasyId, EasyInstance, EasyStatus};
11
12/// Start an easy protocol instance
13///
14/// This function:
15/// 1. Creates an EasyInstance and adds it to AppState
16/// 2. Calls the Easy protocol's generate_startup_action() to get underlying protocol action
17/// 3. Executes the action to start the underlying server/client
18/// 4. Links the underlying server/client to the easy instance for event routing
19///
20/// # Arguments
21/// * `protocol_name` - Easy protocol name (e.g., "http-easy")
22/// * `user_instruction` - Optional custom instruction from user
23/// * `port` - Optional port override
24/// * `state` - Application state
25/// * `llm_client` - LLM client
26///
27/// # Returns
28/// Easy protocol ID
29pub async fn start_easy_protocol(
30    protocol_name: &str,
31    user_instruction: Option<String>,
32    port: Option<u16>,
33    state: Arc<AppState>,
34    llm_client: Arc<OllamaClient>,
35) -> Result<EasyId> {
36    // Get easy protocol from registry
37    let easy_protocol = EASY_REGISTRY
38        .get_by_name(protocol_name)
39        .ok_or_else(|| anyhow::anyhow!("Easy protocol '{}' not found", protocol_name))?;
40
41    let underlying_protocol = easy_protocol.underlying_protocol();
42
43    info!(
44        "Starting easy protocol '{}' (wrapping '{}')",
45        protocol_name, underlying_protocol
46    );
47
48    // Create easy instance
49    let easy_instance = EasyInstance::new(
50        EasyId::new(0), // Will be assigned by AppState
51        protocol_name.to_string(),
52        underlying_protocol.to_string(),
53        user_instruction.clone(),
54    );
55
56    // Add to state and get assigned ID
57    let easy_id = state.add_easy_instance(easy_instance).await;
58
59    // Update status to Starting
60    state
61        .update_easy_status(easy_id, EasyStatus::Starting)
62        .await;
63
64    // Generate startup action for underlying protocol
65    let action = match easy_protocol.generate_startup_action(user_instruction.clone(), port) {
66        Ok(action) => action,
67        Err(error) => {
68            state
69                .update_easy_status(easy_id, EasyStatus::Error(error.to_string()))
70                .await;
71            return Err(error).context("Failed to generate startup action");
72        }
73    };
74
75    info!(
76        "Generated startup action: {}",
77        serde_json::to_string_pretty(&action)?
78    );
79
80    // Execute the startup action (open_server or open_client)
81    match execute_easy_startup_action(&action, &state, &llm_client).await {
82        Ok(underlying_id) => {
83            match underlying_id {
84                EasyUnderlyingId::Server(server_id) => {
85                    state.link_server_to_easy(server_id, easy_id).await
86                }
87                EasyUnderlyingId::Client(client_id) => {
88                    state.link_client_to_easy(client_id, easy_id).await
89                }
90            }
91
92            // Update status to Running
93            state.update_easy_status(easy_id, EasyStatus::Running).await;
94
95            Ok(easy_id)
96        }
97        Err(e) => {
98            // Update status to Error
99            state
100                .update_easy_status(easy_id, EasyStatus::Error(e.to_string()))
101                .await;
102            Err(e).context("Failed to start underlying protocol")
103        }
104    }
105}
106
107/// Typed ownership avoids narrowing arbitrary JSON integers when linking wrappers.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum EasyUnderlyingId {
110    Server(crate::state::ServerId),
111    Client(crate::state::ClientId),
112}
113
114/// Execute a generated easy action through the same validated forms as normal
115/// startup. Optional handlers, memory, parameters and tasks are preserved.
116pub async fn execute_easy_startup_action(
117    action: &JsonValue,
118    state: &AppState,
119    llm_client: &OllamaClient,
120) -> Result<EasyUnderlyingId> {
121    let (status_tx, mut status_rx) = tokio::sync::mpsc::unbounded_channel();
122    // Keep lifecycle messages observable without needing a dashboard receiver.
123    tokio::spawn(async move {
124        while let Some(message) = status_rx.recv().await {
125            tracing::info!("{}", message);
126        }
127    });
128    match action.get("type").and_then(JsonValue::as_str) {
129        Some("open_server") => {
130            let form: crate::cli::management::ServerForm =
131                serde_json::from_value(action.clone())
132                    .context("Invalid easy open_server action")?;
133            Ok(EasyUnderlyingId::Server(
134                form.create(state, status_tx).await?,
135            ))
136        }
137        Some("open_client") => {
138            let form: crate::cli::management::ClientForm =
139                serde_json::from_value(action.clone())
140                    .context("Invalid easy open_client action")?;
141            Ok(EasyUnderlyingId::Client(
142                form.create(state, llm_client.clone(), status_tx).await?,
143            ))
144        }
145        Some(other) => anyhow::bail!("Unknown easy startup action type: {}", other),
146        None => anyhow::bail!("Startup action missing string 'type' field"),
147    }
148}