teams_cli/setup/
capabilities.rs1use crate::config::DesktopClientKind;
2use crate::{desktop, error::AppError};
3
4#[derive(Clone, Debug)]
5pub enum Availability {
6 Ready { download: bool },
7 Blocked(String),
8}
9
10impl Availability {
11 fn from_check(result: Result<bool, AppError>) -> Self {
12 match result {
13 Ok(download) => Self::Ready { download },
14 Err(error) => Self::Blocked(error.to_string()),
15 }
16 }
17
18 pub fn reason(&self) -> Option<&str> {
19 match self {
20 Self::Ready { .. } => None,
21 Self::Blocked(reason) => Some(reason),
22 }
23 }
24}
25
26#[derive(Clone, Debug)]
27pub struct Capabilities {
28 pub graph: Availability,
29 pub pending: Vec<crate::config::PendingBrowserSetup>,
30 pub browser: Availability,
31 pub core: Availability,
32 pub native: Availability,
33 pub graph_device_code: bool,
34}
35
36impl Capabilities {
37 pub async fn detect() -> Self {
38 #[cfg(any(target_os = "macos", target_os = "linux"))]
39 let (browser, core, native) = tokio::join!(
40 desktop::browser::preflight(false),
41 desktop::browser::preflight(true),
42 desktop::connection::preflight(),
43 );
44 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
45 let (browser, core, native) = {
46 let unavailable = || {
47 Err(AppError::Unsupported(
48 "Browser mode needs macOS, Linux or WSL2. On Windows, run `teams init` inside WSL2.".into()))
49 };
50 (
51 unavailable(),
52 unavailable(),
53 desktop::connection::preflight().await,
54 )
55 };
56 #[allow(unused_mut)] let mut detected = Self {
58 graph: graph_availability(|key| std::env::var_os(key)),
59 pending: Vec::new(),
60 browser: Availability::from_check(browser),
61 core: Availability::from_check(core),
62 native: Availability::from_check(native.map(|()| false)),
63 graph_device_code: graph_device_code_needed(),
64 };
65 #[cfg(any(target_os = "macos", target_os = "linux"))]
66 match desktop::browser_setup::list() {
67 Ok(pending) => detected.pending = pending,
68 Err(error) => {
69 let reason = format!(
70 "{error} Check the browser setup records before retrying; no saved sign-in was changed."
71 );
72 detected.browser = Availability::Blocked(reason.clone());
73 detected.core = Availability::Blocked(reason);
74 }
75 }
76 detected
77 }
78
79 pub fn availability(&self, kind: DesktopClientKind) -> &Availability {
80 match kind {
81 DesktopClientKind::Browser => &self.browser,
82 DesktopClientKind::BrowserCore => &self.core,
83 DesktopClientKind::Native => &self.native,
84 }
85 }
86
87 pub fn preferred(&self, personal: bool) -> Option<DesktopClientKind> {
88 use DesktopClientKind::{Browser, BrowserCore, Native};
89 let preference: &[_] = if personal {
90 &[BrowserCore, Browser, Native]
91 } else {
92 &[Browser, Native]
93 };
94 preference
95 .iter()
96 .copied()
97 .find(|kind| self.availability(*kind).reason().is_none())
98 }
99}
100
101pub fn graph_device_code_needed() -> bool {
102 cfg!(target_os = "linux")
103 && !["DISPLAY", "WAYLAND_DISPLAY"]
104 .iter()
105 .any(|key| std::env::var_os(key).is_some_and(|value| !value.is_empty()))
106}
107
108fn graph_availability(get: impl Fn(&str) -> Option<std::ffi::OsString>) -> Availability {
109 let blocked = if get("TEAMS_ACCESS_TOKEN").is_some() {
110 Some(
111 "TEAMS_ACCESS_TOKEN overrides browser sign-in. Unset it before guided Graph setup, or use explicit init flags for automation.",
112 )
113 } else if get("TEAMS_READ_ONLY").is_some_and(|value| {
114 matches!(
115 value
116 .to_str()
117 .map(str::trim)
118 .map(str::to_ascii_lowercase)
119 .as_deref(),
120 Some("false" | "0" | "no" | "off")
121 )
122 }) {
123 Some(
124 "TEAMS_READ_ONLY=false overrides Graph's saved read-only setting. Unset it before guided Graph setup, or use explicit init flags.",
125 )
126 } else if get("TEAMS_CLIENT_ID").is_some() {
127 Some(
128 "TEAMS_CLIENT_ID overrides saved Graph application settings. Unset it before guided setup, or use explicit init flags for a custom application.",
129 )
130 } else if get("TEAMS_TENANT").is_some() {
131 Some(
132 "TEAMS_TENANT overrides saved Graph tenant settings. Unset it before guided setup, or use explicit init flags for a custom tenant.",
133 )
134 } else {
135 None
136 };
137 match blocked {
138 Some(reason) => Availability::Blocked(reason.into()),
139 None => Availability::Ready { download: false },
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn guided_graph_excludes_overrides_that_could_verify_another_session_or_unlock_reads() {
149 for (key, value) in [
150 ("TEAMS_ACCESS_TOKEN", "synthetic-token"),
151 ("TEAMS_ACCESS_TOKEN", ""),
152 ("TEAMS_READ_ONLY", "false"),
153 ("TEAMS_READ_ONLY", " OFF "),
154 ("TEAMS_CLIENT_ID", " "),
155 ("TEAMS_CLIENT_ID", "synthetic-app"),
156 ("TEAMS_TENANT", ""),
157 ("TEAMS_TENANT", "synthetic-tenant"),
158 ] {
159 let availability = graph_availability(|name| (name == key).then(|| value.into()));
160 assert!(availability.reason().unwrap().contains(key));
161 if value.starts_with("synthetic-") {
162 assert!(!availability.reason().unwrap().contains(value));
163 }
164 }
165 for value in [None, Some("true")] {
166 assert!(
167 graph_availability(|name| {
168 (name == "TEAMS_READ_ONLY")
169 .then_some(value)
170 .flatten()
171 .map(Into::into)
172 })
173 .reason()
174 .is_none()
175 );
176 }
177 }
178}