use anyhow::{Context, Result};
use clap::Args;
use crate::{commands::init as init_cmd, config};
fn is_tty() -> bool {
atty::is(atty::Stream::Stdin) && atty::is(atty::Stream::Stdout)
}
fn resolve_interactive_mode(
explicit_interactive: bool,
explicit_non_interactive: bool,
) -> Result<bool> {
match (explicit_interactive, explicit_non_interactive) {
(true, _) => {
if is_tty() {
Ok(true)
} else {
anyhow::bail!(
"Interactive mode requested (--interactive) but stdin/stdout is not a TTY. \
Use --non-interactive for CI/piped environments."
)
}
}
(_, true) => {
Ok(false)
}
(false, false) => {
Ok(is_tty())
}
}
}
pub fn handle_init(args: InitArgs, force_lock: bool) -> Result<()> {
let resolved = if args.check {
config::resolve_from_cwd()?
} else if args.trust_project_commands {
config::resolve_from_cwd_skipping_project_execution_trust()?
} else {
config::resolve_from_cwd()?
};
if args.check {
let check_result = init_cmd::check_readme_current(&resolved)?;
match check_result {
init_cmd::ReadmeCheckResult::Current(version) => {
log::info!("readme: current (version {})", version);
return Ok(());
}
init_cmd::ReadmeCheckResult::Outdated {
current_version,
embedded_version,
} => {
log::warn!(
"readme: outdated (current version {}, embedded version {})",
current_version,
embedded_version
);
log::info!("Run 'ralph init --update-readme' to update");
std::process::exit(1);
}
init_cmd::ReadmeCheckResult::Missing => {
log::warn!("readme: missing (would be created on normal init)");
std::process::exit(1);
}
init_cmd::ReadmeCheckResult::NotApplicable => {
log::info!("readme: not applicable (prompts don't reference README)");
return Ok(());
}
}
}
let interactive = resolve_interactive_mode(args.interactive, args.non_interactive)
.with_context(|| {
"Failed to determine interactive mode. \
Use --non-interactive for CI/piped environments."
})?;
let report = init_cmd::run_init(
&resolved,
init_cmd::InitOptions {
force: args.force,
force_lock,
interactive,
update_readme: args.update_readme,
},
)?;
if args.trust_project_commands {
config::initialize_repo_trust_file(&resolved.repo_root)?;
}
fn report_status(label: &str, status: init_cmd::FileInitStatus, path: &std::path::Path) {
match status {
init_cmd::FileInitStatus::Created => {
log::info!("{}: created ({})", label, path.display())
}
init_cmd::FileInitStatus::Valid => {
log::info!("{}: exists (valid) ({})", label, path.display())
}
init_cmd::FileInitStatus::Updated => {
log::info!("{}: updated ({})", label, path.display())
}
}
}
report_status("queue", report.queue_status, &report.queue_path);
report_status("done", report.done_status, &report.done_path);
if let Some((status, version_info)) = report.readme_status {
let readme_path = resolved.repo_root.join(".ralph/README.md");
match status {
init_cmd::FileInitStatus::Created => {
if let Some(version) = version_info {
log::info!(
"readme: created (version {}) ({})",
version,
readme_path.display()
);
} else {
log::info!("readme: created ({})", readme_path.display());
}
}
init_cmd::FileInitStatus::Valid => {
if let Some(version) = version_info {
log::info!(
"readme: exists (version {}) ({})",
version,
readme_path.display()
);
} else {
log::info!("readme: exists (valid) ({})", readme_path.display());
}
}
init_cmd::FileInitStatus::Updated => {
if let Some(version) = version_info {
log::info!(
"readme: updated (version {}) ({})",
version,
readme_path.display()
);
} else {
log::info!("readme: updated ({})", readme_path.display());
}
}
}
}
report_status("config", report.config_status, &report.config_path);
Ok(())
}
#[derive(Args)]
#[command(
about = "Bootstrap Ralph files in the current repository",
after_long_help = "Examples:\n ralph init\n ralph init --force\n ralph init --interactive\n ralph init --non-interactive\n ralph init --trust-project-commands\n ralph init --update-readme\n ralph init --check"
)]
pub struct InitArgs {
#[arg(long)]
pub force: bool,
#[arg(short, long)]
pub interactive: bool,
#[arg(long, conflicts_with = "interactive")]
pub non_interactive: bool,
#[arg(long)]
pub update_readme: bool,
#[arg(long)]
pub check: bool,
#[arg(long = "trust-project-commands", visible_alias = "trust")]
pub trust_project_commands: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_interactive_mode_explicit_non_interactive() {
let result = resolve_interactive_mode(false, true);
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[test]
fn resolve_interactive_mode_explicit_interactive_without_tty() {
let result = resolve_interactive_mode(true, false);
if !is_tty() {
assert!(result.is_err());
} else {
assert!(result.is_ok());
assert!(result.unwrap());
}
}
#[test]
fn resolve_interactive_mode_auto_detect() {
let result = resolve_interactive_mode(false, false);
assert!(result.is_ok());
assert_eq!(result.unwrap(), is_tty());
}
#[test]
fn resolve_interactive_mode_explicit_interactive_wins_over_non_interactive() {
let result = resolve_interactive_mode(true, true);
if !is_tty() {
assert!(result.is_err());
} else {
assert!(result.is_ok());
assert!(result.unwrap());
}
}
}