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 biome = Biome()
29
30 if force or not biome.is_installed() or biome.needs_update():
31 version_to_install = get_code_config().get("biome", {}).get("version", "")
32 if version_to_install:
33 click.secho(
34 f"Installing Biome standalone version {version_to_install}...",
35 bold=True,
36 nl=False,
37 )
38 installed = biome.install(version_to_install)
39 click.secho(f"Biome {installed} installed", fg="green")
40 else:
41 ctx.invoke(update)
42 else:
43 click.secho("Biome already installed", fg="green")
44
45
46@cli.command()
47def update():
48 """Update the Biome standalone binary to the latest release."""
49 biome = Biome()
50 click.secho("Updating Biome standalone...", bold=True)
51 version = biome.install()
52 click.secho(f"Biome {version} installed", fg="green")
53
54
55@cli.command()
56@click.pass_context
57@click.argument("path", default=".")
58def check(ctx, path):
59 """Check the given path for formatting or linting issues."""
60 ruff_args = ["--config", str(DEFAULT_RUFF_CONFIG)]
61 config = get_code_config()
62
63 for e in config.get("exclude", []):
64 ruff_args.extend(["--exclude", e])
65
66 print_event("Ruff check")
67 result = subprocess.run(["ruff", "check", path, *ruff_args])
68 if result.returncode != 0:
69 sys.exit(result.returncode)
70
71 print_event("Ruff format check")
72 result = subprocess.run(["ruff", "format", path, "--check", *ruff_args])
73 if result.returncode != 0:
74 sys.exit(result.returncode)
75
76 if config.get("biome", {}).get("enabled", True):
77 biome = Biome()
78
79 if biome.needs_update():
80 ctx.invoke(install)
81
82 print_event("Biome check")
83 result = biome.invoke("check", path)
84 if result.returncode != 0:
85 sys.exit(result.returncode)
86
87
88@register_cli("fix")
89@cli.command()
90@click.pass_context
91@click.argument("path", default=".")
92@click.option("--unsafe-fixes", is_flag=True, help="Apply ruff unsafe fixes")
93@click.option("--add-noqa", is_flag=True, help="Add noqa comments to suppress errors")
94def fix(ctx, path, unsafe_fixes, add_noqa):
95 """Lint and format the given path."""
96 ruff_args = ["--config", str(DEFAULT_RUFF_CONFIG)]
97 config = get_code_config()
98
99 for e in config.get("exclude", []):
100 ruff_args.extend(["--exclude", e])
101
102 if unsafe_fixes and add_noqa:
103 print("Cannot use both --unsafe-fixes and --add-noqa")
104 sys.exit(1)
105
106 if unsafe_fixes:
107 print_event("Ruff fix (with unsafe fixes)")
108 result = subprocess.run(
109 ["ruff", "check", path, "--fix", "--unsafe-fixes", *ruff_args]
110 )
111 elif add_noqa:
112 print_event("Ruff fix (add noqa)")
113 result = subprocess.run(["ruff", "check", path, "--add-noqa", *ruff_args])
114 else:
115 print_event("Ruff fix")
116 result = subprocess.run(["ruff", "check", path, "--fix", *ruff_args])
117
118 if result.returncode != 0:
119 sys.exit(result.returncode)
120
121 print_event("Ruff format")
122 result = subprocess.run(["ruff", "format", path, *ruff_args])
123 if result.returncode != 0:
124 sys.exit(result.returncode)
125
126 if config.get("biome", {}).get("enabled", True):
127 biome = Biome()
128
129 if biome.needs_update():
130 ctx.invoke(install)
131
132 print_event("Biome format")
133
134 args = ["check", path, "--write"]
135
136 if unsafe_fixes:
137 args.append("--unsafe")
138
139 result = biome.invoke(*args)
140
141 if result.returncode != 0:
142 sys.exit(result.returncode)
143
144
145def get_code_config():
146 pyproject = Path("pyproject.toml")
147 if not pyproject.exists():
148 return {}
149 with pyproject.open("rb") as f:
150 return tomllib.load(f).get("tool", {}).get("plain", {}).get("code", {})