i-self 0.4.3

Personal developer-companion CLI: scans your repos, indexes code semantically, watches your activity, and moves AI-agent sessions between tools (Claude Code, Aider, Goose, OpenAI Codex CLI, Continue.dev, OpenCode).
use crate::analyzer::{Analyzer, ProjectAnalysis};
use crate::github::{GitHubClient, GitHubScanner};
use crate::storage::{DeveloperProfile, Storage, Config};
use anyhow::Result;
use colored::Colorize;
use dialoguer::{Confirm, Input, Password};
use indicatif::{ProgressBar, ProgressStyle};
use std::path::Path;
use tracing::warn;

pub struct SetupCommand {
    github_token: Option<String>,
    skip_github: bool,
    skip_local: bool,
}

impl SetupCommand {
    pub fn new(github_token: Option<String>, skip_github: bool, skip_local: bool) -> Self {
        Self {
            github_token,
            skip_github,
            skip_local,
        }
    }

    pub async fn run(&self) -> Result<()> {
        println!("{}", "🚀 Welcome to i-self setup!".bold().cyan());
        println!("This will create a digital copy of your development profile.\n");

        // Initialize storage
        let storage = Storage::new()?;
        storage.init().await?;

        println!("📁 Storage initialized at ~/.i-self\n");

        // Load or create config
        let mut config = storage.load_config().await?;

        // GitHub setup
        let github_profile = if !self.skip_github {
            self.setup_github(&storage, &mut config).await?
        } else {
            println!("⏭️  Skipping GitHub setup");
            None
        };

        // Local projects setup
        let _local_projects = if !self.skip_local {
            self.scan_local_projects(&storage).await?
        } else {
            println!("⏭️  Skipping local project scan");
            Vec::new()
        };

        // Build developer profile
        let mut profile = DeveloperProfile::default();
        
        if let Some(github) = github_profile {
            profile.github.username = github.username;
            profile.github.total_repositories = github.total_repos;
            profile.github.total_contributions_30d = github.total_commits_30d;
            profile.github.primary_languages = github.primary_languages;
            profile.github.most_active_repos = github.most_active_repos.into_iter()
                .map(|name| crate::storage::profile::RepoActivity {
                    name,
                    commits_30d: 0,
                    language: None,
                    last_activity: chrono::Utc::now(),
                })
                .collect();
        }

        // Save profile
        storage.save_profile(&profile).await?;
        storage.save_config(&config).await?;

        println!("\n{}", "✅ Setup complete!".bold().green());
        println!("Your developer profile has been saved to ~/.i-self/");
        println!("\nNext steps:");
        println!("  • Run {} to see your profile", "i-self status".cyan());
        println!("  • Run {} to query your knowledge", "i-self query \"your question\"".cyan());
        println!("  • Run {} to update your profile", "i-self refresh".cyan());

        Ok(())
    }

