use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use crate::adapters::{
ConfigValidator, HooksAdapter, RulesAdapter, Severity, SkillsAdapter, ToolAdapter,
ValidationIssue, camel_to_kebab, json_utils, kebab_to_camel, read_rules_file,
validate_all_syntax, write_rules_file,
};
use crate::config::{HooksConfig, McpConfig, McpServer};
use crate::error::LorumError;
use crate::skills::{SkillEntry, copy_dir_recursive, scan_skills_dir};
pub struct OpenCodeSkillsAdapter;
impl SkillsAdapter for OpenCodeSkillsAdapter {
fn name(&self) -> &str {
"opencode"
}
fn skills_base_dir(&self) -> Option<PathBuf> {
dirs::home_dir().map(|h| h.join(".config").join("opencode").join("skills"))
}
fn read_skills(&self) -> Result<Vec<SkillEntry>, LorumError> {
let Some(dir) = self.skills_base_dir() else {
return Ok(Vec::new());
};
scan_skills_dir(&dir)
}
fn write_skill(&self, name: &str, source_dir: &Path) -> Result<(), LorumError> {
let dir = self.skills_base_dir().ok_or_else(|| LorumError::Other {
message: "cannot determine home directory".into(),
})?;
let target = dir.join(name);
if target.exists() {
let old = dir.join(format!(".old-{name}"));
if old.exists() {
std::fs::remove_dir_all(&old)?;
}
std::fs::rename(&target, &old)?;
}
copy_dir_recursive(source_dir, &target)
}
fn remove_skill(&self, name: &str) -> Result<(), LorumError> {
let dir = self.skills_base_dir().ok_or_else(|| LorumError::Other {
message: "cannot determine home directory".into(),
})?;
let target = dir.join(name);
if target.exists() {
std::fs::remove_dir_all(target)?;
}
Ok(())
}
}
pub struct OpenCodeRulesAdapter;
impl RulesAdapter for OpenCodeRulesAdapter {
fn name(&self) -> &str {
"opencode"
}
fn rules_path(&self, project_root: &Path) -> PathBuf {
project_root.join("AGENTS.md")
}
fn read_rules(&self, project_root: &Path) -> Result<Option<String>, LorumError> {
read_rules_file(&self.rules_path(project_root))
}
fn write_rules(&self, project_root: &Path, content: &str) -> Result<(), LorumError> {
write_rules_file(&self.rules_path(project_root), content)
}
}
pub struct OpencodeAdapter {
project_root: Option<PathBuf>,
}
const MCP_FIELD: &str = "mcp";
impl OpencodeAdapter {
pub fn new() -> Self {
Self { project_root: None }
}
pub fn with_project_root(root: PathBuf) -> Self {
Self {
project_root: Some(root),
}
}
fn global_config_path() -> Option<PathBuf> {
dirs::home_dir().map(|h| h.join(".config").join("opencode").join("opencode.json"))
}
fn project_config_path(&self) -> Option<PathBuf> {
let root = self
.project_root
.clone()
.or_else(|| std::env::current_dir().ok())?;
Some(root.join("opencode.json"))
}
fn read_config_path(&self) -> Option<PathBuf> {
let global = Self::global_config_path()?;
if global.exists() {
Some(global)
} else {
self.project_config_path()
}
}
fn write_config_path(&self) -> Option<PathBuf> {
let project = self.project_config_path()?;
let is_project = project.exists()
|| project
.parent()
.is_some_and(|p| p.join(".git").exists() || p.join("Cargo.toml").exists());
if is_project {
Some(project)
} else {
Self::global_config_path()
}
}
fn global_hooks_path() -> Option<PathBuf> {
dirs::home_dir().map(|h| h.join(".config").join("opencode").join("hooks.json"))
}
fn project_hooks_path(&self) -> Option<PathBuf> {
let root = self
.project_root
.clone()
.or_else(|| std::env::current_dir().ok())?;
Some(root.join("hooks.json"))
}
}
impl Default for OpencodeAdapter {
fn default() -> Self {
Self::new()
}
}
impl ConfigValidator for OpencodeAdapter {
fn name(&self) -> &str {
"opencode"
}
fn validate_config(&self) -> Result<Vec<ValidationIssue>, LorumError> {
let mut issues = validate_all_syntax(&ToolAdapter::config_paths(self));
let global_path = Self::global_config_path();
let project_path = self.project_config_path();
let global_exists = global_path.as_ref().is_some_and(|p| p.exists());
let project_exists = project_path.as_ref().is_some_and(|p| p.exists());
if global_exists && project_exists {
let global_servers = if let Some(ref path) = global_path {
json_utils::read_existing_json(path)
.ok()
.and_then(|root| {
root.get(MCP_FIELD).and_then(|v| v.as_object()).map(|obj| {
obj.keys()
.cloned()
.collect::<std::collections::HashSet<String>>()
})
})
.unwrap_or_default()
} else {
std::collections::HashSet::new()
};
let project_servers = if let Some(ref path) = project_path {
json_utils::read_existing_json(path)
.ok()
.and_then(|root| {
root.get(MCP_FIELD).and_then(|v| v.as_object()).map(|obj| {
obj.keys()
.cloned()
.collect::<std::collections::HashSet<String>>()
})
})
.unwrap_or_default()
} else {
std::collections::HashSet::new()
};
for server_name in global_servers.intersection(&project_servers) {
issues.push(ValidationIssue {
severity: Severity::Warning,
message: format!(
"MCP server '{}' is defined in both global and project-level opencode.json configs",
server_name
),
path: None,
line: None,
});
}
}
Ok(issues)
}
}
impl HooksAdapter for OpencodeAdapter {
fn name(&self) -> &str {
"opencode"
}
fn config_paths(&self) -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Some(p) = Self::global_hooks_path() {
paths.push(p);
}
if let Some(p) = self.project_hooks_path() {
paths.push(p);
}
paths
}
fn read_hooks(&self) -> Result<HooksConfig, LorumError> {
let paths = [Self::global_hooks_path(), self.project_hooks_path()];
for path in paths.into_iter().flatten() {
if path.exists() {
let root = json_utils::read_existing_json(&path)?;
return Ok(parse_hooks_from_json(root.get("hooks")));
}
}
Ok(HooksConfig::default())
}
fn write_hooks(&self, config: &HooksConfig) -> Result<(), LorumError> {
let path = match self.project_hooks_path() {
Some(p) => p,
None => match Self::global_hooks_path() {
Some(p) => p,
None => {
return Err(LorumError::Other {
message: "cannot determine hooks directory".into(),
});
}
},
};
let mut root = json_utils::read_existing_json(&path)?;
root["hooks"] = hooks_config_to_json_value(config);
json_utils::write_json(&path, &root)
}
fn lorum_to_tool_event(&self, lorum_event: &str) -> Option<String> {
Some(kebab_to_camel(lorum_event))
}
fn tool_to_lorum_event(&self, tool_event: &str) -> Option<String> {
Some(camel_to_kebab(tool_event))
}
}
impl ToolAdapter for OpencodeAdapter {
fn name(&self) -> &str {
"opencode"
}
fn config_paths(&self) -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Some(global) = Self::global_config_path() {
paths.push(global);
}
if let Some(project) = self.project_config_path() {
paths.push(project);
}
paths
}
fn read_mcp(&self) -> Result<McpConfig, LorumError> {
let path = match self.read_config_path() {
Some(p) => p,
None => return Ok(McpConfig::default()),
};
if !path.exists() {
return Ok(McpConfig::default());
}
let root = json_utils::read_existing_json(&path)?;
let Some(servers) = root.get(MCP_FIELD).and_then(|v| v.as_object()) else {
return Ok(McpConfig::default());
};
let mut map = BTreeMap::new();
for (name, entry) in servers {
let Some(cmd_array) = entry.get("command").and_then(|v| v.as_array()) else {
eprintln!(
"warning: skipping opencode MCP server '{}' with missing or non-array command",
name
);
continue;
};
if cmd_array.is_empty() {
eprintln!(
"warning: skipping opencode MCP server '{}' with empty command array",
name
);
continue;
}
let Some(command) = cmd_array.first().and_then(|v| v.as_str()).map(String::from) else {
eprintln!(
"warning: skipping opencode MCP server '{}' with invalid command",
name
);
continue;
};
let args: Vec<String> = cmd_array
.iter()
.skip(1)
.filter_map(|v| v.as_str().map(String::from))
.collect();
let env = entry
.get("environment")
.and_then(|v| v.as_object())
.map(|obj| {
obj.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default();
map.insert(name.clone(), McpServer { command, args, env });
}
Ok(McpConfig { servers: map })
}
fn write_mcp(&self, config: &McpConfig) -> Result<(), LorumError> {
let path = match self.write_config_path() {
Some(p) => p,
None => {
return Err(LorumError::Other {
message: "cannot determine config directory".into(),
});
}
};
let mut root = json_utils::read_existing_json(&path)?;
let mut mcp_map = serde_json::Map::new();
for (name, server) in &config.servers {
let mut server_obj = serde_json::Map::new();
server_obj.insert("type".into(), serde_json::Value::String("local".into()));
server_obj.insert("enabled".into(), serde_json::Value::Bool(true));
let mut cmd_array: Vec<serde_json::Value> =
vec![serde_json::Value::String(server.command.clone())];
for arg in &server.args {
cmd_array.push(serde_json::Value::String(arg.clone()));
}
server_obj.insert("command".into(), serde_json::Value::Array(cmd_array));
if !server.env.is_empty() {
let env_obj: serde_json::Map<String, serde_json::Value> = server
.env
.iter()
.map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
.collect();
server_obj.insert("environment".into(), serde_json::Value::Object(env_obj));
}
mcp_map.insert(name.clone(), serde_json::Value::Object(server_obj));
}
root[MCP_FIELD] = serde_json::Value::Object(mcp_map);
json_utils::write_json(&path, &root)
}
}
fn parse_hooks_from_json(value: Option<&serde_json::Value>) -> HooksConfig {
json_utils::parse_hooks_from_json_value(value, camel_to_kebab, "matcher")
}
fn hooks_config_to_json_value(config: &HooksConfig) -> serde_json::Value {
json_utils::hooks_config_to_json_value(config, kebab_to_camel, "matcher")
}
#[cfg(test)]
mod opencode_rules_tests {
use super::*;
#[test]
fn rules_path_returns_agents_md() {
let adapter = OpenCodeRulesAdapter;
let path = adapter.rules_path(Path::new("/tmp/myproject"));
assert_eq!(path, PathBuf::from("/tmp/myproject/AGENTS.md"));
}
#[test]
fn read_rules_returns_none_when_file_missing() {
let dir = tempfile::tempdir().unwrap();
let adapter = OpenCodeRulesAdapter;
let result = adapter.read_rules(dir.path()).unwrap();
assert!(result.is_none());
}
#[test]
fn write_rules_creates_file() {
let dir = tempfile::tempdir().unwrap();
let adapter = OpenCodeRulesAdapter;
let path = adapter.rules_path(dir.path());
assert!(!path.exists());
adapter
.write_rules(dir.path(), "Use 4-space indentation.")
.unwrap();
assert!(path.exists());
}
#[test]
fn write_then_read_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let adapter = OpenCodeRulesAdapter;
let content = "## Style\nUse 4-space indentation.\n";
adapter.write_rules(dir.path(), content).unwrap();
let read = adapter.read_rules(dir.path()).unwrap();
assert_eq!(read, Some(content.to_owned()));
}
#[test]
fn rules_adapter_name() {
let adapter = OpenCodeRulesAdapter;
assert_eq!(adapter.name(), "opencode");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::adapters::test_utils::make_server;
use std::collections::BTreeMap;
use std::fs;
#[test]
#[serial_test::serial]
fn read_mcp_from_valid_file() {
let dir = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("HOME", dir.path()) };
let path = dir.path().join("opencode.json");
let json = r#"{
"mcp": {
"test-server": {
"type": "local",
"command": ["npx", "-y", "some-pkg"],
"enabled": true,
"environment": {"KEY": "value"},
"timeout": 5000
}
},
"otherField": true
}"#;
fs::write(&path, json).unwrap();
let adapter = OpencodeAdapter::with_project_root(dir.path().to_path_buf());
let config = adapter.read_mcp().unwrap();
assert_eq!(config.servers.len(), 1);
let server = &config.servers["test-server"];
assert_eq!(server.command, "npx");
assert_eq!(server.args, vec!["-y", "some-pkg"]);
assert_eq!(server.env.get("KEY").unwrap(), "value");
}
#[test]
#[serial_test::serial]
fn read_mcp_skips_empty_command() {
let dir = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("HOME", dir.path()) };
let path = dir.path().join("opencode.json");
let json = r#"{
"mcp": {
"bad-server": {
"type": "local",
"command": [],
"enabled": true
},
"good-server": {
"type": "local",
"command": ["node"],
"enabled": true
}
}
}"#;
fs::write(&path, json).unwrap();
let adapter = OpencodeAdapter::with_project_root(dir.path().to_path_buf());
let config = adapter.read_mcp().unwrap();
assert_eq!(config.servers.len(), 1);
assert!(config.servers.contains_key("good-server"));
assert!(!config.servers.contains_key("bad-server"));
}
#[test]
fn read_mcp_empty_when_no_field() {
let root: serde_json::Value = serde_json::json!({ "otherField": true });
let servers = root.get("mcp").and_then(|v| v.as_object());
assert!(servers.is_none());
}
#[test]
#[serial_test::serial]
fn write_mcp_preserves_other_fields() {
let dir = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("HOME", dir.path()) };
let path = dir.path().join("opencode.json");
let original = r#"{"otherField": true, "mcp": {}}"#;
fs::write(&path, original).unwrap();
let adapter = OpencodeAdapter::with_project_root(dir.path().to_path_buf());
let config = McpConfig {
servers: {
let mut m = BTreeMap::new();
m.insert("svr".into(), make_server("cmd", &["a"], &[("K", "V")]));
m
},
};
adapter.write_mcp(&config).unwrap();
let result: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(result["otherField"], true);
assert_eq!(result["mcp"]["svr"]["command"][0], "cmd");
assert_eq!(result["mcp"]["svr"]["type"], "local");
assert_eq!(result["mcp"]["svr"]["enabled"], true);
}
#[test]
#[serial_test::serial]
fn write_mcp_creates_file_when_missing() {
let dir = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("HOME", dir.path()) };
let subdir = dir.path().join("subdir");
fs::create_dir_all(subdir.join(".git")).unwrap();
let path = subdir.join("opencode.json");
assert!(!path.exists());
let adapter = OpencodeAdapter::with_project_root(subdir);
let config = McpConfig {
servers: {
let mut m = BTreeMap::new();
m.insert("s".into(), make_server("c", &[], &[]));
m
},
};
adapter.write_mcp(&config).unwrap();
assert!(path.exists());
let result: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(result["mcp"]["s"]["command"][0], "c");
assert_eq!(result["mcp"]["s"]["type"], "local");
assert_eq!(result["mcp"]["s"]["enabled"], true);
}
#[test]
#[serial_test::serial]
fn roundtrip_json() {
let dir = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("HOME", dir.path()) };
let path = dir.path().join("opencode.json");
fs::write(&path, "{}").unwrap();
let adapter = OpencodeAdapter::with_project_root(dir.path().to_path_buf());
let config = McpConfig {
servers: {
let mut m = BTreeMap::new();
m.insert(
"a".into(),
make_server("node", &["index.js"], &[("PORT", "3000")]),
);
m.insert("b".into(), make_server("python", &["main.py"], &[]));
m
},
};
adapter.write_mcp(&config).unwrap();
let parsed = adapter.read_mcp().unwrap();
assert_eq!(config, parsed);
}
#[test]
fn adapter_name() {
let adapter = OpencodeAdapter::new();
assert_eq!(ToolAdapter::name(&adapter), "opencode");
}
#[test]
fn config_paths_returns_both() {
let adapter = OpencodeAdapter::new();
let paths = ToolAdapter::config_paths(&adapter);
assert_eq!(paths.len(), 2);
assert!(paths[0].ends_with(".config/opencode/opencode.json"));
assert!(paths[1].ends_with("opencode.json"));
}
#[test]
fn with_project_root_overrides_cwd() {
let dir = tempfile::tempdir().unwrap();
let adapter = OpencodeAdapter::with_project_root(dir.path().to_path_buf());
let paths = ToolAdapter::config_paths(&adapter);
assert_eq!(paths.len(), 2);
assert_eq!(paths[1], dir.path().join("opencode.json"));
}
}
#[cfg(test)]
mod hooks_tests {
use super::*;
use crate::config::HookHandler;
use std::fs;
#[test]
fn opencode_hooks_event_mapping() {
let adapter = OpencodeAdapter::new();
assert_eq!(
adapter.lorum_to_tool_event("pre-tool-use"),
Some("preToolUse".into())
);
assert_eq!(
adapter.lorum_to_tool_event("post-tool-use"),
Some("postToolUse".into())
);
assert_eq!(
adapter.lorum_to_tool_event("session-start"),
Some("sessionStart".into())
);
assert_eq!(
adapter.tool_to_lorum_event("preToolUse"),
Some("pre-tool-use".into())
);
assert_eq!(
adapter.tool_to_lorum_event("postToolUse"),
Some("post-tool-use".into())
);
assert_eq!(
adapter.tool_to_lorum_event("sessionStart"),
Some("session-start".into())
);
assert_eq!(
adapter.tool_to_lorum_event(&adapter.lorum_to_tool_event("pre-read-file").unwrap()),
Some("pre-read-file".into())
);
}
#[test]
#[serial_test::serial]
fn opencode_hooks_roundtrip() {
let dir = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("HOME", dir.path()) };
let adapter = OpencodeAdapter::with_project_root(dir.path().to_path_buf());
let hooks_path = dir.path().join("hooks.json");
fs::write(&hooks_path, "{}").unwrap();
let mut config = HooksConfig::default();
config.events.insert(
"pre-tool-use".into(),
vec![HookHandler {
matcher: "Bash".into(),
command: "check.sh".into(),
timeout: Some(30),
handler_type: Some("command".into()),
}],
);
config.events.insert(
"post-tool-use".into(),
vec![HookHandler {
matcher: "*".into(),
command: "notify.sh".into(),
timeout: None,
handler_type: None,
}],
);
adapter.write_hooks(&config).unwrap();
let read = adapter.read_hooks().unwrap();
assert_eq!(read.events.len(), 2);
let handlers = &read.events["pre-tool-use"];
assert_eq!(handlers.len(), 1);
assert_eq!(handlers[0].matcher, "Bash");
assert_eq!(handlers[0].command, "check.sh");
assert_eq!(handlers[0].timeout, Some(30));
assert_eq!(handlers[0].handler_type, Some("command".into()));
unsafe { std::env::remove_var("HOME") };
}
#[test]
#[serial_test::serial]
fn opencode_hooks_preserves_other_fields() {
let dir = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("HOME", dir.path()) };
let path = dir.path().join("hooks.json");
let original = r#"{"version": 2, "hooks": {}}"#;
fs::write(&path, original).unwrap();
let adapter = OpencodeAdapter::with_project_root(dir.path().to_path_buf());
let mut config = HooksConfig::default();
config.events.insert(
"pre-tool-use".into(),
vec![HookHandler {
matcher: "Bash".into(),
command: "check.sh".into(),
timeout: None,
handler_type: None,
}],
);
adapter.write_hooks(&config).unwrap();
let result: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(result["version"], 2);
assert_eq!(result["hooks"]["preToolUse"][0]["matcher"], "Bash");
unsafe { std::env::remove_var("HOME") };
}
#[test]
fn opencode_hooks_config_paths() {
let dir = tempfile::tempdir().unwrap();
let adapter = OpencodeAdapter::with_project_root(dir.path().to_path_buf());
let paths = HooksAdapter::config_paths(&adapter);
assert_eq!(paths.len(), 2);
assert!(paths[0].ends_with(".config/opencode/hooks.json"));
assert_eq!(paths[1], dir.path().join("hooks.json"));
}
#[test]
#[serial_test::serial]
fn opencode_hooks_reads_global_fallback() {
let dir = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("HOME", dir.path()) };
let global_hooks_dir = dir.path().join(".config").join("opencode");
fs::create_dir_all(&global_hooks_dir).unwrap();
let global_hooks_path = global_hooks_dir.join("hooks.json");
let json = serde_json::json!({
"hooks": {
"preToolUse": [
{ "matcher": "*", "command": "global.sh" }
]
}
});
fs::write(&global_hooks_path, serde_json::to_string(&json).unwrap()).unwrap();
let adapter =
OpencodeAdapter::with_project_root(dir.path().join("nonexistent").to_path_buf());
let result = adapter.read_hooks().unwrap();
assert_eq!(result.events.len(), 1);
assert!(result.events.contains_key("pre-tool-use"));
assert_eq!(result.events["pre-tool-use"][0].command, "global.sh");
unsafe { std::env::remove_var("HOME") };
}
#[test]
fn opencode_hooks_empty_when_no_file() {
let dir = tempfile::tempdir().unwrap();
let adapter = OpencodeAdapter::with_project_root(dir.path().to_path_buf());
let result = adapter.read_hooks().unwrap();
assert!(result.events.is_empty());
}
#[test]
#[serial_test::serial]
fn opencode_hooks_unknown_events_preserved_on_read() {
let dir = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("HOME", dir.path()) };
let path = dir.path().join("hooks.json");
let json = serde_json::json!({
"hooks": {
"customEvent": [
{ "matcher": "*", "command": "run.sh" }
]
}
});
fs::write(&path, serde_json::to_string(&json).unwrap()).unwrap();
let adapter = OpencodeAdapter::with_project_root(dir.path().to_path_buf());
let config = adapter.read_hooks().unwrap();
assert!(config.events.contains_key("custom-event"));
let handlers = &config.events["custom-event"];
assert_eq!(handlers[0].command, "run.sh");
unsafe { std::env::remove_var("HOME") };
}
#[test]
#[serial_test::serial]
fn opencode_hooks_write_creates_file_when_missing() {
let dir = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("HOME", dir.path()) };
let path = dir.path().join("hooks.json");
assert!(!path.exists());
let adapter = OpencodeAdapter::with_project_root(dir.path().to_path_buf());
let mut config = HooksConfig::default();
config.events.insert(
"pre-tool-use".into(),
vec![HookHandler {
matcher: "Bash".into(),
command: "check.sh".into(),
timeout: None,
handler_type: None,
}],
);
adapter.write_hooks(&config).unwrap();
assert!(path.exists());
let result: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(result["hooks"]["preToolUse"][0]["matcher"], "Bash");
assert_eq!(result["hooks"]["preToolUse"][0]["command"], "check.sh");
unsafe { std::env::remove_var("HOME") };
}
}
#[cfg(test)]
mod skills_tests {
use super::*;
#[test]
fn read_skills_empty_when_no_dir() {
let adapter = OpenCodeSkillsAdapter;
let skills = adapter.read_skills().unwrap();
assert!(skills.is_empty());
}
#[test]
#[serial_test::serial]
fn write_skill_copies_directory_contents() {
let home = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("HOME", home.path()) };
let src = tempfile::tempdir().unwrap();
std::fs::write(
src.path().join("SKILL.md"),
"---\nname: test-skill\ndescription: \"Test\"\n---\n",
)
.unwrap();
let adapter = OpenCodeSkillsAdapter;
adapter.write_skill("test-skill", src.path()).unwrap();
let skills = adapter.read_skills().unwrap();
assert!(skills.iter().any(|s| s.manifest.name == "test-skill"));
adapter.remove_skill("test-skill").unwrap();
unsafe { std::env::remove_var("HOME") };
}
#[test]
#[serial_test::serial]
fn remove_skill_deletes_directory() {
let home = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("HOME", home.path()) };
let adapter = OpenCodeSkillsAdapter;
adapter
.write_skill("test-skill", tempfile::tempdir().unwrap().path())
.unwrap();
adapter.remove_skill("test-skill").unwrap();
let skills = adapter.read_skills().unwrap();
assert!(!skills.iter().any(|s| s.manifest.name == "test-skill"));
unsafe { std::env::remove_var("HOME") };
}
}