Skip to main content

omni_dev/cli/
commands.rs

1//! Command template management.
2
3use std::fs;
4use std::path::Path;
5
6use anyhow::{Context, Result};
7use clap::{Parser, Subcommand};
8
9// Embed the template files as strings
10const COMMIT_TWIDDLE_TEMPLATE: &str = include_str!("../templates/commit-twiddle.md");
11const PR_CREATE_TEMPLATE: &str = include_str!("../templates/pr-create.md");
12const PR_UPDATE_TEMPLATE: &str = include_str!("../templates/pr-update.md");
13
14/// Command template management.
15#[derive(Parser)]
16pub struct CommandsCommand {
17    /// Commands subcommand to execute.
18    #[command(subcommand)]
19    pub command: CommandsSubcommands,
20}
21
22/// Commands subcommands.
23#[derive(Subcommand)]
24pub enum CommandsSubcommands {
25    /// Generates command templates.
26    Generate(GenerateCommand),
27}
28
29/// Generate command options.
30#[derive(Parser)]
31pub struct GenerateCommand {
32    /// Generate subcommand to execute.
33    #[command(subcommand)]
34    pub command: GenerateSubcommands,
35}
36
37/// Generate subcommands.
38#[derive(Subcommand)]
39pub enum GenerateSubcommands {
40    /// Generates commit-twiddle command template.
41    #[command(name = "commit-twiddle")]
42    CommitTwiddle,
43    /// Generates pr-create command template.
44    #[command(name = "pr-create")]
45    PrCreate,
46    /// Generates pr-update command template.
47    #[command(name = "pr-update")]
48    PrUpdate,
49    /// Generates all command templates.
50    All,
51}
52
53impl CommandsCommand {
54    /// Executes the commands command.
55    pub fn execute(self) -> Result<()> {
56        match self.command {
57            CommandsSubcommands::Generate(generate_cmd) => generate_cmd.execute(),
58        }
59    }
60}
61
62impl GenerateCommand {
63    /// Executes the generate command.
64    pub fn execute(self) -> Result<()> {
65        match self.command {
66            GenerateSubcommands::CommitTwiddle => {
67                generate_commit_twiddle()?;
68                println!("✅ Generated .claude/commands/commit-twiddle.md");
69            }
70            GenerateSubcommands::PrCreate => {
71                generate_pr_create()?;
72                println!("✅ Generated .claude/commands/pr-create.md");
73            }
74            GenerateSubcommands::PrUpdate => {
75                generate_pr_update()?;
76                println!("✅ Generated .claude/commands/pr-update.md");
77            }
78            GenerateSubcommands::All => {
79                generate_commit_twiddle()?;
80                generate_pr_create()?;
81                generate_pr_update()?;
82                println!("✅ Generated all command templates:");
83                println!("   - .claude/commands/commit-twiddle.md");
84                println!("   - .claude/commands/pr-create.md");
85                println!("   - .claude/commands/pr-update.md");
86            }
87        }
88        Ok(())
89    }
90}
91
92/// Generates the commit-twiddle command template.
93fn generate_commit_twiddle() -> Result<()> {
94    ensure_claude_commands_dir()?;
95    fs::write(
96        ".claude/commands/commit-twiddle.md",
97        COMMIT_TWIDDLE_TEMPLATE,
98    )
99    .context("Failed to write .claude/commands/commit-twiddle.md")?;
100    Ok(())
101}
102
103/// Generates the pr-create command template.
104fn generate_pr_create() -> Result<()> {
105    ensure_claude_commands_dir()?;
106    fs::write(".claude/commands/pr-create.md", PR_CREATE_TEMPLATE)
107        .context("Failed to write .claude/commands/pr-create.md")?;
108    Ok(())
109}
110
111/// Generates the pr-update command template.
112fn generate_pr_update() -> Result<()> {
113    ensure_claude_commands_dir()?;
114    fs::write(".claude/commands/pr-update.md", PR_UPDATE_TEMPLATE)
115        .context("Failed to write .claude/commands/pr-update.md")?;
116    Ok(())
117}
118
119/// Ensures the .claude/commands directory exists.
120fn ensure_claude_commands_dir() -> Result<()> {
121    let commands_dir = Path::new(".claude/commands");
122    if !commands_dir.exists() {
123        fs::create_dir_all(commands_dir).context("Failed to create .claude/commands directory")?;
124    }
125    Ok(())
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn commit_twiddle_template_has_content() {
134        assert!(COMMIT_TWIDDLE_TEMPLATE.len() > 10);
135    }
136
137    #[test]
138    fn pr_create_template_has_content() {
139        assert!(PR_CREATE_TEMPLATE.len() > 10);
140    }
141
142    #[test]
143    fn pr_update_template_has_content() {
144        assert!(PR_UPDATE_TEMPLATE.len() > 10);
145    }
146
147    #[test]
148    fn templates_contain_expected_content() {
149        // commit-twiddle template should reference commit messages
150        assert!(
151            COMMIT_TWIDDLE_TEMPLATE.contains("commit")
152                || COMMIT_TWIDDLE_TEMPLATE.contains("twiddle")
153        );
154
155        // pr-create template should reference pull request
156        assert!(
157            PR_CREATE_TEMPLATE.contains("pull request")
158                || PR_CREATE_TEMPLATE.contains("PR")
159                || PR_CREATE_TEMPLATE.contains("pr")
160        );
161
162        // pr-update template should reference update
163        assert!(
164            PR_UPDATE_TEMPLATE.contains("update")
165                || PR_UPDATE_TEMPLATE.contains("PR")
166                || PR_UPDATE_TEMPLATE.contains("pr")
167        );
168    }
169
170    #[test]
171    fn templates_are_valid_markdown() {
172        // Templates should be valid markdown — basic check: they contain text
173        // and don't start with binary content
174        assert!(COMMIT_TWIDDLE_TEMPLATE.is_ascii() || COMMIT_TWIDDLE_TEMPLATE.contains('#'));
175        assert!(PR_CREATE_TEMPLATE.is_ascii() || PR_CREATE_TEMPLATE.contains('#'));
176        assert!(PR_UPDATE_TEMPLATE.is_ascii() || PR_UPDATE_TEMPLATE.contains('#'));
177    }
178
179    #[test]
180    fn templates_are_distinct() {
181        // Each template should be unique
182        assert_ne!(COMMIT_TWIDDLE_TEMPLATE, PR_CREATE_TEMPLATE);
183        assert_ne!(COMMIT_TWIDDLE_TEMPLATE, PR_UPDATE_TEMPLATE);
184        assert_ne!(PR_CREATE_TEMPLATE, PR_UPDATE_TEMPLATE);
185    }
186}