diff --git a/docs/README.md b/docs/README.md index 1006874..4c7f89d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 diff --git a/docs/development.md b/docs/development.md index 97ea4f7..62c58fa 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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 diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md new file mode 100644 index 0000000..af66aed --- /dev/null +++ b/docs/reference/configuration.md @@ -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). diff --git a/docs/reference/environment.md b/docs/reference/environment.md new file mode 100644 index 0000000..2c62d77 --- /dev/null +++ b/docs/reference/environment.md @@ -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). diff --git a/docs/reference/global-flags.md b/docs/reference/global-flags.md new file mode 100644 index 0000000..f77f02f --- /dev/null +++ b/docs/reference/global-flags.md @@ -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). diff --git a/src/cli.rs b/src/cli.rs index 2479744..fa3e58d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -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)> = 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 = 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, out: &mut Vec<(String, String)>) { out.push((path.join(" "), cmd.render_long_help().to_string()));