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).
#![allow(dead_code)]

use anyhow::Result;
use chrono::{DateTime, Utc};
use octocrab::{Octocrab, Page};
use std::collections::HashMap;
use tracing::info;

pub mod scanner;
pub mod types;

pub use scanner::GitHubScanner;
pub use types::{Repository, Branch, Commit, PullRequest, LanguageStats};

/// GitHub API client wrapper
pub struct GitHubClient {
    client: Octocrab,
    username: String,
}

impl GitHubClient {
    pub fn new(token: Option<String>) -> Result<Self> {
        let client = if let Some(token) = token {
            Octocrab::builder()
                .personal_token(token)
                .build()?
        } else {
            Octocrab::builder().build()?
        };

        Ok(Self {
            client,
            username: String::new(),
        })
    }

    /// Authenticate and get user info
    pub async fn authenticate(&mut self) -> Result<String> {
        let user = self.client.current().user().await?;
        self.username = user.login.clone();
        info!("Authenticated as GitHub user: {}", self.username);
        Ok(self.username.clone())
    }

    /// Get all repositories for the authenticated user
    pub async fn get_all_repos(&self) -> Result<Vec<Repository>> {
        info!("Fetching all repositories for user: {}", self.username);
        
        let mut repos = Vec::new();
        let mut page: Page<octocrab::models::Repository> = self.client
            .current()
            .list_repos_for_authenticated_user()
            .per_page(100)
            .send()
            .await?;

        loop {
            for repo in &page {
                repos.push(Repository::from_octocrab(repo));
            }

            match self.client.get_page(&page.next).await? {
                Some(next_page) => page = next_page,
                None => break,
            }
        }

        info!("Found {} repositories", repos.len());
        Ok(repos)
    }

    /// Get all branches for a repository
    pub async fn get_branches(&self, owner: &str, repo: &str) -> Result<Vec<Branch>> {
        let mut branches = Vec::new();
        let mut page = self.client
            .repos(owner, repo)
            .list_branches()
            .per_page(100)
            .send()
            .await?;

        loop {
            for branch in &page {
                branches.push(Branch::from_octocrab(branch));
            }

            match self.client.get_page(&page.next).await? {
                Some(next_page) => page = next_page,
                None => break,
            }
        }

        Ok(branches)
    }

    /// Get recent commits for a branch
    pub async fn get_recent_commits(
        &self,
        owner: &str,
        repo: &str,
        branch: &str,
        since: Option<DateTime<Utc>>,
    ) -> Result<Vec<Commit>> {
        let mut commits = Vec::new();
        
        // Build the request
        let repos = self.client.repos(owner, repo);
        let mut builder = repos.list_commits().sha(branch).per_page(100);

        if let Some(since_date) = since {
            builder = builder.since(since_date);
        }

        let mut page = builder.send().await?;

        loop {
            for commit in &page {
                commits.push(Commit::from_octocrab(commit));
            }

            if commits.len() >= 100 {
                break;
            }

            match self.client.get_page(&page.next).await? {
                Some(next_page) => page = next_page,
                None => break,
            }
        }

        Ok(commits)
    }

    /// Get language statistics for a repository
    pub async fn get_language_stats(&self, owner: &str, repo: &str) -> Result<LanguageStats> {
        // Use the specific languages endpoint
        let languages: HashMap<String, i64> = self.client
            .get(format!("/repos/{}/{}/languages", owner, repo), None::<&()>)
            .await?;

        let mut stats = HashMap::new();
        let total: i64 = languages.values().sum();

        for (lang, bytes) in languages {
            let percentage = if total > 0 {
                (bytes as f64 / total as f64) * 100.0
            } else {
                0.0
            };
            stats.insert(lang, (bytes, percentage));
        }

        Ok(LanguageStats {
            languages: stats,
            total_bytes: total,
        })
    }

    /// Get recent pull requests
    pub async fn get_recent_prs(
        &self,
        owner: &str,
        repo: &str,
        state: &str,
    ) -> Result<Vec<PullRequest>> {
        use octocrab::params::State;
        
        let state_filter = match state {
            "open" => State::Open,
            "closed" => State::Closed,
            _ => State::All,
        };

        let mut prs = Vec::new();
        let mut page = self.client
            .pulls(owner, repo)
            .list()
            .state(state_filter)
            .per_page(50)
            .send()
            .await?;

        loop {
            for pr in &page {
                prs.push(PullRequest::from_octocrab(pr));
            }

            match self.client.get_page(&page.next).await? {
                Some(next_page) => page = next_page,
                None => break,
            }
        }

        Ok(prs)
    }

    /// Get repository topics
    pub async fn get_topics(&self, owner: &str, repo: &str) -> Result<Vec<String>> {
        let repo_data = self.client
            .repos(owner, repo)
            .get()
            .await?;

        Ok(repo_data.topics.unwrap_or_default())
    }

    pub fn username(&self) -> &str {
        &self.username
    }
}