diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index e197e0238..6f21219ee 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -1283,6 +1283,10 @@ Remove an identity - `` — Identity to remove +###### **Options:** + +- `--force` — Skip confirmation prompt + ###### **Options (Global):** - `--global` — ⚠️ Deprecated: global config is always on diff --git a/cmd/crates/soroban-test/tests/it/integration/keys.rs b/cmd/crates/soroban-test/tests/it/integration/keys.rs index 311a60bc2..604782731 100644 --- a/cmd/crates/soroban-test/tests/it/integration/keys.rs +++ b/cmd/crates/soroban-test/tests/it/integration/keys.rs @@ -210,3 +210,115 @@ async fn unset_default_identity() { .stdout(predicate::str::contains("STELLAR_ACCOUNT=").not()) .success(); } + +#[tokio::test] +async fn rm_requires_confirmation() { + let sandbox = &TestEnv::new(); + sandbox + .new_assert_cmd("keys") + .arg("generate") + .arg("rmtest1") + .assert() + .success(); + + // Piping "n" should cancel removal + sandbox + .new_assert_cmd("keys") + .arg("rm") + .arg("rmtest1") + .write_stdin("n\n") + .assert() + .stderr(predicate::str::contains("removal cancelled by user")) + .failure(); + + sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("rmtest1") + .assert() + .success(); + + // Piping empty input (just Enter) should default to cancel + sandbox + .new_assert_cmd("keys") + .arg("rm") + .arg("rmtest1") + .write_stdin("\n") + .assert() + .stderr(predicate::str::contains("removal cancelled by user")) + .failure(); + + sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("rmtest1") + .assert() + .success(); + + // Piping "y" should confirm removal + sandbox + .new_assert_cmd("keys") + .arg("rm") + .arg("rmtest1") + .write_stdin("y\n") + .assert() + .stderr(predicate::str::contains( + "Removing the key's cli config file", + )) + .success(); + + sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("rmtest1") + .assert() + .failure(); +} + +#[tokio::test] +async fn rm_with_force_skips_confirmation() { + let sandbox = &TestEnv::new(); + sandbox + .new_assert_cmd("keys") + .arg("generate") + .arg("rmtest2") + .assert() + .success(); + + sandbox + .new_assert_cmd("keys") + .arg("rm") + .arg("rmtest2") + .arg("--force") + .assert() + .success(); + + sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("rmtest2") + .assert() + .failure(); +} + +#[tokio::test] +async fn rm_nonexistent_key() { + let sandbox = &TestEnv::new(); + + // Without --force: should fail before prompting + sandbox + .new_assert_cmd("keys") + .arg("rm") + .arg("doesnotexist") + .assert() + .failure(); + + // With --force: should still fail + sandbox + .new_assert_cmd("keys") + .arg("rm") + .arg("doesnotexist") + .arg("--force") + .assert() + .failure(); +} diff --git a/cmd/soroban-cli/src/commands/keys/rm.rs b/cmd/soroban-cli/src/commands/keys/rm.rs index c0ba97909..9cd510882 100644 --- a/cmd/soroban-cli/src/commands/keys/rm.rs +++ b/cmd/soroban-cli/src/commands/keys/rm.rs @@ -1,5 +1,8 @@ +use std::io::{self, BufRead, IsTerminal}; + use crate::commands::global; use crate::config::address::KeyName; +use crate::print::Print; use super::super::config::locator; @@ -7,6 +10,10 @@ use super::super::config::locator; pub enum Error { #[error(transparent)] Locator(#[from] locator::Error), + #[error("removal cancelled by user")] + Cancelled, + #[error(transparent)] + Io(#[from] io::Error), } #[derive(Debug, clap::Parser, Clone)] @@ -15,12 +22,36 @@ pub struct Cmd { /// Identity to remove pub name: KeyName, + /// Skip confirmation prompt + #[arg(long)] + pub force: bool, + #[command(flatten)] pub config: locator::Args, } impl Cmd { pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { + if !self.force { + let print = Print::new(global_args.quiet); + let stdin = io::stdin(); + + // Check that the key exists before asking for confirmation + self.config.read_identity(&self.name)?; + + // Show the prompt only when the user can see it + if stdin.is_terminal() { + print.warnln(format!( + "Are you sure you want to remove the key '{}'? This action cannot be undone. (y/N)", + self.name + )); + } + let mut response = String::new(); + stdin.lock().read_line(&mut response)?; + if !response.trim().eq_ignore_ascii_case("y") { + return Err(Error::Cancelled); + } + } Ok(self.config.remove_identity(&self.name, global_args)?) } } diff --git a/cookbook/stellar-keys.mdx b/cookbook/stellar-keys.mdx index bccf61bef..1356f12c4 100644 --- a/cookbook/stellar-keys.mdx +++ b/cookbook/stellar-keys.mdx @@ -112,7 +112,7 @@ You can also fund the account while creating the key by using `stellar keys gene When you no longer need this identity, remove it using: ```bash -stellar keys rm carol +stellar keys rm carol --force ``` Output: