use anyhow::Result;
use console::style;
use crate::ui::prompts::{prompt_app_selection, prompt_yes_no};
use crate::ui::state::VibeState;
use crate::workspace::WorkspaceManager;
use crate::{
display_println,
git::{CloneCommand, GitConfig},
};
pub enum NextAction {
Complete,
Continue(Box<dyn Workflow + Send + Sync>),
Suggest(Vec<String>),
}
pub trait Workflow: Send + Sync {
fn execute<'a>(
&'a self,
manager: &'a mut WorkspaceManager,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>>;
fn description(&self) -> String;
}
pub struct CloneWorkflow {
pub url: String,
pub app: Option<String>,
}
impl Workflow for CloneWorkflow {
fn execute<'a>(
&'a self,
manager: &'a mut WorkspaceManager,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
Box::pin(async move {
display_println!("{} Cloning repository...", style("📥").blue());
let git_config = GitConfig::default();
let cloned_path = CloneCommand::execute(
self.url.clone(),
None,
false, false, manager,
&git_config,
)
.await?;
let repo_name = cloned_path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| anyhow::anyhow!("Could not determine repository name"))?
.to_string();
display_println!(
"{} Repository '{}' cloned successfully!",
style("✓").green().bold(),
style(&repo_name).cyan()
);
Ok(NextAction::Continue(Box::new(ConfigureAppWorkflow {
repo_name,
suggested_app: self.app.clone(),
open_after: true,
force_configure: false, })))
})
}
fn description(&self) -> String {
format!("Clone repository from {}", self.url)
}
}
pub struct ConfigureAppWorkflow {
pub repo_name: String,
pub suggested_app: Option<String>,
pub open_after: bool,
pub force_configure: bool, }
impl Workflow for ConfigureAppWorkflow {
fn execute<'a>(
&'a self,
manager: &'a mut WorkspaceManager,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
Box::pin(async move {
let apps = manager.list_apps_for_repo(&self.repo_name)?;
if !apps.is_empty() {
display_println!(
"{} Repository already has {} app{} configured",
style("ℹ️").blue(),
apps.len(),
if apps.len() == 1 { "" } else { "s" }
);
if self.open_after {
return Ok(NextAction::Continue(Box::new(OpenRepositoryWorkflow {
repo_name: self.repo_name.clone(),
preferred_app: self.suggested_app.clone(),
})));
} else {
return Ok(NextAction::Complete);
}
}
if !self.force_configure {
if self.open_after {
display_println!(
"\n{} Opening '{}' with smart app selection...",
style("🚀").blue(),
style(&self.repo_name).cyan()
);
return Ok(NextAction::Continue(Box::new(SmartOpenWorkflow {
repo_name: self.repo_name.clone(),
})));
} else {
return Ok(NextAction::Complete);
}
}
display_println!(
"\n{} Configure app for '{}'?",
style("🔧").blue(),
style(&self.repo_name).cyan()
);
if prompt_yes_no("Configure app", true)? {
let app_name = if let Some(app) = &self.suggested_app {
app.clone()
} else {
prompt_app_selection()?
};
manager
.configure_app_for_repo(&self.repo_name, &app_name, "default")
.await?;
display_println!(
"{} Configured {} for repository",
style("✓").green().bold(),
style(&app_name).cyan()
);
if self.open_after {
Ok(NextAction::Continue(Box::new(OpenRepositoryWorkflow {
repo_name: self.repo_name.clone(),
preferred_app: Some(app_name),
})))
} else {
Ok(NextAction::Complete)
}
} else {
if self.open_after {
display_println!("\n{} Opening with default app...", style("🚀").blue());
Ok(NextAction::Continue(Box::new(OpenRepositoryWorkflow {
repo_name: self.repo_name.clone(),
preferred_app: None,
})))
} else {
Ok(NextAction::Complete)
}
}
})
}
fn description(&self) -> String {
format!("Configure app for {}", self.repo_name)
}
}
pub struct SmartOpenWorkflow {
pub repo_name: String,
}
impl Workflow for SmartOpenWorkflow {
fn execute<'a>(
&'a self,
manager: &'a mut WorkspaceManager,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
Box::pin(async move {
manager.smart_open_repository(&self.repo_name).await?;
display_println!(
"{} Repository '{}' opened successfully",
style("🚀").green().bold(),
style(&self.repo_name).cyan()
);
Ok(NextAction::Complete)
})
}
fn description(&self) -> String {
format!("Smart open {}", self.repo_name)
}
}
pub struct OpenRepositoryWorkflow {
pub repo_name: String,
pub preferred_app: Option<String>,
}
impl Workflow for OpenRepositoryWorkflow {
fn execute<'a>(
&'a self,
manager: &'a mut WorkspaceManager,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
Box::pin(async move {
let repo = manager
.get_repository(&self.repo_name)
.ok_or_else(|| anyhow::anyhow!("Repository '{}' not found", self.repo_name))?;
let repo_path = repo.path.clone();
let app_to_use = if let Some(app) = &self.preferred_app {
app.clone()
} else {
let apps = manager.list_apps_for_repo(&self.repo_name)?;
if apps.is_empty() {
"vscode".to_string() } else {
apps[0].0.clone()
}
};
manager
.open_repo_with_app(&self.repo_name, &app_to_use)
.await?;
let mut state = VibeState::load().unwrap_or_default();
state.add_recent_repo(self.repo_name.clone(), repo_path, Some(app_to_use.clone()));
state.save()?;
display_println!(
"{} Opened {} with {}!",
style("🎉").green().bold(),
style(&self.repo_name).cyan(),
style(&app_to_use).blue()
);
Ok(NextAction::Suggest(vec![
"Run 'vibe' to manage more repositories".to_string(),
format!("Use 'vibe launch 1' to quickly reopen {}", self.repo_name),
"Configure additional apps with 'vibe apps configure'".to_string(),
]))
})
}
fn description(&self) -> String {
format!("Open {} repository", self.repo_name)
}
}
pub struct SetupWorkspaceWorkflow {
pub auto_discover: bool,
}
impl Workflow for SetupWorkspaceWorkflow {
fn execute<'a>(
&'a self,
manager: &'a mut WorkspaceManager,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
Box::pin(async move {
crate::ui::setup_wizard::run_enhanced_setup_wizard(manager).await?;
Ok(NextAction::Complete)
})
}
fn description(&self) -> String {
"Setup workspace for first-time use".to_string()
}
}
pub struct ConfigureDefaultAppWorkflow {
pub repo_count: usize,
}
impl Workflow for ConfigureDefaultAppWorkflow {
fn execute<'a>(
&'a self,
manager: &'a mut WorkspaceManager,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
Box::pin(async move {
display_println!(
"\n{} Configure a default app for your {} repositories?",
style("🔧").blue(),
self.repo_count
);
if prompt_yes_no("Configure default app", true)? {
let app_name = prompt_app_selection()?;
let repo_names: Vec<String> = manager
.list_repositories()
.iter()
.map(|r| r.name.clone())
.collect();
for repo_name in &repo_names {
manager
.configure_app_for_repo(repo_name, &app_name, "default")
.await?;
}
display_println!(
"{} Configured {} as default app for all repositories!",
style("✓").green().bold(),
style(&app_name).cyan()
);
}
display_println!("\n{} Workspace setup complete!", style("✨").green().bold());
Ok(NextAction::Suggest(vec![
"Run 'vibe' to start managing repositories".to_string(),
"Use 'vibe launch' to quickly open recent repos".to_string(),
"Clone new repos with 'vibe go <url>'".to_string(),
]))
})
}
fn description(&self) -> String {
format!("Configure default app for {} repositories", self.repo_count)
}
}
pub struct CreateRepositoryWorkflow {
pub suggested_name: Option<String>,
pub app: Option<String>,
pub skip_configure: bool,
pub skip_open: bool,
}
impl Workflow for CreateRepositoryWorkflow {
fn execute<'a>(
&'a self,
manager: &'a mut WorkspaceManager,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NextAction>> + Send + 'a>> {
Box::pin(async move {
use crate::repository::RepositoryCreator;
use inquire::{Select, Text};
display_println!("{} Creating new repository...", style("🆕").blue());
let workspace_root = manager.get_workspace_root().clone();
let creator = RepositoryCreator::new(workspace_root);
let user_info = match creator.get_github_user_info().await {
Ok(info) => info,
Err(e) => {
display_println!(
"{} Warning: Could not get GitHub info: {}",
style("⚠️").yellow(),
e
);
display_println!(
"{} Continuing without GitHub integration...",
style("ℹ️").blue()
);
return self
.create_without_github_integration(manager, &creator)
.await;
}
};
let mut owners = vec![user_info.username.clone()];
owners.extend(user_info.organizations.iter().map(|org| org.login.clone()));
let selected_owner = if owners.len() == 1 {
owners[0].clone()
} else {
display_println!("\n{} Select repository owner:", style("👤").blue());
let owner_choices: Vec<String> = owners
.iter()
.map(|owner| {
if owner == &user_info.username {
format!("{owner} (personal)")
} else {
format!("{owner} (organization)")
}
})
.collect();
let selected_display = Select::new("Repository owner:", owner_choices).prompt()?;
selected_display.split(" (").next().unwrap().to_string()
};
let repo_name = if let Some(name) = &self.suggested_name {
name.clone()
} else {
Text::new("Repository name:").prompt()?
};
if let Err(e) = creator.validate_repository_name(&repo_name) {
display_println!("{} Invalid repository name: {}", style("❌").red(), e);
return Ok(NextAction::Complete);
}
match creator
.check_repository_availability(&selected_owner, &repo_name)
.await
{
Ok(false) => {
display_println!(
"{} Repository {}/{} already exists on GitHub!",
style("⚠️").yellow(),
selected_owner,
repo_name
);
display_println!(
"{} You can still create it locally, but you won't be able to push to GitHub with this name.",
style("ℹ️").blue()
);
}
Ok(true) => {
display_println!(
"{} Repository name is available on GitHub",
style("✅").green()
);
}
Err(e) => {
display_println!(
"{} Could not check GitHub availability: {}",
style("⚠️").yellow(),
e
);
}
}
match creator
.create_local_repository(&selected_owner, &repo_name, manager)
.await
{
Ok(_path) => {
if self.skip_configure && self.skip_open {
Ok(NextAction::Complete)
} else if self.skip_configure {
Ok(NextAction::Continue(Box::new(OpenRepositoryWorkflow {
repo_name: repo_name.clone(),
preferred_app: self.app.clone(),
})))
} else {
Ok(NextAction::Continue(Box::new(ConfigureAppWorkflow {
repo_name: repo_name.clone(),
suggested_app: self.app.clone(),
open_after: !self.skip_open,
force_configure: false, })))
}
}
Err(e) => {
display_println!("{} Failed to create repository: {}", style("❌").red(), e);
Ok(NextAction::Complete)
}
}
})
}
fn description(&self) -> String {
if let Some(name) = &self.suggested_name {
format!("Create repository '{name}'")
} else {
"Create new repository".to_string()
}
}
}
impl CreateRepositoryWorkflow {
async fn create_without_github_integration(
&self,
manager: &mut WorkspaceManager,
creator: &crate::repository::RepositoryCreator,
) -> Result<NextAction> {
use inquire::Text;
let current_user = std::env::var("USER").unwrap_or_else(|_| "user".to_string());
let repo_name = if let Some(name) = &self.suggested_name {
name.clone()
} else {
Text::new("Repository name:").prompt()?
};
if let Err(e) = creator.validate_repository_name(&repo_name) {
display_println!("{} Invalid repository name: {}", style("❌").red(), e);
return Ok(NextAction::Complete);
}
match creator
.create_local_repository(¤t_user, &repo_name, manager)
.await
{
Ok(_path) => {
if self.skip_configure && self.skip_open {
Ok(NextAction::Complete)
} else if self.skip_configure {
Ok(NextAction::Continue(Box::new(OpenRepositoryWorkflow {
repo_name: repo_name.clone(),
preferred_app: self.app.clone(),
})))
} else {
Ok(NextAction::Continue(Box::new(ConfigureAppWorkflow {
repo_name: repo_name.clone(),
suggested_app: self.app.clone(),
open_after: !self.skip_open,
force_configure: false, })))
}
}
Err(e) => {
display_println!("{} Failed to create repository: {}", style("❌").red(), e);
Ok(NextAction::Complete)
}
}
}
}
pub async fn execute_workflow(
workflow: Box<dyn Workflow + Send + Sync>,
manager: &mut WorkspaceManager,
) -> Result<()> {
let mut current_workflow = workflow;
loop {
display_println!(
"\n{} {}",
style("▶").blue(),
style(current_workflow.description()).dim()
);
match current_workflow.execute(manager).await? {
NextAction::Complete => {
display_println!("\n{} Workflow complete!", style("✓").green().bold());
break;
}
NextAction::Continue(next) => {
display_println!("\n{} Continuing workflow...", style("→").cyan());
current_workflow = next;
}
NextAction::Suggest(suggestions) => {
display_println!("\n{} Next steps:", style("💡").yellow());
for suggestion in suggestions {
display_println!(" {} {}", style("•").dim(), suggestion);
}
break;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_workflow_description() {
let workflow = CloneWorkflow {
url: "https://github.com/user/repo".to_string(),
app: Some("vscode".to_string()),
};
assert_eq!(
workflow.description(),
"Clone repository from https://github.com/user/repo"
);
}
}