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 roots the run touches and the receipt that vouches
6//! for them.
7
8use camino::Utf8PathBuf;
9
10use crate::cli::skill::{Agent, SkillArgs, SkillCommand};
11use crate::context::AppContext;
12use crate::domain::paths::{
13    AgentId, LEGACY_SHARED_ROOT, LEGACY_SKILL_RECEIPT_PATH, SKILL_RECEIPT_FILE, UserEnv,
14};
15use crate::error::AppError;
16use crate::output;
17use crate::services::skill_installer::{self, Layout, home};
18
19/// The roots one run touches, and the receipt that vouches for them.
20fn layout(agent: Agent) -> Result<Layout, AppError> {
21    let home = home()?;
22    let env = UserEnv::from_process();
23    let state_root = env
24        .state_root()
25        .ok_or_else(|| AppError::Usage("no state root resolves".to_string()))?
26        .path;
27    Ok(Layout {
28        roots: roots(&env, agent),
29        receipt: state_root.join(SKILL_RECEIPT_FILE),
30        legacy_receipt: home.join(LEGACY_SKILL_RECEIPT_PATH),
31        legacy_shared: home.join(LEGACY_SHARED_ROOT),
32        state_root,
33    })
34}
35
36/// The selected roots, resolved through the table and deduplicated there.
37fn roots(env: &UserEnv, agent: Agent) -> Vec<Utf8PathBuf> {
38    let selected: &[AgentId] = match agent {
39        Agent::Claude => &[AgentId::Claude],
40        Agent::Agents => &[AgentId::Agents],
41        Agent::All => &[AgentId::Claude, AgentId::Agents],
42    };
43    env.agent_roots(selected)
44        .into_iter()
45        .map(|entry| entry.path)
46        .collect()
47}
48
49/// List, print, install, or uninstall the embedded skills.
50///
51/// # Errors
52///
53/// [`AppError::Usage`] for an unknown skill or an unset home, and the
54/// installer's refusals and I/O errors.
55pub fn run(_ctx: &AppContext, args: SkillArgs) -> Result<(), AppError> {
56    match args.command {
57        SkillCommand::List => {
58            for name in crate::embedded::skill_names() {
59                output::line(name);
60            }
61            Ok(())
62        }
63        SkillCommand::Show(show) => {
64            let Some(text) = crate::embedded::skill(&show.name) else {
65                return Err(AppError::Usage(format!("no such skill: {}", show.name)));
66            };
67            output::line(text.trim_end_matches('\n'));
68            Ok(())
69        }
70        SkillCommand::Install(install) => {
71            let layout = layout(install.agent)?;
72            let lines = skill_installer::install(&layout, install.apply, install.force)?;
73            for line in lines {
74                output::line(line);
75            }
76            Ok(())
77        }
78        SkillCommand::Uninstall(uninstall) => {
79            let layout = layout(uninstall.agent)?;
80            let lines = skill_installer::uninstall(&layout, uninstall.apply)?;
81            for line in lines {
82                output::line(line);
83            }
84            Ok(())
85        }
86    }
87}