1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
//! Aggregate dispatch for `zad service status` (no `--service` filter).
//!
//! This is the agent-facing entrypoint: one command that pings every
//! service registered in [`zad::service::registry::SERVICES`] and
//! returns a single JSON envelope describing which ones work. Pings
//! run in parallel so adding a third service doesn't linearly inflate
//! startup time.
//!
//! Each row is a [`lifecycle::ServiceStatusOutput`] — the same shape
//! emitted by `zad service status --service <svc>` — so an agent that
//! already handles the single-service form can consume this output
//! unchanged.
use serde::Serialize;
use crate::cli::lifecycle::{self, ServiceStatusOutput};
use crate::cli::service::StatusArgs;
use crate::cli::{
service_discord::DiscordLifecycle, service_gcal::GcalLifecycle,
service_onepass::OnePassLifecycle, service_telegram::TelegramLifecycle,
service_ymusic::YmusicLifecycle,
};
use zad::error::Result;
#[derive(Debug, Serialize)]
struct AggregateOutput {
command: &'static str,
/// True iff every service that has an effective scope pinged OK.
/// Services with no credentials at all (`effective: null`) don't
/// affect this value — they're reported but not counted as
/// failures, since "not configured" isn't the same as "broken".
ok: bool,
services: Vec<ServiceStatusOutput>,
}
pub async fn run_all(args: StatusArgs) -> Result<()> {
// Independent network calls — fan them out. Order here matches the
// alphabetical order of `zad::service::registry::SERVICES`;
// adding a new service means adding one line here and one match
// arm in `service::run()`.
let (onepass, discord, gcal, telegram, ymusic) = tokio::join!(
lifecycle::status_for::<OnePassLifecycle>(),
lifecycle::status_for::<DiscordLifecycle>(),
lifecycle::status_for::<GcalLifecycle>(),
lifecycle::status_for::<TelegramLifecycle>(),
lifecycle::status_for::<YmusicLifecycle>(),
);
let services = vec![onepass?, discord?, gcal?, telegram?, ymusic?];
let ok = services
.iter()
.filter(|s| s.effective.is_some())
.all(|s| s.ok);
if args.json {
let out = AggregateOutput {
command: "service.status",
ok,
services,
};
println!("{}", serde_json::to_string_pretty(&out).unwrap());
} else {
print_human(ok, &services);
}
if !ok {
std::process::exit(1);
}
Ok(())
}
fn print_human(ok: bool, services: &[ServiceStatusOutput]) {
println!(
"zad service status: {}",
if ok {
"all configured services ok"
} else {
"one or more services FAILED"
}
);
for svc in services {
let effective = svc.effective.unwrap_or("(not configured)");
let state = match svc.effective {
None => "not configured".to_string(),
Some(_) if svc.ok => {
let name = svc
.global
.check
.as_ref()
.or(svc.local.check.as_ref())
.and_then(|c| c.authenticated_as.as_deref())
.unwrap_or("(unknown)");
format!("ok (authenticated as `{name}`)")
}
Some(_) => {
let err = svc
.global
.check
.as_ref()
.or(svc.local.check.as_ref())
.and_then(|c| c.error.as_deref())
.unwrap_or("(no detail)");
format!("FAILED ({err})")
}
};
println!(" {:<10} [{effective:<5}] {state}", svc.service);
}
}