mod amend;
mod check;
mod create_pr;
pub(crate) mod formatting;
mod info;
mod twiddle;
mod view;
pub use amend::AmendCommand;
pub use check::{run_check, CheckCommand, CheckOutcome};
pub use create_pr::{run_create_pr, CreatePrCommand, CreatePrOutcome, PrContent};
pub use info::{run_info, InfoCommand};
pub use twiddle::{run_twiddle, TwiddleCommand, TwiddleOutcome};
pub use view::{run_view, ViewCommand};
use anyhow::Result;
use clap::{Parser, Subcommand};
pub(crate) static CWD_MUTEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
pub(crate) struct CwdGuard {
original: std::path::PathBuf,
_lock: tokio::sync::MutexGuard<'static, ()>,
}
impl CwdGuard {
pub(crate) async fn enter<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
let lock = CWD_MUTEX.lock().await;
let original =
std::env::current_dir().map_err(|e| anyhow::anyhow!("current_dir failed: {e}"))?;
std::env::set_current_dir(path.as_ref())
.map_err(|e| anyhow::anyhow!("set_current_dir failed: {e}"))?;
Ok(Self {
original,
_lock: lock,
})
}
}
impl Drop for CwdGuard {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.original);
}
}
pub(super) fn read_interactive_line(
reader: &mut (dyn std::io::BufRead + Send),
) -> std::io::Result<Option<String>> {
let mut input = String::new();
let bytes = reader.read_line(&mut input)?;
if bytes == 0 {
Ok(None)
} else {
Ok(Some(input))
}
}
pub(crate) fn parse_beta_header(s: &str) -> Result<(String, String)> {
let (k, v) = s
.split_once(':')
.ok_or_else(|| anyhow::anyhow!("Invalid --beta-header format '{s}'. Expected key:value"))?;
Ok((k.to_string(), v.to_string()))
}
#[derive(Parser)]
pub struct GitCommand {
#[command(subcommand)]
pub command: GitSubcommands,
}
#[derive(Subcommand)]
pub enum GitSubcommands {
Commit(CommitCommand),
Branch(BranchCommand),
}
#[derive(Parser)]
pub struct CommitCommand {
#[command(subcommand)]
pub command: CommitSubcommands,
}
#[derive(Subcommand)]
pub enum CommitSubcommands {
Message(MessageCommand),
}
#[derive(Parser)]
pub struct MessageCommand {
#[command(subcommand)]
pub command: MessageSubcommands,
}
#[derive(Subcommand)]
pub enum MessageSubcommands {
View(ViewCommand),
Amend(AmendCommand),
Twiddle(TwiddleCommand),
Check(CheckCommand),
}
#[derive(Parser)]
pub struct BranchCommand {
#[command(subcommand)]
pub command: BranchSubcommands,
}
#[derive(Subcommand)]
pub enum BranchSubcommands {
Info(InfoCommand),
Create(CreateCommand),
}
#[derive(Parser)]
pub struct CreateCommand {
#[command(subcommand)]
pub command: CreateSubcommands,
}
#[derive(Subcommand)]
pub enum CreateSubcommands {
Pr(CreatePrCommand),
}
impl GitCommand {
pub async fn execute(self) -> Result<()> {
match self.command {
GitSubcommands::Commit(commit_cmd) => commit_cmd.execute().await,
GitSubcommands::Branch(branch_cmd) => branch_cmd.execute().await,
}
}
}
impl CommitCommand {
pub async fn execute(self) -> Result<()> {
match self.command {
CommitSubcommands::Message(message_cmd) => message_cmd.execute().await,
}
}
}
impl MessageCommand {
pub async fn execute(self) -> Result<()> {
match self.command {
MessageSubcommands::View(view_cmd) => view_cmd.execute(),
MessageSubcommands::Amend(amend_cmd) => amend_cmd.execute(),
MessageSubcommands::Twiddle(twiddle_cmd) => twiddle_cmd.execute().await,
MessageSubcommands::Check(check_cmd) => check_cmd.execute().await,
}
}
}
impl BranchCommand {
pub async fn execute(self) -> Result<()> {
match self.command {
BranchSubcommands::Info(info_cmd) => info_cmd.execute(),
BranchSubcommands::Create(create_cmd) => create_cmd.execute().await,
}
}
}
impl CreateCommand {
pub async fn execute(self) -> Result<()> {
match self.command {
CreateSubcommands::Pr(pr_cmd) => pr_cmd.execute().await,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::cli::Cli;
use clap::Parser as _ClapParser;
#[test]
fn parse_beta_header_valid() {
let (key, value) = parse_beta_header("anthropic-beta:output-128k-2025-02-19").unwrap();
assert_eq!(key, "anthropic-beta");
assert_eq!(value, "output-128k-2025-02-19");
}
#[test]
fn parse_beta_header_multiple_colons() {
let (key, value) = parse_beta_header("key:value:with:colons").unwrap();
assert_eq!(key, "key");
assert_eq!(value, "value:with:colons");
}
#[test]
fn parse_beta_header_missing_colon() {
let result = parse_beta_header("no-colon-here");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("no-colon-here"));
}
#[test]
fn parse_beta_header_empty_value() {
let (key, value) = parse_beta_header("key:").unwrap();
assert_eq!(key, "key");
assert_eq!(value, "");
}
#[test]
fn parse_beta_header_empty_key() {
let (key, value) = parse_beta_header(":value").unwrap();
assert_eq!(key, "");
assert_eq!(value, "value");
}
#[test]
fn cli_parses_git_commit_message_view() {
let cli = Cli::try_parse_from([
"omni-dev",
"git",
"commit",
"message",
"view",
"HEAD~3..HEAD",
]);
assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
}
#[test]
fn cli_parses_git_commit_message_amend() {
let cli = Cli::try_parse_from([
"omni-dev",
"git",
"commit",
"message",
"amend",
"amendments.yaml",
]);
assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
}
#[test]
fn cli_parses_git_branch_info() {
let cli = Cli::try_parse_from(["omni-dev", "git", "branch", "info"]);
assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
}
#[test]
fn cli_parses_git_branch_info_with_base() {
let cli = Cli::try_parse_from(["omni-dev", "git", "branch", "info", "develop"]);
assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
}
#[test]
fn cli_parses_config_models_show() {
let cli = Cli::try_parse_from(["omni-dev", "config", "models", "show"]);
assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
}
#[test]
fn cli_parses_help_all() {
let cli = Cli::try_parse_from(["omni-dev", "help-all"]);
assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
}
#[test]
fn cli_rejects_unknown_command() {
let cli = Cli::try_parse_from(["omni-dev", "nonexistent"]);
assert!(cli.is_err());
}
#[test]
fn cli_parses_twiddle_with_options() {
let cli = Cli::try_parse_from([
"omni-dev",
"git",
"commit",
"message",
"twiddle",
"--auto-apply",
"--no-context",
"--concurrency",
"8",
]);
assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
}
#[test]
fn cli_parses_check_with_options() {
let cli = Cli::try_parse_from([
"omni-dev", "git", "commit", "message", "check", "--strict", "--quiet", "--format",
"json",
]);
assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
}
#[test]
fn cli_parses_commands_generate_all() {
let cli = Cli::try_parse_from(["omni-dev", "commands", "generate", "all"]);
assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
}
#[test]
fn cli_parses_ai_chat() {
let cli = Cli::try_parse_from(["omni-dev", "ai", "chat"]);
assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
}
#[test]
fn cli_parses_ai_chat_with_model() {
let cli = Cli::try_parse_from(["omni-dev", "ai", "chat", "--model", "claude-sonnet-4"]);
assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
}
#[test]
fn cli_parses_ai_claude_cli_model_resolve() {
let cli = Cli::try_parse_from(["omni-dev", "ai", "claude", "cli", "model", "resolve"]);
assert!(cli.is_ok(), "Failed to parse: {:?}", cli.err());
}
#[test]
fn read_interactive_line_returns_input() {
let mut reader = std::io::Cursor::new(b"hello\n" as &[u8]);
let result = read_interactive_line(&mut reader).unwrap();
assert_eq!(result, Some("hello\n".to_string()));
}
#[test]
fn read_interactive_line_eof_returns_none() {
let mut reader = std::io::Cursor::new(b"" as &[u8]);
let result = read_interactive_line(&mut reader).unwrap();
assert_eq!(result, None);
}
#[test]
fn read_interactive_line_empty_line() {
let mut reader = std::io::Cursor::new(b"\n" as &[u8]);
let result = read_interactive_line(&mut reader).unwrap();
assert_eq!(result, Some("\n".to_string()));
}
#[tokio::test]
async fn cwd_guard_invalid_path_returns_error() {
let result = CwdGuard::enter("/no/such/path/exists").await;
assert!(result.is_err(), "expected error for nonexistent path");
}
}