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, QN_CLI__API_KEY env var,\n\
25                  ~/.config/qn/config.toml. Run `qn auth login` to save a key the first time.",
26    propagate_version = true,
27    disable_help_subcommand = true
28)]
29pub struct Cli {
30    /// API key. Overrides QN_CLI__API_KEY and the config file.
31    #[arg(long, global = true, env = "QN_CLI__API_KEY", hide_env_values = true)]
32    pub api_key: Option<String>,
33
34    /// Output format. `table` is the default human view; the others are
35    /// pipeline-friendly serialized forms. If unset, falls back to the
36    /// `[output] format = "…"` value in ~/.config/qn/config.toml, then `table`.
37    #[arg(short = 'o', long = "format", global = true, value_enum)]
38    pub format: Option<Format>,
39
40    /// Disable ANSI colors. Also honored: NO_COLOR env var, TERM=dumb, non-TTY stdout.
41    #[arg(long, global = true)]
42    pub no_color: bool,
43
44    /// Suppress non-essential output (state-change confirmations on stderr).
45    #[arg(short, long, global = true)]
46    pub quiet: bool,
47
48    /// Show additional columns in list-style tables (e.g. URLs in `endpoint list`).
49    /// Mirrors `kubectl get -o wide`. Only affects `table` and `md` formats —
50    /// `json`/`yaml`/`toon` always include everything.
51    #[arg(short = 'w', long = "wide", global = true)]
52    pub wide: bool,
53
54    /// Verbose output: include error bodies and other details.
55    #[arg(short, long, global = true)]
56    pub verbose: bool,
57
58    /// Never prompt interactively; fail with a clear message if input is needed.
59    #[arg(long, global = true)]
60    pub no_input: bool,
61
62    /// Skip confirmation prompts. Pass twice for destructive bulk operations like `stream delete-all`.
63    #[arg(short = 'y', long = "yes", global = true, action = ArgAction::Count)]
64    pub yes: u8,
65
66    /// Override the Quicknode API base URL (used for testing or on-prem mirrors).
67    /// All four sub-clients (admin/streams/webhooks/kv) hang off this host.
68    #[arg(long, global = true, hide = true)]
69    pub base_url: Option<String>,
70
71    #[command(subcommand)]
72    pub command: Command,
73}
74
75#[derive(Debug, Subcommand)]
76pub enum Command {
77    /// Manage CLI authentication (API key).
78    Auth(commands::auth::Args),
79
80    /// Manage RPC endpoints on your account.
81    #[command(visible_alias = "endpoints")]
82    Endpoint(commands::endpoint::Args),
83
84    /// Manage account-level tags.
85    #[command(visible_alias = "tags")]
86    Tag(commands::tag::Args),
87
88    /// Manage teams.
89    #[command(visible_alias = "teams")]
90    Team(commands::team::Args),
91
92    /// View account usage.
93    Usage(commands::usage::Args),
94
95    /// View account or endpoint metrics.
96    Metrics(commands::metrics::Args),
97
98    /// List supported blockchains.
99    #[command(visible_alias = "chains")]
100    Chain(commands::chain::Args),
101
102    /// View invoices and payments.
103    Billing(commands::billing::Args),
104
105    /// Bulk operations across many endpoints.
106    Bulk(commands::bulk::Args),
107
108    /// Manage blockchain data streams.
109    #[command(visible_alias = "streams")]
110    Stream(commands::stream::Args),
111
112    /// Manage filter-template webhooks.
113    #[command(visible_alias = "webhooks")]
114    Webhook(commands::webhook::Args),
115
116    /// Manage the Quicknode KV store (sets and lists).
117    Kv(commands::kv::Args),
118
119    /// Generate shell completions.
120    Completions {
121        /// Shell to generate completions for.
122        #[arg(value_enum)]
123        shell: Shell,
124    },
125}
126
127impl Cli {
128    /// Build a [`GlobalArgs`] suitable for [`Ctx::from_global`].
129    pub fn global_args(&self) -> GlobalArgs {
130        GlobalArgs {
131            api_key: self.api_key.clone(),
132            format: self.format,
133            wide: self.wide,
134            // format resolved-from-config in Ctx::from_global; auth.rs falls
135            // back to Table directly if it stays None there.
136            no_color: self.no_color,
137            quiet: self.quiet,
138            verbose: self.verbose,
139            no_input: self.no_input,
140            yes_count: self.yes,
141            base_url: self.base_url.clone(),
142        }
143    }
144
145    /// Dispatch the parsed command.
146    ///
147    /// Some commands (auth, completions) are handled without constructing the
148    /// SDK — they have nothing to talk to and shouldn't trigger an API-key
149    /// prompt.
150    pub async fn run(self) -> Result<(), CliError> {
151        let global = self.global_args();
152        match self.command {
153            Command::Completions { shell } => {
154                let mut cmd = <Self as CommandFactory>::command();
155                let bin_name = cmd.get_name().to_string();
156                let mut out = std::io::stdout().lock();
157                clap_complete::generate(shell, &mut cmd, bin_name, &mut out);
158                out.flush()?;
159                Ok(())
160            }
161            Command::Auth(args) => commands::auth::run(args, global).await,
162            Command::Endpoint(args) => {
163                commands::endpoint::run(args, Ctx::from_global(global)?).await
164            }
165            Command::Tag(args) => commands::tag::run(args, Ctx::from_global(global)?).await,
166            Command::Team(args) => commands::team::run(args, Ctx::from_global(global)?).await,
167            Command::Usage(args) => commands::usage::run(args, Ctx::from_global(global)?).await,
168            Command::Metrics(args) => commands::metrics::run(args, Ctx::from_global(global)?).await,
169            Command::Chain(args) => commands::chain::run(args, Ctx::from_global(global)?).await,
170            Command::Billing(args) => commands::billing::run(args, Ctx::from_global(global)?).await,
171            Command::Bulk(args) => commands::bulk::run(args, Ctx::from_global(global)?).await,
172            Command::Stream(args) => commands::stream::run(args, Ctx::from_global(global)?).await,
173            Command::Webhook(args) => commands::webhook::run(args, Ctx::from_global(global)?).await,
174            Command::Kv(args) => commands::kv::run(args, Ctx::from_global(global)?).await,
175        }
176    }
177}