Skip to main content

systemprompt_cli/commands/core/plugins/
list.rs

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