1use std::path::PathBuf;
28
29use crate::config::Config;
30use crate::framework::{
31 ConfigFormat, FrameworkAdapter, FrameworkResolutionMode, InitOptions, all_adapters,
32 generate_config, initialize_framework, resolve_frameworks,
33};
34use crate::model::error::{FrameworkResolutionError, InitError};
35
36#[derive(Debug, Clone)]
38pub struct FrameworkEnablement {
39 pub name: String,
41 pub display_name: String,
43 pub settings_path: PathBuf,
45 pub hooks_config: serde_json::Value,
47 pub commands_run: Vec<String>,
49}
50
51#[derive(Debug, Clone)]
53pub struct FrameworkFailure {
54 pub name: String,
56 pub error: String,
58}
59
60#[derive(Debug, Clone)]
62pub struct EnableResult {
63 pub frameworks: Vec<FrameworkEnablement>,
65 pub failures: Vec<FrameworkFailure>,
67 pub db_path: Option<PathBuf>,
69 pub should_init_db: bool,
71}
72
73#[derive(Debug)]
75pub enum EnableError {
76 NoFrameworks {
78 supported: Vec<String>,
80 },
81 UnknownFramework(String),
83 Config(String),
85 Init(InitError),
87}
88
89impl std::fmt::Display for EnableError {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 match self {
92 Self::NoFrameworks { supported } => {
93 write!(
94 f,
95 "no supported AI coding frameworks detected.\n\
96 Supported frameworks: {}\n\
97 Install one first, or specify explicitly.",
98 supported.join(", ")
99 )
100 }
101 Self::UnknownFramework(name) => write!(f, "unknown framework: {name}"),
102 Self::Config(msg) => write!(f, "configuration error: {msg}"),
103 Self::Init(e) => write!(f, "{e}"),
104 }
105 }
106}
107
108impl std::error::Error for EnableError {
109 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
110 match self {
111 Self::Init(e) => Some(e),
112 _ => None,
113 }
114 }
115}
116
117impl From<InitError> for EnableError {
118 fn from(e: InitError) -> Self {
119 Self::Init(e)
120 }
121}
122
123impl From<FrameworkResolutionError> for EnableError {
124 fn from(e: FrameworkResolutionError) -> Self {
125 match e {
126 FrameworkResolutionError::NoFrameworksFound(_) => Self::NoFrameworks {
127 supported: all_adapters()
128 .iter()
129 .map(|a| a.name().to_string())
130 .collect(),
131 },
132 FrameworkResolutionError::UnknownFramework(name) => Self::UnknownFramework(name),
133 }
134 }
135}
136
137#[derive(Debug, Clone)]
139pub struct PreviewResult {
140 pub name: String,
142 pub hooks_config: serde_json::Value,
144 pub config_format: ConfigFormat,
146}
147
148pub fn enable(options: InitOptions) -> Result<EnableResult, EnableError> {
165 let config = Config::load().map_err(|e| EnableError::Config(e.to_string()))?;
166
167 let mut result = EnableResult {
168 frameworks: Vec::new(),
169 failures: Vec::new(),
170 db_path: None,
171 should_init_db: !options.hooks_only,
172 };
173
174 if result.should_init_db {
175 result.db_path = Config::db_path().ok();
176 }
177
178 if options.db_only {
179 return Ok(result);
180 }
181
182 let adapters = resolve_frameworks(
183 &options.frameworks,
184 Some(FrameworkResolutionMode::Installed),
185 )?;
186
187 for adapter in adapters {
189 match enable_single_framework(&config, adapter, &options) {
190 Ok(enablement) => {
191 result.frameworks.push(enablement);
192 }
193 Err(e) => {
194 result.failures.push(FrameworkFailure {
195 name: adapter.name().to_string(),
196 error: e.to_string(),
197 });
198 }
199 }
200 }
201
202 if result.frameworks.is_empty() && !result.failures.is_empty() {
204 let first_failure = &result.failures[0];
206 return Err(EnableError::Config(first_failure.error.clone()));
207 }
208
209 Ok(result)
210}
211
212fn enable_single_framework(
214 config: &Config,
215 adapter: &dyn FrameworkAdapter,
216 options: &InitOptions,
217) -> Result<FrameworkEnablement, InitError> {
218 let init_result = initialize_framework(config, adapter.name(), options)?;
219
220 Ok(FrameworkEnablement {
221 name: adapter.name().to_string(),
222 display_name: adapter.display_name().to_string(),
223 settings_path: init_result.settings_path,
224 hooks_config: init_result.hooks_config,
225 commands_run: init_result.commands_run,
226 })
227}
228
229pub fn preview_enable(options: &InitOptions) -> Result<Vec<PreviewResult>, EnableError> {
233 let config = Config::load().map_err(|e| EnableError::Config(e.to_string()))?;
234 let adapters = resolve_frameworks(
235 &options.frameworks,
236 Some(FrameworkResolutionMode::Installed),
237 )?;
238
239 Ok(adapters
240 .into_iter()
241 .map(|adapter| PreviewResult {
242 name: adapter.name().to_string(),
243 hooks_config: generate_config(adapter, &config, options),
244 config_format: adapter.config_format(),
245 })
246 .collect())
247}
248
249#[derive(Debug, Clone)]
251pub struct FrameworkDisablement {
252 pub name: String,
254 pub display_name: String,
256 pub settings_path: PathBuf,
258 pub hooks_removed: bool,
260 pub commands_run: Vec<String>,
262}
263
264#[derive(Debug, Clone)]
266pub struct DisableResult {
267 pub frameworks: Vec<FrameworkDisablement>,
269}
270
271pub fn disable(frameworks: &[&str]) -> Result<DisableResult, EnableError> {
289 disable_with_options(frameworks, false, false)
290}
291
292pub fn disable_with_options(
299 frameworks: &[&str],
300 local: bool,
301 settings_local: bool,
302) -> Result<DisableResult, EnableError> {
303 use crate::framework::get_adapter;
304
305 let adapters = if frameworks.is_empty() {
306 all_adapters()
308 .into_iter()
309 .filter(|a| a.has_mi6_hooks(local, settings_local))
310 .collect::<Vec<_>>()
311 } else {
312 let mut adapters = Vec::new();
314 for name in frameworks {
315 let adapter = get_adapter(name)
316 .ok_or_else(|| EnableError::UnknownFramework((*name).to_string()))?;
317 adapters.push(adapter);
318 }
319 adapters
320 };
321
322 let mut result = DisableResult {
323 frameworks: Vec::new(),
324 };
325
326 for adapter in adapters {
327 let settings_path = match adapter.settings_path(local, settings_local) {
328 Ok(path) => path,
329 Err(_) => continue,
330 };
331
332 let uninstall_result = adapter
334 .uninstall_hooks(local, settings_local)
335 .unwrap_or_default();
336
337 result.frameworks.push(FrameworkDisablement {
338 name: adapter.name().to_string(),
339 display_name: adapter.display_name().to_string(),
340 settings_path,
341 hooks_removed: uninstall_result.hooks_removed,
342 commands_run: uninstall_result.commands_run,
343 });
344 }
345
346 Ok(result)
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn test_enable_error_display() {
355 let err = EnableError::NoFrameworks {
356 supported: vec!["claude".into(), "gemini".into()],
357 };
358 assert!(
359 err.to_string()
360 .contains("no supported AI coding frameworks")
361 );
362
363 let err = EnableError::UnknownFramework("foo".into());
364 assert!(err.to_string().contains("unknown framework: foo"));
365 }
366
367 #[test]
368 fn test_enable_error_from_framework_resolution_error() {
369 let err: EnableError = FrameworkResolutionError::UnknownFramework("foo".to_string()).into();
370 assert!(matches!(err, EnableError::UnknownFramework(name) if name == "foo"));
371
372 let err: EnableError =
373 FrameworkResolutionError::NoFrameworksFound("no frameworks".to_string()).into();
374 assert!(matches!(err, EnableError::NoFrameworks { .. }));
375 }
376
377 #[test]
378 fn test_framework_failure_struct() {
379 let failure = FrameworkFailure {
380 name: "codex".to_string(),
381 error: "config.toml has invalid TOML syntax".to_string(),
382 };
383 assert_eq!(failure.name, "codex");
384 assert!(failure.error.contains("invalid TOML"));
385 }
386
387 #[test]
388 fn test_enable_result_has_failures_field() {
389 let result = EnableResult {
390 frameworks: vec![],
391 failures: vec![FrameworkFailure {
392 name: "codex".to_string(),
393 error: "some error".to_string(),
394 }],
395 db_path: None,
396 should_init_db: true,
397 };
398 assert!(result.frameworks.is_empty());
399 assert_eq!(result.failures.len(), 1);
400 assert_eq!(result.failures[0].name, "codex");
401 }
402
403 #[test]
404 fn test_enable_result_with_mixed_success_failure() {
405 let result = EnableResult {
406 frameworks: vec![FrameworkEnablement {
407 name: "claude".to_string(),
408 display_name: "Claude Code".to_string(),
409 settings_path: PathBuf::from("/home/user/.claude/settings.json"),
410 hooks_config: serde_json::json!({}),
411 commands_run: vec![],
412 }],
413 failures: vec![FrameworkFailure {
414 name: "codex".to_string(),
415 error: "config error".to_string(),
416 }],
417 db_path: None,
418 should_init_db: true,
419 };
420 assert_eq!(result.frameworks.len(), 1);
421 assert_eq!(result.frameworks[0].name, "claude");
422 assert_eq!(result.failures.len(), 1);
423 assert_eq!(result.failures[0].name, "codex");
424 }
425}