intent_engine/setup/
interactive.rs1use crate::error::{IntentError, Result};
4use crate::setup::claude_code::ClaudeCodeSetup;
5use crate::setup::{SetupModule, SetupOptions, SetupResult};
6use dialoguer::{theme::ColorfulTheme, Select};
7
8#[derive(Debug, Clone)]
10pub enum SetupTarget {
11 ClaudeCode { status: TargetStatus },
13 GeminiCli { status: TargetStatus },
15 Codex { status: TargetStatus },
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum TargetStatus {
22 Configured,
24 PartiallyConfigured,
26 NotConfigured,
28 ComingSoon,
30}
31
32impl SetupTarget {
33 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 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 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 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 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 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 pub fn is_selectable(&self) -> bool {
115 matches!(self, SetupTarget::ClaudeCode { .. })
116 }
117}
118
119pub struct SetupWizard {
121 targets: Vec<SetupTarget>,
122}
123
124impl SetupWizard {
125 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 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 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 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 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}