#![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;
pub struct Storage {
base_dir: PathBuf,
repos_dir: PathBuf,
profile_path: PathBuf,
knowledge_path: PathBuf,
config_path: PathBuf,
}
impl Storage {
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,
})
}
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?;
if !self.config_path.exists() {
self.create_default_config().await?;
}
info!("Storage initialized at {:?}", self.base_dir);
Ok(())
}
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(())
}
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))
}
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)
}
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(())
}
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)
}
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(())
}
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)
}
pub async fn save_config(&self, config: &Config) -> Result<()> {
let toml = toml::to_string_pretty(config)?;
fs::write(&self.config_path, toml).await?;
Ok(())
}
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)
}
pub fn base_dir(&self) -> &Path {
&self.base_dir
}
pub fn repos_dir(&self) -> &Path {
&self.repos_dir
}
async fn create_default_config(&self) -> Result<()> {
let config = Config::default();
self.save_config(&config).await?;
Ok(())
}
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(())
}
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(),
})
}
}
#[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),
}
}
}
#[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,
}