systemprompt_cli/session/resolution/
mod.rs1mod helpers;
10
11use std::path::{Path, PathBuf};
12
13use anyhow::{Context, Result};
14use systemprompt_cloud::{SessionKey, SessionStore};
15use systemprompt_config::{ProfileBootstrap, SecretsBootstrap};
16use systemprompt_loader::ProfileLoader;
17use systemprompt_logging::CliService;
18use systemprompt_models::Profile;
19
20use super::context::CliSessionContext;
21use crate::CliConfig;
22use crate::cli_settings::{OutputFormat, VerbosityLevel};
23use crate::paths::ResolvedPaths;
24use helpers::{
25 create_new_session, extract_profile_name, initialize_profile_bootstraps,
26 resolve_profile_path_from_session, resolve_profile_path_without_session, try_session_from_env,
27 try_validate_context,
28};
29
30pub(super) struct ProfileContext<'a> {
31 pub name: &'a str,
32 pub path: PathBuf,
33}
34
35async fn get_session_for_profile(
36 profile_input: &str,
37 config: &CliConfig,
38) -> Result<CliSessionContext> {
39 let (profile_path, profile) = crate::shared::resolve_profile_with_data(profile_input)
40 .map_err(|e| anyhow::anyhow!("{}", e))?;
41
42 if !ProfileBootstrap::is_initialized() {
43 ProfileBootstrap::init_from_path(&profile_path)
44 .with_context(|| format!("Failed to initialize profile '{}'", profile_input))?;
45 }
46
47 if !SecretsBootstrap::is_initialized() {
48 SecretsBootstrap::try_init().with_context(
49 || "Failed to initialize secrets. Check your profile's secrets configuration.",
50 )?;
51 }
52
53 get_session_for_loaded_profile(&profile, &profile_path, config).await
54}
55
56async fn get_session_for_loaded_profile(
57 profile: &Profile,
58 profile_path: &Path,
59 config: &CliConfig,
60) -> Result<CliSessionContext> {
61 if let Some(ctx) = try_session_from_env(profile) {
62 return Ok(ctx);
63 }
64
65 let profile_name = extract_profile_name(profile_path)?;
66 let tenant_id = profile.cloud.as_ref().and_then(|c| c.tenant_id.as_ref());
67 let session_key = SessionKey::from_tenant_id(tenant_id);
68 let sessions_dir = ResolvedPaths::discover().sessions_dir();
69 let mut store = SessionStore::load_or_create(&sessions_dir)?;
70
71 if let Some(mut session) = store.get_valid_session(&session_key).cloned() {
72 session.touch();
73
74 if let Some(refreshed) = try_validate_context(&mut session, &profile_name).await {
75 session = refreshed;
76 }
77
78 store.upsert_session(&session_key, session.clone());
79 store.save(&sessions_dir)?;
80 return Ok(CliSessionContext {
81 session,
82 profile: profile.clone(),
83 });
84 }
85
86 let session_email_hint = store
87 .get_session(&session_key)
88 .map(|s| s.user_email.to_string());
89
90 let profile_ctx = ProfileContext {
91 name: &profile_name,
92 path: profile_path.to_path_buf(),
93 };
94
95 let session = create_new_session(
96 profile,
97 &profile_ctx,
98 &session_key,
99 config,
100 session_email_hint.as_deref(),
101 )
102 .await?;
103
104 store.upsert_session(&session_key, session.clone());
105 store.set_active_with_profile(&session_key, &profile_name);
106 store.save(&sessions_dir)?;
107
108 if session.session_token.as_str().is_empty() {
109 anyhow::bail!("Session token is empty. Session creation failed.");
110 }
111
112 Ok(CliSessionContext {
113 session,
114 profile: profile.clone(),
115 })
116}
117
118async fn try_session_from_active_key(config: &CliConfig) -> Result<Option<CliSessionContext>> {
119 let paths = ResolvedPaths::discover();
120 let sessions_dir = paths.sessions_dir();
121 let store = SessionStore::load_or_create(&sessions_dir)?;
122
123 let Some(ref active_key_str) = store.active_key else {
124 return Ok(None);
125 };
126
127 let active_key = store
128 .active_session_key()
129 .ok_or_else(|| anyhow::anyhow!("Invalid active session key: {}", active_key_str))?;
130
131 let active_profile = store.active_profile_name.as_deref();
132
133 let profile_path = if let Some(session) = store.active_session() {
134 match resolve_profile_path_from_session(session, active_profile)? {
135 Some(path) => path,
136 None => return Ok(None),
137 }
138 } else {
139 resolve_profile_path_without_session(&paths, &store, &active_key, active_profile)?
140 };
141
142 let profile = ProfileLoader::load_from_path(&profile_path).with_context(|| {
143 format!(
144 "Failed to load profile from stored path: {}",
145 profile_path.display()
146 )
147 })?;
148
149 initialize_profile_bootstraps(&profile_path)?;
150
151 let ctx = get_session_for_loaded_profile(&profile, &profile_path, config).await?;
152 Ok(Some(ctx))
153}
154
155pub async fn get_or_create_session(config: &CliConfig) -> Result<CliSessionContext> {
156 let ctx = resolve_session(config).await?;
157
158 let banner_requested = config.verbosity >= VerbosityLevel::Verbose;
159 let banner_warranted = ctx.profile.target.is_cloud();
160 if config.is_interactive()
161 && config.output_format == OutputFormat::Table
162 && config.verbosity != VerbosityLevel::Quiet
163 && (banner_requested || banner_warranted)
164 {
165 let tenant = ctx
166 .session
167 .tenant_key
168 .as_ref()
169 .map_or("local", systemprompt_identifiers::TenantId::as_str);
170 CliService::session_context_with_url(
171 ctx.session.profile_name.as_str(),
172 &ctx.session.session_id,
173 Some(tenant),
174 Some(&ctx.profile.server.api_external_url),
175 );
176 }
177
178 Ok(ctx)
179}
180
181async fn resolve_session(config: &CliConfig) -> Result<CliSessionContext> {
182 if let Some(ref profile_name) = config.profile_override {
183 return get_session_for_profile(profile_name, config).await;
184 }
185
186 let env_profile_set = std::env::var("SYSTEMPROMPT_PROFILE").is_ok();
187
188 if !env_profile_set {
189 if let Some(ctx) = try_session_from_active_key(config).await? {
190 return Ok(ctx);
191 }
192 }
193
194 let profile = ProfileBootstrap::get()
195 .map_err(|_e| {
196 anyhow::anyhow!(
197 "Profile required.\n\nSet SYSTEMPROMPT_PROFILE environment variable to your \
198 profile.yaml path, or use --profile <name>."
199 )
200 })?
201 .clone();
202
203 let profile_path_str = ProfileBootstrap::get_path().map_err(|_e| {
204 anyhow::anyhow!(
205 "Profile path required.\n\nSet SYSTEMPROMPT_PROFILE environment variable or use \
206 --profile <name>."
207 )
208 })?;
209
210 let profile_path = Path::new(profile_path_str);
211 get_session_for_loaded_profile(&profile, profile_path, config).await
212}