-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathconfig.rs
More file actions
168 lines (147 loc) · 4.96 KB
/
Copy pathconfig.rs
File metadata and controls
168 lines (147 loc) · 4.96 KB
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::OnceLock;
use crate::error::AgfError;
use crate::model::Agent;
pub fn home_dir() -> Result<PathBuf, AgfError> {
dirs::home_dir().ok_or(AgfError::NoHomeDir)
}
pub fn claude_dir() -> Result<PathBuf, AgfError> {
Ok(home_dir()?.join(".claude"))
}
pub fn codex_dir() -> Result<PathBuf, AgfError> {
Ok(home_dir()?.join(".codex"))
}
pub fn opencode_data_dir() -> Result<PathBuf, AgfError> {
Ok(home_dir()?.join(".local/share/opencode"))
}
pub fn pi_sessions_dir() -> Result<PathBuf, AgfError> {
Ok(home_dir()?.join(".pi/agent/sessions"))
}
pub fn oh_my_pi_sessions_dir() -> Result<PathBuf, AgfError> {
Ok(home_dir()?.join(".omp/agent/sessions"))
}
pub fn gemini_dir() -> Result<PathBuf, AgfError> {
Ok(home_dir()?.join(".gemini"))
}
pub fn cursor_dir() -> Result<PathBuf, AgfError> {
Ok(home_dir()?.join(".cursor"))
}
pub fn hermes_dir() -> Result<PathBuf, AgfError> {
Ok(home_dir()?.join(".hermes"))
}
pub fn yolop_sessions_dir() -> Result<PathBuf, AgfError> {
dirs::data_dir()
.map(|d| d.join("yolop").join("sessions"))
.ok_or(AgfError::NoHomeDir)
}
pub fn kiro_data_dir() -> Result<PathBuf, AgfError> {
// Kiro CLI stores data via dirs::data_local_dir()
// macOS: ~/Library/Application Support/kiro-cli/
// Linux: ~/.local/share/kiro-cli/
dirs::data_local_dir()
.map(|d| d.join("kiro-cli"))
.ok_or(AgfError::NoHomeDir)
}
/// Cached set of executable names found in `$PATH`, built once per process.
/// On Windows entries are lower-cased and `%PATHEXT%` stems are inserted
/// alongside the full filename so bare-name lookups match `.exe`/`.cmd`/etc.
fn path_executables() -> &'static HashSet<String> {
static CACHE: OnceLock<HashSet<String>> = OnceLock::new();
CACHE.get_or_init(|| {
let pathext = windows_pathext();
let mut set = HashSet::new();
if let Some(path) = std::env::var_os("PATH") {
for dir in std::env::split_paths(&path) {
if let Ok(entries) = std::fs::read_dir(&dir) {
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() {
insert_executable_name(&mut set, name, &pathext);
}
}
}
}
}
set
})
}
/// Return the list of PATHEXT suffixes (lower-cased, each beginning with `.`)
/// on Windows; empty on other platforms.
fn windows_pathext() -> Vec<String> {
if !cfg!(windows) {
return Vec::new();
}
std::env::var("PATHEXT")
.unwrap_or_else(|_| String::from(".COM;.EXE;.BAT;.CMD"))
.split(';')
.filter(|s| !s.is_empty())
.map(|s| s.to_lowercase())
.collect()
}
/// Insert `filename` into `set`. On Windows, also insert its lower-cased
/// stem (without a `PATHEXT` suffix). On other platforms, insert verbatim.
fn insert_executable_name(set: &mut HashSet<String>, filename: &str, pathext: &[String]) {
if cfg!(windows) {
let lower = filename.to_lowercase();
for ext in pathext {
if let Some(stem) = lower.strip_suffix(ext.as_str()) {
set.insert(stem.to_string());
break;
}
}
set.insert(lower);
} else {
set.insert(filename.to_string());
}
}
pub fn is_agent_installed(agent: Agent) -> bool {
let name = agent.cli_name();
let execs = path_executables();
if cfg!(windows) {
execs.contains(&name.to_lowercase())
} else {
execs.contains(name)
}
}
pub fn installed_agents() -> Vec<Agent> {
Agent::all()
.iter()
.copied()
.filter(|a| is_agent_installed(*a))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(windows)]
fn insert_executable_name_adds_stem_on_windows() {
let pathext: Vec<String> = [".com", ".exe", ".bat", ".cmd"]
.iter()
.map(|s| s.to_string())
.collect();
let mut set = HashSet::new();
insert_executable_name(&mut set, "Claude.EXE", &pathext);
// Full lower-cased name and stem both present.
assert!(set.contains("claude.exe"));
assert!(set.contains("claude"));
}
#[test]
#[cfg(windows)]
fn insert_executable_name_no_stem_when_ext_missing() {
let pathext: Vec<String> = [".exe"].iter().map(|s| s.to_string()).collect();
let mut set = HashSet::new();
insert_executable_name(&mut set, "README.md", &pathext);
assert!(set.contains("readme.md"));
// No bare "readme" stem should be inserted — .md is not in PATHEXT.
assert!(!set.contains("readme"));
}
#[test]
#[cfg(not(windows))]
fn insert_executable_name_verbatim_on_unix() {
let mut set = HashSet::new();
insert_executable_name(&mut set, "claude", &[]);
assert!(set.contains("claude"));
assert_eq!(set.len(), 1);
}
}