//! Custom error types for gitlore. //! //! Uses thiserror for ergonomic error definitions with structured error codes. use serde::Serialize; use thiserror::Error; /// Error codes for programmatic error handling. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ErrorCode { ConfigNotFound, ConfigInvalid, GitLabAuthFailed, GitLabNotFound, GitLabRateLimited, GitLabNetworkError, DatabaseLocked, DatabaseError, MigrationFailed, TokenNotSet, TransformError, IoError, InternalError, OllamaUnavailable, OllamaModelNotFound, EmbeddingFailed, } impl std::fmt::Display for ErrorCode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let code = match self { Self::ConfigNotFound => "CONFIG_NOT_FOUND", Self::ConfigInvalid => "CONFIG_INVALID", Self::GitLabAuthFailed => "GITLAB_AUTH_FAILED", Self::GitLabNotFound => "GITLAB_NOT_FOUND", Self::GitLabRateLimited => "GITLAB_RATE_LIMITED", Self::GitLabNetworkError => "GITLAB_NETWORK_ERROR", Self::DatabaseLocked => "DB_LOCKED", Self::DatabaseError => "DB_ERROR", Self::MigrationFailed => "MIGRATION_FAILED", Self::TokenNotSet => "TOKEN_NOT_SET", Self::TransformError => "TRANSFORM_ERROR", Self::IoError => "IO_ERROR", Self::InternalError => "INTERNAL_ERROR", Self::OllamaUnavailable => "OLLAMA_UNAVAILABLE", Self::OllamaModelNotFound => "OLLAMA_MODEL_NOT_FOUND", Self::EmbeddingFailed => "EMBEDDING_FAILED", }; write!(f, "{code}") } } impl ErrorCode { /// Get the exit code for this error (for robot mode). pub fn exit_code(&self) -> i32 { match self { Self::InternalError => 1, Self::ConfigNotFound => 2, Self::ConfigInvalid => 3, Self::TokenNotSet => 4, Self::GitLabAuthFailed => 5, Self::GitLabNotFound => 6, Self::GitLabRateLimited => 7, Self::GitLabNetworkError => 8, Self::DatabaseLocked => 9, Self::DatabaseError => 10, Self::MigrationFailed => 11, Self::IoError => 12, Self::TransformError => 13, Self::OllamaUnavailable => 14, Self::OllamaModelNotFound => 15, Self::EmbeddingFailed => 16, } } } /// Main error type for gitlore. #[derive(Error, Debug)] pub enum LoreError { #[error("Config file not found at {path}. Run \"lore init\" first.")] ConfigNotFound { path: String }, #[error("Invalid config: {details}")] ConfigInvalid { details: String }, #[error("GitLab authentication failed. Check your token has read_api scope.")] GitLabAuthFailed, #[error("GitLab resource not found: {resource}")] GitLabNotFound { resource: String }, #[error("Rate limited. Retry after {retry_after}s")] GitLabRateLimited { retry_after: u64 }, #[error("Cannot connect to GitLab at {base_url}")] GitLabNetworkError { base_url: String, #[source] source: Option, }, #[error( "Another sync is running (owner: {owner}, started: {started_at}). Use --force to override if stale." )] DatabaseLocked { owner: String, started_at: String }, #[error("Migration {version} failed: {message}")] MigrationFailed { version: i32, message: String, #[source] source: Option, }, #[error("GitLab token not set. Export {env_var} environment variable.")] TokenNotSet { env_var: String }, #[error("Database error: {0}")] Database(#[from] rusqlite::Error), #[error("HTTP error: {0}")] Http(#[from] reqwest::Error), #[error("JSON error: {0}")] Json(#[from] serde_json::Error), #[error("IO error: {0}")] Io(#[from] std::io::Error), #[error("Transform error: {0}")] Transform(#[from] crate::gitlab::transformers::issue::TransformError), #[error("Not found: {0}")] NotFound(String), #[error("Ambiguous: {0}")] Ambiguous(String), #[error("{0}")] Other(String), #[error("Cannot connect to Ollama at {base_url}. Is it running?")] OllamaUnavailable { base_url: String, #[source] source: Option, }, #[error("Ollama model '{model}' not found. Run: ollama pull {model}")] OllamaModelNotFound { model: String }, #[error("Embedding failed for document {document_id}: {reason}")] EmbeddingFailed { document_id: i64, reason: String }, #[error("No embeddings found. Run: lore embed")] EmbeddingsNotBuilt, } impl LoreError { /// Get the error code for programmatic handling. pub fn code(&self) -> ErrorCode { match self { Self::ConfigNotFound { .. } => ErrorCode::ConfigNotFound, Self::ConfigInvalid { .. } => ErrorCode::ConfigInvalid, Self::GitLabAuthFailed => ErrorCode::GitLabAuthFailed, Self::GitLabNotFound { .. } => ErrorCode::GitLabNotFound, Self::GitLabRateLimited { .. } => ErrorCode::GitLabRateLimited, Self::GitLabNetworkError { .. } => ErrorCode::GitLabNetworkError, Self::DatabaseLocked { .. } => ErrorCode::DatabaseLocked, Self::MigrationFailed { .. } => ErrorCode::MigrationFailed, Self::TokenNotSet { .. } => ErrorCode::TokenNotSet, Self::Database(_) => ErrorCode::DatabaseError, Self::Http(_) => ErrorCode::GitLabNetworkError, Self::Json(_) => ErrorCode::InternalError, Self::Io(_) => ErrorCode::IoError, Self::Transform(_) => ErrorCode::TransformError, Self::NotFound(_) => ErrorCode::GitLabNotFound, Self::Ambiguous(_) => ErrorCode::GitLabNotFound, Self::Other(_) => ErrorCode::InternalError, Self::OllamaUnavailable { .. } => ErrorCode::OllamaUnavailable, Self::OllamaModelNotFound { .. } => ErrorCode::OllamaModelNotFound, Self::EmbeddingFailed { .. } => ErrorCode::EmbeddingFailed, Self::EmbeddingsNotBuilt => ErrorCode::EmbeddingFailed, } } /// Get a suggestion for how to fix this error, including inline examples. pub fn suggestion(&self) -> Option<&'static str> { match self { Self::ConfigNotFound { .. } => Some( "Run 'lore init' to set up your GitLab connection.\n\n Expected: ~/.config/lore/config.json", ), Self::ConfigInvalid { .. } => Some( "Check config file syntax or run 'lore init' to recreate.\n\n Example:\n lore init\n lore init --force", ), Self::GitLabAuthFailed => Some( "Verify token has read_api scope and is not expired.\n\n Example:\n export GITLAB_TOKEN=glpat-xxxxxxxxxxxx\n lore auth", ), Self::GitLabNotFound { .. } => Some( "Check the resource path exists and you have access.\n\n Example:\n lore issues -p group/project\n lore mrs -p group/project", ), Self::GitLabRateLimited { .. } => Some("Wait and retry, or reduce request frequency"), Self::GitLabNetworkError { .. } => Some( "Check network connection and GitLab URL.\n\n Example:\n lore doctor\n lore auth", ), Self::DatabaseLocked { .. } => Some( "Wait for other sync to complete or use --force.\n\n Example:\n lore ingest --force\n lore ingest issues --force", ), Self::MigrationFailed { .. } => Some( "Check database file permissions or reset with 'lore reset'.\n\n Example:\n lore migrate\n lore reset --yes", ), Self::TokenNotSet { .. } => Some( "Export the token to your shell:\n\n export GITLAB_TOKEN=glpat-xxxxxxxxxxxx\n\n Your token needs the read_api scope.", ), Self::Database(_) => Some( "Check database file permissions or reset with 'lore reset'.\n\n Example:\n lore doctor\n lore reset --yes", ), Self::Http(_) => Some("Check network connection"), Self::NotFound(_) => Some( "Verify the entity exists.\n\n Example:\n lore issues\n lore mrs", ), Self::Ambiguous(_) => Some( "Use -p to choose a specific project.\n\n Example:\n lore issues 42 -p group/project-a\n lore mrs 99 -p group/project-b", ), Self::OllamaUnavailable { .. } => Some("Start Ollama: ollama serve"), Self::OllamaModelNotFound { .. } => { Some("Pull the model: ollama pull nomic-embed-text") } Self::EmbeddingFailed { .. } => { Some("Check Ollama logs or retry with 'lore embed --retry-failed'") } Self::EmbeddingsNotBuilt => Some("Generate embeddings first: lore embed"), Self::Json(_) | Self::Io(_) | Self::Transform(_) | Self::Other(_) => None, } } /// Get the exit code for this error. pub fn exit_code(&self) -> i32 { self.code().exit_code() } /// Convert to robot-mode JSON error output. pub fn to_robot_error(&self) -> RobotError { RobotError { code: self.code().to_string(), message: self.to_string(), suggestion: self.suggestion().map(String::from), } } } /// Structured error for robot mode JSON output. #[derive(Debug, Serialize)] pub struct RobotError { pub code: String, pub message: String, #[serde(skip_serializing_if = "Option::is_none")] pub suggestion: Option, } /// Wrapper for robot mode error output. #[derive(Debug, Serialize)] pub struct RobotErrorOutput { pub error: RobotError, } impl From<&LoreError> for RobotErrorOutput { fn from(e: &LoreError) -> Self { Self { error: e.to_robot_error(), } } } pub type Result = std::result::Result;