Skip to main content

systemprompt_cli/commands/plugins/
list.rs

1//! `plugins list` command over compiled and manifest-discovered extensions.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use clap::Args;
7use systemprompt_extension::Extension;
8use systemprompt_loader::ExtensionLoader;
9
10use super::discover_registry;
11use super::types::{CapabilitySummary, ExtensionListOutput, ExtensionSource, ExtensionSummary};
12use crate::CliConfig;
13use crate::shared::CommandOutput;
14
15#[derive(Debug, Clone, Args)]
16pub struct ListArgs {
17    #[arg(long, help = "Filter by extension ID (substring match)")]
18    pub filter: Option<String>,
19
20    #[arg(long, value_parser = ["jobs", "templates", "schemas", "routes", "tools", "roles", "llm", "storage"])]
21    pub capability: Option<String>,
22
23    #[arg(long, value_parser = ["compiled", "manifest", "cli", "mcp", "all"], default_value = "all", help = "Filter by extension type")]
24    pub r#type: String,
25}
26
27pub fn execute(args: &ListArgs, _config: &CliConfig) -> CommandOutput {
28    let mut extensions: Vec<ExtensionSummary> = Vec::new();
29
30    if matches!(args.r#type.as_str(), "all" | "compiled") {
31        extensions.extend(collect_compiled(args));
32    }
33
34    if matches!(args.r#type.as_str(), "all" | "manifest" | "cli" | "mcp") {
35        extensions.extend(collect_manifest(args));
36    }
37
38    extensions.sort_by_key(|e| e.priority);
39
40    let total = extensions.len();
41
42    let output = ExtensionListOutput { extensions, total };
43
44    CommandOutput::table_of(
45        vec![
46            "id",
47            "name",
48            "version",
49            "priority",
50            "source",
51            "capabilities",
52        ],
53        &output.extensions,
54    )
55    .with_title("Extensions")
56}
57
58fn collect_compiled(args: &ListArgs) -> Vec<ExtensionSummary> {
59    let registry = discover_registry();
60    registry
61        .extensions()
62        .iter()
63        .filter(|ext| matches_compiled_filters(ext.as_ref(), args))
64        .map(|ext| compiled_summary(ext.as_ref()))
65        .collect()
66}
67
68fn matches_compiled_filters(ext: &dyn Extension, args: &ListArgs) -> bool {
69    if let Some(ref filter) = args.filter
70        && !ext.id().to_lowercase().contains(&filter.to_lowercase())
71        && !ext.name().to_lowercase().contains(&filter.to_lowercase())
72    {
73        return false;
74    }
75
76    if let Some(ref cap) = args.capability {
77        match cap.as_str() {
78            "jobs" => return ext.has_jobs(),
79            "templates" => return ext.has_template_providers(),
80            "schemas" => return ext.has_schemas(),
81            "routes" => return false,
82            "tools" => return ext.has_tool_providers(),
83            "roles" => return ext.has_roles(),
84            "llm" => return ext.has_llm_providers(),
85            "storage" => return ext.has_storage_paths(),
86            _ => {},
87        }
88    }
89
90    true
91}
92
93fn compiled_summary(ext: &dyn Extension) -> ExtensionSummary {
94    let capabilities = CapabilitySummary {
95        jobs: ext.jobs().len(),
96        templates: ext.template_providers().len(),
97        schemas: ext.schemas().len(),
98        routes: 0,
99        tools: ext.tool_providers().len(),
100        roles: ext.roles().len(),
101        llm_providers: ext.llm_providers().len(),
102        storage_paths: ext.required_storage_paths().len(),
103    };
104
105    ExtensionSummary {
106        id: systemprompt_identifiers::PluginId::new(ext.id()),
107        name: ext.name().to_owned(),
108        version: ext.version().to_owned(),
109        priority: ext.priority(),
110        source: ExtensionSource::Compiled,
111        enabled: true,
112        capabilities,
113    }
114}
115
116fn collect_manifest(args: &ListArgs) -> Vec<ExtensionSummary> {
117    let project_root = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::new());
118    let mut summaries = Vec::new();
119
120    for ext in ExtensionLoader::discover(&project_root) {
121        let type_matches = match args.r#type.as_str() {
122            "cli" => ext.is_cli(),
123            "mcp" => ext.is_mcp(),
124            _ => true,
125        };
126
127        if !type_matches {
128            continue;
129        }
130
131        if let Some(ref filter) = args.filter
132            && !ext
133                .manifest
134                .extension
135                .name
136                .to_lowercase()
137                .contains(&filter.to_lowercase())
138        {
139            continue;
140        }
141
142        summaries.push(ExtensionSummary {
143            id: systemprompt_identifiers::PluginId::new(ext.manifest.extension.name.clone()),
144            name: ext.manifest.extension.name.clone(),
145            version: "manifest".to_owned(),
146            priority: 100,
147            source: ExtensionSource::Manifest,
148            enabled: ext.is_enabled(),
149            capabilities: CapabilitySummary::default(),
150        });
151    }
152
153    summaries
154}