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", "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            "storage" => return ext.has_storage_paths(),
85            _ => {},
86        }
87    }
88
89    true
90}
91
92fn compiled_summary(ext: &dyn Extension) -> ExtensionSummary {
93    let capabilities = CapabilitySummary {
94        jobs: ext.jobs().len(),
95        templates: ext.template_providers().len(),
96        schemas: ext.schemas().len(),
97        routes: 0,
98        tools: ext.tool_providers().len(),
99        roles: ext.roles().len(),
100        storage_paths: ext.required_storage_paths().len(),
101    };
102
103    ExtensionSummary {
104        id: systemprompt_identifiers::PluginId::new(ext.id()),
105        name: ext.name().to_owned(),
106        version: ext.version().to_owned(),
107        priority: ext.priority(),
108        source: ExtensionSource::Compiled,
109        enabled: true,
110        capabilities,
111    }
112}
113
114fn collect_manifest(args: &ListArgs) -> Vec<ExtensionSummary> {
115    let project_root = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::new());
116    let mut summaries = Vec::new();
117
118    for ext in ExtensionLoader::discover(&project_root) {
119        let type_matches = match args.r#type.as_str() {
120            "cli" => ext.is_cli(),
121            "mcp" => ext.is_mcp(),
122            _ => true,
123        };
124
125        if !type_matches {
126            continue;
127        }
128
129        if let Some(ref filter) = args.filter
130            && !ext
131                .manifest
132                .extension
133                .name
134                .to_lowercase()
135                .contains(&filter.to_lowercase())
136        {
137            continue;
138        }
139
140        summaries.push(ExtensionSummary {
141            id: systemprompt_identifiers::PluginId::new(ext.manifest.extension.name.clone()),
142            name: ext.manifest.extension.name.clone(),
143            version: "manifest".to_owned(),
144            priority: 100,
145            source: ExtensionSource::Manifest,
146            enabled: ext.is_enabled(),
147            capabilities: CapabilitySummary::default(),
148        });
149    }
150
151    summaries
152}