Skip to content

Commit a956f5c

Browse files
authored
CLI polish: help layout, security confirmations, error copy, ID value names, login onboarding (DX-5694) (#31)
* Group -h/--help and -V/--version under Global options (DX-5694) The auto-generated help/version flags rendered under a lone "Options" heading on every help screen, separate from the real global flags. Disable clap's built-ins and re-declare them as global args so they list with the rest of the globals. Behavior is unchanged: -h prints the summary, --help the long form, and -V works on subcommands via propagate_version. * Gate all endpoint security removes behind confirmation (DX-5694) Only 'security token delete' asked for confirmation; the other six remove verbs (referrer, ip, jwt, domain-mask, request-filter, ip-header) deleted protection rules immediately. All now use the mild confirmation: --yes skips, a TTY prompts, and non-TTY without --yes exits 5 before any request is sent. Prompts name the resource and endpoint. Adds 12 tests (with/without --yes per command, the latter asserting zero requests reach the mock). * Soften the 5xx error message (DX-5694) "Quicknode API is having issues" assigned blame for what can be a transient failure. Replace with a neutral message that keeps the HTTP code for debugging and points at support: "something went wrong (HTTP {code}). Please try again; if the problem persists, contact support at https://support.quicknode.com." * Use resource-specific value names for positional ids (DX-5694) Bare 'id' positionals rendered as a generic <ID> in usage lines. Every id positional now declares value_name — <ENDPOINT_ID>, <STREAM_ID>, <WEBHOOK_ID>, <TEAM_ID> — so 'qn endpoint show --help' reads 'Usage: qn endpoint show [OPTIONS] <ENDPOINT_ID>'. Multi-word fields (referrer_id, tag_id, …) already rendered descriptively. Documents the convention in CLAUDE.md. * Add onboarding preamble to interactive 'qn auth login' (DX-5694) The command jumped straight to a hidden key prompt with no context. Interactive logins now print a short welcome on stderr explaining that an API key is needed, where it will be stored, and where to get one (dashboard.quicknode.com/api-keys, quicknode.com/signup). Suppressed by --quiet; not shown when --api-key is passed. * Accept true/false for --fix-block-reorgs (DX-5694) The flag took a raw 0/1 integer. It now parses as a bool via clap's BoolishValueParser, so the documented form is true/false while 0/1 (and yes/no) keep working. The value still maps to the integer the streams API expects. * Reword usage --to help text (DX-5694) "Omit for now." read as a temporary instruction rather than "defaults to the current time". Now: "End time. Defaults to now." * Make 'list' the primary verb for kv listings (DX-5694) 'qn kv set ls' and 'qn kv list ls' were the only listings with 'ls' as the primary name. Renamed to 'list' with 'ls' as a visible alias, matching the convention everywhere else in the CLI. Both spellings keep working; existing tests invoke 'ls' and pass via the alias. * Apply rustfmt to new security confirmation tests * Trim README auth and install docs (DX-5694) Scope the Homebrew install section to macOS, and shorten the authentication and environment sections: state where the key comes from and how to log in, without the extended design rationale. * Strip trailing whitespace in README
1 parent 91d4fb9 commit a956f5c

19 files changed

Lines changed: 498 additions & 53 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ Things to verify for each new endpoint:
7070

7171
- **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").
7272
- **Aliases**: every list-style command gets `#[command(visible_alias = "ls")]`. Plural top-level nouns get one too (`#[command(visible_alias = "endpoints")]`).
73+
- **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>`.
7374
- **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`).
7475
- **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.
7576
- **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.

README.md

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ error: null
3131

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

34-
### Homebrew (macOS, Linux)
34+
### Homebrew (macOS)
3535

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

