use std::path::Path;
pub trait AgentProvider {
fn binary(&self) -> &str;
fn build_args(&self, instruction_path: &Path, prompt: &str) -> Vec<String>;
fn display_name(&self) -> &str;
#[allow(dead_code)]
fn instruction_name(&self) -> &str;
fn instruction_dir(&self, role: &str) -> String;
#[allow(dead_code)]
fn instruction_extension(&self) -> &str {
"md"
}
}
pub struct PiProvider;
impl AgentProvider for PiProvider {
fn binary(&self) -> &str {
"pi"
}
fn display_name(&self) -> &str {
"pi"
}
fn instruction_name(&self) -> &str {
"skill"
}
fn instruction_dir(&self, role: &str) -> String {
let dir_name = role.replace('_', "-");
format!(".pi/skills/{dir_name}/SKILL.md")
}
fn build_args(&self, instruction: &Path, prompt: &str) -> Vec<String> {
vec![
"-p".to_string(),
prompt.to_string(),
"--skill".to_string(),
instruction.to_string_lossy().to_string(),
"--no-session".to_string(),
]
}
}
pub struct ClaudeCodeProvider;
impl AgentProvider for ClaudeCodeProvider {
fn binary(&self) -> &str {
"claude"
}
fn display_name(&self) -> &str {
"Claude Code"
}
fn instruction_name(&self) -> &str {
"agent"
}
fn instruction_dir(&self, role: &str) -> String {
format!(".claude/agents/{role}.md")
}
fn build_args(&self, instruction: &Path, prompt: &str) -> Vec<String> {
vec![
"-p".to_string(),
prompt.to_string(),
"--append-system-prompt-file".to_string(),
instruction.to_string_lossy().to_string(),
"--permission-mode".to_string(),
"bypassPermissions".to_string(),
]
}
}
pub struct CodexProvider;
impl AgentProvider for CodexProvider {
fn binary(&self) -> &str {
"codex"
}
fn display_name(&self) -> &str {
"Codex"
}
fn instruction_name(&self) -> &str {
"skill"
}
fn instruction_dir(&self, role: &str) -> String {
format!(".agents/skills/{role}/SKILL.md")
}
fn build_args(&self, _instruction: &Path, prompt: &str) -> Vec<String> {
vec![
"exec".to_string(),
"--sandbox".to_string(),
"workspace-write".to_string(),
prompt.to_string(),
]
}
}
pub struct OpenCodeProvider;
impl AgentProvider for OpenCodeProvider {
fn binary(&self) -> &str {
"opencode"
}
fn display_name(&self) -> &str {
"OpenCode"
}
fn instruction_name(&self) -> &str {
"agent"
}
fn instruction_dir(&self, role: &str) -> String {
format!(".opencode/agents/{role}.md")
}
fn build_args(&self, instruction: &Path, prompt: &str) -> Vec<String> {
let agent_name = instruction
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("build");
vec![
"run".to_string(),
"--agent".to_string(),
agent_name.to_string(),
"--dangerously-skip-permissions".to_string(),
prompt.to_string(),
]
}
}
pub fn from_name(name: &str) -> Box<dyn AgentProvider> {
match name.to_lowercase().as_str() {
"pi" => Box::new(PiProvider),
"claude" | "claude-code" | "claude_code" => Box::new(ClaudeCodeProvider),
"codex" => Box::new(CodexProvider),
"opencode" | "open-code" | "open_code" => Box::new(OpenCodeProvider),
other => panic!(
"provider desconocido: '{other}'. Providers vΓ‘lidos: pi, claude, codex, opencode"
),
}
}
#[allow(dead_code)]
pub fn supported_providers() -> Vec<&'static str> {
vec!["pi", "claude", "codex", "opencode"]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_name_returns_pi() {
let p = from_name("pi");
assert_eq!(p.binary(), "pi");
assert_eq!(p.display_name(), "pi");
assert_eq!(p.instruction_name(), "skill");
}
#[test]
fn from_name_returns_claude() {
let p = from_name("claude");
assert_eq!(p.binary(), "claude");
assert_eq!(p.display_name(), "Claude Code");
assert_eq!(p.instruction_name(), "agent");
}
#[test]
fn from_name_aliases_claude() {
for alias in &["claude-code", "claude_code"] {
let p = from_name(alias);
assert_eq!(
p.binary(),
"claude",
"alias '{alias}' deberΓa resolver a claude"
);
}
}
#[test]
fn from_name_returns_codex() {
let p = from_name("codex");
assert_eq!(p.binary(), "codex");
assert_eq!(p.display_name(), "Codex");
}
#[test]
fn from_name_returns_opencode() {
let p = from_name("opencode");
assert_eq!(p.binary(), "opencode");
assert_eq!(p.display_name(), "OpenCode");
}
#[test]
fn from_name_aliases_opencode() {
for alias in &["open-code", "open_code"] {
let p = from_name(alias);
assert_eq!(
p.binary(),
"opencode",
"alias '{alias}' deberΓa resolver a opencode"
);
}
}
#[test]
#[should_panic(expected = "provider desconocido")]
fn from_name_panics_on_unknown() {
from_name("chatgpt");
}
#[test]
fn from_name_is_case_insensitive() {
let p = from_name("CLAUDE");
assert_eq!(p.binary(), "claude");
}
#[test]
fn pi_instruction_dir() {
let p = PiProvider;
assert_eq!(
p.instruction_dir("product_owner"),
".pi/skills/product-owner/SKILL.md"
);
assert_eq!(
p.instruction_dir("qa_engineer"),
".pi/skills/qa-engineer/SKILL.md"
);
assert_eq!(
p.instruction_dir("developer"),
".pi/skills/developer/SKILL.md"
);
}
#[test]
fn pi_build_args() {
let p = PiProvider;
let args = p.build_args(Path::new("skills/po/SKILL.md"), "haz esto");
assert!(args.contains(&"-p".to_string()));
assert!(args.contains(&"haz esto".to_string()));
assert!(args.contains(&"--skill".to_string()));
assert!(args.contains(&"skills/po/SKILL.md".to_string()));
assert!(args.contains(&"--no-session".to_string()));
}
#[test]
fn claude_instruction_dir() {
let p = ClaudeCodeProvider;
assert_eq!(
p.instruction_dir("developer"),
".claude/agents/developer.md"
);
}
#[test]
fn claude_build_args_includes_bypass_permissions() {
let p = ClaudeCodeProvider;
let args = p.build_args(Path::new(".claude/agents/po.md"), "revisa esto");
assert!(args.contains(&"bypassPermissions".to_string()));
assert!(args.contains(&"--append-system-prompt-file".to_string()));
assert!(args.contains(&"-p".to_string()));
}
#[test]
fn codex_instruction_dir() {
let p = CodexProvider;
assert_eq!(
p.instruction_dir("product_owner"),
".agents/skills/product_owner/SKILL.md"
);
}
#[test]
fn codex_build_args_uses_exec_subcommand() {
let p = CodexProvider;
let args = p.build_args(Path::new("ignored"), "mi tarea");
assert_eq!(args[0], "exec");
assert!(args.contains(&"--sandbox".to_string()));
assert!(args.contains(&"workspace-write".to_string()));
assert!(args.contains(&"mi tarea".to_string()));
assert!(!args.contains(&"-p".to_string()));
}
#[test]
fn opencode_instruction_dir() {
let p = OpenCodeProvider;
assert_eq!(
p.instruction_dir("reviewer"),
".opencode/agents/reviewer.md"
);
}
#[test]
fn opencode_build_args_uses_run_with_agent() {
let p = OpenCodeProvider;
let args = p.build_args(
Path::new(".opencode/agents/product_owner.md"),
"refina esta historia",
);
assert_eq!(args[0], "run");
assert!(args.contains(&"--agent".to_string()));
assert!(args.contains(&"product_owner".to_string()));
assert!(args.contains(&"--dangerously-skip-permissions".to_string()));
assert!(args.contains(&"refina esta historia".to_string()));
assert!(!args.contains(&"-p".to_string()));
assert!(!args.contains(&"-q".to_string()));
assert!(!args.contains(&"-f".to_string()));
}
#[test]
fn supported_providers_includes_all_four() {
let names = supported_providers();
assert_eq!(names.len(), 4);
assert!(names.contains(&"pi"));
assert!(names.contains(&"claude"));
assert!(names.contains(&"codex"));
assert!(names.contains(&"opencode"));
}
}