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