systemprompt_cli/commands/core/marketplace/
mod.rs1use 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
56pub 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}