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 {
342            code: ExitCode::Success,
343            message: None,
344        }
345    }
346
347    /// Create an error result
348    pub fn error(code: ExitCode, message: impl Into<String>) -> Self {
349        Self {
350            code,
351            message: Some(message.into()),
352        }
353    }
354
355    /// Create from just an exit code
356    pub fn from_code(code: ExitCode) -> Self {
357        Self {
358            code,
359            message: None,
360        }
361    }
362
363    /// Get the exit code
364    pub fn code(&self) -> ExitCode {
365        self.code
366    }
367
368    /// Get the message, if any
369    pub fn message(&self) -> Option<&str> {
370        self.message.as_deref()
371    }
372
373    /// Check if this is a success result
374    pub fn is_success(&self) -> bool {
375        self.code.is_success()
376    }
377
378    /// Convert to a process exit code
379    pub fn exit(self) -> ! {
380        if let Some(ref msg) = self.message {
381            if self.code.is_success() {
382                println!("{}", msg);
383            } else {
384                eprintln!("Error: {}", msg);
385            }
386        }
387        std::process::exit(self.code.code() as i32)
388    }
389}
390
391impl From<ExitCode> for ExitResult {
392    fn from(code: ExitCode) -> Self {
393        Self::from_code(code)
394    }
395}
396
397/// Helper trait to convert errors to exit codes
398pub trait ToExitCode {
399    /// Convert to an appropriate exit code
400    fn to_exit_code(&self) -> ExitCode;
401}
402
403impl ToExitCode for std::io::Error {
404    fn to_exit_code(&self) -> ExitCode {
405        use std::io::ErrorKind;
406        match self.kind() {
407            ErrorKind::NotFound => ExitCode::NotFound,
408            ErrorKind::PermissionDenied => ExitCode::PermissionDenied,
409            ErrorKind::TimedOut => ExitCode::Timeout,
410            ErrorKind::Interrupted => ExitCode::Interrupted,
411            ErrorKind::WriteZero | ErrorKind::StorageFull => ExitCode::DiskFull,
412            _ => ExitCode::GeneralError,
413        }
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420
421    #[test]
422    fn test_exit_code_values() {
423        assert_eq!(ExitCode::Success.code(), 0);
424        assert_eq!(ExitCode::GeneralError.code(), 1);
425        assert_eq!(ExitCode::SecretsDetected.code(), 10);
426        assert_eq!(ExitCode::PiiDetected.code(), 11);
427        assert_eq!(ExitCode::ConfigNotFound.code(), 20);
428        assert_eq!(ExitCode::NotFound.code(), 30);
429        assert_eq!(ExitCode::NoFilesMatched.code(), 40);
430    }
431
432    #[test]
433    fn test_exit_code_categories() {
434        assert_eq!(ExitCode::Success.category(), ExitCodeCategory::Success);
435        assert_eq!(ExitCode::GeneralError.category(), ExitCodeCategory::GeneralError);
436        assert_eq!(ExitCode::SecretsDetected.category(), ExitCodeCategory::SecurityIssue);
437        assert_eq!(ExitCode::ConfigNotFound.category(), ExitCodeCategory::ConfigurationError);
438        assert_eq!(ExitCode::NotFound.category(), ExitCodeCategory::IoError);
439        assert_eq!(ExitCode::NoFilesMatched.category(), ExitCodeCategory::ValidationError);
440    }
441
442    #[test]
443    fn test_is_security_issue() {
444        assert!(ExitCode::SecretsDetected.is_security_issue());
445        assert!(ExitCode::PiiDetected.is_security_issue());
446        assert!(ExitCode::LicenseViolation.is_security_issue());
447        assert!(!ExitCode::Success.is_security_issue());
448        assert!(!ExitCode::NotFound.is_security_issue());
449    }
450
451    #[test]
452    fn test_from_code() {
453        assert_eq!(ExitCode::from_code(0), ExitCode::Success);
454        assert_eq!(ExitCode::from_code(10), ExitCode::SecretsDetected);
455        assert_eq!(ExitCode::from_code(255), ExitCode::GeneralError); // Unknown maps to general
456    }
457
458    #[test]
459    fn test_display() {
460        let code = ExitCode::SecretsDetected;
461        let display = format!("{}", code);
462        assert!(display.contains("SECRETS_DETECTED"));
463        assert!(display.contains("10"));
464    }
465
466    #[test]
467    fn test_exit_result() {
468        let success = ExitResult::success();
469        assert!(success.is_success());
470        assert_eq!(success.code(), ExitCode::Success);
471
472        let error = ExitResult::error(ExitCode::SecretsDetected, "Found API keys");
473        assert!(!error.is_success());
474        assert_eq!(error.code(), ExitCode::SecretsDetected);
475        assert_eq!(error.message(), Some("Found API keys"));
476    }
477
478    #[test]
479    fn test_io_error_conversion() {
480        use std::io::{Error, ErrorKind};
481
482        let not_found = Error::new(ErrorKind::NotFound, "file not found");
483        assert_eq!(not_found.to_exit_code(), ExitCode::NotFound);
484
485        let permission = Error::new(ErrorKind::PermissionDenied, "access denied");
486        assert_eq!(permission.to_exit_code(), ExitCode::PermissionDenied);
487    }
488
489    #[test]
490    fn test_conversions() {
491        let code = ExitCode::SecretsDetected;
492
493        let u8_code: u8 = code.into();
494        assert_eq!(u8_code, 10);
495
496        let i32_code: i32 = code.into();
497        assert_eq!(i32_code, 10);
498    }
499}