Skip to main content

systemprompt_cli/runner/
mod.rs

1//! CLI runtime entry point and bootstrap helpers.
2//!
3//! Owns argument parsing (`args`), profile/secrets bootstrap (`bootstrap`),
4//! and cloud routing ([`routing`], [`profile_routing`]). [`run`] is the
5//! process entry point.
6//!
7//! Copyright (c) systemprompt.io — Business Source License 1.1.
8//! See <https://systemprompt.io> for licensing details.
9
10pub mod args;
11mod bootstrap;
12mod db_url;
13pub mod profile_routing;
14pub mod routing;
15mod structured_output;
16
17use anyhow::{Context, Result, bail};
18use clap::Parser;
19use systemprompt_logging::set_startup_mode;
20use systemprompt_runtime::DatabaseContext;
21
22use crate::cli_settings::{CliConfig, OutputFormat};
23use crate::commands::{admin, analytics, cloud, core, infrastructure, plugins, web};
24use crate::context::CommandContext;
25use crate::descriptor::{CommandDescriptor, DescribeCommand};
26use crate::env_overrides::EnvOverrides;
27
28pub async fn run() -> Result<()> {
29    let outcome = Box::pin(run_inner()).await;
30    structured_output::finalize(&outcome);
31    outcome
32}
33
34async fn run_inner() -> Result<()> {
35    let cli = args::Cli::parse();
36
37    set_startup_mode(cli.command.is_none());
38
39    let env = EnvOverrides::from_process_env();
40    let cli_config = args::build_cli_config(&cli, &env);
41    systemprompt_logging::set_structured_output(cli_config.output_format() != OutputFormat::Table);
42
43    if cli.display.no_color || !cli_config.should_use_color() {
44        console::set_colors_enabled(false);
45    }
46
47    if let Some(database_url) = cli.database.database_url.clone() {
48        match cli.command.as_ref().map(args::Commands::db_url_routing) {
49            Some(db_url::DbUrlRouting::Direct) => {
50                return Box::pin(run_with_database_url(
51                    cli.command,
52                    cli_config,
53                    env,
54                    &database_url,
55                ))
56                .await;
57            },
58            Some(db_url::DbUrlRouting::Unsupported) => bail!(
59                "This command cannot run with --database-url; it requires full profile \
60                 initialization. Remove --database-url."
61            ),
62            Some(db_url::DbUrlRouting::ProfileDriven) | None => {},
63        }
64    }
65
66    let desc = cli
67        .command
68        .as_ref()
69        .map_or(CommandDescriptor::FULL, DescribeCommand::descriptor);
70
71    if !desc.database() {
72        let effective_level = resolve_log_level(&cli_config, &env);
73        systemprompt_logging::init_console_logging_with_level(effective_level.as_deref());
74    }
75
76    if desc.profile()
77        && let Some(external_db_url) =
78            profile_routing::bootstrap_profile(&cli, &desc, &cli_config, &env).await?
79    {
80        return Box::pin(run_with_database_url(
81            cli.command,
82            cli_config,
83            env,
84            &external_db_url,
85        ))
86        .await;
87    }
88
89    let ctx = CommandContext::new(cli_config, env);
90    Box::pin(dispatch_command(cli.command, &ctx)).await
91}
92
93fn resolve_log_level(cli_config: &CliConfig, env: &EnvOverrides) -> Option<String> {
94    if env.rust_log.is_some() {
95        return None;
96    }
97
98    if let Some(level) = cli_config.verbosity.as_tracing_filter() {
99        return Some(level.to_owned());
100    }
101
102    if let Ok(resolved) = bootstrap::resolve_profile(cli_config.profile_override.as_deref(), env)
103        && let Some(log_level) = bootstrap::try_load_log_level(&resolved.path)
104    {
105        return Some(log_level.as_tracing_filter().to_owned());
106    }
107
108    Some("warn".to_owned())
109}
110
111async fn dispatch_command(command: Option<args::Commands>, ctx: &CommandContext) -> Result<()> {
112    if ctx.is_database_scoped() {
113        match &command {
114            Some(
115                args::Commands::Core(_)
116                | args::Commands::Infra(_)
117                | args::Commands::Admin(_)
118                | args::Commands::Analytics(_),
119            ) => {},
120            Some(_) => bail!(
121                "This command requires full profile initialization. Remove --database-url flag."
122            ),
123            None => bail!("No subcommand provided. Use --help to see available commands."),
124        }
125    }
126
127    match command {
128        Some(args::Commands::Core(cmd)) => core::execute(cmd, ctx).await?,
129        Some(args::Commands::Infra(cmd)) => infrastructure::execute(cmd, ctx).await?,
130        Some(args::Commands::Admin(cmd)) => Box::pin(admin::execute(cmd, ctx)).await?,
131        Some(args::Commands::Cloud(cmd)) => cloud::execute(cmd, ctx).await?,
132        Some(args::Commands::Analytics(cmd)) => analytics::execute(cmd, ctx).await?,
133        Some(args::Commands::Web(cmd)) => web::execute(cmd, ctx)?,
134        Some(args::Commands::Plugins(cmd)) => Box::pin(plugins::execute(cmd, ctx)).await?,
135        Some(args::Commands::Build(cmd)) => {
136            crate::commands::build::execute(cmd, ctx)?;
137        },
138        None => {
139            args::Cli::parse_from(["systemprompt", "--help"]);
140        },
141    }
142
143    Ok(())
144}
145
146async fn run_with_database_url(
147    command: Option<args::Commands>,
148    cli_config: CliConfig,
149    env: EnvOverrides,
150    database_url: &str,
151) -> Result<()> {
152    let db_ctx = DatabaseContext::from_url(database_url)
153        .await
154        .context("Failed to connect to database")?;
155
156    systemprompt_logging::init_logging(db_ctx.db_pool_arc());
157
158    let ctx = CommandContext::with_database(cli_config, env, db_ctx, database_url.to_owned());
159    Box::pin(dispatch_command(command, &ctx)).await
160}