use anyhow::{Context, bail};
use clap::Args;
use inquire::{Confirm, Select};
use owo_colors::OwoColorize;
use tracing::{debug, instrument};
use scrat_core::config;
use scrat_core::ecosystem::Ecosystem;
use scrat_core::init::{self, ConfigFormat, ConfigStyle, InitSelections};
#[derive(Args, Debug, Default)]
pub struct InitArgs {
#[arg(long, value_enum)]
pub format: Option<ConfigFormat>,
#[arg(long, value_enum)]
pub style: Option<ConfigStyle>,
#[arg(long, short = 'y')]
pub yes: bool,
#[arg(long, short = 'o')]
pub output: Option<String>,
}
enum ExistingAction {
Merge,
Overwrite,
Exit,
}
#[instrument(name = "cmd_init", skip_all)]
pub fn cmd_init(args: InitArgs, global_json: bool, cwd: &camino::Utf8Path) -> anyhow::Result<()> {
debug!(
json_output = global_json,
yes = args.yes,
format = ?args.format,
style = ?args.style,
"executing init command"
);
let plan = init::plan_init(cwd);
if let Some(ref existing_path) = plan.existing_config {
if args.yes {
debug!(%existing_path, "overwriting existing config (--yes)");
} else {
match prompt_existing_action(existing_path)? {
ExistingAction::Merge => {
debug!(%existing_path, "merge: loading existing config");
let _existing = config::ConfigLoader::new()
.with_project_search(cwd)
.load()
.ok();
}
ExistingAction::Overwrite => {
debug!(%existing_path, "overwriting existing config");
}
ExistingAction::Exit => {
println!("{}", "Init cancelled.".yellow());
return Ok(());
}
}
}
}
let selections = if args.yes {
InitSelections {
format: args.format.unwrap_or_default(),
style: args.style.unwrap_or_default(),
ecosystem: plan.ecosystem,
release_branch: None,
github_release: true,
draft: true,
}
} else {
prompt_selections(&args, &plan)?
};
let config_content = init::generate_config(&selections);
let ext = match selections.format {
ConfigFormat::Toml => "toml",
ConfigFormat::Yaml => "yaml",
};
let output_path = args.output.as_ref().map_or_else(
|| cwd.join(format!("scrat.{ext}")),
camino::Utf8PathBuf::from,
);
if !args.yes {
println!();
let confirmed = Confirm::new(&format!("Write config to {output_path}?"))
.with_default(true)
.prompt()
.context("confirmation prompt failed")?;
if !confirmed {
println!("{}", "Init cancelled.".yellow());
return Ok(());
}
}
std::fs::write(output_path.as_std_path(), &config_content)
.with_context(|| format!("failed to write {output_path}"))?;
if global_json {
let result = serde_json::json!({
"path": output_path.as_str(),
"format": ext,
"style": format!("{:?}", selections.style).to_lowercase(),
});
println!("{}", serde_json::to_string_pretty(&result)?);
} else {
println!("{} Wrote {}", "\u{2713}".green(), output_path.bold());
println!(
"{} Run {} to verify",
"\u{2192}".dimmed(),
"scrat preflight".cyan()
);
}
Ok(())
}
fn prompt_existing_action(existing_path: &str) -> anyhow::Result<ExistingAction> {
println!(
"\n{} {}",
"Found existing config at".yellow().bold(),
existing_path.bold(),
);
let options = vec![
"Merge/update (use existing values as defaults)".to_string(),
"Overwrite (start fresh)".to_string(),
"Exit".to_string(),
];
let selection = Select::new("What do you want to do?", options)
.with_starting_cursor(0)
.prompt()
.context("selection cancelled")?;
match selection.as_str() {
s if s.starts_with("Merge") => Ok(ExistingAction::Merge),
s if s.starts_with("Overwrite") => Ok(ExistingAction::Overwrite),
"Exit" => Ok(ExistingAction::Exit),
_ => bail!("unexpected selection: {selection}"),
}
}
fn prompt_selections(args: &InitArgs, plan: &init::InitPlan) -> anyhow::Result<InitSelections> {
let format = if let Some(f) = args.format {
f
} else {
prompt_format()?
};
let style = if let Some(s) = args.style {
s
} else {
prompt_style()?
};
let ecosystem = prompt_ecosystem(plan)?;
let (github_release, draft) = prompt_github_release()?;
Ok(InitSelections {
format,
style,
ecosystem,
release_branch: None,
github_release,
draft,
})
}
fn prompt_format() -> anyhow::Result<ConfigFormat> {
let options = vec!["TOML (recommended)".to_string(), "YAML".to_string()];
let selection = Select::new("Config format:", options)
.with_starting_cursor(0)
.prompt()
.context("format selection cancelled")?;
if selection.starts_with("TOML") {
Ok(ConfigFormat::Toml)
} else {
Ok(ConfigFormat::Yaml)
}
}
fn prompt_style() -> anyhow::Result<ConfigStyle> {
let options = vec![
"Documented \u{2014} all options with comments (recommended)".to_string(),
"Minimal \u{2014} only active values".to_string(),
];
let selection = Select::new("Config style:", options)
.with_starting_cursor(0)
.prompt()
.context("style selection cancelled")?;
if selection.starts_with("Documented") {
Ok(ConfigStyle::Documented)
} else {
Ok(ConfigStyle::Minimal)
}
}
fn prompt_ecosystem(plan: &init::InitPlan) -> anyhow::Result<Option<Ecosystem>> {
if let Some(detected) = plan.ecosystem {
let confirmed = Confirm::new(&format!("Detected ecosystem: {detected}. Correct?"))
.with_default(true)
.prompt()
.context("ecosystem confirmation cancelled")?;
if confirmed {
return Ok(Some(detected));
}
let eco = super::prompt_ecosystem_selection()?;
return Ok(Some(eco));
}
let options = vec![
"Generic (no ecosystem-specific behavior)".to_string(),
"Rust".to_string(),
"Node".to_string(),
"Go".to_string(),
"PHP".to_string(),
"Python".to_string(),
"Ruby".to_string(),
"Swift".to_string(),
"Skip (omit from config)".to_string(),
];
let selection = Select::new("Project ecosystem:", options)
.with_starting_cursor(0)
.prompt()
.context("ecosystem selection cancelled")?;
match selection.as_str() {
s if s.starts_with("Generic") => Ok(Some(Ecosystem::Generic)),
"Rust" => Ok(Some(Ecosystem::Rust)),
"Node" => Ok(Some(Ecosystem::Node)),
"Go" => Ok(Some(Ecosystem::Go)),
"PHP" => Ok(Some(Ecosystem::Php)),
"Python" => Ok(Some(Ecosystem::Python)),
"Ruby" => Ok(Some(Ecosystem::Ruby)),
"Swift" => Ok(Some(Ecosystem::Swift)),
s if s.starts_with("Skip") => Ok(None),
_ => bail!("unexpected selection: {selection}"),
}
}
fn prompt_github_release() -> anyhow::Result<(bool, bool)> {
let options = vec![
"Yes, as drafts (recommended)".to_string(),
"Yes, published immediately".to_string(),
"No".to_string(),
];
let selection = Select::new("Create GitHub releases?", options)
.with_starting_cursor(0)
.prompt()
.context("GitHub release selection cancelled")?;
match selection.as_str() {
s if s.starts_with("Yes, as drafts") => Ok((true, true)),
s if s.starts_with("Yes, published") => Ok((true, false)),
"No" => Ok((false, false)),
_ => bail!("unexpected selection: {selection}"),
}
}