1use clap::{Arg, ArgAction, ArgMatches, Command};
27use skill_embed::{AgentSelector, Error, InstallOptions, Installer, Result, Scope};
28
29#[must_use]
33pub fn command(skills: &Installer) -> Command {
34 let tool = skills.tool_name().to_owned();
35 Command::new(skills.command_name().to_owned())
36 .about(format!("Manage the agent skills embedded in {tool}"))
37 .subcommand_required(true)
38 .arg_required_else_help(true)
39 .subcommand(flags(
40 Command::new("install").about(format!(
41 "Install the agent skills embedded in {tool}, \
42 and remove the ones it no longer carries"
43 )),
44 skills,
45 ))
46 .subcommand(flags(
47 Command::new("uninstall")
48 .visible_alias("remove")
49 .about(format!("Remove the agent skills embedded in {tool}")),
50 skills,
51 ))
52 .subcommand(flags(
53 Command::new("list").visible_alias("ls").about(format!(
54 "Show the agent skills embedded in {tool}, and where each one stands"
55 )),
56 skills,
57 ))
58}
59
60fn flags(command: Command, skills: &Installer) -> Command {
62 let defaults: Vec<String> = skills.default_agents().iter().map(ToString::to_string).collect();
63 command
64 .arg(
65 Arg::new("agent")
66 .long("agent")
67 .value_name("AGENT")
68 .action(ArgAction::Append)
69 .value_delimiter(',')
70 .default_value(defaults.join(","))
71 .help(format!("Target agent: {}, or all, or detected", skills.agent_choices())),
72 )
73 .arg(
74 Arg::new("dir")
75 .long("dir")
76 .value_name("DIR")
77 .help("Install to a custom directory (overrides --agent and --scope)"),
78 )
79 .arg(
80 Arg::new("scope")
81 .long("scope")
82 .value_name("SCOPE")
83 .value_parser(["project", "user"])
84 .default_value(skills.default_scope().as_str())
85 .help("Installation scope"),
86 )
87 .arg(
88 Arg::new("force")
89 .long("force")
90 .short('f')
91 .action(ArgAction::SetTrue)
92 .help("Overwrite existing skills"),
93 )
94 .arg(
95 Arg::new("dry-run")
96 .long("dry-run")
97 .action(ArgAction::SetTrue)
98 .help("Report what would happen without writing"),
99 )
100 .arg(
101 Arg::new("skill")
102 .value_name("SKILL")
103 .action(ArgAction::Append)
104 .help("The skills to act on, or every embedded skill"),
105 )
106}
107
108pub fn options(matches: &ArgMatches) -> Result<InstallOptions> {
118 let given: Vec<&String> = matches.get_many::<String>("agent").into_iter().flatten().collect();
119 let agents: Vec<AgentSelector> =
120 given.iter().flat_map(|v| AgentSelector::parse_list(v).collect::<Vec<_>>()).collect();
121 if !given.is_empty() && agents.is_empty() {
124 return Err(Error::NoAgentSelected);
125 }
126 let scope = match matches.get_one::<String>("scope") {
127 Some(s) => Some(s.parse::<Scope>()?),
128 None => None,
129 };
130 Ok(InstallOptions {
131 agents,
132 scope,
133 dir: matches.get_one::<String>("dir").map(Into::into),
134 force: matches.get_flag("force"),
135 dry_run: matches.get_flag("dry-run"),
136 names: matches.get_many::<String>("skill").into_iter().flatten().cloned().collect(),
137 cancel: None,
138 })
139}
140
141pub fn run(skills: &Installer, matches: &ArgMatches) -> Result<()> {
152 let Some((name, args)) = matches.subcommand() else {
153 return Err(Error::Usage(format!("{} needs a subcommand", skills.command_name())));
154 };
155 let options = options(args)?;
156 match name {
157 "list" => {
158 let statuses = skills.status(&options)?;
159 skills.write_report(&skill_embed::render_status(&statuses))
160 }
161 "install" | "uninstall" => {
162 let (results, outcome) = if name == "install" {
163 skills.install(&options)
164 } else {
165 skills.uninstall(&options)
166 };
167 skills.write_report(&skill_embed::render_results(&results, options.dry_run))?;
171 outcome
172 }
173 _ => Err(Error::Usage(format!("unknown {} subcommand {name:?}", skills.command_name()))),
174 }
175}