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
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}