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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Things to verify for each new endpoint:

- **HTTP-noun → CLI-noun-verb**: `admin.get_endpoints` → `qn endpoint list`, `admin.show_endpoint` → `qn endpoint show <ID>`, `admin.update_endpoint_status(id, "paused")` → `qn endpoint pause <ID>` (split the verb out, since the user thinks "pause", not "update status to paused").
- **Aliases**: every list-style command gets `#[command(visible_alias = "ls")]`. Plural top-level nouns get one too (`#[command(visible_alias = "endpoints")]`).
- **Positional value names**: a field named `id` renders as an uninformative `<ID>` in help. Give every positional an explicit, resource-specific `#[arg(value_name = "ENDPOINT_ID")]` (uppercase, underscored: `STREAM_ID`, `WEBHOOK_ID`, `TEAM_ID`, …). Multi-word fields like `referrer_id` already render fine as `<REFERRER_ID>`.
- **Hyphenation**: clap kebab-cases enum variants by default. `RateLimit` → `rate-limit`. Test invocations must use the kebab form (`qn endpoint rate-limit method-create`, not `ratelimit`).
- **Negative numbers**: any `i64` flag that accepts `-1` (`--end`, etc.) needs `#[arg(long, allow_hyphen_values = true)]` or clap will read it as another flag.
- **Multi-value flags**: prefer repeatable `--method foo --method bar` (clap `Vec<String>` with `#[arg(long = "method")]`). Optionally also accept `--methods foo,bar` via a second field with `value_delimiter = ','`. The command body extends one into the other.
Expand Down
22 changes: 6 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ error: null

