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