Coverage for lintro / cli_utils / commands / install.py: 84%
100 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Install command for managing lintro tool dependencies.
3Installs, upgrades, and manages the external tools that lintro uses for
4linting, formatting, and code quality checks.
6Usage:
7 lintro install # Install all missing tools
8 lintro install ruff prettier # Install specific tools
9 lintro install --profile minimal # Install a profile's tools
10 lintro install --upgrade # Upgrade to manifest versions
11 lintro install --dry-run # Show plan without executing
12"""
14from __future__ import annotations
16import click
17from rich.console import Console
19from lintro.tools.core.install_context import RuntimeContext
20from lintro.tools.core.tool_installer import ToolInstaller
21from lintro.tools.core.tool_registry import ToolRegistry
24@click.command()
25@click.argument("tools", nargs=-1)
26@click.option(
27 "--profile",
28 type=str,
29 help="Install tools for a named profile (minimal, recommended, complete, ci).",
30)
31@click.option(
32 "--upgrade",
33 is_flag=True,
34 help="Upgrade already-installed tools to manifest versions.",
35)
36@click.option(
37 "--dry-run",
38 is_flag=True,
39 help="Show what would be installed without executing.",
40)
41@click.option(
42 "--all",
43 "install_all",
44 is_flag=True,
45 help="Install all supported tools.",
46)
47def install_command(
48 tools: tuple[str, ...],
49 profile: str | None,
50 *,
51 upgrade: bool,
52 dry_run: bool,
53 install_all: bool,
54) -> None:
55 """Install or upgrade external tools used by lintro.
57 Without arguments, installs all missing tools for the current project.
58 Specify tool names to install specific tools, or use --profile for
59 predefined sets.
61 Args:
62 tools: Tool names to install (positional args).
63 profile: Named profile to install.
64 upgrade: Upgrade existing tools.
65 dry_run: Show plan only.
66 install_all: Install all tools.
68 Raises:
69 SystemExit: When tool installation fails.
70 click.UsageError: When conflicting options or invalid profile given.
72 Examples:
73 lintro install
74 lintro install ruff prettier hadolint
75 lintro install --profile minimal
76 lintro install --upgrade
77 lintro install --dry-run
78 """
79 console = Console()
81 registry = ToolRegistry.load()
82 context = RuntimeContext.detect()
83 installer = ToolInstaller(registry, context)
85 # Determine tool list — reject conflicting selectors
86 tool_list: list[str] | None = list(tools) if tools else None
87 effective_profile = profile
88 selectors = sum(bool(x) for x in (tool_list, install_all, profile))
89 if selectors > 1:
90 raise click.UsageError(
91 "Cannot combine tool names, --profile, and --all; supply exactly one",
92 )
93 if install_all:
94 effective_profile = "complete"
96 # Validate tool names against registry
97 if tool_list:
98 unknown = [n for n in tool_list if n not in registry]
99 if unknown:
100 available = ", ".join(
101 sorted(t.name for t in registry.all_tools(include_dev=True)),
102 )
103 raise click.UsageError(
104 f"Unknown tools: {', '.join(unknown)}. " f"Available: {available}",
105 )
107 # Validate profile name
108 if effective_profile and effective_profile not in registry.profile_names:
109 raise click.UsageError(
110 f"Unknown profile {effective_profile!r}. "
111 f"Available: {', '.join(registry.profile_names)}",
112 )
114 # Detect project languages for auto-detect / recommended profile
115 detected_langs: list[str] | None = None
116 if not tool_list:
117 if not effective_profile:
118 effective_profile = "recommended"
119 if effective_profile == "recommended":
120 detected_langs = _detect_languages()
122 # Create plan
123 plan = installer.plan(
124 tools=tool_list,
125 profile=effective_profile,
126 upgrade=upgrade,
127 detected_langs=detected_langs,
128 )
130 # Display plan
131 console.print()
132 if plan.to_install:
133 console.print(f" [bold]To install ({len(plan.to_install)}):[/bold]")
134 for tool, cmd in plan.to_install:
135 console.print(f" {tool.name:<20} [dim]{cmd}[/dim]")
137 if plan.to_upgrade:
138 console.print(f" [bold]To upgrade ({len(plan.to_upgrade)}):[/bold]")
139 for tool, current, cmd in plan.to_upgrade:
140 console.print(
141 f" {tool.name:<20} [yellow]{current}[/yellow] → "
142 f"[green]{tool.version}[/green] [dim]{cmd}[/dim]",
143 )
145 if plan.already_ok:
146 console.print(
147 f" [dim]Already installed: {len(plan.already_ok)} tools[/dim]",
148 )
150 if plan.skipped:
151 console.print(f" [yellow]Skipped ({len(plan.skipped)}):[/yellow]")
152 for tool, reason in plan.skipped:
153 console.print(f" {tool.name:<20} [dim]{reason}[/dim]")
155 if not plan.has_work:
156 if plan.outdated:
157 console.print(
158 f" [yellow]Outdated: {len(plan.outdated)} tool(s) "
159 f"(use --upgrade to update)[/yellow]",
160 )
161 if not plan.outdated and not plan.skipped:
162 console.print(" [green]All tools are already installed.[/green]")
163 console.print()
164 if plan.skipped or plan.outdated:
165 raise SystemExit(1)
166 return
168 if dry_run:
169 console.print()
170 console.print(
171 f" [dim]Dry run: {plan.total_actions} tool(s) would be installed.[/dim]",
172 )
173 if plan.outdated:
174 console.print(
175 f" [yellow]Outdated: {len(plan.outdated)} tool(s) "
176 f"(use --upgrade to update)[/yellow]",
177 )
178 console.print()
179 if plan.skipped or plan.outdated:
180 raise SystemExit(1)
181 return
183 # Execute
184 console.print()
185 results = installer.execute(plan)
187 # Report results
188 succeeded = sum(1 for r in results if r.success)
189 failed = sum(1 for r in results if not r.success)
191 for r in results:
192 if r.success:
193 console.print(
194 f" [green]OK[/green] {r.tool.name} "
195 f"[dim]({r.duration_seconds:.1f}s)[/dim]",
196 )
197 else:
198 console.print(f" [red]FAIL[/red] {r.tool.name}: {r.message}")
200 console.print()
201 has_issues = failed > 0 or plan.skipped or plan.outdated
203 if failed > 0:
204 console.print(
205 f" [yellow]{succeeded} installed, {failed} failed[/yellow]",
206 )
207 elif has_issues:
208 console.print(f" [green]{succeeded} tools installed.[/green]")
209 else:
210 console.print(f" [green]All {succeeded} tools installed.[/green]")
212 if plan.outdated:
213 console.print(
214 f" [yellow]Outdated: {len(plan.outdated)} tool(s) "
215 f"(use --upgrade to update)[/yellow]",
216 )
218 console.print()
219 if has_issues:
220 raise SystemExit(1)
223def _detect_languages() -> list[str]:
224 """Detect project languages for profile resolution.
226 Uses the full-ecosystem detector to cover Docker, YAML, Markdown,
227 TOML, Shell, SQL, GitHub Actions, Astro, Svelte, Vue, etc.
229 Returns:
230 List of detected language identifiers.
231 """
232 try:
233 from lintro.utils.project_detection import detect_project_languages
234 except ImportError:
235 return []
236 try:
237 return detect_project_languages()
238 except OSError:
239 return []