systemprompt-cli 0.1.22

systemprompt.io OS - CLI for agent orchestration, AI operations, and system management
Documentation
use crate::shared::CommandResult;
use anyhow::{Context, Result};
use dialoguer::theme::ColorfulTheme;
use dialoguer::{Confirm, Input};
use std::path::{Path, PathBuf};
use systemprompt_logging::CliService;

use super::postgres::PostgresConfig;
use super::types::{DatabaseSetupInfo, SecretsConfiguredInfo, SetupOutput};
use super::{SetupArgs, postgres, profile, secrets};
use crate::CliConfig;

pub async fn execute(args: SetupArgs, config: &CliConfig) -> Result<CommandResult<SetupOutput>> {
    if !config.is_json_output() {
        CliService::section("systemprompt.io Setup Wizard");
    }

    let project_root = detect_project_root()?;
    let project_name = project_root
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("systemprompt");

    if !config.is_json_output() {
        CliService::success(&format!(
            "Project: {} ({})",
            project_name,
            project_root.display()
        ));
    }

    let systemprompt_dir = project_root.join(".systemprompt");

    let env_name = get_environment_name(&args, config)?;

    if !config.is_json_output() {
        CliService::info(&format!("Configuring environment: {}", env_name));
    }

    if !args.dry_run && !args.yes && config.is_interactive() {
        let confirmed = Confirm::with_theme(&ColorfulTheme::default())
            .with_prompt(format!(
                "This will create/update configuration for '{}' environment. Continue?",
                env_name
            ))
            .default(true)
            .interact()?;

        if !confirmed {
            let output = SetupOutput {
                environment: env_name.clone(),
                profile_path: String::new(),
                database: DatabaseSetupInfo {
                    host: args.db_host.clone(),
                    port: args.db_port,
                    name: args.effective_db_name(&env_name),
                    user: args.effective_db_user(&env_name),
                    connection_status: "cancelled".to_string(),
                    docker: args.docker,
                },
                secrets_configured: SecretsConfiguredInfo {
                    anthropic: args.anthropic_key.is_some(),
                    openai: args.openai_key.is_some(),
                    gemini: args.gemini_key.is_some(),
                    github: args.github_token.is_some(),
                },
                migrations_run: false,
                message: "Setup cancelled by user".to_string(),
            };

            if !config.is_json_output() {
                CliService::info("Setup cancelled");
            }

            let result = CommandResult::text(output).with_title("Setup Cancelled");
            if config.is_json_output() {
                return Ok(result);
            }
            return Ok(result.with_skip_render());
        }
    }

    if args.dry_run {
        return Ok(execute_dry_run(&args, &env_name, &systemprompt_dir, config));
    }

    if !config.is_json_output() {
        CliService::section(&format!("Setting up '{}' environment", env_name));
    }

    let pg_config = setup_postgres(&args, config, &env_name).await?;

    let connection_status = if postgres::test_connection(&pg_config).await {
        "connected"
    } else {
        "unreachable"
    };

    let mut secrets_data = collect_secrets(&args, config, &env_name)?;
    secrets_data.database_url = Some(pg_config.database_url());

    let secrets_path = secrets::default_path(&systemprompt_dir, &env_name);
    secrets::save(&secrets_data, &secrets_path)?;

    let relative_secrets_path = format!("../secrets/{}.secrets.json", env_name);
    let profile_data = profile::build(&env_name, &relative_secrets_path, &project_root, None)?;
    let profile_path = profile::default_path(&systemprompt_dir, &env_name);
    profile::save(&profile_data, &profile_path)?;

    match profile_data.validate() {
        Ok(()) => {
            if !config.is_json_output() {
                CliService::success("Profile validated successfully");
            }
        },
        Err(e) => {
            if !config.is_json_output() {
                CliService::warning(&format!("Profile validation warnings: {}", e));
            }
        },
    }

    let run_migrations = should_run_migrations(&args, config)?;
    if run_migrations {
        profile::run_migrations(&profile_path)?;
    }

    let output = SetupOutput {
        environment: env_name.clone(),
        profile_path: profile_path.to_string_lossy().to_string(),
        database: DatabaseSetupInfo {
            host: pg_config.host.clone(),
            port: pg_config.port,
            name: pg_config.database.clone(),
            user: pg_config.user.clone(),
            connection_status: connection_status.to_string(),
            docker: args.docker,
        },
        secrets_configured: SecretsConfiguredInfo {
            anthropic: secrets_data.anthropic.is_some(),
            openai: secrets_data.openai.is_some(),
            gemini: secrets_data.gemini.is_some(),
            github: secrets_data.github.is_some(),
        },
        migrations_run: run_migrations,
        message: format!("Environment '{}' setup completed successfully", env_name),
    };

    if !config.is_json_output() {
        print_summary(&env_name, &profile_path);
    }

    let result = CommandResult::text(output).with_title("Setup Complete");
    if config.is_json_output() {
        Ok(result)
    } else {
        Ok(result.with_skip_render())
    }
}

