Skip to main content

spec_driven_docs/commands/
skill.rs

1//! `skill` subcommand: runtime-shape.
2//!
3//! Lists, prints, installs, and removes the embedded skills. Install and
4//! uninstall semantics live in `services::skill_installer`; this handler
5//! only resolves the home directory, the chosen roots, and the user-scope
6//! record that sits beside them.
7
8use camino::{Utf8Path, Utf8PathBuf};
9
10use crate::cli::skill::{Agent, SkillArgs, SkillCommand};
11use crate::context::AppContext;
12use crate::domain::skill_record::RECORD_PATH;
13use crate::error::AppError;
14use crate::output;
15use crate::services::skill_installer::{self, Layout};
16
17fn home() -> Result<Utf8PathBuf, AppError> {
18    std::env::var("HOME")
19        .ok()
20        .filter(|home| !home.is_empty())
21        .map(Utf8PathBuf::from)
22        .ok_or_else(|| AppError::Usage("HOME is not set".to_string()))
23}
24
25/// The root holding what the skills share, relative to the home directory.
26///
27/// Home-relative rather than `XDG_STATE_HOME`-relative for the reason the
28/// record states: the skills naming these artifacts live under
29/// `$HOME/.agents` and `$HOME/.claude`, which no XDG variable moves, and a
30/// shared file reachable under a different home than the skills reading it
31/// would be worse than no shared file at all.
32const SHARED_ROOT: &str = ".local/state/spec-driven-docs/skills/shared";
33
34/// The roots one run touches, and the record that vouches for them.
35fn layout(agent: Agent) -> Result<Layout, AppError> {
36    let home = home()?;
37    Ok(Layout {
38        roots: roots(&home, agent),
39        every_root: roots(&home, Agent::All),
40        shared: home.join(SHARED_ROOT),
41        record: home.join(RECORD_PATH),
42    })
43}
44
45fn roots(home: &Utf8Path, agent: Agent) -> Vec<Utf8PathBuf> {
46    let mut roots = Vec::new();
47    if matches!(agent, Agent::Codex | Agent::All) {
48        roots.push(home.join(".agents/skills"));
49    }
50    if matches!(agent, Agent::Claude | Agent::All) {
51        roots.push(home.join(".claude/skills"));
52    }
53    roots
54}
55
56/// List, print, install, or uninstall the embedded skills.
57///
58/// # Errors
59///
60/// [`AppError::Usage`] for an unknown skill or an unset home, and the
61/// installer's refusals and I/O errors.
62pub fn run(_ctx: &AppContext, args: SkillArgs) -> Result<(), AppError> {
63    match args.command {
64        SkillCommand::List => {
65            for name in crate::embedded::skill_names() {
66                output::line(name);
67            }
68            Ok(())
69        }
70        SkillCommand::Show(show) => {
71            let Some(text) = crate::embedded::skill(&show.name) else {
72                return Err(AppError::Usage(format!("no such skill: {}", show.name)));
73            };
74            output::line(text.trim_end_matches('\n'));
75            Ok(())
76        }
77        SkillCommand::Install(install) => {
78            let layout = layout(install.agent)?;
79            let lines = skill_installer::install(&layout, install.apply, install.force)?;
80            for line in lines {
81                output::line(line);
82            }
83            Ok(())
84        }
85        SkillCommand::Uninstall(uninstall) => {
86            let layout = layout(uninstall.agent)?;
87            let lines = skill_installer::uninstall(&layout, uninstall.apply)?;
88            for line in lines {
89                output::line(line);
90            }
91            Ok(())
92        }
93    }
94}