use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Environment {
ClaudeCode,
OpenCode,
Codex,
Generic,
}
impl Environment {
pub fn detect() -> Self {
detect_from(|key| std::env::var_os(key))
}
pub fn skill_rel_path(&self, name: &str) -> PathBuf {
PathBuf::from(format!(".claude/skills/{name}/SKILL.md"))
}
pub fn skill_path(&self, name: &str, root: Option<&Path>) -> PathBuf {
let rel = self.skill_rel_path(name);
match root {
Some(r) => r.join(rel),
None => rel,
}
}
}
impl std::fmt::Display for Environment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ClaudeCode => write!(f, "Claude Code"),
Self::OpenCode => write!(f, "OpenCode"),
Self::Codex => write!(f, "Codex"),
Self::Generic => write!(f, "Generic"),
}
}
}
pub fn detect() -> Environment {
Environment::detect()
}
fn detect_from<F, V>(var: F) -> Environment
where
F: Fn(&str) -> Option<V>,
V: AsRef<std::ffi::OsStr>,
{
if var("CLAUDE_CODE").is_some() || var("CLAUDE_CODE_ENTRYPOINT").is_some() {
return Environment::ClaudeCode;
}
if var("OPENCODE").is_some() {
return Environment::OpenCode;
}
if var("CODEX_CLI").is_some() || var("CODEX").is_some() {
return Environment::Codex;
}
Environment::Generic
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::ffi::OsString;
fn env_with(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<OsString> {
let map: HashMap<String, OsString> = pairs
.iter()
.map(|(k, v)| (k.to_string(), OsString::from(v)))
.collect();
move |key: &str| map.get(key).cloned()
}
#[test]
fn detects_claude_code_via_claude_code_var() {
let detect = detect_from(env_with(&[("CLAUDE_CODE", "1")]));
assert_eq!(detect, Environment::ClaudeCode);
}
#[test]
fn detects_claude_code_via_entrypoint() {
let detect = detect_from(env_with(&[("CLAUDE_CODE_ENTRYPOINT", "/usr/bin/claude")]));
assert_eq!(detect, Environment::ClaudeCode);
}
#[test]
fn detects_opencode() {
let detect = detect_from(env_with(&[("OPENCODE", "1")]));
assert_eq!(detect, Environment::OpenCode);
}
#[test]
fn detects_codex_cli() {
let detect = detect_from(env_with(&[("CODEX_CLI", "1")]));
assert_eq!(detect, Environment::Codex);
}
#[test]
fn detects_codex_var() {
let detect = detect_from(env_with(&[("CODEX", "1")]));
assert_eq!(detect, Environment::Codex);
}
#[test]
fn falls_back_to_generic() {
let detect = detect_from(env_with(&[]));
assert_eq!(detect, Environment::Generic);
}
#[test]
fn claude_code_takes_priority_over_others() {
let detect = detect_from(env_with(&[("CLAUDE_CODE", "1"), ("OPENCODE", "1")]));
assert_eq!(detect, Environment::ClaudeCode);
}
#[test]
fn skill_rel_path_format() {
let env = Environment::ClaudeCode;
assert_eq!(
env.skill_rel_path("agent-doc"),
PathBuf::from(".claude/skills/agent-doc/SKILL.md")
);
}
#[test]
fn skill_path_with_root() {
let env = Environment::Generic;
let path = env.skill_path("my-tool", Some(Path::new("/project")));
assert_eq!(path, PathBuf::from("/project/.claude/skills/my-tool/SKILL.md"));
}
#[test]
fn skill_path_without_root() {
let env = Environment::Generic;
let path = env.skill_path("my-tool", None);
assert_eq!(path, PathBuf::from(".claude/skills/my-tool/SKILL.md"));
}
#[test]
fn display_variants() {
assert_eq!(Environment::ClaudeCode.to_string(), "Claude Code");
assert_eq!(Environment::OpenCode.to_string(), "OpenCode");
assert_eq!(Environment::Codex.to_string(), "Codex");
assert_eq!(Environment::Generic.to_string(), "Generic");
}
}