Skip to main content

systemprompt_cli/
lib.rs

1mod args;
2mod bootstrap;
3pub mod cli_settings;
4mod commands;
5pub mod descriptor;
6pub mod environment;
7pub mod interactive;
8pub mod paths;
9mod presentation;
10mod routing;
11pub mod session;
12pub mod shared;
13
14pub use cli_settings::{CliConfig, ColorMode, OutputFormat, VerbosityLevel};
15pub use commands::{admin, analytics, build, cloud, core, infrastructure, plugins, web};
16
17use anyhow::{bail, Context, Result};
18use clap::Parser;
19use systemprompt_logging::{set_startup_mode, CliService};
20use systemprompt_models::ProfileBootstrap;
21use systemprompt_runtime::DatabaseContext;
22
23use crate::descriptor::{CommandDescriptor, DescribeCommand};
24
25pub async fn run() -> Result<()> {
26    let cli = args::Cli::parse();
27
28    set_startup_mode(cli.command.is_none());
29
30    let cli_config = args::build_cli_config(&cli);
31    cli_settings::set_global_config(cli_config.clone());
32
33    if cli.display.no_color || !cli_config.should_use_color() {
34        console::set_colors_enabled(false);
35    }
36
37    if let Some(database_url) = cli.database.database_url.clone() {
38        return run_with_database_url(cli.command, &cli_config, &database_url).await;
39    }
40
41    let desc = cli
42        .command
43        .as_ref()
44        .map_or(CommandDescriptor::FULL, DescribeCommand::descriptor);
45
46    if !desc.database {
47        systemprompt_logging::init_console_logging();
48    }
49
50    if desc.profile {
51        init_profile_and_route(&cli, &desc, &cli_config).await?;
52    }
53
54    dispatch_command(cli.command, &cli_config).await
55}
56
57async fn init_profile_and_route(
58    cli: &args::Cli,
59    desc: &CommandDescriptor,
60    cli_config: &CliConfig,
61) -> Result<()> {
62    let profile_path = bootstrap::resolve_profile(cli_config.profile_override.as_deref())?;
63    bootstrap::init_profile(&profile_path)?;
64
65    let profile = ProfileBootstrap::get()?;
66
67    if cli_config.output_format == OutputFormat::Table
68        && cli_config.verbosity != VerbosityLevel::Quiet
69    {
70        let tenant = profile.cloud.as_ref().and_then(|c| c.tenant_id.as_deref());
71        CliService::profile_banner(&profile.name, profile.target.is_cloud(), tenant);
72    }
73
74    let is_cloud = profile.target.is_cloud();
75    let env = environment::ExecutionEnvironment::detect();
76
77    if !env.is_fly && desc.remote_eligible {
78        try_remote_routing(cli, profile).await?;
79    } else if is_cloud
80        && !env.is_fly
81        && !profile.database.external_db_access
82        && !matches!(
83            cli.command.as_ref(),
84            Some(
85                args::Commands::Cloud(_) | args::Commands::Admin(admin::AdminCommands::Session(_))
86            )
87        )
88    {
89        bail!(
90            "Cloud profile '{}' selected but this command doesn't support remote execution.\nUse \
91             a local profile with --profile <name> or enable external database access.",
92            profile.name
93        );
94    }
95
96    if !is_cloud || profile.database.external_db_access {
97        if let Err(e) = bootstrap::init_credentials().await {
98            tracing::debug!(error = %e, "Cloud credentials not available, continuing in local-only mode");
99        }
100    }
101
102    if desc.secrets {
103        bootstrap::init_secrets()?;
104    }
105
106    if desc.paths {
107        bootstrap::init_paths()?;
108        if !desc.skip_validation {
109            bootstrap::run_validation()?;
110        }
111    }
112
113    if !is_cloud {
114        bootstrap::validate_cloud_credentials(&env);
115    }
116
117    Ok(())
118}
119
120async fn try_remote_routing(cli: &args::Cli, profile: &systemprompt_models::Profile) -> Result<()> {
121    let is_cloud = profile.target.is_cloud();
122
123    match routing::determine_execution_target() {
124        Ok(routing::ExecutionTarget::Remote {
125            hostname,
126            token,
127            context_id,
128        }) => {
129            let args = args::reconstruct_args(cli);
130            let exit_code =
131                routing::remote::execute_remote(&hostname, &token, &context_id, &args, 300).await?;
132            #[allow(clippy::exit)]
133            std::process::exit(exit_code);
134        },
135        Ok(routing::ExecutionTarget::Local) if is_cloud => {
136            require_external_db_access(profile, "no tenant is configured")?;
137        },
138        Err(e) if is_cloud => {
139            require_external_db_access(profile, &format!("routing failed: {}", e))?;
140        },
141        _ => {},
142    }
143
144    Ok(())
145}
146
147fn require_external_db_access(profile: &systemprompt_models::Profile, reason: &str) -> Result<()> {
148    if profile.database.external_db_access {
149        tracing::debug!(
150            profile_name = %profile.name,
151            reason = reason,
152            "Cloud profile allowing local execution via external_db_access"
153        );
154        Ok(())
155    } else {
156        bail!(
157            "Cloud profile '{}' requires remote execution but {}.\nRun 'systemprompt admin \
158             session login' to authenticate.",
159            profile.name,
160            reason
161        )
162    }
163}
164
165async fn dispatch_command(command: Option<args::Commands>, config: &CliConfig) -> Result<()> {
166    match command {
167        Some(args::Commands::Core(cmd)) => core::execute(cmd, config).await?,
168        Some(args::Commands::Infra(cmd)) => infrastructure::execute(cmd, config).await?,
169        Some(args::Commands::Admin(cmd)) => admin::execute(cmd, config).await?,
170        Some(args::Commands::Cloud(cmd)) => cloud::execute(cmd, config).await?,
171        Some(args::Commands::Analytics(cmd)) => analytics::execute(cmd, config).await?,
172        Some(args::Commands::Web(cmd)) => web::execute(cmd)?,
173        Some(args::Commands::Plugins(cmd)) => plugins::execute(cmd, config).await?,
174        Some(args::Commands::Build(cmd)) => {
175            build::execute(cmd, config)?;
176        },
177        None => {
178            args::Cli::parse_from(["systemprompt", "--help"]);
179        },
180    }
181
182    Ok(())
183}
184
185async fn run_with_database_url(
186    command: Option<args::Commands>,
187    config: &CliConfig,
188    database_url: &str,
189) -> Result<()> {
190    let db_ctx = DatabaseContext::from_url(database_url)
191        .await
192        .context("Failed to connect to database")?;
193
194    systemprompt_logging::init_logging(db_ctx.db_pool_arc());
195
196    match command {
197        Some(args::Commands::Core(cmd)) => core::execute_with_db(cmd, &db_ctx, config).await,
198        Some(args::Commands::Infra(cmd)) => {
199            infrastructure::execute_with_db(cmd, &db_ctx, config).await
200        },
201        Some(args::Commands::Admin(cmd)) => admin::execute_with_db(cmd, &db_ctx, config).await,
202        Some(args::Commands::Analytics(cmd)) => {
203            analytics::execute_with_db(cmd, &db_ctx, config).await
204        },
205        Some(_) => {
206            bail!("This command requires full profile initialization. Remove --database-url flag.")
207        },
208        None => bail!("No subcommand provided. Use --help to see available commands."),
209    }
210}