1mod cleanup;
2pub mod restart;
3pub mod serve;
4mod start;
5mod status;
6mod stop;
7mod types;
8
9use crate::cli_settings::CliConfig;
10use crate::shared::render_result;
11use anyhow::{Context, Result};
12use clap::Subcommand;
13use std::sync::Arc;
14use systemprompt_runtime::AppContext;
15
16#[derive(Debug, Clone, Subcommand)]
17pub enum StartTarget {
18 Agent { agent_id: String },
19 Mcp { server_name: String },
20}
21
22#[derive(Debug, Clone, Subcommand)]
23pub enum StopTarget {
24 Agent {
25 agent_id: String,
26 #[arg(long, help = "Force stop (SIGKILL)")]
27 force: bool,
28 },
29 Mcp {
30 server_name: String,
31 #[arg(long, help = "Force stop (SIGKILL)")]
32 force: bool,
33 },
34}
35
36#[derive(Debug, Subcommand)]
37pub enum ServicesCommands {
38 #[command(
39 about = "Start API, agents, and MCP servers",
40 after_help = "EXAMPLES:\n systemprompt infra services start\n systemprompt infra \
41 services start --api\n systemprompt infra services start --agents --mcp\n \
42 systemprompt infra services start agent <name>"
43 )]
44 Start {
45 #[command(subcommand)]
46 target: Option<StartTarget>,
47
48 #[arg(long, help = "Start all services")]
49 all: bool,
50
51 #[arg(long, help = "Start API server only")]
52 api: bool,
53
54 #[arg(long, help = "Start agents only")]
55 agents: bool,
56
57 #[arg(long, help = "Start MCP servers only")]
58 mcp: bool,
59
60 #[arg(long, help = "Run in foreground (default)")]
61 foreground: bool,
62
63 #[arg(long, help = "Skip database migrations")]
64 skip_migrate: bool,
65
66 #[arg(long, help = "Kill process using the port if occupied")]
67 kill_port_process: bool,
68 },
69
70 #[command(
71 about = "Stop running services gracefully",
72 after_help = "EXAMPLES:\n systemprompt infra services stop\n systemprompt infra \
73 services stop --api\n systemprompt infra services stop agent <name> \
74 [--force]"
75 )]
76 Stop {
77 #[command(subcommand)]
78 target: Option<StopTarget>,
79
80 #[arg(long, help = "Stop all services")]
81 all: bool,
82
83 #[arg(long, help = "Stop API server only")]
84 api: bool,
85
86 #[arg(long, help = "Stop agents only")]
87 agents: bool,
88
89 #[arg(long, help = "Stop MCP servers only")]
90 mcp: bool,
91
92 #[arg(long, help = "Force stop (SIGKILL)")]
93 force: bool,
94 },
95
96 #[command(about = "Restart services")]
97 Restart {
98 #[command(subcommand)]
99 target: Option<RestartTarget>,
100
101 #[arg(long, help = "Restart only failed services")]
102 failed: bool,
103
104 #[arg(long, help = "Restart all agents")]
105 agents: bool,
106
107 #[arg(long, help = "Restart all MCP servers")]
108 mcp: bool,
109 },
110
111 #[command(about = "Show detailed service status")]
112 Status {
113 #[arg(long, help = "Show detailed information")]
114 detailed: bool,
115
116 #[arg(long, help = "Output as JSON")]
117 json: bool,
118
119 #[arg(long, help = "Include health check results")]
120 health: bool,
121 },
122
123 #[command(about = "Clean up orphaned processes and stale entries")]
124 Cleanup {
125 #[arg(short = 'y', long, help = "Skip confirmation prompt")]
126 yes: bool,
127
128 #[arg(long, help = "Preview cleanup without executing")]
129 dry_run: bool,
130 },
131
132 #[command(about = "Start API server (automatically starts agents and MCP servers)")]
133 Serve {
134 #[arg(long, help = "Run in foreground mode")]
135 foreground: bool,
136
137 #[arg(long, help = "Kill process using the port if occupied")]
138 kill_port_process: bool,
139 },
140}
141
142#[derive(Debug, Clone, Subcommand)]
143pub enum RestartTarget {
144 Api,
145 Agent {
146 agent_id: String,
147 },
148 Mcp {
149 server_name: String,
150 #[arg(long, help = "Rebuild the binary before restarting")]
151 build: bool,
152 },
153}
154
155pub async fn execute(command: ServicesCommands, config: &CliConfig) -> Result<()> {
156 match command {
157 ServicesCommands::Start {
158 target,
159 all,
160 api,
161 agents,
162 mcp,
163 foreground: _,
164 skip_migrate,
165 kill_port_process,
166 } => {
167 if let Some(individual) = target {
168 let ctx = Arc::new(
169 AppContext::new()
170 .await
171 .context("Failed to initialize application context")?,
172 );
173 return match individual {
174 StartTarget::Agent { agent_id } => {
175 start::execute_individual_agent(&ctx, &agent_id, config).await
176 },
177 StartTarget::Mcp { server_name } => {
178 start::execute_individual_mcp(&ctx, &server_name, config).await
179 },
180 };
181 }
182
183 let flags = start::ServiceFlags {
184 all,
185 targets: start::ServiceTargetFlags { api, agents, mcp },
186 };
187 let service_target = start::ServiceTarget::from_flags(flags);
188 let options = start::StartupOptions {
189 skip_migrate,
190 kill_port_process,
191 };
192 start::execute(service_target, options, config).await
193 },
194
195 ServicesCommands::Stop {
196 target,
197 all,
198 api,
199 agents,
200 mcp,
201 force,
202 } => {
203 if let Some(individual) = target {
204 let ctx = Arc::new(
205 AppContext::new()
206 .await
207 .context("Failed to initialize application context")?,
208 );
209 return match individual {
210 StopTarget::Agent { agent_id, force } => {
211 let result =
212 stop::execute_individual_agent(&ctx, &agent_id, force, config).await?;
213 render_result(&result);
214 Ok(())
215 },
216 StopTarget::Mcp { server_name, force } => {
217 let result =
218 stop::execute_individual_mcp(&ctx, &server_name, force, config).await?;
219 render_result(&result);
220 Ok(())
221 },
222 };
223 }
224
225 let flags = start::ServiceFlags {
226 all,
227 targets: start::ServiceTargetFlags { api, agents, mcp },
228 };
229 let service_target = start::ServiceTarget::from_flags(flags);
230 let result = stop::execute(service_target, force, config).await?;
231 render_result(&result);
232 Ok(())
233 },
234
235 ServicesCommands::Restart {
236 target,
237 failed,
238 agents,
239 mcp,
240 } => {
241 let ctx = Arc::new(
242 AppContext::new()
243 .await
244 .context("Failed to initialize application context")?,
245 );
246
247 let result = if failed {
248 restart::execute_failed(&ctx, config).await?
249 } else if agents {
250 restart::execute_all_agents(&ctx, config).await?
251 } else if mcp {
252 restart::execute_all_mcp(&ctx, config).await?
253 } else {
254 match target {
255 Some(RestartTarget::Api) => restart::execute_api(config).await?,
256 Some(RestartTarget::Agent { agent_id }) => {
257 restart::execute_agent(&ctx, &agent_id, config).await?
258 },
259 Some(RestartTarget::Mcp { server_name, build }) => {
260 restart::execute_mcp(&ctx, &server_name, build, config).await?
261 },
262 None => {
263 return Err(anyhow::anyhow!(
264 "Must specify target (api, agent, mcp) or use --failed/--agents/--mcp \
265 flag"
266 ))
267 },
268 }
269 };
270 render_result(&result);
271 Ok(())
272 },
273
274 ServicesCommands::Status {
275 detailed,
276 json,
277 health,
278 } => {
279 let result = status::execute(detailed, json, health, config).await?;
280 render_result(&result);
281 Ok(())
282 },
283
284 ServicesCommands::Cleanup { yes, dry_run } => {
285 let result = cleanup::execute(yes, dry_run, config).await?;
286 render_result(&result);
287 Ok(())
288 },
289
290 ServicesCommands::Serve {
291 foreground,
292 kill_port_process,
293 } => serve::execute(foreground, kill_port_process, config).await,
294 }
295}
296
297pub fn load_service_configs(
298 _ctx: &AppContext,
299) -> Result<Vec<systemprompt_scheduler::ServiceConfig>> {
300 use systemprompt_loader::ConfigLoader;
301 use systemprompt_scheduler::{ServiceConfig, ServiceType};
302
303 let services_config = ConfigLoader::load()?;
304 let mut configs = Vec::new();
305
306 for (name, agent) in &services_config.agents {
307 configs.push(ServiceConfig {
308 name: name.clone(),
309 service_type: ServiceType::Agent,
310 port: agent.port,
311 enabled: agent.enabled,
312 });
313 }
314
315 for (name, mcp) in &services_config.mcp_servers {
316 configs.push(ServiceConfig {
317 name: name.clone(),
318 service_type: ServiceType::Mcp,
319 port: mcp.port,
320 enabled: mcp.enabled,
321 });
322 }
323
324 Ok(configs)
325}