Skip to content

Commit 9999d0e

Browse files
domenkozarclaude
andcommitted
feat: improve interactive prompt for missing secrets
List all missing secrets upfront with descriptions before prompting, add step counter ([1/3]), show profile and provider in header, and use inquire::Password for consistent masked input. Remove rpassword dependency in favor of inquire which was already used elsewhere. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 943f4bd commit 9999d0e

File tree

5 files changed

+63
-50
lines changed

5 files changed

+63
-50
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Changed
11+
- Improved interactive prompt for missing secrets: lists all missing secrets upfront with descriptions, adds step counter (`[1/3]`), and uses `inquire::Password` for consistent masked input. Removed `rpassword` dependency.
12+
813
## [0.7.0] - 2026-02-08
914

1015
### Added

Cargo.lock

Lines changed: 0 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ toml = "0.9"
1818
thiserror = "2.0"
1919
directories = "6.0"
2020
colored = "3.0"
21-
rpassword = "7.4.0"
2221
dotenvy = "0.15"
2322
serde-envfile = "0.3"
2423
inquire = "0.9"

secretspec/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ toml.workspace = true
2323
thiserror.workspace = true
2424
directories.workspace = true
2525
colored.workspace = true
26-
rpassword.workspace = true
2726
dotenvy.workspace = true
2827
serde-envfile.workspace = true
2928
inquire.workspace = true

secretspec/src/secrets.rs

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use secrecy::{ExposeSecret, SecretString};
99
use std::collections::{HashMap, HashSet};
1010
use std::convert::TryFrom;
1111
use std::env;
12-
use std::io::{self, IsTerminal, Read, Write};
12+
use std::io::{self, IsTerminal, Read};
1313
use std::path::Path;
1414
use std::process::Command;
1515

@@ -552,11 +552,11 @@ impl Secrets {
552552
let value = if let Some(v) = value {
553553
SecretString::new(v.into())
554554
} else if io::stdin().is_terminal() {
555-
// Use rpassword for single-line input (most common case)
556-
// For multiline secrets, users should pipe the content
557-
let secret = rpassword::prompt_password(format!(
558-
"Enter value for {name} (profile: {profile_name}): "
559-
))?;
555+
let secret = inquire::Password::new(&format!(
556+
"Enter value for {name} (profile: {profile_name}):"
557+
))
558+
.without_confirmation()
559+
.prompt()?;
560560
SecretString::new(secret.into())
561561
} else {
562562
// Read from stdin when input is piped
@@ -696,29 +696,61 @@ impl Secrets {
696696
Err(validation_errors) => {
697697
// If we're in interactive mode and have missing required secrets, prompt for them
698698
if interactive && !validation_errors.missing_required.is_empty() {
699-
eprintln!("\nThe following required secrets are missing:");
700-
for secret_name in &validation_errors.missing_required {
699+
if !io::stdin().is_terminal() {
700+
return Err(SecretSpecError::RequiredSecretMissing(
701+
validation_errors.missing_required.join(", "),
702+
));
703+
}
704+
705+
let missing = &validation_errors.missing_required;
706+
let total = missing.len();
707+
let default_backend = self.get_provider(provider_arg.clone())?;
708+
709+
// List all missing secrets upfront
710+
eprintln!(
711+
"\n{} required {} missing in profile {} with provider {}:\n",
712+
total,
713+
if total == 1 {
714+
"secret is"
715+
} else {
716+
"secrets are"
717+
},
718+
profile_display.bold(),
719+
default_backend.name().bold(),
720+
);
721+
for secret_name in missing {
722+
let description = self
723+
.resolve_secret_config(secret_name, Some(&profile_display))
724+
.and_then(|c| c.description)
725+
.unwrap_or_default();
726+
if description.is_empty() {
727+
eprintln!(" {} {}", "-".dimmed(), secret_name.bold());
728+
} else {
729+
eprintln!(
730+
" {} {} - {}",
731+
"-".dimmed(),
732+
secret_name.bold(),
733+
description
734+
);
735+
}
736+
}
737+
eprintln!();
738+
739+
// Prompt for each missing secret
740+
for (i, secret_name) in missing.iter().enumerate() {
701741
if let Some(secret_config) =
702742
self.resolve_secret_config(secret_name, Some(&profile_display))
703743
{
704-
let description = secret_config
705-
.description
706-
.as_deref()
707-
.unwrap_or("No description");
708-
eprintln!("\n{} - {}", secret_name.bold(), description);
709-
let value = if io::stdin().is_terminal() {
710-
print!(
711-
"Enter value for {} (profile: {}): ",
712-
secret_name, profile_display
713-
);
714-
io::stdout().flush()?;
715-
rpassword::read_password()?
716-
} else {
717-
// When stdin is not a terminal, we can't prompt interactively
718-
return Err(SecretSpecError::RequiredSecretMissing(
719-
validation_errors.missing_required.join(", "),
720-
));
721-
};
744+
let prompt_msg =
745+
format!("[{}/{}] Enter value for {}:", i + 1, total, secret_name,);
746+
let mut prompt =
747+
inquire::Password::new(&prompt_msg).without_confirmation();
748+
749+
if let Some(ref desc) = secret_config.description {
750+
prompt = prompt.with_help_message(desc);
751+
}
752+
753+
let value = prompt.prompt()?;
722754

723755
// Get the provider for this specific secret
724756
// Use first provider in list if specified, otherwise use CLI provider or default

0 commit comments

Comments
 (0)