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            super::serve::ServeOptions {
139                foreground: true,
140                kill_port_process: options.kill_port_process,
141                run_migrations: false,
142            },
143            &ctx.cli,
144            Some(events),
145        )
146        .await?;
147        return Ok(api_url);
148    }
149
150    if plan.agents_standalone_notice {
151        events.phase_started(Phase::Agents);
152        events.warning("Standalone agent start not supported");
153        events.info("Agents are managed by the API server lifecycle");
154        events.info("Use 'services start' or 'services serve' to start all services");
155        events.phase_completed(Phase::Agents);
156    }
157
158    if plan.mcp_standalone_notice {
159        events.phase_started(Phase::McpServers);
160        events.warning("Standalone MCP server start not supported");
161        events.info("MCP servers are managed by the API server lifecycle");
162        events.info("Use 'services start' or 'services serve' to start all services");
163        events.phase_completed(Phase::McpServers);
164    }
165
166    Ok(format!(
167        "http://127.0.0.1:{}",
168        ProfileBootstrap::get().map_or(8080, |p| p.server.port)
169    ))
170}
171
172pub(super) async fn execute_individual_agent(
173    ctx: &Arc<AppContext>,
174    agent: &str,
175    _config: &CliConfig,
176) -> Result<()> {
177    CliService::section(&format!("Starting Agent: {}", agent));
178
179    let orchestrator = lifecycle::agent_orchestrator(ctx).await?;
180    let name = lifecycle::resolve_agent_name(agent).await?;
181    let service_id = orchestrator.start_agent(&name, None).await?;
182
183    CliService::success(&format!(
184        "Agent {} started successfully (service ID: {})",
185        agent, service_id
186    ));
187
188    Ok(())
189}
190
191pub(super) async fn execute_individual_mcp(
192    ctx: &Arc<AppContext>,
193    server_name: &str,
194    _config: &CliConfig,
195) -> Result<()> {
196    CliService::section(&format!("Starting MCP Server: {}", server_name));
197
198    let manager = lifecycle::mcp_orchestrator(ctx)?;
199    manager.start_services(Some(server_name.to_owned())).await?;
200
201    CliService::success(&format!("MCP server {} started successfully", server_name));
202
203    Ok(())
204}