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 { .. } => "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 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 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 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 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 pub fn is_selectable(&self) -> bool {
113 matches!(self, SetupTarget::ClaudeCode { .. })
114 }
115}
116
117pub struct SetupWizard {
119 targets: Vec<SetupTarget>,
120}
121
122impl SetupWizard {
123 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 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 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 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 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}