lintro 0.13.2__py3-none-any.whl → 0.17.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. lintro/__init__.py +1 -1
  2. lintro/cli.py +226 -16
  3. lintro/cli_utils/commands/__init__.py +8 -1
  4. lintro/cli_utils/commands/check.py +1 -0
  5. lintro/cli_utils/commands/config.py +325 -0
  6. lintro/cli_utils/commands/init.py +361 -0
  7. lintro/cli_utils/commands/list_tools.py +180 -42
  8. lintro/cli_utils/commands/test.py +316 -0
  9. lintro/cli_utils/commands/versions.py +81 -0
  10. lintro/config/__init__.py +62 -0
  11. lintro/config/config_loader.py +420 -0
  12. lintro/config/lintro_config.py +189 -0
  13. lintro/config/tool_config_generator.py +403 -0
  14. lintro/enums/tool_name.py +2 -0
  15. lintro/enums/tool_type.py +2 -0
  16. lintro/formatters/tools/__init__.py +12 -0
  17. lintro/formatters/tools/eslint_formatter.py +108 -0
  18. lintro/formatters/tools/markdownlint_formatter.py +88 -0
  19. lintro/formatters/tools/pytest_formatter.py +201 -0
  20. lintro/parsers/__init__.py +69 -9
  21. lintro/parsers/bandit/__init__.py +6 -0
  22. lintro/parsers/bandit/bandit_issue.py +49 -0
  23. lintro/parsers/bandit/bandit_parser.py +99 -0
  24. lintro/parsers/black/black_issue.py +4 -0
  25. lintro/parsers/eslint/__init__.py +6 -0
  26. lintro/parsers/eslint/eslint_issue.py +26 -0
  27. lintro/parsers/eslint/eslint_parser.py +63 -0
  28. lintro/parsers/markdownlint/__init__.py +6 -0
  29. lintro/parsers/markdownlint/markdownlint_issue.py +22 -0
  30. lintro/parsers/markdownlint/markdownlint_parser.py +113 -0
  31. lintro/parsers/pytest/__init__.py +21 -0
  32. lintro/parsers/pytest/pytest_issue.py +28 -0
  33. lintro/parsers/pytest/pytest_parser.py +483 -0
  34. lintro/tools/__init__.py +2 -0
  35. lintro/tools/core/timeout_utils.py +112 -0
  36. lintro/tools/core/tool_base.py +255 -45
  37. lintro/tools/core/tool_manager.py +77 -24
  38. lintro/tools/core/version_requirements.py +482 -0
  39. lintro/tools/implementations/pytest/pytest_command_builder.py +311 -0
  40. lintro/tools/implementations/pytest/pytest_config.py +200 -0
  41. lintro/tools/implementations/pytest/pytest_error_handler.py +128 -0
  42. lintro/tools/implementations/pytest/pytest_executor.py +122 -0
  43. lintro/tools/implementations/pytest/pytest_handlers.py +375 -0
  44. lintro/tools/implementations/pytest/pytest_option_validators.py +212 -0
  45. lintro/tools/implementations/pytest/pytest_output_processor.py +408 -0
  46. lintro/tools/implementations/pytest/pytest_result_processor.py +113 -0
  47. lintro/tools/implementations/pytest/pytest_utils.py +697 -0
  48. lintro/tools/implementations/tool_actionlint.py +106 -16
  49. lintro/tools/implementations/tool_bandit.py +23 -7
  50. lintro/tools/implementations/tool_black.py +236 -29
  51. lintro/tools/implementations/tool_darglint.py +180 -21
  52. lintro/tools/implementations/tool_eslint.py +374 -0
  53. lintro/tools/implementations/tool_hadolint.py +94 -25
  54. lintro/tools/implementations/tool_markdownlint.py +354 -0
  55. lintro/tools/implementations/tool_prettier.py +313 -26
  56. lintro/tools/implementations/tool_pytest.py +327 -0
  57. lintro/tools/implementations/tool_ruff.py +247 -70
  58. lintro/tools/implementations/tool_yamllint.py +448 -34
  59. lintro/tools/tool_enum.py +6 -0
  60. lintro/utils/config.py +41 -18
  61. lintro/utils/console_logger.py +211 -25
  62. lintro/utils/path_utils.py +42 -0
  63. lintro/utils/tool_executor.py +336 -39
  64. lintro/utils/tool_utils.py +38 -2
  65. lintro/utils/unified_config.py +926 -0
  66. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/METADATA +131 -29
  67. lintro-0.17.2.dist-info/RECORD +134 -0
  68. lintro-0.13.2.dist-info/RECORD +0 -96
  69. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/WHEEL +0 -0
  70. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/entry_points.txt +0 -0
  71. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/licenses/LICENSE +0 -0
  72. {lintro-0.13.2.dist-info → lintro-0.17.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,361 @@
1
+ """Init command for Lintro.
2
+
3
+ Creates configuration files for Lintro and optionally native tool configs.
4
+ """
5
+
6
+ import json
7
+ from pathlib import Path
8
+
9
+ import click
10
+ from loguru import logger
11
+ from rich.console import Console
12
+ from rich.panel import Panel
13
+
14
+ # Default Lintro config template
15
+ DEFAULT_CONFIG_TEMPLATE = """\
16
+ # Lintro Configuration
17
+ # https://github.com/TurboCoder13/py-lintro
18
+ #
19
+ # Lintro acts as the master configuration source for all tools.
20
+ # Native tool configs (e.g., .prettierrc) are ignored by default unless
21
+ # explicitly referenced via config_source.
22
+
23
+ enforce:
24
+ # Line length limit applied to all supporting tools
25
+ # Maps to: ruff line-length, black line-length, prettier printWidth, etc.
26
+ line_length: 88
27
+
28
+ # Python version target (e.g., "py313", "py312", "py310")
29
+ # Maps to: ruff target-version, black target-version
30
+ # Omit to let tools infer from requires-python in pyproject.toml
31
+ # target_python: "py310"
32
+
33
+ execution:
34
+ # List of tools to run (empty = all available tools)
35
+ # enabled_tools: ["ruff", "prettier", "markdownlint", "yamllint"]
36
+ enabled_tools: []
37
+
38
+ # Execution order strategy:
39
+ # - "priority": Formatters before linters (default)
40
+ # - "alphabetical": Alphabetical order
41
+ # - ["tool1", "tool2", ...]: Custom order
42
+ tool_order: "priority"
43
+
44
+ # Stop on first tool failure
45
+ fail_fast: false
46
+
47
+ tools:
48
+ # Ruff - Python linter and formatter
49
+ ruff:
50
+ enabled: true
51
+ # config_source: ".ruff.toml" # Optional: inherit from native config
52
+ # Settings are passed directly to Ruff
53
+ # select: ["E", "F", "W", "I"]
54
+ # ignore: ["E501"]
55
+
56
+ # Black - Python formatter
57
+ black:
58
+ enabled: true
59
+ # config_source: "pyproject.toml" # Optional: use [tool.black] section
60
+
61
+ # Prettier - Multi-language formatter
62
+ prettier:
63
+ enabled: true
64
+ # config_source: ".prettierrc" # Optional: inherit from native config
65
+ # overrides:
66
+ # printWidth: 88 # Override to match enforce line_length
67
+
68
+ # Markdownlint - Markdown linter
69
+ markdownlint:
70
+ enabled: true
71
+ # MD013 line_length is automatically synced from enforce.line_length
72
+
73
+ # Yamllint - YAML linter
74
+ yamllint:
75
+ enabled: true
76
+
77
+ # Bandit - Security linter
78
+ bandit:
79
+ enabled: true
80
+
81
+ # Hadolint - Dockerfile linter
82
+ hadolint:
83
+ enabled: true
84
+
85
+ # Actionlint - GitHub Actions linter
86
+ actionlint:
87
+ enabled: true
88
+
89
+ # Darglint - Docstring linter
90
+ darglint:
91
+ enabled: true
92
+ """
93
+
94
+ MINIMAL_CONFIG_TEMPLATE = """\
95
+ # Lintro Configuration (Minimal)
96
+ # https://github.com/TurboCoder13/py-lintro
97
+
98
+ enforce:
99
+ line_length: 88
100
+ # target_python: "py310" # Omit to infer from requires-python
101
+
102
+ execution:
103
+ tool_order: "priority"
104
+
105
+ tools:
106
+ ruff:
107
+ enabled: true
108
+ black:
109
+ enabled: true
110
+ prettier:
111
+ enabled: true
112
+ """
113
+
114
+ # Native config templates
115
+ PRETTIERRC_TEMPLATE = {
116
+ "semi": True,
117
+ "singleQuote": True,
118
+ "tabWidth": 2,
119
+ "printWidth": 88,
120
+ "trailingComma": "es5",
121
+ "proseWrap": "always",
122
+ "overrides": [
123
+ {
124
+ "files": "*.md",
125
+ "options": {
126
+ "printWidth": 88,
127
+ "proseWrap": "always",
128
+ },
129
+ },
130
+ ],
131
+ }
132
+
133
+ PRETTIERIGNORE_TEMPLATE = """\
134
+ # Ignore artifacts
135
+ build
136
+ dist
137
+ node_modules
138
+ .venv
139
+ .lintro
140
+ """
141
+
142
+ MARKDOWNLINT_TEMPLATE = {
143
+ "config": {
144
+ "MD013": {
145
+ "line_length": 88,
146
+ "code_blocks": False,
147
+ "tables": False,
148
+ },
149
+ },
150
+ }
151
+
152
+
153
+ def _write_file(
154
+ path: Path,
155
+ content: str,
156
+ console: Console,
157
+ force: bool,
158
+ ) -> bool:
159
+ """Write content to a file, handling existing files.
160
+
161
+ Args:
162
+ path: Path to write to.
163
+ content: Content to write.
164
+ console: Rich console for output.
165
+ force: Whether to overwrite existing files.
166
+
167
+ Returns:
168
+ bool: True if file was written, False if skipped.
169
+ """
170
+ if path.exists() and not force:
171
+ console.print(f" [yellow]⏭️ Skipped {path} (already exists)[/yellow]")
172
+ return False
173
+
174
+ try:
175
+ path.write_text(content, encoding="utf-8")
176
+ console.print(f" [green]✅ Created {path}[/green]")
177
+ return True
178
+ except (OSError, Exception) as e:
179
+ console.print(f" [red]❌ Failed to write {path}: {e}[/red]")
180
+ return False
181
+
182
+
183
+ def _write_json_file(
184
+ path: Path,
185
+ data: dict,
186
+ console: Console,
187
+ force: bool,
188
+ ) -> bool:
189
+ """Write JSON content to a file, handling existing files.
190
+
191
+ Args:
192
+ path: Path to write to.
193
+ data: Dictionary to serialize as JSON.
194
+ console: Rich console for output.
195
+ force: Whether to overwrite existing files.
196
+
197
+ Returns:
198
+ bool: True if file was written, False if skipped.
199
+ """
200
+ content = json.dumps(obj=data, indent=2) + "\n"
201
+ return _write_file(path=path, content=content, console=console, force=force)
202
+
203
+
204
+ def _generate_native_configs(
205
+ console: Console,
206
+ force: bool,
207
+ ) -> list[str]:
208
+ """Generate native tool configuration files.
209
+
210
+ Args:
211
+ console: Rich console for output.
212
+ force: Whether to overwrite existing files.
213
+
214
+ Returns:
215
+ list[str]: List of created file names.
216
+ """
217
+ created: list[str] = []
218
+
219
+ console.print("\n[bold cyan]Generating native tool configs:[/bold cyan]")
220
+
221
+ # Prettier config
222
+ if _write_json_file(
223
+ path=Path(".prettierrc.json"),
224
+ data=PRETTIERRC_TEMPLATE,
225
+ console=console,
226
+ force=force,
227
+ ):
228
+ created.append(".prettierrc.json")
229
+
230
+ # Prettier ignore
231
+ if _write_file(
232
+ path=Path(".prettierignore"),
233
+ content=PRETTIERIGNORE_TEMPLATE,
234
+ console=console,
235
+ force=force,
236
+ ):
237
+ created.append(".prettierignore")
238
+
239
+ # Markdownlint config
240
+ if _write_json_file(
241
+ path=Path(".markdownlint-cli2.jsonc"),
242
+ data=MARKDOWNLINT_TEMPLATE,
243
+ console=console,
244
+ force=force,
245
+ ):
246
+ created.append(".markdownlint-cli2.jsonc")
247
+
248
+ return created
249
+
250
+
251
+ @click.command("init")
252
+ @click.option(
253
+ "--minimal",
254
+ "-m",
255
+ is_flag=True,
256
+ help="Create a minimal config file with fewer comments.",
257
+ )
258
+ @click.option(
259
+ "--force",
260
+ "-f",
261
+ is_flag=True,
262
+ help="Overwrite existing configuration files.",
263
+ )
264
+ @click.option(
265
+ "--output",
266
+ "-o",
267
+ type=click.Path(),
268
+ default=".lintro-config.yaml",
269
+ help="Output file path (default: .lintro-config.yaml).",
270
+ )
271
+ @click.option(
272
+ "--with-native-configs",
273
+ is_flag=True,
274
+ help="Also generate native tool configs (.prettierrc.json, etc.).",
275
+ )
276
+ def init_command(
277
+ minimal: bool,
278
+ force: bool,
279
+ output: str,
280
+ with_native_configs: bool,
281
+ ) -> None:
282
+ """Initialize Lintro configuration for your project.
283
+
284
+ Creates a scaffold configuration file with sensible defaults.
285
+ Lintro will use this file as the master configuration source,
286
+ ignoring native tool configs unless explicitly referenced.
287
+
288
+ Use --with-native-configs to also generate native tool configuration
289
+ files for IDE integration (e.g., Prettier extension, markdownlint extension).
290
+
291
+ Args:
292
+ minimal: Use minimal template with fewer comments.
293
+ force: Overwrite existing config file if it exists.
294
+ output: Output file path for the config file.
295
+ with_native_configs: Also generate native tool config files.
296
+
297
+ Raises:
298
+ SystemExit: If file exists and --force not provided, or write fails.
299
+ """
300
+ console = Console()
301
+ output_path = Path(output)
302
+ created_files: list[str] = []
303
+
304
+ # Check if main config file already exists
305
+ if output_path.exists() and not force:
306
+ console.print(
307
+ f"[red]Error: {output_path} already exists. "
308
+ "Use --force to overwrite.[/red]",
309
+ )
310
+ raise SystemExit(1)
311
+
312
+ # Select template
313
+ template = MINIMAL_CONFIG_TEMPLATE if minimal else DEFAULT_CONFIG_TEMPLATE
314
+
315
+ # Write main config file
316
+ try:
317
+ output_path.write_text(template, encoding="utf-8")
318
+ created_files.append(str(output_path))
319
+ logger.debug(f"Created config file: {output_path.resolve()}")
320
+
321
+ except (OSError, PermissionError) as e:
322
+ console.print(f"[red]Error: Failed to write {output_path}: {e}[/red]")
323
+ raise SystemExit(1) from e
324
+
325
+ # Generate native configs if requested
326
+ if with_native_configs:
327
+ native_files = _generate_native_configs(console=console, force=force)
328
+ created_files.extend(native_files)
329
+
330
+ # Success panel
331
+ console.print()
332
+ if len(created_files) == 1:
333
+ console.print(
334
+ Panel.fit(
335
+ f"[bold green]✅ Created {output_path}[/bold green]",
336
+ border_style="green",
337
+ ),
338
+ )
339
+ else:
340
+ files_list = "\n".join(f" • {f}" for f in created_files)
341
+ msg = f"[bold green]✅ Created {len(created_files)} files:[/bold green]"
342
+ console.print(
343
+ Panel.fit(
344
+ f"{msg}\n{files_list}",
345
+ border_style="green",
346
+ ),
347
+ )
348
+
349
+ console.print()
350
+
351
+ # Next steps
352
+ console.print("[bold cyan]Next steps:[/bold cyan]")
353
+ console.print(" [dim]1.[/dim] Review and customize the configuration")
354
+ console.print(
355
+ " [dim]2.[/dim] Run [cyan]lintro config[/cyan] to view config",
356
+ )
357
+ console.print(" [dim]3.[/dim] Run [cyan]lintro check .[/cyan] to lint")
358
+ if with_native_configs:
359
+ console.print(
360
+ " [dim]3.[/dim] Commit the config files to your repository",
361
+ )
@@ -3,11 +3,46 @@
3
3
  This module provides the core logic for the 'list_tools' command.
4
4
  """
5
5
 
6
+ from typing import Any
7
+
6
8
  import click
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+ from rich.table import Table
7
12
 
8
13
  from lintro.enums.action import Action
9
14
  from lintro.tools import tool_manager
15
+ from lintro.tools.tool_enum import ToolEnum
10
16
  from lintro.utils.console_logger import get_tool_emoji
17
+ from lintro.utils.unified_config import get_tool_priority, is_tool_injectable
18
+
19
+
20
+ def _resolve_conflicts(
21
+ tool_config: Any,
22
+ name_to_enum_map: dict[str, ToolEnum],
23
+ available_tools: dict[ToolEnum, Any],
24
+ ) -> list[str]:
25
+ """Resolve conflict names for a tool.
26
+
27
+ Args:
28
+ tool_config: Tool configuration object.
29
+ name_to_enum_map: Mapping of tool names to ToolEnum.
30
+ available_tools: Dictionary of available tools.
31
+
32
+ Returns:
33
+ List of conflict tool names.
34
+ """
35
+ conflict_names: list[str] = []
36
+ if hasattr(tool_config, "conflicts_with") and tool_config.conflicts_with:
37
+ for conflict in tool_config.conflicts_with:
38
+ conflict_enum: ToolEnum | None = None
39
+ if isinstance(conflict, str):
40
+ conflict_enum = name_to_enum_map.get(conflict.lower())
41
+ elif isinstance(conflict, ToolEnum):
42
+ conflict_enum = conflict
43
+ if conflict_enum is not None and conflict_enum in available_tools:
44
+ conflict_names.append(conflict_enum.name.lower())
45
+ return conflict_names
11
46
 
12
47
 
13
48
  @click.command("list-tools")
@@ -44,24 +79,145 @@ def list_tools(
44
79
  output: Output file path.
45
80
  show_conflicts: Whether to show potential conflicts between tools.
46
81
  """
82
+ console = Console()
47
83
  available_tools = tool_manager.get_available_tools()
48
84
  check_tools = tool_manager.get_check_tools()
49
85
  fix_tools = tool_manager.get_fix_tools()
50
86
 
51
- output_lines = []
87
+ # Header panel
88
+ console.print(
89
+ Panel.fit(
90
+ "[bold cyan]🔧 Available Tools[/bold cyan]",
91
+ border_style="cyan",
92
+ ),
93
+ )
94
+ console.print()
95
+
96
+ # Main tools table
97
+ table = Table(title="Tool Details")
98
+ table.add_column("Tool", style="cyan", no_wrap=True)
99
+ table.add_column("Description", style="white", max_width=40)
100
+ table.add_column("Capabilities", style="green")
101
+ table.add_column("Priority", justify="center", style="yellow")
102
+ table.add_column("Type", style="magenta")
103
+
104
+ if show_conflicts:
105
+ table.add_column("Conflicts", style="red")
106
+
107
+ # Build name-to-enum map for conflict resolution
108
+ name_to_enum_map = {t.name.lower(): t for t in ToolEnum}
109
+
110
+ for tool_enum, tool in available_tools.items():
111
+ tool_name = tool_enum.name.lower()
112
+ tool_description = getattr(
113
+ tool.config,
114
+ "description",
115
+ tool.__class__.__name__,
116
+ )
117
+ emoji = get_tool_emoji(tool_name)
118
+
119
+ # Capabilities
120
+ capabilities: list[str] = []
121
+ if tool_enum in check_tools:
122
+ capabilities.append("check")
123
+ if tool_enum in fix_tools:
124
+ capabilities.append("fix")
125
+ caps_display = ", ".join(capabilities) if capabilities else "-"
126
+
127
+ # Priority and type
128
+ priority = get_tool_priority(tool_name)
129
+ injectable = is_tool_injectable(tool_name)
130
+ tool_type = "Syncable" if injectable else "Native only"
131
+
132
+ row = [
133
+ f"{emoji} {tool_name}",
134
+ tool_description,
135
+ caps_display,
136
+ str(priority),
137
+ tool_type,
138
+ ]
139
+
140
+ # Conflicts
141
+ if show_conflicts:
142
+ conflict_names = _resolve_conflicts(
143
+ tool_config=tool.config,
144
+ name_to_enum_map=name_to_enum_map,
145
+ available_tools=available_tools,
146
+ )
147
+ row.append(", ".join(conflict_names) if conflict_names else "-")
148
+
149
+ table.add_row(*row)
150
+
151
+ console.print(table)
152
+ console.print()
153
+
154
+ # Summary table
155
+ summary_table = Table(
156
+ title="Summary",
157
+ show_header=False,
158
+ box=None,
159
+ )
160
+ summary_table.add_column("Metric", style="cyan", width=20)
161
+ summary_table.add_column("Count", style="yellow", justify="right")
162
+
163
+ summary_table.add_row("📊 Total tools", str(len(available_tools)))
164
+ summary_table.add_row("🔍 Check tools", str(len(check_tools)))
165
+ summary_table.add_row("🔧 Fix tools", str(len(fix_tools)))
166
+
167
+ console.print(summary_table)
168
+
169
+ # Write to file if specified
170
+ if output:
171
+ try:
172
+ # For file output, use plain text format
173
+ output_lines = _generate_plain_text_output(
174
+ available_tools=available_tools,
175
+ check_tools=check_tools,
176
+ fix_tools=fix_tools,
177
+ show_conflicts=show_conflicts,
178
+ )
179
+ with open(output, "w", encoding="utf-8") as f:
180
+ f.write("\n".join(output_lines) + "\n")
181
+ console.print()
182
+ console.print(f"[green]✅ Output written to: {output}[/green]")
183
+ except OSError as e:
184
+ console.print(f"[red]Error writing to file {output}: {e}[/red]")
185
+
186
+
187
+ def _generate_plain_text_output(
188
+ available_tools: dict[ToolEnum, Any],
189
+ check_tools: dict[ToolEnum, Any],
190
+ fix_tools: dict[ToolEnum, Any],
191
+ show_conflicts: bool,
192
+ ) -> list[str]:
193
+ """Generate plain text output for file writing.
194
+
195
+ Args:
196
+ available_tools: Dictionary of available tools.
197
+ check_tools: Dictionary of check-capable tools.
198
+ fix_tools: Dictionary of fix-capable tools.
199
+ show_conflicts: Whether to include conflict information.
52
200
 
53
- # Create header with emojis
201
+ Returns:
202
+ List of output lines.
203
+ """
204
+ output_lines: list[str] = []
54
205
  border = "=" * 70
55
- header_title = "🔧 Available Tools"
56
- emojis = "🔧 🔧 🔧 🔧 🔧"
57
- output_lines.append(f"{border}")
58
- output_lines.append(f"{header_title} {emojis}")
59
- output_lines.append(f"{border}")
206
+
207
+ output_lines.append(border)
208
+ output_lines.append("Available Tools")
209
+ output_lines.append(border)
60
210
  output_lines.append("")
61
211
 
212
+ name_to_enum_map = {t.name.lower(): t for t in ToolEnum}
213
+
62
214
  for tool_enum, tool in available_tools.items():
63
215
  tool_name = tool_enum.name.lower()
64
- tool_description = getattr(tool.config, "description", tool.__class__.__name__)
216
+ tool_description = getattr(
217
+ tool.config,
218
+ "description",
219
+ tool.__class__.__name__,
220
+ )
65
221
  emoji = get_tool_emoji(tool_name)
66
222
 
67
223
  capabilities: list[str] = []
@@ -70,45 +226,27 @@ def list_tools(
70
226
  if tool_enum in fix_tools:
71
227
  capabilities.append(Action.FIX.value)
72
228
 
229
+ capabilities_display = ", ".join(capabilities) if capabilities else "-"
230
+
73
231
  output_lines.append(f"{emoji} {tool_name}: {tool_description}")
74
- output_lines.append(f" Capabilities: {', '.join(capabilities)}")
75
-
76
- if (
77
- show_conflicts
78
- and hasattr(tool.config, "conflicts_with")
79
- and tool.config.conflicts_with
80
- ):
81
- conflicts = [
82
- conflict.name.lower()
83
- for conflict in tool.config.conflicts_with
84
- if conflict in available_tools
85
- ]
86
- if conflicts:
87
- output_lines.append(f" Conflicts with: {', '.join(conflicts)}")
232
+ output_lines.append(f" Capabilities: {capabilities_display}")
233
+
234
+ if show_conflicts:
235
+ conflict_names = _resolve_conflicts(
236
+ tool_config=tool.config,
237
+ name_to_enum_map=name_to_enum_map,
238
+ available_tools=available_tools,
239
+ )
240
+ if conflict_names:
241
+ output_lines.append(f" Conflicts with: {', '.join(conflict_names)}")
88
242
 
89
243
  output_lines.append("")
90
244
 
91
- # Add summary footer
92
245
  summary_border = "-" * 70
93
246
  output_lines.append(summary_border)
94
- output_lines.append(f"📊 Total tools: {len(available_tools)}")
95
- output_lines.append(f"🔍 Check tools: {len(check_tools)}")
96
- output_lines.append(f"🔧 Fix tools: {len(fix_tools)}")
247
+ output_lines.append(f"Total tools: {len(available_tools)}")
248
+ output_lines.append(f"Check tools: {len(check_tools)}")
249
+ output_lines.append(f"Fix tools: {len(fix_tools)}")
97
250
  output_lines.append(summary_border)
98
251
 
99
- # Format output
100
- output_text = "\n".join(output_lines)
101
-
102
- # Print to console using click.echo for consistency
103
- click.echo(output_text)
104
-
105
- # Write to file if specified
106
- if output:
107
- try:
108
- with open(output, "w", encoding="utf-8") as f:
109
- f.write(output_text + "\n")
110
- success_msg = f"Output written to: {output}"
111
- click.echo(success_msg)
112
- except OSError as e:
113
- error_msg = f"Error writing to file {output}: {e}"
114
- click.echo(error_msg, err=True)
252
+ return output_lines