use std::io::Write;
use anyhow::{Context, Result};
use crate::cli::desired::confirmed;
use crate::cli::output;
const PROMPT_PATH_LIMIT: usize = 3;
pub(crate) fn plan_has_changes(plan: &suno_core::Plan) -> bool {
plan.downloads()
+ plan.reformats()
+ plan.retags()
+ plan.renames()
+ plan.artifact_moves()
+ plan.stem_moves()
+ plan.deletes()
+ plan.artifact_writes()
+ plan.artifact_deletes()
+ plan.stem_writes()
+ plan.stem_deletes()
> 0
}
fn deletion_paths(plan: &suno_core::Plan) -> Vec<String> {
plan.actions
.iter()
.filter_map(|action| match action {
suno_core::Action::Delete { path, .. }
| suno_core::Action::DeleteArtifact { path, .. }
| suno_core::Action::DeleteStem { path, .. } => Some(path.clone()),
_ => None,
})
.collect()
}
pub(crate) fn prompt_delete(plan: &suno_core::Plan, verbosity: i8) -> Result<bool> {
let paths = deletion_paths(plan);
let show = if verbosity >= 1 {
paths.len()
} else {
PROMPT_PATH_LIMIT
};
eprint!("{} [y/N] ", output::delete_prompt(&paths, show));
std::io::stderr().flush().ok();
let mut answer = String::new();
std::io::stdin()
.read_line(&mut answer)
.context("could not read confirmation")?;
Ok(confirmed(&answer))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::desired::{Confirm, confirm_decision};
#[test]
fn artifact_only_deletes_drive_the_confirmation_gate() {
use suno_core::{Action, ArtifactKind, Plan};
let plan = Plan {
actions: (0..3)
.map(|i| Action::DeleteArtifact {
kind: ArtifactKind::CoverJpg,
path: format!("c{i}/cover.jpg"),
owner_id: format!("c{i}"),
})
.collect(),
};
let delete_count = plan.deletes() + plan.artifact_deletes();
assert_eq!(plan.deletes(), 0);
assert_eq!(delete_count, 3);
assert_eq!(
confirm_decision(true, delete_count, false, true),
Confirm::Prompt
);
assert_eq!(
confirm_decision(true, delete_count, false, false),
Confirm::RefuseNonInteractive
);
assert_eq!(
confirm_decision(true, delete_count, true, false),
Confirm::Proceed
);
assert_eq!(
deletion_paths(&plan),
vec!["c0/cover.jpg", "c1/cover.jpg", "c2/cover.jpg"]
);
}
#[test]
fn deletion_paths_lists_both_audio_and_sidecar_removals() {
use suno_core::{Action, ArtifactKind, Plan};
let plan = Plan {
actions: vec![
Action::Delete {
path: "a.flac".to_owned(),
clip_id: "a".to_owned(),
},
Action::DeleteArtifact {
kind: ArtifactKind::CoverJpg,
path: "a/cover.jpg".to_owned(),
owner_id: "a".to_owned(),
},
Action::Skip {
clip_id: "z".to_owned(),
},
],
};
assert_eq!(deletion_paths(&plan), vec!["a.flac", "a/cover.jpg"]);
}
}