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 default 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 `table`.
56    #[arg(short = 'o', long = "format", global = true, value_enum)]
57    pub format: Option<Format>,
58
59    /// Disable ANSI colors. Also honored: NO_COLOR env var, TERM=dumb, non-TTY stdout.
60    #[arg(long, global = true)]
61    pub no_color: bool,
62
63    /// Suppress non-essential output (state-change confirmations on stderr).
64    #[arg(short, long, global = true)]
65    pub quiet: bool,
66
67    /// Show additional columns in list-style tables (e.g. URLs in `endpoint list`).
68    /// Mirrors `kubectl get -o wide`. Only affects `table` and `md` formats —
69    /// `json`/`yaml`/`toon` always include everything.
70    #[arg(short = 'w', long = "wide", global = true)]
71    pub wide: bool,
72
73    /// Verbose output: include error bodies and other details.
74    #[arg(short, long, global = true)]
75    pub verbose: bool,
76
77    /// Never prompt interactively; fail with a clear message if input is needed.
78    #[arg(long, global = true)]
79    pub no_input: bool,
80
81    /// Max automatic retries for read-only commands on transient failures
82    /// (HTTP 429/500/502/503/504, timeouts). Uses exponential backoff with
83    /// jitter. 0 disables retries. Commands that modify resources never retry.
84    #[arg(long, global = true, default_value_t = 3, value_name = "N")]
85    pub retries: u32,
86
87    /// Skip confirmation prompts on destructive operations.
88    #[arg(short = 'y', long = "yes", global = true, action = ArgAction::Count)]
89    pub yes: u8,
90
91    /// Override the Quicknode API base URL (used for testing or on-prem mirrors).
92    /// All four sub-clients (admin/streams/webhooks/kv) hang off this host.
93    #[arg(long, global = true, hide = true)]
94    pub base_url: Option<String>,
95
96    /// Print help (see a summary with '-h').
97    #[arg(short = 'h', long, global = true, action = ArgAction::Help)]
98    pub help: Option<bool>,
99
100    /// Print version.
101    #[arg(short = 'V', long, global = true, action = ArgAction::Version)]
102    pub version: Option<bool>,
103
104    #[command(subcommand)]
105    pub command: Command,
106}
107
108#[derive(Debug, Subcommand)]
109pub enum Command {
110    /// Manage CLI authentication (API key).
111    Auth(commands::auth::Args),
112
113    /// Resources for AI agents and automated tools.
114    Agent(commands::agent::Args),
115
116    /// Manage RPC endpoints on your account.
117    #[command(visible_alias = "endpoints")]
118    Endpoint(commands::endpoint::Args),
119
120    /// Manage teams.
121    #[command(visible_alias = "teams")]
122    Team(commands::team::Args),
123
124    /// View account usage.
125    Usage(commands::usage::Args),
126
127    /// View account or endpoint metrics.
128    Metrics(commands::metrics::Args),
129
130    /// List supported blockchains.
131    #[command(visible_alias = "chains")]
132    Chain(commands::chain::Args),
133
134    /// View invoices and payments.
135    Billing(commands::billing::Args),
136
137    /// Manage blockchain data streams.
138    #[command(visible_alias = "streams")]
139    Stream(commands::stream::Args),
140
141    /// Manage filter-template webhooks.
142    #[command(visible_alias = "webhooks")]
143    Webhook(commands::webhook::Args),
144
145    /// Manage the Quicknode KV store (sets and lists).
146    Kv(commands::kv::Args),
147
148    /// Generate shell completions.
149    Completions {
150        /// Shell to generate completions for.
151        #[arg(value_enum)]
152        shell: Shell,
153    },
154}
155
156impl Cli {
157    /// Build a [`GlobalArgs`] suitable for [`Ctx::from_global`].
158    pub fn global_args(&self) -> GlobalArgs {
159        GlobalArgs {
160            api_key: self.api_key.clone(),
161            config_file: self.config_file.clone(),
162            format: self.format,
163            wide: self.wide,
164            // format resolved-from-config in Ctx::from_global; auth.rs falls
165            // back to Table directly if it stays None there.
166            no_color: self.no_color,
167            quiet: self.quiet,
168            verbose: self.verbose,
169            no_input: self.no_input,
170            yes_count: self.yes,
171            retries: self.retries,
172            base_url: self.base_url.clone(),
173        }
174    }
175
176    /// Dispatch the parsed command.
177    ///
178    /// Some commands (auth, completions) are handled without constructing the
179    /// SDK — they have nothing to talk to and shouldn't trigger an API-key
180    /// prompt.
181    pub async fn run(self) -> Result<(), CliError> {
182        let global = self.global_args();
183        match self.command {
184            Command::Completions { shell } => {
185                let mut cmd = <Self as CommandFactory>::command();
186                let bin_name = cmd.get_name().to_string();
187                let mut out = std::io::stdout().lock();
188                clap_complete::generate(shell, &mut cmd, bin_name, &mut out);
189                out.flush()?;
190                Ok(())
191            }
192            Command::Auth(args) => commands::auth::run(args, global).await,
193            Command::Agent(args) => commands::agent::run(args, global).await,
194            Command::Endpoint(args) => {
195                commands::endpoint::run(args, Ctx::from_global(global)?).await
196            }
197            Command::Team(args) => commands::team::run(args, Ctx::from_global(global)?).await,
198            Command::Usage(args) => commands::usage::run(args, Ctx::from_global(global)?).await,
199            Command::Metrics(args) => commands::metrics::run(args, Ctx::from_global(global)?).await,
200            Command::Chain(args) => commands::chain::run(args, Ctx::from_global(global)?).await,
201            Command::Billing(args) => commands::billing::run(args, Ctx::from_global(global)?).await,
202            Command::Stream(args) => commands::stream::run(args, Ctx::from_global(global)?).await,
203            Command::Webhook(args) => commands::webhook::run(args, Ctx::from_global(global)?).await,
204            Command::Kv(args) => commands::kv::run(args, Ctx::from_global(global)?).await,
205        }
206    }
207}