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