Skip to content
Draft
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
26 changes: 26 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
name: Bug report
about: Something in the voltage CLI does not work as documented
labels: bug
---

<!-- Search existing issues first and add to a matching one instead of filing another. `voltage feedback` drafts this report with your version and platform filled in. Agents: check the box below, and give steps a person can run. Replace IDs, invoices, addresses, and keys with placeholders such as WALLET_ID. -->

### What happened

What you expected, and what happened instead.

### Steps to reproduce

The commands to run, with placeholders such as `WALLET_ID` for IDs.

### Environment

- Version: (`voltage --version`)
- Platform:
- Command:

### Checks

- [ ] I searched existing issues: https://github.com/voltagecloud/voltage-cli/issues?q=is%3Aissue
- [ ] An AI agent wrote this report
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ User-visible changes to `voltage`, newest first. Each pull request that changes
- A payment that settles on a terminal closes with `⚡ Payment ID completed` instead of a check mark, printed after the payment's record so it stays on screen.
- `voltage payments list --watch` follows payments until Ctrl-C: a live table that marks new and changed payments on a terminal, or one `snapshot`, `new`, or `updated` event per payment when piped or with `--json`. `--interval` sets the refresh (default 5 seconds).
- `voltage` with no command, on a terminal, greets you with the wordmark, who you are signed in as, your saved scope, and commands to try; off a terminal it is still a usage error. An empty wallet, payment, or webhook list, and a new profile, end with a `›` line naming the next command.
- `voltage feedback` prints a link that opens a new GitHub issue with the CLI version, platform, and the misbehaving command's words and flag names (never their values) filled in, and a link to search existing issues first. On a terminal it opens the link in your browser instead of printing it, unless `--no-browser`; off a terminal or with `--json` it never does, marks the report as agent-written, and returns the title, body, and labels for `gh issue create`. After a panic, the CLI prints the same kind of link with the source line and a scrubbed message.

## 0.2.0 - 2026-10-22

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Every command also has `--help`. [The docs index](docs/README.md) lists everythi

## Contributing

