Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions FULL_HELP_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1283,6 +1283,10 @@ Remove an identity

- `<NAME>` — Identity to remove

###### **Options:**

- `--force` — Skip confirmation prompt

###### **Options (Global):**

- `--global` — ⚠️ Deprecated: global config is always on
Expand Down
112 changes: 112 additions & 0 deletions cmd/crates/soroban-test/tests/it/integration/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
31 changes: 31 additions & 0 deletions cmd/soroban-cli/src/commands/keys/rm.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
use std::io::{self, BufRead, IsTerminal};

use crate::commands::global;
use crate::config::address::KeyName;
use crate::print::Print;

use super::super::config::locator;

#[derive(thiserror::Error, Debug)]
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)]
Expand All @@ -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
Comment thread
mootz12 marked this conversation as resolved.
Outdated
));
Comment thread
mootz12 marked this conversation as resolved.
}
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)?)
}
}
2 changes: 1 addition & 1 deletion cookbook/stellar-keys.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
mootz12 marked this conversation as resolved.
```

Output:
Expand Down
Loading