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}; pub struct InitInputs { pub gitlab_url: String, pub token_env_var: String, pub project_paths: Vec, pub default_project: Option, } pub struct InitOptions { pub config_path: Option, pub force: bool, pub non_interactive: bool, } pub struct InitResult { pub config_path: String, pub data_dir: String, pub user: UserInfo, pub projects: Vec, pub default_project: Option, } pub struct UserInfo { pub username: String, pub name: String, } pub struct ProjectInfo { pub path: String, pub name: String, } 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(); 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(), )); } if url::Url::parse(&inputs.gitlab_url).is_err() { return Err(LoreError::Other(format!( "Invalid GitLab URL: {}", inputs.gitlab_url ))); } let token = std::env::var(&inputs.token_env_var).map_err(|_| LoreError::TokenNotSet { env_var: inputs.token_env_var.clone(), })?; 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, }; 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, )); } // Validate default_project matches one of the configured project paths if let Some(ref dp) = inputs.default_project { let matched = inputs.project_paths.iter().any(|p| { p.eq_ignore_ascii_case(dp) || p.to_ascii_lowercase() .ends_with(&format!("/{}", dp.to_ascii_lowercase())) }); if !matched { return Err(LoreError::Other(format!( "defaultProject '{dp}' does not match any configured project path" ))); } } if let Some(parent) = config_path.parent() { fs::create_dir_all(parent)?; } 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(), default_project: inputs.default_project.clone(), }; let config_json = serde_json::to_string_pretty(&config)?; fs::write(&config_path, format!("{config_json}\n"))?; fs::create_dir_all(&data_dir)?; let db_path = data_dir.join("lore.db"); let conn = create_connection(&db_path)?; run_migrations(&conn)?; for (_, gitlab_project) in &validated_projects { conn.execute( "INSERT INTO projects (gitlab_project_id, path_with_namespace, default_branch, web_url) VALUES (?, ?, ?, ?) ON CONFLICT(gitlab_project_id) DO UPDATE SET path_with_namespace = excluded.path_with_namespace, default_branch = excluded.default_branch, web_url = excluded.web_url", ( 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(), default_project: inputs.default_project, }) }