Skip to main content

zad_cli/cli/
service.rs

1//! Dispatch for `zad service <action> <name>`.
2//!
3//! Each action is a thin clap enum: one variant per service that
4//! routes to the generic `lifecycle::run_*::<T>()` driver with the
5//! service's `LifecycleService` impl as the type parameter. Adding a
6//! new service means adding one variant to each enum below plus one
7//! dispatch arm in `run()` — about 10 lines total.
8
9use clap::{Args, Subcommand, builder::PossibleValuesParser};
10
11use crate::cli::lifecycle::{self, DeleteArgs, DisableArgs, EnableArgs, ShowArgs};
12use zad::error::Result;
13use zad::service::registry::SERVICES;
14
15use super::{
16    service_discord, service_gcal, service_list, service_onepass, service_slack, service_spotify,
17    service_status, service_telegram, service_ymusic,
18};
19use service_discord::DiscordLifecycle;
20use service_gcal::GcalLifecycle;
21use service_onepass::OnePassLifecycle;
22use service_slack::SlackLifecycle;
23use service_spotify::SpotifyLifecycle;
24use service_telegram::TelegramLifecycle;
25use service_ymusic::YmusicLifecycle;
26
27#[derive(Debug, Args)]
28pub struct ServiceArgs {
29    #[command(subcommand)]
30    pub action: Action,
31}
32
33#[derive(Debug, Subcommand)]
34#[allow(clippy::large_enum_variant)]
35pub enum Action {
36    /// Create credentials for a service.
37    Create(CreateArgs),
38    /// Enable a service in the current project (using existing credentials).
39    Enable(EnableAction),
40    /// Disable a service in the current project (inverse of `enable`).
41    Disable(DisableAction),
42    /// List all services with credential and project-enablement status.
43    List(service_list::ListArgs),
44    /// Show details for a configured service.
45    Show(ShowAction),
46    /// Check whether service credentials work by pinging the provider.
47    /// Without `--service`, every configured service is pinged in parallel.
48    Status(StatusArgs),
49    /// Delete credentials for a service (inverse of `create`).
50    Delete(DeleteAction),
51}
52
53#[derive(Debug, Args)]
54pub struct CreateArgs {
55    #[command(subcommand)]
56    pub service: CreateService,
57}
58
59#[derive(Debug, Subcommand)]
60pub enum CreateService {
61    /// Create 1Password (1pass) credentials (global by default,
62    /// `--local` for project-scoped).
63    #[command(name = "1pass")]
64    OnePass(service_onepass::CreateArgs),
65    /// Create Discord credentials (global by default, `--local` for
66    /// project-scoped).
67    Discord(service_discord::CreateArgs),
68    /// Create Google Calendar credentials (global by default,
69    /// `--local` for project-scoped).
70    Gcal(service_gcal::CreateArgs),
71    /// Create Spotify credentials (global by default, `--local` for
72    /// project-scoped).
73    Spotify(service_spotify::CreateArgs),
74    /// Create Slack credentials (global by default, `--local` for
75    /// project-scoped).
76    Slack(service_slack::CreateArgs),
77    /// Create Telegram credentials (global by default, `--local` for
78    /// project-scoped).
79    Telegram(service_telegram::CreateArgs),
80    /// Create YouTube Music credentials (global by default, `--local`
81    /// for project-scoped).
82    Ymusic(service_ymusic::CreateArgs),
83}
84
85#[derive(Debug, Args)]
86pub struct EnableAction {
87    #[command(subcommand)]
88    pub service: EnableService,
89}
90
91#[derive(Debug, Subcommand)]
92pub enum EnableService {
93    /// Enable the 1Password service in the current project.
94    #[command(name = "1pass")]
95    OnePass(EnableArgs),
96    /// Enable the Discord service in the current project.
97    Discord(EnableArgs),
98    /// Enable the Google Calendar service in the current project.
99    Gcal(EnableArgs),
100    /// Enable the Slack service in the current project.
101    Slack(EnableArgs),
102    /// Enable the Spotify service in the current project.
103    Spotify(EnableArgs),
104    /// Enable the Telegram service in the current project.
105    Telegram(EnableArgs),
106    /// Enable the YouTube Music service in the current project.
107    Ymusic(EnableArgs),
108}
109
110#[derive(Debug, Args)]
111pub struct DisableAction {
112    #[command(subcommand)]
113    pub service: DisableService,
114}
115
116#[derive(Debug, Subcommand)]
117pub enum DisableService {
118    /// Disable the 1Password service in the current project.
119    #[command(name = "1pass")]
120    OnePass(DisableArgs),
121    /// Disable the Discord service in the current project.
122    Discord(DisableArgs),
123    /// Disable the Google Calendar service in the current project.
124    Gcal(DisableArgs),
125    /// Disable the Slack service in the current project.
126    Slack(DisableArgs),
127    /// Disable the Spotify service in the current project.
128    Spotify(DisableArgs),
129    /// Disable the Telegram service in the current project.
130    Telegram(DisableArgs),
131    /// Disable the YouTube Music service in the current project.
132    Ymusic(DisableArgs),
133}
134
135#[derive(Debug, Args)]
136pub struct ShowAction {
137    #[command(subcommand)]
138    pub service: ShowService,
139}
140
141#[derive(Debug, Subcommand)]
142pub enum ShowService {
143    /// Show the 1Password service's effective configuration.
144    #[command(name = "1pass")]
145    OnePass(ShowArgs),
146    /// Show the Discord service's effective configuration.
147    Discord(ShowArgs),
148    /// Show the Google Calendar service's effective configuration.
149    Gcal(ShowArgs),
150    /// Show the Slack service's effective configuration.
151    Slack(ShowArgs),
152    /// Show the Spotify service's effective configuration.
153    Spotify(ShowArgs),
154    /// Show the Telegram service's effective configuration.
155    Telegram(ShowArgs),
156    /// Show the YouTube Music service's effective configuration.
157    Ymusic(ShowArgs),
158}
159
160/// Args for `zad service status [--service <NAME>] [--json]`.
161///
162/// Without `--service`, every service registered in
163/// [`zad::service::registry::SERVICES`] is pinged in parallel and a
164/// single aggregate envelope is emitted. With `--service`, only the
165/// named service is pinged and the per-service envelope is emitted.
166#[derive(Debug, Args)]
167pub struct StatusArgs {
168    /// Limit the check to a single service (e.g. `discord`, `telegram`).
169    /// Without this flag, every service in the registry is pinged.
170    #[arg(long, value_name = "NAME", value_parser = PossibleValuesParser::new(SERVICES))]
171    pub service: Option<String>,
172
173    /// Emit machine-readable JSON instead of human-readable text.
174    /// Recommended for agents — the envelope is stable.
175    #[arg(long)]
176    pub json: bool,
177}
178
179#[derive(Debug, Args)]
180pub struct DeleteAction {
181    #[command(subcommand)]
182    pub service: DeleteService,
183}
184
185#[derive(Debug, Subcommand)]
186pub enum DeleteService {
187    /// Delete 1Password credentials (global by default, `--local` for
188    /// project-scoped).
189    #[command(name = "1pass")]
190    OnePass(DeleteArgs),
191    /// Delete Discord credentials (global by default, `--local` for
192    /// project-scoped).
193    Discord(DeleteArgs),
194    /// Delete Google Calendar credentials (global by default,
195    /// `--local` for project-scoped).
196    Gcal(DeleteArgs),
197    /// Delete Slack credentials (global by default, `--local` for
198    /// project-scoped).
199    Slack(DeleteArgs),
200    /// Delete Spotify credentials (global by default, `--local` for
201    /// project-scoped).
202    Spotify(DeleteArgs),
203    /// Delete Telegram credentials (global by default, `--local` for
204    /// project-scoped).
205    Telegram(DeleteArgs),
206    /// Delete YouTube Music credentials (global by default, `--local`
207    /// for project-scoped).
208    Ymusic(DeleteArgs),
209}
210
211pub async fn run(args: ServiceArgs) -> Result<()> {
212    match args.action {
213        Action::Create(c) => match c.service {
214            CreateService::OnePass(a) => lifecycle::run_create::<OnePassLifecycle>(a).await,
215            CreateService::Discord(a) => lifecycle::run_create::<DiscordLifecycle>(a).await,
216            CreateService::Gcal(a) => lifecycle::run_create::<GcalLifecycle>(a).await,
217            CreateService::Slack(a) => lifecycle::run_create::<SlackLifecycle>(a).await,
218            CreateService::Spotify(a) => lifecycle::run_create::<SpotifyLifecycle>(a).await,
219            CreateService::Telegram(a) => lifecycle::run_create::<TelegramLifecycle>(a).await,
220            CreateService::Ymusic(a) => lifecycle::run_create::<YmusicLifecycle>(a).await,
221        },
222        Action::Enable(a) => match a.service {
223            EnableService::OnePass(a) => lifecycle::run_enable::<OnePassLifecycle>(a),
224            EnableService::Discord(a) => lifecycle::run_enable::<DiscordLifecycle>(a),
225            EnableService::Gcal(a) => lifecycle::run_enable::<GcalLifecycle>(a),
226            EnableService::Slack(a) => lifecycle::run_enable::<SlackLifecycle>(a),
227            EnableService::Spotify(a) => lifecycle::run_enable::<SpotifyLifecycle>(a),
228            EnableService::Telegram(a) => lifecycle::run_enable::<TelegramLifecycle>(a),
229            EnableService::Ymusic(a) => lifecycle::run_enable::<YmusicLifecycle>(a),
230        },
231        Action::Disable(d) => match d.service {
232            DisableService::OnePass(a) => lifecycle::run_disable::<OnePassLifecycle>(a),
233            DisableService::Discord(a) => lifecycle::run_disable::<DiscordLifecycle>(a),
234            DisableService::Gcal(a) => lifecycle::run_disable::<GcalLifecycle>(a),
235            DisableService::Slack(a) => lifecycle::run_disable::<SlackLifecycle>(a),
236            DisableService::Spotify(a) => lifecycle::run_disable::<SpotifyLifecycle>(a),
237            DisableService::Telegram(a) => lifecycle::run_disable::<TelegramLifecycle>(a),
238            DisableService::Ymusic(a) => lifecycle::run_disable::<YmusicLifecycle>(a),
239        },
240        Action::List(a) => service_list::run(a),
241        Action::Show(s) => match s.service {
242            ShowService::OnePass(a) => lifecycle::run_show::<OnePassLifecycle>(a),
243            ShowService::Discord(a) => lifecycle::run_show::<DiscordLifecycle>(a),
244            ShowService::Gcal(a) => lifecycle::run_show::<GcalLifecycle>(a),
245            ShowService::Slack(a) => lifecycle::run_show::<SlackLifecycle>(a),
246            ShowService::Spotify(a) => lifecycle::run_show::<SpotifyLifecycle>(a),
247            ShowService::Telegram(a) => lifecycle::run_show::<TelegramLifecycle>(a),
248            ShowService::Ymusic(a) => lifecycle::run_show::<YmusicLifecycle>(a),
249        },
250        Action::Status(s) => match s.service.as_deref() {
251            None => service_status::run_all(s).await,
252            Some("1pass") => {
253                lifecycle::run_status::<OnePassLifecycle>(lifecycle::StatusArgs { json: s.json })
254                    .await
255            }
256            Some("discord") => {
257                lifecycle::run_status::<DiscordLifecycle>(lifecycle::StatusArgs { json: s.json })
258                    .await
259            }
260            Some("gcal") => {
261                lifecycle::run_status::<GcalLifecycle>(lifecycle::StatusArgs { json: s.json }).await
262            }
263            Some("slack") => {
264                lifecycle::run_status::<SlackLifecycle>(lifecycle::StatusArgs { json: s.json })
265                    .await
266            }
267            Some("spotify") => {
268                lifecycle::run_status::<SpotifyLifecycle>(lifecycle::StatusArgs { json: s.json })
269                    .await
270            }
271            Some("telegram") => {
272                lifecycle::run_status::<TelegramLifecycle>(lifecycle::StatusArgs { json: s.json })
273                    .await
274            }
275            Some("ymusic") => {
276                lifecycle::run_status::<YmusicLifecycle>(lifecycle::StatusArgs { json: s.json })
277                    .await
278            }
279            // PossibleValuesParser rejects unknown values before we get
280            // here, so this arm only fires if a new entry is added to
281            // `SERVICES` without a matching match arm.
282            Some(other) => Err(zad::error::ZadError::Invalid(format!(
283                "unhandled service in status dispatch: `{other}`"
284            ))),
285        },
286        Action::Delete(d) => match d.service {
287            DeleteService::OnePass(a) => lifecycle::run_delete::<OnePassLifecycle>(a),
288            DeleteService::Discord(a) => lifecycle::run_delete::<DiscordLifecycle>(a),
289            DeleteService::Gcal(a) => lifecycle::run_delete::<GcalLifecycle>(a),
290            DeleteService::Slack(a) => lifecycle::run_delete::<SlackLifecycle>(a),
291            DeleteService::Spotify(a) => lifecycle::run_delete::<SpotifyLifecycle>(a),
292            DeleteService::Telegram(a) => lifecycle::run_delete::<TelegramLifecycle>(a),
293            DeleteService::Ymusic(a) => lifecycle::run_delete::<YmusicLifecycle>(a),
294        },
295    }
296}