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 serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tokio::fs;
use tracing::{info, debug};

pub mod profile;
pub mod knowledge;

pub use profile::DeveloperProfile;
pub use knowledge::KnowledgeBase;

/// Storage manager for ~/.i-self directory
pub struct Storage {
    base_dir: PathBuf,
    repos_dir: PathBuf,
    profile_path: PathBuf,
    knowledge_path: PathBuf,
    config_path: PathBuf,
}

impl Storage {
    /// Initialize storage in ~/.i-self
    pub fn new() -> Result<Self> {
        let home = dirs::home_dir()
            .ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?;
        
        let base_dir = home.join(".i-self");
        let repos_dir = base_dir.join("repos");
        let profile_path = base_dir.join("profile.json");
        let knowledge_path = base_dir.join("knowledge.json");
        let config_path = base_dir.join("config.toml");

        Ok(Self {
            base_dir,
            repos_dir,
            profile_path,
            knowledge_path,
            config_path,
        })
    }

    /// Initialize directory structure
    pub async fn init(&self) -> Result<()> {
        debug!("Initializing storage at {:?}", self.base_dir);
        
        fs::create_dir_all(&self.base_dir).await?;
        fs::create_dir_all(&self.repos_dir).await?;
        
        // Create default config if not exists
        if !self.config_path.exists() {
            self.create_default_config().await?;
        }

        info!("Storage initialized at {:?}", self.base_dir);
        Ok(())
    }

    /// Save repository scan data
    pub async fn save_repo_scan(&self, repo_name: &str, data: impl Serialize) -> Result<()> {
        let safe_name = repo_name.replace('/', "_");
        let path = self.repos_dir.join(format!("{}.json", safe_name));
        
        let json = serde_json::to_string_pretty(&data)?;
        fs::write(&path, json).await?;
        
        debug!("Saved repo scan: {}", path.display());
        Ok(())
    }

    /// Load repository scan data
    pub async fn load_repo_scan<T: for<'de> Deserialize<'de>>(&self, repo_name: &str) -> Result<Option<T>> {
        let safe_name = repo_name.replace('/', "_");
        let path = self.repos_dir.join(format!("{}.json", safe_name));
        
        if !path.exists() {
            return Ok(None);
        }
        
        let content = fs::read_to_string(&path).await?;
        let data = serde_json::from_str(&content)?;
        
        Ok(Some(data))
    }

    /// List all saved repository scans
    pub async fn list_repo_scans(&self) -> Result<Vec<String>> {
        let mut repos = Vec::new();
        
        if self.repos_dir.exists() {
            let mut entries = fs::read_dir(&self.repos_dir).await?;
            
            while let Some(entry) = entries.next_entry().await? {
                let path = entry.path();
                if path.extension().map(|e| e == "json").unwrap_or(false) {
                    if let Some(stem) = path.file_stem() {
                        repos.push(stem.to_string_lossy().replace('_', "/"));
                    }
                }
            }
        }
        
        Ok(repos)
    }

    /// Save developer profile
    pub async fn save_profile(&self, profile: &DeveloperProfile) -> Result<()> {
        let json = serde_json::to_string_pretty(profile)?;
        fs::write(&self.profile_path, json).await?;
        
        info!("Saved developer profile");
        Ok(())
    }

    /// Load developer profile
    pub async fn load_profile(&self) -> Result<DeveloperProfile> {
        if !self.profile_path.exists() {
            return Ok(DeveloperProfile::default());
        }
        
        let content = fs::read_to_string(&self.profile_path).await?;
        let profile = serde_json::from_str(&content)?;
        
        Ok(profile)
    }

    /// Save knowledge base
    pub async fn save_knowledge(&self, knowledge: &KnowledgeBase) -> Result<()> {
        let json = serde_json::to_string_pretty(knowledge)?;
        fs::write(&self.knowledge_path, json).await?;
        
        info!("Saved knowledge base");
        Ok(())
    }

    /// Load knowledge base
    pub async fn load_knowledge(&self) -> Result<KnowledgeBase> {
        if !self.knowledge_path.exists() {
            return Ok(KnowledgeBase::default());
        }
        
        let content = fs::read_to_string(&self.knowledge_path).await?;
        let knowledge = serde_json::from_str(&content)?;
        
        Ok(knowledge)
    }

    /// Save configuration
    pub async fn save_config(&self, config: &Config) -> Result<()> {
        let toml = toml::to_string_pretty(config)?;
        fs::write(&self.config_path, toml).await?;
        
        Ok(())
    }

    /// Load configuration
    pub async fn load_config(&self) -> Result<Config> {
        if !self.config_path.exists() {
            return Ok(Config::default());
        }
        
        let content = fs::read_to_string(&self.config_path).await?;
        let config = toml::from_str(&content)?;
        
        Ok(config)
    }

    /// Get base directory path
    pub fn base_dir(&self) -> &Path {
        &self.base_dir
    }

    /// Get repos directory path
    pub fn repos_dir(&self) -> &Path {
        &self.repos_dir
    }

    /// Create default configuration
    async fn create_default_config(&self) -> Result<()> {
        let config = Config::default();
        self.save_config(&config).await?;
        Ok(())
    }

    /// Clear all stored data
    pub async fn clear_all(&self) -> Result<()> {
        if self.base_dir.exists() {
            fs::remove_dir_all(&self.base_dir).await?;
            fs::create_dir_all(&self.base_dir).await?;
            fs::create_dir_all(&self.repos_dir).await?;
        }
        
        info!("Cleared all storage data");
        Ok(())
    }

    /// Get storage statistics
    pub async fn get_stats(&self) -> Result<StorageStats> {
        let mut total_size = 0u64;
        let mut file_count = 0u64;
        
        if self.base_dir.exists() {
            let mut entries = fs::read_dir(&self.base_dir).await?;
            
            while let Some(entry) = entries.next_entry().await? {
                let metadata = entry.metadata().await?;
                if metadata.is_file() {
                    total_size += metadata.len();
                    file_count += 1;
                }
            }
        }

        let repo_count = self.list_repo_scans().await?.len() as u64;

        Ok(StorageStats {
            total_size_bytes: total_size,
            file_count,
            repo_count,
            profile_exists: self.profile_path.exists(),
            knowledge_exists: self.knowledge_path.exists(),
        })
    }
}

/// Configuration structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    pub github_token: Option<String>,
    pub scan_depth_days: i64,
    pub max_repos_to_scan: usize,
    pub exclude_repos: Vec<String>,
    pub include_forks: bool,
    pub include_archived: bool,
    pub auto_refresh_interval_hours: Option<u64>,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            github_token: None,
            scan_depth_days: 30,
            max_repos_to_scan: 100,
            exclude_repos: Vec::new(),
            include_forks: false,
            include_archived: false,
            auto_refresh_interval_hours: Some(24),
        }
    }
}

/// Storage statistics
#[derive(Debug, Clone)]
pub struct StorageStats {
    pub total_size_bytes: u64,
    pub file_count: u64,
    pub repo_count: u64,
    pub profile_exists: bool,
    pub knowledge_exists: bool,
}