Skip to main content

qn/
cli.rs

1//! Top-level clap derive entry point.
2//!
3//! This file is the single source of truth for the CLI shape. Subcommand
4//! bodies live under `commands::*` and dispatch happens via [`Cli::run`].
5
6use std::io::Write;
7
8use clap::{ArgAction, CommandFactory, Parser, Subcommand};
9use clap_complete::Shell;
10
11use crate::commands;
12use crate::context::{Ctx, GlobalArgs};
13use crate::errors::CliError;
14use crate::output::Format;
15
16/// qn — command-line interface for the Quicknode API.
17#[derive(Debug, Parser)]
18#[command(
19    name = "qn",
20    version,
21    about = "Command-line interface for the Quicknode API.",
22    long_about = "qn lets you manage Quicknode endpoints, streams, webhooks, and the KV store from the terminal.\n\n\
23                  Use `qn <noun> --help` (e.g. `qn endpoint --help`) for command details.\n\n\
24                  Authentication is resolved in this order: --api-key flag, then the config file\n\
25                  (--config-file path if given, else ~/.config/qn/config.toml). Run `qn auth login`\n\
26                  to save a key the first time.",
27    propagate_version = 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"
38)]
39pub struct Cli {
40    /// API key. Overrides the config file.
41    #[arg(long, global = true)]
42    pub api_key: Option<String>,
43
44    /// Path to an alternate config file (default: ~/.config/qn/config.toml).
45    #[arg(long, global = true, value_name = "PATH")]
46    pub config_file: Option<std::path::PathBuf>,
47
48    /// Output format. `table` is the default human view; the others are
49    /// pipeline-friendly serialized forms. If unset, falls back to the
50    /// `[output] format = "…"` value in ~/.config/qn/config.toml, then `table`.
51    #[arg(short = 'o', long = "format", global = true, value_enum)]
52    pub format: Option<Format>,
53
54    /// Disable ANSI colors. Also honored: NO_COLOR env var, TERM=dumb, non-TTY stdout.
55    #[arg(long, global = true)]
56    pub no_color: bool,
57
58    /// Suppress non-essential output (state-change confirmations on stderr).
59    #[arg(short, long, global = true)]
60    pub quiet: bool,
61
62    /// Show additional columns in list-style tables (e.g. URLs in `endpoint list`).
63    /// Mirrors `kubectl get -o wide`. Only affects `table` and `md` formats —
64    /// `json`/`yaml`/`toon` always include everything.
65    #[arg(short = 'w', long = "wide", global = true)]
66    pub wide: bool,
67
68    /// Verbose output: include error bodies and other details.
69    #[arg(short, long, global = true)]
70    pub verbose: bool,
71
72    /// Never prompt interactively; fail with a clear message if input is needed.
73    #[arg(long, global = true)]
74    pub no_input: bool,
75
76    /// Max automatic retries for read-only commands on transient failures
77    /// (HTTP 429/500/502/503/504, timeouts). Uses exponential backoff with
78    /// jitter. 0 disables retries. Commands that modify resources never retry.
79    #[arg(long, global = true, default_value_t = 3, value_name = "N")]
80    pub retries: u32,
81
82    /// Skip confirmation prompts on destructive operations.
83    #[arg(short = 'y', long = "yes", global = true, action = ArgAction::Count)]
84    pub yes: u8,
85
86    /// Override the Quicknode API base URL (used for testing or on-prem mirrors).
87    /// All four sub-clients (admin/streams/webhooks/kv) hang off this host.
88    #[arg(long, global = true, hide = true)]
89    pub base_url: Option<String>,
90
91    #[command(subcommand)]
92    pub command: Command,
93}
94
95#[derive(Debug, Subcommand)]
96pub enum Command {
97    /// Manage CLI authentication (API key).
98    Auth(commands::auth::Args),
99
100    /// Manage RPC endpoints on your account.
101    #[command(visible_alias = "endpoints")]
102    Endpoint(commands::endpoint::Args),
103
104    /// Manage teams.
105    #[command(visible_alias = "teams")]
106    Team(commands::team::Args),
107
108    /// View account usage.
109    Usage(commands::usage::Args),
110
111    /// View account or endpoint metrics.
112    Metrics(commands::metrics::Args),
113
114    /// List supported blockchains.
115    #[command(visible_alias = "chains")]
116    Chain(commands::chain::Args),
117
118    /// View invoices and payments.
119    Billing(commands::billing::Args),
120
121    /// Manage blockchain data streams.
122    #[command(visible_alias = "streams")]
123    Stream(commands::stream::Args),
124
125    /// Manage filter-template webhooks.
126    #[command(visible_alias = "webhooks")]
127    Webhook(commands::webhook::Args),
128
129    /// Manage the Quicknode KV store (sets and lists).
130    Kv(commands::kv::Args),
131
132    /// Generate shell completions.
133    Completions {
134        /// Shell to generate completions for.
135        #[arg(value_enum)]
136        shell: Shell,
137    },
138}
139
140impl Cli {
141    /// Build a [`GlobalArgs`] suitable for [`Ctx::from_global`].
142    pub fn global_args(&self) -> GlobalArgs {
143        GlobalArgs {
144            api_key: self.api_key.clone(),
145            config_file: self.config_file.clone(),
146            format: self.format,
147            wide: self.wide,
148            // format resolved-from-config in Ctx::from_global; auth.rs falls
149            // back to Table directly if it stays None there.
150            no_color: self.no_color,
151            quiet: self.quiet,
152            verbose: self.verbose,
153            no_input: self.no_input,
154            yes_count: self.yes,
155            retries: self.retries,
156            base_url: self.base_url.clone(),
157        }
158    }
159
160    /// Dispatch the parsed command.
161    ///
162    /// Some commands (auth, completions) are handled without constructing the
163    /// SDK — they have nothing to talk to and shouldn't trigger an API-key
164    /// prompt.
165    pub async fn run(self) -> Result<(), CliError> {
166        let global = self.global_args();
167        match self.command {
168            Command::Completions { shell } => {
169                let mut cmd = <Self as CommandFactory>::command();
170                let bin_name = cmd.get_name().to_string();
171                let mut out = std::io::stdout().lock();
172                clap_complete::generate(shell, &mut cmd, bin_name, &mut out);
173                out.flush()?;
174                Ok(())
175            }
176            Command::Auth(args) => commands::auth::run(args, global).await,
177            Command::Endpoint(args) => {
178                commands::endpoint::run(args, Ctx::from_global(global)?).await
179            }
180            Command::Team(args) => commands::team::run(args, Ctx::from_global(global)?).await,
181            Command::Usage(args) => commands::usage::run(args, Ctx::from_global(global)?).await,
182            Command::Metrics(args) => commands::metrics::run(args, Ctx::from_global(global)?).await,
183            Command::Chain(args) => commands::chain::run(args, Ctx::from_global(global)?).await,
184            Command::Billing(args) => commands::billing::run(args, Ctx::from_global(global)?).await,
185            Command::Stream(args) => commands::stream::run(args, Ctx::from_global(global)?).await,
186            Command::Webhook(args) => commands::webhook::run(args, Ctx::from_global(global)?).await,
187            Command::Kv(args) => commands::kv::run(args, Ctx::from_global(global)?).await,
188        }
189    }
190}