systemprompt_cli/commands/infrastructure/services/
start.rs1use 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}