Skip to main content

vibe_workspace/workspace/
manager.rs

1use anyhow::{Context, Result};
2use colored::*;
3use console::style;
4use std::path::{Path, PathBuf};
5use tracing::{info, warn};
6
7use crate::display_println;
8
9use crate::cache::{GitStatusCache, RepositoryCache};
10
11use super::{
12    config::{AppConfig, Repository, WorkspaceConfig},
13    discovery::{
14        discover_git_repositories, get_current_branch, get_remote_url, get_repository_name,
15    },
16    operations::{get_git_status, GitOperation, GitStatus},
17    templates::TemplateManager,
18};
19
20#[derive(Debug, Clone)]
21pub struct AppSelection {
22    pub app: String,
23    pub selected: bool,
24    pub template: Option<String>,
25    pub currently_configured: bool,
26}
27
28#[derive(Debug, Clone)]
29pub struct AppChoice {
30    pub app: String,
31    pub display: String,
32    pub is_configured: bool,
33}
34
35#[derive(Debug, Default)]
36pub struct AppConfigState {
37    pub warp: Option<String>, // template name if configured
38    pub iterm2: Option<String>,
39    pub wezterm: Option<String>,
40    pub vscode: Option<String>,
41    pub cursor: Option<String>,
42    pub windsurf: Option<String>,
43}
44
45#[derive(Debug, Clone)]
46pub struct BackupInfo {
47    pub file_name: String,
48    pub path: PathBuf,
49    pub size: u64,
50    pub created: std::time::SystemTime,
51    pub display_name: String,
52    pub contents: Option<BackupContents>,
53}
54
55#[derive(Debug, Clone)]
56pub struct BackupContents {
57    pub has_config: bool,
58    pub has_state: bool,
59    pub has_templates: bool,
60    pub app_configs: Vec<String>,
61    pub total_files: usize,
62}
63
64#[derive(Debug, Clone)]
65pub struct RepoWithStatus {
66    pub name: String,
67    pub path: String,
68    pub apps: Vec<(String, String)>, // (app_name, template)
69    pub git_status: GitStatus,
70    pub display_string: String, // Formatted for display
71}
72
73pub struct WorkspaceManager {
74    config_path: PathBuf,
75    config: WorkspaceConfig,
76    template_manager: TemplateManager,
77    repo_cache: Option<RepositoryCache>,
78    git_cache: Option<GitStatusCache>,
79}
80
81impl WorkspaceManager {
82    pub async fn new(config_path: PathBuf) -> Result<Self> {
83        let config = WorkspaceConfig::load_from_file(&config_path).await?;
84
85        let vibe_dir = super::constants::get_config_dir();
86        let template_manager = TemplateManager::new(vibe_dir.join("templates"));
87
88        // Initialize caches
89        let cache_dir = vibe_dir.join("cache");
90        let repo_cache = Self::init_repository_cache(&cache_dir).await.ok();
91        let git_cache = Self::init_git_status_cache(&cache_dir).await.ok();
92
93        Ok(Self {
94            config_path,
95            config,
96            template_manager,
97            repo_cache,
98            git_cache,
99        })
100    }
101
102    pub async fn new_with_root_override(
103        config_path: PathBuf,
104        root_override: Option<PathBuf>,
105    ) -> Result<Self> {
106        let mut config = WorkspaceConfig::load_from_file(&config_path).await?;
107
108        // Override the workspace root if specified
109        if let Some(root) = root_override {
110            let expanded_root = crate::utils::fs::expand_tilde(&root);
111            info!("Overriding workspace root to: {}", expanded_root.display());
112            config.workspace.root = expanded_root;
113        }
114
115        let vibe_dir = super::constants::get_config_dir();
116        let template_manager = TemplateManager::new(vibe_dir.join("templates"));
117
118        // Initialize caches
119        let cache_dir = vibe_dir.join("cache");
120        let repo_cache = Self::init_repository_cache(&cache_dir).await.ok();
121        let git_cache = Self::init_git_status_cache(&cache_dir).await.ok();
122
123        Ok(Self {
124            config_path,
125            config,
126            template_manager,
127            repo_cache,
128            git_cache,
129        })
130    }
131
132    pub async fn init_workspace(&mut self, name: &str, root: &Path) -> Result<()> {
133        info!("Initializing workspace '{}' in {}", name, root.display());
134
135        // Update workspace configuration
136        self.config.workspace.name = name.to_string();
137        self.config.workspace.root = root.to_path_buf();
138
139        // Auto-discover repositories if requested
140        if self.config.workspace.auto_discover {
141            let discovered = discover_git_repositories(root, 3).await?;
142
143            for repo_path in discovered {
144                let repo_name =
145                    get_repository_name(&repo_path).unwrap_or_else(|| "unknown".to_string());
146
147                let relative_path = repo_path
148                    .strip_prefix(root)
149                    .unwrap_or(&repo_path)
150                    .to_path_buf();
151
152                let mut repo = Repository::new(repo_name, relative_path);
153
154                // Try to get remote URL and branch
155                if let Ok(Some(url)) = get_remote_url(&repo_path) {
156                    repo = repo.with_url(url);
157                }
158
159                if let Ok(Some(branch)) = get_current_branch(&repo_path) {
160                    repo = repo.with_branch(branch);
161                }
162
163                self.config.add_repository(repo);
164            }
165        }
166
167        // Save configuration
168        self.save_config().await?;
169
170        // Configure Claude agents if enabled
171        if let Err(e) = super::claude_agents::configure_claude_agents(&self.config).await {
172            warn!("Failed to configure Claude agents: {}", e);
173        }
174
175        // Initialize default templates
176        if let Err(e) = self.init_templates().await {
177            warn!("Failed to initialize default templates: {}", e);
178        }
179
180        Ok(())
181    }
182
183    pub async fn discover_repositories(&self, path: &Path, depth: usize) -> Result<Vec<PathBuf>> {
184        discover_git_repositories(path, depth).await
185    }
186
187    pub async fn add_discovered_repositories(&mut self, repo_paths: &[PathBuf]) -> Result<()> {
188        let workspace_root = self.config.workspace.root.clone();
189
190        for repo_path in repo_paths {
191            let repo_name = get_repository_name(repo_path).unwrap_or_else(|| "unknown".to_string());
192
193            let relative_path = repo_path
194                .strip_prefix(&workspace_root)
195                .unwrap_or(repo_path)
196                .to_path_buf();
197
198            let mut repo = Repository::new(repo_name, relative_path);
199
200            // Try to get additional repository information
201            if let Ok(Some(url)) = get_remote_url(repo_path) {
202                repo = repo.with_url(url);
203            }
204
205            if let Ok(Some(branch)) = get_current_branch(repo_path) {
206                repo = repo.with_branch(branch);
207            }
208
209            self.config.add_repository(repo);
210        }
211
212        self.save_config().await?;
213        Ok(())
214    }
215
216    pub async fn show_status(
217        &self,
218        dirty_only: bool,
219        format: &str,
220        group: Option<&str>,
221    ) -> Result<()> {
222        use super::repo_analyzer::analyze_workspace;
223        use crate::ui::hierarchical_display::render_status_summary;
224
225        // For JSON and compact formats, use the legacy behavior
226        if format == "json" || format == "compact" {
227            return self.show_status_legacy(dirty_only, format, group).await;
228        }
229
230        println!("{} Analyzing repository status...", style("🔍").blue());
231
232        // Analyze workspace to get hierarchical organization
233        let analysis = analyze_workspace(&self.config.workspace.root, &self.config, 3).await?;
234
235        // Use hierarchical display for status
236        render_status_summary(&analysis).await;
237
238        // TODO: Add WIP branch detection and out-of-sync tracking branch detection
239        // This should scan for:
240        // - Local branches with 'dirty/' or 'wip/' prefix
241        // - Branches that are ahead/behind their tracking branch
242        // - Uncommitted changes in working directory
243        // Example implementation:
244        // - Use `git branch --list 'dirty/*' 'wip/*'` to find WIP branches
245        // - Use `git for-each-ref --format='%(refname:short) %(upstream:trackshort)'` to check tracking status
246        // - Use `git status --porcelain` to check for uncommitted changes
247
248        Ok(())
249    }
250
251    /// Legacy status implementation for JSON and compact formats
252    async fn show_status_legacy(
253        &self,
254        dirty_only: bool,
255        format: &str,
256        group: Option<&str>,
257    ) -> Result<()> {
258        let repositories = if let Some(group_name) = group {
259            self.config.get_repositories_in_group(group_name)
260        } else {
261            self.config.repositories.iter().collect()
262        };
263
264        if repositories.is_empty() {
265            println!("{} No repositories found", style("ℹ").yellow());
266            return Ok(());
267        }
268
269        let mut statuses = Vec::new();
270
271        for repo in repositories {
272            let repo_path = self.config.workspace.root.join(&repo.path);
273
274            match get_git_status(&repo_path).await {
275                Ok(status) => {
276                    if !dirty_only || status.is_dirty() {
277                        statuses.push(status);
278                    }
279                }
280                Err(e) => {
281                    warn!("Failed to get status for {}: {}", repo.name, e);
282                    eprintln!(
283                        "{} Failed to get status for {}: {}",
284                        style("⚠").yellow(),
285                        style(&repo.name).cyan(),
286                        e
287                    );
288                }
289            }
290        }
291
292        if statuses.is_empty() {
293            if dirty_only {
294                println!("{} All repositories are clean", style("✓").green());
295            } else {
296                println!("{} No repositories to display", style("ℹ").yellow());
297            }
298            return Ok(());
299        }
300
301        match format {
302            "json" => {
303                let json = serde_json::to_string_pretty(&statuses)
304                    .context("Failed to serialize status to JSON")?;
305                println!("{json}");
306            }
307            "compact" => {
308                for status in &statuses {
309                    let indicator = if status.clean {
310                        "✓".green()
311                    } else {
312                        "●".red()
313                    };
314                    println!("{} {}", indicator, status.repository_name.cyan());
315                }
316            }
317            _ => unreachable!("Legacy status only handles json and compact formats"),
318        }
319
320        Ok(())
321    }
322
323    pub async fn execute_command(
324        &self,
325        command: &str,
326        repos: Option<&str>,
327        group: Option<&str>,
328        parallel: bool,
329    ) -> Result<()> {
330        let repositories = self.get_target_repositories(repos, group);
331
332        if repositories.is_empty() {
333            println!(
334                "{} No repositories found to execute command on",
335                style("ℹ").yellow()
336            );
337            return Ok(());
338        }
339
340        println!(
341            "{} Executing '{}' on {} repositories...",
342            style("⚡").blue(),
343            style(command).cyan(),
344            repositories.len()
345        );
346
347        let operation = GitOperation::Custom(command.to_string());
348
349        if parallel {
350            // Execute commands in parallel
351            let mut tasks = Vec::new();
352
353            for repo in repositories {
354                let repo_path = self.config.workspace.root.join(&repo.path);
355                let operation = operation.clone();
356                let repo_name = repo.name.clone();
357
358                let task =
359                    tokio::spawn(async move { (repo_name, operation.execute(&repo_path).await) });
360
361                tasks.push(task);
362            }
363
364            // Wait for all tasks to complete
365            for task in tasks {
366                match task.await {
367                    Ok((repo_name, result)) => match result {
368                        Ok(output) => {
369                            if !output.trim().is_empty() {
370                                println!(
371                                    "{} {}:\n{}",
372                                    style("✓").green(),
373                                    style(&repo_name).cyan(),
374                                    output
375                                );
376                            } else {
377                                println!(
378                                    "{} {} (no output)",
379                                    style("✓").green(),
380                                    style(&repo_name).cyan()
381                                );
382                            }
383                        }
384                        Err(e) => {
385                            eprintln!(
386                                "{} {} failed: {}",
387                                style("✗").red(),
388                                style(&repo_name).cyan(),
389                                e
390                            );
391                        }
392                    },
393                    Err(e) => {
394                        eprintln!("{} Task failed: {}", style("✗").red(), e);
395                    }
396                }
397            }
398        } else {
399            // Execute commands sequentially
400            for repo in repositories {
401                let repo_path = self.config.workspace.root.join(&repo.path);
402
403                print!(
404                    "{} Executing on {}... ",
405                    style("→").dim(),
406                    style(&repo.name).cyan()
407                );
408
409                match operation.execute(&repo_path).await {
410                    Ok(output) => {
411                        println!("{}", style("✓").green());
412                        if !output.trim().is_empty() {
413                            println!("{output}");
414                        }
415                    }
416                    Err(e) => {
417                        println!("{}", style("✗").red());
418                        eprintln!("  Error: {e}");
419                    }
420                }
421            }
422        }
423
424        Ok(())
425    }
426
427    fn get_target_repositories(
428        &self,
429        repos: Option<&str>,
430        group: Option<&str>,
431    ) -> Vec<&Repository> {
432        if let Some(group_name) = group {
433            self.config.get_repositories_in_group(group_name)
434        } else if let Some(repo_names) = repos {
435            repo_names
436                .split(',')
437                .filter_map(|name| self.config.get_repository(name.trim()))
438                .collect()
439        } else {
440            self.config.repositories.iter().collect()
441        }
442    }
443
444    pub fn get_workspace_root(&self) -> &PathBuf {
445        &self.config.workspace.root
446    }
447
448    pub fn get_config_path(&self) -> &PathBuf {
449        &self.config_path
450    }
451
452    pub fn config(&self) -> &WorkspaceConfig {
453        &self.config
454    }
455
456    pub fn config_mut(&mut self) -> &mut WorkspaceConfig {
457        &mut self.config
458    }
459
460    pub async fn add_repository(&mut self, repo: Repository) -> Result<()> {
461        self.config.add_repository(repo);
462        self.save_config().await
463    }
464
465    pub fn get_config(&self) -> &WorkspaceConfig {
466        &self.config
467    }
468
469    pub fn get_template_manager(&self) -> &TemplateManager {
470        &self.template_manager
471    }
472
473    /// Scan workspace for repositories with new hierarchical display and sync options
474    pub async fn scan_repositories(
475        &mut self,
476        scan_path: &Path,
477        depth: usize,
478        import: bool,
479        restore: bool,
480        clean: bool,
481    ) -> Result<()> {
482        use super::config_validator::{deduplicate_config, validate_config};
483        use super::repo_analyzer::analyze_workspace;
484        use super::sync_operations::{execute_sync_operations, print_sync_summary, SyncOptions};
485        use crate::ui::hierarchical_display::{render_workspace_analysis, DisplayOptions};
486
487        println!(
488            "{} Scanning repositories in {} (depth: {})",
489            style("🔍").blue(),
490            style(scan_path.display()).cyan(),
491            depth
492        );
493
494        // Validate and clean up config before analysis
495        let validation_report = validate_config(&self.config, scan_path)?;
496        if validation_report.has_issues() {
497            println!();
498            validation_report.print_report();
499
500            // Ask user if they want to auto-fix duplicates
501            if !validation_report.duplicates.is_empty() {
502                println!(
503                    "{} Auto-fixing duplicate repositories...",
504                    style("🔧").blue()
505                );
506                let dedup_report = deduplicate_config(&mut self.config, scan_path)?;
507
508                if dedup_report.duplicates.len() < validation_report.duplicates.len() {
509                    println!(
510                        "{} Removed {} duplicate entries",
511                        style("✓").green(),
512                        validation_report.duplicates.len() - dedup_report.duplicates.len()
513                    );
514                    // Save the cleaned config
515                    self.save_config().await?;
516                }
517                println!();
518            }
519        }
520
521        // Analyze workspace state
522        let analysis = analyze_workspace(scan_path, &self.config, depth).await?;
523
524        // Display results with hierarchical organization
525        let display_options = DisplayOptions::default();
526        render_workspace_analysis(&analysis, &display_options);
527
528        // Set up sync options
529        let mut sync_options = SyncOptions::new();
530        if import {
531            sync_options = sync_options.with_import();
532        }
533        if restore {
534            sync_options = sync_options.with_restore();
535        }
536        if clean {
537            sync_options = sync_options.with_clean();
538        }
539
540        // Show sync summary if any actions are requested
541        if sync_options.has_actions() {
542            print_sync_summary(&analysis, &sync_options);
543
544            // Execute sync operations
545            execute_sync_operations(scan_path, &mut self.config, &analysis, &sync_options).await?;
546
547            // Save updated config
548            self.save_config().await?;
549
550            // Re-analyze workspace to show updated state
551            println!();
552            println!("{} Updated workspace state:", style("📊").blue().bold());
553            println!("{}", "─".repeat(30));
554
555            let updated_analysis = analyze_workspace(scan_path, &self.config, depth).await?;
556            render_workspace_analysis(&updated_analysis, &display_options);
557        }
558
559        Ok(())
560    }
561
562    /// Enhanced sync repositories with dirty handling
563    pub async fn sync_repositories(
564        &self,
565        fetch_only: bool,
566        prune: bool,
567        save_dirty: bool,
568        group: Option<&str>,
569    ) -> Result<()> {
570        let repositories = if let Some(group_name) = group {
571            self.config.get_repositories_in_group(group_name)
572        } else {
573            self.config.repositories.iter().collect()
574        };
575
576        if repositories.is_empty() {
577            println!("{} No repositories found", style("ℹ").yellow());
578            return Ok(());
579        }
580
581        let action = if fetch_only { "Fetching" } else { "Syncing" };
582        println!(
583            "{} {} {} repositories...",
584            style("🔄").blue(),
585            action,
586            repositories.len()
587        );
588
589        if save_dirty {
590            println!(
591                "{} Auto-commit mode enabled - dirty repositories will be committed to dirty/{{timestamp}} branches",
592                style("💾").blue()
593            );
594        }
595
596        let mut operations = vec![GitOperation::Fetch];
597        if prune {
598            operations.push(GitOperation::Custom("fetch --prune".to_string()));
599        }
600        if !fetch_only {
601            operations.push(GitOperation::Pull);
602        }
603
604        for repo in repositories {
605            let repo_path = self.config.workspace.root.join(&repo.path);
606
607            print!("{} {}... ", style("→").dim(), style(&repo.name).cyan());
608
609            // Handle dirty repositories if save_dirty is enabled
610            if save_dirty {
611                if let Err(e) = self.handle_dirty_repository(&repo_path).await {
612                    println!("{} (dirty handling failed: {})", style("âš ī¸").yellow(), e);
613                    continue;
614                }
615            }
616
617            let mut success = true;
618            for operation in &operations {
619                match operation.execute(&repo_path).await {
620                    Ok(_) => {}
621                    Err(e) => {
622                        if e.to_string().contains("dirty") && !save_dirty {
623                            println!(
624                                "{} (dirty working directory - use --save-dirty to auto-commit)",
625                                style("âš ī¸").yellow()
626                            );
627                        } else {
628                            println!("{}", style("✗").red());
629                            eprintln!("  Error: {e}");
630                        }
631                        success = false;
632                        break;
633                    }
634                }
635            }
636
637            if success {
638                println!("{}", style("✓").green());
639            }
640        }
641
642        Ok(())
643    }
644
645    /// Handle dirty repository by creating a dirty/{timestamp} branch
646    async fn handle_dirty_repository(&self, repo_path: &Path) -> Result<()> {
647        use chrono::Utc;
648        use std::process::Command;
649
650        // Check if repository is dirty
651        let status_output = Command::new("git")
652            .args(["status", "--porcelain"])
653            .current_dir(repo_path)
654            .output()?;
655
656        if status_output.stdout.is_empty() {
657            // Repository is clean, nothing to do
658            return Ok(());
659        }
660
661        // Create timestamp for branch name
662        let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
663        let branch_name = format!("dirty/{timestamp}");
664
665        // Get current branch name
666        let current_branch_output = Command::new("git")
667            .args(["branch", "--show-current"])
668            .current_dir(repo_path)
669            .output()?;
670        let current_branch = String::from_utf8_lossy(&current_branch_output.stdout)
671            .trim()
672            .to_string();
673
674        // Create and switch to dirty branch
675        Command::new("git")
676            .args(["checkout", "-b", &branch_name])
677            .current_dir(repo_path)
678            .output()?;
679
680        // Add all changes
681        Command::new("git")
682            .args(["add", "-A"])
683            .current_dir(repo_path)
684            .output()?;
685
686        // Commit changes
687        let commit_message = format!("WIP: auto-saved dirty changes from {current_branch}");
688        Command::new("git")
689            .args(["commit", "-m", &commit_message])
690            .current_dir(repo_path)
691            .output()?;
692
693        // Switch back to original branch
694        Command::new("git")
695            .args(["checkout", &current_branch])
696            .current_dir(repo_path)
697            .output()?;
698
699        Ok(())
700    }
701
702    pub async fn save_config(&self) -> Result<()> {
703        self.config.save_to_file(&self.config_path).await
704    }
705
706    pub async fn init_config(
707        &mut self,
708        name: Option<&str>,
709        root: Option<&Path>,
710        auto_discover: bool,
711    ) -> Result<()> {
712        info!("Initializing workspace configuration");
713
714        // Set workspace name
715        if let Some(n) = name {
716            self.config.workspace.name = n.to_string();
717        } else {
718            let current_dir = std::env::current_dir()?;
719            self.config.workspace.name = current_dir
720                .file_name()
721                .map(|n| n.to_string_lossy().to_string())
722                .unwrap_or_else(|| "workspace".to_string());
723        }
724
725        // Set workspace root
726        if let Some(r) = root {
727            self.config.workspace.root = r.to_path_buf();
728        } else {
729            self.config.workspace.root = std::env::current_dir()?;
730        }
731
732        // Set auto-discover
733        self.config.workspace.auto_discover = auto_discover;
734
735        // Auto-discover repositories if enabled
736        if auto_discover {
737            let discovered = discover_git_repositories(&self.config.workspace.root, 3).await?;
738            for repo_path in discovered {
739                let repo_name =
740                    get_repository_name(&repo_path).unwrap_or_else(|| "unknown".to_string());
741
742                let relative_path = repo_path
743                    .strip_prefix(&self.config.workspace.root)
744                    .unwrap_or(&repo_path)
745                    .to_path_buf();
746
747                let mut repo = Repository::new(repo_name, relative_path);
748
749                // Try to get remote URL and branch
750                if let Ok(Some(url)) = get_remote_url(&repo_path) {
751                    repo = repo.with_url(url);
752                }
753
754                if let Ok(Some(branch)) = get_current_branch(&repo_path) {
755                    repo = repo.with_branch(branch);
756                }
757
758                self.config.add_repository(repo);
759            }
760        }
761
762        // Save configuration
763        self.save_config().await?;
764
765        // Configure Claude agents if enabled
766        if let Err(e) = super::claude_agents::configure_claude_agents(&self.config).await {
767            warn!("Failed to configure Claude agents: {}", e);
768        }
769
770        println!(
771            "{} Initialized workspace '{}' in {}",
772            style("✓").green().bold(),
773            style(&self.config.workspace.name).cyan().bold(),
774            style(self.config.workspace.root.display()).dim()
775        );
776
777        if auto_discover && !self.config.repositories.is_empty() {
778            println!(
779                "{} Auto-discovered {} repositories",
780                style("📁").green(),
781                self.config.repositories.len()
782            );
783        }
784
785        Ok(())
786    }
787
788    pub async fn edit_config(&self, direct: bool) -> Result<()> {
789        use std::process::Command;
790
791        // Get editor from environment
792        let editor = std::env::var("EDITOR")
793            .or_else(|_| std::env::var("VISUAL"))
794            .unwrap_or_else(|_| {
795                if cfg!(target_os = "windows") {
796                    "notepad".to_string()
797                } else {
798                    "vi".to_string()
799                }
800            });
801
802        if !direct {
803            println!(
804                "{} Opening config file in {}...",
805                style("📝").blue(),
806                style(&editor).cyan()
807            );
808        }
809
810        // Open editor
811        let status = Command::new(&editor)
812            .arg(&self.config_path)
813            .status()
814            .with_context(|| format!("Failed to open editor: {editor}"))?;
815
816        if !status.success() {
817            anyhow::bail!("Editor exited with non-zero status");
818        }
819
820        println!(
821            "{} Configuration edited successfully",
822            style("✓").green().bold()
823        );
824
825        Ok(())
826    }
827
828    pub async fn show_config(&self, format: &str, section: Option<&str>) -> Result<()> {
829        let output = match section {
830            Some("workspace") => match format {
831                "json" => serde_json::to_string_pretty(&self.config.workspace)?,
832                "pretty" => format!(
833                    "đŸ—ī¸  Workspace Configuration\n\
834                     ━━━━━━━━━━━━━━━━━━━━━━━━\n\
835                     Name: {}\n\
836                     Root: {}\n\
837                     Auto-discover: {}",
838                    style(&self.config.workspace.name).cyan(),
839                    style(self.config.workspace.root.display()).dim(),
840                    if self.config.workspace.auto_discover {
841                        style("enabled").green()
842                    } else {
843                        style("disabled").red()
844                    }
845                ),
846                _ => serde_yaml::to_string(&self.config.workspace)?,
847            },
848            Some("repositories") => match format {
849                "json" => serde_json::to_string_pretty(&self.config.repositories)?,
850                "pretty" => {
851                    let mut output = format!(
852                        "📁 Repositories ({})\n━━━━━━━━━━━━━━━━━",
853                        self.config.repositories.len()
854                    );
855                    for repo in &self.config.repositories {
856                        output.push_str(&format!(
857                            "\n\nâ€ĸ {}\n  Path: {}\n  URL: {}\n  Branch: {}",
858                            style(&repo.name).cyan().bold(),
859                            style(repo.path.display()).dim(),
860                            repo.url.as_deref().unwrap_or("(none)"),
861                            repo.branch.as_deref().unwrap_or("(default)")
862                        ));
863                    }
864                    output
865                }
866                _ => serde_yaml::to_string(&self.config.repositories)?,
867            },
868            Some("groups") => match format {
869                "json" => serde_json::to_string_pretty(&self.config.groups)?,
870                "pretty" => {
871                    let mut output =
872                        format!("đŸ‘Ĩ Groups ({})\n━━━━━━━━━━━━", self.config.groups.len());
873                    for group in &self.config.groups {
874                        output.push_str(&format!(
875                            "\n\nâ€ĸ {}\n  Repositories: {}",
876                            style(&group.name).cyan().bold(),
877                            group.repos.join(", ")
878                        ));
879                    }
880                    output
881                }
882                _ => serde_yaml::to_string(&self.config.groups)?,
883            },
884            Some("apps") => match format {
885                "json" => serde_json::to_string_pretty(&self.config.apps)?,
886                "pretty" => {
887                    let mut output = "🔧 App Integrations\n━━━━━━━━━━━━━━━━━".to_string();
888
889                    if let Some(github) = &self.config.apps.github {
890                        output.push_str(&format!(
891                            "\n\nâ€ĸ GitHub: {}\n  Token source: {}",
892                            if github.enabled {
893                                style("enabled").green()
894                            } else {
895                                style("disabled").red()
896                            },
897                            github.token_source
898                        ));
899                    }
900
901                    if let Some(warp) = &self.config.apps.warp {
902                        output.push_str(&format!(
903                            "\n\nâ€ĸ Warp: {}\n  Config dir: {}",
904                            if warp.enabled {
905                                style("enabled").green()
906                            } else {
907                                style("disabled").red()
908                            },
909                            warp.config_dir.display()
910                        ));
911                    }
912
913                    if let Some(iterm2) = &self.config.apps.iterm2 {
914                        output.push_str(&format!(
915                            "\n\nâ€ĸ iTerm2: {}\n  Config dir: {}",
916                            if iterm2.enabled {
917                                style("enabled").green()
918                            } else {
919                                style("disabled").red()
920                            },
921                            iterm2.config_dir.display()
922                        ));
923                    }
924
925                    if let Some(vscode) = &self.config.apps.vscode {
926                        output.push_str(&format!(
927                            "\n\nâ€ĸ VSCode: {}\n  Workspace dir: {}",
928                            if vscode.enabled {
929                                style("enabled").green()
930                            } else {
931                                style("disabled").red()
932                            },
933                            vscode.workspace_dir.display()
934                        ));
935                    }
936
937                    output
938                }
939                _ => serde_yaml::to_string(&self.config.apps)?,
940            },
941            _ => match format {
942                "json" => serde_json::to_string_pretty(&self.config)?,
943                "pretty" => {
944                    // Show all sections in pretty format
945                    let mut output = String::new();
946
947                    // Workspace section
948                    output.push_str(&format!(
949                        "đŸ—ī¸  Workspace Configuration\n\
950                         ━━━━━━━━━━━━━━━━━━━━━━━━\n\
951                         Name: {}\n\
952                         Root: {}\n\
953                         Auto-discover: {}\n\n",
954                        style(&self.config.workspace.name).cyan(),
955                        style(self.config.workspace.root.display()).dim(),
956                        if self.config.workspace.auto_discover {
957                            style("enabled").green()
958                        } else {
959                            style("disabled").red()
960                        }
961                    ));
962
963                    // Repositories section
964                    output.push_str(&format!(
965                        "📁 Repositories ({})\n━━━━━━━━━━━━━━━━━",
966                        self.config.repositories.len()
967                    ));
968                    for repo in &self.config.repositories {
969                        output.push_str(&format!(
970                            "\nâ€ĸ {} ({})",
971                            style(&repo.name).cyan(),
972                            style(repo.path.display()).dim()
973                        ));
974                    }
975
976                    // Groups section
977                    if !self.config.groups.is_empty() {
978                        output.push_str(&format!(
979                            "\n\nđŸ‘Ĩ Groups ({})\n━━━━━━━━━━━",
980                            self.config.groups.len()
981                        ));
982                        for group in &self.config.groups {
983                            output.push_str(&format!(
984                                "\nâ€ĸ {} ({} repos)",
985                                style(&group.name).cyan(),
986                                group.repos.len()
987                            ));
988                        }
989                    }
990
991                    output
992                }
993                _ => serde_yaml::to_string(&self.config)?,
994            },
995        };
996
997        println!("{output}");
998        Ok(())
999    }
1000
1001    pub async fn validate_config(
1002        &self,
1003        check_paths: bool,
1004        check_remotes: bool,
1005        check_apps: bool,
1006    ) -> Result<()> {
1007        let mut issues = Vec::new();
1008        let mut warnings = Vec::new();
1009
1010        println!(
1011            "{} Validating workspace configuration...",
1012            style("🔍").blue()
1013        );
1014
1015        // Check workspace root
1016        if !self.config.workspace.root.exists() {
1017            issues.push(format!(
1018                "Workspace root does not exist: {}",
1019                self.config.workspace.root.display()
1020            ));
1021        }
1022
1023        // Check repository paths
1024        if check_paths {
1025            println!("  {} Checking repository paths...", style("→").dim());
1026            for repo in &self.config.repositories {
1027                let repo_path = self.config.workspace.root.join(&repo.path);
1028                if !repo_path.exists() {
1029                    issues.push(format!(
1030                        "Repository path does not exist: {} ({})",
1031                        repo.name,
1032                        repo_path.display()
1033                    ));
1034                } else if !repo_path.join(".git").exists() {
1035                    warnings.push(format!(
1036                        "Path exists but is not a git repository: {} ({})",
1037                        repo.name,
1038                        repo_path.display()
1039                    ));
1040                }
1041            }
1042        }
1043
1044        // Check remote URLs
1045        if check_remotes {
1046            println!("  {} Checking remote URLs...", style("→").dim());
1047            for repo in &self.config.repositories {
1048                if let Some(url) = &repo.url {
1049                    // Basic URL validation
1050                    if !url.starts_with("https://")
1051                        && !url.starts_with("git@")
1052                        && !url.starts_with("ssh://")
1053                    {
1054                        warnings.push(format!(
1055                            "Unusual remote URL format for {}: {}",
1056                            repo.name, url
1057                        ));
1058                    }
1059                }
1060            }
1061        }
1062
1063        // Check app integrations
1064        if check_apps {
1065            println!("  {} Checking app integrations...", style("→").dim());
1066
1067            if let Some(warp) = &self.config.apps.warp {
1068                if warp.enabled && !warp.config_dir.exists() {
1069                    warnings.push(format!(
1070                        "Warp config directory does not exist: {}",
1071                        warp.config_dir.display()
1072                    ));
1073                }
1074            }
1075
1076            if let Some(iterm2) = &self.config.apps.iterm2 {
1077                if iterm2.enabled && !iterm2.config_dir.exists() {
1078                    warnings.push(format!(
1079                        "iTerm2 config directory does not exist: {}",
1080                        iterm2.config_dir.display()
1081                    ));
1082                }
1083            }
1084
1085            if let Some(vscode) = &self.config.apps.vscode {
1086                if vscode.enabled && !vscode.workspace_dir.exists() {
1087                    warnings.push(format!(
1088                        "VSCode workspace directory does not exist: {}",
1089                        vscode.workspace_dir.display()
1090                    ));
1091                }
1092            }
1093        }
1094
1095        // Check groups reference existing repositories
1096        for group in &self.config.groups {
1097            for repo_name in &group.repos {
1098                if !self
1099                    .config
1100                    .repositories
1101                    .iter()
1102                    .any(|r| &r.name == repo_name)
1103                {
1104                    issues.push(format!(
1105                        "Group '{}' references non-existent repository: {}",
1106                        group.name, repo_name
1107                    ));
1108                }
1109            }
1110        }
1111
1112        // Report results
1113        println!();
1114        if issues.is_empty() && warnings.is_empty() {
1115            println!("{} Configuration is valid!", style("✓").green().bold());
1116        } else {
1117            if !issues.is_empty() {
1118                println!("{} Issues found:", style("❌").red().bold());
1119                for issue in &issues {
1120                    println!("  â€ĸ {issue}");
1121                }
1122            }
1123
1124            if !warnings.is_empty() {
1125                println!("\n{} Warnings:", style("âš ī¸").yellow().bold());
1126                for warning in &warnings {
1127                    println!("  â€ĸ {warning}");
1128                }
1129            }
1130
1131            if !issues.is_empty() {
1132                anyhow::bail!(
1133                    "Configuration validation failed with {} issues",
1134                    issues.len()
1135                );
1136            }
1137        }
1138
1139        Ok(())
1140    }
1141
1142    // App configuration management methods
1143
1144    /// Configure an app for a repository
1145    pub async fn configure_app_for_repo(
1146        &mut self,
1147        repo_name: &str,
1148        app: &str,
1149        template: &str,
1150    ) -> Result<()> {
1151        let repo = self
1152            .config
1153            .repositories
1154            .iter_mut()
1155            .find(|r| r.name == repo_name)
1156            .context("Repository not found")?;
1157
1158        // Check if template exists
1159        let templates = self.template_manager.list_templates(app).await?;
1160        if !templates.contains(&template.to_string()) {
1161            anyhow::bail!("Template '{}' not found for app '{}'", template, app);
1162        }
1163
1164        repo.apps.insert(
1165            app.to_string(),
1166            AppConfig::WithTemplate {
1167                template: template.to_string(),
1168            },
1169        );
1170
1171        self.config.save_to_file(&self.config_path).await?;
1172
1173        Ok(())
1174    }
1175
1176    /// List configured apps for a repository
1177    pub fn list_apps_for_repo(&self, repo_name: &str) -> Result<Vec<(String, String)>> {
1178        let repo = self
1179            .config
1180            .repositories
1181            .iter()
1182            .find(|r| r.name == repo_name)
1183            .context("Repository not found")?;
1184
1185        let mut apps = Vec::new();
1186        for (app_name, config) in &repo.apps {
1187            if let AppConfig::WithTemplate { template } = config {
1188                apps.push((app_name.clone(), template.clone()));
1189            } else if config.is_enabled() {
1190                apps.push((app_name.clone(), "default".to_string()));
1191            }
1192        }
1193
1194        Ok(apps)
1195    }
1196
1197    /// Get repositories that have at least one app configured, with git status
1198    pub async fn get_repos_with_apps_and_status(&self) -> Result<Vec<RepoWithStatus>> {
1199        let mut repos_with_status = Vec::new();
1200
1201        for repo in &self.config.repositories {
1202            let apps = self.list_apps_for_repo(&repo.name)?;
1203
1204            // Only include repos that have at least one app configured
1205            if !apps.is_empty() {
1206                let repo_path = self.config.workspace.root.join(&repo.path);
1207                let git_status = get_git_status(&repo_path)
1208                    .await
1209                    .unwrap_or_else(|_| GitStatus {
1210                        repository_name: repo.name.clone(),
1211                        path: repo.path.display().to_string(),
1212                        branch: None,
1213                        clean: true,
1214                        ahead: 0,
1215                        behind: 0,
1216                        staged: 0,
1217                        unstaged: 0,
1218                        untracked: 0,
1219                        remote_url: None,
1220                    });
1221
1222                // Create display string with status indicators
1223                let status_indicator = if git_status.clean {
1224                    "✓".to_string()
1225                } else {
1226                    let mut indicators = Vec::new();
1227                    if git_status.staged > 0 {
1228                        indicators.push(format!("{}S", git_status.staged));
1229                    }
1230                    if git_status.unstaged > 0 {
1231                        indicators.push(format!("{}U", git_status.unstaged));
1232                    }
1233                    if git_status.untracked > 0 {
1234                        indicators.push(format!("{}?", git_status.untracked));
1235                    }
1236                    if git_status.ahead > 0 {
1237                        indicators.push(format!("↑{}", git_status.ahead));
1238                    }
1239                    if git_status.behind > 0 {
1240                        indicators.push(format!("↓{}", git_status.behind));
1241                    }
1242                    if indicators.is_empty() {
1243                        "●".to_string()
1244                    } else {
1245                        indicators.join(" ")
1246                    }
1247                };
1248
1249                let app_names: Vec<String> = apps.iter().map(|(name, _)| name.clone()).collect();
1250                let display_string = format!(
1251                    "{} [{}] (apps: {})",
1252                    repo.name,
1253                    status_indicator,
1254                    app_names.join(", ")
1255                );
1256
1257                repos_with_status.push(RepoWithStatus {
1258                    name: repo.name.clone(),
1259                    path: repo.path.display().to_string(),
1260                    apps,
1261                    git_status,
1262                    display_string,
1263                });
1264            }
1265        }
1266
1267        Ok(repos_with_status)
1268    }
1269
1270    /// List repositories configured with a specific app
1271    pub fn list_repos_with_app(&self, app: &str) -> Vec<(&Repository, String)> {
1272        let mut repos = Vec::new();
1273
1274        for repo in &self.config.repositories {
1275            if let Some(config) = repo.apps.get(app) {
1276                if config.is_enabled() {
1277                    let template = match config {
1278                        AppConfig::WithTemplate { template } => template.clone(),
1279                        AppConfig::WithConfig { template, .. } => template.clone(),
1280                        AppConfig::Enabled(_) => "default".to_string(),
1281                    };
1282                    repos.push((repo, template));
1283                }
1284            }
1285        }
1286
1287        repos
1288    }
1289
1290    /// Initialize default templates if they don't exist
1291    pub async fn init_templates(&self) -> Result<()> {
1292        self.template_manager.init_default_templates().await?;
1293
1294        println!(
1295            "{} Initialized default templates in {}",
1296            style("✓").green().bold(),
1297            style(super::constants::CONFIG_DIR_DISPLAY)
1298                .cyan()
1299                .to_string()
1300                + "/templates"
1301        );
1302
1303        Ok(())
1304    }
1305
1306    /// List available templates for an app
1307    pub async fn list_templates(&self, app: &str) -> Result<Vec<String>> {
1308        self.template_manager.list_templates(app).await
1309    }
1310
1311    /// Create a new template from an existing one
1312    pub async fn create_template(
1313        &self,
1314        app: &str,
1315        template_name: &str,
1316        from_template: &str,
1317    ) -> Result<()> {
1318        let content = self
1319            .template_manager
1320            .load_template(app, from_template)
1321            .await?;
1322        self.template_manager
1323            .save_template(app, template_name, &content)
1324            .await?;
1325
1326        println!(
1327            "{} Created template '{}' for {}",
1328            style("✓").green().bold(),
1329            style(template_name).cyan(),
1330            style(app).cyan()
1331        );
1332
1333        Ok(())
1334    }
1335
1336    /// Delete a template
1337    pub async fn delete_template(&self, app: &str, template_name: &str) -> Result<()> {
1338        if template_name == "default" {
1339            anyhow::bail!("Cannot delete the default template");
1340        }
1341
1342        self.template_manager
1343            .delete_template(app, template_name)
1344            .await?;
1345
1346        println!(
1347            "{} Deleted template '{}' for {}",
1348            style("✓").green().bold(),
1349            style(template_name).cyan(),
1350            style(app).cyan()
1351        );
1352
1353        Ok(())
1354    }
1355
1356    /// Show configured apps for all repositories
1357    pub async fn show_app_configurations(&self) -> Result<()> {
1358        println!("\n{} App Configurations:", style("📱").blue());
1359        println!();
1360
1361        for repo in &self.config.repositories {
1362            if repo.apps.is_empty() {
1363                continue;
1364            }
1365
1366            println!("{} {}", style("→").dim(), style(&repo.name).cyan().bold());
1367            for (app_name, config) in &repo.apps {
1368                if config.is_enabled() {
1369                    let template = match config {
1370                        AppConfig::WithTemplate { template } => template.as_str(),
1371                        AppConfig::WithConfig { template, .. } => template.as_str(),
1372                        AppConfig::Enabled(_) => "default",
1373                    };
1374                    println!(
1375                        "    {} {} (template: {})",
1376                        style("â€ĸ").dim(),
1377                        style(app_name).green(),
1378                        style(template).yellow()
1379                    );
1380                }
1381            }
1382            println!();
1383        }
1384        Ok(())
1385    }
1386
1387    /// Get the default template content for an app
1388    pub async fn get_default_template(&self, app: &str) -> Result<String> {
1389        // Try to load from file first
1390        match self.template_manager.load_template(app, "default").await {
1391            Ok(content) => Ok(content),
1392            Err(_) => {
1393                // Fall back to bundled defaults
1394                let default_content = match app {
1395                    "warp" => crate::workspace::templates::DEFAULT_WARP_TEMPLATE,
1396                    "iterm2" => crate::workspace::templates::DEFAULT_ITERM2_TEMPLATE,
1397                    "wezterm" => crate::workspace::templates::DEFAULT_WEZTERM_TEMPLATE,
1398                    "vscode" => crate::workspace::templates::DEFAULT_VSCODE_TEMPLATE,
1399                    "cursor" => crate::workspace::templates::DEFAULT_CURSOR_TEMPLATE,
1400                    "windsurf" => crate::workspace::templates::DEFAULT_WINDSURF_TEMPLATE,
1401                    _ => anyhow::bail!("Unknown app '{}' and no default template found", app),
1402                };
1403                Ok(default_content.to_string())
1404            }
1405        }
1406    }
1407
1408    /// Save a template with content
1409    pub async fn save_template(&self, app: &str, name: &str, content: &str) -> Result<()> {
1410        self.template_manager
1411            .save_template(app, name, content)
1412            .await
1413    }
1414
1415    /// Update default templates with current bundled versions
1416    pub async fn update_default_templates(&self, apps: Vec<String>) -> Result<()> {
1417        for app in apps {
1418            let default_content = match app.as_str() {
1419                "warp" => crate::workspace::templates::DEFAULT_WARP_TEMPLATE,
1420                "iterm2" => crate::workspace::templates::DEFAULT_ITERM2_TEMPLATE,
1421                "wezterm" => crate::workspace::templates::DEFAULT_WEZTERM_TEMPLATE,
1422                "vscode" => crate::workspace::templates::DEFAULT_VSCODE_TEMPLATE,
1423                "cursor" => crate::workspace::templates::DEFAULT_CURSOR_TEMPLATE,
1424                "windsurf" => crate::workspace::templates::DEFAULT_WINDSURF_TEMPLATE,
1425                _ => {
1426                    println!("{} Unknown app '{}', skipping", style("âš ī¸").yellow(), app);
1427                    continue;
1428                }
1429            };
1430
1431            self.template_manager
1432                .save_template(&app, "default", default_content)
1433                .await?;
1434            println!(
1435                "{} Updated default template for {}",
1436                style("✓").green(),
1437                style(&app).cyan()
1438            );
1439        }
1440
1441        Ok(())
1442    }
1443
1444    /// Smart open repository - shows app choice menu with configured and available apps
1445    pub async fn smart_open_repository(&self, repo_name: &str) -> Result<()> {
1446        let repo = self
1447            .config
1448            .repositories
1449            .iter()
1450            .find(|r| r.name == repo_name)
1451            .context("Repository not found")?;
1452
1453        // Get configured apps (if any)
1454        let configured_apps: Vec<String> = repo.apps.keys().cloned().collect();
1455
1456        // Get all available apps on system
1457        let available_apps = self.get_available_apps().await;
1458
1459        // Build app choice menu
1460        let app_choices = self.build_app_choice_menu(&configured_apps, &available_apps);
1461
1462        if app_choices.is_empty() {
1463            anyhow::bail!("No compatible apps found on this system");
1464        }
1465
1466        // Prompt user to choose app
1467        let selected_app = self.prompt_app_selection(&app_choices)?;
1468
1469        // Open with selected app using existing logic
1470        self.open_repo_with_app_options(repo_name, &selected_app, false)
1471            .await
1472    }
1473
1474    /// Open a repository with a configured app
1475    pub async fn open_repo_with_app(&self, repo_name: &str, app: &str) -> Result<()> {
1476        self.open_repo_with_app_options(repo_name, app, false).await
1477    }
1478
1479    /// Open a repository with a configured app with options
1480    pub async fn open_repo_with_app_options(
1481        &self,
1482        repo_name: &str,
1483        app: &str,
1484        no_itermocil: bool,
1485    ) -> Result<()> {
1486        let repo = self
1487            .config
1488            .repositories
1489            .iter()
1490            .find(|r| r.name == repo_name)
1491            .context("Repository not found")?;
1492
1493        // Use configured opening if available, otherwise fall back to basic opening
1494        if repo.is_app_enabled(app) {
1495            // Use configured opening with templates and automation
1496            self.open_repo_with_configured_app(repo, app, no_itermocil)
1497                .await
1498        } else {
1499            // Use basic opening without configuration
1500            self.open_repo_with_basic_app(repo, app).await
1501        }
1502    }
1503
1504    /// Open repository with configured app (templates and automation)
1505    async fn open_repo_with_configured_app(
1506        &self,
1507        repo: &Repository,
1508        app: &str,
1509        no_itermocil: bool,
1510    ) -> Result<()> {
1511        match app {
1512            "warp" => {
1513                crate::apps::open_with_warp(&self.config, repo, &self.template_manager).await?;
1514            }
1515            "iterm2" => {
1516                crate::apps::open_with_iterm2_options(
1517                    &self.config,
1518                    repo,
1519                    &self.template_manager,
1520                    no_itermocil,
1521                )
1522                .await?;
1523            }
1524            "wezterm" => {
1525                crate::apps::open_with_wezterm_options(
1526                    &self.config,
1527                    repo,
1528                    &self.template_manager,
1529                    no_itermocil,
1530                )
1531                .await?;
1532            }
1533            "vscode" => {
1534                crate::apps::open_with_vscode(&self.config, repo, &self.template_manager).await?;
1535            }
1536            "cursor" => {
1537                crate::apps::open_with_cursor(&self.config, repo, &self.template_manager).await?;
1538            }
1539            "windsurf" => {
1540                crate::apps::open_with_windsurf(&self.config, repo, &self.template_manager).await?;
1541            }
1542            _ => {
1543                anyhow::bail!("Unknown app: {}", app);
1544            }
1545        }
1546
1547        Ok(())
1548    }
1549
1550    /// Open repository with basic app (no configuration required)
1551    async fn open_repo_with_basic_app(&self, repo: &Repository, app: &str) -> Result<()> {
1552        // Get full path to repository
1553        let repo_path = self.config.workspace.root.join(&repo.path);
1554
1555        // Check if app is available on system
1556        if !self.is_app_available(app).await {
1557            anyhow::bail!("App '{}' is not available on this system", app);
1558        }
1559
1560        println!(
1561            "{} Opening {} with {} (basic mode - no custom templates)",
1562            style("📂").blue(),
1563            style(&repo.name).cyan(),
1564            style(app).blue()
1565        );
1566
1567        match app {
1568            "vscode" => {
1569                // Basic: Open folder directly with code command
1570                let status = std::process::Command::new("code")
1571                    .arg(&repo_path)
1572                    .status()
1573                    .context("Failed to execute VS Code")?;
1574
1575                if !status.success() {
1576                    anyhow::bail!("VS Code failed to open repository");
1577                }
1578            }
1579            "cursor" => {
1580                // Basic: Open folder directly with cursor command
1581                let status = std::process::Command::new("cursor")
1582                    .arg(&repo_path)
1583                    .status()
1584                    .context("Failed to execute Cursor")?;
1585
1586                if !status.success() {
1587                    anyhow::bail!("Cursor failed to open repository");
1588                }
1589            }
1590            "windsurf" => {
1591                // Basic: Open folder directly with windsurf command
1592                let status = std::process::Command::new("windsurf")
1593                    .arg(&repo_path)
1594                    .status()
1595                    .context("Failed to execute Windsurf")?;
1596
1597                if !status.success() {
1598                    anyhow::bail!("Windsurf failed to open repository");
1599                }
1600            }
1601            "warp" => {
1602                // Basic: Open new tab in Warp with cd to repo
1603                let status = std::process::Command::new("open")
1604                    .args(["-a", "Warp", &format!("--args cd {}", repo_path.display())])
1605                    .status()
1606                    .context("Failed to execute Warp")?;
1607
1608                if !status.success() {
1609                    anyhow::bail!("Warp failed to open repository");
1610                }
1611            }
1612            "iterm2" => {
1613                // Basic: Open new tab in iTerm2 with cd to repo
1614                let applescript = format!(
1615                    r#"tell application "iTerm2"
1616                        activate
1617                        tell current window
1618                            create tab with default profile
1619                            tell current session
1620                                write text "cd '{}'"
1621                            end tell
1622                        end tell
1623                    end tell"#,
1624                    repo_path.display()
1625                );
1626
1627                let status = std::process::Command::new("osascript")
1628                    .args(["-e", &applescript])
1629                    .status()
1630                    .context("Failed to execute iTerm2 AppleScript")?;
1631
1632                if !status.success() {
1633                    anyhow::bail!("iTerm2 failed to open repository");
1634                }
1635            }
1636            "wezterm" => {
1637                // Basic: Open new tab in WezTerm with cd to repo
1638                let status = std::process::Command::new("wezterm")
1639                    .args(["cli", "spawn", "--cwd", &repo_path.to_string_lossy()])
1640                    .status()
1641                    .context("Failed to execute WezTerm")?;
1642
1643                if !status.success() {
1644                    anyhow::bail!("WezTerm failed to open repository");
1645                }
1646            }
1647            _ => {
1648                anyhow::bail!("Unknown app: {}", app);
1649            }
1650        }
1651
1652        println!(
1653            "{} Successfully opened {} with {}",
1654            style("✓").green(),
1655            style(&repo.name).cyan(),
1656            style(app).blue()
1657        );
1658
1659        Ok(())
1660    }
1661
1662    /// Get current app configuration states for a repository
1663    pub fn get_current_app_states(&self, repo_name: &str) -> Result<AppConfigState> {
1664        let repo = self
1665            .config
1666            .repositories
1667            .iter()
1668            .find(|r| r.name == repo_name)
1669            .context("Repository not found")?;
1670
1671        let mut state = AppConfigState::default();
1672
1673        for (app_name, config) in &repo.apps {
1674            if config.is_enabled() {
1675                let template = match config {
1676                    AppConfig::WithTemplate { template } => template.clone(),
1677                    AppConfig::WithConfig { template, .. } => template.clone(),
1678                    AppConfig::Enabled(_) => "default".to_string(),
1679                };
1680
1681                match app_name.as_str() {
1682                    "warp" => state.warp = Some(template),
1683                    "iterm2" => state.iterm2 = Some(template),
1684                    "wezterm" => state.wezterm = Some(template),
1685                    "vscode" => state.vscode = Some(template),
1686                    "cursor" => state.cursor = Some(template),
1687                    "windsurf" => state.windsurf = Some(template),
1688                    _ => {} // ignore unknown apps
1689                }
1690            }
1691        }
1692
1693        Ok(state)
1694    }
1695
1696    /// Remove app configuration for a repository
1697    pub async fn remove_app_for_repo(&mut self, repo_name: &str, app: &str) -> Result<()> {
1698        let repo = self
1699            .config
1700            .repositories
1701            .iter_mut()
1702            .find(|r| r.name == repo_name)
1703            .context("Repository not found")?;
1704
1705        repo.apps.remove(app);
1706        self.config.save_to_file(&self.config_path).await?;
1707
1708        Ok(())
1709    }
1710
1711    /// Clean up app-generated files for a repository
1712    pub async fn cleanup_app_files(&self, repo_name: &str, app: &str) -> Result<()> {
1713        let repo = self
1714            .config
1715            .repositories
1716            .iter()
1717            .find(|r| r.name == repo_name)
1718            .context("Repository not found")?;
1719
1720        match app {
1721            "warp" => {
1722                crate::apps::cleanup_warp_config(&self.config, repo).await?;
1723            }
1724            "iterm2" => {
1725                crate::apps::cleanup_iterm2_config(&self.config, repo).await?;
1726            }
1727            "wezterm" => {
1728                crate::apps::cleanup_wezterm_config(&self.config, repo).await?;
1729            }
1730            "vscode" => {
1731                crate::apps::cleanup_vscode_config(&self.config, repo).await?;
1732            }
1733            "cursor" => {
1734                crate::apps::cleanup_cursor_config(&self.config, repo).await?;
1735            }
1736            "windsurf" => {
1737                crate::apps::cleanup_windsurf_config(&self.config, repo).await?;
1738            }
1739            _ => {
1740                warn!("Unknown app '{}' for cleanup", app);
1741            }
1742        }
1743
1744        Ok(())
1745    }
1746
1747    /// Configure multiple apps for a repository
1748    pub async fn configure_multiple_apps(
1749        &mut self,
1750        repo_name: &str,
1751        app_selections: Vec<AppSelection>,
1752    ) -> Result<Vec<String>> {
1753        let mut changes = Vec::new();
1754
1755        // Get current state to determine what needs to be added/removed
1756        let current_state = self.get_current_app_states(repo_name)?;
1757        let current_apps = [
1758            ("warp", current_state.warp.as_ref()),
1759            ("iterm2", current_state.iterm2.as_ref()),
1760            ("vscode", current_state.vscode.as_ref()),
1761            ("cursor", current_state.cursor.as_ref()),
1762            ("windsurf", current_state.windsurf.as_ref()),
1763        ];
1764
1765        // Process each app selection
1766        for selection in app_selections {
1767            let currently_configured = current_apps
1768                .iter()
1769                .find(|(app, _)| *app == selection.app)
1770                .map(|(_, template)| template.is_some())
1771                .unwrap_or(false);
1772
1773            if selection.selected && !currently_configured {
1774                // Add new app configuration
1775                let template = selection.template.as_deref().unwrap_or("default");
1776                self.configure_app_for_repo(repo_name, &selection.app, template)
1777                    .await?;
1778                changes.push(format!(
1779                    "✅ Configured {} with template '{}'",
1780                    selection.app, template
1781                ));
1782            } else if selection.selected && currently_configured {
1783                // Update existing app configuration if template changed
1784                let current_template = current_apps
1785                    .iter()
1786                    .find(|(app, _)| *app == selection.app)
1787                    .and_then(|(_, template)| template.as_ref())
1788                    .map(|s| s.as_str())
1789                    .unwrap_or("default");
1790
1791                let new_template = selection.template.as_deref().unwrap_or("default");
1792                if current_template != new_template {
1793                    self.configure_app_for_repo(repo_name, &selection.app, new_template)
1794                        .await?;
1795                    changes.push(format!(
1796                        "🔄 Updated {} template to '{}'",
1797                        selection.app, new_template
1798                    ));
1799                }
1800            } else if !selection.selected && currently_configured {
1801                // Remove app configuration and clean up files
1802                self.cleanup_app_files(repo_name, &selection.app).await?;
1803                self.remove_app_for_repo(repo_name, &selection.app).await?;
1804                changes.push(format!("đŸ—‘ī¸  Removed {} configuration", selection.app));
1805            }
1806        }
1807
1808        Ok(changes)
1809    }
1810
1811    /// Discover all configuration files that would be affected by reset or backup
1812    async fn discover_all_config_files(&self) -> Result<Vec<PathBuf>> {
1813        let mut config_files = Vec::new();
1814
1815        // Main config file
1816        if self.config_path.exists() {
1817            config_files.push(self.config_path.clone());
1818        }
1819
1820        let vibe_dir = super::constants::get_config_dir();
1821
1822        // State file (user preferences and recent repos)
1823        let state_file = vibe_dir.join("state.json");
1824        if state_file.exists() {
1825            config_files.push(state_file);
1826        }
1827
1828        // Templates directory
1829        let templates_dir = vibe_dir.join("templates");
1830        if templates_dir.exists() {
1831            config_files.push(templates_dir);
1832        }
1833
1834        // App configuration files for each repository
1835        for repo in &self.config.repositories {
1836            // Skip repos that don't have any app configurations
1837            if repo.apps.is_empty() {
1838                continue;
1839            }
1840
1841            for app in repo.apps.keys() {
1842                match app.as_str() {
1843                    "warp" => {
1844                        if let Some(warp_integration) = &self.config.apps.warp {
1845                            let config_name =
1846                                format!("vibe-{}-{}.yaml", self.config.workspace.name, repo.name);
1847                            let config_path = warp_integration.config_dir.join(&config_name);
1848                            if config_path.exists() {
1849                                config_files.push(config_path);
1850                            }
1851                        }
1852                    }
1853                    "iterm2" => {
1854                        if let Some(iterm2_integration) = &self.config.apps.iterm2 {
1855                            let config_name =
1856                                format!("vibe-{}-{}.json", self.config.workspace.name, repo.name);
1857                            let config_path = iterm2_integration.config_dir.join(&config_name);
1858                            if config_path.exists() {
1859                                config_files.push(config_path);
1860                            }
1861                        }
1862                    }
1863                    "wezterm" => {
1864                        if let Some(wezterm_integration) = &self.config.apps.wezterm {
1865                            let config_name =
1866                                format!("vibe-{}-{}.lua", self.config.workspace.name, repo.name);
1867                            let config_path = wezterm_integration.config_dir.join(&config_name);
1868                            if config_path.exists() {
1869                                config_files.push(config_path);
1870                            }
1871                        }
1872                    }
1873                    "vscode" => {
1874                        if let Some(vscode_integration) = &self.config.apps.vscode {
1875                            let config_name = format!(
1876                                "vibe-{}-{}.code-workspace",
1877                                self.config.workspace.name, repo.name
1878                            );
1879                            let config_path = vscode_integration.workspace_dir.join(&config_name);
1880                            if config_path.exists() {
1881                                config_files.push(config_path);
1882                            }
1883                        }
1884                    }
1885                    _ => {}
1886                }
1887            }
1888        }
1889
1890        Ok(config_files)
1891    }
1892
1893    /// Clean up all app configuration files for all repositories
1894    async fn cleanup_all_app_configs(&self) -> Result<()> {
1895        for repo in &self.config.repositories {
1896            for app in repo.apps.keys() {
1897                if let Err(e) = self.cleanup_app_files(&repo.name, app).await {
1898                    warn!("Failed to cleanup {} config for {}: {}", app, repo.name, e);
1899                }
1900            }
1901        }
1902        Ok(())
1903    }
1904
1905    /// Create a backup archive of all configuration files
1906    pub async fn create_backup(
1907        &self,
1908        output_dir: Option<PathBuf>,
1909        custom_name: Option<String>,
1910    ) -> Result<PathBuf> {
1911        use chrono::Utc;
1912        use std::process::Command;
1913
1914        // Determine output directory - default to ~/.toolprint/vibe-workspace/backups/
1915        let backup_dir = output_dir.unwrap_or_else(|| {
1916            dirs::home_dir()
1917                .unwrap_or_else(|| PathBuf::from("."))
1918                .join(super::constants::CONFIG_DIR_PATH)
1919                .join("backups")
1920        });
1921
1922        // Create backup directory if it doesn't exist
1923        tokio::fs::create_dir_all(&backup_dir)
1924            .await
1925            .with_context(|| {
1926                format!(
1927                    "Failed to create backup directory: {}",
1928                    backup_dir.display()
1929                )
1930            })?;
1931
1932        // Create timestamped backup name
1933        let timestamp = Utc::now().format("%Y%m%d-%H%M%S");
1934        let backup_name = custom_name.unwrap_or_else(|| format!("vibe-backup-{timestamp}"));
1935        let backup_filename = format!("{backup_name}.tgz");
1936        let backup_path = backup_dir.join(&backup_filename);
1937
1938        println!("{} Creating backup archive...", style("đŸ“Ļ").blue());
1939
1940        // Discover all configuration files
1941        let config_files = self.discover_all_config_files().await?;
1942
1943        if config_files.is_empty() {
1944            println!(
1945                "{} No configuration files found to backup",
1946                style("âš ī¸").yellow()
1947            );
1948            return Ok(backup_path);
1949        }
1950
1951        // Create temporary directory for organizing backup content
1952        let temp_dir = tempfile::tempdir().context("Failed to create temporary directory")?;
1953        let temp_path = temp_dir.path();
1954
1955        // Copy files to temporary directory with organized structure
1956        for config_file in &config_files {
1957            let file_name = config_file
1958                .file_name()
1959                .context("Invalid file name")?
1960                .to_string_lossy();
1961
1962            if config_file == &self.config_path {
1963                // Main config file goes to root
1964                let dest_path = temp_path.join("config.yaml");
1965                tokio::fs::copy(config_file, &dest_path)
1966                    .await
1967                    .with_context(|| format!("Failed to copy {}", config_file.display()))?;
1968            } else if file_name == "state.json" {
1969                // State file goes to root
1970                let dest_path = temp_path.join("state.json");
1971                tokio::fs::copy(config_file, &dest_path)
1972                    .await
1973                    .with_context(|| format!("Failed to copy {}", config_file.display()))?;
1974            } else if config_file.to_string_lossy().contains("templates") {
1975                // Templates directory
1976                let dest_dir = temp_path.join("templates");
1977                tokio::fs::create_dir_all(&dest_dir).await?;
1978                copy_dir_recursive(config_file, &dest_dir)?;
1979            } else {
1980                // App config files - organize by app type
1981                let app_type = if file_name.ends_with(".yaml") {
1982                    "warp"
1983                } else if file_name.ends_with(".json") && file_name != "state.json" {
1984                    "iterm2"
1985                } else if file_name.ends_with(".lua") {
1986                    "wezterm"
1987                } else if file_name.ends_with(".code-workspace") {
1988                    "vscode"
1989                } else {
1990                    "other"
1991                };
1992
1993                let app_dir = temp_path.join("app-configs").join(app_type);
1994                tokio::fs::create_dir_all(&app_dir).await?;
1995                let dest_path = app_dir.join(file_name.as_ref());
1996                tokio::fs::copy(config_file, &dest_path)
1997                    .await
1998                    .with_context(|| format!("Failed to copy {}", config_file.display()))?;
1999            }
2000        }
2001
2002        // Create tar archive
2003        let tar_output = Command::new("tar")
2004            .args(["-czf"])
2005            .arg(&backup_path)
2006            .args(["-C"])
2007            .arg(temp_path)
2008            .arg(".")
2009            .output()
2010            .context("Failed to execute tar command")?;
2011
2012        if !tar_output.status.success() {
2013            let error_msg = String::from_utf8_lossy(&tar_output.stderr);
2014            anyhow::bail!("Tar command failed: {}", error_msg);
2015        }
2016
2017        println!(
2018            "{} Backup contains {} configuration files:",
2019            style("📋").green(),
2020            config_files.len()
2021        );
2022        for file in &config_files {
2023            println!("  {} {}", style("→").dim(), style(file.display()).cyan());
2024        }
2025
2026        Ok(backup_path)
2027    }
2028
2029    /// Factory reset - clear all configuration and reinitialize
2030    pub async fn factory_reset(&mut self, force: bool) -> Result<()> {
2031        self.factory_reset_with_options(force, false).await
2032    }
2033
2034    /// Reset repository configuration only (clear all tracked repositories)
2035    pub async fn reset_repositories(&mut self, force: bool) -> Result<()> {
2036        let repo_count = self.config.repositories.len();
2037
2038        if repo_count == 0 {
2039            println!("{} No repositories to reset", style("â„šī¸").blue());
2040            return Ok(());
2041        }
2042
2043        if !force {
2044            println!(
2045                "{} This will remove all {} tracked repositories from your configuration",
2046                style("âš ī¸").yellow(),
2047                style(repo_count).bold()
2048            );
2049            println!(
2050                "{} This will NOT delete the actual repository folders",
2051                style("â„šī¸").blue()
2052            );
2053            println!();
2054
2055            // Show repositories that will be removed
2056            println!(
2057                "{} Repositories to be removed from config:",
2058                style("📋").blue()
2059            );
2060            for repo in &self.config.repositories {
2061                println!(
2062                    "  {} {} ({})",
2063                    style("→").dim(),
2064                    style(&repo.name).cyan(),
2065                    style(repo.path.display()).dim()
2066                );
2067            }
2068            println!();
2069
2070            use inquire::Confirm;
2071            let confirm = Confirm::new("Continue with repository reset?")
2072                .with_default(false)
2073                .prompt()
2074                .context("Failed to get user confirmation")?;
2075
2076            if !confirm {
2077                println!("{} Repository reset cancelled", style("✓").green());
2078                return Ok(());
2079            }
2080        }
2081
2082        // Clear repositories from config
2083        self.config.repositories.clear();
2084
2085        // Save the updated config
2086        self.config
2087            .save_to_file(&self.config_path)
2088            .await
2089            .context("Failed to save updated configuration")?;
2090
2091        println!(
2092            "{} Cleared {} repositories from configuration",
2093            style("✅").green().bold(),
2094            style(repo_count).bold()
2095        );
2096        println!(
2097            "{} Use 'vibe git scan --import' to re-discover repositories",
2098            style("💡").blue()
2099        );
2100
2101        Ok(())
2102    }
2103
2104    pub async fn factory_reset_with_options(
2105        &mut self,
2106        force: bool,
2107        skip_final_confirmation: bool,
2108    ) -> Result<()> {
2109        if !force {
2110            // Show warning and get confirmation
2111            display_println!(
2112                "{} {}",
2113                style("âš ī¸  WARNING").red().bold(),
2114                style("This will permanently delete ALL vibe-workspace configuration!").red()
2115            );
2116            display_println!();
2117
2118            // Discover and show files that will be deleted
2119            let config_files = self.discover_all_config_files().await?;
2120
2121            if !config_files.is_empty() {
2122                display_println!("{} The following files will be deleted:", style("đŸ—‘ī¸").red());
2123                for file in &config_files {
2124                    display_println!("  {} {}", style("×").red(), style(file.display()).dim());
2125                }
2126                display_println!();
2127            }
2128
2129            // Require typing exact confirmation
2130            use inquire::Text;
2131            let confirmation = Text::new("Type 'reset my vibe' to confirm factory reset:")
2132                .prompt()
2133                .context("Failed to get user confirmation")?;
2134
2135            if confirmation != "reset my vibe" {
2136                display_println!(
2137                    "{} Vibe Check: make sure you're ready for irreversable change and try again",
2138                    style("🔍").yellow()
2139                );
2140                return Ok(());
2141            }
2142
2143            // Final confirmation (only if not skipped)
2144            if !skip_final_confirmation {
2145                use inquire::Confirm;
2146                let final_confirm = Confirm::new("Are you absolutely sure? This cannot be undone.")
2147                    .with_default(false)
2148                    .prompt()
2149                    .context("Failed to get final confirmation")?;
2150
2151                if !final_confirm {
2152                    display_println!("{} Vibe Check: make sure you're ready for irreversable change and try again", style("🔍").yellow());
2153                    return Ok(());
2154                }
2155            }
2156        }
2157
2158        display_println!("{} Performing factory reset...", style("🔄").blue());
2159
2160        // Clean up all app configuration files first
2161        self.cleanup_all_app_configs().await?;
2162
2163        // Delete main config file
2164        if self.config_path.exists() {
2165            tokio::fs::remove_file(&self.config_path)
2166                .await
2167                .with_context(|| {
2168                    format!(
2169                        "Failed to remove config file: {}",
2170                        self.config_path.display()
2171                    )
2172                })?;
2173            display_println!("{} Removed main configuration file", style("✓").green());
2174        }
2175
2176        // Delete templates directory
2177        let vibe_dir = super::constants::get_config_dir();
2178        let templates_dir = vibe_dir.join("templates");
2179        if templates_dir.exists() {
2180            tokio::fs::remove_dir_all(&templates_dir)
2181                .await
2182                .with_context(|| {
2183                    format!(
2184                        "Failed to remove templates directory: {}",
2185                        templates_dir.display()
2186                    )
2187                })?;
2188            display_println!("{} Removed templates directory", style("✓").green());
2189        }
2190
2191        // Delete cache directory
2192        let cache_dir = vibe_dir.join("cache");
2193        if cache_dir.exists() {
2194            tokio::fs::remove_dir_all(&cache_dir)
2195                .await
2196                .with_context(|| {
2197                    format!("Failed to remove cache directory: {}", cache_dir.display())
2198                })?;
2199            display_println!("{} Removed cache directory", style("✓").green());
2200        }
2201
2202        // Delete state.json file
2203        let state_file = vibe_dir.join("state.json");
2204        if state_file.exists() {
2205            tokio::fs::remove_file(&state_file).await.with_context(|| {
2206                format!("Failed to remove state file: {}", state_file.display())
2207            })?;
2208            display_println!("{} Removed state file", style("✓").green());
2209        }
2210
2211        display_println!("{} Factory reset completed", style("✅").green().bold());
2212        display_println!();
2213        display_println!(
2214            "{} All vibe configuration has been cleared.",
2215            style("â„šī¸").blue()
2216        );
2217        display_println!(
2218            "{} Run 'vibe' again to start the setup wizard.",
2219            style("💡").yellow()
2220        );
2221
2222        Ok(())
2223    }
2224
2225    /// Get a repository by name
2226    pub fn get_repository(&self, name: &str) -> Option<&Repository> {
2227        self.config.get_repository(name)
2228    }
2229
2230    /// Get a repository by flexible name lookup (supports owner/repo format)
2231    pub fn get_repository_flexible(&self, name: &str) -> Option<&Repository> {
2232        self.config.get_repository_flexible(name)
2233    }
2234
2235    /// List all repositories
2236    pub fn list_repositories(&self) -> &[Repository] {
2237        &self.config.repositories
2238    }
2239
2240    /// Remove a repository from the workspace
2241    pub async fn remove_repository(&mut self, name: &str) -> Result<()> {
2242        self.config.repositories.retain(|r| r.name != name);
2243        self.save_config().await?;
2244        Ok(())
2245    }
2246
2247    /// Check if an app is available on the system
2248    pub async fn is_app_available(&self, app_name: &str) -> bool {
2249        match app_name {
2250            "vscode" => {
2251                // Check if VS Code is available
2252                tokio::process::Command::new("code")
2253                    .arg("--version")
2254                    .output()
2255                    .await
2256                    .map(|output| output.status.success())
2257                    .unwrap_or(false)
2258            }
2259            "warp" => {
2260                // Check if Warp is available
2261                #[cfg(target_os = "macos")]
2262                {
2263                    tokio::fs::metadata("/Applications/Warp.app").await.is_ok()
2264                }
2265                #[cfg(not(target_os = "macos"))]
2266                {
2267                    false
2268                }
2269            }
2270            "iterm2" => {
2271                // Check if iTerm2 is available
2272                #[cfg(target_os = "macos")]
2273                {
2274                    tokio::fs::metadata("/Applications/iTerm.app").await.is_ok()
2275                }
2276                #[cfg(not(target_os = "macos"))]
2277                {
2278                    false
2279                }
2280            }
2281            "wezterm" => {
2282                // Check if WezTerm is available
2283                tokio::process::Command::new("wezterm")
2284                    .arg("--version")
2285                    .output()
2286                    .await
2287                    .map(|output| output.status.success())
2288                    .unwrap_or(false)
2289            }
2290            "cursor" => {
2291                // Check if Cursor is available
2292                #[cfg(target_os = "macos")]
2293                {
2294                    tokio::fs::metadata("/Applications/Cursor.app")
2295                        .await
2296                        .is_ok()
2297                }
2298                #[cfg(not(target_os = "macos"))]
2299                {
2300                    // Try command line for other platforms
2301                    tokio::process::Command::new("cursor")
2302                        .arg("--version")
2303                        .output()
2304                        .await
2305                        .map(|output| output.status.success())
2306                        .unwrap_or(false)
2307                }
2308            }
2309            "windsurf" => {
2310                // Check if Windsurf is available
2311                #[cfg(target_os = "macos")]
2312                {
2313                    tokio::fs::metadata("/Applications/Windsurf.app")
2314                        .await
2315                        .is_ok()
2316                }
2317                #[cfg(not(target_os = "macos"))]
2318                {
2319                    // Try command line for other platforms
2320                    tokio::process::Command::new("windsurf")
2321                        .arg("--version")
2322                        .output()
2323                        .await
2324                        .map(|output| output.status.success())
2325                        .unwrap_or(false)
2326                }
2327            }
2328            _ => false,
2329        }
2330    }
2331
2332    /// Get all available apps on the system
2333    pub async fn get_available_apps(&self) -> Vec<String> {
2334        let potential_apps = vec!["vscode", "cursor", "windsurf", "warp", "iterm2", "wezterm"];
2335        let mut available_apps = Vec::new();
2336
2337        for app in potential_apps {
2338            if self.is_app_available(app).await {
2339                available_apps.push(app.to_string());
2340            }
2341        }
2342
2343        available_apps
2344    }
2345
2346    /// Build app choice menu with configured and available apps
2347    fn build_app_choice_menu(
2348        &self,
2349        configured_apps: &[String],
2350        available_apps: &[String],
2351    ) -> Vec<AppChoice> {
2352        let mut choices = Vec::new();
2353
2354        // Configured apps first (with ✅ indicator)
2355        for app in configured_apps {
2356            choices.push(AppChoice {
2357                app: app.clone(),
2358                display: format!("✅ {} (configured with templates)", app),
2359                is_configured: true,
2360            });
2361        }
2362
2363        // Available apps second (with 📁 indicator) - only if not already configured
2364        for app in available_apps {
2365            if !configured_apps.contains(app) {
2366                choices.push(AppChoice {
2367                    app: app.clone(),
2368                    display: format!("📁 {} (basic mode)", app),
2369                    is_configured: false,
2370                });
2371            }
2372        }
2373
2374        choices
2375    }
2376
2377    /// Prompt user to select an app from the choice menu
2378    fn prompt_app_selection(&self, app_choices: &[AppChoice]) -> Result<String> {
2379        use inquire::Select;
2380
2381        let options: Vec<String> = app_choices
2382            .iter()
2383            .map(|choice| choice.display.clone())
2384            .collect();
2385
2386        let selected_display = Select::new("Choose app to open repository:", options)
2387            .with_help_message("Configured apps include templates and automation")
2388            .prompt()
2389            .map_err(|e| anyhow::anyhow!("App selection cancelled: {}", e))?;
2390
2391        // Find the app name from the selected display string
2392        for choice in app_choices {
2393            if choice.display == selected_display {
2394                return Ok(choice.app.clone());
2395            }
2396        }
2397
2398        anyhow::bail!("Invalid app selection")
2399    }
2400
2401    // Cache management methods
2402
2403    /// Initialize repository cache
2404    async fn init_repository_cache(cache_dir: &Path) -> Result<RepositoryCache> {
2405        tokio::fs::create_dir_all(cache_dir).await?;
2406        let repo_cache = RepositoryCache::new(cache_dir.join("repositories.db"));
2407        repo_cache.initialize().await?;
2408        Ok(repo_cache)
2409    }
2410
2411    /// Initialize git status cache
2412    async fn init_git_status_cache(cache_dir: &Path) -> Result<GitStatusCache> {
2413        tokio::fs::create_dir_all(cache_dir).await?;
2414        let git_cache = GitStatusCache::new(cache_dir.join("git_status.db"));
2415        git_cache.initialize().await?;
2416        Ok(git_cache)
2417    }
2418
2419    /// Get repository cache (lazy initialization if needed)
2420    pub async fn get_repository_cache(&mut self) -> Result<&RepositoryCache> {
2421        if self.repo_cache.is_none() {
2422            let vibe_dir = super::constants::get_config_dir();
2423            let cache_dir = vibe_dir.join("cache");
2424            self.repo_cache = Some(Self::init_repository_cache(&cache_dir).await?);
2425        }
2426        Ok(self.repo_cache.as_ref().unwrap())
2427    }
2428
2429    /// Get git status cache (lazy initialization if needed)
2430    pub async fn get_git_status_cache(&mut self) -> Result<&GitStatusCache> {
2431        if self.git_cache.is_none() {
2432            let vibe_dir = super::constants::get_config_dir();
2433            let cache_dir = vibe_dir.join("cache");
2434            self.git_cache = Some(Self::init_git_status_cache(&cache_dir).await?);
2435        }
2436        Ok(self.git_cache.as_ref().unwrap())
2437    }
2438
2439    /// Refresh repository cache from current configuration
2440    pub async fn refresh_repository_cache(&mut self) -> Result<()> {
2441        // Get repositories data first to avoid borrowing issues
2442        let repositories = self.config.repositories.clone();
2443        let workspace_root = self.config.workspace.root.clone();
2444        let current_names: Vec<String> = repositories.iter().map(|r| r.name.clone()).collect();
2445
2446        if let Ok(cache) = self.get_repository_cache().await {
2447            cache
2448                .refresh_from_config(&repositories, &workspace_root)
2449                .await?;
2450            cache.cleanup_stale_entries(&current_names).await?;
2451        }
2452        Ok(())
2453    }
2454
2455    /// Update git status cache for repositories (background operation)
2456    pub async fn update_git_status_cache(&mut self, repo_names: &[String]) -> Result<()> {
2457        // Clone data to avoid borrowing issues
2458        let repositories = self.config.repositories.clone();
2459        let workspace_root = self.config.workspace.root.clone();
2460
2461        if let Ok(cache) = self.get_git_status_cache().await {
2462            for repo_name in repo_names {
2463                if let Some(repo_config) = repositories.iter().find(|r| r.name == *repo_name) {
2464                    let repo_path = workspace_root.join(&repo_config.path);
2465
2466                    match get_git_status(&repo_path).await {
2467                        Ok(git_status) => {
2468                            let cached_status = git_status.into();
2469                            if let Err(e) = cache.cache_git_status(&cached_status).await {
2470                                warn!("Failed to cache git status for {}: {}", repo_name, e);
2471                            }
2472                        }
2473                        Err(e) => {
2474                            warn!("Failed to get git status for {}: {}", repo_name, e);
2475                        }
2476                    }
2477                }
2478            }
2479        }
2480        Ok(())
2481    }
2482
2483    /// Get quick launcher for fast repository selection
2484    pub async fn get_quick_launcher(&self) -> Result<crate::ui::quick_launcher::QuickLauncher> {
2485        let vibe_dir = super::constants::get_config_dir();
2486        let cache_dir = vibe_dir.join("cache");
2487        crate::ui::quick_launcher::QuickLauncher::new(&cache_dir).await
2488    }
2489
2490    // Page size access methods
2491
2492    /// Get page size for main menu
2493    pub fn get_main_menu_page_size(&self) -> usize {
2494        self.config
2495            .preferences
2496            .as_ref()
2497            .map(|p| p.page_sizes.main_menu)
2498            .unwrap_or(15)
2499    }
2500
2501    /// Get page size for repository list
2502    pub fn get_repository_list_page_size(&self) -> usize {
2503        self.config
2504            .preferences
2505            .as_ref()
2506            .map(|p| p.page_sizes.repository_list)
2507            .unwrap_or(15)
2508    }
2509
2510    /// Get page size for quick launch
2511    pub fn get_quick_launch_page_size(&self) -> usize {
2512        self.config
2513            .preferences
2514            .as_ref()
2515            .map(|p| p.page_sizes.quick_launch)
2516            .unwrap_or(9)
2517    }
2518
2519    /// Get page size for app selection
2520    pub fn get_app_selection_page_size(&self) -> usize {
2521        self.config
2522            .preferences
2523            .as_ref()
2524            .map(|p| p.page_sizes.app_selection)
2525            .unwrap_or(10)
2526    }
2527
2528    /// Get page size for git search results
2529    pub fn get_git_search_results_page_size(&self) -> usize {
2530        self.config
2531            .preferences
2532            .as_ref()
2533            .map(|p| p.page_sizes.git_search_results)
2534            .unwrap_or(15)
2535    }
2536
2537    /// Get page size for management menus
2538    pub fn get_management_menus_page_size(&self) -> usize {
2539        self.config
2540            .preferences
2541            .as_ref()
2542            .map(|p| p.page_sizes.management_menus)
2543            .unwrap_or(10)
2544    }
2545
2546    /// Get page size for app installer
2547    pub fn get_app_installer_page_size(&self) -> usize {
2548        self.config
2549            .preferences
2550            .as_ref()
2551            .map(|p| p.page_sizes.app_installer)
2552            .unwrap_or(15)
2553    }
2554
2555    // Backup and Restore methods
2556
2557    /// List available backup files in the default backup directory
2558    pub async fn list_available_backups(&self) -> Result<Vec<BackupInfo>> {
2559        let backup_dir = dirs::home_dir()
2560            .unwrap_or_else(|| PathBuf::from("."))
2561            .join(super::constants::CONFIG_DIR_PATH)
2562            .join("backups");
2563
2564        if !backup_dir.exists() {
2565            return Ok(Vec::new());
2566        }
2567
2568        let mut backups = Vec::new();
2569        let mut entries = tokio::fs::read_dir(&backup_dir).await?;
2570
2571        while let Some(entry) = entries.next_entry().await? {
2572            let path = entry.path();
2573            if let Some(extension) = path.extension() {
2574                if extension == "tgz" {
2575                    let metadata = entry.metadata().await?;
2576                    let file_name = path
2577                        .file_name()
2578                        .unwrap_or_default()
2579                        .to_string_lossy()
2580                        .to_string();
2581
2582                    // Analyze backup contents (optional, for display purposes)
2583                    let contents = self.analyze_backup(&path).await.ok();
2584
2585                    backups.push(BackupInfo {
2586                        file_name: file_name.clone(),
2587                        path: path.clone(),
2588                        size: metadata.len(),
2589                        created: metadata.created().unwrap_or(std::time::UNIX_EPOCH),
2590                        display_name: self.format_backup_display_name(&file_name),
2591                        contents,
2592                    });
2593                }
2594            }
2595        }
2596
2597        // Sort by creation time, newest first
2598        backups.sort_by(|a, b| b.created.cmp(&a.created));
2599        Ok(backups)
2600    }
2601
2602    /// Format backup file name for display
2603    fn format_backup_display_name(&self, file_name: &str) -> String {
2604        // Remove .tgz extension and format timestamp
2605        let name_without_ext = file_name.strip_suffix(".tgz").unwrap_or(file_name);
2606
2607        if let Some(timestamp_part) = name_without_ext.strip_prefix("vibe-backup-") {
2608            if let Ok(parsed) =
2609                chrono::NaiveDateTime::parse_from_str(timestamp_part, "%Y%m%d-%H%M%S")
2610            {
2611                return format!(
2612                    "{} (created {})",
2613                    name_without_ext,
2614                    parsed.format("%Y-%m-%d %H:%M:%S")
2615                );
2616            }
2617        }
2618
2619        name_without_ext.to_string()
2620    }
2621
2622    /// Format file size for human-readable display
2623    fn format_file_size(bytes: u64) -> String {
2624        const KB: u64 = 1024;
2625        const MB: u64 = KB * 1024;
2626        const GB: u64 = MB * 1024;
2627
2628        if bytes >= GB {
2629            format!("{:.1} GB", bytes as f64 / GB as f64)
2630        } else if bytes >= MB {
2631            format!("{:.1} MB", bytes as f64 / MB as f64)
2632        } else if bytes >= KB {
2633            format!("{:.1} kB", bytes as f64 / KB as f64)
2634        } else {
2635            format!("{bytes} B")
2636        }
2637    }
2638
2639    /// Restore configuration from a backup file
2640    pub async fn restore_from_backup(
2641        &mut self,
2642        backup_path: Option<PathBuf>,
2643        force: bool,
2644    ) -> Result<()> {
2645        let backup_file = if let Some(path) = backup_path {
2646            path
2647        } else {
2648            // Interactive selection
2649            self.select_backup_interactively().await?
2650        };
2651
2652        if !backup_file.exists() {
2653            anyhow::bail!("Backup file does not exist: {}", backup_file.display());
2654        }
2655
2656        // Analyze backup contents
2657        let backup_contents = self.analyze_backup(&backup_file).await?;
2658
2659        if !force {
2660            self.confirm_restore(&backup_file, &backup_contents).await?;
2661        }
2662
2663        println!("{} Starting restore process...", style("🔄").blue());
2664
2665        // Perform factory reset first
2666        println!(
2667            "{} Clearing existing configuration...",
2668            style("đŸ—‘ī¸").yellow()
2669        );
2670        self.factory_reset_with_options(true, true).await?;
2671
2672        // Extract and restore backup
2673        self.extract_backup(&backup_file).await?;
2674
2675        // Reinitialize caches
2676        println!("{} Rebuilding cache databases...", style("🔄").blue());
2677        self.reinitialize_caches().await?;
2678
2679        // Reload configuration
2680        self.config = WorkspaceConfig::load_from_file(&self.config_path).await?;
2681
2682        println!(
2683            "{} Restore completed successfully!",
2684            style("✅").green().bold()
2685        );
2686        println!(
2687            "{} Run 'vibe menu' to continue using Vibe Workspace",
2688            style("💡").blue()
2689        );
2690
2691        Ok(())
2692    }
2693
2694    /// Interactive backup selection
2695    async fn select_backup_interactively(&self) -> Result<PathBuf> {
2696        let backups = self.list_available_backups().await?;
2697
2698        if backups.is_empty() {
2699            anyhow::bail!("No backup files found in ~/.toolprint/vibe-workspace/backups/");
2700        }
2701
2702        println!("\n{} Available backups:", style("đŸ“Ļ").blue());
2703
2704        let backup_options: Vec<String> = backups
2705            .iter()
2706            .map(|backup| {
2707                let size_str = Self::format_file_size(backup.size);
2708                let mut details = vec![size_str];
2709
2710                if let Some(contents) = &backup.contents {
2711                    let mut content_parts = Vec::new();
2712                    if contents.has_config {
2713                        content_parts.push("config".to_string());
2714                    }
2715                    if contents.has_state {
2716                        content_parts.push("state".to_string());
2717                    }
2718                    if contents.has_templates {
2719                        content_parts.push("templates".to_string());
2720                    }
2721                    if !contents.app_configs.is_empty() {
2722                        content_parts.push(format!("{} apps", contents.app_configs.len()));
2723                    }
2724
2725                    if !content_parts.is_empty() {
2726                        details.push(format!("{} files", contents.total_files));
2727                        details.push(content_parts.join("+"));
2728                    }
2729                }
2730
2731                format!("{} ({})", backup.display_name, details.join(", "))
2732            })
2733            .collect();
2734
2735        use inquire::Select;
2736        let selection = Select::new("Select backup to restore:", backup_options)
2737            .with_help_message("Use arrow keys to navigate, Enter to select")
2738            .with_page_size(10)
2739            .prompt()?;
2740
2741        // Find the selected backup by matching the display format
2742        let selected_backup = backups
2743            .iter()
2744            .find(|backup| {
2745                let size_str = Self::format_file_size(backup.size);
2746                let mut details = vec![size_str];
2747
2748                if let Some(contents) = &backup.contents {
2749                    let mut content_parts = Vec::new();
2750                    if contents.has_config {
2751                        content_parts.push("config".to_string());
2752                    }
2753                    if contents.has_state {
2754                        content_parts.push("state".to_string());
2755                    }
2756                    if contents.has_templates {
2757                        content_parts.push("templates".to_string());
2758                    }
2759                    if !contents.app_configs.is_empty() {
2760                        content_parts.push(format!("{} apps", contents.app_configs.len()));
2761                    }
2762
2763                    if !content_parts.is_empty() {
2764                        details.push(format!("{} files", contents.total_files));
2765                        details.push(content_parts.join("+"));
2766                    }
2767                }
2768
2769                let display = format!("{} ({})", backup.display_name, details.join(", "));
2770                display == selection
2771            })
2772            .context("Selected backup not found")?;
2773
2774        Ok(selected_backup.path.clone())
2775    }
2776
2777    /// Analyze backup contents
2778    async fn analyze_backup(&self, backup_path: &Path) -> Result<BackupContents> {
2779        use std::process::Command;
2780
2781        // List contents of the tar file
2782        let output = Command::new("tar")
2783            .args(["-tzf"])
2784            .arg(backup_path)
2785            .output()
2786            .context("Failed to analyze backup archive")?;
2787
2788        if !output.status.success() {
2789            anyhow::bail!("Failed to read backup archive: Invalid or corrupted file");
2790        }
2791
2792        let contents_list = String::from_utf8_lossy(&output.stdout);
2793        let files: Vec<String> = contents_list.lines().map(|s| s.to_string()).collect();
2794
2795        let mut contents = BackupContents {
2796            has_config: false,
2797            has_state: false,
2798            has_templates: false,
2799            app_configs: Vec::new(),
2800            total_files: files.len(),
2801        };
2802
2803        for file in &files {
2804            // Remove leading ./ if present
2805            let clean_file = file.strip_prefix("./").unwrap_or(file);
2806
2807            if clean_file == "config.yaml" {
2808                contents.has_config = true;
2809            } else if clean_file == "state.json" {
2810                contents.has_state = true;
2811            } else if clean_file.starts_with("templates/") {
2812                contents.has_templates = true;
2813            } else if clean_file.starts_with("app-configs/") {
2814                let parts: Vec<&str> = clean_file.split('/').collect();
2815                if parts.len() >= 2 && !contents.app_configs.contains(&parts[1].to_string()) {
2816                    contents.app_configs.push(parts[1].to_string());
2817                }
2818            }
2819        }
2820
2821        Ok(contents)
2822    }
2823
2824    /// Confirm restore operation with user
2825    async fn confirm_restore(&self, backup_path: &Path, contents: &BackupContents) -> Result<()> {
2826        use inquire::Confirm;
2827
2828        println!(
2829            "\n{} {}",
2830            style("âš ī¸  RESTORE CONFIRMATION").yellow().bold(),
2831            style("This will replace ALL current configuration!").yellow()
2832        );
2833        println!();
2834
2835        // Get backup file size
2836        let backup_size = if let Ok(metadata) = std::fs::metadata(backup_path) {
2837            Self::format_file_size(metadata.len())
2838        } else {
2839            "unknown".to_string()
2840        };
2841
2842        println!(
2843            "{} Backup file: {} ({})",
2844            style("đŸ“Ļ").blue(),
2845            backup_path.display(),
2846            backup_size
2847        );
2848        println!("{} Backup contains:", style("📋").blue());
2849
2850        if contents.has_config {
2851            println!("  {} Main configuration (config.yaml)", style("✓").green());
2852        }
2853        if contents.has_state {
2854            println!(
2855                "  {} User state and preferences (state.json)",
2856                style("✓").green()
2857            );
2858        }
2859        if contents.has_templates {
2860            println!("  {} Template files", style("✓").green());
2861        }
2862        if !contents.app_configs.is_empty() {
2863            println!(
2864                "  {} App configurations: {}",
2865                style("✓").green(),
2866                contents.app_configs.join(", ")
2867            );
2868        }
2869
2870        // Show what's missing from backup (if anything)
2871        if !contents.has_config {
2872            println!("  {} Main configuration (missing)", style("âš ī¸").yellow());
2873        }
2874        if !contents.has_state {
2875            println!(
2876                "  {} User state (missing - will use defaults)",
2877                style("â„šī¸").blue()
2878            );
2879        }
2880
2881        println!(
2882            "  {} Total files: {}",
2883            style("📊").blue(),
2884            contents.total_files
2885        );
2886        println!();
2887
2888        println!("{} This will:", style("âš ī¸").yellow());
2889        println!("  â€ĸ Delete all current configuration");
2890        println!("  â€ĸ Delete all app-generated files");
2891        println!("  â€ĸ Restore configuration from backup");
2892        println!("  â€ĸ Rebuild cache databases");
2893        println!();
2894
2895        let confirm = Confirm::new("Are you sure you want to proceed with the restore?")
2896            .with_default(false)
2897            .prompt()?;
2898
2899        if !confirm {
2900            anyhow::bail!("Restore cancelled by user");
2901        }
2902
2903        Ok(())
2904    }
2905
2906    /// Extract backup archive
2907    async fn extract_backup(&self, backup_path: &Path) -> Result<()> {
2908        use std::process::Command;
2909
2910        // Create temporary extraction directory
2911        let temp_dir = tempfile::tempdir().context("Failed to create temporary directory")?;
2912        let temp_path = temp_dir.path();
2913
2914        // Extract archive
2915        println!("{} Extracting backup archive...", style("đŸ“Ļ").blue());
2916        let output = Command::new("tar")
2917            .args(["-xzf"])
2918            .arg(backup_path)
2919            .args(["-C"])
2920            .arg(temp_path)
2921            .output()
2922            .context("Failed to extract backup archive")?;
2923
2924        if !output.status.success() {
2925            let error_msg = String::from_utf8_lossy(&output.stderr);
2926            anyhow::bail!("Failed to extract backup: {}", error_msg);
2927        }
2928
2929        // Copy files to their proper locations
2930        let vibe_dir = super::constants::get_config_dir();
2931        tokio::fs::create_dir_all(&vibe_dir).await?;
2932
2933        // Copy main config file
2934        let config_src = temp_path.join("config.yaml");
2935        if config_src.exists() {
2936            tokio::fs::copy(&config_src, &self.config_path).await?;
2937            println!("{} Restored main configuration", style("✓").green());
2938        }
2939
2940        // Copy state file
2941        let state_src = temp_path.join("state.json");
2942        let state_dest = vibe_dir.join("state.json");
2943        if state_src.exists() {
2944            tokio::fs::copy(&state_src, &state_dest).await?;
2945            println!("{} Restored user state", style("✓").green());
2946        }
2947
2948        // Copy templates directory
2949        let templates_src = temp_path.join("templates");
2950        let templates_dest = vibe_dir.join("templates");
2951        if templates_src.exists() {
2952            if templates_dest.exists() {
2953                tokio::fs::remove_dir_all(&templates_dest).await?;
2954            }
2955            copy_dir_recursive(&templates_src, &templates_dest)?;
2956            println!("{} Restored templates", style("✓").green());
2957        }
2958
2959        // Copy app configuration files
2960        let app_configs_src = temp_path.join("app-configs");
2961        if app_configs_src.exists() {
2962            self.restore_app_configs(&app_configs_src).await?;
2963        }
2964
2965        Ok(())
2966    }
2967
2968    /// Restore app configuration files to their proper locations
2969    async fn restore_app_configs(&self, app_configs_dir: &Path) -> Result<()> {
2970        // Load the configuration to get app integration settings
2971        let temp_config = WorkspaceConfig::load_from_file(&self.config_path).await?;
2972
2973        // Restore each app type
2974        for app_type in ["warp", "iterm2", "wezterm", "vscode", "cursor", "windsurf"] {
2975            let app_dir = app_configs_dir.join(app_type);
2976            if !app_dir.exists() {
2977                continue;
2978            }
2979
2980            let dest_dir = match app_type {
2981                "warp" => temp_config.apps.warp.as_ref().map(|w| &w.config_dir),
2982                "iterm2" => temp_config.apps.iterm2.as_ref().map(|i| &i.config_dir),
2983                "wezterm" => temp_config.apps.wezterm.as_ref().map(|w| &w.config_dir),
2984                "vscode" => temp_config.apps.vscode.as_ref().map(|v| &v.workspace_dir),
2985                _ => None,
2986            };
2987
2988            if let Some(dest) = dest_dir {
2989                tokio::fs::create_dir_all(dest).await?;
2990                copy_dir_recursive(&app_dir, dest)?;
2991                println!(
2992                    "{} Restored {} configurations",
2993                    style("✓").green(),
2994                    app_type
2995                );
2996            }
2997        }
2998
2999        Ok(())
3000    }
3001
3002    /// Reinitialize cache databases after restore
3003    async fn reinitialize_caches(&mut self) -> Result<()> {
3004        let vibe_dir = super::constants::get_config_dir();
3005        let cache_dir = vibe_dir.join("cache");
3006
3007        // Remove existing cache files
3008        if cache_dir.exists() {
3009            tokio::fs::remove_dir_all(&cache_dir).await?;
3010        }
3011
3012        // Reinitialize caches
3013        tokio::fs::create_dir_all(&cache_dir).await?;
3014        self.repo_cache = Some(Self::init_repository_cache(&cache_dir).await?);
3015        self.git_cache = Some(Self::init_git_status_cache(&cache_dir).await?);
3016
3017        // Populate repository cache from restored configuration
3018        let repositories = self.config.repositories.clone();
3019        let workspace_root = self.config.workspace.root.clone();
3020        if let Ok(cache) = self.get_repository_cache().await {
3021            cache
3022                .refresh_from_config(&repositories, &workspace_root)
3023                .await?;
3024        }
3025
3026        println!("{} Cache databases rebuilt", style("✓").green());
3027        Ok(())
3028    }
3029}
3030
3031// Helper function to recursively copy directories using std::fs
3032fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
3033    use std::fs;
3034
3035    if src.is_dir() {
3036        fs::create_dir_all(dst)?;
3037
3038        for entry in fs::read_dir(src)? {
3039            let entry = entry?;
3040            let src_path = entry.path();
3041            let dst_path = dst.join(entry.file_name());
3042
3043            if src_path.is_dir() {
3044                copy_dir_recursive(&src_path, &dst_path)?;
3045            } else {
3046                fs::copy(&src_path, &dst_path)?;
3047            }
3048        }
3049    } else {
3050        fs::copy(src, dst)?;
3051    }
3052
3053    Ok(())
3054}