1use super::native_search::{binary_exists, run_cmd};
4use super::Tool;
5use async_trait::async_trait;
6use serde_json::{json, Value};
7use std::path::PathBuf;
8
9pub struct MiseTool {
12 workspace_root: PathBuf,
13}
14
15impl MiseTool {
16 pub fn new(workspace_root: PathBuf) -> Self {
17 Self { workspace_root }
18 }
19
20 fn resolve_mise_bin() -> crate::Result<String> {
21 if binary_exists("mise") {
22 Ok("mise".to_string())
23 } else {
24 let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
25 let local = format!("{}/.local/bin/mise", home);
26 if std::path::Path::new(&local).exists() {
27 Ok(local)
28 } else {
29 Err(crate::PawanError::Tool(
30 "mise not found. Install: curl https://mise.run | sh".into(),
31 ))
32 }
33 }
34 }
35
36 fn parse_mise_action(args: &Value) -> crate::Result<&str> {
37 args["action"]
38 .as_str()
39 .ok_or_else(|| crate::PawanError::Tool("action required".into()))
40 }
41
42 fn build_mise_command(action: &str, args: &Value, global: bool) -> crate::Result<Vec<String>> {
43 match action {
44 "install" => Self::mise_install(args),
45 "uninstall" => Self::mise_uninstall(args),
46 "upgrade" => Self::mise_upgrade(args),
47 "list" => Ok(Self::mise_list()),
48 "search" => Self::mise_search(args),
49 "use" => Self::mise_use(args, global),
50 "outdated" => Self::mise_outdated(args),
51 "prune" => Self::mise_prune(args),
52 "exec" => Self::mise_exec(args),
53 "run" => Self::mise_run(args),
54 "watch" => Self::mise_watch(args),
55 "tasks" => Ok(Self::mise_tasks()),
56 "env" => Ok(Self::mise_env()),
57 "doctor" => Ok(Self::mise_doctor()),
58 "self-update" => Ok(Self::mise_self_update()),
59 "trust" => Self::mise_trust(args),
60 _ => Err(crate::PawanError::Tool(format!(
61 "Unknown action: {action}. See tool description for available actions."
62 ))),
63 }
64 }
65
66 fn mise_install(args: &Value) -> crate::Result<Vec<String>> {
67 let tool = args["tool"]
68 .as_str()
69 .ok_or_else(|| crate::PawanError::Tool("tool required for install".into()))?;
70 Ok(vec!["install".into(), tool.into(), "-y".into()])
71 }
72
73 fn mise_uninstall(args: &Value) -> crate::Result<Vec<String>> {
74 let tool = args["tool"]
75 .as_str()
76 .ok_or_else(|| crate::PawanError::Tool("tool required for uninstall".into()))?;
77 Ok(vec!["uninstall".into(), tool.into()])
78 }
79
80 fn mise_upgrade(args: &Value) -> crate::Result<Vec<String>> {
81 let mut v = vec!["upgrade".into()];
82 if let Some(tool) = args["tool"].as_str() {
83 v.push(tool.into());
84 }
85 Ok(v)
86 }
87
88 fn mise_list() -> Vec<String> {
89 vec!["ls".into()]
90 }
91
92 fn mise_search(args: &Value) -> crate::Result<Vec<String>> {
93 let tool = args["tool"]
94 .as_str()
95 .ok_or_else(|| crate::PawanError::Tool("tool required for search".into()))?;
96 Ok(vec!["registry".into(), tool.into()])
97 }
98
99 fn mise_use(args: &Value, global: bool) -> crate::Result<Vec<String>> {
100 let tool = args["tool"]
101 .as_str()
102 .ok_or_else(|| crate::PawanError::Tool("tool required for use".into()))?;
103 let mut v = vec!["use".into()];
104 if global {
105 v.push("--global".into());
106 }
107 v.push(tool.into());
108 Ok(v)
109 }
110
111 fn mise_outdated(args: &Value) -> crate::Result<Vec<String>> {
112 let mut v = vec!["outdated".into()];
113 if let Some(tool) = args["tool"].as_str() {
114 v.push(tool.into());
115 }
116 Ok(v)
117 }
118
119 fn mise_prune(args: &Value) -> crate::Result<Vec<String>> {
120 let mut v = vec!["prune".into(), "-y".into()];
121 if let Some(tool) = args["tool"].as_str() {
122 v.push(tool.into());
123 }
124 Ok(v)
125 }
126
127 fn mise_exec(args: &Value) -> crate::Result<Vec<String>> {
128 let tool = args["tool"]
129 .as_str()
130 .ok_or_else(|| crate::PawanError::Tool("tool required for exec".into()))?;
131 let extra = args["args"].as_str().unwrap_or("");
132 let mut v = vec!["exec".into(), tool.into(), "--".into()];
133 if !extra.is_empty() {
134 v.extend(extra.split_whitespace().map(|s| s.to_string()));
135 }
136 Ok(v)
137 }
138
139 fn mise_run(args: &Value) -> crate::Result<Vec<String>> {
140 let task = args["task"]
141 .as_str()
142 .ok_or_else(|| crate::PawanError::Tool("task required for run".into()))?;
143 let mut v = vec!["run".into(), task.into()];
144 if let Some(extra) = args["args"].as_str() {
145 v.push("--".into());
146 v.extend(extra.split_whitespace().map(|s| s.to_string()));
147 }
148 Ok(v)
149 }
150
151 fn mise_watch(args: &Value) -> crate::Result<Vec<String>> {
152 let task = args["task"]
153 .as_str()
154 .ok_or_else(|| crate::PawanError::Tool("task required for watch".into()))?;
155 let mut v = vec!["watch".into(), task.into()];
156 if let Some(extra) = args["args"].as_str() {
157 v.push("--".into());
158 v.extend(extra.split_whitespace().map(|s| s.to_string()));
159 }
160 Ok(v)
161 }
162
163 fn mise_tasks() -> Vec<String> {
164 vec!["tasks".into(), "ls".into()]
165 }
166
167 fn mise_env() -> Vec<String> {
168 vec!["env".into()]
169 }
170
171 fn mise_doctor() -> Vec<String> {
172 vec!["doctor".into()]
173 }
174
175 fn mise_self_update() -> Vec<String> {
176 vec!["self-update".into(), "-y".into()]
177 }
178
179 fn mise_trust(args: &Value) -> crate::Result<Vec<String>> {
180 let mut v = vec!["trust".into()];
181 if let Some(extra) = args["args"].as_str() {
182 v.push(extra.into());
183 }
184 Ok(v)
185 }
186}
187
188#[async_trait]
189impl Tool for MiseTool {
190 fn name(&self) -> &str {
191 "mise"
192 }
193
194 fn description(&self) -> &str {
195 "mise — polyglot tool manager, environment manager, and task runner. Replaces asdf, nvm, \
196 pyenv, direnv, make, and npm scripts. Three powers: (1) install/manage any dev tool or \
197 language runtime, (2) manage per-project env vars, (3) run/watch project tasks. \
198 Pawan should use this to self-install any missing CLI tool (erd, ast-grep, fd, rg, etc)."
199 }
200
201 fn parameters_schema(&self) -> Value {
202 json!({
203 "type": "object",
204 "properties": {
205 "action": {
206 "type": "string",
207 "enum": [
208 "install", "uninstall", "upgrade", "list", "use", "search",
209 "exec", "run", "tasks", "env", "outdated", "prune",
210 "doctor", "self-update", "trust", "watch"
211 ],
212 "description": "Tool management: install, uninstall, upgrade, list, use, search, outdated, prune. \
213 Execution: exec (run with tool env), run (run a task), watch (rerun task on file change). \
214 Environment: env (show/set env vars). Tasks: tasks (list/manage tasks). \
215 Maintenance: doctor, self-update, trust, prune."
216 },
217 "tool": {
218 "type": "string",
219 "description": "Tool name with optional version. Examples: 'erdtree', 'node@22', 'python@3.12', \
220 'ast-grep', 'ripgrep', 'fd', 'sd', 'bat', 'delta', 'jq', 'yq', 'go', 'bun', 'deno'"
221 },
222 "task": {
223 "type": "string",
224 "description": "Task name for run/watch/tasks actions (defined in mise.toml or .mise/tasks/)"
225 },
226 "args": {
227 "type": "string",
228 "description": "Additional arguments (space-separated). For exec: command to run. For run: task args."
229 },
230 "global": {
231 "type": "boolean",
232 "description": "Apply globally (--global flag) instead of project-local. Default: false."
233 }
234 },
235 "required": ["action"]
236 })
237 }
238
239 fn thulp_definition(&self) -> thulp_core::ToolDefinition {
240 use thulp_core::{Parameter, ParameterType};
241 thulp_core::ToolDefinition::builder(self.name())
242 .description(self.description())
243 .parameter(
244 Parameter::builder("action")
245 .param_type(ParameterType::String)
246 .required(true)
247 .description("Tool management: install, uninstall, upgrade, list, use, search, outdated, prune. \
248 Execution: exec (run with tool env), run (run a task), watch (rerun task on file change). \
249 Environment: env (show/set env vars). Tasks: tasks (list/manage tasks). \
250 Maintenance: doctor, self-update, trust, prune.")
251 .build(),
252 )
253 .parameter(
254 Parameter::builder("tool")
255 .param_type(ParameterType::String)
256 .required(false)
257 .description("Tool name with optional version. Examples: 'erdtree', 'node@22', 'python@3.12', \
258 'ast-grep', 'ripgrep', 'fd', 'sd', 'bat', 'delta', 'jq', 'yq', 'go', 'bun', 'deno'")
259 .build(),
260 )
261 .parameter(
262 Parameter::builder("task")
263 .param_type(ParameterType::String)
264 .required(false)
265 .description("Task name for run/watch/tasks actions (defined in mise.toml or .mise/tasks/)")
266 .build(),
267 )
268 .parameter(
269 Parameter::builder("args")
270 .param_type(ParameterType::String)
271 .required(false)
272 .description("Additional arguments (space-separated). For exec: command to run. For run: task args.")
273 .build(),
274 )
275 .parameter(
276 Parameter::builder("global")
277 .param_type(ParameterType::Boolean)
278 .required(false)
279 .description("Apply globally (--global flag) instead of project-local. Default: false.")
280 .build(),
281 )
282 .build()
283 }
284
285 async fn execute(&self, args: Value) -> crate::Result<Value> {
286 let mise_bin = Self::resolve_mise_bin()?;
287 let action = Self::parse_mise_action(&args)?;
288 let global = args["global"].as_bool().unwrap_or(false);
289 let cmd_args = Self::build_mise_command(action, &args, global)?;
290
291 let cmd_refs: Vec<&str> = cmd_args.iter().map(|s| s.as_str()).collect();
292 let (stdout, stderr, success) = run_cmd(&mise_bin, &cmd_refs, &self.workspace_root)
293 .await
294 .map_err(crate::PawanError::Tool)?;
295
296 Ok(json!({
297 "success": success,
298 "action": action,
299 "output": stdout,
300 "stderr": if stderr.is_empty() { None::<String> } else { Some(stderr) }
301 }))
302 }
303}
304
305pub struct ZoxideTool {
308 workspace_root: PathBuf,
309}
310
311impl ZoxideTool {
312 pub fn new(workspace_root: PathBuf) -> Self {
313 Self { workspace_root }
314 }
315}
316
317#[async_trait]
318impl Tool for ZoxideTool {
319 fn name(&self) -> &str {
320 "z"
321 }
322
323 fn description(&self) -> &str {
324 "zoxide — smart directory jumper. Learns from your cd history. \
325 Use 'query' to find a directory by fuzzy match (e.g. 'myproject' finds ~/projects/myproject). \
326 Use 'add' to teach it a new path. Use 'list' to see known paths."
327 }
328
329 fn parameters_schema(&self) -> Value {
330 json!({
331 "type": "object",
332 "properties": {
333 "action": { "type": "string", "description": "query, add, or list" },
334 "path": { "type": "string", "description": "Path or search term" }
335 },
336 "required": ["action"]
337 })
338 }
339
340 fn thulp_definition(&self) -> thulp_core::ToolDefinition {
341 use thulp_core::{Parameter, ParameterType};
342 thulp_core::ToolDefinition::builder(self.name())
343 .description(self.description())
344 .parameter(
345 Parameter::builder("action")
346 .param_type(ParameterType::String)
347 .required(true)
348 .description("query, add, or list")
349 .build(),
350 )
351 .parameter(
352 Parameter::builder("path")
353 .param_type(ParameterType::String)
354 .required(false)
355 .description("Path or search term")
356 .build(),
357 )
358 .build()
359 }
360
361 async fn execute(&self, args: Value) -> crate::Result<Value> {
362 let action = args["action"]
363 .as_str()
364 .ok_or_else(|| crate::PawanError::Tool("action required (query/add/list)".into()))?;
365
366 let cmd_args: Vec<String> = match action {
367 "query" => {
368 let path = args["path"].as_str().ok_or_else(|| {
369 crate::PawanError::Tool("path/search term required for query".into())
370 })?;
371 vec!["query".into(), path.into()]
372 }
373 "add" => {
374 let path = args["path"]
375 .as_str()
376 .ok_or_else(|| crate::PawanError::Tool("path required for add".into()))?;
377 vec!["add".into(), path.into()]
378 }
379 "list" => vec!["query".into(), "--list".into()],
380 _ => {
381 return Err(crate::PawanError::Tool(format!(
382 "Unknown action: {}. Use query/add/list",
383 action
384 )))
385 }
386 };
387
388 let cmd_refs: Vec<&str> = cmd_args.iter().map(|s| s.as_str()).collect();
389 let (stdout, stderr, success) = run_cmd("zoxide", &cmd_refs, &self.workspace_root)
390 .await
391 .map_err(crate::PawanError::Tool)?;
392
393 Ok(json!({
394 "success": success,
395 "result": stdout.trim(),
396 "stderr": if stderr.is_empty() { None::<String> } else { Some(stderr) }
397 }))
398 }
399}
400
401#[cfg(test)]
404mod tests {
405 use super::*;
406 use tempfile::TempDir;
407
408 #[tokio::test]
409 async fn test_mise_tool_schema() {
410 let tmp = TempDir::new().unwrap();
411 let tool = MiseTool::new(tmp.path().to_path_buf());
412 assert_eq!(tool.name(), "mise");
413 let schema = tool.parameters_schema();
414 assert!(schema["properties"]["action"].is_object());
415 assert!(schema["properties"]["task"].is_object());
416 }
417
418 #[tokio::test]
419 async fn test_zoxide_tool_basics() {
420 let tmp = TempDir::new().unwrap();
421 let tool = ZoxideTool::new(tmp.path().into());
422 assert_eq!(tool.name(), "z");
423 assert!(!tool.description().is_empty());
424 let schema = tool.parameters_schema();
425 assert!(schema["required"]
426 .as_array()
427 .unwrap()
428 .contains(&serde_json::json!("action")));
429 }
430
431 #[tokio::test]
432 async fn test_mise_tool_unknown_action_returns_error() {
433 let tmp = TempDir::new().unwrap();
434 let tool = MiseTool::new(tmp.path().into());
435 let result = tool
436 .execute(serde_json::json!({ "action": "totally_not_a_real_verb" }))
437 .await;
438 let err = result.expect_err("unknown mise action must error");
439 let msg = format!("{}", err);
440 assert!(
441 (msg.contains("Unknown action") && msg.contains("totally_not_a_real_verb"))
442 || msg.contains("mise not found"),
443 "error must name the unknown action (or report mise missing), got: {}",
444 msg
445 );
446 }
447
448 #[tokio::test]
449 async fn test_mise_tool_install_without_tool_returns_error() {
450 let tmp = TempDir::new().unwrap();
451 let tool = MiseTool::new(tmp.path().into());
452 let result = tool
453 .execute(serde_json::json!({ "action": "install" }))
454 .await;
455 let err = result.expect_err("mise install without tool must error");
456 let msg = format!("{}", err);
457 assert!(
458 msg.contains("tool required for install") || msg.contains("mise not found"),
459 "error should mention 'tool required for install' (or report mise missing), got: {}",
460 msg
461 );
462 }
463}