//! Init command - initialize configuration and database. use std::fs; use crate::core::config::{MinimalConfig, MinimalGitLabConfig, ProjectConfig}; use crate::core::db::{create_connection, run_migrations}; use crate::core::error::{LoreError, Result}; use crate::core::paths::{get_config_path, get_data_dir}; use crate::gitlab::{GitLabClient, GitLabProject}; /// Input data for init command. pub struct InitInputs { pub gitlab_url: String, pub token_env_var: String, pub project_paths: Vec, } /// Options for init command. pub struct InitOptions { pub config_path: Option, pub force: bool, pub non_interactive: bool, } /// Result of successful init. pub struct InitResult { pub config_path: String, pub data_dir: String, pub user: UserInfo, pub projects: Vec, } pub struct UserInfo { pub username: String, pub name: String, } pub struct ProjectInfo { pub path: String, pub name: String, } /// Run the init command programmatically. pub async fn run_init(inputs: InitInputs, options: InitOptions) -> Result { let config_path = get_config_path(options.config_path.as_deref()); let data_dir = get_data_dir(); // 1. Check if config exists (force takes precedence over non_interactive) if config_path.exists() && !options.force { if options.non_interactive { return Err(LoreError::Other(format!( "Config file exists at {}. Use --force to overwrite.", config_path.display() ))); } return Err(LoreError::Other( "User cancelled config overwrite.".to_string(), )); } // 2. Validate GitLab URL format if url::Url::parse(&inputs.gitlab_url).is_err() { return Err(LoreError::Other(format!( "Invalid GitLab URL: {}", inputs.gitlab_url ))); } // 3. Check token is set in environment let token = std::env::var(&inputs.token_env_var).map_err(|_| LoreError::TokenNotSet { env_var: inputs.token_env_var.clone(), })?; // 4. Create GitLab client and test authentication let client = GitLabClient::new(&inputs.gitlab_url, &token, None); let gitlab_user = client.get_current_user().await.map_err(|e| { if matches!(e, LoreError::GitLabAuthFailed) { LoreError::Other(format!("Authentication failed for {}", inputs.gitlab_url)) } else { e } })?; let user = UserInfo { username: gitlab_user.username, name: gitlab_user.name, }; // 5. Validate each project path let mut validated_projects: Vec<(ProjectInfo, GitLabProject)> = Vec::new(); for project_path in &inputs.project_paths { let project = client.get_project(project_path).await.map_err(|e| { if matches!(e, LoreError::GitLabNotFound { .. }) { LoreError::Other(format!("Project not found: {project_path}")) } else { e } })?; validated_projects.push(( ProjectInfo { path: project_path.clone(), name: project.name.clone().unwrap_or_else(|| { project_path .split('/') .next_back() .unwrap_or(project_path) .to_string() }), }, project, )); } // 6. All validations passed - now write config and setup DB // Create config directory if needed if let Some(parent) = config_path.parent() { fs::create_dir_all(parent)?; } // Write minimal config (rely on serde defaults) let config = MinimalConfig { gitlab: MinimalGitLabConfig { base_url: inputs.gitlab_url, token_env_var: inputs.token_env_var, }, projects: inputs .project_paths .iter() .map(|p| ProjectConfig { path: p.clone() }) .collect(), }; let config_json = serde_json::to_string_pretty(&config)?; fs::write(&config_path, format!("{config_json}\n"))?; // 7. Create data directory and initialize database fs::create_dir_all(&data_dir)?; let db_path = data_dir.join("lore.db"); let conn = create_connection(&db_path)?; // Run embedded migrations run_migrations(&conn)?; // 8. Insert validated projects for (_, gitlab_project) in &validated_projects { conn.execute( "INSERT INTO projects (gitlab_project_id, path_with_namespace, default_branch, web_url) VALUES (?, ?, ?, ?)", ( gitlab_project.id, &gitlab_project.path_with_namespace, &gitlab_project.default_branch, &gitlab_project.web_url, ), )?; } Ok(InitResult { config_path: config_path.display().to_string(), data_dir: data_dir.display().to_string(), user, projects: validated_projects.into_iter().map(|(p, _)| p).collect(), }) }