113-
There is deliberately **no environment-variable key source**: a key left
114-
exported in a shell is invisible state that outlives the session it was set
115-
for, and makes it far too easy to run a destructive command against the wrong
116-
account. For CI, write a config file and point `--config-file` at it (or pass
117-
`--api-key` from your secret store).
118-
119113
If no source matches, `qn` exits with code 4 and tells you to run
120-
`qn auth login`. Regular commands never prompt — only `qn auth login` does.
121-
This keeps scripts and CI deterministic.
114+
`qn auth login`.
122115

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

253246
## Configuration via environment
254247

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

263253
The hidden `--base-url <URL>` flag overrides the API host for all four
264254
sub-clients at once (used for integration tests and on-prem mirrors).

src/cli.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ use crate::output::Format;
2626
to save a key the first time.",
2727
propagate_version = true,
2828
disable_help_subcommand = true,
29+
// The auto-generated -h/-V land under a separate "Options" heading; we
30+
// re-declare them below so they group with the other global flags.
31+
disable_help_flag = true,
32+
disable_version_flag = true,
2933
after_help = "Examples:\n \
3034
qn auth login\n \
3135
qn endpoint create --chain ethereum --network mainnet\n \
@@ -88,6 +92,14 @@ pub struct Cli {
8892
#[arg(long, global = true, hide = true)]
8993
pub base_url: Option<String>,
9094

95+
/// Print help (see a summary with '-h').
96+
#[arg(short = 'h', long, global = true, action = ArgAction::Help)]
97+
pub help: Option<bool>,
98+
99+
/// Print version.
100+
#[arg(short = 'V', long, global = true, action = ArgAction::Version)]
101+
pub version: Option<bool>,
102+
91103
#[command(subcommand)]
92104
pub command: Command,
93105
}

src/commands/auth.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ async fn login(args: LoginArgs, global: GlobalArgs) -> Result<(), CliError> {
7070
"no TTY available; pass --api-key to log in non-interactively".to_string(),
7171
));
7272
}
73+
if !global.quiet {
74+
let _ = writeln!(
75+
std::io::stderr(),
76+
"Welcome! The qn CLI uses a Quicknode API key to manage your account.\n\
77+
Your key is stored locally in {}.\n\n \
78+
Get an API key: https://dashboard.quicknode.com/api-keys\n \
79+
Need an account? https://www.quicknode.com/signup\n",
80+
path.display()
81+
);
82+
}
7383
config::prompt_for_api_key()?
7484
}
7585
};

