casting_cli/
commands.rs

1/// Command handlers for CLI operations
2use crate::api::{fetch_api_spec, format_api_spec_as_help};
3use crate::cli::{Cli, TypeArg};
4use crate::prompt::generate_system_prompt;
5use crate::tool_config::{setup_config_migrator, ToolConfig, ToolType};
6use crate::utils::{
7    find_tool_binary, generate_unique_aliases, get_tool_config_dir, get_tool_help,
8    resolve_tool_name,
9};
10use clap::CommandFactory;
11use clap_complete::{generate, Shell};
12use std::fs;
13use std::io;
14use std::path::PathBuf;
15use std::process::Command;
16
17/// Detected type result with additional info
18enum DetectedType {
19    Cli { bin_path: String },
20    Api { spec_url: String, base_url: String },
21    Expert,
22    Kaiba { profile: String },
23}
24
25/// Detect tool type based on arguments and heuristics
26fn detect_tool_type(
27    name: &str,
28    explicit_type: Option<TypeArg>,
29    spec: Option<String>,
30    base_url: Option<String>,
31    profile: Option<String>,
32) -> anyhow::Result<(DetectedType, String)> {
33    // 1. Explicit --type takes priority
34    if let Some(type_arg) = explicit_type {
35        match type_arg {
36            TypeArg::Api => {
37                let spec_url = spec
38                    .ok_or_else(|| anyhow::anyhow!("--type api requires --spec and --base-url"))?;
39                let base_url_str = base_url
40                    .ok_or_else(|| anyhow::anyhow!("--type api requires --spec and --base-url"))?;
41                println!("πŸ”§ Type: API (explicit --type)");
42                let api_spec = fetch_api_spec(&spec_url)?;
43                let help = format_api_spec_as_help(&api_spec);
44                return Ok((
45                    DetectedType::Api {
46                        spec_url,
47                        base_url: base_url_str,
48                    },
49                    help,
50                ));
51            }
52            TypeArg::Cli => {
53                println!("πŸ”§ Type: CLI (explicit --type)");
54                let bin_path = find_tool_binary(name)?;
55                let help = get_tool_help(name)?;
56                return Ok((DetectedType::Cli { bin_path }, help));
57            }
58            TypeArg::Expert => {
59                println!("πŸ”§ Type: Expert (explicit --type)");
60                return Ok((DetectedType::Expert, String::new()));
61            }
62            TypeArg::Kaiba => {
63                let profile_name = profile
64                    .ok_or_else(|| anyhow::anyhow!("--type kaiba requires --profile <NAME>"))?;
65                println!("πŸ”§ Type: Kaiba (explicit --type)");
66                println!("🧠 Profile: {}", profile_name);
67                // Verify kaiba CLI is available
68                if Command::new("kaiba").arg("--version").output().is_err() {
69                    anyhow::bail!(
70                        "kaiba CLI not found. Install with: cargo install --path crates/kaiba-cli"
71                    );
72                }
73                return Ok((
74                    DetectedType::Kaiba {
75                        profile: profile_name,
76                    },
77                    String::new(),
78                ));
79            }
80        }
81    }
82
83    // 2. --spec provided β†’ API
84    if let (Some(spec_url), Some(base_url_str)) = (spec, base_url) {
85        println!("πŸ” Detected: API (--spec provided)");
86        let api_spec = fetch_api_spec(&spec_url)?;
87        let help = format_api_spec_as_help(&api_spec);
88        return Ok((
89            DetectedType::Api {
90                spec_url,
91                base_url: base_url_str,
92            },
93            help,
94        ));
95    }
96
97    // 3. Multi-word name β†’ Expert
98    if name.contains(' ') {
99        println!("πŸ” Detected: Expert (multi-word name)");
100        return Ok((DetectedType::Expert, String::new()));
101    }
102
103    // 4. Single word β†’ try CLI, fallback to Expert
104    println!("πŸ” Detecting type for '{}'...", name);
105    match find_tool_binary(name) {
106        Ok(bin_path) => {
107            match get_tool_help(name) {
108                Ok(help) => {
109                    println!("βœ“ Found CLI tool at: {}", bin_path);
110                    Ok((DetectedType::Cli { bin_path }, help))
111                }
112                Err(_) => {
113                    // Binary found but no help - still treat as CLI
114                    println!("βœ“ Found CLI tool at: {} (no help available)", bin_path);
115                    Ok((DetectedType::Cli { bin_path }, String::new()))
116                }
117            }
118        }
119        Err(_) => {
120            println!("βœ— '{}' not found as CLI tool", name);
121            println!("βœ“ Registering as Expert");
122            Ok((DetectedType::Expert, String::new()))
123        }
124    }
125}
126
127/// Handle the `make` command - create a new tool configuration
128pub fn handle_make(
129    tool_name: String,
130    persona: Option<String>,
131    explicit_type: Option<TypeArg>,
132    spec: Option<String>,
133    base_url: Option<String>,
134    profile: Option<String>,
135) -> anyhow::Result<()> {
136    println!("✨ Creating configuration for: {}", tool_name);
137    println!();
138
139    // 1. Detect or use explicit type
140    let (detected, help_output) =
141        detect_tool_type(&tool_name, explicit_type, spec, base_url, profile)?;
142
143    let (tool_type, type_label) = match detected {
144        DetectedType::Cli { bin_path } => (ToolType::Cli { bin_path }, "CLI"),
145        DetectedType::Api { spec_url, base_url } => (ToolType::Api { spec_url, base_url }, "API"),
146        DetectedType::Expert => (ToolType::Expert, "Expert"),
147        DetectedType::Kaiba { profile } => (ToolType::Kaiba { profile }, "Kaiba"),
148    };
149
150    if !help_output.is_empty() {
151        println!("πŸ“– Retrieved documentation ({} bytes)", help_output.len());
152    }
153
154    // 2. Create output directory
155    let output_dir = get_tool_config_dir(&tool_name)?;
156    fs::create_dir_all(&output_dir)?;
157
158    // 3. Generate aliases for multi-word names (especially Expert types)
159    let aliases = generate_unique_aliases(&tool_name)?;
160
161    // 4. Generate config.json (save as V3 format with version)
162    let config = ToolConfig {
163        tool_name: tool_name.clone(),
164        tool_type: tool_type.clone(),
165        persona: persona.clone(),
166        aliases: aliases.clone(),
167        created_at: chrono::Utc::now().to_rfc3339(),
168    };
169
170    let migrator = setup_config_migrator()?;
171    let config_json = migrator.save_domain("tool_config", config)?;
172
173    let config_path = output_dir.join("config.json");
174    fs::write(&config_path, config_json)?;
175
176    // 5. Save help.txt (even if empty for Expert)
177    let help_path = output_dir.join("help.txt");
178    fs::write(&help_path, &help_output)?;
179
180    // 6. Generate system_prompt.txt
181    let system_prompt = generate_system_prompt(&tool_name, &persona, &help_output, &tool_type);
182    let prompt_path = output_dir.join("system_prompt.txt");
183    fs::write(&prompt_path, &system_prompt)?;
184
185    // 7. Summary output
186    println!();
187    println!("β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€");
188    println!("β”‚ βœ… Registered: {} ({})", tool_name, type_label);
189    if let Some(ref p) = persona {
190        println!("β”‚ 🎭 Persona: {}", p);
191    }
192    match &tool_type {
193        ToolType::Cli { bin_path } => {
194            println!("β”‚ πŸ“ Binary: {}", bin_path);
195        }
196        ToolType::Api { base_url, .. } => {
197            println!("β”‚ 🌐 Base URL: {}", base_url);
198        }
199        ToolType::Expert => {
200            println!("β”‚ πŸ’‘ Generic expertise persona");
201        }
202        ToolType::Kaiba { profile } => {
203            println!("β”‚ 🧠 Kaiba Profile: {}", profile);
204            println!("β”‚ πŸ’‘ Dynamic prompt from Kaiba API");
205        }
206    }
207    if !aliases.is_empty() {
208        println!("β”‚ 🏷️  Aliases: {}", aliases.join(", "));
209    }
210    println!("β”‚ πŸ“ {}", output_dir.display());
211    println!("└─────────────────────────────────────────");
212    println!();
213    if !aliases.is_empty() {
214        println!("πŸ’‘ Run: casting repl {} (or \"{}\")", aliases[0], tool_name);
215    } else {
216        println!("πŸ’‘ Run: casting repl \"{}\"", tool_name);
217    }
218
219    Ok(())
220}
221
222/// Handle the `list` command - list all configured tools
223pub fn handle_list() -> anyhow::Result<()> {
224    let home = std::env::var("HOME")?;
225    let tools_dir = PathBuf::from(home).join(".casting").join("tools");
226
227    if !tools_dir.exists() {
228        println!("πŸ“­ No tools configured yet.");
229        println!("\nπŸ’‘ Create your first tool with: casting make <TOOL> --persona <KEYWORD>");
230        return Ok(());
231    }
232
233    let migrator = setup_config_migrator()?;
234    let entries = fs::read_dir(&tools_dir)?;
235    let mut configs = Vec::new();
236
237    for entry in entries {
238        let entry = entry?;
239        let path = entry.path();
240
241        if path.is_dir() {
242            let config_path = path.join("config.json");
243            if config_path.exists() {
244                let config_str = fs::read_to_string(&config_path)?;
245
246                // Try to load with migration support
247                let config: ToolConfig = migrator.load_with_fallback("tool_config", &config_str)?;
248                configs.push(config);
249            }
250        }
251    }
252
253    if configs.is_empty() {
254        println!("πŸ“­ No tools configured yet.");
255        println!("\nπŸ’‘ Create your first tool with: casting make <TOOL> --persona <KEYWORD>");
256        return Ok(());
257    }
258
259    println!("πŸ“ Configured Tools:\n");
260
261    for config in configs {
262        println!("  {}", config.tool_name);
263        if let Some(persona) = &config.persona {
264            println!("    🎭 Persona: {}", persona);
265        } else {
266            println!("    🎭 Persona: (none)");
267        }
268
269        match &config.tool_type {
270            ToolType::Cli { bin_path } => {
271                println!("    πŸ’» Type: CLI");
272                println!("    πŸ“ Binary: {}", bin_path);
273            }
274            ToolType::Api { spec_url, base_url } => {
275                println!("    🌐 Type: API");
276                println!("    πŸ“ Base URL: {}", base_url);
277                println!("    πŸ“„ Spec: {}", spec_url);
278            }
279            ToolType::Expert => {
280                println!("    🧠 Type: Expert");
281            }
282            ToolType::Kaiba { profile } => {
283                println!("    🧠 Type: Kaiba");
284                println!("    πŸ“ Profile: {}", profile);
285            }
286        }
287
288        if !config.aliases.is_empty() {
289            println!("    🏷️  Aliases: {}", config.aliases.join(", "));
290        }
291
292        // ζ—₯ζ™‚γ‚’θ¦‹γ‚„γ™γγƒ•γ‚©γƒΌγƒžγƒƒγƒˆ
293        if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&config.created_at) {
294            println!("    πŸ“… Created: {}", dt.format("%Y-%m-%d %H:%M:%S"));
295        } else {
296            println!("    πŸ“… Created: {}", config.created_at);
297        }
298        println!();
299    }
300
301    println!("πŸ’‘ Use with: casting repl <TOOL> (or alias)");
302
303    Ok(())
304}
305
306/// Handle the `repl` command - start a REPL session with Claude Code
307pub fn handle_repl(tool: Option<String>) -> anyhow::Result<()> {
308    let mut cmd = Command::new("claude");
309
310    if let Some(ref input_name) = tool {
311        // Try to resolve alias to actual tool name
312        let tool_name = resolve_tool_name(input_name)?.unwrap_or_else(|| input_name.clone());
313
314        if &tool_name != input_name {
315            println!("πŸ”— Resolved alias '{}' β†’ '{}'", input_name, tool_name);
316        }
317        println!("🎬 Starting REPL for: {}", tool_name);
318
319        // Load config to check tool type
320        let config_path = get_tool_config_dir(&tool_name)?.join("config.json");
321
322        if !config_path.exists() {
323            anyhow::bail!(
324                "Tool '{}' is not configured. Run: casting make \"{}\"",
325                input_name,
326                input_name
327            );
328        }
329
330        let config_str = fs::read_to_string(&config_path)?;
331        let migrator = setup_config_migrator()?;
332        let config: ToolConfig = migrator.load_with_fallback("tool_config", &config_str)?;
333
334        // Get system prompt based on tool type
335        let system_prompt = match &config.tool_type {
336            ToolType::Kaiba { profile } => {
337                // Dynamic prompt from Kaiba API
338                println!(
339                    "🧠 Fetching dynamic prompt from Kaiba (profile: {})...",
340                    profile
341                );
342
343                let output = Command::new("kaiba")
344                    .args(["prompt", "-f", "casting", "-p", profile, "-m"])
345                    .output()
346                    .map_err(|e| {
347                        anyhow::anyhow!("Failed to run kaiba CLI: {}. Is it installed?", e)
348                    })?;
349
350                if !output.status.success() {
351                    let stderr = String::from_utf8_lossy(&output.stderr);
352                    anyhow::bail!("kaiba prompt failed: {}", stderr);
353                }
354
355                let prompt = String::from_utf8(output.stdout)
356                    .map_err(|e| anyhow::anyhow!("Invalid UTF-8 from kaiba: {}", e))?;
357
358                println!("βœ“ Loaded dynamic prompt ({} bytes)", prompt.len());
359                prompt
360            }
361            _ => {
362                // Static prompt from file
363                let prompt_path = get_tool_config_dir(&tool_name)?.join("system_prompt.txt");
364                let prompt = fs::read_to_string(&prompt_path)?;
365                println!("πŸ“ Loaded system prompt from: {}", prompt_path.display());
366                prompt
367            }
368        };
369
370        // Claude Code に --system-prompt を渑す
371        cmd.arg("--system-prompt").arg(&system_prompt);
372    } else {
373        println!("🎬 Starting Claude Code REPL...");
374    }
375
376    // Claude Code γ‚’θ΅·ε‹•
377    let status = cmd.status()?;
378
379    if !status.success() {
380        anyhow::bail!("Claude Code exited with error");
381    }
382
383    Ok(())
384}
385
386/// Handle the `completion` command - generate shell completion scripts
387pub fn handle_completion(shell: Shell) -> anyhow::Result<()> {
388    let mut cmd = Cli::command();
389    let name = cmd.get_name().to_string();
390    generate(shell, &mut cmd, name, &mut io::stdout());
391    Ok(())
392}