Skip to main content

systemprompt_cli/commands/core/marketplace/
mod.rs

1//! `marketplace` CLI command group: manifest-assembly diagnostics.
2//!
3//! [`MarketplaceCommands::Explain`] dry-runs the same assembly the bridge
4//! manifest endpoint performs and reports, per catalogue entry, whether it is
5//! delivered and — when it is not — the stage that dropped it and why.
6//!
7//! Copyright (c) systemprompt.io — Business Source License 1.1.
8//! See <https://systemprompt.io> for licensing details.
9
10use anyhow::{Context, Result};
11use clap::{Args, Subcommand};
12use serde::Serialize;
13use systemprompt_identifiers::UserId;
14use systemprompt_marketplace::{AllowAllFilter, ManifestService, ManifestTrace};
15
16use crate::context::CommandContext;
17use crate::shared::{CommandOutput, render_result};
18
19#[derive(Debug, Subcommand)]
20pub enum MarketplaceCommands {
21    #[command(about = "Explain which catalogue entries reach the bridge manifest and why")]
22    Explain(ExplainArgs),
23}
24
25#[derive(Debug, Clone, Args)]
26pub struct ExplainArgs {
27    #[arg(long, help = "Only report this skill id")]
28    pub skill: Option<String>,
29
30    #[arg(long, help = "Only report this plugin id")]
31    pub plugin: Option<String>,
32
33    #[arg(long, help = "User id to assemble for (extension filters may use it)")]
34    pub user: Option<String>,
35}
36
37#[derive(Debug, Serialize)]
38struct ExplainRow {
39    kind: String,
40    id: String,
41    delivered: bool,
42    dropped_at: String,
43    reason: String,
44}
45
46pub async fn execute(command: MarketplaceCommands, ctx: &CommandContext) -> Result<()> {
47    match command {
48        MarketplaceCommands::Explain(args) => {
49            let result = explain(&args).await.context("Failed to explain manifest")?;
50            render_result(&result, &ctx.cli);
51            Ok(())
52        },
53    }
54}
55
56async fn explain(args: &ExplainArgs) -> Result<CommandOutput> {
57    let profile = systemprompt_config::ProfileBootstrap::get().context("Failed to get profile")?;
58    let services =
59        systemprompt_loader::ConfigLoader::load().context("Failed to load services config")?;
60    let services_root = std::path::PathBuf::from(profile.paths.services.clone());
61    let user_id = UserId::new(
62        args.user
63            .clone()
64            .unwrap_or_else(|| "cli-explain".to_owned()),
65    );
66
67    let mut trace = ManifestTrace::default();
68    let candidate = ManifestService::assemble_candidate_traced(
69        &services,
70        &services_root,
71        &profile.server.api_external_url,
72        &AllowAllFilter,
73        &user_id,
74        &mut trace,
75    )
76    .await
77    .context("Manifest assembly failed")?;
78
79    let mut rows = build_rows(&trace, &candidate);
80
81    if let Some(skill) = &args.skill {
82        rows.retain(|r| r.kind == "skill" && &r.id == skill);
83    }
84    if let Some(plugin) = &args.plugin {
85        rows.retain(|r| r.kind == "plugin" && &r.id == plugin);
86    }
87    rows.sort_by(|a, b| (&a.kind, &a.id).cmp(&(&b.kind, &b.id)));
88
89    Ok(CommandOutput::table_of(
90        vec!["kind", "id", "delivered", "dropped_at", "reason"],
91        &rows,
92    )
93    .with_title("Manifest Assembly Explain"))
94}
95
96fn build_rows(
97    trace: &ManifestTrace,
98    candidate: &systemprompt_marketplace::MarketplaceCandidate,
99) -> Vec<ExplainRow> {
100    let mut rows: Vec<ExplainRow> = trace
101        .events
102        .iter()
103        .map(|event| ExplainRow {
104            kind: event.kind.to_string(),
105            id: event.id.clone(),
106            delivered: false,
107            dropped_at: event.stage.to_string(),
108            reason: event.reason.clone(),
109        })
110        .collect();
111    let delivered = |kind: &str, id: String| ExplainRow {
112        kind: kind.to_owned(),
113        id,
114        delivered: true,
115        dropped_at: String::new(),
116        reason: String::new(),
117    };
118    rows.extend(
119        candidate
120            .skills
121            .iter()
122            .map(|s| delivered("skill", s.id.as_str().to_owned())),
123    );
124    rows.extend(
125        candidate
126            .plugins
127            .iter()
128            .map(|p| delivered("plugin", p.id.as_str().to_owned())),
129    );
130    rows.extend(
131        candidate
132            .agents
133            .iter()
134            .map(|a| delivered("agent", a.id.as_str().to_owned())),
135    );
136    rows.extend(
137        candidate
138            .managed_mcp_servers
139            .iter()
140            .map(|m| delivered("mcp-server", m.name.as_str().to_owned())),
141    );
142    rows.extend(
143        candidate
144            .artifacts
145            .iter()
146            .map(|a| delivered("artifact", a.id.as_str().to_owned())),
147    );
148    rows
149}