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 AllFrameworksFailed(Vec<FrameworkFailure>),
89}
90
91impl std::fmt::Display for EnableError {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 match self {
94 Self::NoFrameworks { supported } => {
95 write!(
96 f,
97 "no supported AI coding frameworks detected.\n\
98 Supported frameworks: {}\n\
99 Install one first, or specify explicitly.",
100 supported.join(", ")
101 )
102 }
103 Self::UnknownFramework(name) => write!(f, "unknown framework: {name}"),
104 Self::Config(msg) => write!(f, "configuration error: {msg}"),
105 Self::Init(e) => write!(f, "{e}"),
106 Self::AllFrameworksFailed(failures) => {
107 writeln!(f, "all frameworks failed to enable:")?;
108 for failure in failures {
109 writeln!(f, " - {}: {}", failure.name, failure.error)?;
110 }
111 Ok(())
112 }
113 }
114 }
115}
116
117impl std::error::Error for EnableError {
118 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
119 match self {
120 Self::Init(e) => Some(e),
121 _ => None,
122 }
123 }
124}
125
126impl From<InitError> for EnableError {
127 fn from(e: InitError) -> Self {
128 Self::Init(e)
129 }
130}
131
132impl From<FrameworkResolutionError> for EnableError {
133 fn from(e: FrameworkResolutionError) -> Self {
134 match e {
135 FrameworkResolutionError::NoFrameworksFound(_) => Self::NoFrameworks {
136 supported: all_adapters()
137 .iter()
138 .map(|a| a.name().to_string())
139 .collect(),
140 },
141 FrameworkResolutionError::UnknownFramework(name) => Self::UnknownFramework(name),
142 }
143 }
144}
145
146#[derive(Debug, Clone)]
148pub struct PreviewResult {
149 pub name: String,
151 pub hooks_config: serde_json::Value,
153 pub config_format: ConfigFormat,
155}
156
157pub fn enable(options: InitOptions) -> Result<EnableResult, EnableError> {
174 let config = Config::load().map_err(|e| EnableError::Config(e.to_string()))?;
175
176 let mut result = EnableResult {
177 frameworks: Vec::new(),
178 failures: Vec::new(),
179 db_path: None,
180 should_init_db: !options.hooks_only,
181 };
182
183 if result.should_init_db {
184 result.db_path = Config::db_path().ok();
185 }
186
187 if options.db_only {
188 return Ok(result);
189 }
190
191 let adapters = resolve_frameworks(
192 &options.frameworks,
193 Some(FrameworkResolutionMode::Installed),
194 )?;
195
196 for adapter in adapters {
198 match enable_single_framework(&config, adapter, &options) {
199 Ok(enablement) => {
200 result.frameworks.push(enablement);
201 }
202 Err(e) => {
203 result.failures.push(FrameworkFailure {
204 name: adapter.name().to_string(),
205 error: e.to_string(),
206 });
207 }
208 }
209 }
210
211 if result.frameworks.is_empty() && !result.failures.is_empty() {
213 return Err(EnableError::AllFrameworksFailed(result.failures));
214 }
215
216 Ok(result)
217}
218
219fn enable_single_framework(
221 config: &Config,
222 adapter: &dyn FrameworkAdapter,
223 options: &InitOptions,
224) -> Result<FrameworkEnablement, InitError> {
225 let init_result = initialize_framework(config, adapter.name(), options)?;
226
227 Ok(FrameworkEnablement {
228 name: adapter.name().to_string(),
229 display_name: adapter.display_name().to_string(),
230 settings_path: init_result.settings_path,
231 hooks_config: init_result.hooks_config,
232 commands_run: init_result.commands_run,
233 })
234}
235
236pub fn preview_enable(options: &InitOptions) -> Result<Vec<PreviewResult>, EnableError> {
240 let config = Config::load().map_err(|e| EnableError::Config(e.to_string()))?;
241 let adapters = resolve_frameworks(
242 &options.frameworks,
243 Some(FrameworkResolutionMode::Installed),
244 )?;
245
246 Ok(adapters
247 .into_iter()
248 .map(|adapter| PreviewResult {
249 name: adapter.name().to_string(),
250 hooks_config: generate_config(adapter, &config, options),
251 config_format: adapter.config_format(),
252 })
253 .collect())
254}
255
256#[derive(Debug, Clone)]
258pub struct FrameworkDisablement {
259 pub name: String,
261 pub display_name: String,
263 pub settings_path: PathBuf,
265 pub hooks_removed: bool,
267 pub commands_run: Vec<String>,
269}
270
271#[derive(Debug, Clone)]
273pub struct DisableResult {
274 pub frameworks: Vec<FrameworkDisablement>,
276}
277
278pub fn disable(frameworks: &[&str]) -> Result<DisableResult, EnableError> {
296 disable_with_options(frameworks, false, false)
297}
298
299pub fn disable_with_options(
306 frameworks: &[&str],
307 local: bool,
308 settings_local: bool,
309) -> Result<DisableResult, EnableError> {
310 use crate::framework::get_adapter;
311
312 let adapters = if frameworks.is_empty() {
313 all_adapters()
315 .into_iter()
316 .filter(|a| a.has_mi6_hooks(local, settings_local))
317 .collect::<Vec<_>>()
318 } else {
319 let mut adapters = Vec::new();
321 for name in frameworks {
322 let adapter = get_adapter(name)
323 .ok_or_else(|| EnableError::UnknownFramework((*name).to_string()))?;
324 adapters.push(adapter);
325 }
326 adapters
327 };
328
329 let mut result = DisableResult {
330 frameworks: Vec::new(),
331 };
332
333 for adapter in adapters {
334 let settings_path = match adapter.settings_path(local, settings_local) {
335 Ok(path) => path,
336 Err(_) => continue,
337 };
338
339 let uninstall_result = adapter
341 .uninstall_hooks(local, settings_local)
342 .unwrap_or_default();
343
344 result.frameworks.push(FrameworkDisablement {
345 name: adapter.name().to_string(),
346 display_name: adapter.display_name().to_string(),
347 settings_path,
348 hooks_removed: uninstall_result.hooks_removed,
349 commands_run: uninstall_result.commands_run,
350 });
351 }
352
353 Ok(result)
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn test_enable_error_display() {
362 let err = EnableError::NoFrameworks {
363 supported: vec!["claude".into(), "gemini".into()],
364 };
365 assert!(
366 err.to_string()
367 .contains("no supported AI coding frameworks")
368 );
369
370 let err = EnableError::UnknownFramework("foo".into());
371 assert!(err.to_string().contains("unknown framework: foo"));
372 }
373
374 #[test]
375 fn test_enable_error_from_framework_resolution_error() {
376 let err: EnableError = FrameworkResolutionError::UnknownFramework("foo".to_string()).into();
377 assert!(matches!(err, EnableError::UnknownFramework(name) if name == "foo"));
378
379 let err: EnableError =
380 FrameworkResolutionError::NoFrameworksFound("no frameworks".to_string()).into();
381 assert!(matches!(err, EnableError::NoFrameworks { .. }));
382 }
383
384 #[test]
385 fn test_framework_failure_struct() {
386 let failure = FrameworkFailure {
387 name: "codex".to_string(),
388 error: "config.toml has invalid TOML syntax".to_string(),
389 };
390 assert_eq!(failure.name, "codex");
391 assert!(failure.error.contains("invalid TOML"));
392 }
393
394 #[test]
395 fn test_enable_result_has_failures_field() {
396 let result = EnableResult {
397 frameworks: vec![],
398 failures: vec![FrameworkFailure {
399 name: "codex".to_string(),
400 error: "some error".to_string(),
401 }],
402 db_path: None,
403 should_init_db: true,
404 };
405 assert!(result.frameworks.is_empty());
406 assert_eq!(result.failures.len(), 1);
407 assert_eq!(result.failures[0].name, "codex");
408 }
409
410 #[test]
411 fn test_enable_result_with_mixed_success_failure() {
412 let result = EnableResult {
413 frameworks: vec![FrameworkEnablement {
414 name: "claude".to_string(),
415 display_name: "Claude Code".to_string(),
416 settings_path: PathBuf::from("/home/user/.claude/settings.json"),
417 hooks_config: serde_json::json!({}),
418 commands_run: vec![],
419 }],
420 failures: vec![FrameworkFailure {
421 name: "codex".to_string(),
422 error: "config error".to_string(),
423 }],
424 db_path: None,
425 should_init_db: true,
426 };
427 assert_eq!(result.frameworks.len(), 1);
428 assert_eq!(result.frameworks[0].name, "claude");
429 assert_eq!(result.failures.len(), 1);
430 assert_eq!(result.failures[0].name, "codex");
431 }
432}