use crate::config::YamlConfig;
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandSource {
User,
Project,
}
impl CommandSource {
pub fn label(&self) -> &'static str {
match self {
CommandSource::User => "用户",
CommandSource::Project => "项目",
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct CommandFrontmatter {
pub name: String,
pub description: String,
}
#[derive(Debug, Clone)]
pub struct CustomCommand {
pub frontmatter: CommandFrontmatter,
pub body: String,
pub source: CommandSource,
}
pub fn commands_dir() -> PathBuf {
let dir = YamlConfig::data_dir().join("agent").join("commands");
let _ = fs::create_dir_all(&dir);
dir
}
pub fn project_commands_dir() -> Option<PathBuf> {
use super::permission::JcliConfig;
let config_dir = JcliConfig::find_config_dir()?;
let dir = config_dir.join("commands");
if dir.is_dir() { Some(dir) } else { None }
}
fn parse_command_md(path: &Path, source: CommandSource) -> Option<CustomCommand> {
let content = fs::read_to_string(path).ok()?;
let (fm_str, body) = super::skill::split_frontmatter(&content)?;
let frontmatter: CommandFrontmatter = serde_yaml::from_str(&fm_str).ok()?;
if frontmatter.name.is_empty() {
return None;
}
Some(CustomCommand {
frontmatter,
body: body.trim().to_string(),
source,
})
}
fn load_commands_from_dir(dir: &Path, source: CommandSource) -> Vec<CustomCommand> {
let mut commands = Vec::new();
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return commands,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let cmd_md = path.join("COMMAND.md");
if cmd_md.exists()
&& let Some(cmd) = parse_command_md(&cmd_md, source)
{
commands.push(cmd);
}
} else if path.extension().is_some_and(|ext| ext == "md") {
if let Some(cmd) = parse_command_md(&path, source) {
commands.push(cmd);
}
}
}
commands
}
pub fn load_all_commands() -> Vec<CustomCommand> {
let mut map: HashMap<String, CustomCommand> = HashMap::new();
for cmd in load_commands_from_dir(&commands_dir(), CommandSource::User) {
map.insert(cmd.frontmatter.name.clone(), cmd);
}
if let Some(dir) = project_commands_dir() {
for cmd in load_commands_from_dir(&dir, CommandSource::Project) {
map.insert(cmd.frontmatter.name.clone(), cmd);
}
}
let mut commands: Vec<CustomCommand> = map.into_values().collect();
commands.sort_by(|a, b| a.frontmatter.name.cmp(&b.frontmatter.name));
commands
}
pub fn expand_command_mentions(
text: &str,
commands: &[CustomCommand],
disabled: &[String],
) -> String {
let mut result = text.to_string();
loop {
let Some(start) = result.find("@command:") else {
break;
};
let name_start = start + "@command:".len();
let name_end = result[name_start..]
.find(|c: char| c.is_whitespace())
.map(|i| name_start + i)
.unwrap_or(result.len());
let name = &result[name_start..name_end];
if let Some(cmd) = commands
.iter()
.find(|c| c.frontmatter.name == name && !disabled.iter().any(|d| d == name))
{
result.replace_range(start..name_end, &cmd.body);
} else {
break;
}
}
result
}