Coverage for lintro / cli_utils / commands / list_tools.py: 47%

131 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""List tools command implementation for lintro CLI. 

2 

3This module provides the core logic for the 'list_tools' command. 

4""" 

5 

6import json as json_lib 

7 

8import click 

9from rich.console import Console 

10from rich.panel import Panel 

11from rich.table import Table 

12 

13from lintro.enums.action import Action 

14from lintro.plugins.base import BaseToolPlugin 

15from lintro.tools import tool_manager 

16from lintro.utils.console import get_tool_emoji 

17from lintro.utils.unified_config import get_tool_priority, is_tool_injectable 

18 

19 

20def _resolve_conflicts( 

21 plugin: BaseToolPlugin, 

22 available_tools: dict[str, BaseToolPlugin], 

23) -> list[str]: 

24 """Resolve conflict names for a tool. 

25 

26 Args: 

27 plugin: The plugin instance. 

28 available_tools: Dictionary of available tools. 

29 

30 Returns: 

31 List of conflict tool names. 

32 """ 

33 conflict_names: list[str] = [] 

34 conflicts_with = plugin.definition.conflicts_with 

35 if conflicts_with: 

36 for conflict in conflicts_with: 

37 conflict_lower = conflict.lower() 

38 if conflict_lower in available_tools: 

39 conflict_names.append(conflict_lower) 

40 return conflict_names 

41 

42 

43@click.command("list-tools") 

44@click.option( 

45 "--output", 

46 type=click.Path(), 

47 help="Output file path for writing results", 

48) 

49@click.option( 

50 "--show-conflicts", 

51 is_flag=True, 

52 help="Show potential conflicts between tools", 

53) 

54@click.option( 

55 "--json", 

56 "json_output", 

57 is_flag=True, 

58 help="Output tool list as JSON", 

59) 

60@click.option( 

61 "--verbose", 

62 "-v", 

63 is_flag=True, 

64 help="Show verbose output including file extensions and patterns", 

65) 

66def list_tools_command( 

67 output: str | None, 

68 show_conflicts: bool, 

69 json_output: bool, 

70 verbose: bool, 

71) -> None: 

72 """List all available tools and their configurations. 

73 

74 Args: 

75 output: Path to output file for writing results. 

76 show_conflicts: Whether to show potential conflicts between tools. 

77 json_output: Output tool list as JSON. 

78 verbose: Show verbose output including file extensions and patterns. 

79 """ 

80 list_tools( 

81 output=output, 

82 show_conflicts=show_conflicts, 

83 json_output=json_output, 

84 verbose=verbose, 

85 ) 

86 

87 

88def list_tools( 

89 output: str | None, 

90 show_conflicts: bool, 

91 json_output: bool = False, 

92 verbose: bool = False, 

93) -> None: 

94 """List all available tools. 

95 

96 Args: 

97 output: Output file path. 

98 show_conflicts: Whether to show potential conflicts between tools. 

99 json_output: Output tool list as JSON. 

100 verbose: Show verbose output including file extensions and patterns. 

