Skip to main content

zenith_cli/commands/plugin/
paths.rs

1//! Per-(agent, scope) target paths for the Zenith skill.
2
3use std::path::{Path, PathBuf};
4
5use super::agent::{Agent, Scope};
6
7/// The skill's stable name — used as directory slug and file stem.
8pub const SKILL_NAME: &str = "zenith";
9
10/// Where a skill is written for an agent.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum SkillTarget {
13    /// A folder skill: the whole tree is written under this directory.
14    Folder(PathBuf),
15    /// A single rule/markdown file written at this path.
16    Rule(PathBuf),
17}
18
19impl SkillTarget {
20    /// The root path on disk (the directory for a folder, the file for a rule).
21    pub fn root(&self) -> &Path {
22        match self {
23            SkillTarget::Folder(p) | SkillTarget::Rule(p) => p,
24        }
25    }
26}
27
28/// Resolve `$HOME` for user-scope installs.
29pub fn home_dir() -> Option<PathBuf> {
30    std::env::var_os("HOME").map(PathBuf::from)
31}
32
33/// Compute the skill target for `(agent, scope)`, with `project_root` as the
34/// base for project-scope paths. Returns `None` when the combination has no
35/// automatic filesystem target (e.g. Cursor/Windsurf user scope are UI-only).
36pub fn skill_target(agent: Agent, scope: Scope, project_root: &Path) -> Option<SkillTarget> {
37    // Folder agents: a `<base>/<conf>/skills/zenith/` directory.
38    let folder = |conf_project: &Path, conf_user: Option<PathBuf>| -> Option<SkillTarget> {
39        let base = match scope {
40            Scope::Project => project_root.join(conf_project),
41            Scope::User => conf_user?,
42        };
43        Some(SkillTarget::Folder(base.join("skills").join(SKILL_NAME)))
44    };
45    // Rule agents: a single file `<dir>/<file>`.
46    let rule =
47        |dir_project: PathBuf, dir_user: Option<PathBuf>, file: &str| -> Option<SkillTarget> {
48            let dir = match scope {
49                Scope::Project => project_root.join(dir_project),
50                Scope::User => dir_user?,
51            };
52            Some(SkillTarget::Rule(dir.join(file)))
53        };
54    let home = home_dir();
55
56    match agent {
57        Agent::ClaudeCode => folder(
58            Path::new(".claude"),
59            home.as_ref().map(|h| h.join(".claude")),
60        ),
61        // Codex adopts the cross-agent `.agents/skills/` location.
62        Agent::Codex => folder(
63            Path::new(".agents"),
64            home.as_ref().map(|h| h.join(".agents")),
65        ),
66        // OpenCode: project `.opencode/`, global `~/.config/opencode/`.
67        Agent::OpenCode => folder(
68            Path::new(".opencode"),
69            home.as_ref().map(|h| h.join(".config").join("opencode")),
70        ),
71        // Cursor rules — project-only (`.mdc`); user scope is UI-managed.
72        Agent::Cursor => rule(PathBuf::from(".cursor").join("rules"), None, "zenith.mdc"),
73        // Windsurf rules — project-only; user scope is UI-managed.
74        Agent::Windsurf => rule(PathBuf::from(".windsurf").join("rules"), None, "zenith.md"),
75        Agent::Aider => rule(
76            PathBuf::from(".aider"),
77            home.as_ref().map(|h| h.join(".aider")),
78            "zenith-skill.md",
79        ),
80        Agent::Zed => rule(
81            PathBuf::from(".zed"),
82            home.as_ref().map(|h| h.join(".zed")),
83            "zenith-skill.md",
84        ),
85        Agent::Gemini => rule(
86            PathBuf::from(".gemini"),
87            home.as_ref().map(|h| h.join(".gemini")),
88            "zenith-skill.md",
89        ),
90        Agent::Copilot => rule(
91            PathBuf::from(".copilot"),
92            home.as_ref().map(|h| h.join(".copilot")),
93            "zenith-skill.md",
94        ),
95        Agent::Continue => rule(
96            PathBuf::from(".continue").join("skills"),
97            home.as_ref().map(|h| h.join(".continue").join("skills")),
98            "zenith.md",
99        ),
100        Agent::Kiro => rule(
101            PathBuf::from(".kiro").join("steering"),
102            home.as_ref().map(|h| h.join(".kiro").join("steering")),
103            "zenith-skill.md",
104        ),
105        Agent::Antigravity => rule(
106            PathBuf::from(".antigravity"),
107            home.as_ref().map(|h| h.join(".antigravity")),
108            "zenith-skill.md",
109        ),
110    }
111}
112
113/// Slash-command directory for agents that support project commands. Returns
114/// `None` for agents with no known command convention.
115pub fn command_dir(agent: Agent, scope: Scope, project_root: &Path) -> Option<PathBuf> {
116    match agent {
117        Agent::ClaudeCode => Some(match scope {
118            Scope::Project => project_root.join(".claude").join("commands"),
119            Scope::User => home_dir()?.join(".claude").join("commands"),
120        }),
121        Agent::OpenCode => Some(match scope {
122            Scope::Project => project_root.join(".opencode").join("command"),
123            Scope::User => home_dir()?.join(".config").join("opencode").join("command"),
124        }),
125        _ => None,
126    }
127}