1use std::fs;
4use std::path::Path;
5
6use anyhow::{Context, Result};
7use clap::{Parser, Subcommand};
8
9const 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#[derive(Parser)]
16pub struct CommandsCommand {
17 #[command(subcommand)]
19 pub command: CommandsSubcommands,
20}
21
22#[derive(Subcommand)]
24pub enum CommandsSubcommands {
25 Generate(GenerateCommand),
27}
28
29#[derive(Parser)]
31pub struct GenerateCommand {
32 #[command(subcommand)]
34 pub command: GenerateSubcommands,
35}
36
37#[derive(Subcommand)]
39pub enum GenerateSubcommands {
40 #[command(name = "commit-twiddle")]
42 CommitTwiddle,
43 #[command(name = "pr-create")]
45 PrCreate,
46 #[command(name = "pr-update")]
48 PrUpdate,
49 All,
51}
52
53impl CommandsCommand {
54 pub fn execute(self) -> Result<()> {
56 match self.command {
57 CommandsSubcommands::Generate(generate_cmd) => generate_cmd.execute(),
58 }
59 }
60}
61
62impl GenerateCommand {
63 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
92fn 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
103fn 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
111fn 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
119fn 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 assert!(
151 COMMIT_TWIDDLE_TEMPLATE.contains("commit")
152 || COMMIT_TWIDDLE_TEMPLATE.contains("twiddle")
153 );
154
155 assert!(
157 PR_CREATE_TEMPLATE.contains("pull request")
158 || PR_CREATE_TEMPLATE.contains("PR")
159 || PR_CREATE_TEMPLATE.contains("pr")
160 );
161
162 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 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 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}