systemprompt_cli/runner/
mod.rs1pub mod args;
11mod bootstrap;
12mod db_url;
13pub mod profile_routing;
14pub mod routing;
15pub mod routing_decision;
16mod structured_output;
17
18use anyhow::{Context, Result, bail};
19use clap::Parser;
20use systemprompt_loader::subprocess::{self, ApiServerStamp};
21use systemprompt_logging::set_startup_mode;
22use systemprompt_runtime::DatabaseContext;
23
24use crate::cli_settings::{CliConfig, OutputFormat};
25use crate::commands::{admin, analytics, cloud, core, infrastructure, plugins, web};
26use crate::context::CommandContext;
27use crate::descriptor::{CommandDescriptor, DescribeCommand};
28use crate::env_overrides::EnvOverrides;
29
30pub async fn run() -> Result<()> {
31 install_tls_crypto_provider();
32 let outcome = Box::pin(run_inner()).await;
33 structured_output::finalize(&outcome);
34 outcome
35}
36
37fn install_tls_crypto_provider() {
41 if rustls::crypto::CryptoProvider::get_default().is_some() {
42 return;
43 }
44 if rustls::crypto::ring::default_provider()
45 .install_default()
46 .is_err()
47 {
48 tracing::debug!("rustls crypto provider was installed concurrently");
49 }
50}
51
52async fn run_inner() -> Result<()> {
53 let cli = args::Cli::parse();
54 stamp_api_server_identity(&cli)?;
55
56 set_startup_mode(cli.command.is_none());
57
58 let env = EnvOverrides::from_process_env();
59 let cli_config = args::build_cli_config(&cli, &env);
60 systemprompt_logging::set_structured_output(cli_config.output_format() != OutputFormat::Table);
61
62 if cli.display.no_color || !cli_config.should_use_color() {
63 console::set_colors_enabled(false);
64 }
65
66 if let Some(database_url) = cli.database.database_url.clone() {
67 match cli.command.as_ref().map(args::Commands::db_url_routing) {
68 Some(db_url::DbUrlRouting::Direct) => {
69 return Box::pin(run_with_database_url(
70 cli.command,
71 cli_config,
72 env,
73 &database_url,
74 ))
75 .await;
76 },
77 Some(db_url::DbUrlRouting::Unsupported) => bail!(
78 "This command cannot run with --database-url; it requires full profile \
79 initialization. Remove --database-url."
80 ),
81 Some(db_url::DbUrlRouting::ProfileDriven) | None => {},
82 }
83 }
84
85 let desc = cli
86 .command
87 .as_ref()
88 .map_or(CommandDescriptor::FULL, DescribeCommand::descriptor);
89
90 if !desc.database() {
91 let effective_level = resolve_log_level(&cli_config, &env);
92 systemprompt_logging::init_console_logging_with_level(effective_level.as_deref());
93 }
94
95 if desc.profile() {
96 match profile_routing::bootstrap_profile(&cli, &desc, &cli_config, &env).await? {
97 profile_routing::BootstrapOutcome::RemoteExecuted => return Ok(()),
98 profile_routing::BootstrapOutcome::ExternalDbUrl(external_db_url) => {
99 return Box::pin(run_with_database_url(
100 cli.command,
101 cli_config,
102 env,
103 &external_db_url,
104 ))
105 .await;
106 },
107 profile_routing::BootstrapOutcome::ContinueLocal => {},
108 }
109 }
110
111 let ctx = CommandContext::new(cli_config, env);
112 Box::pin(dispatch_command(cli.command, &ctx)).await
113}
114
115fn resolve_log_level(cli_config: &CliConfig, env: &EnvOverrides) -> Option<String> {
116 if env.rust_log.is_some() {
117 return None;
118 }
119
120 if let Some(level) = cli_config.verbosity.as_tracing_filter() {
121 return Some(level.to_owned());
122 }
123
124 if let Ok(resolved) = bootstrap::resolve_profile(cli_config.profile_override.as_deref(), env)
125 && let Some(log_level) = bootstrap::try_load_log_level(&resolved.path)
126 {
127 return Some(log_level.as_tracing_filter().to_owned());
128 }
129
130 Some("warn".to_owned())
131}
132
133async fn dispatch_command(command: Option<args::Commands>, ctx: &CommandContext) -> Result<()> {
134 if ctx.is_database_scoped() {
135 match &command {
136 Some(
137 args::Commands::Core(_)
138 | args::Commands::Infra(_)
139 | args::Commands::Admin(_)
140 | args::Commands::Analytics(_),
141 ) => {},
142 Some(_) => bail!(
143 "This command requires full profile initialization. Remove --database-url flag."
144 ),
145 None => bail!("No subcommand provided. Use --help to see available commands."),
146 }
147 }
148
149 match command {
150 Some(args::Commands::Core(cmd)) => core::execute(cmd, ctx).await?,
151 Some(args::Commands::Infra(cmd)) => infrastructure::execute(cmd, ctx).await?,
152 Some(args::Commands::Admin(cmd)) => Box::pin(admin::execute(cmd, ctx)).await?,
153 Some(args::Commands::Cloud(cmd)) => cloud::execute(cmd, ctx).await?,
154 Some(args::Commands::Analytics(cmd)) => analytics::execute(cmd, ctx).await?,
155 Some(args::Commands::Web(cmd)) => web::execute(cmd, ctx)?,
156 Some(args::Commands::Plugins(cmd)) => Box::pin(plugins::execute(cmd, ctx)).await?,
157 Some(args::Commands::Build(cmd)) => {
158 crate::commands::build::execute(cmd, ctx)?;
159 },
160 None => {
161 args::Cli::parse_from(["systemprompt", "--help"]);
162 },
163 }
164
165 Ok(())
166}
167
168async fn run_with_database_url(
169 command: Option<args::Commands>,
170 cli_config: CliConfig,
171 env: EnvOverrides,
172 database_url: &str,
173) -> Result<()> {
174 let db_ctx = DatabaseContext::from_url(database_url)
175 .await
176 .context("Failed to connect to database")?;
177
178 systemprompt_logging::init_logging(&db_ctx.db_pool_arc());
179
180 let ctx = CommandContext::with_database(cli_config, env, db_ctx, database_url.to_owned());
181 Box::pin(dispatch_command(command, &ctx)).await
182}
183
184fn stamp_api_server_identity(cli: &args::Cli) -> Result<()> {
185 if !cli.command.as_ref().is_some_and(args::Commands::serves_api) {
186 return Ok(());
187 }
188 match subprocess::stamp_api_server()
189 .context("Failed to re-execute the API server with its identity marker")?
190 {
191 ApiServerStamp::Stamped | ApiServerStamp::Unverifiable => Ok(()),
192 }
193}