Skip to main content

systemprompt_cli/commands/admin/config/
services.rs

1//! `admin config services` command: show and edit profile-level services
2//! overrides.
3//!
4//! [`ServicesCommands`] reports and updates the port offset applied to the
5//! committed services manifests, letting a second installation on one host
6//! move its MCP and agent ports without editing tracked config.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use anyhow::{Context, Result, bail};
12use clap::{Args, Subcommand};
13use std::fs;
14use systemprompt_config::ProfileBootstrap;
15use systemprompt_logging::CliService;
16use systemprompt_models::Profile;
17
18use super::runtime::save_profile;
19use super::types::{ServicesConfigOutput, ServicesSetOutput};
20use crate::CliConfig;
21use crate::cli_settings::OutputFormat;
22use crate::shared::{CommandOutput, render_result};
23
24#[derive(Debug, Clone, Copy, Subcommand)]
25pub enum ServicesCommands {
26    #[command(about = "Show services configuration", alias = "list")]
27    Show,
28
29    #[command(about = "Set services configuration value")]
30    Set(SetArgs),
31}
32
33#[derive(Debug, Clone, Copy, Args)]
34pub struct SetArgs {
35    #[arg(
36        long,
37        help = "Shift every locally-bound MCP and agent port by this amount"
38    )]
39    pub port_offset: Option<u16>,
40}
41
42pub fn execute(command: &ServicesCommands, config: &CliConfig) -> Result<()> {
43    match command {
44        ServicesCommands::Show => execute_show(config),
45        ServicesCommands::Set(args) => execute_set(args, config),
46    }
47}
48
49pub(super) fn execute_show(config: &CliConfig) -> Result<()> {
50    let profile = ProfileBootstrap::get()?;
51
52    let output = ServicesConfigOutput {
53        port_offset: profile.services.port_offset,
54    };
55
56    render_result(
57        &CommandOutput::card_value("Services Configuration", &output),
58        config,
59    );
60
61    Ok(())
62}
63
64pub(super) fn execute_set(args: &SetArgs, config: &CliConfig) -> Result<()> {
65    let Some(port_offset) = args.port_offset else {
66        bail!("Must specify at least one option: --port-offset");
67    };
68
69    let profile_path = ProfileBootstrap::get_path()?;
70    let mut profile = load_profile(profile_path)?;
71
72    let old = profile.services.port_offset;
73    profile.services.port_offset = port_offset;
74
75    save_profile(&profile, profile_path)?;
76
77    let change = ServicesSetOutput {
78        field: "port_offset".to_owned(),
79        old_value: old.to_string(),
80        new_value: port_offset.to_string(),
81        message: format!("Updated port_offset to {port_offset}"),
82    };
83
84    render_result(
85        &CommandOutput::card_value("Services Updated", &change),
86        config,
87    );
88
89    if config.output_format() == OutputFormat::Table {
90        CliService::warning("Restart services for changes to take effect");
91    }
92
93    Ok(())
94}
95
96fn load_profile(path: &str) -> Result<Profile> {
97    let content =
98        fs::read_to_string(path).with_context(|| format!("Failed to read profile: {path}"))?;
99    let profile: Profile = serde_yaml::from_str(&content)
100        .with_context(|| format!("Failed to parse profile: {path}"))?;
101    Ok(profile)
102}