systemprompt-cli 0.2.2

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
pub mod auth;
pub mod db;
mod deploy;
pub mod dockerfile;
mod domain;
mod init;
pub mod profile;
mod restart;
mod secrets;
mod status;
pub mod sync;
pub mod templates;
pub mod tenant;
pub mod types;

pub use systemprompt_cloud::{Environment, OAuthProvider};

use crate::cli_settings::CliConfig;
use crate::descriptor::{CommandDescriptor, DescribeCommand};
use anyhow::Result;
use clap::Subcommand;

#[derive(Debug, Subcommand)]
pub enum CloudCommands {
    #[command(subcommand, about = "Authentication (login, logout, whoami)")]
    Auth(auth::AuthCommands),

    #[command(about = "Initialize project structure")]
    Init {
        #[arg(long)]
        force: bool,
    },

    #[command(subcommand_required = false, about = "Manage tenants (local or cloud)")]
    Tenant {
        #[command(subcommand)]
        command: Option<tenant::TenantCommands>,
    },

    #[command(subcommand_required = false, about = "Manage profiles")]
    Profile {
        #[command(subcommand)]
        command: Option<profile::ProfileCommands>,
    },

    #[command(about = "Deploy to systemprompt.io Cloud")]
    Deploy {
        #[arg(long)]
        skip_push: bool,

        #[arg(long, short = 'p', help = "Profile name to deploy")]
        profile: Option<String>,

        #[arg(
            long,
            help = "Skip pre-deploy sync from cloud (WARNING: may lose runtime files)"
        )]
        no_sync: bool,

        #[arg(short = 'y', long, help = "Skip confirmation prompts")]
        yes: bool,

        #[arg(long, help = "Preview sync without deploying")]
        dry_run: bool,
    },

    #[command(about = "Check cloud deployment status")]
    Status,

    #[command(about = "Restart tenant machine")]
    Restart {
        #[arg(long)]
        tenant: Option<String>,

        #[arg(short = 'y', long, help = "Skip confirmation prompts")]
        yes: bool,
    },

    #[command(
        subcommand_required = false,
        about = "Sync between local and cloud environments"
    )]
    Sync {
        #[command(subcommand)]
        command: Option<sync::SyncCommands>,
    },

    #[command(subcommand, about = "Manage secrets for cloud tenant")]
    Secrets(secrets::SecretsCommands),

    #[command(about = "Generate Dockerfile based on discovered extensions")]
    Dockerfile,

    #[command(subcommand, about = "Cloud database operations")]
    Db(db::CloudDbCommands),

    #[command(subcommand, about = "Manage custom domain and TLS certificates")]
    Domain(domain::DomainCommands),
}

impl DescribeCommand for CloudCommands {
    fn descriptor(&self) -> CommandDescriptor {
        match self {
            Self::Sync {
                command: Some(sync::SyncCommands::Local(_)),
            } => CommandDescriptor::PROFILE_SECRETS_AND_PATHS,
            Self::Deploy { .. } => CommandDescriptor::PROFILE_AND_SECRETS,
            Self::Sync { command: Some(_) } | Self::Secrets { .. } => {
                CommandDescriptor::PROFILE_AND_SECRETS
            },
            Self::Status | Self::Restart { .. } | Self::Domain { .. } => {
                CommandDescriptor::PROFILE_ONLY
            },
            _ => CommandDescriptor::NONE,
        }
    }
}

impl CloudCommands {
    pub const fn requires_profile(&self) -> bool {
        matches!(
            self,
            Self::Sync { command: Some(_) }
                | Self::Status
                | Self::Restart { .. }
                | Self::Secrets { .. }
                | Self::Domain { .. }
        )
    }

    pub const fn requires_secrets(&self) -> bool {
        matches!(self, Self::Sync { command: Some(_) } | Self::Secrets { .. })
    }
}

pub async fn execute(cmd: CloudCommands, config: &CliConfig) -> Result<()> {
    match cmd {
        CloudCommands::Auth(cmd) => auth::execute(cmd, config).await,
        CloudCommands::Init { force } => init::execute(force, config),
        CloudCommands::Tenant { command } => tenant::execute(command, config).await,
        CloudCommands::Profile { command } => profile::execute(command, config).await,
        CloudCommands::Deploy {
            skip_push,
            profile,
            no_sync,
            yes,
            dry_run,
        } => {
            deploy::execute(
                deploy::DeployArgs {
                    skip_push,
                    profile_name: profile,
                    no_sync,
                    yes,
                    dry_run,
                },
                config,
            )
            .await
        },
        CloudCommands::Status => {
            let result = status::execute(config).await?;
            crate::shared::render_result(&result);
            Ok(())
        },
        CloudCommands::Restart { tenant, yes } => {
            let result = restart::execute(tenant, yes, config).await?;
            crate::shared::render_result(&result);
            Ok(())
        },
        CloudCommands::Sync { command } => sync::execute(command, config).await,
        CloudCommands::Secrets(cmd) => secrets::execute(cmd, config).await,
        CloudCommands::Dockerfile => execute_dockerfile(config),
        CloudCommands::Db(cmd) => db::execute(cmd, config).await,
        CloudCommands::Domain(cmd) => domain::execute(cmd, config).await,
    }
}

fn execute_dockerfile(config: &CliConfig) -> Result<()> {
    use crate::shared::project::ProjectRoot;
    use types::DockerfileOutput;

    let project = ProjectRoot::discover().map_err(|e| anyhow::anyhow!("{}", e))?;
    let content = dockerfile::generate_dockerfile_content(project.as_path());

    let output = DockerfileOutput {
        content: content.clone(),
    };

    if config.is_json_output() {
        crate::shared::render_result(
            &crate::shared::CommandResult::copy_paste(output).with_title("Dockerfile"),
        );
    } else {
        systemprompt_logging::CliService::info(&content);
    }

    Ok(())
}