    async fn setup_github(&self, storage: &Storage, config: &mut Config) -> Result<Option<GitHubSummary>> {
        println!("{}", "🔐 GitHub Configuration".bold());
        
        let token = if let Some(token) = &self.github_token {
            token.clone()
        } else if let Some(token) = &config.github_token {
            println!("Using saved GitHub token");
            token.clone()
        } else {
            let use_github = Confirm::new()
                .with_prompt("Would you like to connect your GitHub account?")
                .default(true)
                .interact()?;

            if !use_github {
                return Ok(None);
            }

            let token = Password::new()
                .with_prompt("Enter your GitHub personal access token")
                .interact()?;

            // Save token to config
            config.github_token = Some(token.clone());
            
            token
        };

        println!("\n{}", "🔍 Scanning GitHub repositories...".yellow());
        
        let pb = ProgressBar::new_spinner();
        pb.set_style(
            ProgressStyle::default_spinner()
                .template("{spinner:.green} {msg}")
                .unwrap(),
        );
        pb.set_message("Connecting to GitHub API...");

        let mut client = GitHubClient::new(Some(token))?;
        let username = client.authenticate().await?;
        
        pb.set_message(format!("Authenticated as {}", username));

        let scanner = GitHubScanner::new(client);
        let scans = scanner.scan_all_repos().await?;
        
        pb.set_message(format!("Scanned {} repositories", scans.len()));

        // Save individual repo scans
        for scan in &scans {
            storage.save_repo_scan(&scan.repository.full_name, scan).await?;
        }

        // Generate summary
        let summary = scanner.generate_summary(&scans).await;
        
        pb.finish_with_message(format!("✓ Analyzed {} repositories", scans.len()));

        // Show summary
        println!("\n{}", "📊 GitHub Summary:".bold());
        println!("  Total repositories: {}", summary.total_repos);
        println!("  Total branches: {}", summary.total_branches);
        println!("  Commits (30 days): {}", summary.total_commits_30d);
        println!("  Open PRs: {}", summary.total_prs_open);
        println!("  Merged PRs (30 days): {}", summary.total_prs_merged_30d);
        
        if !summary.top_languages.is_empty() {
            println!("  Top languages:");
            for (lang, bytes) in &summary.top_languages[..5.min(summary.top_languages.len())] {
                println!("{}: {} bytes", lang, bytes);
            }
        }

        Ok(Some(GitHubSummary {
            username,
            total_repos: summary.total_repos,
            total_commits_30d: summary.total_commits_30d as i64,
            primary_languages: summary.top_languages.into_iter()
                .map(|(l, _)| (l, 0.0))
                .collect(),
            most_active_repos: summary.most_active_repos,
        }))
    }

    async fn scan_local_projects(&self, storage: &Storage) -> Result<Vec<ProjectAnalysis>> {
        println!("\n{}", "💻 Local Project Scan".bold());

        let scan_dirs: Vec<String> = Input::new()
            .with_prompt("Enter directories to scan (comma-separated, or 'skip')")
            .default("~/projects,~/code,~/dev".to_string())
            .interact_text()?
            .split(',')
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty() && s != "skip")
            .collect();

        if scan_dirs.is_empty() {
            println!("Skipping local project scan");
            return Ok(Vec::new());
        }

        let analyzer = Analyzer::new();
        let mut all_analyses = Vec::new();

        for dir in scan_dirs {
            let expanded = shellexpand::tilde(&dir).to_string();
            let path = Path::new(&expanded);

            if !path.exists() {
                warn!("Directory does not exist: {}", path.display());
                continue;
            }

            println!("\n  Scanning {}...", dir);

            // Find git repositories
            let repos = self.find_git_repos(path).await?;
            println!("    Found {} repositories", repos.len());

            let pb = ProgressBar::new(repos.len() as u64);
            pb.set_style(
                ProgressStyle::default_bar()
                    .template("    {spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
                    .unwrap()
                    .progress_chars("#>-"),
            );

            for repo_path in repos {
                pb.set_message(format!("Analyzing {}", repo_path.file_name()
                    .and_then(|n| n.to_str())
                    .unwrap_or("unknown")));
                
                match analyzer.analyze_project(&repo_path).await {
                    Ok(analysis) => {
                        all_analyses.push(analysis);
                    }
                    Err(e) => {
                        warn!("Failed to analyze {}: {}", repo_path.display(), e);
                    }
                }
                pb.inc(1);
            }

            pb.finish_with_message("Done");
        }

        println!("\n  Analyzed {} local projects", all_analyses.len());

        // Save analyses
        for analysis in &all_analyses {
            let name = analysis.path.file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("unknown");
            storage.save_repo_scan(&format!("local:{}", name), analysis).await?;
        }

        Ok(all_analyses)
    }

    async fn find_git_repos(&self, root: &Path) -> Result<Vec<std::path::PathBuf>> {
        let mut repos = Vec::new();
        
        for entry in walkdir::WalkDir::new(root)
            .max_depth(3)
            .into_iter()
            .filter_map(|e| e.ok())
        {
            if entry.file_type().is_dir() {
                let git_dir = entry.path().join(".git");
                if git_dir.exists() {
                    repos.push(entry.path().to_path_buf());
                }
            }
        }

        Ok(repos)
    }
}

struct GitHubSummary {
    username: String,
    total_repos: usize,
    total_commits_30d: i64,
    primary_languages: Vec<(String, f64)>,
    most_active_repos: Vec<String>,
}