Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!-- What changed for users, and why. Name the base PR if this is stacked. -->

- [ ] User-visible change: a guide or reference under `docs/` and `CHANGELOG.md` (**Unreleased**) are updated, or the PR has the `no-changelog` label.
- [ ] The verification gate in `docs/development.md` passes.
- [ ] The diff was reviewed against its base with the `voltage-pr-review` skill, and every new error and early-return path has a test.
- [ ] CRAP report, when `src/` or `tests/` changed: the highest score and each function at or above 20.
- [ ] Checks not run, and why:
33 changes: 33 additions & 0 deletions .github/workflows/changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Changelog

on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]

permissions:
contents: read

jobs:
changelog:
name: Changelog updated for CLI changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-changelog') }}
runs-on: ubuntu-24.04
timeout-minutes: 5

steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false

- name: Require a CHANGELOG entry when the command surface changes
env:
BASE: ${{ github.event.pull_request.base.sha }}
run: |
changed="$(git diff --name-only "$BASE"...HEAD)"
if grep -qxE 'src/cli\.rs|api/commands\.json' <<<"$changed" \
&& ! grep -qx 'CHANGELOG.md' <<<"$changed"; then
echo "::error::src/cli.rs or api/commands.json changed without CHANGELOG.md. Add a line under Unreleased, or label the PR no-changelog."
exit 1
fi
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ reviewed project documentation or a source of truth. Keep durable decisions and
behavior in tracked docs, code, and tests; create a subfolder per work item and revise or
remove it as work progresses.

## Documentation

- A user-visible change updates its guide or reference under `docs/` and adds a line under
**Unreleased** in `CHANGELOG.md`. CI fails a pull request that changes `src/cli.rs` or
`api/commands.json` without `CHANGELOG.md`, unless it has the `no-changelog` label.
- That check is a heuristic, not a rule about user-visible changes. It also fires on a
test-only change to `src/cli.rs`, and it misses user-visible changes in other files,
such as `src/input.rs` or `docs/`. Add the changelog line yourself when the change is
user-visible, and add the `no-changelog` label when it fires on a change that is not.

## Agent friction log

The CLI must be easy for AI agents to use. When it is not, record why in
Expand Down
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Changelog

User-visible changes to `voltage`, newest first. Each pull request that changes what users see adds a line under **Unreleased**; see [AGENTS.md](AGENTS.md).

## Unreleased

### Added

- Pick a missing organization, environment, or wallet from a list on a terminal.
- `voltage context` shows the effective profile, scope, credential, API and auth URLs, and where each came from.
- `--dry-run` describes any API request offline; changes are only described unless `--execute` or `VOLTAGE_EXECUTE=1` asks to send them.
- `--no-input` and `--quiet`, with interrupt-safe progress and the time waited so far on payment waits and device approval.
- `voltage --version` shows the git build.
- Every command that changes something has examples in `--help`, and every command links its guide.
- Guides for wallets, webhooks, troubleshooting, and getting started, and references for global flags, environment variables, and configuration.

### Changed

- Human tables fit the terminal width, and all API text is scrubbed of terminal control characters.
- Each item of a string list gets its own line in human output.
- Errors name the next command: missing scope names the command that lists IDs, credential and profile errors list the saved names, HTTP 401 and 403 point to `voltage auth status` and `voltage context`, and `quote_required` points to `voltage quotes create`.
- An API rejection's error leads with the API's reason, and human output shows the rest of the API error as field/value lines instead of JSON.
- `-h` shows usage and options; `--help` adds notes, examples, and the guide link.

### Fixed

- Help and README examples for changes include `--execute`, so copying one sends it.

## 0.1.0 - 2026-09-18

First release: a command for every Voltage API operation, browser login and API keys, profiles, payment waits with a request journal, hosted checkout, prices, and signed release artifacts.
3 changes: 2 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Voltage CLI documentation

Start with the [README](../README.md) for install and a quick start.
Start with the [README](../README.md) for install and a quick start, or follow [getting started](getting-started.md) from install to a settled test payment.

## Do a task

