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_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}