use std::ffi::OsString;
use std::path::PathBuf;
use clap::{Arg, Command};
use qli_ext::{Discovery, Extension, ExtensionOrigin, Group};
use rmcp::schemars;
use serde::Serialize;
use crate::paths;
pub fn discover() -> Discovery {
let extensions_root = match paths::data_dir() {
Ok(d) => d.join("extensions"),
Err(err) => {
eprintln!(
"warning: could not resolve XDG data dir ({err:#}); extensions are disabled. \
Set $XDG_DATA_HOME or $HOME and retry."
);
PathBuf::new()
}
};
let embedded_root = materialize_embedded_layer();
let mut sources: Vec<(&std::path::Path, ExtensionOrigin)> = Vec::with_capacity(2);
sources.push((extensions_root.as_path(), ExtensionOrigin::Xdg));
if let Some(root) = embedded_root.as_deref() {
sources.push((root, ExtensionOrigin::Embedded));
}
qli_ext::discover(&sources)
}
fn materialize_embedded_layer() -> Option<PathBuf> {
let cache = match paths::cache_dir() {
Ok(c) => c,
Err(err) => {
eprintln!(
"warning: could not resolve XDG cache dir ({err:#}); embedded \
defaults are disabled. XDG extensions still work."
);
return None;
}
};
let root = cache.join("embedded").join(env!("CARGO_PKG_VERSION"));
if let Err(err) = std::fs::create_dir_all(&root) {
eprintln!(
"warning: could not create embedded cache at {} ({err}); \
embedded defaults are disabled.",
root.display(),
);
return None;
}
if let Err(err) = qli_ext::materialize_to(&root, false) {
eprintln!(
"warning: could not extract embedded defaults to {} ({err}); \
embedded defaults are disabled.",
root.display(),
);
return None;
}
Some(root)
}
#[derive(Clone, Debug, Serialize, schemars::JsonSchema)]
#[schemars(crate = "rmcp::schemars")]
pub struct ExtensionRecord {
pub extension: String,
pub group: String,
pub origin: String,
pub path: String,
}
impl ExtensionRecord {
pub fn new(group: &Group, extension: &Extension) -> Self {
Self {
extension: extension.name.clone(),
group: group.name.clone(),
origin: extension.origin.as_str().to_owned(),
path: extension.path.display().to_string(),
}
}
}
pub fn records(discovery: &Discovery) -> Vec<ExtensionRecord> {
discovery
.groups
.values()
.flat_map(|group| {
group
.extensions
.values()
.map(|extension| ExtensionRecord::new(group, extension))
})
.collect()
}
pub fn build_group_command(group: &Group) -> Command {
let mut cmd = Command::new(leak_str(&group.name))
.about(group.manifest.description.clone())
.arg_required_else_help(true);
for ext in group.extensions.values() {
cmd = cmd.subcommand(build_extension_command(ext));
}
cmd
}
fn build_extension_command(ext: &Extension) -> Command {
Command::new(leak_str(&ext.name))
.about(describe(ext))
.disable_help_flag(true)
.disable_version_flag(true)
.arg(
Arg::new("args")
.num_args(0..)
.trailing_var_arg(true)
.allow_hyphen_values(true)
.value_parser(clap::value_parser!(OsString))
.help("Arguments forwarded to the extension"),
)
}
fn leak_str(s: &str) -> &'static str {
Box::leak(s.to_owned().into_boxed_str())
}
fn describe(ext: &Extension) -> String {
format!("{}: {}", ext.origin.as_str(), ext.path.display())
}