Skip to main content

systemprompt_cli/commands/infrastructure/services/
start.rs

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