intent_engine/setup/
interactive.rs

1//! Interactive setup wizard for configuring intent-engine with AI tools
2
3use crate::error::{IntentError, Result};
4use crate::setup::claude_code::ClaudeCodeSetup;
5use crate::setup::{SetupModule, SetupOptions, SetupResult};
6use dialoguer::{theme::ColorfulTheme, Select};
7
8/// Available setup targets
9#[derive(Debug, Clone)]
10pub enum SetupTarget {
11    /// Claude Code - fully supported
12    ClaudeCode { status: TargetStatus },
13    /// Gemini CLI - coming soon
14    GeminiCli { status: TargetStatus },
15    /// Codex - coming soon
16    Codex { status: TargetStatus },
17}
18
19/// Status of a setup target
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum TargetStatus {
22    /// Already configured
23    Configured,
24    /// Partially configured or needs updates
25    PartiallyConfigured,
26    /// Not configured yet
27    NotConfigured,
28    /// Coming soon (not implemented)
29    ComingSoon,
30}
31
32impl SetupTarget {
33    /// Get display name for the target
34    pub fn display_name(&self) -> &str {
35        match self {
36            SetupTarget::ClaudeCode { .. } => "Claude Code",
37            SetupTarget::GeminiCli { .. } => "Gemini CLI",
38            SetupTarget::Codex { .. } => "Codex",
39        }
40    }
41
42    /// Get description for the target
43    pub fn description(&self) -> &str {
44        match self {
45            SetupTarget::ClaudeCode { .. } => {
46                "Install MCP server and session hooks for Claude Code"
47            },
48            SetupTarget::GeminiCli { .. } => "Install MCP server for Google Gemini CLI",
49            SetupTarget::Codex { .. } => "Install MCP server for OpenAI Codex",
50        }
51    }
52
53    /// Get status icon
54    pub fn status_icon(&self) -> &str {
55        let status = match self {
56            SetupTarget::ClaudeCode { status } => status,
57            SetupTarget::GeminiCli { status } => status,
58            SetupTarget::Codex { status } => status,
59        };
60
61        match status {
62            TargetStatus::Configured => "āœ“",
63            TargetStatus::PartiallyConfigured => "⚠",
64            TargetStatus::NotConfigured => "ā—‹",
65            TargetStatus::ComingSoon => "šŸ”œ",
66        }
67    }
68
69    /// Get status description
70    pub fn status_description(&self) -> String {
71        let status = match self {
72            SetupTarget::ClaudeCode { status } => status,
73            SetupTarget::GeminiCli { status } => status,
74            SetupTarget::Codex { status } => status,
75        };
76
77        match status {
78            TargetStatus::Configured => "Already configured".to_string(),
79            TargetStatus::PartiallyConfigured => "Partially configured".to_string(),
80            TargetStatus::NotConfigured => {
81                match self {
82                    SetupTarget::ClaudeCode { .. } => {
83                        // Check if .claude directory exists
84                        if let Ok(home) = crate::setup::common::get_home_dir() {
85                            let claude_dir = home.join(".claude");
86                            if claude_dir.exists() {
87                                "Detected at ~/.claude/".to_string()
88                            } else {
89                                "Not configured".to_string()
90                            }
91                        } else {
92                            "Not configured".to_string()
93                        }
94                    },
95                    _ => "Not configured".to_string(),
96                }
97            },
98            TargetStatus::ComingSoon => "Not yet supported".to_string(),
99        }
100    }
101
102    /// Format for display in selection menu
103    pub fn format_for_menu(&self) -> String {
104        format!(
105            "{} {} - {}\n    Status: {}",
106            self.status_icon(),
107            self.display_name(),
108            self.description(),
109            self.status_description()
110        )
111    }
112
113    /// Check if target is selectable (implemented)
114    pub fn is_selectable(&self) -> bool {
115        matches!(self, SetupTarget::ClaudeCode { .. })
116    }
117}
118
119/// Interactive setup wizard
120pub struct SetupWizard {
121    targets: Vec<SetupTarget>,
122}
123
124impl SetupWizard {
125    /// Create a new setup wizard
126    pub fn new() -> Self {
127        Self {
128            targets: vec![
129                SetupTarget::ClaudeCode {
130                    status: Self::detect_claude_code_status(),
131                },
132                SetupTarget::GeminiCli {
133                    status: TargetStatus::ComingSoon,
134                },
135                SetupTarget::Codex {
136                    status: TargetStatus::ComingSoon,
137                },
138            ],
139        }
140    }
141
142    /// Detect Claude Code configuration status
143    fn detect_claude_code_status() -> TargetStatus {
144        let home = match crate::setup::common::get_home_dir() {
145            Ok(h) => h,
146            Err(_) => return TargetStatus::NotConfigured,
147        };
148
149        let claude_json = home.join(".claude.json");
150        let hooks_dir = home.join(".claude/hooks");
151        let settings_json = home.join(".claude/settings.json");
152
153        let has_mcp = claude_json.exists();
154        let has_hooks = hooks_dir.exists();
155        let has_settings = settings_json.exists();
156
157        if has_mcp && has_hooks && has_settings {
158            TargetStatus::Configured
159        } else if has_mcp || has_hooks || has_settings {
160            TargetStatus::PartiallyConfigured
161        } else {
162            TargetStatus::NotConfigured
163        }
164    }
165
166    /// Run the interactive wizard
167    pub fn run(&self, opts: &SetupOptions) -> Result<SetupResult> {
168        println!("\nšŸš€ Intent-Engine Setup Wizard\n");
169        println!("Please select the tool you want to configure:\n");
170
171        let items: Vec<String> = self.targets.iter().map(|t| t.format_for_menu()).collect();
172
173        let selection = Select::with_theme(&ColorfulTheme::default())
174            .items(&items)
175            .default(0)
176            .interact()
177            .map_err(|e| IntentError::InvalidInput(format!("Selection cancelled: {}", e)))?;
178
179        let selected_target = &self.targets[selection];
180
181        // Check if target is selectable
182        if !selected_target.is_selectable() {
183            println!("\nāš ļø  This target is not yet supported.\n");
184            println!(
185                "The {} integration is planned for a future release.",
186                selected_target.display_name()
187            );
188            println!("\nCurrently supported:");
189            println!("  • Claude Code (fully functional)\n");
190            println!(
191                "Want to see {} support sooner?",
192                selected_target.display_name()
193            );
194            println!("šŸ‘‰ Vote for it: https://github.com/wayfind/intent-engine/issues\n");
195
196            return Ok(SetupResult {
197                success: false,
198                message: format!("{} is not yet supported", selected_target.display_name()),
199                files_modified: vec![],
200                connectivity_test: None,
201            });
202        }
203
204        // Execute setup for the selected target
205        match selected_target {
206            SetupTarget::ClaudeCode { .. } => {
207                println!("\nšŸ“¦ Setting up Claude Code integration...\n");
208                let module = ClaudeCodeSetup;
209                module.setup(opts)
210            },
211            _ => Err(IntentError::InvalidInput(
212                "Target not implemented".to_string(),
213            )),
214        }
215    }
216}
217
218impl Default for SetupWizard {
219    fn default() -> Self {
220        Self::new()
221    }
222}