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, AGENTS_ROOT, CLAUDE_ROOT, Layout, SHARED_ROOT, home};
16
17/// The roots one run touches, and the record that vouches for them.
18fn layout(agent: Agent) -> Result<Layout, AppError> {
19    let home = home()?;
20    Ok(Layout {
21        roots: roots(&home, agent),
22        every_root: roots(&home, Agent::All),
23        shared: home.join(SHARED_ROOT),
24        record: home.join(RECORD_PATH),
25    })
26}
27
28fn roots(home: &Utf8Path, agent: Agent) -> Vec<Utf8PathBuf> {
29    let mut roots = Vec::new();
30    if matches!(agent, Agent::Codex | Agent::All) {
31        roots.push(home.join(AGENTS_ROOT));
32    }
33    if matches!(agent, Agent::Claude | Agent::All) {
34        roots.push(home.join(CLAUDE_ROOT));
35    }
36    roots
37}
38
39/// List, print, install, or uninstall the embedded skills.
40///
41/// # Errors
42///
43/// [`AppError::Usage`] for an unknown skill or an unset home, and the
44/// installer's refusals and I/O errors.
45pub fn run(_ctx: &AppContext, args: SkillArgs) -> Result<(), AppError> {
46    match args.command {
47        SkillCommand::List => {
48            for name in crate::embedded::skill_names() {
49                output::line(name);
50            }
51            Ok(())
52        }
53        SkillCommand::Show(show) => {
54            let Some(text) = crate::embedded::skill(&show.name) else {
55                return Err(AppError::Usage(format!("no such skill: {}", show.name)));
56            };
57            output::line(text.trim_end_matches('\n'));
58            Ok(())
59        }
60        SkillCommand::Install(install) => {
61            let layout = layout(install.agent)?;
62            let lines = skill_installer::install(&layout, install.apply, install.force)?;
63            for line in lines {
64                output::line(line);
65            }
66            Ok(())
67        }
68        SkillCommand::Uninstall(uninstall) => {
69            let layout = layout(uninstall.agent)?;
70            let lines = skill_installer::uninstall(&layout, uninstall.apply)?;
71            for line in lines {
72                output::line(line);
73            }
74            Ok(())
75        }
76    }
77}