Skip to main content

battlecommand_forge/
custom_commands.rs

1/// Custom commands from `.battlecommand/commands/*.md` files.
2///
3/// Each .md file becomes a command:
4///   .battlecommand/commands/deploy.md -> /deploy
5///
6/// File format:
7/// ```markdown
8/// # Deploy
9/// Description: Deploy the current project
10/// Model: qwen2.5-coder:7b
11///
12/// ## Prompt
13/// Deploy the application by creating a Dockerfile and docker-compose.yml...
14/// ```
15use anyhow::Result;
16use std::path::Path;
17
18#[derive(Debug, Clone)]
19pub struct CustomCommand {
20    pub name: String,
21    pub description: String,
22    pub model: Option<String>,
23    pub prompt: String,
24}
25
26/// Load all custom commands from `.battlecommand/commands/`.
27pub async fn load_commands(workspace: &str) -> Result<Vec<CustomCommand>> {
28    let dir = format!("{}/.battlecommand/commands", workspace);
29    if !Path::new(&dir).exists() {
30        return Ok(Vec::new());
31    }
32
33    let mut commands = Vec::new();
34    let mut entries = tokio::fs::read_dir(&dir).await?;
35
36    while let Some(entry) = entries.next_entry().await? {
37        let path = entry.path();
38        if path.extension().map(|e| e == "md").unwrap_or(false) {
39            if let Ok(content) = tokio::fs::read_to_string(&path).await {
40                if let Some(cmd) = parse_command_file(&path, &content) {
41                    commands.push(cmd);
42                }
43            }
44        }
45    }
46
47    commands.sort_by(|a, b| a.name.cmp(&b.name));
48    Ok(commands)
49}
50
51fn parse_command_file(path: &Path, content: &str) -> Option<CustomCommand> {
52    let name = path.file_stem()?.to_str()?.to_string();
53    let mut description = String::new();
54    let mut model = None;
55    let mut prompt = String::new();
56    let mut in_prompt = false;
57
58    for line in content.lines() {
59        if line.starts_with("Description:") {
60            description = line.trim_start_matches("Description:").trim().to_string();
61        } else if line.starts_with("Model:") {
62            model = Some(line.trim_start_matches("Model:").trim().to_string());
63        } else if line.trim() == "## Prompt" {
64            in_prompt = true;
65        } else if in_prompt {
66            prompt.push_str(line);
67            prompt.push('\n');
68        }
69    }
70
71    if prompt.is_empty() {
72        prompt = content.to_string();
73    }
74
75    Some(CustomCommand {
76        name,
77        description,
78        model,
79        prompt: prompt.trim().to_string(),
80    })
81}
82
83/// Create an example custom command.
84pub async fn create_example_command(workspace: &str) -> Result<()> {
85    let dir = format!("{}/.battlecommand/commands", workspace);
86    tokio::fs::create_dir_all(&dir).await?;
87
88    let example = r#"# Review
89Description: Run a comprehensive code review on the project
90Model: qwen2.5-coder:7b
91
92## Prompt
93Review all Python files in the tasks/ directory. For each file:
941. Check code quality and style
952. Identify potential bugs
963. Suggest improvements
974. Rate each file 1-10
98
99Output a summary table with file names and scores.
100"#;
101
102    tokio::fs::write(format!("{}/review.md", dir), example).await?;
103    println!("Created example command: .battlecommand/commands/review.md");
104    Ok(())
105}
106
107/// Format commands for help display.
108pub fn format_commands_help(commands: &[CustomCommand]) -> String {
109    if commands.is_empty() {
110        return "No custom commands. Create .battlecommand/commands/<name>.md to add one."
111            .to_string();
112    }
113    let mut output = String::from("Custom Commands:\n");
114    for cmd in commands {
115        output.push_str(&format!(
116            "  /{:<15} {}\n",
117            cmd.name,
118            if cmd.description.is_empty() {
119                "(no description)"
120            } else {
121                &cmd.description
122            }
123        ));
124    }
125    output
126}