1use crate::{
2 command_result::{CommandSuccess, StartupUpdateOutcome},
3 commands::{self, RootCommand},
4 config::{ServerSelection, TargetResolver, profiles::FileProfileStore},
5 credentials::{
6 CredentialStore, KeyringCredentialStore, SecretInputProvider, SystemSecretInputProvider,
7 },
8 error::AppError,
9 input::{ConfirmationProvider, SystemConfirmationProvider},
10 output::OutputFormat,
11 update::{FileCache, StartupEligibility, UpdateService},
12};
13
14pub struct App<S, P, F = FileProfileStore, C = SystemConfirmationProvider> {
15 target_resolver: TargetResolver,
16 credential_store: S,
17 secret_input: P,
18 profile_store: F,
19 confirmation: C,
20 update_service_factory: fn() -> Result<UpdateService, AppError>,
21}
22
23impl
24 App<
25 KeyringCredentialStore,
26 SystemSecretInputProvider,
27 FileProfileStore,
28 SystemConfirmationProvider,
29 >
30{
31 pub fn production(selection: ServerSelection) -> Self {
32 Self::production_with_confirmation_interactivity(selection, true)
33 }
34
35 pub fn production_with_confirmation_interactivity(
36 selection: ServerSelection,
37 allow_interactive_confirmation: bool,
38 ) -> Self {
39 Self::with_dependencies(
40 selection,
41 KeyringCredentialStore,
42 SystemSecretInputProvider,
43 FileProfileStore::production(),
44 SystemConfirmationProvider::for_output(allow_interactive_confirmation),
45 )
46 }
47}
48
49impl<S, P, F> App<S, P, F, SystemConfirmationProvider>
50where
51 S: CredentialStore,
52 P: SecretInputProvider,
53 F: crate::config::profiles::ProfileStore,
54{
55 pub const fn with_profile_store(
56 selection: ServerSelection,
57 credential_store: S,
58 secret_input: P,
59 profile_store: F,
60 ) -> Self {
61 Self::with_dependencies(
62 selection,
63 credential_store,
64 secret_input,
65 profile_store,
66 SystemConfirmationProvider::interactive(),
67 )
68 }
69}
70
71impl<S, P, F, C> App<S, P, F, C>
72where
73 S: CredentialStore,
74 P: SecretInputProvider,
75 F: crate::config::profiles::ProfileStore,
76 C: ConfirmationProvider,
77{
78 pub const fn with_dependencies(
79 selection: ServerSelection,
80 credential_store: S,
81 secret_input: P,
82 profile_store: F,
83 confirmation: C,
84 ) -> Self {
85 Self {
86 target_resolver: TargetResolver::new(selection),
87 credential_store,
88 secret_input,
89 profile_store,
90 confirmation,
91 update_service_factory: UpdateService::production,
92 }
93 }
94
95 pub fn with_confirmation_provider<C2>(self, confirmation: C2) -> App<S, P, F, C2>
96 where
97 C2: ConfirmationProvider,
98 {
99 App {
100 target_resolver: self.target_resolver,
101 credential_store: self.credential_store,
102 secret_input: self.secret_input,
103 profile_store: self.profile_store,
104 confirmation,
105 update_service_factory: self.update_service_factory,
106 }
107 }
108
109 pub const fn credential_store(&self) -> &S {
110 &self.credential_store
111 }
112
113 pub(crate) fn check_for_update(
114 &self,
115 output: OutputFormat,
116 command: &RootCommand,
117 ) -> impl std::future::Future<Output = Result<StartupUpdateOutcome, AppError>> + '_ {
118 let eligible = StartupEligibility::production(
119 output,
120 matches!(command, RootCommand::SelfManagement(_)),
121 );
122 Box::pin(async move {
124 if !eligible.eligible() {
125 return Ok(StartupUpdateOutcome::Continue);
126 }
127 let Ok(service) = (self.update_service_factory)() else {
130 return Ok(StartupUpdateOutcome::Continue);
131 };
132 let Ok(mut cache) = FileCache::production() else {
133 return Ok(StartupUpdateOutcome::Continue);
134 };
135 service.automatic(&mut cache, &self.confirmation).await
136 })
137 }
138
139 pub fn execute(
140 &self,
141 command: RootCommand,
142 ) -> impl std::future::Future<Output = Result<CommandSuccess, AppError>> + '_ {
143 let task: std::pin::Pin<
150 Box<dyn std::future::Future<Output = Result<CommandSuccess, AppError>> + Send + '_>,
151 > = if let RootCommand::SelfManagement(args) = command {
152 if self.target_resolver.has_explicit_target() {
153 Box::pin(async {
154 Err(AppError::invalid_input(
155 "--server and --profile cannot be used with self-management commands",
156 ))
157 })
158 } else {
159 match (self.update_service_factory)() {
160 Ok(service) => Box::pin(commands::self_management::dispatch(
161 args.command,
162 service,
163 &self.confirmation,
164 )),
165 Err(error) => Box::pin(async move { Err(error) }),
166 }
167 }
168 } else {
169 Box::pin(self.execute_command(command))
170 };
171 task
172 }
173
174 async fn execute_command(&self, command: RootCommand) -> Result<CommandSuccess, AppError> {
175 if let RootCommand::Skill(args) = command {
176 return commands::skill::dispatch(args.command);
177 }
178 if let RootCommand::Profile(args) = command {
179 if self.target_resolver.has_explicit_target() {
180 return Err(AppError::invalid_input(
181 "--server and --profile cannot be used with profile-management commands",
182 ));
183 }
184 return commands::profile::dispatch(
185 args.command,
186 &self.profile_store,
187 &self.credential_store,
188 &self.confirmation,
189 );
190 }
191 command.validate_input()?;
192 let policy = match &command {
193 RootCommand::Auth(args) => args.command.missing_profile_resolution(),
194 RootCommand::System(_) => crate::config::MissingProfileResolution::AnonymousRequest,
195 _ => crate::config::MissingProfileResolution::Reject,
196 };
197 let mut target = self.target_resolver.resolve(&self.profile_store, policy)?;
198 let profile = target.profile().to_owned();
199 let result = match command {
200 RootCommand::System(args) => {
201 commands::system::dispatch(args.command, target.client_factory()).await
202 }
203 RootCommand::Admin(args) => {
204 commands::admin::dispatch(
205 args.command,
206 target.client_factory(),
207 &self.credential_store,
208 &self.secret_input,
209 &self.confirmation,
210 )
211 .await
212 }
213 RootCommand::Rule(args) => {
214 commands::rules::dispatch(
215 args.command,
216 target.client_factory(),
217 &self.credential_store,
218 &self.confirmation,
219 )
220 .await
221 }
222 RootCommand::Integration(args) => {
223 commands::integrations::dispatch(
224 args.command,
225 target.client_factory(),
226 &self.credential_store,
227 &self.secret_input,
228 &self.confirmation,
229 )
230 .await
231 }
232 RootCommand::Dependency(args) => {
233 commands::dependencies::dispatch(
234 args.command,
235 target.client_factory(),
236 &self.credential_store,
237 &self.confirmation,
238 )
239 .await
240 }
241 RootCommand::Attachment(args) => {
242 commands::attachments::dispatch(
243 args.command,
244 target.client_factory(),
245 &self.credential_store,
246 &self.confirmation,
247 )
248 .await
249 }
250 RootCommand::Api(args) => {
251 commands::api::dispatch(
252 args.command,
253 target.client_factory(),
254 &self.credential_store,
255 &self.confirmation,
256 )
257 .await
258 }
259 RootCommand::User(args) => {
260 commands::users::dispatch(
261 args.command,
262 target.client_factory(),
263 &self.credential_store,
264 &self.secret_input,
265 &self.confirmation,
266 )
267 .await
268 }
269 RootCommand::Board(args) => {
270 commands::boards::dispatch(
271 args.command,
272 target.client_factory(),
273 &self.credential_store,
274 &self.secret_input,
275 &self.confirmation,
276 )
277 .await
278 }
279 RootCommand::List(args) => {
280 commands::lists::dispatch(
281 args.command,
282 target.client_factory(),
283 &self.credential_store,
284 &self.confirmation,
285 )
286 .await
287 }
288 RootCommand::Card(args) => {
289 commands::cards::dispatch(
290 args.command,
291 target.client_factory(),
292 &self.credential_store,
293 &self.confirmation,
294 )
295 .await
296 }
297 RootCommand::Checklist(args) => {
298 commands::checklists::dispatch(
299 args.command,
300 target.client_factory(),
301 &self.credential_store,
302 &self.confirmation,
303 )
304 .await
305 }
306 RootCommand::CustomField(args) => {
307 commands::custom_fields::dispatch(
308 args.command,
309 target.client_factory(),
310 &self.credential_store,
311 &self.confirmation,
312 )
313 .await
314 }
315 RootCommand::Comment(args) => {
316 commands::comments::dispatch(
317 args.command,
318 target.client_factory(),
319 &self.credential_store,
320 &self.confirmation,
321 )
322 .await
323 }
324 RootCommand::Swimlane(args) => {
325 commands::swimlanes::dispatch(
326 args.command,
327 target.client_factory(),
328 &self.credential_store,
329 &self.confirmation,
330 )
331 .await
332 }
333 RootCommand::Auth(args) => {
334 commands::auth::dispatch(
335 args.command,
336 &mut target,
337 &self.credential_store,
338 &self.secret_input,
339 &self.confirmation,
340 )
341 .await
342 }
343 RootCommand::Profile(_) | RootCommand::Skill(_) | RootCommand::SelfManagement(_) => {
344 unreachable!("local commands return before target resolution")
345 }
346 };
347 result.map_err(|error| {
348 let error = error.with_profile_context(profile);
349 if policy.allows_initialization() {
350 error.with_profile_state(target.profile_created(), target.profile_active())
351 } else {
352 error
353 }
354 })
355 }
356}
357
358#[cfg(test)]
359mod tests;