1use anyhow::Result;
2use console::style;
3
4use crate::ui::prompts::{prompt_app_selection, prompt_yes_no};
5use crate::ui::state::VibeState;
6use crate::workspace::WorkspaceManager;
7use crate::{
8 display_println,
9 git::{CloneCommand, GitConfig},
10};
11
12pub enum NextAction {
14 Complete,
16 Continue(Box<dyn Workflow + Send + Sync>),
18 Suggest(Vec<String>),
20}
21
22pub trait Workflow: Send + Sync {
24 fn execute<'a>(
26 &'a self,
27 manager: &'a mut WorkspaceManager,
28 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>>;
29
30 fn description(&self) -> String;
32}
33
34pub struct CloneWorkflow {
36 pub url: String,
37 pub app: Option<String>,
38}
39
40impl Workflow for CloneWorkflow {
41 fn execute<'a>(
42 &'a self,
43 manager: &'a mut WorkspaceManager,
44 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
45 Box::pin(async move {
46 display_println!("{} Cloning repository...", style("đĨ").blue());
47
48 let git_config = GitConfig::default();
50 let cloned_path = CloneCommand::execute(
51 self.url.clone(),
52 None,
53 false, false, manager,
56 &git_config,
57 )
58 .await?;
59
60 let repo_name = cloned_path
62 .file_name()
63 .and_then(|n| n.to_str())
64 .ok_or_else(|| anyhow::anyhow!("Could not determine repository name"))?
65 .to_string();
66
67 display_println!(
68 "{} Repository '{}' cloned successfully!",
69 style("â").green().bold(),
70 style(&repo_name).cyan()
71 );
72
73 Ok(NextAction::Continue(Box::new(ConfigureAppWorkflow {
75 repo_name,
76 suggested_app: self.app.clone(),
77 open_after: true,
78 force_configure: false, })))
80 })
81 }
82
83 fn description(&self) -> String {
84 format!("Clone repository from {}", self.url)
85 }
86}
87
88pub struct ConfigureAppWorkflow {
90 pub repo_name: String,
91 pub suggested_app: Option<String>,
92 pub open_after: bool,
93 pub force_configure: bool, }
95
96impl Workflow for ConfigureAppWorkflow {
97 fn execute<'a>(
98 &'a self,
99 manager: &'a mut WorkspaceManager,
100 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
101 Box::pin(async move {
102 let apps = manager.list_apps_for_repo(&self.repo_name)?;
104
105 if !apps.is_empty() {
106 display_println!(
107 "{} Repository already has {} app{} configured",
108 style("âšī¸").blue(),
109 apps.len(),
110 if apps.len() == 1 { "" } else { "s" }
111 );
112
113 if self.open_after {
114 return Ok(NextAction::Continue(Box::new(OpenRepositoryWorkflow {
115 repo_name: self.repo_name.clone(),
116 preferred_app: self.suggested_app.clone(),
117 })));
118 } else {
119 return Ok(NextAction::Complete);
120 }
121 }
122
123 if !self.force_configure {
125 if self.open_after {
126 display_println!(
128 "\n{} Opening '{}' with smart app selection...",
129 style("đ").blue(),
130 style(&self.repo_name).cyan()
131 );
132
133 return Ok(NextAction::Continue(Box::new(SmartOpenWorkflow {
134 repo_name: self.repo_name.clone(),
135 })));
136 } else {
137 return Ok(NextAction::Complete);
138 }
139 }
140
141 display_println!(
143 "\n{} Configure app for '{}'?",
144 style("đ§").blue(),
145 style(&self.repo_name).cyan()
146 );
147
148 if prompt_yes_no("Configure app", true)? {
149 let app_name = if let Some(app) = &self.suggested_app {
150 app.clone()
151 } else {
152 prompt_app_selection()?
153 };
154
155 manager
156 .configure_app_for_repo(&self.repo_name, &app_name, "default")
157 .await?;
158
159 display_println!(
160 "{} Configured {} for repository",
161 style("â").green().bold(),
162 style(&app_name).cyan()
163 );
164
165 if self.open_after {
166 Ok(NextAction::Continue(Box::new(OpenRepositoryWorkflow {
168 repo_name: self.repo_name.clone(),
169 preferred_app: Some(app_name),
170 })))
171 } else {
172 Ok(NextAction::Complete)
173 }
174 } else {
175 if self.open_after {
177 display_println!("\n{} Opening with default app...", style("đ").blue());
178 Ok(NextAction::Continue(Box::new(OpenRepositoryWorkflow {
179 repo_name: self.repo_name.clone(),
180 preferred_app: None,
181 })))
182 } else {
183 Ok(NextAction::Complete)
184 }
185 }
186 })
187 }
188
189 fn description(&self) -> String {
190 format!("Configure app for {}", self.repo_name)
191 }
192}
193
194pub struct SmartOpenWorkflow {
196 pub repo_name: String,
197}
198
199impl Workflow for SmartOpenWorkflow {
200 fn execute<'a>(
201 &'a self,
202 manager: &'a mut WorkspaceManager,
203 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
204 Box::pin(async move {
205 manager.smart_open_repository(&self.repo_name).await?;
207
208 display_println!(
209 "{} Repository '{}' opened successfully",
210 style("đ").green().bold(),
211 style(&self.repo_name).cyan()
212 );
213
214 Ok(NextAction::Complete)
215 })
216 }
217
218 fn description(&self) -> String {
219 format!("Smart open {}", self.repo_name)
220 }
221}
222
223pub struct OpenRepositoryWorkflow {
225 pub repo_name: String,
226 pub preferred_app: Option<String>,
227}
228
229impl Workflow for OpenRepositoryWorkflow {
230 fn execute<'a>(
231 &'a self,
232 manager: &'a mut WorkspaceManager,
233 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
234 Box::pin(async move {
235 let repo = manager
237 .get_repository(&self.repo_name)
238 .ok_or_else(|| anyhow::anyhow!("Repository '{}' not found", self.repo_name))?;
239
240 let repo_path = repo.path.clone();
241
242 let app_to_use = if let Some(app) = &self.preferred_app {
244 app.clone()
245 } else {
246 let apps = manager.list_apps_for_repo(&self.repo_name)?;
248 if apps.is_empty() {
249 "vscode".to_string() } else {
251 apps[0].0.clone()
252 }
253 };
254
255 manager
257 .open_repo_with_app(&self.repo_name, &app_to_use)
258 .await?;
259
260 let mut state = VibeState::load().unwrap_or_default();
262 state.add_recent_repo(self.repo_name.clone(), repo_path, Some(app_to_use.clone()));
263 state.save()?;
264
265 display_println!(
266 "{} Opened {} with {}!",
267 style("đ").green().bold(),
268 style(&self.repo_name).cyan(),
269 style(&app_to_use).blue()
270 );
271
272 Ok(NextAction::Suggest(vec![
274 "Run 'vibe' to manage more repositories".to_string(),
275 format!("Use 'vibe launch 1' to quickly reopen {}", self.repo_name),
276 "Configure additional apps with 'vibe apps configure'".to_string(),
277 ]))
278 })
279 }
280
281 fn description(&self) -> String {
282 format!("Open {} repository", self.repo_name)
283 }
284}
285
286pub struct SetupWorkspaceWorkflow {
288 pub auto_discover: bool,
289}
290
291impl Workflow for SetupWorkspaceWorkflow {
292 fn execute<'a>(
293 &'a self,
294 manager: &'a mut WorkspaceManager,
295 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
296 Box::pin(async move {
297 crate::ui::setup_wizard::run_enhanced_setup_wizard(manager).await?;
299
300 Ok(NextAction::Complete)
302 })
303 }
304
305 fn description(&self) -> String {
306 "Setup workspace for first-time use".to_string()
307 }
308}
309
310pub struct ConfigureDefaultAppWorkflow {
312 pub repo_count: usize,
313}
314
315impl Workflow for ConfigureDefaultAppWorkflow {
316 fn execute<'a>(
317 &'a self,
318 manager: &'a mut WorkspaceManager,
319 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
320 Box::pin(async move {
321 display_println!(
322 "\n{} Configure a default app for your {} repositories?",
323 style("đ§").blue(),
324 self.repo_count
325 );
326
327 if prompt_yes_no("Configure default app", true)? {
328 let app_name = prompt_app_selection()?;
329
330 let repo_names: Vec<String> = manager
332 .list_repositories()
333 .iter()
334 .map(|r| r.name.clone())
335 .collect();
336
337 for repo_name in &repo_names {
338 manager
339 .configure_app_for_repo(repo_name, &app_name, "default")
340 .await?;
341 }
342
343 display_println!(
344 "{} Configured {} as default app for all repositories!",
345 style("â").green().bold(),
346 style(&app_name).cyan()
347 );
348 }
349
350 display_println!("\n{} Workspace setup complete!", style("â¨").green().bold());
352
353 Ok(NextAction::Suggest(vec![
354 "Run 'vibe' to start managing repositories".to_string(),
355 "Use 'vibe launch' to quickly open recent repos".to_string(),
356 "Clone new repos with 'vibe go <url>'".to_string(),
357 ]))
358 })
359 }
360
361 fn description(&self) -> String {
362 format!("Configure default app for {} repositories", self.repo_count)
363 }
364}
365
366pub struct CreateRepositoryWorkflow {
368 pub suggested_name: Option<String>,
369 pub app: Option<String>,
370 pub skip_configure: bool,
371 pub skip_open: bool,
372}
373
374impl Workflow for CreateRepositoryWorkflow {
375 fn execute<'a>(
376 &'a self,
377 manager: &'a mut WorkspaceManager,
378 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
379 Box::pin(async move {
380 use crate::repository::RepositoryCreator;
381 use inquire::{Select, Text};
382
383 display_println!("{} Creating new repository...", style("đ").blue());
384
385 let workspace_root = manager.get_workspace_root().clone();
387 let creator = RepositoryCreator::new(workspace_root);
388
389 let user_info = match creator.get_github_user_info().await {
391 Ok(info) => info,
392 Err(e) => {
393 display_println!(
394 "{} Warning: Could not get GitHub info: {}",
395 style("â ī¸").yellow(),
396 e
397 );
398 display_println!(
399 "{} Continuing without GitHub integration...",
400 style("âšī¸").blue()
401 );
402 return self
404 .create_without_github_integration(manager, &creator)
405 .await;
406 }
407 };
408
409 let mut owners = vec![user_info.username.clone()];
411 owners.extend(user_info.organizations.iter().map(|org| org.login.clone()));
412
413 let selected_owner = if owners.len() == 1 {
414 owners[0].clone()
415 } else {
416 display_println!("\n{} Select repository owner:", style("đ¤").blue());
417
418 let owner_choices: Vec<String> = owners
419 .iter()
420 .map(|owner| {
421 if owner == &user_info.username {
422 format!("{owner} (personal)")
423 } else {
424 format!("{owner} (organization)")
425 }
426 })
427 .collect();
428
429 let selected_display = Select::new("Repository owner:", owner_choices).prompt()?;
430
431 selected_display.split(" (").next().unwrap().to_string()
433 };
434
435 let repo_name = if let Some(name) = &self.suggested_name {
437 name.clone()
438 } else {
439 Text::new("Repository name:").prompt()?
440 };
441
442 if let Err(e) = creator.validate_repository_name(&repo_name) {
444 display_println!("{} Invalid repository name: {}", style("â").red(), e);
445 return Ok(NextAction::Complete);
446 }
447
448 match creator
450 .check_repository_availability(&selected_owner, &repo_name)
451 .await
452 {
453 Ok(false) => {
454 display_println!(
455 "{} Repository {}/{} already exists on GitHub!",
456 style("â ī¸").yellow(),
457 selected_owner,
458 repo_name
459 );
460 display_println!(
461 "{} You can still create it locally, but you won't be able to push to GitHub with this name.",
462 style("âšī¸").blue()
463 );
464 }
465 Ok(true) => {
466 display_println!(
467 "{} Repository name is available on GitHub",
468 style("â
").green()
469 );
470 }
471 Err(e) => {
472 display_println!(
473 "{} Could not check GitHub availability: {}",
474 style("â ī¸").yellow(),
475 e
476 );
477 }
478 }
479
480 match creator
482 .create_local_repository(&selected_owner, &repo_name, manager)
483 .await
484 {
485 Ok(_path) => {
486 if self.skip_configure && self.skip_open {
488 Ok(NextAction::Complete)
490 } else if self.skip_configure {
491 Ok(NextAction::Continue(Box::new(OpenRepositoryWorkflow {
493 repo_name: repo_name.clone(),
494 preferred_app: self.app.clone(),
495 })))
496 } else {
497 Ok(NextAction::Continue(Box::new(ConfigureAppWorkflow {
499 repo_name: repo_name.clone(),
500 suggested_app: self.app.clone(),
501 open_after: !self.skip_open,
502 force_configure: false, })))
504 }
505 }
506 Err(e) => {
507 display_println!("{} Failed to create repository: {}", style("â").red(), e);
508 Ok(NextAction::Complete)
509 }
510 }
511 })
512 }
513
514 fn description(&self) -> String {
515 if let Some(name) = &self.suggested_name {
516 format!("Create repository '{name}'")
517 } else {
518 "Create new repository".to_string()
519 }
520 }
521}
522
523impl CreateRepositoryWorkflow {
524 async fn create_without_github_integration(
525 &self,
526 manager: &mut WorkspaceManager,
527 creator: &crate::repository::RepositoryCreator,
528 ) -> Result<NextAction> {
529 use inquire::Text;
530
531 let current_user = std::env::var("USER").unwrap_or_else(|_| "user".to_string());
533
534 let repo_name = if let Some(name) = &self.suggested_name {
536 name.clone()
537 } else {
538 Text::new("Repository name:").prompt()?
539 };
540
541 if let Err(e) = creator.validate_repository_name(&repo_name) {
543 display_println!("{} Invalid repository name: {}", style("â").red(), e);
544 return Ok(NextAction::Complete);
545 }
546
547 match creator
549 .create_local_repository(¤t_user, &repo_name, manager)
550 .await
551 {
552 Ok(_path) => {
553 if self.skip_configure && self.skip_open {
555 Ok(NextAction::Complete)
557 } else if self.skip_configure {
558 Ok(NextAction::Continue(Box::new(OpenRepositoryWorkflow {
560 repo_name: repo_name.clone(),
561 preferred_app: self.app.clone(),
562 })))
563 } else {
564 Ok(NextAction::Continue(Box::new(ConfigureAppWorkflow {
566 repo_name: repo_name.clone(),
567 suggested_app: self.app.clone(),
568 open_after: !self.skip_open,
569 force_configure: false, })))
571 }
572 }
573 Err(e) => {
574 display_println!("{} Failed to create repository: {}", style("â").red(), e);
575 Ok(NextAction::Complete)
576 }
577 }
578 }
579}
580
581pub async fn execute_workflow(
583 workflow: Box<dyn Workflow + Send + Sync>,
584 manager: &mut WorkspaceManager,
585) -> Result<()> {
586 let mut current_workflow = workflow;
587
588 loop {
589 display_println!(
590 "\n{} {}",
591 style("âļ").blue(),
592 style(current_workflow.description()).dim()
593 );
594
595 match current_workflow.execute(manager).await? {
596 NextAction::Complete => {
597 display_println!("\n{} Workflow complete!", style("â").green().bold());
598 break;
599 }
600 NextAction::Continue(next) => {
601 display_println!("\n{} Continuing workflow...", style("â").cyan());
602 current_workflow = next;
603 }
604 NextAction::Suggest(suggestions) => {
605 display_println!("\n{} Next steps:", style("đĄ").yellow());
606 for suggestion in suggestions {
607 display_println!(" {} {}", style("âĸ").dim(), suggestion);
608 }
609 break;
610 }
611 }
612 }
613
614 Ok(())
615}
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620
621 #[test]
622 fn test_workflow_description() {
623 let workflow = CloneWorkflow {
624 url: "https://github.com/user/repo".to_string(),
625 app: Some("vscode".to_string()),
626 };
627
628 assert_eq!(
629 workflow.description(),
630 "Clone repository from https://github.com/user/repo"
631 );
632 }
633}