use crate::config::{Bundle, Config};
use crate::paths;
use anyhow::Context;
use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum StatusSection {
Scopes,
Tags,
Bundles,
Mcps,
Plugins,
Marketplaces,
All,
}
pub fn run_status(section: Option<StatusSection>, use_color: bool) -> anyhow::Result<()> {
match section {
Some(StatusSection::Scopes) => run_scope_ls(use_color),
Some(StatusSection::Tags) => run_tag_ls(use_color),
Some(StatusSection::Bundles) => run_bundle_ls(use_color),
Some(StatusSection::Mcps) => run_mcp_ls(use_color),
Some(StatusSection::Plugins) => run_plugin_ls(use_color),
Some(StatusSection::Marketplaces) => run_marketplace_ls(use_color),
Some(StatusSection::All) => {
run_scope_ls(use_color)?;
run_tag_ls(use_color)?;
run_bundle_ls(use_color)?;
run_mcp_ls(use_color)?;
run_plugin_ls(use_color)?;
run_marketplace_ls(use_color)
}
None => run_status_overview(use_color),
}
}
fn run_status_overview(use_color: bool) -> anyhow::Result<()> {
let config_path = paths::config_path()?;
let config = Config::load(&config_path)?;
eprintln!(
"{} Configuration loaded from {}",
super::doctor_pass(use_color),
config_path.display()
);
eprintln!(" Scopes:");
eprintln!(" Network: {}", config.scope.network.len());
eprintln!(" Host: {}", config.scope.host.len());
eprintln!(" User: {}", config.scope.user.len());
let env = crate::scope::matcher::Env::detect();
let active = crate::scope::evaluate(&config, &env);
if let Some(proj) = active.scopes.iter().find(|s| s.kind == "project") {
let label = proj.name.as_deref().unwrap_or(&proj.id);
if let Some(desc) = &proj.description {
eprintln!(" Project: {label} — {desc}");
} else {
eprintln!(" Project: {label}");
}
} else {
eprintln!(" Project: (none)");
}
eprintln!(" Bundles: {}", config.bundle.len());
Ok(())
}
fn run_scope_ls(use_color: bool) -> anyhow::Result<()> {
let config_path = paths::config_path()?;
let config = Config::load(&config_path)?;
let env = crate::scope::matcher::Env::detect();
let active = crate::scope::evaluate(&config, &env);
let consumed = super::all_consumed_tags(&config);
let active_ids: HashSet<(&str, &str)> = active
.scopes
.iter()
.map(|s| (s.kind, s.id.as_str()))
.collect();
let mut rows: Vec<(String, bool, bool)> = Vec::new();
let push = |rows: &mut Vec<(String, bool, bool)>,
kind: &str,
id: &str,
tags: &[String],
active_ids: &HashSet<(&str, &str)>,
consumed: &HashSet<String>| {
let is_active = active_ids.contains(&(kind, id));
let is_orphan = !tags.iter().any(|t| consumed.contains(t));
rows.push((format!("{}:{}", kind, id), is_active, is_orphan));
};
for s in &config.scope.network {
push(&mut rows, "network", &s.id, &s.tags, &active_ids, &consumed);
}
for s in &config.scope.host {
push(&mut rows, "host", &s.id, &s.tags, &active_ids, &consumed);
}
for s in &config.scope.user {
push(&mut rows, "user", &s.id, &s.tags, &active_ids, &consumed);
}
for scope in &active.scopes {
if scope.kind == "project" {
push(
&mut rows,
"project",
&scope.id,
&scope.tags,
&active_ids,
&consumed,
);
}
}
rows.sort_by(|a, b| a.0.cmp(&b.0));
if rows.is_empty() {
println!("(none configured)");
return Ok(());
}
for (name, is_active, is_orphan) in rows {
let mark = if is_active {
super::active_marker(use_color)
} else {
" ".to_string()
};
println!(
"{} {}{}",
mark,
name,
super::annotate(is_active, is_orphan, use_color)
);
}
Ok(())
}
fn run_tag_ls(use_color: bool) -> anyhow::Result<()> {
let config_path = paths::config_path()?;
let config = Config::load(&config_path)?;
let env = crate::scope::matcher::Env::detect();
let active = crate::scope::evaluate(&config, &env);
let emitted = super::all_emitted_tags(&config);
let consumed = super::all_consumed_tags(&config);
let mut universe: HashSet<String> = HashSet::new();
universe.extend(emitted.iter().cloned());
universe.extend(consumed.iter().cloned());
universe.extend(active.tags.iter().cloned());
let mut tags: Vec<String> = universe.into_iter().collect();
tags.sort();
if tags.is_empty() {
println!("(none configured)");
return Ok(());
}
for tag in tags {
let is_active = active.tags.contains(&tag);
let emitted_anywhere = emitted.contains(&tag) || active.tags.contains(&tag);
let consumed_anywhere = consumed.contains(&tag);
let is_orphan = !(emitted_anywhere && consumed_anywhere);
let mark = if is_active {
super::active_marker(use_color)
} else {
" ".to_string()
};
println!(
"{} {}{}",
mark,
tag,
super::annotate(is_active, is_orphan, use_color)
);
}
Ok(())
}
fn run_bundle_ls(use_color: bool) -> anyhow::Result<()> {
let config_path = paths::config_path()?;
let config = Config::load(&config_path)?;
let env = crate::scope::matcher::Env::detect();
let active = crate::scope::evaluate(&config, &env);
let mut emitted = super::all_emitted_tags(&config);
emitted.extend(active.tags.iter().cloned());
let marker_enabled = super::marker_enabled_bundle_names(&active);
let firing_names: HashSet<&str> = config
.bundle
.iter()
.filter(|b| {
b.when.iter().any(|t| active.tags.contains(t))
|| marker_enabled.contains(b.name.as_str())
})
.map(|b| b.name.as_str())
.collect();
let mut rows: Vec<(String, bool, bool)> = config
.bundle
.iter()
.map(|b| {
let is_active = firing_names.contains(b.name.as_str());
let has_emitted_tag = b.when.iter().any(|t| emitted.contains(t));
let is_orphan = !has_emitted_tag && !marker_enabled.contains(&b.name);
(b.name.clone(), is_active, is_orphan)
})
.collect();
rows.sort_by(|a, b| a.0.cmp(&b.0));
if rows.is_empty() {
println!("(none configured)");
return Ok(());
}
for (name, is_active, is_orphan) in rows {
let mark = if is_active {
super::active_marker(use_color)
} else {
" ".to_string()
};
println!(
"{} {}{}",
mark,
name,
super::annotate(is_active, is_orphan, use_color)
);
}
Ok(())
}
fn run_mcp_ls(use_color: bool) -> anyhow::Result<()> {
use crate::mcp::resolve::MEMORY_MCP_NAME;
use crate::mcp::resolve::{ResolvedKind, resolve_bundle_mcps, resolve_mcps};
let config_path = paths::config_path()?;
let config_dir = config_path
.parent()
.ok_or_else(|| anyhow::anyhow!("config path has no parent"))?;
let config = Config::load(&config_path)?;
let env = crate::scope::matcher::Env::detect();
let active = crate::scope::evaluate(&config, &env);
let mut emitted = super::all_emitted_tags(&config);
emitted.extend(active.tags.iter().cloned());
let manually_enabled: std::collections::HashSet<&str> = active
.scopes
.iter()
.flat_map(|s| s.enable_bundles.iter().map(String::as_str))
.collect();
let firing: Vec<&Bundle> = config
.bundle
.iter()
.filter(|b| {
b.when.iter().any(|bt| active.tags.contains(bt))
|| manually_enabled.contains(b.name.as_str())
})
.collect();
let bundle_refs = super::build_bundle_refs(config_dir, &active, &firing);
let bundle_caps = if !bundle_refs.is_empty() {
crate::merge::merge(&config.capabilities, &config.native, &bundle_refs)
.context("merging bundles for mcp-ls")?
.capabilities
} else {
crate::config::Capabilities::default()
};
let top_memory_ls = config
.features
.as_ref()
.map(|f| f.memory.as_slice())
.unwrap_or_default();
let bundle_memory_ls = bundle_caps
.features
.as_ref()
.map(|f| f.memory.as_slice())
.unwrap_or_default();
let mut all_memory_ls: Vec<crate::config::Memory> = top_memory_ls
.iter()
.chain(bundle_memory_ls.iter())
.cloned()
.collect();
crate::util::dedup(&mut all_memory_ls);
let mut all_host_ls = bundle_caps.host.clone();
for (k, v) in &config.host {
all_host_ls.insert(k.clone(), v.clone());
}
let mut all_resolved: std::collections::HashMap<String, ResolvedKind> =
resolve_mcps(&config.mcp, &all_memory_ls, &all_host_ls, &active.tags)
.context("resolving MCP servers for listing")?
.into_iter()
.map(|m| (m.name, m.kind))
.collect();
let bundle_mcp_entries = bundle_caps.mcp;
let bundle_resolved = resolve_bundle_mcps(&bundle_mcp_entries, &active.tags)
.context("resolving bundle MCP servers for listing")?;
for m in bundle_resolved {
all_resolved.entry(m.name).or_insert(m.kind);
}
let mut rows: Vec<(String, bool, bool, String)> = config
.mcp
.iter()
.map(|m| {
let is_active = m.when.is_empty() || m.when.iter().any(|t| active.tags.contains(t));
let is_orphan = !m.when.is_empty() && !m.when.iter().any(|t| emitted.contains(t));
let detail = mcp_kind_detail(
&m.name,
&format!("{:?}", m.transport).to_lowercase(),
&all_resolved,
);
(m.name.clone(), is_active, is_orphan, detail)
})
.collect();
for m in &bundle_mcp_entries {
let is_active = m.when.is_empty() || m.when.iter().any(|t| active.tags.contains(t));
let is_orphan = !m.when.is_empty() && !m.when.iter().any(|t| emitted.contains(t));
let detail = format!(
"{} (bundle)",
mcp_kind_detail(&m.name, "stdio server", &all_resolved)
);
rows.push((m.name.clone(), is_active, is_orphan, detail));
}
for mem in &all_memory_ls {
let is_active = mem.when.is_empty() || mem.when.iter().any(|t| active.tags.contains(t));
let is_orphan = !mem.when.is_empty() && !mem.when.iter().any(|t| emitted.contains(t));
let detail = mcp_kind_detail(MEMORY_MCP_NAME, "memory", &all_resolved);
let name = format!("{} ({})", MEMORY_MCP_NAME, mem.server_host);
rows.push((name, is_active, is_orphan, detail));
}
rows.sort_by(|a, b| a.0.cmp(&b.0));
print_detail_rows(&rows, use_color);
Ok(())
}
fn mcp_kind_detail(
name: &str,
fallback: &str,
resolved: &std::collections::HashMap<String, crate::mcp::resolve::ResolvedKind>,
) -> String {
use crate::mcp::resolve::ResolvedKind;
match resolved.get(name) {
Some(ResolvedKind::Stdio { .. }) => "stdio server".to_string(),
Some(ResolvedKind::Remote { transport, .. }) => {
format!("{} client", format!("{transport:?}").to_lowercase())
}
None => fallback.to_string(),
}
}
fn print_detail_rows(rows: &[(String, bool, bool, String)], use_color: bool) {
if rows.is_empty() {
println!("(none configured)");
return;
}
for (name, is_active, is_orphan, detail) in rows {
let mark = if *is_active {
super::active_marker(use_color)
} else {
" ".to_string()
};
println!(
"{} {} ({}){}",
mark,
name,
detail,
super::annotate(*is_active, *is_orphan, use_color)
);
}
}
fn run_marketplace_ls(use_color: bool) -> anyhow::Result<()> {
use crate::config::split_plugin_ref;
let config_path = paths::config_path()?;
let config = Config::load(&config_path)?;
let env = crate::scope::matcher::Env::detect();
let active = crate::scope::evaluate(&config, &env);
let mut emitted = super::all_emitted_tags(&config);
emitted.extend(active.tags.iter().cloned());
let active_refs: std::collections::HashSet<&str> = config
.plugin_collection
.iter()
.filter(|c| c.when.iter().any(|t| active.tags.contains(t)))
.flat_map(|c| c.plugins.iter())
.filter_map(|p| split_plugin_ref(p).map(|(m, _)| m))
.collect();
let referenceable: std::collections::HashSet<&str> = config
.plugin_collection
.iter()
.filter(|c| c.when.iter().any(|t| emitted.contains(t)))
.flat_map(|c| c.plugins.iter())
.filter_map(|p| split_plugin_ref(p).map(|(m, _)| m))
.collect();
let mut rows: Vec<(String, bool, bool, String)> = config
.marketplace
.iter()
.map(|m| {
let is_active = active_refs.contains(m.name.as_str());
let is_orphan = !referenceable.contains(m.name.as_str());
let kind = match m.classify_source() {
crate::config::MarketplaceSource::Git => "git",
crate::config::MarketplaceSource::Path => "path",
};
(m.name.clone(), is_active, is_orphan, kind.to_string())
})
.collect();
rows.sort_by(|a, b| a.0.cmp(&b.0));
print_detail_rows(&rows, use_color);
Ok(())
}
fn run_plugin_ls(use_color: bool) -> anyhow::Result<()> {
use crate::config::split_plugin_ref;
let config_path = paths::config_path()?;
let config = Config::load(&config_path)?;
let env = crate::scope::matcher::Env::detect();
let active = crate::scope::evaluate(&config, &env);
let mut emitted = super::all_emitted_tags(&config);
emitted.extend(active.tags.iter().cloned());
let mut rows: Vec<(String, bool, bool, String)> = Vec::new();
for collection in &config.plugin_collection {
let is_active = collection.when.iter().any(|t| active.tags.contains(t));
let is_orphan = !collection.when.iter().any(|t| emitted.contains(t));
for plugin in &collection.plugins {
let display = split_plugin_ref(plugin)
.map_or_else(|| plugin.clone(), |(m, p)| format!("{p}@{m}"));
rows.push((display, is_active, is_orphan, collection.name.clone()));
}
}
rows.sort_by(|a, b| a.0.cmp(&b.0));
if rows.is_empty() {
println!("(none configured)");
return Ok(());
}
for (name, is_active, is_orphan, collection) in rows {
let mark = if is_active {
super::active_marker(use_color)
} else {
" ".to_string()
};
println!(
"{} {} (from {}){}",
mark,
name,
collection,
super::annotate(is_active, is_orphan, use_color)
);
}
Ok(())
}