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}