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