#![allow(dead_code)]
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use crate::utils::hooks::hooks_settings::{
EditableSettingSource, HookMatcher as SettingsHookMatcher,
get_hooks_for_source as get_settings_hooks_for_source, parse_hook_event,
};
use crate::utils::settings::{get_settings_file_path_for_source, read_settings_file};
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct HooksSettings {
#[serde(flatten)]
pub events: HashMap<String, Vec<HookMatcher>>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct HookMatcher {
#[serde(skip_serializing_if = "Option::is_none")]
pub matcher: Option<String>,
pub hooks: Vec<serde_json::Value>,
}
lazy_static::lazy_static! {
static ref HOOKS_CONFIG_SNAPSHOT: Arc<Mutex<Option<HooksSettings>>> = Arc::new(Mutex::new(None));
}
fn get_hooks_from_allowed_sources_impl() -> HooksSettings {
if should_disable_all_hooks_including_managed() {
return HooksSettings::default();
}
if should_allow_managed_hooks_only() {
return HooksSettings::default();
}
if is_restricted_to_plugin_only() {
return HooksSettings::default();
}
if is_disable_all_hooks_in_merged() {
return HooksSettings::default();
}
let mut merged = HooksSettings::default();
let sources = [
EditableSettingSource::UserSettings,
EditableSettingSource::ProjectSettings,
EditableSettingSource::LocalSettings,
];
for source in &sources {
if let Some(parsed_hooks) = get_settings_hooks_for_source(source) {
for (event_name, matchers) in parsed_hooks {
let converted: Vec<HookMatcher> = matchers
.into_iter()
.map(|m| HookMatcher {
matcher: m.matcher,
hooks: m
.hooks
.into_iter()
.filter_map(|h| serde_json::to_value(&h).ok())
.collect(),
})
.collect();
if !converted.is_empty() {
merged
.events
.entry(event_name)
.or_insert_with(Vec::new)
.extend(converted);
}
}
}
}
merged
}
pub fn should_allow_managed_hooks_only() -> bool {
if std::env::var("AI_CODE_ALLOW_MANAGED_HOOKS_ONLY")
.ok()
.map(|v| v == "true" || v == "1")
.unwrap_or(false)
{
return true;
}
false
}
pub fn should_disable_all_hooks_including_managed() -> bool {
if std::env::var("AI_CODE_DISABLE_ALL_HOOKS")
.ok()
.map(|v| v == "true" || v == "1")
.unwrap_or(false)
{
return true;
}
false
}
fn is_disable_all_hooks_in_merged() -> bool {
let sources = [
EditableSettingSource::UserSettings,
EditableSettingSource::ProjectSettings,
EditableSettingSource::LocalSettings,
];
for source in &sources {
if let Some(path) = get_settings_file_path_for_source(source) {
if let Some(settings) = read_settings_file(&path) {
if settings.get("disableAllHooks").and_then(|v| v.as_bool()) == Some(true) {
return true;
}
}
}
}
false
}
fn is_restricted_to_plugin_only() -> bool {
if std::env::var("AI_CODE_STRICT_PLUGIN_ONLY_HOOKS")
.ok()
.map(|v| v == "true" || v == "1")
.unwrap_or(false)
{
return true;
}
false
}
pub fn capture_hooks_config_snapshot() {
let hooks = get_hooks_from_allowed_sources_impl();
let mut snapshot = HOOKS_CONFIG_SNAPSHOT.lock().unwrap();
*snapshot = Some(hooks);
}
pub fn update_hooks_config_snapshot() {
reset_settings_cache();
let hooks = get_hooks_from_allowed_sources_impl();
let mut snapshot = HOOKS_CONFIG_SNAPSHOT.lock().unwrap();
*snapshot = Some(hooks);
}
pub fn get_hooks_config_from_snapshot() -> Option<HooksSettings> {
let mut snapshot = HOOKS_CONFIG_SNAPSHOT.lock().unwrap();
if snapshot.is_none() {
let hooks = get_hooks_from_allowed_sources_impl();
*snapshot = Some(hooks.clone());
}
snapshot.clone()
}
pub fn reset_hooks_config_snapshot() {
let mut snapshot = HOOKS_CONFIG_SNAPSHOT.lock().unwrap();
*snapshot = None;
reset_sdk_init_state();
}
fn reset_settings_cache() {
log::debug!("Resetting settings cache");
}
fn reset_sdk_init_state() {
log::debug!("Resetting SDK init state");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_capture_and_get_snapshot() {
reset_hooks_config_snapshot();
capture_hooks_config_snapshot();
let snapshot = get_hooks_config_from_snapshot();
assert!(snapshot.is_some());
reset_hooks_config_snapshot();
}
#[test]
fn test_update_snapshot() {
reset_hooks_config_snapshot();
capture_hooks_config_snapshot();
update_hooks_config_snapshot();
let snapshot = get_hooks_config_from_snapshot();
assert!(snapshot.is_some());
reset_hooks_config_snapshot();
}
#[test]
fn test_get_snapshot_auto_capture() {
reset_hooks_config_snapshot();
let snapshot = get_hooks_config_from_snapshot();
assert!(snapshot.is_some());
reset_hooks_config_snapshot();
}
#[test]
fn test_should_allow_managed_hooks_only_env_var() {
let original = std::env::var("AI_CODE_ALLOW_MANAGED_HOOKS_ONLY").ok();
unsafe {
std::env::set_var("AI_CODE_ALLOW_MANAGED_HOOKS_ONLY", "true");
}
assert!(should_allow_managed_hooks_only());
unsafe {
std::env::set_var("AI_CODE_ALLOW_MANAGED_HOOKS_ONLY", "1");
}
assert!(should_allow_managed_hooks_only());
unsafe {
std::env::set_var("AI_CODE_ALLOW_MANAGED_HOOKS_ONLY", "false");
}
assert!(!should_allow_managed_hooks_only());
unsafe {
std::env::remove_var("AI_CODE_ALLOW_MANAGED_HOOKS_ONLY");
}
assert!(!should_allow_managed_hooks_only());
match original {
Some(val) => unsafe {
std::env::set_var("AI_CODE_ALLOW_MANAGED_HOOKS_ONLY", val);
},
None => unsafe {
std::env::remove_var("AI_CODE_ALLOW_MANAGED_HOOKS_ONLY");
},
}
}
#[test]
fn test_should_disable_all_hooks_env_var() {
let original = std::env::var("AI_CODE_DISABLE_ALL_HOOKS").ok();
unsafe {
std::env::set_var("AI_CODE_DISABLE_ALL_HOOKS", "true");
}
assert!(should_disable_all_hooks_including_managed());
unsafe {
std::env::set_var("AI_CODE_DISABLE_ALL_HOOKS", "1");
}
assert!(should_disable_all_hooks_including_managed());
unsafe {
std::env::set_var("AI_CODE_DISABLE_ALL_HOOKS", "false");
}
assert!(!should_disable_all_hooks_including_managed());
unsafe {
std::env::remove_var("AI_CODE_DISABLE_ALL_HOOKS");
}
assert!(!should_disable_all_hooks_including_managed());
match original {
Some(val) => unsafe {
std::env::set_var("AI_CODE_DISABLE_ALL_HOOKS", val);
},
None => unsafe {
std::env::remove_var("AI_CODE_DISABLE_ALL_HOOKS");
},
}
}
#[test]
fn test_hooks_settings_serialization() {
let mut settings = HooksSettings::default();
settings.events.insert(
"Stop".to_string(),
vec![HookMatcher {
matcher: None,
hooks: vec![serde_json::json!({"type": "command", "command": "echo hi"})],
}],
);
let json = serde_json::to_string(&settings).unwrap();
let parsed: HooksSettings = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.events.len(), 1);
assert!(parsed.events.contains_key("Stop"));
}
#[test]
fn test_empty_snapshot_returns_default() {
reset_hooks_config_snapshot();
let snapshot = get_hooks_config_from_snapshot();
assert!(snapshot.is_some());
assert!(snapshot.unwrap().events.is_empty());
reset_hooks_config_snapshot();
}
}