Skip to main content

pitboard_core/
context.rs

1//! Everything pitboard takes from its environment, read in one place. The CLI builds a
2//! `Context` from the process environment once. A program linking the library builds one
3//! itself: an app started from Finder does not see a shell's environment.
4
5use std::path::PathBuf;
6
7#[derive(Clone, Debug)]
8pub struct Context {
9    pub(crate) home: PathBuf,
10    pub(crate) pitboard_home: PathBuf,
11    /// `CLAUDE_CONFIG_DIR`, which Claude Code reads with `||`: empty means unset.
12    pub(crate) claude_config_dir: Option<String>,
13    /// `CLAUDE_SECURESTORAGE_CONFIG_DIR`, which Claude Code reads with `!== undefined`:
14    /// empty is set, and pins the default credential slot.
15    pub(crate) secure_storage_dir: Option<String>,
16    /// `$USER`, which names Claude Code's keychain account once screened by `slot`.
17    pub(crate) user: Option<String>,
18    /// `CLAUDE_CODE_CUSTOM_OAUTH_URL`. Set, it renames both the keychain item and the config
19    /// file Claude Code uses, so pitboard would be reading and writing the wrong ones.
20    pub(crate) custom_oauth: bool,
21    /// The `claude` that runs a sign-in; a bare name is looked up on `PATH`.
22    pub(crate) claude_program: PathBuf,
23    /// Where Anthropic's endpoints are reached instead, for tests; `api` honours loopback only.
24    pub(crate) api_base: Option<String>,
25    /// `CLAUDE_CODE_HOVER_REST`, which switches on Claude Code's successor credential backend.
26    pub(crate) hover_rest: bool,
27}
28
29impl Context {
30    /// Claude Code's defaults for a person whose home is `home`: `~/.pitboard`, `~/.claude`,
31    /// the default credential slot, `claude` looked up on `PATH`. An app starts here and sets
32    /// only what differs.
33    pub fn new(home: PathBuf) -> Context {
34        Context {
35            pitboard_home: home.join(".pitboard"),
36            home,
37            claude_config_dir: None,
38            secure_storage_dir: None,
39            user: None,
40            custom_oauth: false,
41            claude_program: PathBuf::from("claude"),
42            api_base: None,
43            hover_rest: false,
44        }
45    }
46
47    pub fn with_pitboard_home(mut self, dir: PathBuf) -> Context {
48        self.pitboard_home = dir;
49        self
50    }
51
52    /// Empty means unset, as Claude Code reads `CLAUDE_CONFIG_DIR`.
53    pub fn with_claude_config_dir(mut self, dir: String) -> Context {
54        self.claude_config_dir = Some(dir).filter(|d| !d.is_empty());
55        self
56    }
57
58    /// Empty is set, and pins the default slot, as Claude Code reads
59    /// `CLAUDE_SECURESTORAGE_CONFIG_DIR`.
60    pub fn with_secure_storage_dir(mut self, dir: String) -> Context {
61        self.secure_storage_dir = Some(dir);
62        self
63    }
64
65    /// The login name whose keychain account Claude Code stores under.
66    pub fn with_user(mut self, user: String) -> Context {
67        self.user = Some(user);
68        self
69    }
70
71    /// An app started from Finder does not see the shell's `PATH`, so it names `claude` itself.
72    pub fn with_claude_program(mut self, program: PathBuf) -> Context {
73        self.claude_program = program;
74        self
75    }
76
77    pub fn from_env() -> Context {
78        let var = |name: &str| std::env::var(name).ok();
79        let home = std::env::var_os("HOME")
80            .map(PathBuf::from)
81            .unwrap_or_default();
82        Context {
83            pitboard_home: std::env::var_os("PITBOARD_HOME")
84                .map(PathBuf::from)
85                .unwrap_or_else(|| home.join(".pitboard")),
86            home,
87            claude_config_dir: var("CLAUDE_CONFIG_DIR").filter(|v| !v.is_empty()),
88            secure_storage_dir: var("CLAUDE_SECURESTORAGE_CONFIG_DIR"),
89            user: var("USER"),
90            custom_oauth: var("CLAUDE_CODE_CUSTOM_OAUTH_URL").is_some_and(|v| !v.is_empty()),
91            claude_program: PathBuf::from("claude"),
92            api_base: var("PITBOARD_API_BASE"),
93            hover_rest: var("CLAUDE_CODE_HOVER_REST").is_some_and(|v| v == "1" || v == "true"),
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn an_explicit_context_reads_claude_codes_settings_the_way_the_environment_does() {
104        let ctx = Context::new(PathBuf::from("/home/x"))
105            .with_claude_config_dir(String::new())
106            .with_secure_storage_dir(String::new());
107        assert_eq!(ctx.pitboard_home, PathBuf::from("/home/x/.pitboard"));
108        assert_eq!(ctx.claude_config_dir, None, "empty means unset");
109        assert_eq!(
110            ctx.secure_storage_dir.as_deref(),
111            Some(""),
112            "empty is set, and pins the default slot"
113        );
114        assert_eq!(ctx.claude_program, PathBuf::from("claude"));
115    }
116}