use camino::Utf8PathBuf;
use clap::builder::styling::{AnsiColor, Effects, Styles};
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
use clap_complete::Shell;
use crate::cmd;
use crate::error::Result;
use crate::manifest::{AgentKind, AiMode};
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum AiBackendArg {
Auto,
Claude,
Gemini,
Codex,
Off,
}
impl AiBackendArg {
pub fn into_runner_inputs(self) -> (AgentKind, bool) {
match self {
AiBackendArg::Auto => (AgentKind::Auto, false),
AiBackendArg::Claude => (AgentKind::Claude, false),
AiBackendArg::Gemini => (AgentKind::Gemini, false),
AiBackendArg::Codex => (AgentKind::Codex, false),
AiBackendArg::Off => (AgentKind::Auto, true),
}
}
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum AiModeArg {
Chat,
Handoff,
}
impl From<AiModeArg> for AiMode {
fn from(a: AiModeArg) -> Self {
match a {
AiModeArg::Chat => AiMode::Chat,
AiModeArg::Handoff => AiMode::Handoff,
}
}
}
const HELP_STYLES: Styles = Styles::styled()
.header(AnsiColor::BrightCyan.on_default().effects(Effects::BOLD))
.usage(AnsiColor::BrightCyan.on_default().effects(Effects::BOLD))
.literal(AnsiColor::Magenta.on_default().effects(Effects::BOLD))
.placeholder(AnsiColor::Cyan.on_default())
.error(AnsiColor::Red.on_default().effects(Effects::BOLD))
.valid(AnsiColor::Green.on_default())
.invalid(AnsiColor::Yellow.on_default().effects(Effects::BOLD));
#[derive(Parser, Debug)]
#[command(version, about, long_about = None, styles = HELP_STYLES)]
pub struct Cli {
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
pub verbose: u8,
#[arg(long, global = true)]
pub no_color: bool,
#[arg(long, global = true)]
pub non_interactive: bool,
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand, Debug)]
pub enum Command {
Init {
preset: String,
#[arg(long, value_name = "DIR")]
at: Option<Utf8PathBuf>,
#[arg(long = "var", value_name = "NAME=VAL")]
vars: Vec<String>,
#[arg(long, value_enum, default_value_t = AiBackendArg::Auto)]
ai: AiBackendArg,
#[arg(long, conflicts_with = "ai")]
no_ai: bool,
#[arg(long)]
yes: bool,
#[arg(long = "ai-prompt", value_name = "MSG")]
ai_prompt: Option<String>,
#[arg(long = "ai-mode", value_enum, value_name = "MODE")]
ai_mode: Option<AiModeArg>,
#[arg(long = "ai-concurrency", value_name = "N")]
ai_concurrency: Option<usize>,
},
Apply {
#[arg(long, value_name = "DIR")]
at: Option<Utf8PathBuf>,
#[arg(long)]
dry_run: bool,
#[arg(long = "var", value_name = "NAME=VAL")]
vars: Vec<String>,
#[arg(long, value_enum, default_value_t = AiBackendArg::Auto)]
ai: AiBackendArg,
#[arg(long, conflicts_with = "ai")]
no_ai: bool,
#[arg(long)]
yes: bool,
#[arg(long = "ai-prompt", value_name = "MSG")]
ai_prompt: Option<String>,
#[arg(long = "ai-mode", value_enum, value_name = "MODE")]
ai_mode: Option<AiModeArg>,
#[arg(long = "ai-concurrency", value_name = "N")]
ai_concurrency: Option<usize>,
},
Status {
#[arg(long, value_name = "DIR")]
at: Option<Utf8PathBuf>,
},
Add {
template: String,
#[arg(long)]
rev: Option<String>,
#[arg(long, value_name = "DIR")]
at: Option<Utf8PathBuf>,
#[arg(long = "var", value_name = "NAME=VAL")]
vars: Vec<String>,
#[arg(long, value_enum, default_value_t = AiBackendArg::Auto)]
ai: AiBackendArg,
#[arg(long, conflicts_with = "ai")]
no_ai: bool,
#[arg(long)]
yes: bool,
#[arg(long = "ai-prompt", value_name = "MSG")]
ai_prompt: Option<String>,
#[arg(long = "ai-mode", value_enum, value_name = "MODE")]
ai_mode: Option<AiModeArg>,
#[arg(long = "ai-concurrency", value_name = "N")]
ai_concurrency: Option<usize>,
},
Remove {
template: String,
#[arg(long, value_name = "DIR")]
at: Option<Utf8PathBuf>,
},
Update {
templates: Vec<String>,
#[arg(long)]
rev: Option<String>,
#[arg(long, value_name = "DIR")]
at: Option<Utf8PathBuf>,
},
List {
#[arg(long, value_name = "DIR")]
at: Option<Utf8PathBuf>,
},
Doctor,
Completion {
shell: Shell,
},
}
fn resolve_ai_inputs(ai: AiBackendArg, no_ai: bool) -> (AgentKind, bool) {
let (kind, off) = ai.into_runner_inputs();
(kind, off || no_ai)
}
impl Cli {
pub async fn run(self) -> Result<()> {
let interactive = !self.non_interactive;
let no_color = self.no_color;
match self.command {
Command::Init {
preset,
at,
vars,
ai,
no_ai,
yes,
ai_prompt,
ai_mode,
ai_concurrency,
} => {
let (kind, no_ai) = resolve_ai_inputs(ai, no_ai);
cmd::init::run(
preset,
at,
vars,
kind,
no_ai,
yes,
ai_prompt,
ai_mode.map(Into::into),
ai_concurrency,
interactive,
no_color,
)
.await
}
Command::Apply {
at,
dry_run,
vars,
ai,
no_ai,
yes,
ai_prompt,
ai_mode,
ai_concurrency,
} => {
let (kind, no_ai) = resolve_ai_inputs(ai, no_ai);
cmd::apply::run(
at,
dry_run,
vars,
kind,
no_ai,
yes,
ai_prompt,
ai_mode.map(Into::into),
ai_concurrency,
interactive,
no_color,
)
.await
}
Command::Status { at } => cmd::status::run(at, interactive, no_color).await,
Command::Add {
template,
rev,
at,
vars,
ai,
no_ai,
yes,
ai_prompt,
ai_mode,
ai_concurrency,
} => {
let (kind, no_ai) = resolve_ai_inputs(ai, no_ai);
cmd::add::run(
template,
rev,
at,
vars,
kind,
no_ai,
yes,
ai_prompt,
ai_mode.map(Into::into),
ai_concurrency,
interactive,
no_color,
)
.await
}
Command::Remove { template, at } => cmd::remove::run(template, at, no_color).await,
Command::Update { templates, rev, at } => {
cmd::update::run(templates, rev, at, no_color).await
}
Command::List { at } => cmd::list::run(at, no_color),
Command::Doctor => cmd::doctor::run(no_color),
Command::Completion { shell } => {
let mut c = Cli::command();
clap_complete::generate(shell, &mut c, "kata", &mut std::io::stdout());
Ok(())
}
}
}
}