Skip to content

Commit 00ad0b3

Browse files
committed
Truthful help and exit codes; grouped global flags; examples
- Required flags are now clap-enforced and shown in usage lines: endpoint create (--chain/--network), endpoint update (--label), and the six stream create flags via required_unless_present(config_file). Missing-flag errors now list everything missing at once instead of failing one runtime check at a time. - clap usage errors (typo'd subcommand, missing required flag) exit 1, matching runtime argument errors, so exit 2 always and only means "the API returned an error". --help/--version still exit 0. - Global flags are grouped under a "Global options" heading in every subcommand's --help, so command-specific flags surface first. - after_help examples on the top level and on endpoint/stream/webhook/ kv/auth. - README exit-code table updated.
1 parent b985a86 commit 00ad0b3

12 files changed

Lines changed: 106 additions & 61 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ re-running it.
240240
| Code | Meaning |
241241
|---|---|
242242
| 0 | Success |
243-
| 1 | CLI error (bad argument, IO, decode) |
243+
| 1 | CLI error (usage/bad argument, IO, decode) |
244244
| 2 | API error (server returned 4xx/5xx) |
245245
| 3 | Network failure (timeout, connect, transport) |
246246
| 4 | Missing or invalid API key / config |

src/cli.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,16 @@ use crate::output::Format;
2525
(--config-file path if given, else ~/.config/qn/config.toml). Run `qn auth login`\n\
2626
to save a key the first time.",
2727
propagate_version = true,
28-
disable_help_subcommand = true
28+
disable_help_subcommand = true,
29+
after_help = "Examples:\n \
30+
qn auth login\n \
31+
qn endpoint create --chain ethereum --network mainnet\n \
32+
qn endpoint list -o json\n \
33+
qn endpoint logs ep-1234 --from 1h\n \
34+
qn chain list",
35+
// Group the global flags under their own heading in every subcommand's
36+
// --help, so command-specific flags surface first under "Options".
37+
next_help_heading = "Global options"
2938
)]
3039
pub struct Cli {
3140
/// API key. Overrides the config file.

src/commands/auth.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ use crate::context::GlobalArgs;
1616
use crate::errors::CliError;
1717

1818
#[derive(Debug, ClapArgs)]
19+
#[command(after_help = "Examples:\n \
20+
qn auth login # prompts for the key (hidden input)\n \
21+
qn auth login --api-key <KEY> # non-interactive (e.g. CI)\n \
22+
qn auth whoami # verify the key against the API\n \
23+
qn --config-file ./ci.toml auth status")]
1924
pub struct Args {
2025
#[command(subcommand)]
2126
pub cmd: AuthCmd,

src/commands/endpoint/mod.rs

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ pub use security::SecurityCmd;
2424
pub use tag::TagCmd;
2525

2626
#[derive(Debug, ClapArgs)]
27+
#[command(after_help = "Examples:\n \
28+
qn endpoint create --chain ethereum --network mainnet\n \
29+
qn endpoint list -o json\n \
30+
qn endpoint logs ep-1234 --from 1h --details\n \
31+
qn endpoint pause ep-1234\n \
32+
qn endpoint bulk pause ep-1 ep-2 --yes")]
2733
pub struct Args {
2834
#[command(subcommand)]
2935
pub cmd: EndpointCmd,
@@ -123,12 +129,12 @@ pub struct ListArgs {
123129

124130
#[derive(Debug, ClapArgs)]
125131
pub struct CreateArgs {
126-
/// Blockchain (e.g. `ethereum`, `solana`).
132+
/// Blockchain (e.g. `ethereum`, `solana`; run `qn chain list` to see all).
127133
#[arg(long)]
128-
pub chain: Option<String>,
134+
pub chain: String,
129135
/// Network (e.g. `mainnet`).
130136
#[arg(long)]
131-
pub network: Option<String>,
137+
pub network: String,
132138
}
133139

134140
#[derive(Debug, ClapArgs)]
@@ -137,7 +143,7 @@ pub struct UpdateArgs {
137143
pub id: String,
138144
/// New label.
139145
#[arg(long)]
140-
pub label: Option<String>,
146+
pub label: String,
141147
}
142148

143149
#[derive(Debug, ClapArgs)]
@@ -232,22 +238,9 @@ async fn list(a: ListArgs, ctx: Ctx) -> Result<(), CliError> {
232238
}
233239

234240
async fn create(a: CreateArgs, ctx: Ctx) -> Result<(), CliError> {
235-
let missing: Vec<&str> = [
236-
("--chain", a.chain.is_none()),
237-
("--network", a.network.is_none()),
238-
]
239-
.iter()
240-
.filter_map(|(name, missing)| if *missing { Some(*name) } else { None })
241-
.collect();
242-
if !missing.is_empty() {
243-
return Err(CliError::Arg(format!(
244-
"'endpoint create' requires {}. Run 'qn chain list' to see available chains.",
245-
missing.join(" and "),
246-
)));
247-
}
248241
let req = CreateEndpointRequest {
249-
chain: a.chain,
250-
network: a.network,
242+
chain: Some(a.chain),
243+
network: Some(a.network),
251244
};
252245
let resp = ctx.sdk.admin.create_endpoint(&req).await?;
253246
ctx.out
@@ -264,10 +257,9 @@ async fn show(id: &str, ctx: Ctx) -> Result<(), CliError> {
264257
}
265258

266259
async fn update(a: UpdateArgs, ctx: Ctx) -> Result<(), CliError> {
267-
if a.label.is_none() {
268-
return Err(CliError::Arg("'endpoint update' requires --label.".into()));
269-
}
270-
let req = UpdateEndpointRequest { label: a.label };
260+
let req = UpdateEndpointRequest {
261+
label: Some(a.label),
262+
};
271263
ctx.sdk.admin.update_endpoint(&a.id, &req).await?;
272264
ctx.out.note(&format!("✓ Updated endpoint {}", a.id));
273265
Ok(())

src/commands/kv/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ use crate::context::Ctx;
1111
use crate::errors::CliError;
1212

1313
#[derive(Debug, ClapArgs)]
14+
#[command(after_help = "Examples:\n \
15+
qn kv set put mykey myvalue\n \
16+
qn kv set get mykey\n \
17+
qn kv list create mylist item1 item2\n \
18+
qn kv list contains mylist item1")]
1419
pub struct Args {
1520
#[command(subcommand)]
1621
pub cmd: KvCmd,

src/commands/stream/actions.rs

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,27 +39,16 @@ pub(super) async fn create(a: CreateArgs, ctx: Ctx) -> Result<(), CliError> {
3939
}
4040

4141
fn build_create_params(a: CreateArgs) -> Result<CreateStreamParams, CliError> {
42-
let name = a
43-
.name
44-
.ok_or_else(|| CliError::Arg("--name is required".into()))?;
45-
let network = a
46-
.network
47-
.ok_or_else(|| CliError::Arg("--network is required".into()))?;
48-
let dataset = a
49-
.dataset
50-
.ok_or_else(|| CliError::Arg("--dataset is required".into()))?;
51-
let start = a
52-
.start
53-
.ok_or_else(|| CliError::Arg("--start is required".into()))?;
54-
let end = a
55-
.end
56-
.ok_or_else(|| CliError::Arg("--end is required (-1 for continuous)".into()))?;
57-
let region = a
58-
.region
59-
.ok_or_else(|| CliError::Arg("--region is required".into()))?;
60-
let url = a.webhook.ok_or_else(|| {
61-
CliError::Arg("--webhook is required (or use --config-file for other destinations)".into())
62-
})?;
42+
// These flags are `required_unless_present = "config_file"` in clap, and
43+
// this function is only reached when --config-file was NOT supplied.
44+
const ENFORCED: &str = "enforced by clap unless --config-file is present";
45+
let name = a.name.expect(ENFORCED);
46+
let network = a.network.expect(ENFORCED);
47+
let dataset = a.dataset.expect(ENFORCED);
48+
let start = a.start.expect(ENFORCED);
49+
let end = a.end.expect(ENFORCED);
50+
let region = a.region.expect(ENFORCED);
51+
let url = a.webhook.expect(ENFORCED);
6352

6453
let filter_function = match (a.filter, a.filter_file) {
6554
(Some(s), None) => Some(STANDARD.encode(s)),

src/commands/stream/mod.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ use crate::context::Ctx;
1818
use crate::errors::CliError;
1919

2020
#[derive(Debug, ClapArgs)]
21+
#[command(after_help = "Examples:\n \
22+
qn stream list --limit 20\n \
23+
qn stream create --name blocks --network ethereum-mainnet --dataset block \\\n \
24+
--start 24691804 --end=-1 --region usa-east --webhook https://hook.example.com\n \
25+
qn stream create --config-file stream.json\n \
26+
qn stream pause s-1234")]
2127
pub struct Args {
2228
#[command(subcommand)]
2329
pub cmd: StreamCmd,
@@ -72,28 +78,33 @@ pub struct CreateArgs {
7278
pub config_file: Option<PathBuf>,
7379

7480
/// Stream name.
75-
#[arg(long)]
81+
#[arg(long, required_unless_present = "config_file")]
7682
pub name: Option<String>,
7783
/// Network (e.g. `ethereum-mainnet`).
78-
#[arg(long)]
84+
#[arg(long, required_unless_present = "config_file")]
7985
pub network: Option<String>,
8086
/// Dataset (snake_case).
81-
#[arg(long, value_enum)]
87+
#[arg(long, value_enum, required_unless_present = "config_file")]
8288
pub dataset: Option<DatasetArg>,
8389
/// Start block.
84-
#[arg(long)]
90+
#[arg(long, required_unless_present = "config_file")]
8591
pub start: Option<i64>,
8692
/// End block (`-1` for continuous).
87-
#[arg(long, allow_hyphen_values = true)]
93+
#[arg(
94+
long,
95+
allow_hyphen_values = true,
96+
required_unless_present = "config_file"
97+
)]
8898
pub end: Option<i64>,
8999
/// Region.
90-
#[arg(long, value_enum)]
100+
#[arg(long, value_enum, required_unless_present = "config_file")]
91101
pub region: Option<RegionArg>,
92102
/// Billing plan slug (optional).
93103
#[arg(long)]
94104
pub plan: Option<String>,
95-
/// Webhook URL for the primary destination.
96-
#[arg(long)]
105+
/// Webhook URL for the primary destination (use `--config-file` for other
106+
/// destination types).
107+
#[arg(long, required_unless_present = "config_file")]
97108
pub webhook: Option<String>,
98109
/// Webhook security token (32-byte minimum). Optional.
99110
#[arg(long)]

src/commands/webhook/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ use crate::context::Ctx;
1212
use crate::errors::CliError;
1313

1414
#[derive(Debug, ClapArgs)]
15+
#[command(after_help = "Examples:\n \
16+
qn webhook list\n \
17+
qn webhook create --name alerts --network ethereum-mainnet \\\n \
18+
--url https://hook.example.com --template evmWalletFilter --wallet 0xabc\n \
19+
qn webhook pause wh-1234")]
1520
pub struct Args {
1621
#[command(subcommand)]
1722
pub cmd: WebhookCmd,

src/errors.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ pub enum CliError {
5353
/// Maps a [`CliError`] to a process exit code per the plan.
5454
///
5555
/// - 0: success (never produced here)
56-
/// - 1: generic CLI failure (arg parse, IO, decode)
56+
/// - 1: generic CLI failure (arg parse, IO, decode). clap usage errors are
57+
/// mapped to 1 in main.rs too, so 2 always and only means an API error.
5758
/// - 2: SdkError::Api (server returned a non-2xx)
5859
/// - 3: SdkError::Http (network failure)
5960
/// - 4: NoApiKey / BadConfig

src/main.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,21 @@ use qn::errors::{exit_code_for, render};
88

99
#[tokio::main]
1010
async fn main() -> ExitCode {
11-
let cli = Cli::parse();
11+
// Map clap usage errors to exit 1 — the same bucket as runtime argument
12+
// errors — so exit 2 always and only means "the API returned an error".
13+
// (clap's own Error::exit would use 2 for both.)
14+
let cli = match Cli::try_parse() {
15+
Ok(cli) => cli,
16+
Err(e) => {
17+
// --help/--version print to stdout and are not errors.
18+
let _ = e.print();
19+
return if e.use_stderr() {
20+
ExitCode::from(1)
21+
} else {
22+
ExitCode::SUCCESS
23+
};
24+
}
25+
};
1226
let verbose = cli.verbose;
1327
match cli.run().await {
1428
Ok(()) => ExitCode::SUCCESS,

0 commit comments

Comments
 (0)