Skip to main content

systemprompt_cli/commands/core/plugins/
list.rs

1use 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}