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    // 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,
33    after_help = "Examples:\n  \
34        qn auth login\n  \
35        qn endpoint create --chain ethereum --network mainnet\n  \
36        qn endpoint list -o json\n  \
37        qn endpoint logs ep-1234 --from 1h\n  \
38        qn chain list\n\n\
39        AI agents: run 'qn agent context' for a machine-readable usage guide.",
40    // Group the global flags under their own heading in every subcommand's
41    // --help, so command-specific flags surface first under "Options".
42    next_help_heading = "Global options"
43)]
44pub struct Cli {
45    /// API key. Overrides the config file.
46    #[arg(long, global = true)]
47    pub api_key: Option<String>,
48
49    /// Path to an alternate config file (default: ~/.config/qn/config.toml).
50    #[arg(long, global = true, value_name = "PATH")]
51    pub config_file: Option<std::path::PathBuf>,
52
53    /// Output format. `table` is the human view; the others are
54    /// pipeline-friendly serialized forms. If unset, falls back to the
55    /// `[output] format = "…"` value in ~/.config/qn/config.toml, then the
56    /// TTY-aware default: `table` when stdout is a terminal, `json` otherwise.
57    #[arg(short = 'o', long = "format", global = true, value_enum)]
58    pub format: Option<Format>,
59
60    /// Disable ANSI colors. Also honored: NO_COLOR env var, TERM=dumb, non-TTY stdout.
61    #[arg(long, global = true)]
62    pub no_color: bool,
63
64    /// Suppress non-essential output (state-change confirmations on stderr).
65    #[arg(short, long, global = true)]
66    pub quiet: bool,
67
68    /// Show additional columns in list-style tables (e.g. URLs in `endpoint list`).
69    /// Mirrors `kubectl get -o wide`. Only affects `table` and `md` formats —
70    /// `json`/`yaml`/`toon` always include everything.
71    #[arg(short = 'w', long = "wide", global = true)]
72    pub wide: bool,
73
74    /// Verbose output: include error bodies and other details.
75    #[arg(short, long, global = true)]
76    pub verbose: bool,
77
78    /// Never prompt interactively; fail with a clear message if input is needed.
79    #[arg(long, global = true)]
80    pub no_input: bool,
81
82    /// Max automatic retries for read-only commands on transient failures
83    /// (HTTP 429/500/502/503/504, timeouts). Uses exponential backoff with
84    /// jitter. 0 disables retries. Commands that modify resources never retry.
85    #[arg(long, global = true, default_value_t = 3, value_name = "N")]
86    pub retries: u32,
87
88    /// Skip confirmation prompts on destructive operations.
89    #[arg(short = 'y', long = "yes", global = true, action = ArgAction::Count)]
90    pub yes: u8,
91
92    /// Override the Quicknode API base URL (used for testing or on-prem mirrors).
93    /// All four sub-clients (admin/streams/webhooks/kv) hang off this host.
94    #[arg(long, global = true, hide = true)]
95    pub base_url: Option<String>,
96
97    /// Path prefix inserted between the host and each sub-client's path (e.g.
98    /// `/console-api`). For reverse-proxy / gateway environments. Requires
99    /// `--base-url`.
100    #[arg(long, global = true, hide = true)]
101    pub base_prefix: Option<String>,
102
103    /// Print help (see a summary with '-h').
104    #[arg(short = 'h', long, global = true, action = ArgAction::Help)]
105    pub help: Option<bool>,
106
107    /// Print version.
108    #[arg(short = 'V', long, global = true, action = ArgAction::Version)]
109    pub version: Option<bool>,
110
111    #[command(subcommand)]
112    pub command: Command,
113}
114
115#[derive(Debug, Subcommand)]
116pub enum Command {
117    /// Manage CLI authentication (API key).
118    Auth(commands::auth::Args),
119
120    /// Resources for AI agents and automated tools.
121    Agent(commands::agent::Args),
122
123    /// Manage RPC endpoints on your account.
124    #[command(visible_alias = "endpoints")]
125    Endpoint(commands::endpoint::Args),
126
127    /// Manage teams.
128    #[command(visible_alias = "teams")]
129    Team(commands::team::Args),
130
131    /// View account usage.
132    Usage(commands::usage::Args),
133
134    /// View account or endpoint metrics.
135    Metrics(commands::metrics::Args),
136
137    /// List supported blockchains.
138    #[command(visible_alias = "chains")]
139    Chain(commands::chain::Args),
140
141    /// View invoices and payments.
142    Billing(commands::billing::Args),
143
144    /// Manage blockchain data streams.
145    #[command(visible_alias = "streams")]
146    Stream(commands::stream::Args),
147
148    /// Manage filter-template webhooks.
149    #[command(visible_alias = "webhooks")]
150    Webhook(commands::webhook::Args),
151
152    /// Manage the Quicknode KV store (sets and lists).
153    Kv(commands::kv::Args),
154
155    /// Run SQL queries and inspect cluster schemas.
156    Sql(commands::sql::Args),
157
158    /// Make RPC calls.
159    Rpc(commands::rpc::Args),
160
161    /// Manage local payment wallets for the paid RPC lane (`--x402`/`--mpp`).
162    #[command(visible_alias = "wallets")]
163    Wallet(commands::wallet::Args),
164
165    /// Manage Tooling Access (the endpoint `qn rpc` uses).
166    #[command(name = "tooling-access")]
167    ToolingAccess(commands::tooling_access::Args),
168
169    /// Generate shell completion scripts.
170    ///
171    /// When installing qn through a package manager, it's possible that no
172    /// additional shell configuration is necessary to gain completion support.
173    /// Homebrew and distro packages place the script for you.
174    ///
175    /// If you need to set up completions manually, follow the instructions
176    /// below. The exact config file locations might vary based on your system.
177    /// Make sure to restart your shell before testing whether completions are
178    /// working.
179    #[command(after_long_help = "### bash\n\n  \
180        First, ensure that you install `bash-completion` using your package manager.\n\n  \
181        After, add this to your `~/.bashrc`:\n\n      \
182        eval \"$(qn completions bash)\"\n\n\
183        ### zsh\n\n  \
184        Homebrew already creates this `_qn` file for you on `brew install`. To\n  \
185        set it up manually, generate the script into a directory on your\n  \
186        `$fpath` (Apple Silicon shown; Intel brew uses\n  \
187        `/usr/local/share/zsh/site-functions`):\n\n      \
188        qn completions zsh > /opt/homebrew/share/zsh/site-functions/_qn\n\n  \
189        Ensure that the following is present in your `~/.zshrc`:\n\n      \
190        autoload -U compinit\n      \
191        compinit\n\n  \
192        See the zsh manual for details:\n  \
193        https://zsh.sourceforge.io/Doc/Release/Completion-System.html\n\n\
194        ### fish\n\n  \
195        Generate a `qn.fish` completion script:\n\n      \
196        qn completions fish > ~/.config/fish/completions/qn.fish\n\n\
197        ### PowerShell\n\n  \
198        Add the following line to your profile script (`$PROFILE`):\n\n      \
199        qn completions powershell | Out-String | Invoke-Expression\n\n  \
200        Or append the generated script so it loads each session:\n\n      \
201        qn completions powershell >> $PROFILE")]
202    Completions {
203        /// Shell to generate completions for.
204        #[arg(value_enum)]
205        shell: Shell,
206    },
207}
208
209impl Cli {
210    /// Build a [`GlobalArgs`] suitable for [`Ctx::from_global`].
211    pub fn global_args(&self) -> GlobalArgs {
212        GlobalArgs {
213            api_key: self.api_key.clone(),
214            config_file: self.config_file.clone(),
215            format: self.format,
216            wide: self.wide,
217            // Resolve format defaults in the context.
218            no_color: self.no_color,
219            quiet: self.quiet,
220            verbose: self.verbose,
221            no_input: self.no_input,
222            yes_count: self.yes,
223            retries: self.retries,
224            base_url: self.base_url.clone(),
225            base_prefix: self.base_prefix.clone(),
226        }
227    }
228
229    /// Dispatch the parsed command.
230    ///
231    /// Some commands (auth, completions) are handled without constructing the
232    /// SDK — they have nothing to talk to and shouldn't trigger an API-key
233    /// prompt.
234    pub async fn run(self) -> Result<(), CliError> {
235        let global = self.global_args();
236        match self.command {
237            Command::Completions { shell } => {
238                let mut cmd = <Self as CommandFactory>::command();
239                let bin_name = cmd.get_name().to_string();
240                let mut out = std::io::stdout().lock();
241                clap_complete::generate(shell, &mut cmd, bin_name, &mut out);
242                out.flush()?;
243                Ok(())
244            }
245            Command::Auth(args) => commands::auth::run(args, global).await,
246            Command::Agent(args) => commands::agent::run(args, global).await,
247            Command::Endpoint(args) => {
248                commands::endpoint::run(args, Ctx::from_global(global)?).await
249            }
250            Command::Team(args) => commands::team::run(args, Ctx::from_global(global)?).await,
251            Command::Usage(args) => commands::usage::run(args, Ctx::from_global(global)?).await,
252            Command::Metrics(args) => commands::metrics::run(args, Ctx::from_global(global)?).await,
253            Command::Chain(args) => commands::chain::run(args, Ctx::from_global(global)?).await,
254            Command::Billing(args) => commands::billing::run(args, Ctx::from_global(global)?).await,
255            Command::Stream(args) => commands::stream::run(args, Ctx::from_global(global)?).await,
256            Command::Webhook(args) => commands::webhook::run(args, Ctx::from_global(global)?).await,
257            Command::Kv(args) => commands::kv::run(args, Ctx::from_global(global)?).await,
258            Command::Sql(args) => commands::sql::run(args, Ctx::from_global(global)?).await,
259            // RPC resolves its own context for token-cache seeding.
260            Command::Rpc(args) => commands::rpc::run(args, global).await,
261            // Wallet management is local and keyless.
262            Command::Wallet(args) => {
263                commands::wallet::run(args, Ctx::from_global_keyless(global)?).await
264            }
265            Command::ToolingAccess(args) => {
266                commands::tooling_access::run(args, Ctx::from_global(global)?).await
267            }
268        }
269    }
270}