1use std::path::{Path, PathBuf};
4use std::sync::atomic::{AtomicBool, Ordering};
5
6use crate::tools::path::{expand, home_dir};
7
8pub const APP_NAME: &str = "yapi";
10pub const PROJECT_DIR: &str = ".yapi";
12pub const AGENT_DIR_ENV: &str = "YAPI_CODING_AGENT_DIR";
14pub const SESSION_DIR_ENV: &str = "YAPI_CODING_AGENT_SESSION_DIR";
16
17static OFFLINE: AtomicBool = AtomicBool::new(false);
19
20pub const PROJECT_ONLY_ENV: &str = "YAPI_PROJECT_ONLY";
23
24static PROJECT_ONLY: AtomicBool = AtomicBool::new(false);
26
27pub fn set_project_only() {
32 PROJECT_ONLY.store(true, Ordering::Relaxed);
33}
34
35pub fn project_only() -> bool {
37 PROJECT_ONLY.load(Ordering::Relaxed)
38}
39
40const OFFLINE_ENV: [&str; 2] = ["PI_OFFLINE", "PI_SKIP_VERSION_CHECK"];
42
43pub fn set_offline() {
47 OFFLINE.store(true, Ordering::Relaxed);
48}
49
50pub fn env_var(name: &str) -> Option<String> {
52 if OFFLINE.load(Ordering::Relaxed) && OFFLINE_ENV.contains(&name) {
53 return Some("1".to_owned());
54 }
55 std::env::var(name).ok()
56}
57
58pub fn env_set(name: &str) -> bool {
61 env_var(name).is_some_and(|value| !value.is_empty())
62}
63
64pub fn child_env() -> Vec<(&'static str, String)> {
70 let mut env = vec![
71 ("PI_CODING_AGENT", "true".to_owned()),
72 ("AI_AGENT", APP_NAME.to_owned()),
73 ];
74 if OFFLINE.load(Ordering::Relaxed) {
75 env.extend(OFFLINE_ENV.map(|name| (name, "1".to_owned())));
76 }
77 if project_only() {
78 env.push((PROJECT_ONLY_ENV, "1".to_owned()));
79 }
80 env.extend(yapi_ai::http::proxy_env());
81 env
82}
83
84pub fn agent_dir() -> PathBuf {
86 match std::env::var(AGENT_DIR_ENV) {
87 Ok(dir) if !dir.is_empty() => PathBuf::from(expand(&dir)),
88 _ => home_dir().join(PROJECT_DIR).join("agent"),
89 }
90}
91
92pub fn default_session_dir(agent_dir: &Path, cwd: &Path) -> PathBuf {
96 let base = std::env::current_dir().unwrap_or_default();
97 let resolve = |path: &Path| crate::tools::path::resolve_lexically(&base, path);
98 let cwd = resolve(cwd);
99 let text = cwd.to_string_lossy();
100 let trimmed = text.trim_start_matches(['/', '\\']);
101 let safe: String = trimmed
102 .chars()
103 .map(|c| {
104 if matches!(c, '/' | '\\' | ':') {
105 '-'
106 } else {
107 c
108 }
109 })
110 .collect();
111 resolve(agent_dir)
112 .join("sessions")
113 .join(format!("--{safe}--"))
114}
115
116pub fn env_flag(name: &str) -> bool {
119 env_var(name).is_some_and(|value| {
120 value == "1" || matches!(value.to_lowercase().as_str(), "true" | "yes")
121 })
122}
123
124pub fn docs_dir(agent_dir: &Path) -> PathBuf {
127 agent_dir.join("docs")
128}
129
130pub fn bin_dir(agent_dir: &Path) -> PathBuf {
132 agent_dir.join("bin")
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn session_dirs_match_pi() {
141 assert_eq!(
142 default_session_dir(Path::new("/h/.yapi/agent"), Path::new("/home/user/project")),
143 PathBuf::from("/h/.yapi/agent/sessions/--home-user-project--")
144 );
145 }
146}