Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ User-visible changes to `voltage`, newest first. Each pull request that changes

## Unreleased

- `organizations list`, `environments list`, and `profiles list` accept `--all` and ignore it, like the other lists that return every item at once. Before, they rejected it.
- `ctutils` 0.4.5, whose constant-time comparisons no longer branch on secret values (RUSTSEC-2026-0385). It reaches the CLI through `sha2` and `digest`.
- Filter `payments list` with `--check-only true|false`, and `wallets ledger list` with `--event-type adjustment`, from the refreshed API contract. Omit `--event-type` for payment ledger entries. Filters carry across pagination.
- `webhooks test WEBHOOK_ID` without `--data` sends a test event, with a new `delivery_id` and a payload of type `test` and event `created`. Before, it failed with "This operation requires --data". `--data` still sends your own payload.
Expand Down
2 changes: 2 additions & 0 deletions docs/guides/requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ voltage payments list --profile prod --statuses completed --statuses failed \

`--all` follows pagination by cursor, which the API recommends and the CLI requests by default; endpoints that only page by offset are followed by offset and count. The API has deprecated explicit offset paging (`--offset`, `--pagination offset`) where cursors exist, and the CLI warns when you use it. `--timeout SECONDS` (default 60) bounds the whole request, including paging and waits.

Every `list` command accepts `--all`. A list without `--limit`, such as `wallets list`, `organizations list`, `environments list`, or `profiles list`, returns every item at once, so it ignores `--all`.

Related: [safety model](../concepts/safety-model.md), [output reference](../reference/output.md).
80 changes: 73 additions & 7 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,12 +292,14 @@ pub struct ImportKeyFlags {

#[derive(Debug, Subcommand)]
pub enum ListCommand {
#[command(arg = all_at_once_arg())]
List,
}

#[derive(Debug, Subcommand, Eq, PartialEq)]
pub enum ProfileCommand {
/// Show every saved profile
#[command(arg = all_at_once_arg())]
List,
/// Show one profile
Get { name: String },
Expand Down Expand Up @@ -1027,18 +1029,21 @@ fn list_args(cmd: ClapCommand, operation: &Operation) -> ClapCommand {
if operation.has_parameter("limit") {
PaginationFlags::augment_args(cmd)
} else if operation.lists_everything_at_once() {
cmd.arg(
Arg::new("all_at_once")
.long("all")
.action(ArgAction::SetTrue)
.help("No effect: this list returns every item in one response")
.help_heading("Query"),
)
cmd.arg(all_at_once_arg())
} else {
cmd
}
}

/// `--all` on a list without paging, such as `environments list`: accepted and ignored.
fn all_at_once_arg() -> Arg {
Arg::new("all_at_once")
.long("all")
.action(ArgAction::SetTrue)
.help("No effect: this list shows every item at once")
.help_heading("Query")
}

/// The resource ID argument and `--id`, one of which is required. A wallet command can
/// name its wallet with `--wallet` instead, so `target_id` checks that one was given.
fn target_args(cmd: ClapCommand, target: ResourceTarget, wallet: bool) -> ClapCommand {
Expand Down Expand Up @@ -1556,6 +1561,67 @@ mod tests {
);
}

#[test]
fn all_is_accepted_by_every_list_command() {
fn lists(cmd: &ClapCommand, path: &mut Vec<String>, found: &mut Vec<(String, bool)>) {
path.push(cmd.get_name().into());
if cmd.get_name() == "list" {
let all = cmd.get_arguments().any(|arg| arg.get_long() == Some("all"));
found.push((path.join(" "), all));
}
for sub in cmd.get_subcommands() {
lists(sub, path, found);
}
path.pop();
}
let mut found = Vec::new();
lists(&command(), &mut Vec::new(), &mut found);
assert!(
found
.iter()
.any(|(path, _)| path == "voltage environments list"),
"{found:?}"
);
let missing: Vec<_> = found.iter().filter(|(_, all)| !all).collect();
assert!(missing.is_empty(), "lists without --all: {missing:?}");
}

#[test]
fn all_is_ignored_by_local_lists_and_rejected_by_other_local_commands() {
let parse = |args: &[&str]| {
Invocation::try_parse_from(["voltage"].iter().chain(args)).map(|i| i.command)
};
assert!(matches!(
parse(&["environments", "list", "--all"]),
Ok(Command::Local(LocalCommand::Environments {
command: ListCommand::List
}))
));
assert!(matches!(
parse(&["organizations", "list", "--all"]),
Ok(Command::Local(LocalCommand::Organizations {
command: ListCommand::List
}))
));
assert!(matches!(
parse(&["profiles", "list", "--all"]),
Ok(Command::Local(LocalCommand::Profiles {
command: ProfileCommand::List
}))
));
for args in [
&["profiles", "get", "stage", "--all"][..],
&["context", "--all"],
] {
let error = parse(args).unwrap_err();
assert!(
error.message.contains("unexpected argument '--all'"),
"{args:?}: {}",
error.message
);
}
}

#[test]
fn wallet_flag_names_the_wallet_a_wallet_command_acts_on() {
const OTHER: &str = "44444444-4444-4444-8444-444444444444";
Expand Down
Loading