foundry_mcp/cli/commands/
uninstall.rs

1//! Implementation of the uninstall command
2
3use crate::cli::args::UninstallArgs;
4use crate::core::installation;
5use crate::types::responses::{InstallationStatus, UninstallResponse};
6use crate::utils::formatting::format_uninstall_output;
7use anyhow::{Context, Result};
8
9pub async fn execute(args: UninstallArgs) -> Result<String> {
10    // Validate uninstallation target
11    validate_target(&args.target)?;
12
13    // Perform uninstallation based on target
14    let result = match args.target.as_str() {
15        "claude-code" => installation::uninstall_from_claude_code()
16            .await
17            .context("Failed to uninstall from Claude Code")?,
18        "cursor" => installation::uninstall_from_cursor(args.remove_config)
19            .await
20            .context("Failed to uninstall from Cursor")?,
21
22        _ => {
23            return Err(anyhow::anyhow!(
24                "Unsupported uninstallation target: {}. Supported targets: claude-code, cursor",
25                args.target
26            ));
27        }
28    };
29
30    // Build response data for both JSON and human-readable output
31    let response_data = UninstallResponse {
32        target: args.target.clone(),
33        config_path: result.config_path.clone(),
34        uninstallation_status: if result.success {
35            InstallationStatus::Success
36        } else {
37            InstallationStatus::Partial
38        },
39        actions_taken: result.actions_taken.clone(),
40        files_removed: result.files_removed.clone(),
41    };
42
43    // Return formatted output based on --json flag
44    if args.json {
45        // Return JSON format - only include the data for clean CLI output
46        Ok(serde_json::to_string_pretty(&response_data)?)
47    } else {
48        // Return human-readable format
49        Ok(format_uninstall_output(
50            &args.target,
51            &result.config_path,
52            result.success,
53            &result.actions_taken,
54            &result.files_removed,
55        ))
56    }
57}
58
59/// Validate the uninstallation target
60fn validate_target(target: &str) -> Result<()> {
61    match target {
62        "claude-code" | "cursor" => Ok(()),
63        _ => Err(anyhow::anyhow!(
64            "Unsupported uninstallation target: {}. Supported targets: claude-code, cursor",
65            target
66        )),
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use crate::core::installation::create_uninstallation_result;
74
75    #[test]
76    fn test_validate_target_valid() {
77        let valid_targets = vec!["claude-code", "cursor"];
78
79        for target in valid_targets {
80            assert!(
81                validate_target(target).is_ok(),
82                "Target '{}' should be valid",
83                target
84            );
85        }
86    }
87
88    #[test]
89    fn test_validate_target_invalid() {
90        let invalid_targets = vec!["", "vscode", "claude-desktop"];
91
92        for target in invalid_targets {
93            assert!(
94                validate_target(target).is_err(),
95                "Target '{}' should be invalid",
96                target
97            );
98        }
99    }
100
101    #[test]
102    fn test_execute_invalid_target() {
103        let args = UninstallArgs {
104            target: "invalid-target".to_string(),
105            remove_config: false,
106            json: false,
107        };
108
109        // Test the validation logic without calling execute()
110        let validation_result = validate_target(&args.target);
111        assert!(validation_result.is_err());
112        assert!(
113            validation_result
114                .unwrap_err()
115                .to_string()
116                .contains("Unsupported uninstallation target")
117        );
118    }
119
120    #[test]
121    fn test_uninstall_args_creation() {
122        let args = UninstallArgs {
123            target: "claude-code".to_string(),
124            remove_config: true,
125            json: false,
126        };
127
128        assert_eq!(args.target, "claude-code");
129        assert!(args.remove_config);
130    }
131
132    #[test]
133    fn test_uninstall_args_default_values() {
134        let args = UninstallArgs {
135            target: "cursor".to_string(),
136            remove_config: false,
137            json: false,
138        };
139
140        assert_eq!(args.target, "cursor");
141        assert!(!args.remove_config);
142    }
143
144    #[test]
145    fn test_create_uninstallation_result() {
146        let result = create_uninstallation_result(
147            true,
148            "/path/to/config.json".to_string(),
149            vec!["Action 1".to_string(), "Action 2".to_string()],
150            vec!["file1.json".to_string(), "file2.json".to_string()],
151        );
152
153        assert!(result.success);
154        assert_eq!(result.config_path, "/path/to/config.json");
155        assert_eq!(result.actions_taken.len(), 2);
156        assert_eq!(result.files_removed.len(), 2);
157        assert_eq!(result.actions_taken[0], "Action 1");
158        assert_eq!(result.files_removed[0], "file1.json");
159    }
160
161    #[test]
162    fn test_uninstall_args_with_config_removal() {
163        let args = UninstallArgs {
164            target: "cursor".to_string(),
165            remove_config: true,
166            json: false,
167        };
168
169        assert_eq!(args.target, "cursor");
170        assert!(args.remove_config);
171    }
172}