1use super::config::ToolDefinition;
7use std::collections::BTreeMap;
8use std::collections::HashMap;
9use std::sync::LazyLock;
10
11pub struct ToolRegistry {
13 user_tools: BTreeMap<String, ToolDefinition>,
15}
16
17impl ToolRegistry {
18 pub fn new(user_tools: BTreeMap<String, ToolDefinition>) -> Self {
20 Self { user_tools }
21 }
22
23 pub fn get(&self, tool_id: &str) -> Option<&ToolDefinition> {
27 self.user_tools.get(tool_id).or_else(|| BUILTIN_TOOLS.get(tool_id))
28 }
29
30 pub fn contains(&self, tool_id: &str) -> bool {
32 self.user_tools.contains_key(tool_id) || BUILTIN_TOOLS.contains_key(tool_id)
33 }
34
35 pub fn list_tools(&self) -> Vec<&str> {
37 let mut tools: Vec<&str> = self.user_tools.keys().map(std::string::String::as_str).collect();
38 for key in BUILTIN_TOOLS.keys() {
39 if !self.user_tools.contains_key(*key) {
40 tools.push(key);
41 }
42 }
43 tools.sort_unstable();
44 tools
45 }
46}
47
48impl Default for ToolRegistry {
49 fn default() -> Self {
50 Self::new(BTreeMap::new())
51 }
52}
53
54static BUILTIN_TOOLS: LazyLock<HashMap<&'static str, ToolDefinition>> = LazyLock::new(|| {
58 let mut m = HashMap::new();
59
60 m.insert(
62 "ruff:check",
63 ToolDefinition {
64 command: vec![
65 "ruff".to_string(),
66 "check".to_string(),
67 "--output-format=concise".to_string(),
68 "--stdin-filename=_.py".to_string(),
69 "-".to_string(),
70 ],
71 stdin: true,
72 stdout: true,
73 lint_args: vec![],
74 format_args: vec![],
75 },
76 );
77
78 m.insert(
79 "ruff:format",
80 ToolDefinition {
81 command: vec![
82 "ruff".to_string(),
83 "format".to_string(),
84 "--stdin-filename=_.py".to_string(),
85 "-".to_string(),
86 ],
87 stdin: true,
88 stdout: true,
89 lint_args: vec![],
90 format_args: vec![],
91 },
92 );
93
94 m.insert(
96 "black",
97 ToolDefinition {
98 command: vec!["black".to_string(), "--quiet".to_string(), "-".to_string()],
99 stdin: true,
100 stdout: true,
101 lint_args: vec!["--check".to_string()],
102 format_args: vec![],
103 },
104 );
105
106 m.insert(
108 "prettier",
109 ToolDefinition {
110 command: vec!["prettier".to_string(), "--stdin-filepath=_.js".to_string()],
111 stdin: true,
112 stdout: true,
113 lint_args: vec!["--check".to_string()],
114 format_args: vec![],
115 },
116 );
117
118 m.insert(
119 "prettier:json",
120 ToolDefinition {
121 command: vec!["prettier".to_string(), "--stdin-filepath=_.json".to_string()],
122 stdin: true,
123 stdout: true,
124 lint_args: vec!["--check".to_string()],
125 format_args: vec![],
126 },
127 );
128
129 m.insert(
130 "prettier:yaml",
131 ToolDefinition {
132 command: vec!["prettier".to_string(), "--stdin-filepath=_.yaml".to_string()],
133 stdin: true,
134 stdout: true,
135 lint_args: vec!["--check".to_string()],
136 format_args: vec![],
137 },
138 );
139
140 m.insert(
141 "prettier:html",
142 ToolDefinition {
143 command: vec!["prettier".to_string(), "--stdin-filepath=_.html".to_string()],
144 stdin: true,
145 stdout: true,
146 lint_args: vec!["--check".to_string()],
147 format_args: vec![],
148 },
149 );
150
151 m.insert(
152 "prettier:css",
153 ToolDefinition {
154 command: vec!["prettier".to_string(), "--stdin-filepath=_.css".to_string()],
155 stdin: true,
156 stdout: true,
157 lint_args: vec!["--check".to_string()],
158 format_args: vec![],
159 },
160 );
161
162 m.insert(
163 "prettier:markdown",
164 ToolDefinition {
165 command: vec!["prettier".to_string(), "--stdin-filepath=_.md".to_string()],
166 stdin: true,
167 stdout: true,
168 lint_args: vec!["--check".to_string()],
169 format_args: vec![],
170 },
171 );
172
173 m.insert(
175 "eslint",
176 ToolDefinition {
177 command: vec![
178 "eslint".to_string(),
179 "--stdin".to_string(),
180 "--stdin-filename=_.js".to_string(),
181 ],
182 stdin: true,
183 stdout: true,
184 lint_args: vec![],
185 format_args: vec!["--fix-dry-run".to_string()],
186 },
187 );
188
189 m.insert(
191 "shellcheck",
192 ToolDefinition {
193 command: vec!["shellcheck".to_string(), "-".to_string()],
194 stdin: true,
195 stdout: true,
196 lint_args: vec![],
197 format_args: vec![],
198 },
199 );
200
201 m.insert(
203 "shfmt",
204 ToolDefinition {
205 command: vec!["shfmt".to_string()],
206 stdin: true,
207 stdout: true,
208 lint_args: vec!["-d".to_string()], format_args: vec![],
210 },
211 );
212
213 m.insert(
215 "rustfmt",
216 ToolDefinition {
217 command: vec!["rustfmt".to_string()],
218 stdin: true,
219 stdout: true,
220 lint_args: vec!["--check".to_string()],
221 format_args: vec![],
222 },
223 );
224
225 m.insert(
227 "gofmt",
228 ToolDefinition {
229 command: vec!["gofmt".to_string()],
230 stdin: true,
231 stdout: true,
232 lint_args: vec!["-d".to_string()], format_args: vec![],
234 },
235 );
236
237 m.insert(
239 "goimports",
240 ToolDefinition {
241 command: vec!["goimports".to_string()],
242 stdin: true,
243 stdout: true,
244 lint_args: vec!["-d".to_string()],
245 format_args: vec![],
246 },
247 );
248
249 m.insert(
251 "clang-format",
252 ToolDefinition {
253 command: vec!["clang-format".to_string()],
254 stdin: true,
255 stdout: true,
256 lint_args: vec!["--dry-run".to_string(), "--Werror".to_string()],
257 format_args: vec![],
258 },
259 );
260
261 m.insert(
263 "sqlfluff:lint",
264 ToolDefinition {
265 command: vec!["sqlfluff".to_string(), "lint".to_string(), "-".to_string()],
266 stdin: true,
267 stdout: true,
268 lint_args: vec![],
269 format_args: vec![],
270 },
271 );
272
273 m.insert(
274 "sqlfluff:fix",
275 ToolDefinition {
276 command: vec!["sqlfluff".to_string(), "fix".to_string(), "-".to_string()],
277 stdin: true,
278 stdout: true,
279 lint_args: vec![],
280 format_args: vec![],
281 },
282 );
283
284 m.insert(
286 "jq",
287 ToolDefinition {
288 command: vec!["jq".to_string(), ".".to_string()],
289 stdin: true,
290 stdout: true,
291 lint_args: vec![],
292 format_args: vec![],
293 },
294 );
295
296 m.insert(
298 "yamlfmt",
299 ToolDefinition {
300 command: vec!["yamlfmt".to_string()],
301 stdin: true,
302 stdout: true,
303 lint_args: vec!["-lint".to_string(), "-".to_string()],
304 format_args: vec!["-".to_string()],
305 },
306 );
307
308 m.insert(
310 "taplo",
311 ToolDefinition {
312 command: vec!["taplo".to_string(), "fmt".to_string(), "-".to_string()],
313 stdin: true,
314 stdout: true,
315 lint_args: vec!["--check".to_string()],
316 format_args: vec![],
317 },
318 );
319
320 m.insert(
322 "terraform-fmt",
323 ToolDefinition {
324 command: vec!["terraform".to_string(), "fmt".to_string(), "-".to_string()],
325 stdin: true,
326 stdout: true,
327 lint_args: vec!["-check".to_string()],
328 format_args: vec![],
329 },
330 );
331
332 m.insert(
334 "nixfmt",
335 ToolDefinition {
336 command: vec!["nixfmt".to_string()],
337 stdin: true,
338 stdout: true,
339 lint_args: vec!["--check".to_string()],
340 format_args: vec![],
341 },
342 );
343
344 m.insert(
346 "stylua",
347 ToolDefinition {
348 command: vec!["stylua".to_string(), "-".to_string()],
349 stdin: true,
350 stdout: true,
351 lint_args: vec!["--check".to_string()],
352 format_args: vec![],
353 },
354 );
355
356 m.insert(
358 "rubocop",
359 ToolDefinition {
360 command: vec!["rubocop".to_string(), "--stdin".to_string(), "_.rb".to_string()],
361 stdin: true,
362 stdout: true,
363 lint_args: vec![],
364 format_args: vec!["--autocorrect".to_string()],
365 },
366 );
367
368 m.insert(
370 "ormolu",
371 ToolDefinition {
372 command: vec!["ormolu".to_string()],
373 stdin: true,
374 stdout: true,
375 lint_args: vec!["--check-idempotence".to_string()],
376 format_args: vec![],
377 },
378 );
379
380 m.insert(
382 "elm-format",
383 ToolDefinition {
384 command: vec!["elm-format".to_string(), "--stdin".to_string()],
385 stdin: true,
386 stdout: true,
387 lint_args: vec!["--validate".to_string()],
388 format_args: vec![],
389 },
390 );
391
392 m.insert(
394 "zig-fmt",
395 ToolDefinition {
396 command: vec!["zig".to_string(), "fmt".to_string(), "--stdin".to_string()],
397 stdin: true,
398 stdout: true,
399 lint_args: vec!["--check".to_string()],
400 format_args: vec![],
401 },
402 );
403
404 m.insert(
406 "dart-format",
407 ToolDefinition {
408 command: vec!["dart".to_string(), "format".to_string()],
409 stdin: true,
410 stdout: true,
411 lint_args: vec!["--output=none".to_string(), "--set-exit-if-changed".to_string()],
412 format_args: vec![],
413 },
414 );
415
416 m.insert(
418 "swift-format",
419 ToolDefinition {
420 command: vec!["swift-format".to_string()],
421 stdin: true,
422 stdout: true,
423 lint_args: vec!["lint".to_string()],
424 format_args: vec![],
425 },
426 );
427
428 m.insert(
430 "ktfmt",
431 ToolDefinition {
432 command: vec!["ktfmt".to_string(), "--stdin".to_string()],
433 stdin: true,
434 stdout: true,
435 lint_args: vec!["--dry-run".to_string()],
436 format_args: vec![],
437 },
438 );
439
440 m.insert(
442 "djlint",
443 ToolDefinition {
444 command: vec!["djlint".to_string(), "-".to_string()],
445 stdin: true,
446 stdout: true,
447 lint_args: vec![],
448 format_args: vec!["--reformat".to_string()],
449 },
450 );
451
452 m.insert(
453 "djlint:lint",
454 ToolDefinition {
455 command: vec!["djlint".to_string(), "-".to_string()],
456 stdin: true,
457 stdout: true,
458 lint_args: vec![],
459 format_args: vec![],
460 },
461 );
462
463 m.insert(
464 "djlint:reformat",
465 ToolDefinition {
466 command: vec!["djlint".to_string(), "-".to_string(), "--reformat".to_string()],
467 stdin: true,
468 stdout: true,
469 lint_args: vec![],
470 format_args: vec![],
471 },
472 );
473
474 m.insert(
476 "beautysh",
477 ToolDefinition {
478 command: vec!["beautysh".to_string(), "-".to_string()],
479 stdin: true,
480 stdout: true,
481 lint_args: vec!["--check".to_string()],
482 format_args: vec![],
483 },
484 );
485
486 m.insert(
488 "tombi",
489 ToolDefinition {
490 command: vec!["tombi".to_string(), "lint".to_string(), "-".to_string()],
491 stdin: true,
492 stdout: true,
493 lint_args: vec![],
494 format_args: vec![],
495 },
496 );
497
498 m.insert(
499 "tombi:format",
500 ToolDefinition {
501 command: vec!["tombi".to_string(), "format".to_string(), "-".to_string()],
502 stdin: true,
503 stdout: true,
504 lint_args: vec![],
505 format_args: vec![],
506 },
507 );
508
509 m.insert(
510 "tombi:lint",
511 ToolDefinition {
512 command: vec!["tombi".to_string(), "lint".to_string(), "-".to_string()],
513 stdin: true,
514 stdout: true,
515 lint_args: vec![],
516 format_args: vec![],
517 },
518 );
519
520 m.insert(
522 "oxfmt",
523 ToolDefinition {
524 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.js".to_string()],
525 stdin: true,
526 stdout: true,
527 lint_args: vec!["--check".to_string()],
528 format_args: vec![],
529 },
530 );
531
532 m.insert(
533 "oxfmt:js",
534 ToolDefinition {
535 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.js".to_string()],
536 stdin: true,
537 stdout: true,
538 lint_args: vec!["--check".to_string()],
539 format_args: vec![],
540 },
541 );
542
543 m.insert(
544 "oxfmt:ts",
545 ToolDefinition {
546 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.ts".to_string()],
547 stdin: true,
548 stdout: true,
549 lint_args: vec!["--check".to_string()],
550 format_args: vec![],
551 },
552 );
553
554 m.insert(
555 "oxfmt:jsx",
556 ToolDefinition {
557 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.jsx".to_string()],
558 stdin: true,
559 stdout: true,
560 lint_args: vec!["--check".to_string()],
561 format_args: vec![],
562 },
563 );
564
565 m.insert(
566 "oxfmt:tsx",
567 ToolDefinition {
568 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.tsx".to_string()],
569 stdin: true,
570 stdout: true,
571 lint_args: vec!["--check".to_string()],
572 format_args: vec![],
573 },
574 );
575
576 m.insert(
577 "oxfmt:json",
578 ToolDefinition {
579 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.json".to_string()],
580 stdin: true,
581 stdout: true,
582 lint_args: vec!["--check".to_string()],
583 format_args: vec![],
584 },
585 );
586
587 m.insert(
588 "oxfmt:css",
589 ToolDefinition {
590 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.css".to_string()],
591 stdin: true,
592 stdout: true,
593 lint_args: vec!["--check".to_string()],
594 format_args: vec![],
595 },
596 );
597
598 m
599});
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604
605 #[test]
606 fn test_get_builtin_tool() {
607 let registry = ToolRegistry::default();
608
609 let tool = registry.get("ruff:check").expect("Should find ruff:check");
610 assert!(tool.command.contains(&"ruff".to_string()));
611 assert!(tool.stdin);
612 assert!(tool.stdout);
613
614 let tool = registry.get("shellcheck").expect("Should find shellcheck");
615 assert!(tool.command.contains(&"shellcheck".to_string()));
616 }
617
618 #[test]
619 fn test_builtin_yamlfmt_lint_command_validates_stdin() {
620 let registry = ToolRegistry::default();
621
622 let tool = registry.get("yamlfmt").expect("Should find yamlfmt");
623 let mut argv = tool.command.clone();
624 argv.extend(tool.lint_args.clone());
625
626 assert_eq!(argv, vec!["yamlfmt", "-lint", "-"]);
627 }
628
629 #[test]
630 fn test_get_user_tool_overrides_builtin() {
631 let mut user_tools = BTreeMap::new();
632 user_tools.insert(
633 "ruff:check".to_string(),
634 ToolDefinition {
635 command: vec!["custom-ruff".to_string()],
636 stdin: false,
637 stdout: false,
638 lint_args: vec![],
639 format_args: vec![],
640 },
641 );
642
643 let registry = ToolRegistry::new(user_tools);
644
645 let tool = registry.get("ruff:check").expect("Should find ruff:check");
646 assert_eq!(tool.command, vec!["custom-ruff"]);
647 assert!(!tool.stdin); }
649
650 #[test]
651 fn test_contains() {
652 let registry = ToolRegistry::default();
653
654 assert!(registry.contains("ruff:check"));
655 assert!(registry.contains("prettier"));
656 assert!(registry.contains("shellcheck"));
657 assert!(!registry.contains("nonexistent-tool"));
658 }
659
660 #[test]
661 fn test_list_tools() {
662 let registry = ToolRegistry::default();
663 let tools = registry.list_tools();
664
665 assert!(tools.contains(&"ruff:check"));
666 assert!(tools.contains(&"ruff:format"));
667 assert!(tools.contains(&"prettier"));
668 assert!(tools.contains(&"shellcheck"));
669 assert!(tools.contains(&"shfmt"));
670 assert!(tools.contains(&"rustfmt"));
671 assert!(tools.contains(&"gofmt"));
672 }
673
674 #[test]
675 fn test_user_tools_in_list() {
676 let mut user_tools = BTreeMap::new();
677 user_tools.insert("my-custom-tool".to_string(), ToolDefinition::default());
678
679 let registry = ToolRegistry::new(user_tools);
680 let tools = registry.list_tools();
681
682 assert!(tools.contains(&"my-custom-tool"));
683 assert!(tools.contains(&"ruff:check")); }
685
686 #[test]
687 fn test_new_builtin_tools() {
688 let registry = ToolRegistry::default();
689
690 let tool = registry.get("djlint").expect("Should find djlint");
692 assert!(tool.command.contains(&"djlint".to_string()));
693 assert!(tool.stdin);
694
695 let tool = registry.get("beautysh").expect("Should find beautysh");
697 assert!(tool.command.contains(&"beautysh".to_string()));
698 assert!(tool.stdin);
699
700 let tool = registry.get("tombi").expect("Should find tombi");
702 assert!(tool.command.contains(&"tombi".to_string()));
703 assert!(tool.stdin);
704
705 let tool = registry.get("tombi:lint").expect("Should find tombi:lint");
706 assert!(tool.command.contains(&"lint".to_string()));
707
708 let tool = registry.get("tombi:format").expect("Should find tombi:format");
709 assert!(
710 tool.command.contains(&"format".to_string()),
711 "tombi:format should use 'format' subcommand, got: {:?}",
712 tool.command
713 );
714
715 let tool = registry.get("oxfmt").expect("Should find oxfmt");
717 assert!(tool.command.contains(&"oxfmt".to_string()));
718 assert!(tool.stdin);
719
720 let tool = registry.get("oxfmt:ts").expect("Should find oxfmt:ts");
721 assert!(tool.command.iter().any(|s| s.contains("_.ts")));
722 }
723
724 #[test]
732 fn test_bare_tombi_resolves_to_lint_not_format() {
733 let registry = ToolRegistry::default();
734
735 let bare = registry.get("tombi").expect("Should find bare tombi");
736 let format = registry.get("tombi:format").expect("Should find tombi:format");
737
738 assert!(
740 bare.command.contains(&"lint".to_string()),
741 "Bare 'tombi' uses lint subcommand: {:?}",
742 bare.command
743 );
744
745 assert!(
747 format.command.contains(&"format".to_string()),
748 "tombi:format uses format subcommand: {:?}",
749 format.command
750 );
751
752 assert_ne!(
754 bare.command, format.command,
755 "Bare 'tombi' and 'tombi:format' should have different commands (this is the root cause of #527)"
756 );
757 }
758
759 #[test]
762 fn test_tools_with_lint_format_variants_are_distinct() {
763 let registry = ToolRegistry::default();
764
765 let ruff_check = registry.get("ruff:check").expect("ruff:check");
767 let ruff_format = registry.get("ruff:format").expect("ruff:format");
768 assert_ne!(
769 ruff_check.command, ruff_format.command,
770 "ruff:check and ruff:format should be distinct"
771 );
772
773 let tombi_lint = registry.get("tombi:lint").expect("tombi:lint");
775 let tombi_format = registry.get("tombi:format").expect("tombi:format");
776 assert_ne!(
777 tombi_lint.command, tombi_format.command,
778 "tombi:lint and tombi:format should be distinct"
779 );
780 }
781}