src/commands/endpoint/mod.rs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,32 +43,51 @@ pub enum EndpointCmd {
4343
/// Create a new endpoint on a chain/network.
4444
Create(CreateArgs),
4545
/// Show full details for a single endpoint.
46-
Show { id: String },
46+
Show {
47+
#[arg(value_name = "ENDPOINT_ID")]
48+
id: String,
49+
},
4750
/// Update an endpoint's label.
4851
Update(UpdateArgs),
4952
/// Archive an endpoint (irreversible from the CLI).
5053
Archive(ArchiveArgs),
5154
/// Pause an endpoint (stops accepting requests).
52-
Pause { id: String },
55+
Pause {
56+
#[arg(value_name = "ENDPOINT_ID")]
57+
id: String,
58+
},
5359
/// Resume a paused endpoint.
54-
Resume { id: String },
60+
Resume {
61+
#[arg(value_name = "ENDPOINT_ID")]
62+
id: String,
63+
},
5564
/// Show the HTTP and WebSocket URLs for an endpoint.
56-
Urls { id: String },
65+
Urls {
66+
#[arg(value_name = "ENDPOINT_ID")]
67+
id: String,
68+
},
5769
/// Fetch request logs for an endpoint.
5870
Logs(LogsArgs),
5971
/// Fetch a single request log's full request/response payloads.
6072
LogDetails {
6173
/// Endpoint id.
74+
#[arg(value_name = "ENDPOINT_ID")]
6275
id: String,
6376
/// Request id (UUID from the logs listing).
6477
request_id: String,
6578
},
6679
/// Fetch metric series for an endpoint.
6780
Metrics(MetricsArgs),
6881
/// Enable multichain on an endpoint.
69-
EnableMultichain { id: String },
82+
EnableMultichain {
83+
#[arg(value_name = "ENDPOINT_ID")]
84+
id: String,
85+
},
7086
/// Disable multichain on an endpoint.
71-
DisableMultichain { id: String },
87+
DisableMultichain {
88+
#[arg(value_name = "ENDPOINT_ID")]
89+
id: String,
90+
},
7291

7392
/// Manage endpoint tags (per-endpoint add/remove and account-wide list/rename/delete).
7493
#[command(subcommand, visible_alias = "tags")]
@@ -140,6 +159,7 @@ pub struct CreateArgs {
140159
#[derive(Debug, ClapArgs)]
141160
pub struct UpdateArgs {
142161
/// Endpoint id.
162+
#[arg(value_name = "ENDPOINT_ID")]
143163
pub id: String,
144164
/// New label.
145165
#[arg(long)]
@@ -149,12 +169,14 @@ pub struct UpdateArgs {
149169
#[derive(Debug, ClapArgs)]
150170
pub struct ArchiveArgs {
151171
/// Endpoint id.
172+
#[arg(value_name = "ENDPOINT_ID")]
152173
pub id: String,
153174
}
154175

155176
#[derive(Debug, ClapArgs)]
156177
pub struct LogsArgs {
157178
/// Endpoint id.
179+
#[arg(value_name = "ENDPOINT_ID")]
158180
pub id: String,
159181
/// Start of the window (RFC-3339, relative duration like `1h`, or `now`).
160182
#[arg(long)]
@@ -176,6 +198,7 @@ pub struct LogsArgs {
176198
#[derive(Debug, ClapArgs)]
177199
pub struct MetricsArgs {
178200
/// Endpoint id.
201+
#[arg(value_name = "ENDPOINT_ID")]
179202
pub id: String,
180203
/// Metric name (e.g. `method_calls_over_time`, `response_status_breakdown`).
181204
#[arg(long)]

src/commands/endpoint/ratelimit.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,39 @@ use crate::retry::retrying;
1616
#[derive(Debug, Subcommand)]
1717
pub enum RateLimitCmd {
1818
/// Show the endpoint's current per-bucket rate limits.
19-
Get { id: String },
19+
Get {
20+
#[arg(value_name = "ENDPOINT_ID")]
21+
id: String,
22+
},
2023
/// Set endpoint-level rate limits (any of rps/rpm/rpd; omitted are untouched).
2124
Set(SetArgs),
2225
/// Delete a user-set rate-limit override (plan defaults can't be deleted).
23-
DeleteOverride { id: String, override_id: String },
26+
DeleteOverride {
27+
#[arg(value_name = "ENDPOINT_ID")]
28+
id: String,
29+
override_id: String,
30+
},
2431

2532
/// List method-level rate limiters on the endpoint.
26-
MethodList { id: String },
33+
MethodList {
34+
#[arg(value_name = "ENDPOINT_ID")]
35+
id: String,
36+
},
2737
/// Create a new method-level rate limiter.
2838
MethodCreate(MethodCreateArgs),
2939
/// Update an existing method-level rate limiter.
3040
MethodUpdate(MethodUpdateArgs),
3141
/// Delete a method-level rate limiter.
3242
MethodDelete {
43+
#[arg(value_name = "ENDPOINT_ID")]
3344
id: String,
3445
method_rate_limit_id: String,
3546
},
3647
}
3748

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

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

6982
#[derive(Debug, ClapArgs)]
7083
pub struct MethodUpdateArgs {
84+
#[arg(value_name = "ENDPOINT_ID")]
7185
pub id: String,
7286
pub method_rate_limit_id: String,
7387
#[arg(long = "method")]

0 commit comments

Comments
 (0)