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");
let storage = Storage::new()?;
storage.init().await?;
println!("📁 Storage initialized at ~/.i-self\n");
let mut config = storage.load_config().await?;
let github_profile = if !self.skip_github {
self.setup_github(&storage, &mut config).await?
} else {
println!("⏭️ Skipping GitHub setup");
None
};
let _local_projects = if !self.skip_local {
self.scan_local_projects(&storage).await?
} else {
println!("⏭️ Skipping local project scan");
Vec::new()
};
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();
}
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()?;
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()));
for scan in &scans {
storage.save_repo_scan(&scan.repository.full_name, scan).await?;
}
let summary = scanner.generate_summary(&scans).await;
pb.finish_with_message(format!("✓ Analyzed {} repositories", scans.len()));
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);
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());
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>,
}