fn execute_dry_run(
    args: &SetupArgs,
    env_name: &str,
    systemprompt_dir: &Path,
    config: &CliConfig,
) -> CommandResult<SetupOutput> {
    if !config.is_json_output() {
        CliService::section("Dry Run - No changes will be made");
    }

    let profile_path = profile::default_path(systemprompt_dir, env_name);
    let secrets_path = secrets::default_path(systemprompt_dir, env_name);

    let connection_status = if args.docker {
        "docker_pending"
    } else if postgres::detect_postgresql(&args.db_host, args.db_port) {
        "reachable"
    } else {
        "unreachable"
    };

    if !config.is_json_output() {
        CliService::subsection("Configuration Preview");
        CliService::key_value("Environment", env_name);
        CliService::key_value("Profile path", &profile_path.to_string_lossy());
        CliService::key_value("Secrets path", &secrets_path.to_string_lossy());

        CliService::subsection("Database");
        CliService::key_value("Host", &args.db_host);
        CliService::key_value("Port", &args.db_port.to_string());
        CliService::key_value("User", &args.effective_db_user(env_name));
        CliService::key_value("Database", &args.effective_db_name(env_name));
        CliService::key_value("Docker", if args.docker { "yes" } else { "no" });
        CliService::key_value("Connection", connection_status);

        CliService::subsection("API Keys");
        CliService::key_value(
            "Anthropic",
            if args.anthropic_key.is_some() {
                "configured"
            } else {
                "not set"
            },
        );
        CliService::key_value(
            "OpenAI",
            if args.openai_key.is_some() {
                "configured"
            } else {
                "not set"
            },
        );
        CliService::key_value(
            "Gemini",
            if args.gemini_key.is_some() {
                "configured"
            } else {
                "not set"
            },
        );
        CliService::key_value(
            "GitHub",
            if args.github_token.is_some() {
                "configured"
            } else {
                "not set"
            },
        );

        CliService::subsection("Migrations");
        let migration_status = if args.migrate {
            "will run"
        } else if args.no_migrate {
            "skipped"
        } else {
            "will prompt (interactive)"
        };
        CliService::key_value("Status", migration_status);

        CliService::info("");
        CliService::info("Run without --dry-run to execute setup");
    }

    let output = SetupOutput {
        environment: env_name.to_string(),
        profile_path: profile_path.to_string_lossy().to_string(),
        database: DatabaseSetupInfo {
            host: args.db_host.clone(),
            port: args.db_port,
            name: args.effective_db_name(env_name),
            user: args.effective_db_user(env_name),
            connection_status: connection_status.to_string(),
            docker: args.docker,
        },
        secrets_configured: SecretsConfiguredInfo {
            anthropic: args.anthropic_key.is_some(),
            openai: args.openai_key.is_some(),
            gemini: args.gemini_key.is_some(),
            github: args.github_token.is_some(),
        },
        migrations_run: false,
        message: "Dry run completed - no changes made".to_string(),
    };

    let result = CommandResult::text(output).with_title("Setup Dry Run");
    if config.is_json_output() {
        result
    } else {
        result.with_skip_render()
    }
}

fn get_environment_name(args: &SetupArgs, config: &CliConfig) -> Result<String> {
    if let Some(ref env) = args.environment {
        return Ok(env.clone());
    }

    if !config.is_interactive() {
        return Ok("dev".to_string());
    }

    CliService::info("Enter environment name (e.g., 'dev', 'staging', 'prod')");
    CliService::info("Press Enter for default: dev");

    let input: String = Input::with_theme(&ColorfulTheme::default())
        .with_prompt("Environment name")
        .default("dev".to_string())
        .interact_text()?;

    Ok(input.trim().to_lowercase())
}

async fn setup_postgres(
    args: &SetupArgs,
    config: &CliConfig,
    env_name: &str,
) -> Result<PostgresConfig> {
    if !config.is_interactive() {
        return postgres::setup_non_interactive(args, env_name, config).await;
    }
    postgres::setup_interactive(args, env_name, config).await
}

fn collect_secrets(
    args: &SetupArgs,
    config: &CliConfig,
    env_name: &str,
) -> Result<secrets::SecretsData> {
    if !config.is_interactive() {
        return secrets::collect_non_interactive(args, config);
    }
    secrets::collect_interactive(args, env_name, config)
}

fn should_run_migrations(args: &SetupArgs, config: &CliConfig) -> Result<bool> {
    if args.migrate {
        return Ok(true);
    }
    if args.no_migrate {
        return Ok(false);
    }
    if !config.is_interactive() {
        return Ok(false);
    }

    let run = Confirm::with_theme(&ColorfulTheme::default())
        .with_prompt("Run database migrations now?")
        .default(true)
        .interact()?;

    Ok(run)
}

fn detect_project_root() -> Result<PathBuf> {
    let cwd = std::env::current_dir().context("Failed to get current directory")?;

    let indicators = ["Cargo.toml", "services", ".systemprompt", "core"];

    for indicator in indicators {
        if cwd.join(indicator).exists() {
            return Ok(cwd);
        }
    }

    let mut current = cwd.clone();
    for _ in 0..5 {
        if let Some(parent) = current.parent() {
            for indicator in indicators {
                if parent.join(indicator).exists() {
                    return Ok(parent.to_path_buf());
                }
            }
            current = parent.to_path_buf();
        } else {
            break;
        }
    }

    Ok(cwd)
}

fn print_summary(env_name: &str, profile_path: &Path) {
    CliService::section("Setup Complete!");

    CliService::info(&format!(
        "Created profile: {} -> {}",
        env_name,
        profile_path.display()
    ));

    CliService::section("Next Steps");

    CliService::info(&format!(
        "1. Set your profile environment variable for '{}':",
        env_name
    ));
    CliService::info(&format!(
        "   export SYSTEMPROMPT_PROFILE={}",
        profile_path.display()
    ));
    CliService::info("");
    CliService::info("2. Start services:");
    CliService::info("   just start");
    CliService::info("");
    CliService::info("3. (Optional) Configure cloud deployment:");
    CliService::info("   systemprompt cloud login");
    CliService::info("   systemprompt cloud config");
}