infiniloom_engine/
exit_codes.rs

1//! Semantic exit codes for CI/CD integration
2//!
3//! This module defines standardized exit codes that allow CI/CD pipelines
4//! to programmatically handle different error conditions. Exit codes follow
5//! Unix conventions (0 = success, non-zero = error) with specific meanings.
6//!
7//! # Exit Code Ranges
8//!
9//! | Range | Category |
10//! |-------|----------|
11//! | 0 | Success |
12//! | 1-9 | General errors |
13//! | 10-19 | Security issues |
14//! | 20-29 | Configuration errors |
15//! | 30-39 | I/O and resource errors |
16//! | 40-49 | Validation errors |
17//!
18//! # CI/CD Usage
19//!
20//! ```bash
21//! # In a CI pipeline
22//! infiniloom embed /path/to/repo --security-check
23//! exit_code=$?
24//!
25//! case $exit_code in
26//!     0) echo "Success" ;;
27//!     10) echo "Secrets detected - blocking PR" ;;
28//!     11) echo "PII detected - review required" ;;
29//!     12) echo "License violation - legal review needed" ;;
30//!     *) echo "Error: $exit_code" ;;
31//! esac
32//! ```
33//!
34//! # GitHub Actions Integration
35//!
36//! ```yaml
37//! - name: Security Scan
38//!   id: scan
39//!   run: infiniloom embed . --security-check
40//!   continue-on-error: true
41//!
42//! - name: Block on Secrets
43//!   if: steps.scan.outcome == 'failure' && steps.scan.exit-code == 10
44//!   run: |
45//!     echo "::error::Secrets detected in codebase"
46//!     exit 1
47//!
48//! - name: Warn on PII
49//!   if: steps.scan.exit-code == 11
50//!   run: echo "::warning::PII detected - review recommended"
51//! ```
52
53use std::fmt;
54use std::process::ExitCode as StdExitCode;
55
56/// Semantic exit codes for CLI commands
57///
58/// These exit codes provide meaningful status information to CI/CD systems
59/// and shell scripts. Each code represents a specific condition that can
60/// be programmatically handled.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62#[repr(u8)]
63pub enum ExitCode {
64    // === Success (0) ===
65    /// Operation completed successfully
66    Success = 0,
67
68    // === General Errors (1-9) ===
69    /// Unspecified error
70    GeneralError = 1,
71    /// Command-line argument parsing error
72    ArgumentError = 2,
73    /// Internal error (bug)
74    InternalError = 3,
75    /// Operation interrupted by user (Ctrl+C)
76    Interrupted = 4,
77    /// Operation timed out
78    Timeout = 5,
79
80    // === Security Issues (10-19) ===
81    /// Secrets/credentials detected in code
82    SecretsDetected = 10,
83    /// PII (Personally Identifiable Information) detected
84    PiiDetected = 11,
85    /// License compliance violation (GPL, AGPL, etc.)
86    LicenseViolation = 12,
87    /// Path traversal attack blocked
88    PathTraversalBlocked = 13,
89    /// Security scan failed
90    SecurityScanFailed = 14,
91
92    // === Configuration Errors (20-29) ===
93    /// Configuration file not found
94    ConfigNotFound = 20,
95    /// Invalid configuration format
96    ConfigInvalid = 21,
97    /// Missing required configuration
98    ConfigMissing = 22,
99    /// Conflicting configuration options
100    ConfigConflict = 23,
101    /// Unsupported configuration version
102    ConfigVersionError = 24,
103
104    // === I/O and Resource Errors (30-39) ===
105    /// File or directory not found
106    NotFound = 30,
107    /// Permission denied
108    PermissionDenied = 31,
109    /// Disk full or quota exceeded
110    DiskFull = 32,
111    /// Network error (e.g., remote clone failed)
112    NetworkError = 33,
113    /// Resource limit exceeded (too many files, etc.)
114    ResourceLimitExceeded = 34,
115    /// File too large
116    FileTooLarge = 35,
117    /// Binary file detected
118    BinaryFileDetected = 36,
119
120    // === Validation Errors (40-49) ===
121    /// No files matched the include patterns
122    NoFilesMatched = 40,
123    /// No chunks generated (empty output)
124    NoChunksGenerated = 41,
125    /// Invalid pattern (glob/regex)
126    InvalidPattern = 42,
127    /// Unsupported language or file type
128    UnsupportedLanguage = 43,
129    /// Manifest integrity error
130    ManifestCorrupted = 44,
131    /// Token budget exceeded
132    BudgetExceeded = 45,
133}
134
135impl ExitCode {
136    /// Get the numeric exit code value
137    pub fn code(&self) -> u8 {
138        *self as u8
139    }
140
141    /// Get the exit code category
142    pub fn category(&self) -> ExitCodeCategory {
143        match self.code() {
144            0 => ExitCodeCategory::Success,
145            1..=9 => ExitCodeCategory::GeneralError,
146            10..=19 => ExitCodeCategory::SecurityIssue,
147            20..=29 => ExitCodeCategory::ConfigurationError,
148            30..=39 => ExitCodeCategory::IoError,
149            40..=49 => ExitCodeCategory::ValidationError,
150            _ => ExitCodeCategory::GeneralError,
151        }
152    }
153
154    /// Get a human-readable name for this exit code
155    pub fn name(&self) -> &'static str {
156        match self {
157            Self::Success => "SUCCESS",
158            Self::GeneralError => "GENERAL_ERROR",
159            Self::ArgumentError => "ARGUMENT_ERROR",
160            Self::InternalError => "INTERNAL_ERROR",
161            Self::Interrupted => "INTERRUPTED",
162            Self::Timeout => "TIMEOUT",
163            Self::SecretsDetected => "SECRETS_DETECTED",
164            Self::PiiDetected => "PII_DETECTED",
165            Self::LicenseViolation => "LICENSE_VIOLATION",
166            Self::PathTraversalBlocked => "PATH_TRAVERSAL_BLOCKED",
167            Self::SecurityScanFailed => "SECURITY_SCAN_FAILED",
168            Self::ConfigNotFound => "CONFIG_NOT_FOUND",
169            Self::ConfigInvalid => "CONFIG_INVALID",
170            Self::ConfigMissing => "CONFIG_MISSING",
171            Self::ConfigConflict => "CONFIG_CONFLICT",
172            Self::ConfigVersionError => "CONFIG_VERSION_ERROR",
173            Self::NotFound => "NOT_FOUND",
174            Self::PermissionDenied => "PERMISSION_DENIED",
175            Self::DiskFull => "DISK_FULL",
176            Self::NetworkError => "NETWORK_ERROR",
177            Self::ResourceLimitExceeded => "RESOURCE_LIMIT_EXCEEDED",
178            Self::FileTooLarge => "FILE_TOO_LARGE",
179            Self::BinaryFileDetected => "BINARY_FILE_DETECTED",
180            Self::NoFilesMatched => "NO_FILES_MATCHED",
181            Self::NoChunksGenerated => "NO_CHUNKS_GENERATED",
182            Self::InvalidPattern => "INVALID_PATTERN",
183            Self::UnsupportedLanguage => "UNSUPPORTED_LANGUAGE",
184            Self::ManifestCorrupted => "MANIFEST_CORRUPTED",
185            Self::BudgetExceeded => "BUDGET_EXCEEDED",
186        }
187    }
188
189    /// Get a description of this exit code
190    pub fn description(&self) -> &'static str {
191        match self {
192            Self::Success => "Operation completed successfully",
193            Self::GeneralError => "An unspecified error occurred",
194            Self::ArgumentError => "Invalid command-line arguments",
195            Self::InternalError => "Internal error (please report this bug)",
196            Self::Interrupted => "Operation was interrupted by user",
197            Self::Timeout => "Operation timed out",
198            Self::SecretsDetected => "Secrets or credentials detected in code",
199            Self::PiiDetected => "Personally identifiable information detected",
200            Self::LicenseViolation => "License compliance violation detected",
201            Self::PathTraversalBlocked => "Path traversal attack blocked",
202            Self::SecurityScanFailed => "Security scan failed to complete",
203            Self::ConfigNotFound => "Configuration file not found",
204            Self::ConfigInvalid => "Invalid configuration file format",
205            Self::ConfigMissing => "Required configuration is missing",
206            Self::ConfigConflict => "Conflicting configuration options",
207            Self::ConfigVersionError => "Unsupported configuration version",
208            Self::NotFound => "File or directory not found",
209            Self::PermissionDenied => "Permission denied",
210            Self::DiskFull => "Disk full or quota exceeded",
211            Self::NetworkError => "Network operation failed",
212            Self::ResourceLimitExceeded => "Resource limit exceeded",
213            Self::FileTooLarge => "File exceeds size limit",
214            Self::BinaryFileDetected => "Binary file detected",
215            Self::NoFilesMatched => "No files matched the specified patterns",
216            Self::NoChunksGenerated => "No chunks were generated",
217            Self::InvalidPattern => "Invalid glob or regex pattern",
218            Self::UnsupportedLanguage => "Unsupported language or file type",
219            Self::ManifestCorrupted => "Manifest file is corrupted",
220            Self::BudgetExceeded => "Token budget exceeded",
221        }
222    }
223
224    /// Check if this is a security-related exit code
225    pub fn is_security_issue(&self) -> bool {
226        matches!(self.category(), ExitCodeCategory::SecurityIssue)
227    }
228
229    /// Check if this exit code indicates success
230    pub fn is_success(&self) -> bool {
231        *self == Self::Success
232    }
233
234    /// Create an exit code from a numeric value
235    pub fn from_code(code: u8) -> Self {
236        match code {
237            0 => Self::Success,
238            1 => Self::GeneralError,
239            2 => Self::ArgumentError,
240            3 => Self::InternalError,
241            4 => Self::Interrupted,
242            5 => Self::Timeout,
243            10 => Self::SecretsDetected,
244            11 => Self::PiiDetected,
245            12 => Self::LicenseViolation,
246            13 => Self::PathTraversalBlocked,
247            14 => Self::SecurityScanFailed,
248            20 => Self::ConfigNotFound,
249            21 => Self::ConfigInvalid,
250            22 => Self::ConfigMissing,
251            23 => Self::ConfigConflict,
252            24 => Self::ConfigVersionError,
253            30 => Self::NotFound,
254            31 => Self::PermissionDenied,
255            32 => Self::DiskFull,
256            33 => Self::NetworkError,
257            34 => Self::ResourceLimitExceeded,
258            35 => Self::FileTooLarge,
259            36 => Self::BinaryFileDetected,
260            40 => Self::NoFilesMatched,
261            41 => Self::NoChunksGenerated,
262            42 => Self::InvalidPattern,
263            43 => Self::UnsupportedLanguage,
264            44 => Self::ManifestCorrupted,
265            45 => Self::BudgetExceeded,
266            _ => Self::GeneralError,
267        }
268    }
269}
270
271impl fmt::Display for ExitCode {
272    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273        write!(f, "{} ({}): {}", self.name(), self.code(), self.description())
274    }
275}
276
277impl From<ExitCode> for u8 {
278    fn from(code: ExitCode) -> Self {
279        code.code()
280    }
281}
282
283impl From<ExitCode> for i32 {
284    fn from(code: ExitCode) -> Self {
285        code.code() as i32
286    }
287}
288
289impl From<ExitCode> for StdExitCode {
290    fn from(code: ExitCode) -> Self {
291        StdExitCode::from(code.code())
292    }
293}
294
295/// Categories of exit codes
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297pub enum ExitCodeCategory {
298    /// Success (0)
299    Success,
300    /// General errors (1-9)
301    GeneralError,
302    /// Security issues (10-19)
303    SecurityIssue,
304    /// Configuration errors (20-29)
305    ConfigurationError,
306    /// I/O and resource errors (30-39)
307    IoError,
308    /// Validation errors (40-49)
309    ValidationError,
310}
311
312impl ExitCodeCategory {
313    /// Get a human-readable name for this category
314    pub fn name(&self) -> &'static str {
315        match self {
316            Self::Success => "Success",
317            Self::GeneralError => "General Error",
318            Self::SecurityIssue => "Security Issue",
319            Self::ConfigurationError => "Configuration Error",
320            Self::IoError => "I/O Error",
321            Self::ValidationError => "Validation Error",
322        }
323    }
324}
325
326impl fmt::Display for ExitCodeCategory {
327    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328        write!(f, "{}", self.name())
329    }
330}
331
332/// Result type that can be converted to an exit code
333pub struct ExitResult {
334    code: ExitCode,
335    message: Option<String>,
336}
337
338impl ExitResult {
339    /// Create a success result
340    pub fn success() -> Self {
341        Self { code: ExitCode::Success, message: None }
342    }
343
344    /// Create an error result
345    pub fn error(code: ExitCode, message: impl Into<String>) -> Self {
346        Self { code, message: Some(message.into()) }
347    }
348
349    /// Create from just an exit code
350    pub fn from_code(code: ExitCode) -> Self {
351        Self { code, message: None }
352    }
353
354    /// Get the exit code
355    pub fn code(&self) -> ExitCode {
356        self.code
357    }
358
359    /// Get the message, if any
360    pub fn message(&self) -> Option<&str> {
361        self.message.as_deref()
362    }
363
364    /// Check if this is a success result
365    pub fn is_success(&self) -> bool {
366        self.code.is_success()
367    }
368
369    /// Convert to a process exit code
370    pub fn exit(self) -> ! {
371        if let Some(ref msg) = self.message {
372            if self.code.is_success() {
373                println!("{}", msg);
374            } else {
375                eprintln!("Error: {}", msg);
376            }
377        }
378        std::process::exit(self.code.code() as i32)
379    }
380}
381
382impl From<ExitCode> for ExitResult {
383    fn from(code: ExitCode) -> Self {
384        Self::from_code(code)
385    }
386}
387
388/// Helper trait to convert errors to exit codes
389pub trait ToExitCode {
390    /// Convert to an appropriate exit code
391    fn to_exit_code(&self) -> ExitCode;
392}
393
394impl ToExitCode for std::io::Error {
395    fn to_exit_code(&self) -> ExitCode {
396        use std::io::ErrorKind;
397        match self.kind() {
398            ErrorKind::NotFound => ExitCode::NotFound,
399            ErrorKind::PermissionDenied => ExitCode::PermissionDenied,
400            ErrorKind::TimedOut => ExitCode::Timeout,
401            ErrorKind::Interrupted => ExitCode::Interrupted,
402            ErrorKind::WriteZero | ErrorKind::StorageFull => ExitCode::DiskFull,
403            _ => ExitCode::GeneralError,
404        }
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411
412    #[test]
413    fn test_exit_code_values() {
414        assert_eq!(ExitCode::Success.code(), 0);
415        assert_eq!(ExitCode::GeneralError.code(), 1);
416        assert_eq!(ExitCode::SecretsDetected.code(), 10);
417        assert_eq!(ExitCode::PiiDetected.code(), 11);
418        assert_eq!(ExitCode::ConfigNotFound.code(), 20);
419        assert_eq!(ExitCode::NotFound.code(), 30);
420        assert_eq!(ExitCode::NoFilesMatched.code(), 40);
421    }
422
423    #[test]
424    fn test_exit_code_categories() {
425        assert_eq!(ExitCode::Success.category(), ExitCodeCategory::Success);
426        assert_eq!(ExitCode::GeneralError.category(), ExitCodeCategory::GeneralError);
427        assert_eq!(ExitCode::SecretsDetected.category(), ExitCodeCategory::SecurityIssue);
428        assert_eq!(ExitCode::ConfigNotFound.category(), ExitCodeCategory::ConfigurationError);
429        assert_eq!(ExitCode::NotFound.category(), ExitCodeCategory::IoError);
430        assert_eq!(ExitCode::NoFilesMatched.category(), ExitCodeCategory::ValidationError);
431    }
432
433    #[test]
434    fn test_is_security_issue() {
435        assert!(ExitCode::SecretsDetected.is_security_issue());
436        assert!(ExitCode::PiiDetected.is_security_issue());
437        assert!(ExitCode::LicenseViolation.is_security_issue());
438        assert!(!ExitCode::Success.is_security_issue());
439        assert!(!ExitCode::NotFound.is_security_issue());
440    }
441
442    #[test]
443    fn test_from_code() {
444        assert_eq!(ExitCode::from_code(0), ExitCode::Success);
445        assert_eq!(ExitCode::from_code(10), ExitCode::SecretsDetected);
446        assert_eq!(ExitCode::from_code(255), ExitCode::GeneralError); // Unknown maps to general
447    }
448
449    #[test]
450    fn test_display() {
451        let code = ExitCode::SecretsDetected;
452        let display = format!("{}", code);
453        assert!(display.contains("SECRETS_DETECTED"));
454        assert!(display.contains("10"));
455    }
456
457    #[test]
458    fn test_exit_result() {
459        let success = ExitResult::success();
460        assert!(success.is_success());
461        assert_eq!(success.code(), ExitCode::Success);
462
463        let error = ExitResult::error(ExitCode::SecretsDetected, "Found API keys");
464        assert!(!error.is_success());
465        assert_eq!(error.code(), ExitCode::SecretsDetected);
466        assert_eq!(error.message(), Some("Found API keys"));
467    }
468
469    #[test]
470    fn test_io_error_conversion() {
471        use std::io::{Error, ErrorKind};
472
473        let not_found = Error::new(ErrorKind::NotFound, "file not found");
474        assert_eq!(not_found.to_exit_code(), ExitCode::NotFound);
475
476        let permission = Error::new(ErrorKind::PermissionDenied, "access denied");
477        assert_eq!(permission.to_exit_code(), ExitCode::PermissionDenied);
478    }
479
480    #[test]
481    fn test_conversions() {
482        let code = ExitCode::SecretsDetected;
483
484        let u8_code: u8 = code.into();
485        assert_eq!(u8_code, 10);
486
487        let i32_code: i32 = code.into();
488        assert_eq!(i32_code, 10);
489    }
490}