Skip to main content

systemprompt_cli/
env_overrides.rs

1//! Process-environment snapshot for the CLI.
2//!
3//! [`EnvOverrides`] captures every environment variable the CLI consults, read
4//! once at process start ([`EnvOverrides::from_process_env`]) and threaded
5//! through [`crate::context::CommandContext`]. Command code never calls
6//! `std::env::var` directly — tests construct the snapshot with
7//! [`EnvOverrides::from_vars`] instead of mutating process state.
8
9use std::collections::HashMap;
10
11use systemprompt_identifiers::{ContextId, SessionId, UserId};
12
13#[derive(Debug, Clone, Default)]
14pub struct EnvOverrides {
15    pub output_format: Option<String>,
16    pub log_level: Option<String>,
17    pub no_color: bool,
18    pub non_interactive: bool,
19    pub profile: Option<String>,
20    pub rust_log: Option<String>,
21    pub is_fly: bool,
22    pub is_remote_cli: bool,
23    pub editor: Option<String>,
24    pub database_url: Option<String>,
25    pub services_path: Option<String>,
26    pub session: SessionEnv,
27}
28
29#[derive(Debug, Clone, Default)]
30pub struct SessionEnv {
31    pub user_id: Option<UserId>,
32    pub session_id: Option<SessionId>,
33    pub context_id: Option<ContextId>,
34    pub auth_token: Option<String>,
35}
36
37impl EnvOverrides {
38    #[must_use]
39    pub fn from_process_env() -> Self {
40        Self::from_lookup(|key| std::env::var(key).ok())
41    }
42
43    #[must_use]
44    pub fn from_vars<I, K, V>(vars: I) -> Self
45    where
46        I: IntoIterator<Item = (K, V)>,
47        K: Into<String>,
48        V: Into<String>,
49    {
50        let map: HashMap<String, String> = vars
51            .into_iter()
52            .map(|(k, v)| (k.into(), v.into()))
53            .collect();
54        Self::from_lookup(|key| map.get(key).cloned())
55    }
56
57    fn from_lookup(lookup: impl Fn(&str) -> Option<String>) -> Self {
58        Self {
59            output_format: lookup("SYSTEMPROMPT_OUTPUT_FORMAT"),
60            log_level: lookup("SYSTEMPROMPT_LOG_LEVEL"),
61            no_color: lookup("SYSTEMPROMPT_NO_COLOR").is_some() || lookup("NO_COLOR").is_some(),
62            non_interactive: lookup("SYSTEMPROMPT_NON_INTERACTIVE").is_some(),
63            profile: lookup("SYSTEMPROMPT_PROFILE"),
64            rust_log: lookup("RUST_LOG"),
65            is_fly: lookup("FLY_APP_NAME").is_some(),
66            is_remote_cli: lookup("SYSTEMPROMPT_CLI_REMOTE").is_some(),
67            editor: lookup("VISUAL").or_else(|| lookup("EDITOR")),
68            database_url: lookup("DATABASE_URL"),
69            services_path: lookup("SYSTEMPROMPT_SERVICES_PATH"),
70            session: SessionEnv {
71                user_id: lookup("SYSTEMPROMPT_USER_ID").map(UserId::new),
72                session_id: lookup("SYSTEMPROMPT_SESSION_ID").map(SessionId::new),
73                context_id: lookup("SYSTEMPROMPT_CONTEXT_ID").and_then(|value| {
74                    ContextId::try_new(value)
75                        .inspect_err(|error| {
76                            tracing::warn!(
77                                error = %error,
78                                "ignoring malformed SYSTEMPROMPT_CONTEXT_ID; expected a UUID"
79                            );
80                        })
81                        .ok()
82                }),
83                auth_token: lookup("SYSTEMPROMPT_AUTH_TOKEN"),
84            },
85        }
86    }
87}