systemprompt-cli 0.1.22

systemprompt.io OS - CLI for agent orchestration, AI operations, and system management
Documentation
use anyhow::{Context, Result, bail};
use clap::{Args, Subcommand};
use std::fs;
use systemprompt_logging::CliService;
use systemprompt_models::{Profile, ProfileBootstrap};

use super::types::{CorsListOutput, CorsModifyOutput, ServerConfigOutput, ServerSetOutput};
use crate::CliConfig;
use crate::cli_settings::OutputFormat;
use crate::shared::{CommandResult, render_result};

#[derive(Debug, Subcommand)]
pub enum ServerCommands {
    #[command(about = "Show server configuration")]
    Show,

    #[command(about = "Set server configuration value")]
    Set(SetArgs),

    #[command(subcommand, about = "Manage CORS allowed origins")]
    Cors(CorsCommands),
}

#[derive(Debug, Clone, Args)]
pub struct SetArgs {
    #[arg(long, help = "Server host address")]
    pub host: Option<String>,

    #[arg(long, help = "Server port")]
    pub port: Option<u16>,

    #[arg(long, help = "Enable/disable HTTPS")]
    pub use_https: Option<bool>,

    #[arg(long, help = "API server URL")]
    pub api_server_url: Option<String>,

    #[arg(long, help = "API internal URL")]
    pub api_internal_url: Option<String>,

    #[arg(long, help = "API external URL")]
    pub api_external_url: Option<String>,
}

#[derive(Debug, Subcommand)]
pub enum CorsCommands {
    #[command(about = "List CORS allowed origins")]
    List,

    #[command(about = "Add a CORS origin")]
    Add(CorsAddArgs),

    #[command(about = "Remove a CORS origin")]
    Remove(CorsRemoveArgs),
}

#[derive(Debug, Clone, Args)]
pub struct CorsAddArgs {
    #[arg(help = "Origin URL to add (e.g., https://example.com)")]
    pub origin: String,
}

#[derive(Debug, Clone, Args)]
pub struct CorsRemoveArgs {
    #[arg(help = "Origin URL to remove")]
    pub origin: String,
}

pub fn execute(command: &ServerCommands, config: &CliConfig) -> Result<()> {
    match command {
        ServerCommands::Show => execute_show(config),
        ServerCommands::Set(args) => execute_set(args, config),
        ServerCommands::Cors(cmd) => execute_cors(cmd, config),
    }
}

fn execute_show(_config: &CliConfig) -> Result<()> {
    let profile = ProfileBootstrap::get()?;

    let output = ServerConfigOutput {
        host: profile.server.host.clone(),
        port: profile.server.port,
        api_server_url: profile.server.api_server_url.clone(),
        api_internal_url: profile.server.api_internal_url.clone(),
        api_external_url: profile.server.api_external_url.clone(),
        use_https: profile.server.use_https,
        cors_allowed_origins: profile.server.cors_allowed_origins.clone(),
    };

    render_result(&CommandResult::card(output).with_title("Server Configuration"));

    Ok(())
}

fn execute_set(args: &SetArgs, config: &CliConfig) -> Result<()> {
    if args.host.is_none()
        && args.port.is_none()
        && args.use_https.is_none()
        && args.api_server_url.is_none()
        && args.api_internal_url.is_none()
        && args.api_external_url.is_none()
    {
        bail!(
            "Must specify at least one option: --host, --port, --use-https, --api-server-url, \
             --api-internal-url, --api-external-url"
        );
    }

    let profile_path = ProfileBootstrap::get_path()?;
    let mut profile = load_profile(profile_path)?;

    let mut changes: Vec<ServerSetOutput> = Vec::new();

    if let Some(ref host) = args.host {
        let old = profile.server.host.clone();
        profile.server.host.clone_from(host);
        changes.push(ServerSetOutput {
            field: "host".to_string(),
            old_value: old,
            new_value: host.clone(),
            message: format!("Updated host to {}", host),
        });
    }

    if let Some(port) = args.port {
        let old = profile.server.port;
        profile.server.port = port;
        changes.push(ServerSetOutput {
            field: "port".to_string(),
            old_value: old.to_string(),
            new_value: port.to_string(),
            message: format!("Updated port to {}", port),
        });
    }

    if let Some(use_https) = args.use_https {
        let old = profile.server.use_https;
        profile.server.use_https = use_https;
        changes.push(ServerSetOutput {
            field: "use_https".to_string(),
            old_value: old.to_string(),
            new_value: use_https.to_string(),
            message: format!("Updated use_https to {}", use_https),
        });
    }

    if let Some(ref url) = args.api_server_url {
        let old = profile.server.api_server_url.clone();
        profile.server.api_server_url.clone_from(url);
        changes.push(ServerSetOutput {
            field: "api_server_url".to_string(),
            old_value: old,
            new_value: url.clone(),
            message: format!("Updated api_server_url to {}", url),
        });
    }

    if let Some(ref url) = args.api_internal_url {
        let old = profile.server.api_internal_url.clone();
        profile.server.api_internal_url.clone_from(url);
        changes.push(ServerSetOutput {
            field: "api_internal_url".to_string(),
            old_value: old,
            new_value: url.clone(),
            message: format!("Updated api_internal_url to {}", url),
        });
    }

    if let Some(ref url) = args.api_external_url {
        let old = profile.server.api_external_url.clone();
        profile.server.api_external_url.clone_from(url);
        changes.push(ServerSetOutput {
            field: "api_external_url".to_string(),
            old_value: old,
            new_value: url.clone(),
            message: format!("Updated api_external_url to {}", url),
        });
    }

    save_profile(&profile, profile_path)?;

    for change in &changes {
        render_result(&CommandResult::text(change.clone()).with_title("Server Updated"));
    }

    if config.output_format() == OutputFormat::Table {
        CliService::warning("Restart services for changes to take effect");
    }

    Ok(())
}

