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