101 """ 

102 available_tools = tool_manager.get_all_tools() 

103 check_tools = tool_manager.get_check_tools() 

104 fix_tools = tool_manager.get_fix_tools() 

105 

106 # JSON output mode 

107 if json_output: 

108 tools_data: dict[str, dict[str, object]] = {} 

109 for tool_name, plugin in available_tools.items(): 

110 capabilities: list[str] = [] 

111 if tool_name in check_tools: 

112 capabilities.append("check") 

113 if tool_name in fix_tools: 

114 capabilities.append("fix") 

115 

116 tool_info: dict[str, object] = { 

117 "description": plugin.definition.description, 

118 "capabilities": capabilities, 

119 "priority": get_tool_priority(tool_name), 

120 "syncable": is_tool_injectable(tool_name), 

121 } 

122 

123 # Only include file_patterns in verbose mode (consistent with table output) 

124 if verbose: 

125 tool_info["file_patterns"] = plugin.definition.file_patterns 

126 

127 if show_conflicts: 

128 conflict_names = _resolve_conflicts( 

129 plugin=plugin, 

130 available_tools=available_tools, 

131 ) 

132 tool_info["conflicts_with"] = conflict_names 

133 

134 tools_data[tool_name] = tool_info 

135 

136 click.echo(json_lib.dumps(tools_data, indent=2)) 

137 return 

138 

139 console = Console() 

140 

141 # Header panel 

142 console.print( 

143 Panel.fit( 

144 "[bold cyan]🔧 Available Tools[/bold cyan]", 

145 border_style="cyan", 

146 ), 

147 ) 

148 console.print() 

149 

150 # Main tools table 

151 table = Table(title="Tool Details") 

152 table.add_column("Tool", style="cyan", no_wrap=True) 

153 table.add_column("Description", style="white", max_width=40) 

154 table.add_column("Capabilities", style="green") 

155 table.add_column("Priority", justify="center", style="yellow") 

156 table.add_column("Type", style="magenta") 

157 

158 if verbose: 

159 table.add_column("Extensions", style="dim", max_width=30) 

160 

161 if show_conflicts: 

162 table.add_column("Conflicts", style="red") 

163 

164 for tool_name, plugin in available_tools.items(): 

165 tool_description = plugin.definition.description 

166 emoji = get_tool_emoji(tool_name) 

167 

168 # Capabilities 

169 tool_capabilities: list[str] = [] 

170 if tool_name in check_tools: 

171 tool_capabilities.append("check") 

172 if tool_name in fix_tools: 

173 tool_capabilities.append("fix") 

174 caps_display = ", ".join(tool_capabilities) if tool_capabilities else "-" 

175 

176 # Priority and type 

177 priority = get_tool_priority(tool_name) 

178 injectable = is_tool_injectable(tool_name) 

179 tool_type = "Syncable" if injectable else "Native only" 

180 

181 row = [ 

182 f"{emoji} {tool_name}", 

183 tool_description, 

184 caps_display, 

185 str(priority), 

186 tool_type, 

187 ] 

188 

189 # File patterns (verbose mode) 

190 if verbose: 

191 patterns = plugin.definition.file_patterns or [] 

192 pat_display = ", ".join(patterns[:5]) 

193 if len(patterns) > 5: 

194 pat_display += f" (+{len(patterns) - 5})" 

195 row.append(pat_display if patterns else "-") 

196 

197 # Conflicts 

198 if show_conflicts: 

199 conflict_names = _resolve_conflicts( 

200 plugin=plugin, 

201 available_tools=available_tools, 

202 ) 

203 row.append(", ".join(conflict_names) if conflict_names else "-") 

204 

205 table.add_row(*row) 

206 

207 console.print(table) 

208 console.print() 

209 

210 # Summary table 

211 summary_table = Table( 

212 title="Summary", 

213 show_header=False, 

214 box=None, 

215 ) 

216 summary_table.add_column("Metric", style="cyan", width=20) 

217 summary_table.add_column("Count", style="yellow", justify="right") 

218 

219 summary_table.add_row("📊 Total tools", str(len(available_tools))) 

220 summary_table.add_row("🔍 Check tools", str(len(check_tools))) 

221 summary_table.add_row("🔧 Fix tools", str(len(fix_tools))) 

222 

223 console.print(summary_table) 

224 

225 # Write to file if specified 

226 if output: 

227 try: 

228 # For file output, use plain text format 

229 output_lines = _generate_plain_text_output( 

230 available_tools=available_tools, 

231 check_tools=check_tools, 

232 fix_tools=fix_tools, 

233 show_conflicts=show_conflicts, 

234 ) 

235 with open(output, "w", encoding="utf-8") as f: 

236 f.write("\n".join(output_lines) + "\n") 

237 console.print() 

238 console.print(f"[green]✅ Output written to: {output}[/green]") 

239 except OSError as e: 

240 console.print(f"[red]Error writing to file {output}: {e}[/red]") 

241 

242 

243def _generate_plain_text_output( 

244 available_tools: dict[str, BaseToolPlugin], 

245 check_tools: dict[str, BaseToolPlugin], 

246 fix_tools: dict[str, BaseToolPlugin], 

247 show_conflicts: bool, 

248) -> list[str]: 

249 """Generate plain text output for file writing. 

250 

251 Args: 

252 available_tools: Dictionary of available tools. 

253 check_tools: Dictionary of check-capable tools. 

254 fix_tools: Dictionary of fix-capable tools. 

255 show_conflicts: Whether to include conflict information. 

256 

257 Returns: 

258 List of output lines. 

259 """ 

260 output_lines: list[str] = [] 

261 border = "=" * 70 

262 

263 output_lines.append(border) 

264 output_lines.append("Available Tools") 

265 output_lines.append(border) 

266 output_lines.append("") 

267 

268 for tool_name, plugin in available_tools.items(): 

269 tool_description = plugin.definition.description 

270 emoji = get_tool_emoji(tool_name) 

271 

272 capabilities: list[str] = [] 

273 if tool_name in check_tools: 

274 capabilities.append(Action.CHECK.value) 

275 if tool_name in fix_tools: 

276 capabilities.append(Action.FIX.value) 

277 

278 capabilities_display = ", ".join(capabilities) if capabilities else "-" 

279 

280 output_lines.append(f"{emoji} {tool_name}: {tool_description}") 

281 output_lines.append(f" Capabilities: {capabilities_display}") 

282 

283 if show_conflicts: 

284 conflict_names = _resolve_conflicts( 

285 plugin=plugin, 

286 available_tools=available_tools, 

287 ) 

288 if conflict_names: 

289 output_lines.append(f" Conflicts with: {', '.join(conflict_names)}") 

290 

291 output_lines.append("") 

292 

293 summary_border = "-" * 70 

294 output_lines.append(summary_border) 

295 output_lines.append(f"Total tools: {len(available_tools)}") 

296 output_lines.append(f"Check tools: {len(check_tools)}") 

297 output_lines.append(f"Fix tools: {len(fix_tools)}") 

298 output_lines.append(summary_border) 

299 

300 return output_lines