fn execute_cors(command: &CorsCommands, config: &CliConfig) -> Result<()> {
    match command {
        CorsCommands::List => execute_cors_list(),
        CorsCommands::Add(args) => execute_cors_add(args, config),
        CorsCommands::Remove(args) => execute_cors_remove(args, config),
    }
}

fn execute_cors_list() -> Result<()> {
    let profile = ProfileBootstrap::get()?;

    let output = CorsListOutput {
        origins: profile.server.cors_allowed_origins.clone(),
        count: profile.server.cors_allowed_origins.len(),
    };

    render_result(&CommandResult::list(output).with_title("CORS Allowed Origins"));

    Ok(())
}

fn execute_cors_add(args: &CorsAddArgs, config: &CliConfig) -> Result<()> {
    let profile_path = ProfileBootstrap::get_path()?;
    let mut profile = load_profile(profile_path)?;

    if profile.server.cors_allowed_origins.contains(&args.origin) {
        let output = CorsModifyOutput {
            action: "skipped".to_string(),
            origin: args.origin.clone(),
            message: format!("Origin {} already exists", args.origin),
        };
        render_result(&CommandResult::text(output).with_title("CORS Origin"));
        return Ok(());
    }

    profile
        .server
        .cors_allowed_origins
        .push(args.origin.clone());
    save_profile(&profile, profile_path)?;

    let output = CorsModifyOutput {
        action: "added".to_string(),
        origin: args.origin.clone(),
        message: format!("Added CORS origin: {}", args.origin),
    };
    render_result(&CommandResult::text(output).with_title("CORS Origin Added"));

    if config.output_format() == OutputFormat::Table {
        CliService::warning("Restart services for changes to take effect");
    }

    Ok(())
}

fn execute_cors_remove(args: &CorsRemoveArgs, config: &CliConfig) -> Result<()> {
    let profile_path = ProfileBootstrap::get_path()?;
    let mut profile = load_profile(profile_path)?;

    let original_len = profile.server.cors_allowed_origins.len();
    profile
        .server
        .cors_allowed_origins
        .retain(|o| o != &args.origin);

    if profile.server.cors_allowed_origins.len() == original_len {
        let output = CorsModifyOutput {
            action: "skipped".to_string(),
            origin: args.origin.clone(),
            message: format!("Origin {} not found", args.origin),
        };
        render_result(&CommandResult::text(output).with_title("CORS Origin"));
        return Ok(());
    }

    save_profile(&profile, profile_path)?;

    let output = CorsModifyOutput {
        action: "removed".to_string(),
        origin: args.origin.clone(),
        message: format!("Removed CORS origin: {}", args.origin),
    };
    render_result(&CommandResult::text(output).with_title("CORS Origin Removed"));

    if config.output_format() == OutputFormat::Table {
        CliService::warning("Restart services for changes to take effect");
    }

    Ok(())
}

fn load_profile(path: &str) -> Result<Profile> {
    let content =
        fs::read_to_string(path).with_context(|| format!("Failed to read profile: {}", path))?;
    let profile: Profile = serde_yaml::from_str(&content)
        .with_context(|| format!("Failed to parse profile: {}", path))?;
    Ok(profile)
}

fn save_profile(profile: &Profile, path: &str) -> Result<()> {
    let content = serde_yaml::to_string(profile).context("Failed to serialize profile")?;
    fs::write(path, content).with_context(|| format!("Failed to write profile: {}", path))?;
    Ok(())
}