1import subprocess
2import sys
3import tomllib
4from pathlib import Path
5
6import click
7
8from plain.cli import register_cli
9from plain.cli.print import print_event
10
11from .biome import Biome
12
13DEFAULT_RUFF_CONFIG = Path(__file__).parent / "ruff_defaults.toml"
14
15
16@register_cli("code")
17@click.group()
18def cli():
19 """Code formatting and linting"""
20 pass
21
22
23@cli.command()
24@click.option("--force", is_flag=True, help="Reinstall even if up to date")
25@click.pass_context
26def install(ctx, force):
27 """Install or update the Biome standalone per configuration."""
28 config = get_code_config()
29
30 if not config.get("biome", {}).get("enabled", True):
31 click.secho("Biome is disabled in configuration", fg="yellow")
32 return
33
34 biome = Biome()
35
36 if force or not biome.is_installed() or biome.needs_update():
37 version_to_install = config.get("biome", {}).get("version", "")
38 if version_to_install:
39 click.secho(
40 f"Installing Biome standalone version {version_to_install}...",
41 bold=True,
42 nl=False,
43 )
44 installed = biome.install(version_to_install)
45 click.secho(f"Biome {installed} installed", fg="green")
46 else:
47 ctx.invoke(update)
48 else:
49 click.secho("Biome already installed", fg="green")
50
51
52@cli.command()
53def update():
54 """Update the Biome standalone binary to the latest release."""
55 config = get_code_config()
56
57 if not config.get("biome", {}).get("enabled", True):
58 click.secho("Biome is disabled in configuration", fg="yellow")
59 return
60
61 biome = Biome()
62 click.secho("Updating Biome standalone...", bold=True)
63 version = biome.install()
64 click.secho(f"Biome {version} installed", fg="green")
65
66
67@cli.command()
68@click.pass_context
69@click.argument("path", default=".")
70def check(ctx, path):
71 """Check the given path for formatting or linting issues."""
72 ruff_args = ["--config", str(DEFAULT_RUFF_CONFIG)]
73 config = get_code_config()
74
75 for e in config.get("exclude", []):
76 ruff_args.extend(["--exclude", e])
77
78 print_event("Ruff check")
79 result = subprocess.run(["ruff", "check", path, *ruff_args])
80 if result.returncode != 0:
81 sys.exit(result.returncode)
82
83 print_event("Ruff format check")
84 result = subprocess.run(["ruff", "format", path, "--check", *ruff_args])
85 if result.returncode != 0:
86 sys.exit(result.returncode)
87
88 if config.get("biome", {}).get("enabled", True):
89 biome = Biome()
90
91 if biome.needs_update():
92 ctx.invoke(install)
93
94 print_event("Biome check")
95 result = biome.invoke("check", path)
96 if result.returncode != 0:
97 sys.exit(result.returncode)
98
99
100@register_cli("fix")
101@cli.command()
102@click.pass_context
103@click.argument("path", default=".")
104@click.option("--unsafe-fixes", is_flag=True, help="Apply ruff unsafe fixes")
105@click.option("--add-noqa", is_flag=True, help="Add noqa comments to suppress errors")
106def fix(ctx, path, unsafe_fixes, add_noqa):
107 """Lint and format the given path."""
108 ruff_args = ["--config", str(DEFAULT_RUFF_CONFIG)]
109 config = get_code_config()
110
111 for e in config.get("exclude", []):
112 ruff_args.extend(["--exclude", e])
113
114 if unsafe_fixes and add_noqa:
115 raise click.UsageError("Cannot use both --unsafe-fixes and --add-noqa")
116
117 if unsafe_fixes:
118 print_event("Ruff fix (with unsafe fixes)")
119 result = subprocess.run(
120 ["ruff", "check", path, "--fix", "--unsafe-fixes", *ruff_args]
121 )
122 elif add_noqa:
123 print_event("Ruff fix (add noqa)")
124 result = subprocess.run(["ruff", "check", path, "--add-noqa", *ruff_args])
125 else:
126 print_event("Ruff fix")
127 result = subprocess.run(["ruff", "check", path, "--fix", *ruff_args])
128
129 if result.returncode != 0:
130 sys.exit(result.returncode)
131
132 print_event("Ruff format")
133 result = subprocess.run(["ruff", "format", path, *ruff_args])
134 if result.returncode != 0:
135 sys.exit(result.returncode)
136
137 if config.get("biome", {}).get("enabled", True):
138 biome = Biome()
139
140 if biome.needs_update():
141 ctx.invoke(install)
142
143 print_event("Biome format")
144
145 args = ["check", path, "--write"]
146
147 if unsafe_fixes:
148 args.append("--unsafe")
149
150 result = biome.invoke(*args)
151
152 if result.returncode != 0:
153 sys.exit(result.returncode)
154
155
156def get_code_config():
157 pyproject = Path("pyproject.toml")
158 if not pyproject.exists():
159 return {}
160 with pyproject.open("rb") as f:
161 return tomllib.load(f).get("tool", {}).get("plain", {}).get("code", {})