Expand All @@ -11,6 +11,7 @@ Start with the [README](../README.md) for install and a quick start.
- [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
- [Scripting](guides/scripting.md): JSON, `VOLTAGE_EXECUTE`, running without a person

## Look it up
Expand Down
65 changes: 65 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Getting started

This walks one path from install to a settled test payment on mutinynet, then the same steps in a script. It takes about ten minutes. You need a Voltage account with an organization and an environment.

## 1. Install and sign in

Install the CLI from the [install guide](guides/install.md), then sign in through your browser:

```sh
voltage login --account me
```

## 2. Save your scope as a profile

Find your organization and environment, then save both with your login as a profile:

```sh
voltage organizations list
voltage environments list --org ORG_ID
voltage profiles create test --org ORG_ID --env ENV_ID --account me
voltage context --profile test
```

`voltage context` shows what every command with `--profile test` will use, and where each value came from.

## 3. Find or create a mutinynet wallet

```sh
voltage wallets list --profile test
```

Use a wallet on a BTC line of credit. A wallet on a USD line of credit needs a quote for every payment, so step 4 would fail with [`quote_required`](guides/troubleshooting.md#quote_required).

If there is no such wallet, create one on a BTC line of credit from `credit-lines list`. Check the request with a dry run first, then send it with `--execute`:

```sh
voltage credit-lines list --profile test
voltage wallets create --profile test --name tutorial \
--credit-line CREDIT_LINE_ID --network mutinynet --limit 100000 --dry-run
voltage wallets create --profile test --name tutorial \
--credit-line CREDIT_LINE_ID --network mutinynet --limit 100000 --execute
```

## 4. Receive 1,000 sats

Create a Lightning invoice, show it as a QR code, and wait until it is paid. Pay it from any mutinynet wallet or faucet:

```sh
voltage payments receive --profile test --wallet WALLET_ID \
--currency btc --kind bolt11 --amount 1000 --unit sats --qr --wait completed --execute
```

The command prints the payment when it settles. If the wait times out (exit code 5), keep watching with `voltage payments get PAYMENT_ID --profile test --wait completed`.

## 5. Script it

With `--json`, each command writes one JSON envelope to stdout and nothing else, and the exit code says what happened:

```sh
voltage payments receive --profile test --wallet WALLET_ID --json \
--currency btc --kind bolt11 --amount 1000 --unit sats --wait ready --execute \
| jq -r '.data.data.payment_request'
```

Next: [payments](guides/payments.md), [scripting](guides/scripting.md), [troubleshooting](guides/troubleshooting.md).
102 changes: 102 additions & 0 deletions docs/guides/troubleshooting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Troubleshooting

Each entry is headed by the message or code you see. Errors print `error:` and usually a `hint:` with the next command; with `--json`, they are `error.message` and `error.hint`, and `voltage context` shows the profile, scope, and credential a command would use.

## Exit codes

### Exit code 1

The API rejected the request, or a payment reached a failed state. The message carries the HTTP status, and `--json` adds the API's response under `error.detail.data`. Fix the request, or read the payment with `voltage payments get PAYMENT_ID` before trying again.

### Exit code 2

The command line or configuration is invalid: an unknown flag, a missing `--org`, `--env`, or `--wallet`, an unknown profile, or friendly flags combined with `--data`. The hint names what is missing and how to supply it.

### Exit code 3

Authentication or authorization failed. See [HTTP 401](#http-401) and [HTTP 403](#http-403) below, and the credential entries after them.

### Exit code 4

The connection failed. For a payment or treasury movement, the request may have been sent: the error reports the ID, and the CLI never resubmits it. Read it with `voltage payments get PAYMENT_ID` before you decide to retry. See [payment IDs](../concepts/safety-model.md#payment-ids-and-the-request-journal).

### Exit code 5

A wait or paging deadline passed. A pending payment is not failed: keep watching it with `voltage payments get PAYMENT_ID --profile prod --wait completed`, or pass a longer `--timeout`.

### Exit code 6

The change was described but not sent, because neither `--execute` nor `VOLTAGE_EXECUTE=1` asked for it. Read the description, then run the same command with `--execute`.

### Exit code 130

You pressed Ctrl-C. Before a change is submitted, nothing was sent. If a payment may have been sent, the message says so and gives its ID; read it before you retry.

## Credentials

### `no_credential`

`error: No credential is available`. Sign in with `voltage login`, import a key with `voltage auth import-key`, or set `VOLTAGE_API_KEY`.

### `multiple_credentials`

`error: Multiple credentials are saved`. The hint lists them; select one with `--account NAME`, or save a profile with `voltage profiles create`.

### `unknown_credential`

`error: Unknown credential NAME`. The hint lists the saved names. `voltage auth status --account NAME` shows one.

### `empty_api_key`

`error: VOLTAGE_API_KEY is empty`. A blank variable still hides saved credentials. Unset it, or set it to a key.

### `bound_elsewhere`

An imported API key belongs to one organization and environment, and the command selected another. Select a credential for that scope with `--account NAME`, or use a browser login.

### `auth_url_mismatch`

`error: Saved credentials cannot be refreshed at a different auth URL`. A browser login refreshes only at the auth URL it was saved with. Omit `--auth-url` to use that URL, which `voltage context` shows. To move the login to the new URL, run `voltage logout --account NAME`, then `voltage login --account NAME --auth-url URL`.

### The saved login has expired or was revoked

Replace the saved login with the two commands in the hint. `voltage login` refuses to overwrite a saved credential, so remove the stale one first:

```sh
voltage logout --local --account NAME
voltage login --account NAME
```

`--local` skips server revocation, which a revoked or expired login cannot complete. Keep the same `NAME` so profiles that select it keep working.

### needs a browser login

`organizations list` and `environments list` need a browser login; an API key cannot list them. Pass `--account NAME` for a saved login, or unset `VOLTAGE_API_KEY`.

### HTTP 401

The API did not accept the credential. `voltage auth status` shows the selected one and whether it has expired. For a saved login, replace it as described in [The saved login has expired or was revoked](#the-saved-login-has-expired-or-was-revoked).

A checkout command presents its session or stream token, not the account credential. Check the token in `--token-file`, `VOLTAGE_CHECKOUT_TOKEN`, or `VOLTAGE_STREAM_TOKEN`.

### HTTP 403

The credential is valid but cannot reach the selected organization or environment. `voltage context` shows which scope and credential the command used.

If `voltage context` shows the credential's location as `environment`, the command used `VOLTAGE_API_KEY` instead of a saved login. Check that the key belongs to the selected organization, or unset `VOLTAGE_API_KEY` (or pass `--account NAME`) to use a saved login.

### Cannot read OS credential store

The keychain is locked or missing, as on headless Linux without an unlocked Secret Service. Unlock it, or log in again with `--credential-store file`. See [credential stores](../reference/configuration.md#credential-stores).

## API features

### `feature_flag_disabled`

On-chain and BIP21 payments and treasury movements are enabled per organization. Ask Voltage to enable the feature for yours.

### `quote_required`

A wallet on a USD line of credit needs a quote for every payment. Create a quote with `voltage quotes create`, then pass `--quote QUOTE_ID` and the same USD amount to the payment. See [quotes and lines of credit](payments.md#quotes-and-lines-of-credit).

Related: [exit codes](../reference/exit-codes.md), [environment variables](../reference/environment.md), [log in and use profiles](login-and-profiles.md).
39 changes: 39 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,7 @@ fn leaf(matches: &ArgMatches) -> (Vec<String>, &ArgMatches) {
#[cfg(test)]
mod tests {
use super::*;
use crate::config::CredentialProblem as Problem;
use crate::error::ErrorKind as Kind;
use std::path::Path;

Expand Down Expand Up @@ -1850,6 +1851,44 @@ mod tests {
}
}

#[test]
fn troubleshooting_covers_every_exit_code_and_credential_problem() {
let text = std::fs::read_to_string(
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("docs/guides/troubleshooting.md"),
)
.unwrap()
// Git may check the file out with CRLF line endings, as on Windows.
.replace("\r\n", "\n");
for code in error_kinds().map(Kind::exit_code) {
assert!(
text.contains(&format!("\n### Exit code {code}\n")),
"exit code {code}"
);
}
// A new problem fails to compile here until it has a name, but its entry is checked
// only once it is also added to the list below.
let name = |problem: Problem| match problem {
Problem::EmptyApiKey => "empty_api_key",
Problem::NoCredential => "no_credential",
Problem::MultipleCredentials => "multiple_credentials",
Problem::UnknownCredential => "unknown_credential",
Problem::BoundElsewhere => "bound_elsewhere",
Problem::AuthUrlMismatch => "auth_url_mismatch",
};
for problem in [
Problem::EmptyApiKey,
Problem::NoCredential,
Problem::MultipleCredentials,
Problem::UnknownCredential,
Problem::BoundElsewhere,
Problem::AuthUrlMismatch,
] {
let name = name(problem);
assert_eq!(serde_json::to_value(problem).unwrap(), name);
assert!(text.contains(&format!("\n### `{name}`\n")), "{name}");
}
}

/// Every command below `cmd`, by path, with its rendered long help.
fn long_helps(cmd: &mut ClapCommand, path: &mut Vec<String>, out: &mut Vec<(String, String)>) {
out.push((path.join(" "), cmd.render_long_help().to_string()));
Expand Down
2 changes: 1 addition & 1 deletion tests/cli_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4248,7 +4248,7 @@ fn file_names(dir: &Path) -> BTreeSet<OsString> {
async fn documented_changes_pass_as_offline_dry_runs() {
let (server, dir) = fixture().await;
let mut settings = seeded_api_key(dir.path(), "key", ACCOUNT_KEY);
for name in ["prod", "staging"] {
for name in ["prod", "staging", "test"] {
settings.config.profiles.insert(
name.into(),
Profile {
Expand Down
16 changes: 11 additions & 5 deletions tests/support/examples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@

use std::path::PathBuf;

/// The README and the docs index, guides, concepts, reference, and development guide, as
/// (path from the repository root, contents).
/// The README, changelog, docs index, tutorial, guides, concepts, reference, and development
/// guide, as (path from the repository root, contents).
pub fn user_docs() -> Vec<(PathBuf, String)> {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut paths = ["README.md", "docs/README.md", "docs/development.md"]
.map(|path| root.join(path))
.to_vec();
let mut paths = [
"README.md",
"CHANGELOG.md",
"docs/README.md",
"docs/getting-started.md",
"docs/development.md",
]
.map(|path| root.join(path))
.to_vec();
for dir in ["guides", "concepts", "reference"] {
for entry in std::fs::read_dir(root.join("docs").join(dir)).unwrap() {
let path = entry.unwrap().path();
Expand Down
Loading