Skip to main content

zad_cli/cli/
mod.rs

1pub mod commands;
2pub mod debug_agent;
3pub mod discord;
4pub mod docs;
5pub mod echo;
6pub mod gcal;
7pub mod help_agent;
8pub mod lifecycle;
9pub mod man;
10pub mod onepass;
11pub mod permissions;
12pub mod service;
13pub mod service_discord;
14pub mod service_gcal;
15pub mod service_list;
16pub mod service_onepass;
17pub mod service_slack;
18pub mod service_spotify;
19pub mod service_status;
20pub mod service_telegram;
21pub mod service_ymusic;
22pub mod signing;
23pub mod slack;
24pub mod spotify;
25pub mod telegram;
26pub mod ymusic;
27
28use clap::{Parser, Subcommand};
29
30use zad::error::Result;
31
32/// Map a `dialoguer::Error` into [`zad::ZadError::Prompt`]. The library
33/// no longer depends on `dialoguer`, so the orphan rule prevents a
34/// blanket `From` impl in this crate; this trait gives every
35/// `.interact()` call site a one-method shorthand instead.
36pub(crate) trait DialoguerExt<T> {
37    fn into_zad(self) -> Result<T>;
38}
39
40impl<T> DialoguerExt<T> for std::result::Result<T, dialoguer::Error> {
41    fn into_zad(self) -> Result<T> {
42        self.map_err(|e| zad::ZadError::Prompt(e.to_string()))
43    }
44}
45
46#[derive(Debug, Parser)]
47#[command(
48    name = "zad",
49    version,
50    about = "Connect AI agents to external services via scoped service configs.",
51    disable_help_subcommand = true,
52    propagate_version = true
53)]
54pub struct Cli {
55    /// Enable debug-level logging on stderr (file log is always on).
56    #[arg(long, global = true)]
57    pub debug: bool,
58
59    /// Print a compact, prompt-injectable description of this CLI suitable
60    /// for splicing into an agent prompt via command substitution. See
61    /// OSS_SPEC.md §12.1.
62    #[arg(long, global = true)]
63    pub help_agent: bool,
64
65    /// Print a troubleshooting block (log paths, config precedence, env
66    /// vars, diagnostic commands, version). See OSS_SPEC.md §12.2.
67    #[arg(long, global = true)]
68    pub debug_agent: bool,
69
70    /// Block until any persisted rate-limit (HTTP 429) wait window
71    /// passes, then continue. Safe to leave on permanently in
72    /// scripts: if no wait window is active, this is a no-op.
73    #[arg(long, global = true)]
74    pub wait: bool,
75
76    #[command(subcommand)]
77    pub command: Option<Command>,
78}
79
80#[derive(Debug, Subcommand)]
81pub enum Command {
82    /// Configure or inspect external services.
83    Service(service::ServiceArgs),
84    /// Operate the 1Password service (vaults, items, get, read, inject, create).
85    #[command(name = "1pass")]
86    OnePass(onepass::OnePassArgs),
87    /// Operate the Discord service (send, read, channels, join, leave).
88    Discord(discord::DiscordArgs),
89    /// Operate the Google Calendar service (calendars, events, permissions).
90    Gcal(gcal::GcalArgs),
91    /// Operate the Slack service (send, read, channels, discover).
92    Slack(slack::SlackArgs),
93    /// Operate the Spotify service (search, playlists, library).
94    Spotify(spotify::SpotifyArgs),
95    /// Operate the Telegram service (send, read, chats, discover).
96    Telegram(telegram::TelegramArgs),
97    /// Operate the YouTube Music service (search, playlists, library).
98    Ymusic(ymusic::YmusicArgs),
99    /// Manage the local signing key and trust store.
100    Signing(signing::SigningArgs),
101    /// Enumerate CLI commands, flags, and realistic examples.
102    Commands(commands::CommandsArgs),
103    /// Print topic documentation embedded at build time.
104    Docs(docs::DocsArgs),
105    /// Print reference manpages embedded at build time.
106    Man(man::ManArgs),
107}
108
109/// Service name attached to a top-level command, or `None` for commands
110/// that don't hit external APIs (lifecycle, signing, docs, …). Used to
111/// gate dispatch on any persisted rate-limit window.
112fn rate_limit_service_for(cmd: &Command) -> Option<&'static str> {
113    match cmd {
114        Command::Discord(_) => Some("discord"),
115        Command::Gcal(_) => Some("gcal"),
116        Command::Slack(_) => Some("slack"),
117        Command::Spotify(_) => Some("spotify"),
118        Command::Telegram(_) => Some("telegram"),
119        Command::Ymusic(_) => Some("ymusic"),
120        // 1Password shells out to the `op` CLI rather than calling
121        // an HTTP API; no rate-limit gating needed. Service /
122        // signing / commands / docs / man are local-only.
123        Command::OnePass(_)
124        | Command::Service(_)
125        | Command::Signing(_)
126        | Command::Commands(_)
127        | Command::Docs(_)
128        | Command::Man(_) => None,
129    }
130}
131
132pub async fn run() -> Result<()> {
133    let cli = Cli::parse();
134    zad::logging::init(cli.debug);
135
136    if cli.help_agent {
137        print!("{}", help_agent::render());
138        return Ok(());
139    }
140
141    if cli.debug_agent {
142        print!("{}", debug_agent::render());
143        return Ok(());
144    }
145
146    if let Some(cmd) = cli.command.as_ref()
147        && let Some(svc) = rate_limit_service_for(cmd)
148    {
149        zad::rate_limit::precall_check(svc, cli.wait).await?;
150    }
151
152    match cli.command {
153        Some(Command::Service(args)) => service::run(args).await,
154        Some(Command::OnePass(args)) => onepass::run(args).await,
155        Some(Command::Discord(args)) => discord::run(args).await,
156        Some(Command::Gcal(args)) => gcal::run(args).await,
157        Some(Command::Slack(args)) => slack::run(args).await,
158        Some(Command::Spotify(args)) => spotify::run(args).await,
159        Some(Command::Telegram(args)) => telegram::run(args).await,
160        Some(Command::Ymusic(args)) => ymusic::run(args).await,
161        Some(Command::Signing(args)) => signing::run(args),
162        Some(Command::Commands(args)) => commands::run(args),
163        Some(Command::Docs(args)) => docs::run(args),
164        Some(Command::Man(args)) => man::run(args),
165        None => {
166            println!("zad {}", zad::version());
167            println!("Run `zad --help` for usage.");
168            Ok(())
169        }
170    }
171}