Skip to main content

systemprompt_cli/commands/infrastructure/services/
start.rs

1use crate::cli_settings::CliConfig;
2use crate::context::CommandContext;
3use crate::presentation::StartupRenderer;
4use anyhow::Result;
5use std::sync::Arc;
6use std::time::Instant;
7use systemprompt_cloud::CredentialsBootstrap;
8use systemprompt_config::ProfileBootstrap;
9use systemprompt_logging::CliService;
10use systemprompt_runtime::AppContext;
11use systemprompt_scheduler::{StartupPlan, StartupRequest};
12use systemprompt_traits::{Phase, StartupEvent, StartupEventExt, startup_channel};
13
14use super::lifecycle;
15
16#[derive(Debug, Clone, Copy)]
17pub struct ServiceTarget {
18    pub api: bool,
19    pub agents: bool,
20    pub mcp: bool,
21}
22
23#[derive(Debug, Clone, Copy)]
24pub struct ServiceFlags {
25    pub all: bool,
26    pub targets: ServiceTargetFlags,
27}
28
29#[derive(Debug, Clone, Copy)]
30pub struct ServiceTargetFlags {
31    pub api: bool,
32    pub agents: bool,
33    pub mcp: bool,
34}
35
36impl ServiceTarget {
37    pub const fn all() -> Self {
38        Self {
39            api: true,
40            agents: true,
41            mcp: true,
42        }
43    }
44
45    pub const fn from_flags(flags: ServiceFlags) -> Self {
46        if flags.all || (!flags.targets.api && !flags.targets.agents && !flags.targets.mcp) {
47            Self::all()
48        } else {
49            Self {
50                api: flags.targets.api,
51                agents: flags.targets.agents,
52                mcp: flags.targets.mcp,
53            }
54        }
55    }
56}
57
58#[derive(Debug, Clone, Copy)]
59pub struct StartupOptions {
60    pub skip_migrate: bool,
61    pub kill_port_process: bool,
62}
63
64pub(super) async fn execute(
65    target: ServiceTarget,
66    options: StartupOptions,
67    ctx: &CommandContext,
68) -> Result<()> {
69    let start_time = Instant::now();
70
71    let (tx, rx) = startup_channel();
72
73    let renderer = StartupRenderer::new(rx);
74    let render_handle = tokio::spawn(renderer.run());
75
76    let result = run_startup(&target, &options, ctx, &tx).await;
77
78    if let Err(e) = &result
79        && tx
80            .unbounded_send(StartupEvent::StartupFailed {
81                error: e.to_string(),
82                duration: start_time.elapsed(),
83            })
84            .is_err()
85    {
86        tracing::debug!("Failed to send startup failed event (receiver dropped)");
87    }
88
89    drop(tx);
90    if render_handle.await.is_err() {
91        tracing::debug!("Render task panicked or was cancelled");
92    }
93
94    result.map(|_| ())
95}
96
97async fn run_startup(
98    target: &ServiceTarget,
99    options: &StartupOptions,
100    ctx: &CommandContext,
101    events: &systemprompt_traits::StartupEventSender,
102) -> Result<String> {
103    let plan = StartupPlan::compute(StartupRequest {
104        api: target.api,
105        agents: target.agents,
106        mcp: target.mcp,
107        skip_migrate: options.skip_migrate,
108    });
109
110    events.phase_started(Phase::PreFlight);
111
112    match CredentialsBootstrap::get() {
113        Ok(Some(_)) => {
114            events.info("Cloud credentials available");
115        },
116        Ok(None) | Err(_) => {
117            events.info("Running in local-only mode (no cloud sync)");
118        },
119    }
120
121    events.phase_completed(Phase::PreFlight);
122
123    if plan.run_migrations {
124        events.phase_started(Phase::Database);
125        super::super::db::execute(
126            super::super::db::DbCommands::Migrate {
127                allow_checksum_drift: false,
128            },
129            ctx,
130        )
131        .await?;
132        events.phase_completed(Phase::Database);
133    }
134
135    if plan.start_api {
136        let api_url = super::serve::execute_with_events(
137            ctx.prompter(),
138            true,
139            options.kill_port_process,
140            &ctx.cli,
141            Some(events),
142        )
143        .await?;
144        return Ok(api_url);
145    }
146
147    if plan.agents_standalone_notice {
148        events.phase_started(Phase::Agents);
149        events.warning("Standalone agent start not supported");
150        events.info("Agents are managed by the API server lifecycle");
151        events.info("Use 'services start' or 'services serve' to start all services");
152        events.phase_completed(Phase::Agents);
153    }
154
155    if plan.mcp_standalone_notice {
156        events.phase_started(Phase::McpServers);
157        events.warning("Standalone MCP server start not supported");
158        events.info("MCP servers are managed by the API server lifecycle");
159        events.info("Use 'services start' or 'services serve' to start all services");
160        events.phase_completed(Phase::McpServers);
161    }
162
163    Ok(format!(
164        "http://127.0.0.1:{}",
165        ProfileBootstrap::get().map_or(8080, |p| p.server.port)
166    ))
167}
168
169pub(super) async fn execute_individual_agent(
170    ctx: &Arc<AppContext>,
171    agent: &str,
172    _config: &CliConfig,
173) -> Result<()> {
174    CliService::section(&format!("Starting Agent: {}", agent));
175
176    let orchestrator = lifecycle::agent_orchestrator(ctx).await?;
177    let name = lifecycle::resolve_agent_name(agent).await?;
178    let service_id = orchestrator.start_agent(&name, None).await?;
179
180    CliService::success(&format!(
181        "Agent {} started successfully (service ID: {})",
182        agent, service_id
183    ));
184
185    Ok(())
186}
187
188pub(super) async fn execute_individual_mcp(
189    ctx: &Arc<AppContext>,
190    server_name: &str,
191    _config: &CliConfig,
192) -> Result<()> {
193    CliService::section(&format!("Starting MCP Server: {}", server_name));
194
195    let manager = lifecycle::mcp_orchestrator(ctx)?;
196    manager.start_services(Some(server_name.to_owned())).await?;
197
198    CliService::success(&format!("MCP server {} started successfully", server_name));
199
200    Ok(())
201}