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