Report security issues as described in [SECURITY.md](SECURITY.md). Build, test, and release instructions are in [the development guide](docs/development.md). Code conventions are in [the style guide](docs/quality.md).
Report security issues as described in [SECURITY.md](SECURITY.md). Report other problems with `voltage feedback`, which drafts a GitHub issue; see [report a problem](docs/guides/troubleshooting.md#report-a-problem). Build, test, and release instructions are in [the development guide](docs/development.md). Code conventions are in [the style guide](docs/quality.md).

## License

Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Start with the [README](../README.md) for install and a quick start, or follow [
- [Requests](guides/requests.md): request bodies, friendly flags, filters, paging
- [Webhooks](guides/webhooks.md): create with a saved secret, events, deliveries, key rotation
- [Checkout](guides/checkout.md): session and stream tokens, events
- [Troubleshooting](guides/troubleshooting.md): what each exit code and error means, and the fix
- [Troubleshooting](guides/troubleshooting.md): what each exit code and error means, and the fix, and how to report a problem
- [Scripting](guides/scripting.md): JSON, `VOLTAGE_EXECUTE`, running without a person

## Look it up
Expand Down
1 change: 1 addition & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ These commands use the auth service (`https://auth.voltage.cloud/api/v1`, overri
| `voltage profiles list` | local only |
| `voltage profiles delete NAME` | local only |
| `voltage completions SHELL` | local only |
| `voltage feedback` | local only |

- Every organization API call first exchanges the saved login for an organization token with `POST /api/v1/oauth/token`, and an expiring login is refreshed there as well.
- A failed login revokes its new session with `POST /api/v1/oauth/revoke` so nothing is left running on the server.
Expand Down
1 change: 1 addition & 0 deletions docs/guides/scripting.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ VOLTAGE_EXECUTE=1 ./pay-invoices.sh
- `-q, --quiet` suppresses optional status and notices, never requested results, errors, warnings, or recovery IDs.
- Diagnostics, prompts, and progress go to stderr, so stdout carries only results.
- Branch on [exit codes](../reference/exit-codes.md). Exit code 4 means the outcome is unknown: check the payment ID before you retry.
- An agent that finds a bug in the CLI can draft an issue with `voltage feedback --json`; see [report a problem](troubleshooting.md#report-a-problem).

Related: [safety model](../concepts/safety-model.md), [output reference](../reference/output.md).
14 changes: 14 additions & 0 deletions docs/guides/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,18 @@ The CLI refuses a USD payment without `--quote` itself, with `A USD payment need

When the detail names a quoted amount, the quote does not convert to exactly the payment's amount. Check what it converts to with `voltage quotes get QUOTE_ID`. To pay a BTC invoice from a USD wallet, quote the invoice's BTC amount with `--unit sats --to usd`. See [quotes and lines of credit](payments.md#quotes-and-lines-of-credit).

## Report a problem

When the CLI does something wrong, `voltage feedback` prints a link that opens a new GitHub issue with the build, the platform, and a form for what happened and how to reproduce it. On a terminal it opens the link in your browser instead of printing it; `--no-browser` prints it. Name the command that misbehaved after `--`: the report keeps its command words and flag names, never their values.

```sh
voltage feedback --title Balance-looks-wrong -- wallets get WALLET_ID --json
```

Search the existing issues first; the command prints that link too, and you can add to a matching issue instead of opening another. Before you submit, replace IDs, invoices, addresses, and keys with placeholders such as `WALLET_ID`.

Off a terminal, or with `--json`, the command never opens a browser and marks the report as written by an AI agent. `--json` returns the issue's `title`, `labels`, `body`, `url`, and `search_url`, so an agent can search with `search_url` and then file it with `gh issue create --title TITLE --body BODY`, or give the `url` to a person. Leave the agent box checked when an agent wrote the report.

If the CLI itself fails unexpectedly (a panic, exit code 101), it prints the same kind of link after the panic message, with the source line and a scrubbed message filled in. Scrubbing removes words that look like IDs, keys, tokens, invoices, addresses, emails, or URLs.

Related: [exit codes](../reference/exit-codes.md), [environment variables](../reference/environment.md), [log in and use profiles](login-and-profiles.md).
3 changes: 2 additions & 1 deletion docs/quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ not precedent. Reviews must approve and document exceptions.
`terminal`; the animated status line in `progress`; a payment wait's status
lines in `timeline`; and the color palette and symbols in `style`. Keep which commands ask for missing scope, and in what
order, in `pick`. Keep the default `--currency` of a payment, read from its
wallet's line of credit, in `wallet_currency`. Keep exit-code categories and
wallet's line of credit, in `wallet_currency`. Keep prefilled GitHub issue reports, their
scrubbing, and the panic hook that prints one in `feedback`. Keep exit-code categories and
typed error detail in `error`.
- Name each module for the operation that it performs. Do not give two modules
the same capability name. For example, keep private-file mechanics in
Expand Down
1 change: 1 addition & 0 deletions scripts/check-coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
('voltage profiles list', []),
('voltage profiles delete NAME', []),
('voltage completions SHELL', []),
('voltage feedback', []),
]
# Commands that use the coinprice service, with the routes they call.
PRICE_COMMANDS = [
Expand Down
117 changes: 117 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ pub enum LocalCommand {
Price(PriceFlags),
/// Convert an amount between BTC and USD at the current or a past price
Convert(ConvertFlags),
/// Print a link that files a GitHub issue with this build and platform filled in
Feedback(FeedbackFlags),
/// Print shell completions
Completions {
#[arg(value_enum)]
Expand All @@ -230,6 +232,30 @@ pub struct PriceFlags {
pub at: Option<String>,
}

#[derive(Debug, Args)]
pub struct FeedbackFlags {
/// Issue title: what went wrong, in one line
#[arg(long)]
pub title: Option<String>,
/// Print the link without opening it
#[arg(long)]
pub no_browser: bool,
/// The command that misbehaved, after `--`; the report keeps only its command words and
/// flag names, never their values
#[arg(last = true, value_name = "COMMAND")]
pub command: Vec<OsString>,
}

impl FeedbackFlags {
/// The misbehaving command as the report shows it, when one was given.
pub fn command_outline(&self) -> Option<String> {
let args: Vec<OsString> = std::iter::once(OsString::from("voltage"))
.chain(self.command.iter().cloned())
.collect();
(!self.command.is_empty()).then(|| outline(&args))
}
}

#[derive(Debug, Args)]
pub struct ConvertFlags {
/// Exact decimal amount
Expand Down Expand Up @@ -776,6 +802,34 @@ where
.any(|arg| arg.as_ref() == OsStr::new("--json"))
}

/// A command line as an issue report shows it: `voltage`, the subcommand words, and each
/// flag's name, without positional values or flag values, which can hold IDs and secrets.
/// It reads the command tree, not a parse, so it works on a line that does not parse.
pub fn outline(args: &[OsString]) -> String {
let root = command();
let mut current = &root;
let mut words = vec!["voltage".to_owned()];
for arg in args.iter().skip(1).map(|arg| arg.to_string_lossy()) {
if arg == "--" {
break;
}
if let Some(long) = arg.strip_prefix("--") {
words.push(format!("--{}", long.split('=').next().unwrap_or_default()));
} else if let Some(short) = arg
.strip_prefix('-')
.and_then(|rest| rest.chars().next())
.filter(char::is_ascii_alphabetic)
{
// `-ojson` keeps only `-o`; `-5` is a value, not a flag.
words.push(format!("-{short}"));
} else if let Some(sub) = current.find_subcommand(arg.as_ref()) {
words.push(sub.get_name().to_owned());
current = sub;
}
}
words.join(" ")
}

fn parse<T: FromArgMatches>(matches: &ArgMatches) -> Result<T> {
T::from_arg_matches(matches).map_err(|error| Error::usage(error.to_string()))
}
Expand Down Expand Up @@ -1316,6 +1370,7 @@ fn guide(path: &[&str]) -> &'static str {
..,
] => "guides/payments.md",
["completions", ..] => "guides/install.md",
["feedback", ..] => "guides/troubleshooting.md",
_ => "README.md",
}
}
Expand Down Expand Up @@ -1346,6 +1401,12 @@ fn local_examples(path: &[&str]) -> Option<&'static str> {
["context"] => {
" # Show what a command with this profile would use, and where each value came from
voltage context --profile prod"
}
["feedback"] => {
" # File an issue about a command; the link keeps its flag names, not their values
voltage feedback --title Balance-looks-wrong -- wallets get WALLET_ID --json
# Print the prefilled issue as JSON, for an agent that files it with `gh issue create`
voltage feedback --json"
}
["convert"] => {
" # What $10 buys in bitcoin now, and 21,000 sats in dollars on a past date
Expand Down Expand Up @@ -1838,6 +1899,62 @@ mod tests {
);
}

#[test]
fn an_outline_keeps_command_words_and_flag_names_but_no_values() {
let args = |words: &[&str]| words.iter().map(OsString::from).collect::<Vec<_>>();
assert_eq!(
outline(&args(&[
"voltage",
"--profile",
"prod",
"wallets",
"get",
"6f1c2a9e-1b2d-4c3e-8f40-0a1b2c3d4e5f",
"--data=@secret.json",
"-ojson",
"-q",
])),
"voltage --profile wallets get --data -o -q"
);
// A negative amount is a value, and nothing after `--` is read.
assert_eq!(
outline(&args(&["voltage", "convert", "-5", "usd", "--", "--to"])),
"voltage convert"
);
// A line that does not parse still outlines.
assert_eq!(
outline(&args(&["voltage", "walets", "--bogus"])),
"voltage --bogus"
);
let feedback = |words: &[&str]| {
let Command::Local(LocalCommand::Feedback(flags)) =
Invocation::try_parse_from(words).unwrap().command
else {
panic!("expected feedback");
};
flags
};
let flags = feedback(&[
"voltage",
"feedback",
"--title",
"Broken",
"--no-browser",
"--",
"payments",
"send",
"lnbc1secret",
"--execute",
]);
assert_eq!(flags.title.as_deref(), Some("Broken"));
assert!(flags.no_browser);
assert_eq!(
flags.command_outline().as_deref(),
Some("voltage payments send --execute")
);
assert_eq!(feedback(&["voltage", "feedback"]).command_outline(), None);
}

#[test]
fn local_commands_parse_into_their_variants() {
let login = Invocation::try_parse_from([
Expand Down
Loading
Loading