#![allow(dead_code)]
use std::collections::HashSet;
use std::path::Path;
use std::sync::Mutex;
use once_cell::sync::Lazy;
use super::frontmatter_parser::parse_frontmatter;
use super::loader::load_all_plugins_cache_only;
use super::plugin_options_storage::{
load_plugin_options, substitute_plugin_variables, substitute_user_config_in_content,
};
use super::walk_plugin_markdown::{WalkPluginMarkdownOpts, walk_plugin_markdown};
use crate::plugin::types::PluginManifest;
static PLUGIN_COMMAND_CACHE: Lazy<Mutex<Option<Vec<Command>>>> = Lazy::new(|| Mutex::new(None));
#[derive(Clone, Debug)]
pub struct Command {
pub command_type: String,
pub name: String,
pub description: String,
pub content: String,
pub source: String,
pub plugin: Option<String>,
pub is_hidden: bool,
pub allowed_tools: Vec<String>,
}
pub async fn load_plugin_commands() -> Result<Vec<Command>, Box<dyn std::error::Error + Send + Sync>>
{
{
let cache = PLUGIN_COMMAND_CACHE.lock().unwrap();
if let Some(ref commands) = *cache {
return Ok(commands.clone());
}
}
let plugin_result = load_all_plugins_cache_only().await?;
let mut all_commands = Vec::new();
for plugin in &plugin_result.enabled {
let mut loaded_paths = HashSet::new();
if let Some(ref commands_path) = plugin.commands_path {
match load_commands_from_directory(
Path::new(commands_path),
&plugin.name,
&plugin.source,
&plugin.manifest,
&plugin.path,
&mut loaded_paths,
false,
)
.await
{
Ok(commands) => {
log::debug!(
"Loaded {} commands from plugin {} default directory",
commands.len(),
plugin.name
);
all_commands.extend(commands);
}
Err(e) => log::debug!(
"Failed to load commands from plugin {} default directory: {}",
plugin.name,
e
),
}
}
if let Some(ref commands_paths) = plugin.commands_paths {
for command_path in commands_paths {
if let Ok(commands) = load_commands_from_path(
command_path,
&plugin.name,
&plugin.source,
&plugin.manifest,
&plugin.path,
&mut loaded_paths,
)
.await
{
all_commands.extend(commands);
}
}
}
}
log::debug!("Total plugin commands loaded: {}", all_commands.len());
{
let mut cache = PLUGIN_COMMAND_CACHE.lock().unwrap();
*cache = Some(all_commands.clone());
}
Ok(all_commands)
}
async fn load_commands_from_directory(
commands_path: &Path,
plugin_name: &str,
source_name: &str,
plugin_manifest: &PluginManifest,
plugin_path: &str,
loaded_paths: &mut HashSet<String>,
is_skill_mode: bool,
) -> Result<Vec<Command>, Box<dyn std::error::Error + Send + Sync>> {
use std::sync::Arc;
use tokio::sync::Mutex;
let commands: Arc<Mutex<Vec<Command>>> = Arc::new(Mutex::new(Vec::new()));
walk_plugin_markdown(
commands_path,
|full_path, _namespace| {
let plugin_name = plugin_name.to_string();
let source_name = source_name.to_string();
let manifest = plugin_manifest.clone();
let plugin_path = plugin_path.to_string();
let base_dir = commands_path.to_path_buf();
let commands = Arc::clone(&commands);
Box::pin(async move {
let path_str = full_path.clone();
if let Ok(content) = tokio::fs::read_to_string(&full_path).await {
let command_name =
get_command_name_from_file(Path::new(&full_path), &base_dir, &plugin_name);
if let Ok(Some(command)) = create_plugin_command(
&command_name,
&content,
&path_str,
&source_name,
&manifest,
&plugin_path,
false,
is_skill_mode,
)
.await
{
commands.lock().await.push(command);
}
}
})
},
WalkPluginMarkdownOpts {
stop_at_skill_dir: Some(false),
log_label: Some("commands".to_string()),
},
)
.await
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
Ok(Arc::try_unwrap(commands).unwrap().into_inner())
}
fn get_command_name_from_file(file_path: &Path, base_dir: &Path, plugin_name: &str) -> String {
let is_skill = is_skill_file(file_path);
let command_base_name = if is_skill {
file_path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default()
} else {
file_path
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default()
};
let relative_path = file_path
.parent()
.and_then(|p| p.strip_prefix(base_dir).ok())
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
let namespace = if relative_path.is_empty() {
String::new()
} else {
relative_path.replace('/', ":")
};
if namespace.is_empty() {
format!("{}:{}", plugin_name, command_base_name)
} else {
format!("{}:{}:{}", plugin_name, namespace, command_base_name)
}
}
fn is_skill_file(file_path: &Path) -> bool {
file_path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.map(|n| n.to_lowercase() == "skill.md")
.unwrap_or(false)
}
async fn create_plugin_command(
command_name: &str,
content: &str,
file_path: &str,
source_name: &str,
plugin_manifest: &PluginManifest,
plugin_path: &str,
is_skill: bool,
is_skill_mode: bool,
) -> Result<Option<Command>, Box<dyn std::error::Error + Send + Sync>> {
let (frontmatter, markdown_content) = parse_frontmatter(content, file_path);
let description = frontmatter
.get("description")
.and_then(|v| v.as_str())
.unwrap_or(if is_skill {
"Plugin skill"
} else {
"Plugin command"
})
.to_string();
let mut final_content = if is_skill_mode {
if let Some(parent) = std::path::Path::new(file_path).parent() {
format!(
"Base directory for this skill: {}\n\n{}",
parent.display(),
markdown_content
)
} else {
markdown_content.to_string()
}
} else {
markdown_content.to_string()
};
final_content = substitute_plugin_variables(&final_content, plugin_path, source_name);
if plugin_manifest.user_config.is_some() {
let options = load_plugin_options(source_name);
final_content = substitute_user_config_in_content(
&final_content,
&options,
plugin_manifest.user_config.as_ref().unwrap(),
);
}
Ok(Some(Command {
command_type: "prompt".to_string(),
name: command_name.to_string(),
description,
content: final_content,
source: "plugin".to_string(),
plugin: Some(source_name.to_string()),
is_hidden: false,
allowed_tools: Vec::new(),
}))
}
async fn load_commands_from_path(
command_path: &str,
plugin_name: &str,
source_name: &str,
plugin_manifest: &PluginManifest,
plugin_path: &str,
loaded_paths: &mut HashSet<String>,
) -> Result<Vec<Command>, Box<dyn std::error::Error + Send + Sync>> {
let metadata = tokio::fs::metadata(command_path)
.await
.map_err(|e| format!("Failed to stat {}: {}", command_path, e))?;
if metadata.is_dir() {
load_commands_from_directory(
Path::new(command_path),
plugin_name,
source_name,
plugin_manifest,
plugin_path,
loaded_paths,
false,
)
.await
} else if metadata.is_file()
&& Path::new(command_path)
.extension()
.map(|e| e.to_string_lossy() == "md")
.unwrap_or(false)
{
if loaded_paths.contains(command_path) {
return Ok(Vec::new());
}
loaded_paths.insert(command_path.to_string());
let content = tokio::fs::read_to_string(command_path)
.await
.map_err(|e| format!("Failed to read {}: {}", command_path, e))?;
let path_str = command_path.to_string();
let command_name = format!(
"{}:{}",
plugin_name,
Path::new(command_path)
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default()
);
match create_plugin_command(
&command_name,
&content,
&path_str,
source_name,
plugin_manifest,
plugin_path,
false,
false,
)
.await
{
Ok(Some(cmd)) => Ok(vec![cmd]),
_ => Ok(Vec::new()),
}
} else {
Ok(Vec::new())
}
}
pub fn clear_plugin_command_cache() {
let mut cache = PLUGIN_COMMAND_CACHE.lock().unwrap();
*cache = None;
}