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