diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..c98162c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,7 @@ + + +- [ ] 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: diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..e1dbb94 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 622b5fb..e231d44 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..39f30cf --- /dev/null +++ b/CHANGELOG.md @@ -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. diff --git a/docs/README.md b/docs/README.md index 4c7f89d..6a62487 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 @@ -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 diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..4508df9 --- /dev/null +++ b/docs/getting-started.md @@ -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). diff --git a/docs/guides/troubleshooting.md b/docs/guides/troubleshooting.md new file mode 100644 index 0000000..6342c00 --- /dev/null +++ b/docs/guides/troubleshooting.md @@ -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). diff --git a/src/cli.rs b/src/cli.rs index 78d1055..8eb7f03 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1247,6 +1247,7 @@ fn leaf(matches: &ArgMatches) -> (Vec, &ArgMatches) { #[cfg(test)] mod tests { use super::*; + use crate::config::CredentialProblem as Problem; use crate::error::ErrorKind as Kind; use std::path::Path; @@ -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, out: &mut Vec<(String, String)>) { out.push((path.join(" "), cmd.render_long_help().to_string())); diff --git a/tests/cli_integration.rs b/tests/cli_integration.rs index 022786c..4b4c0f0 100644 --- a/tests/cli_integration.rs +++ b/tests/cli_integration.rs @@ -4248,7 +4248,7 @@ fn file_names(dir: &Path) -> BTreeSet { 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 { diff --git a/tests/support/examples.rs b/tests/support/examples.rs index bb75738..66717cc 100644 --- a/tests/support/examples.rs +++ b/tests/support/examples.rs @@ -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();