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
56// Why: public so the assembly can be asserted on directly. `execute` only
57// renders, and rendering goes to stdout where a test cannot see it — the same
58// split `plugins::validate::execute` uses.
59pub async fn explain(args: &ExplainArgs) -> Result<CommandOutput> {
60    let profile = systemprompt_config::ProfileBootstrap::get().context("Failed to get profile")?;
61    let services =
62        systemprompt_loader::ConfigLoader::load().context("Failed to load services config")?;
63    let services_root = std::path::PathBuf::from(profile.paths.services.clone());
64    let user_id = UserId::new(
65        args.user
66            .clone()
67            .unwrap_or_else(|| "cli-explain".to_owned()),
68    );
69
70    let mut trace = ManifestTrace::default();
71    let candidate = ManifestService::assemble_candidate_traced(
72        &services,
73        &services_root,
74        &profile.server.api_external_url,
75        &AllowAllFilter,
76        &user_id,
77        &mut trace,
78    )
79    .await
80    .context("Manifest assembly failed")?;
81
82    let mut rows = build_rows(&trace, &candidate);
83
84    if let Some(skill) = &args.skill {
85        rows.retain(|r| r.kind == "skill" && &r.id == skill);
86    }
87    if let Some(plugin) = &args.plugin {
88        rows.retain(|r| r.kind == "plugin" && &r.id == plugin);
89    }
90    rows.sort_by(|a, b| (&a.kind, &a.id).cmp(&(&b.kind, &b.id)));
91
92    Ok(CommandOutput::table_of(
93        vec!["kind", "id", "delivered", "dropped_at", "reason"],
94        &rows,
95    )
96    .with_title("Manifest Assembly Explain"))
97}
98
99fn build_rows(
100    trace: &ManifestTrace,
101    candidate: &systemprompt_marketplace::MarketplaceCandidate,
102) -> Vec<ExplainRow> {
103    let mut rows: Vec<ExplainRow> = trace
104        .events
105        .iter()
106        .map(|event| ExplainRow {
107            kind: event.kind.to_string(),
108            id: event.id.clone(),
109            delivered: false,
110            dropped_at: event.stage.to_string(),
111            reason: event.reason.clone(),
112        })
113        .collect();
114    let delivered = |kind: &str, id: String| ExplainRow {
115        kind: kind.to_owned(),
116        id,
117        delivered: true,
118        dropped_at: String::new(),
119        reason: String::new(),
120    };
121    rows.extend(
122        candidate
123            .skills
124            .iter()
125            .map(|s| delivered("skill", s.id.as_str().to_owned())),
126    );
127    rows.extend(
128        candidate
129            .plugins
130            .iter()
131            .map(|p| delivered("plugin", p.id.as_str().to_owned())),
132    );
133    rows.extend(
134        candidate
135            .agents
136            .iter()
137            .map(|a| delivered("agent", a.id.as_str().to_owned())),
138    );
139    rows.extend(
140        candidate
141            .managed_mcp_servers
142            .iter()
143            .map(|m| delivered("mcp-server", m.name.as_str().to_owned())),
144    );
145    rows.extend(
146        candidate
147            .artifacts
148            .iter()
149            .map(|a| delivered("artifact", a.id.as_str().to_owned())),
150    );
151    rows
152}