use std::io::{Cursor, Write};
use std::path::PathBuf;
use anyhow::Result;
use clap::{Args, Subcommand, ValueHint};
use schemars::JsonSchema;
use serde::Serialize;
use crate::cli::{FuzzyMode, GlobalArgs};
use crate::cli_args::{BackupOpts, EditLoopArgs, HashArgs, ReplaceArgs, SearchArgs};
use crate::error::AtomwriteError;
use crate::output::NdjsonWriter;
use crate::signal::ShutdownSignal;
#[derive(Args, Debug)]
pub struct RecipeArgs {
#[command(subcommand)]
pub action: RecipeAction,
}
#[derive(Subcommand, Debug)]
pub enum RecipeAction {
Run(Box<RecipeRunArgs>),
List,
}
#[derive(Args, Debug)]
pub struct RecipeRunArgs {
#[arg(long, value_name = "NAME")]
pub name: Option<String>,
#[arg(value_name = "NAME")]
pub name_positional: Option<String>,
#[arg(long, action = clap::ArgAction::SetTrue)]
pub dry_run: bool,
#[arg(long, default_value = ".", value_hint = ValueHint::AnyPath)]
pub path: PathBuf,
#[arg(long)]
pub pattern: Option<String>,
#[arg(long)]
pub replacement: Option<String>,
#[arg(long, value_enum, default_value_t = FuzzyMode::Auto)]
pub fuzzy: FuzzyMode,
#[arg(long)]
pub fuzzy_threshold: Option<f64>,
#[arg(long, action = clap::ArgAction::Append)]
pub include: Vec<String>,
#[arg(long, action = clap::ArgAction::Append)]
pub exclude: Vec<String>,
#[arg(long, value_hint = ValueHint::FilePath)]
pub pairs_file: Option<PathBuf>,
#[arg(long, value_hint = ValueHint::AnyPath)]
pub target: Option<PathBuf>,
#[arg(long)]
pub syntax_check: Option<String>,
}
#[derive(Serialize, JsonSchema, Clone, Debug)]
pub struct RecipeStepResult {
pub step_id: u64,
pub name: String,
pub status: String,
pub detail: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub checksum: Option<String>,
}
#[derive(Serialize, JsonSchema, Debug)]
pub struct RecipeResult {
pub r#type: &'static str,
pub recipe: String,
pub dry_run: bool,
pub steps: Vec<RecipeStepResult>,
#[serde(skip_serializing_if = "Option::is_none")]
pub failed_step_id: Option<u64>,
pub elapsed_ms: u64,
}
#[tracing::instrument(skip_all, fields(command = "recipe"))]
pub fn cmd_recipe(
args: &RecipeArgs,
global: &GlobalArgs,
writer: &mut NdjsonWriter<impl Write>,
shutdown: &ShutdownSignal,
defaults: &crate::config::DefaultsSection,
fuzzy_cfg: &crate::config::FuzzySection,
) -> Result<()> {
match &args.action {
RecipeAction::List => {
for name in ["search-replace-verify", "edit-loop-syntax-check"] {
writer.write_event(&crate::ndjson_types::RecipeListEvent {
r#type: "recipe",
name,
builtin: true,
})?;
}
Ok(())
}
RecipeAction::Run(a) => {
run_recipe(a.as_ref(), global, writer, shutdown, defaults, fuzzy_cfg)
}
}
}
fn run_recipe(
args: &RecipeRunArgs,
global: &GlobalArgs,
writer: &mut NdjsonWriter<impl Write>,
shutdown: &ShutdownSignal,
defaults: &crate::config::DefaultsSection,
fuzzy_cfg: &crate::config::FuzzySection,
) -> Result<()> {
let start = std::time::Instant::now();
let raw_name = args
.name
.as_deref()
.or(args.name_positional.as_deref())
.ok_or_else(|| AtomwriteError::InvalidInput {
reason: "recipe run requires --name NAME or positional NAME".into(),
})?;
let name = raw_name
.trim_end_matches(".yaml")
.trim_end_matches(".yml")
.rsplit('/')
.next()
.unwrap_or(raw_name);
match name {
"search-replace-verify" => {
run_search_replace_verify(args, global, writer, shutdown, defaults, fuzzy_cfg, start)
}
"edit-loop-syntax-check" => {
run_edit_loop_syntax_check(args, global, writer, shutdown, defaults, fuzzy_cfg, start)
}
other => Err(AtomwriteError::InvalidInput {
reason: format!("unknown recipe: {other}"),
}
.into()),
}
}
include!("recipe_run.inc.rs");