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