1mod amend;
4mod check;
5mod create_pr;
6mod formatting;
7mod info;
8mod twiddle;
9mod view;
10
11pub use amend::AmendCommand;
12pub use check::CheckCommand;
13pub use create_pr::{CreatePrCommand, PrContent};
14pub use info::InfoCommand;
15pub use twiddle::TwiddleCommand;
16pub use view::ViewCommand;
17
18use anyhow::Result;
19use clap::{Parser, Subcommand};
20
21pub(super) fn read_interactive_line(
27 reader: &mut (dyn std::io::BufRead + Send),
28) -> std::io::Result<Option<String>> {
29 let mut input = String::new();
30 let bytes = reader.read_line(&mut input)?;
31 if bytes == 0 {
32 Ok(None)
33 } else {
34 Ok(Some(input))
35 }
36}
37
38pub(crate) fn parse_beta_header(s: &str) -> Result<(String, String)> {
40 let (k, v) = s
41 .split_once(':')
42 .ok_or_else(|| anyhow::anyhow!("Invalid --beta-header format '{s}'. Expected key:value"))?;
43 Ok((k.to_string(), v.to_string()))
44}
45
46#[derive(Parser)]
48pub struct GitCommand {
49 #[command(subcommand)]
51 pub command: GitSubcommands,
52}
53
54#[derive(Subcommand)]
56pub enum GitSubcommands {
57 Commit(CommitCommand),
59 Branch(BranchCommand),
61}
62
63#[derive(Parser)]
65pub struct CommitCommand {
66 #[command(subcommand)]
68 pub command: CommitSubcommands,
69}
70
71#[derive(Subcommand)]
73pub enum CommitSubcommands {
74 Message(MessageCommand),
76}
77
78#[derive(Parser)]
80pub struct MessageCommand {
81 #[command(subcommand)]
83 pub command: MessageSubcommands,
84}
85
86#[derive(Subcommand)]
88pub enum MessageSubcommands {
89 View(ViewCommand),
91 Amend(AmendCommand),
93 Twiddle(TwiddleCommand),
95 Check(CheckCommand),
97}
98
99#[derive(Parser)]
101pub struct BranchCommand {
102 #[command(subcommand)]
104 pub command: BranchSubcommands,
105}
106
107#[derive(Subcommand)]
109pub enum BranchSubcommands {
110 Info(InfoCommand),
112 Create(CreateCommand),
114}
115
116#[derive(Parser)]
118pub struct CreateCommand {
119 #[command(subcommand)]
121 pub command: CreateSubcommands,
122}
123
124#[derive(Subcommand)]
126pub enum CreateSubcommands {
127 Pr(CreatePrCommand),
129}
130
131impl GitCommand {
132 pub async fn execute(self) -> Result<()> {
134 match self.command {
135 GitSubcommands::Commit(commit_cmd) => commit_cmd.execute().await,
136 GitSubcommands::Branch(branch_cmd) => branch_cmd.execute().await,
137 }
138 }
139}
140
141impl CommitCommand {
142 pub async fn execute(self) -> Result<()> {
144 match self.command {
145 CommitSubcommands::Message(message_cmd) => message_cmd.execute().await,
146 }
147 }
148}
149
150impl MessageCommand {
151 pub async fn execute(self) -> Result<()> {
153 match self.command {
154 MessageSubcommands::View(view_cmd) => view_cmd.execute(),
155 MessageSubcommands::Amend(amend_cmd) => amend_cmd.execute(),
156 MessageSubcommands::Twiddle(twiddle_cmd) => twiddle_cmd.execute().await,
157 MessageSubcommands::Check(check_cmd) => check_cmd.execute().await,
158 }
159 }
160}
161
162impl BranchCommand {
163 pub async fn execute(self) -> Result<()> {
165 match self.command {
166 BranchSubcommands::Info(info_cmd) => info_cmd.execute(),
167 BranchSubcommands::Create(create_cmd) => create_cmd.execute().await,
168 }
169 }
170}
171
172impl CreateCommand {
173 pub async fn execute(self) -> Result<()> {
175 match self.command {
176 CreateSubcommands::Pr(pr_cmd) => pr_cmd.execute().await,
177 }
178 }
179}
180
181#[cfg(test)]
182#[allow(clippy::unwrap_used, clippy::expect_used)]
183mod tests {
184 use super::*;
185 use crate::cli::Cli;
186 use clap::Parser as _ClapParser;
188
189 #[test]
190 fn parse_beta_header_valid() {
191 let (key, value) = parse_beta_header("anthropic-beta:output-128k-2025-02-19").unwrap();
192 assert_eq!(key, "anthropic-beta");
193 assert_eq!(value, "output-128k-2025-02-19");
194 }
195
196 #[test]
197 fn parse_beta_header_multiple_colons() {
198 let (key, value) = parse_beta_header("key:value:with:colons").unwrap();
200 assert_eq!(key, "key");
201 assert_eq!(value, "value:with:colons");
202 }
203
204 #[test]
205 fn parse_beta_header_missing_colon() {
206 let result = parse_beta_header("no-colon-here");
207 assert!(result.is_err());
208 let err_msg = result.unwrap_err().to_string();
209 assert!(err_msg.contains("no-colon-here"));
210 }
211
212 #[test]
213 fn parse_beta_header_empty_value() {
214 let (key, value) = parse_beta_header("key:").unwrap();
215 assert_eq!(key, "key");
216 assert_eq!(value, "");
217 }
218
219 #[test]
220 fn parse_beta_header_empty_key() {
221 let (key, value) = parse_beta_header(":value").unwrap();
222 assert_eq!(key, "");
223 assert_eq!(value, "value");
224 }
225
226 #[test]
227 fn cli_parses_git_commit_message_view() {
228 let cli = Cli::try_parse_from([
229 "omni-dev",
230 "git",
231 "commit",
232 "message",
233 "view",
234 "HEAD~3..HEAD",
235 ]);
236 assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
237 }
238
239 #[test]
240 fn cli_parses_git_commit_message_amend() {
241 let cli = Cli::try_parse_from([
242 "omni-dev",
243 "git",
244 "commit",
245 "message",
246 "amend",
247 "amendments.yaml",
248 ]);
249 assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
250 }
251
252 #[test]
253 fn cli_parses_git_branch_info() {
254 let cli = Cli::try_parse_from(["omni-dev", "git", "branch", "info"]);
255 assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
256 }
257
258 #[test]
259 fn cli_parses_git_branch_info_with_base() {
260 let cli = Cli::try_parse_from(["omni-dev", "git", "branch", "info", "develop"]);
261 assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
262 }
263
264 #[test]
265 fn cli_parses_config_models_show() {
266 let cli = Cli::try_parse_from(["omni-dev", "config", "models", "show"]);
267 assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
268 }
269
270 #[test]
271 fn cli_parses_help_all() {
272 let cli = Cli::try_parse_from(["omni-dev", "help-all"]);
273 assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
274 }
275
276 #[test]
277 fn cli_rejects_unknown_command() {
278 let cli = Cli::try_parse_from(["omni-dev", "nonexistent"]);
279 assert!(cli.is_err());
280 }
281
282 #[test]
283 fn cli_parses_twiddle_with_options() {
284 let cli = Cli::try_parse_from([
285 "omni-dev",
286 "git",
287 "commit",
288 "message",
289 "twiddle",
290 "--auto-apply",
291 "--no-context",
292 "--concurrency",
293 "8",
294 ]);
295 assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
296 }
297
298 #[test]
299 fn cli_parses_check_with_options() {
300 let cli = Cli::try_parse_from([
301 "omni-dev", "git", "commit", "message", "check", "--strict", "--quiet", "--format",
302 "json",
303 ]);
304 assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
305 }
306
307 #[test]
308 fn cli_parses_commands_generate_all() {
309 let cli = Cli::try_parse_from(["omni-dev", "commands", "generate", "all"]);
310 assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
311 }
312
313 #[test]
314 fn cli_parses_ai_chat() {
315 let cli = Cli::try_parse_from(["omni-dev", "ai", "chat"]);
316 assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
317 }
318
319 #[test]
320 fn cli_parses_ai_chat_with_model() {
321 let cli = Cli::try_parse_from(["omni-dev", "ai", "chat", "--model", "claude-sonnet-4"]);
322 assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
323 }
324
325 #[test]
326 fn read_interactive_line_returns_input() {
327 let mut reader = std::io::Cursor::new(b"hello\n" as &[u8]);
328 let result = read_interactive_line(&mut reader).unwrap();
329 assert_eq!(result, Some("hello\n".to_string()));
330 }
331
332 #[test]
333 fn read_interactive_line_eof_returns_none() {
334 let mut reader = std::io::Cursor::new(b"" as &[u8]);
335 let result = read_interactive_line(&mut reader).unwrap();
336 assert_eq!(result, None);
337 }
338
339 #[test]
340 fn read_interactive_line_empty_line() {
341 let mut reader = std::io::Cursor::new(b"\n" as &[u8]);
342 let result = read_interactive_line(&mut reader).unwrap();
343 assert_eq!(result, Some("\n".to_string()));
344 }
345}