systemprompt_cli/commands/core/plugins/
list.rs1use anyhow::{Context, Result};
2use clap::Args;
3use std::path::Path;
4
5use crate::CliConfig;
6use crate::shared::CommandOutput;
7
8use super::types::{PluginListOutput, PluginSummary};
9
10#[derive(Debug, Clone, Copy, Args)]
11pub struct ListArgs {
12 #[arg(long, help = "Show only enabled plugins")]
13 pub enabled: bool,
14
15 #[arg(long, help = "Show only disabled plugins", conflicts_with = "enabled")]
16 pub disabled: bool,
17}
18
19pub(super) fn execute(args: ListArgs, _config: &CliConfig) -> Result<CommandOutput> {
20 let plugins_path = get_plugins_path()?;
21 let plugins = scan_plugins(&plugins_path)?;
22
23 let filtered: Vec<PluginSummary> = plugins
24 .into_iter()
25 .filter(|p| {
26 if args.enabled {
27 p.enabled
28 } else if args.disabled {
29 !p.enabled
30 } else {
31 true
32 }
33 })
34 .collect();
35
36 let output = PluginListOutput { plugins: filtered };
37
38 Ok(CommandOutput::table_of(
39 vec![
40 "id",
41 "name",
42 "display_name",
43 "enabled",
44 "skill_count",
45 "agent_count",
46 ],
47 &output.plugins,
48 )
49 .with_title("Plugins"))
50}
51
52fn get_plugins_path() -> Result<std::path::PathBuf> {
53 let profile = systemprompt_config::ProfileBootstrap::get().context("Failed to get profile")?;
54 Ok(std::path::PathBuf::from(profile.paths.plugins()))
55}
56
57fn scan_plugins(plugins_path: &Path) -> Result<Vec<PluginSummary>> {
58 if !plugins_path.exists() {
59 return Ok(Vec::new());
60 }
61
62 let mut plugins = Vec::new();
63
64 for entry in std::fs::read_dir(plugins_path)? {
65 let entry = entry?;
66 let plugin_path = entry.path();
67
68 if !plugin_path.is_dir() {
69 continue;
70 }
71
72 let config_path = plugin_path.join("config.yaml");
73 if !config_path.exists() {
74 continue;
75 }
76
77 match parse_plugin_config(&config_path) {
78 Ok(plugin_file) => {
79 let plugin = &plugin_file.plugin;
80 let mut summary: PluginSummary = plugin.into();
81 summary.skill_count = estimate_component_count(&plugin.skills);
82 summary.agent_count = estimate_component_count(&plugin.agents);
83 plugins.push(summary);
84 },
85 Err(e) => {
86 tracing::warn!(
87 path = %config_path.display(),
88 error = %e,
89 "Failed to parse plugin config"
90 );
91 },
92 }
93 }
94
95 plugins.sort_by(|a, b| a.id.cmp(&b.id));
96 Ok(plugins)
97}
98
99fn parse_plugin_config(config_path: &Path) -> Result<systemprompt_models::PluginConfigFile> {
100 let content = std::fs::read_to_string(config_path)
101 .with_context(|| format!("Failed to read {}", config_path.display()))?;
102 let plugin_file: systemprompt_models::PluginConfigFile = serde_yaml::from_str(&content)
103 .with_context(|| format!("Failed to parse {}", config_path.display()))?;
104 Ok(plugin_file)
105}
106
107fn estimate_component_count(component: &systemprompt_models::PluginComponentRef) -> usize {
108 if component.source == systemprompt_models::ComponentSource::Explicit {
109 component.include.len()
110 } else {
111 0
112 }
113}