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
3 changes: 3 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ Start with the [README](../README.md) for install and a quick start.
- [Command reference](commands.md): every command and its API route
- [Output](reference/output.md): tables, the JSON envelope, stdout and stderr
- [Exit codes](reference/exit-codes.md)
- [Global flags](reference/global-flags.md): flags every command accepts
- [Environment variables](reference/environment.md): every `VOLTAGE_*` variable and what beats it
- [Configuration](reference/configuration.md): the configuration directory, credential stores, and the request journal

## Understand

Expand Down
2 changes: 2 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ selection, describe-by-default changes, secret output, pagination, payment waits
checkout tokens and streams, and the credential lifecycle. Every change the docs show with `--execute` also runs as an offline `--dry-run`, so documented flags, units, and scope keep validating. Live payment tests must use an
explicitly configured test-network wallet; routine tests never send a live payment.

`docs/reference/global-flags.md` is rendered from the command definitions; after changing a global flag, run `VOLTAGE_BLESS=1 cargo test` to regenerate it, since plain `cargo test` fails on a stale copy. Tests also require every `VOLTAGE_*` variable read in `src/` to appear in `docs/reference/environment.md`, and every exit code in `docs/reference/exit-codes.md`.

The manual CRAP risk check is described in [the style guide](quality.md#crap-risk-check).

## Continuous integration and releases
Expand Down
23 changes: 23 additions & 0 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Configuration

The configuration directory is `--config-dir`, else `VOLTAGE_CONFIG_DIR`, else `$XDG_CONFIG_HOME/voltage`, else `~/.config/voltage`. It must be an absolute path. The CLI creates it owner-only (mode `0700` on Unix) and refuses one that is a symlink or owned by another user. `voltage context` shows which directory is in use and creates nothing.

| Path | Contents |
|---|---|
| `config.toml` | Saved credentials' non-secret facts (name, kind, store, email, bound organization and environment, auth URL) and profiles. No secrets. |
| `credentials/` | Secrets for credentials saved with `--credential-store file`, one owner-only file each |
| `credentials.lock` | Serializes login refreshes and credential changes across concurrent commands |
| `requests/` | The request journal: each payment's or treasury movement's ID, operation, scope, and request hash, never the body. See [payment IDs](../concepts/safety-model.md#payment-ids-and-the-request-journal). |

## Credential stores

A credential's secret is kept in one store, chosen when you log in or import a key:

- `keychain` (default): the macOS Keychain, the Linux Secret Service, or the Windows Credential Manager, under the service name `voltage-cli`.
- `file`: an owner-only file under `credentials/`, for machines without an unlocked keychain, such as headless Linux.

## Profiles

A profile saves an organization, an environment, and a credential under one name. `--profile NAME` uses it as a complete configuration; see [log in and use profiles](../guides/login-and-profiles.md).

Related: [environment variables](environment.md), [global flags](global-flags.md), [install](../guides/install.md#credential-stores).
18 changes: 18 additions & 0 deletions docs/reference/environment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Environment variables

A flag beats a variable, and `--profile` or `--account` beats the scope and credential variables: a selected profile is a complete configuration, so `VOLTAGE_ORGANIZATION_ID`, `VOLTAGE_ENVIRONMENT_ID`, `VOLTAGE_WALLET_ID`, and `VOLTAGE_API_KEY` are ignored while one is selected. `voltage context` lists every variable that is set but ignored, and where each value it uses came from.

| Variable | Used for | Flag that wins |
|---|---|---|
| `VOLTAGE_API_KEY` | An environment API key, used when no profile or account is selected. A blank value still shadows saved credentials, so commands fail instead of falling back. | `--profile`, `--account` |
| `VOLTAGE_ORGANIZATION_ID` | Organization UUID | `--org`, `--profile` |
| `VOLTAGE_ENVIRONMENT_ID` | Environment UUID | `--env`, `--profile` |
| `VOLTAGE_WALLET_ID` | Wallet UUID | `--wallet`, `--profile` |
| `VOLTAGE_EXECUTE` | `1` or `true` sends every change without `--execute`; `0`, `false`, or empty only describes changes. Any other value fails before a change. | `--dry-run` |
| `VOLTAGE_CHECKOUT_TOKEN` | A checkout session token for `checkout sessions` reads | `--token-file` |
| `VOLTAGE_STREAM_TOKEN` | A checkout stream token for `checkout events watch` | `--token-file` |
| `VOLTAGE_CONFIG_DIR` | The configuration directory, as an absolute path | `--config-dir` |
| `XDG_CONFIG_HOME` | Parent of the default configuration directory, `$XDG_CONFIG_HOME/voltage` (else `~/.config/voltage`) | `--config-dir`, `VOLTAGE_CONFIG_DIR` |
| `COLUMNS` | Terminal width for human tables; otherwise the terminal's own width | — |

Related: [configuration](configuration.md), [global flags](global-flags.md), [scripting](../guides/scripting.md).
47 changes: 47 additions & 0 deletions docs/reference/global-flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Global flags

Generated from the command definitions by `VOLTAGE_BLESS=1 cargo test`; do not edit by hand. Every command accepts these flags, before or after its name.

## Scope

| Flag | Meaning |
|---|---|
| `--profile PROFILE` | Use this named profile as a complete configuration |
| `--account ACCOUNT` | Select a saved credential |
| `--org UUID` | Organization UUID |
| `--env UUID` | Environment UUID (repeat for supported list filters) |
| `--wallet UUID` | Wallet UUID or wallet filter |
| `--webhook UUID` | Parent webhook UUID |

## Output

| Flag | Meaning |
|---|---|
| `--json` | Write a stable JSON result envelope |
| `--output OUTPUT` | Output format (`table`, `json`, `ndjson`) |
| `--output-file PATH` | Save the complete response to a new owner-only file |
| `--show-secrets` | Explicitly allow secrets in result output |
| `-q`, `--quiet` | Hide optional progress and notices, not errors, results, or recovery IDs |

## Safety

| Flag | Meaning |
|---|---|
| `--no-input` | Never prompt for a secret; supply it with --stdin or the environment instead |

## Request

| Flag | Meaning |
|---|---|
| `--timeout SECONDS` | HTTP or wait deadline in seconds; default `60` |

## Configuration

| Flag | Meaning |
|---|---|
| `--config-dir DIR` | Configuration directory (default: ~/.config/voltage) |
| `--api-url URL` | Explicit API base URL; never persisted |
| `--auth-url URL` | Explicit auth base URL; never persisted |
| `--price-url URL` | Explicit price service base URL; never persisted |

Related: [environment variables](environment.md), [configuration](configuration.md), [output](output.md).
133 changes: 133 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1685,6 +1685,139 @@ mod tests {
}
}

/// `docs/reference/global-flags.md`, rendered from the flags every command accepts.
fn global_flags_reference() -> String {
let flags = GlobalFlags::augment_args(ClapCommand::new("voltage"));
// Rows by heading, in the order each heading first appears.
let mut sections: Vec<(&str, Vec<String>)> = Vec::new();
for arg in flags.get_arguments() {
let Some(long) = arg.get_long() else { continue };
let mut flag = match arg.get_short() {
Some(short) => format!("`-{short}`, `--{long}"),
None => format!("`--{long}"),
};
let mut meaning = arg.get_help().map(ToString::to_string).unwrap_or_default();
if arg.get_action().takes_values() {
let names = arg.get_value_names().unwrap_or_default();
let name = names.first().map_or("VALUE", |name| name.as_str());
flag.push_str(&format!(" {name}"));
let values: Vec<String> = arg
.get_possible_values()
.iter()
.filter(|value| !value.is_hide_set())
.map(|value| format!("`{}`", value.get_name()))
.collect();
if !values.is_empty() {
meaning.push_str(&format!(" ({})", values.join(", ")));
}
if let Some(default) = arg.get_default_values().first() {
meaning.push_str(&format!("; default `{}`", default.to_string_lossy()));
}
}
flag.push('`');
let row = format!("| {flag} | {} |", meaning.replace('|', "\\|"));
let heading = arg.get_help_heading().unwrap_or("Other");
match sections.iter_mut().find(|(name, _)| *name == heading) {
Some((_, rows)) => rows.push(row),
None => sections.push((heading, vec![row])),
}
}
let mut text = String::from(
"# Global flags\n\nGenerated from the command definitions by `VOLTAGE_BLESS=1 cargo test`; do not edit by hand. Every command accepts these flags, before or after its name.\n",
);
for (heading, rows) in sections {
text.push_str(&format!(
"\n## {heading}\n\n| Flag | Meaning |\n|---|---|\n{}\n",
rows.join("\n")
));
}
text.push_str("\nRelated: [environment variables](environment.md), [configuration](configuration.md), [output](output.md).\n");
text
}

#[test]
fn global_flags_reference_matches_the_command_definitions() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("docs/reference/global-flags.md");
let expected = global_flags_reference();
if std::env::var_os("VOLTAGE_BLESS").is_some() {
std::fs::write(&path, &expected).unwrap();
}
// Git may check the file out with CRLF line endings, as on Windows.
let actual = std::fs::read_to_string(&path)
.unwrap_or_default()
.replace("\r\n", "\n");
assert!(
actual == expected,
"docs/reference/global-flags.md is stale; run VOLTAGE_BLESS=1 cargo test"
);
}

#[test]
fn every_variable_and_exit_code_is_documented() {
use crate::error::ErrorKind as Kind;
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let read = |path: &str| std::fs::read_to_string(root.join(path)).unwrap();
let environment = read("docs/reference/environment.md");
let mut variables = std::collections::BTreeSet::new();
for entry in std::fs::read_dir(root.join("src")).unwrap() {
let path = entry.unwrap().path();
if path.extension().is_some_and(|ext| ext == "rs") {
let source = std::fs::read_to_string(path).unwrap();
for (start, _) in source.match_indices("\"VOLTAGE_") {
let name: String = source[start + 1..]
.chars()
.take_while(|c| c.is_ascii_uppercase() || *c == '_')
.collect();
// A bare `VOLTAGE_` is a prefix check, not a variable.
if !name.ends_with('_') {
variables.insert(name);
}
}
}
}
// Set by build.rs at compile time, and the contributors' switch for this test file;
// neither is read from a user's environment.
variables.remove("VOLTAGE_VERSION");
variables.remove("VOLTAGE_BLESS");
assert!(variables.len() >= 8, "{variables:?}");
for name in variables
.iter()
.map(String::as_str)
.chain(["COLUMNS", "XDG_CONFIG_HOME"])
{
assert!(
environment.contains(&format!("`{name}`")),
"{name} is undocumented"
);
}
let exit_codes = read("docs/reference/exit-codes.md");
// A new kind fails to compile here until it is added to the list below.
let _listed = |kind: Kind| match kind {
Kind::Api
| Kind::Usage
| Kind::Auth
| Kind::Transport
| Kind::Timeout
| Kind::NotExecuted
| Kind::Interrupted => (),
};
for kind in [
Kind::Api,
Kind::Usage,
Kind::Auth,
Kind::Transport,
Kind::Timeout,
Kind::NotExecuted,
Kind::Interrupted,
] {
let code = kind.exit_code();
assert!(
exit_codes.contains(&format!("| {code} |")),
"exit code {code} is undocumented"
);
}
}

/// 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
Loading