Pick the recommended path for your platform. Other channels are listed under [Alternatives](#alternatives).

### Homebrew (macOS, Linux)
### Homebrew (macOS)

```sh
brew install quicknode/tap/qn
Expand Down Expand Up @@ -110,15 +110,8 @@ You will need a Quicknode API key to get started. Once you have that, you can ru
env var is set. The same layout applies on Windows:
`%USERPROFILE%\.config\qn\config.toml`. Managed by `qn auth login`.

There is deliberately **no environment-variable key source**: a key left
exported in a shell is invisible state that outlives the session it was set
for, and makes it far too easy to run a destructive command against the wrong
account. For CI, write a config file and point `--config-file` at it (or pass
`--api-key` from your secret store).

If no source matches, `qn` exits with code 4 and tells you to run
`qn auth login`. Regular commands never prompt — only `qn auth login` does.
This keeps scripts and CI deterministic.
`qn auth login`.

```sh
qn auth login # prompts for the key, writes it to ~/.config/qn/config.toml
Expand Down Expand Up @@ -252,13 +245,10 @@ qn completions powershell > qn.ps1

## Configuration via environment

`qn` reads no API credentials from the environment (see
[Authentication](#authentication) for why). The conventional variables are
honored: `NO_COLOR` and `TERM=dumb` disable color, and
`XDG_CONFIG_HOME`/`HOME` (`USERPROFILE` on Windows) locate the default
config file. The CLI hands the
key to the SDK explicitly; it does not read the SDK's `QN_SDK__*` environment
namespace.
The conventional variables are honored: `NO_COLOR` and `TERM=dumb` disable color,
and `XDG_CONFIG_HOME`/`HOME` (`USERPROFILE` on Windows) locate the default
config file. The CLI hands the key to the Quicknode SDK explicitly; it does
not read the SDK's `QN_SDK__*` environment namespace.

The hidden `--base-url <URL>` flag overrides the API host for all four
sub-clients at once (used for integration tests and on-prem mirrors).
Expand Down
12 changes: 12 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ use crate::output::Format;
to save a key the first time.",
propagate_version = true,
disable_help_subcommand = true,
// The auto-generated -h/-V land under a separate "Options" heading; we
// re-declare them below so they group with the other global flags.
disable_help_flag = true,
disable_version_flag = true,
after_help = "Examples:\n \
qn auth login\n \
qn endpoint create --chain ethereum --network mainnet\n \
Expand Down Expand Up @@ -88,6 +92,14 @@ pub struct Cli {
#[arg(long, global = true, hide = true)]
pub base_url: Option<String>,

/// Print help (see a summary with '-h').
#[arg(short = 'h', long, global = true, action = ArgAction::Help)]
pub help: Option<bool>,

/// Print version.
#[arg(short = 'V', long, global = true, action = ArgAction::Version)]
pub version: Option<bool>,

#[command(subcommand)]
pub command: Command,
}
Expand Down
10 changes: 10 additions & 0 deletions src/commands/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ async fn login(args: LoginArgs, global: GlobalArgs) -> Result<(), CliError> {
"no TTY available; pass --api-key to log in non-interactively".to_string(),
));
}
if !global.quiet {
let _ = writeln!(
std::io::stderr(),
"Welcome! The qn CLI uses a Quicknode API key to manage your account.\n\
Your key is stored locally in {}.\n\n \
Get an API key: https://dashboard.quicknode.com/api-keys\n \
Need an account? https://www.quicknode.com/signup\n",
path.display()
);
}
config::prompt_for_api_key()?
}
};
Expand Down
35 changes: 29 additions & 6 deletions src/commands/endpoint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,32 +43,51 @@ pub enum EndpointCmd {
/// Create a new endpoint on a chain/network.
Create(CreateArgs),
/// Show full details for a single endpoint.
Show { id: String },
Show {
#[arg(value_name = "ENDPOINT_ID")]
id: String,
},
/// Update an endpoint's label.
Update(UpdateArgs),
/// Archive an endpoint (irreversible from the CLI).
Archive(ArchiveArgs),
/// Pause an endpoint (stops accepting requests).
Pause { id: String },
Pause {
#[arg(value_name = "ENDPOINT_ID")]
id: String,
},
/// Resume a paused endpoint.
Resume { id: String },
Resume {
#[arg(value_name = "ENDPOINT_ID")]
id: String,
},
/// Show the HTTP and WebSocket URLs for an endpoint.
Urls { id: String },
Urls {
#[arg(value_name = "ENDPOINT_ID")]
id: String,
},
/// Fetch request logs for an endpoint.
Logs(LogsArgs),
/// Fetch a single request log's full request/response payloads.
LogDetails {
/// Endpoint id.
#[arg(value_name = "ENDPOINT_ID")]
id: String,
/// Request id (UUID from the logs listing).
request_id: String,
},
/// Fetch metric series for an endpoint.
Metrics(MetricsArgs),
/// Enable multichain on an endpoint.
EnableMultichain { id: String },
EnableMultichain {
#[arg(value_name = "ENDPOINT_ID")]
id: String,
},
/// Disable multichain on an endpoint.
DisableMultichain { id: String },
DisableMultichain {
#[arg(value_name = "ENDPOINT_ID")]
id: String,
},

/// Manage endpoint tags (per-endpoint add/remove and account-wide list/rename/delete).
#[command(subcommand, visible_alias = "tags")]
Expand Down Expand Up @@ -140,6 +159,7 @@ pub struct CreateArgs {
#[derive(Debug, ClapArgs)]
pub struct UpdateArgs {
/// Endpoint id.
#[arg(value_name = "ENDPOINT_ID")]
pub id: String,
/// New label.
#[arg(long)]
Expand All @@ -149,12 +169,14 @@ pub struct UpdateArgs {
#[derive(Debug, ClapArgs)]
pub struct ArchiveArgs {
/// Endpoint id.
#[arg(value_name = "ENDPOINT_ID")]
pub id: String,
}

#[derive(Debug, ClapArgs)]
pub struct LogsArgs {
/// Endpoint id.
#[arg(value_name = "ENDPOINT_ID")]
pub id: String,
/// Start of the window (RFC-3339, relative duration like `1h`, or `now`).
#[arg(long)]
Expand All @@ -176,6 +198,7 @@ pub struct LogsArgs {
#[derive(Debug, ClapArgs)]
pub struct MetricsArgs {
/// Endpoint id.
#[arg(value_name = "ENDPOINT_ID")]
pub id: String,
/// Metric name (e.g. `method_calls_over_time`, `response_status_breakdown`).
#[arg(long)]
Expand Down
20 changes: 17 additions & 3 deletions src/commands/endpoint/ratelimit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,39 @@ use crate::retry::retrying;
#[derive(Debug, Subcommand)]
pub enum RateLimitCmd {
/// Show the endpoint's current per-bucket rate limits.
Get { id: String },
Get {
#[arg(value_name = "ENDPOINT_ID")]
id: String,
},
/// Set endpoint-level rate limits (any of rps/rpm/rpd; omitted are untouched).
Set(SetArgs),
/// Delete a user-set rate-limit override (plan defaults can't be deleted).
DeleteOverride { id: String, override_id: String },
DeleteOverride {
#[arg(value_name = "ENDPOINT_ID")]
id: String,
override_id: String,
},

/// List method-level rate limiters on the endpoint.
MethodList { id: String },
MethodList {
#[arg(value_name = "ENDPOINT_ID")]
id: String,
},
/// Create a new method-level rate limiter.
MethodCreate(MethodCreateArgs),
/// Update an existing method-level rate limiter.
MethodUpdate(MethodUpdateArgs),
/// Delete a method-level rate limiter.
MethodDelete {
#[arg(value_name = "ENDPOINT_ID")]
id: String,
method_rate_limit_id: String,
},
}

#[derive(Debug, ClapArgs)]
pub struct SetArgs {
#[arg(value_name = "ENDPOINT_ID")]
pub id: String,
/// Requests-per-second cap.
#[arg(long)]
Expand All @@ -51,6 +63,7 @@ pub struct SetArgs {

#[derive(Debug, ClapArgs)]
pub struct MethodCreateArgs {
#[arg(value_name = "ENDPOINT_ID")]
pub id: String,
/// Interval (second/minute/hour/day).
#[arg(long, value_parser = ["second", "minute", "hour", "day"])]
Expand All @@ -68,6 +81,7 @@ pub struct MethodCreateArgs {

#[derive(Debug, ClapArgs)]
pub struct MethodUpdateArgs {
#[arg(value_name = "ENDPOINT_ID")]
pub id: String,
pub method_rate_limit_id: String,
#[arg(long = "method")]
Expand Down
Loading
Loading