use std::sync::{Arc, LazyLock};
use serde::{Deserialize, Serialize};
use crate::{
cli::commands::Commands,
config::{
config::ConfigConfig, general::GeneralConfig, hooks::HooksConfig, scanner::ScannerConfig,
status::StatusConfig, sync::SyncConfig,
},
};
pub static CONFIG: LazyLock<Arc<GuardyConfig>> = LazyLock::new(|| {
Arc::new(GuardyConfig::default())
});
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuardyConfig {
pub general: Arc<GeneralConfig>,
pub scanner: Arc<ScannerConfig>,
pub hooks: Arc<HooksConfig>,
pub sync: Arc<SyncConfig>,
pub status: Arc<StatusConfig>,
pub config: Arc<ConfigConfig>,
pub mcp: Arc<McpConfig>,
pub external_tools: Arc<ExternalToolsConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpConfig {
pub port: u16,
pub host: String,
pub enabled: bool,
pub tools: Arc<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExternalToolsConfig {
pub git_crypt: String,
pub nx: String,
pub pnpm: String,
}
impl Default for GuardyConfig {
fn default() -> Self {
let cli = crate::cli::CLI.get();
let (scan_args, hooks_args, sync_args, status_args, config_args) = if let Some(cli) = cli {
let scan_args = if let Some(Commands::Scan(args)) = &cli.command {
Some(args)
} else {
None
};
let hooks_args = if let Some(Commands::Hooks(args)) = &cli.command {
Some(args)
} else {
None
};
let sync_args = if let Some(Commands::Sync(args)) = &cli.command {
Some(args)
} else {
None
};
let status_args = if let Some(Commands::Status(args)) = &cli.command {
Some(args)
} else {
None
};
let config_args = if let Some(Commands::Config(args)) = &cli.command {
Some(args)
} else {
None
};
(scan_args, hooks_args, sync_args, status_args, config_args)
} else {
(None, None, None, None, None)
};
GuardyConfig {
general: Arc::new(crate::config::general::GeneralConfig::build(cli)),
scanner: Arc::new(crate::config::scanner::ScannerConfig::build(scan_args)),
hooks: Arc::new(crate::config::hooks::HooksConfig::build(hooks_args)),
sync: Arc::new(crate::config::sync::SyncConfig::build(sync_args)),
status: Arc::new(crate::config::status::StatusConfig::build(status_args)),
config: Arc::new(crate::config::config::ConfigConfig::build(config_args)),
mcp: Arc::new(McpConfig {
port: 8080,
host: "127.0.0.1".into(),
enabled: false,
tools: Arc::new(vec![
"git-status".into(),
"hook-run".into(),
"security-scan".into(),
]),
}),
external_tools: Arc::new(ExternalToolsConfig {
git_crypt: "git-crypt".into(),
nx: "nx".into(),
pnpm: "pnpm".into(),
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scan::types::ScanMode;
#[test]
fn test_default_config_loads() {
let config = GuardyConfig::default();
assert_eq!(config.scanner.mode, ScanMode::Auto);
assert_eq!(config.scanner.thread_percentage, 70);
assert!(!config.hooks.skip_all);
}
#[test]
fn test_arc_clone_is_cheap() {
let config = GuardyConfig::default();
let original_scanner_ptr = std::sync::Arc::as_ptr(&config.scanner);
let clone = config.clone();
let clone_scanner_ptr = std::sync::Arc::as_ptr(&clone.scanner);
assert_eq!(
original_scanner_ptr, clone_scanner_ptr,
"Arc clone should share the same data"
);
let mut clones = Vec::new();
for _ in 0..100 {
clones.push(config.clone());
}
assert_eq!(clones.len(), 100);
for clone in &clones {
assert_eq!(std::sync::Arc::as_ptr(&clone.scanner), original_scanner_ptr);
}
}
#[test]
fn test_global_config_loads() {
let config = CONFIG.clone();
assert_eq!(config.scanner.mode, ScanMode::Auto);
assert!(!config.hooks.skip_all);
}
#[test]
fn test_config_is_singleton() {
let config1 = CONFIG.clone();
let config2 = CONFIG.clone();
assert!(Arc::ptr_eq(&config1, &config2));
}
}