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