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