plain.code 0.20.2__tar.gz → 0.21.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.code
3
- Version: 0.20.2
3
+ Version: 0.21.0
4
4
  Summary: Preconfigured code formatting and linting.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-Expression: BSD-3-Clause
@@ -32,9 +32,10 @@ Plain.code provides comprehensive code quality tools with sensible defaults:
32
32
 
33
33
  - **[Ruff](https://astral.sh/ruff)** - Python linting and formatting
34
34
  - **[ty](https://astral.sh/ty)** - Python type checking
35
- - **[Biome](https://biomejs.dev/)** - JavaScript, JSON, and CSS formatting
35
+ - **[oxlint](https://oxc.rs/)** - JavaScript and TypeScript linting
36
+ - **[oxfmt](https://oxc.rs/)** - JavaScript, TypeScript, JSON, and CSS formatting
36
37
 
37
- Ruff and ty are installed as Python dependencies. Biome is managed automatically as a standalone binary (npm is not required).
38
+ Ruff and ty are installed as Python dependencies. oxlint and oxfmt are managed automatically as standalone binaries (npm is not required).
38
39
 
39
40
  ## Commands
40
41
 
@@ -73,10 +74,10 @@ You can skip specific tools if needed:
73
74
  plain code check --skip-ty
74
75
 
75
76
  # Only run type checks
76
- plain code check --skip-ruff --skip-biome
77
+ plain code check --skip-ruff --skip-oxc
77
78
 
78
- # Skip Biome checks
79
- plain code check --skip-biome
79
+ # Skip oxlint and oxfmt checks
80
+ plain code check --skip-oxc
80
81
 
81
82
  # Skip annotation coverage checks
82
83
  plain code check --skip-annotations
@@ -108,7 +109,7 @@ plain code annotations --json
108
109
 
109
110
  ## Settings
110
111
 
111
- Default configuration is provided by [`ruff_defaults.toml`](./ruff_defaults.toml) and [`biome_defaults.json`](./biome_defaults.json).
112
+ Default configuration is provided by [`ruff_defaults.toml`](./ruff_defaults.toml).
112
113
 
113
114
  You can customize the behavior in your `pyproject.toml`:
114
115
 
@@ -119,9 +120,9 @@ exclude = ["path/to/exclude"]
119
120
  [tool.plain.code.ty]
120
121
  enabled = true # Set to false to disable ty
121
122
 
122
- [tool.plain.code.biome]
123
- enabled = true # Set to false to disable Biome
124
- version = "1.5.3" # Pin to a specific version
123
+ [tool.plain.code.oxc]
124
+ enabled = true # Set to false to disable oxlint/oxfmt
125
+ version = "1.43.0" # Pin to a specific version
125
126
 
126
127
  [tool.plain.code.annotations]
127
128
  enabled = true # Set to false to disable annotation checks
@@ -130,16 +131,16 @@ exclude = ["migrations"] # Exclude specific patterns
130
131
 
131
132
  For more advanced configuration options, see [`get_code_config`](./cli.py#get_code_config).
132
133
 
133
- Generally you won't need to change the configuration. The defaults are designed to "just work" for most projects. If you find yourself needing extensive customization, consider using the underlying tools (Ruff, ty, Biome) directly instead.
134
+ Generally you won't need to change the configuration. The defaults are designed to "just work" for most projects. If you find yourself needing extensive customization, consider using the underlying tools (Ruff, ty, oxlint, oxfmt) directly instead.
134
135
 
135
136
  ## FAQs
136
137
 
137
- #### How do I install or update Biome manually?
138
+ #### How do I install or update oxlint/oxfmt manually?
138
139
 
139
- Biome is installed automatically when you run `plain fix` or `plain code check`. If you need to manage it manually:
140
+ oxlint and oxfmt are installed automatically when you run `plain fix` or `plain code check`. If you need to manage them manually:
140
141
 
141
142
  ```bash
142
- # Install Biome (or reinstall if corrupted)
143
+ # Install oxlint/oxfmt (or reinstall if corrupted)
143
144
  plain code install
144
145
 
145
146
  # Force reinstall even if up to date
@@ -1,5 +1,29 @@
1
1
  # plain-code changelog
2
2
 
3
+ ## [0.21.0](https://github.com/dropseed/plain/releases/plain-code@0.21.0) (2026-02-25)
4
+
5
+ ### What's changed
6
+
7
+ - Replaced Biome with oxc tools — JS/TS linting is now handled by [oxlint](https://oxc.rs/) and formatting by [oxfmt](https://oxc.rs/) ([5eb7ba6f6f7f](https://github.com/dropseed/plain/commit/5eb7ba6f6f7f))
8
+ - The `--skip-biome` CLI flag is now `--skip-oxc` ([5eb7ba6f6f7f](https://github.com/dropseed/plain/commit/5eb7ba6f6f7f))
9
+ - Configuration key changed from `[tool.plain.code.biome]` to `[tool.plain.code.oxc]` in pyproject.toml ([5eb7ba6f6f7f](https://github.com/dropseed/plain/commit/5eb7ba6f6f7f))
10
+
11
+ ### Upgrade instructions
12
+
13
+ - If you have `[tool.plain.code.biome]` in your pyproject.toml, rename it to `[tool.plain.code.oxc]`.
14
+ - Replace `--skip-biome` with `--skip-oxc` in any scripts or CI configuration.
15
+ - The oxlint and oxfmt binaries will be downloaded automatically on first run.
16
+
17
+ ## [0.20.3](https://github.com/dropseed/plain/releases/plain-code@0.20.3) (2026-02-04)
18
+
19
+ ### What's changed
20
+
21
+ - Removed `@internalcode` decorator from `Biome` class ([e7164d3891b2](https://github.com/dropseed/plain/commit/e7164d3891b2))
22
+
23
+ ### Upgrade instructions
24
+
25
+ - No changes required.
26
+
3
27
  ## [0.20.2](https://github.com/dropseed/plain/releases/plain-code@0.20.2) (2026-01-28)
4
28
 
5
29
  ### What's changed
@@ -17,9 +17,10 @@ Plain.code provides comprehensive code quality tools with sensible defaults:
17
17
 
18
18
  - **[Ruff](https://astral.sh/ruff)** - Python linting and formatting
19
19
  - **[ty](https://astral.sh/ty)** - Python type checking
20
- - **[Biome](https://biomejs.dev/)** - JavaScript, JSON, and CSS formatting
20
+ - **[oxlint](https://oxc.rs/)** - JavaScript and TypeScript linting
21
+ - **[oxfmt](https://oxc.rs/)** - JavaScript, TypeScript, JSON, and CSS formatting
21
22
 
22
- Ruff and ty are installed as Python dependencies. Biome is managed automatically as a standalone binary (npm is not required).
23
+ Ruff and ty are installed as Python dependencies. oxlint and oxfmt are managed automatically as standalone binaries (npm is not required).
23
24
 
24
25
  ## Commands
25
26
 
@@ -58,10 +59,10 @@ You can skip specific tools if needed:
58
59
  plain code check --skip-ty
59
60
 
60
61
  # Only run type checks
61
- plain code check --skip-ruff --skip-biome
62
+ plain code check --skip-ruff --skip-oxc
62
63
 
63
- # Skip Biome checks
64
- plain code check --skip-biome
64
+ # Skip oxlint and oxfmt checks
65
+ plain code check --skip-oxc
65
66
 
66
67
  # Skip annotation coverage checks
67
68
  plain code check --skip-annotations
@@ -93,7 +94,7 @@ plain code annotations --json
93
94
 
94
95
  ## Settings
95
96
 
96
- Default configuration is provided by [`ruff_defaults.toml`](./ruff_defaults.toml) and [`biome_defaults.json`](./biome_defaults.json).
97
+ Default configuration is provided by [`ruff_defaults.toml`](./ruff_defaults.toml).
97
98
 
98
99
  You can customize the behavior in your `pyproject.toml`:
99
100
 
@@ -104,9 +105,9 @@ exclude = ["path/to/exclude"]
104
105
  [tool.plain.code.ty]
105
106
  enabled = true # Set to false to disable ty
106
107
 
107
- [tool.plain.code.biome]
108
- enabled = true # Set to false to disable Biome
109
- version = "1.5.3" # Pin to a specific version
108
+ [tool.plain.code.oxc]
109
+ enabled = true # Set to false to disable oxlint/oxfmt
110
+ version = "1.43.0" # Pin to a specific version
110
111
 
111
112
  [tool.plain.code.annotations]
112
113
  enabled = true # Set to false to disable annotation checks
@@ -115,16 +116,16 @@ exclude = ["migrations"] # Exclude specific patterns
115
116
 
116
117
  For more advanced configuration options, see [`get_code_config`](./cli.py#get_code_config).
117
118
 
118
- Generally you won't need to change the configuration. The defaults are designed to "just work" for most projects. If you find yourself needing extensive customization, consider using the underlying tools (Ruff, ty, Biome) directly instead.
119
+ Generally you won't need to change the configuration. The defaults are designed to "just work" for most projects. If you find yourself needing extensive customization, consider using the underlying tools (Ruff, ty, oxlint, oxfmt) directly instead.
119
120
 
120
121
  ## FAQs
121
122
 
122
- #### How do I install or update Biome manually?
123
+ #### How do I install or update oxlint/oxfmt manually?
123
124
 
124
- Biome is installed automatically when you run `plain fix` or `plain code check`. If you need to manage it manually:
125
+ oxlint and oxfmt are installed automatically when you run `plain fix` or `plain code check`. If you need to manage them manually:
125
126
 
126
127
  ```bash
127
- # Install Biome (or reinstall if corrupted)
128
+ # Install oxlint/oxfmt (or reinstall if corrupted)
128
129
  plain code install
129
130
 
130
131
  # Force reinstall even if up to date
@@ -11,7 +11,7 @@ paths:
11
11
  uv run plain fix [path]
12
12
  ```
13
13
 
14
- Automatically fixes formatting and linting issues using ruff and biome.
14
+ Automatically fixes formatting and linting issues using ruff and oxlint/oxfmt.
15
15
 
16
16
  Options:
17
17
 
@@ -24,7 +24,7 @@ Options:
24
24
  uv run plain code check [path]
25
25
  ```
26
26
 
27
- Runs ruff, ty (type checking), biome, and annotation coverage checks without auto-fixing.
27
+ Runs ruff, ty (type checking), oxlint/oxfmt, and annotation coverage checks without auto-fixing.
28
28
 
29
29
  ## Code Style
30
30
 
@@ -14,7 +14,7 @@ from plain.cli.print import print_event
14
14
  from plain.cli.runtime import common_command, without_runtime_setup
15
15
 
16
16
  from .annotations import AnnotationResult, check_annotations
17
- from .biome import Biome
17
+ from .oxc import OxcTool, install_oxc
18
18
 
19
19
  DEFAULT_RUFF_CONFIG = Path(__file__).parent / "ruff_defaults.toml"
20
20
 
@@ -32,45 +32,44 @@ def cli() -> None:
32
32
  @click.option("--force", is_flag=True, help="Reinstall even if up to date")
33
33
  @click.pass_context
34
34
  def install(ctx: click.Context, force: bool) -> None:
35
- """Install or update Biome binary"""
35
+ """Install or update oxlint and oxfmt binaries"""
36
36
  config = get_code_config()
37
37
 
38
- if not config.get("biome", {}).get("enabled", True):
39
- click.secho("Biome is disabled in configuration", fg="yellow")
38
+ if not config.get("oxc", {}).get("enabled", True):
39
+ click.secho("Oxc is disabled in configuration", fg="yellow")
40
40
  return
41
41
 
42
- biome = Biome()
42
+ oxlint = OxcTool("oxlint")
43
43
 
44
- if force or not biome.is_installed() or biome.needs_update():
45
- version_to_install = config.get("biome", {}).get("version", "")
44
+ if force or not oxlint.is_installed() or oxlint.needs_update():
45
+ version_to_install = config.get("oxc", {}).get("version", "")
46
46
  if version_to_install:
47
47
  click.secho(
48
- f"Installing Biome standalone version {version_to_install}...",
48
+ f"Installing oxlint and oxfmt {version_to_install}...",
49
49
  bold=True,
50
50
  nl=False,
51
51
  )
52
- installed = biome.install(version_to_install)
53
- click.secho(f"Biome {installed} installed", fg="green")
52
+ installed = install_oxc(version_to_install)
53
+ click.secho(f"oxlint and oxfmt {installed} installed", fg="green")
54
54
  else:
55
55
  ctx.invoke(update)
56
56
  else:
57
- click.secho("Biome already installed", fg="green")
57
+ click.secho("oxlint and oxfmt already installed", fg="green")
58
58
 
59
59
 
60
60
  @without_runtime_setup
61
61
  @cli.command()
62
62
  def update() -> None:
63
- """Update Biome to latest version"""
63
+ """Update oxlint and oxfmt to latest version"""
64
64
  config = get_code_config()
65
65
 
66
- if not config.get("biome", {}).get("enabled", True):
67
- click.secho("Biome is disabled in configuration", fg="yellow")
66
+ if not config.get("oxc", {}).get("enabled", True):
67
+ click.secho("Oxc is disabled in configuration", fg="yellow")
68
68
  return
69
69
 
70
- biome = Biome()
71
- click.secho("Updating Biome standalone...", bold=True)
72
- version = biome.install()
73
- click.secho(f"Biome {version} installed", fg="green")
70
+ click.secho("Updating oxlint and oxfmt...", bold=True)
71
+ version = install_oxc()
72
+ click.secho(f"oxlint and oxfmt {version} installed", fg="green")
74
73
 
75
74
 
76
75
  @without_runtime_setup
@@ -79,14 +78,14 @@ def update() -> None:
79
78
  @click.argument("path", default=".")
80
79
  @click.option("--skip-ruff", is_flag=True, help="Skip Ruff checks")
81
80
  @click.option("--skip-ty", is_flag=True, help="Skip ty type checks")
82
- @click.option("--skip-biome", is_flag=True, help="Skip Biome checks")
81
+ @click.option("--skip-oxc", is_flag=True, help="Skip oxlint and oxfmt checks")
83
82
  @click.option("--skip-annotations", is_flag=True, help="Skip type annotation checks")
84
83
  def check(
85
84
  ctx: click.Context,
86
85
  path: str,
87
86
  skip_ruff: bool,
88
87
  skip_ty: bool,
89
- skip_biome: bool,
88
+ skip_oxc: bool,
90
89
  skip_annotations: bool,
91
90
  ) -> None:
92
91
  """Check for formatting and linting issues"""
@@ -122,14 +121,19 @@ def check(
122
121
  result = subprocess.run(ty_args)
123
122
  maybe_exit(result.returncode)
124
123
 
125
- if not skip_biome and config.get("biome", {}).get("enabled", True):
126
- biome = Biome()
124
+ if not skip_oxc and config.get("oxc", {}).get("enabled", True):
125
+ oxlint = OxcTool("oxlint")
126
+ oxfmt = OxcTool("oxfmt")
127
127
 
128
- if biome.needs_update():
128
+ if oxlint.needs_update():
129
129
  ctx.invoke(install)
130
130
 
131
- print_event("biome check...", newline=False)
132
- result = biome.invoke("check", path)
131
+ print_event("oxlint...", newline=False)
132
+ result = oxlint.invoke(path)
133
+ maybe_exit(result.returncode)
134
+
135
+ print_event("oxfmt --check...", newline=False)
136
+ result = oxfmt.invoke("--check", path)
133
137
  maybe_exit(result.returncode)
134
138
 
135
139
  if not skip_annotations and config.get("annotations", {}).get("enabled", True):
@@ -276,21 +280,25 @@ def fix(ctx: click.Context, path: str, unsafe_fixes: bool, add_noqa: bool) -> No
276
280
  if result.returncode != 0:
277
281
  sys.exit(result.returncode)
278
282
 
279
- if config.get("biome", {}).get("enabled", True):
280
- biome = Biome()
283
+ if config.get("oxc", {}).get("enabled", True):
284
+ oxlint = OxcTool("oxlint")
285
+ oxfmt = OxcTool("oxfmt")
281
286
 
282
- if biome.needs_update():
287
+ if oxlint.needs_update():
283
288
  ctx.invoke(install)
284
289
 
285
- args = ["check", path, "--write"]
286
-
287
290
  if unsafe_fixes:
288
- args.append("--unsafe")
289
- print_event("biome check --write --unsafe...", newline=False)
291
+ print_event("oxlint --fix-dangerously...", newline=False)
292
+ result = oxlint.invoke(path, "--fix-dangerously")
290
293
  else:
291
- print_event("biome check --write...", newline=False)
294
+ print_event("oxlint --fix...", newline=False)
295
+ result = oxlint.invoke(path, "--fix")
296
+
297
+ if result.returncode != 0:
298
+ sys.exit(result.returncode)
292
299
 
293
- result = biome.invoke(*args)
300
+ print_event("oxfmt...", newline=False)
301
+ result = oxfmt.invoke(path)
294
302
 
295
303
  if result.returncode != 0:
296
304
  sys.exit(result.returncode)
@@ -0,0 +1,202 @@
1
+ """
2
+ Oxc standalone binary management for plain-code.
3
+
4
+ Downloads and manages oxlint (linter) and oxfmt (formatter) binaries
5
+ from the oxc-project/oxc GitHub releases.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import io
11
+ import os
12
+ import platform
13
+ import subprocess
14
+ import tarfile
15
+ import zipfile
16
+
17
+ import click
18
+ import requests
19
+ import tomlkit
20
+
21
+ from plain.runtime import PLAIN_TEMP_PATH
22
+
23
+ TAG_PREFIX = "apps_v"
24
+
25
+
26
+ class OxcTool:
27
+ """Download, install, and invoke an Oxc CLI binary (oxlint or oxfmt)."""
28
+
29
+ def __init__(self, name: str) -> None:
30
+ if name not in ("oxlint", "oxfmt"):
31
+ raise ValueError(f"Unknown Oxc tool: {name}")
32
+ self.name = name
33
+
34
+ @property
35
+ def target_directory(self) -> str:
36
+ return str(PLAIN_TEMP_PATH)
37
+
38
+ @property
39
+ def standalone_path(self) -> str:
40
+ exe = ".exe" if platform.system() == "Windows" else ""
41
+ return os.path.join(self.target_directory, f"{self.name}{exe}")
42
+
43
+ @property
44
+ def version_lockfile_path(self) -> str:
45
+ return os.path.join(self.target_directory, "oxc.version")
46
+
47
+ def is_installed(self) -> bool:
48
+ td = self.target_directory
49
+ if not os.path.isdir(td):
50
+ os.makedirs(td, exist_ok=True)
51
+ return os.path.exists(self.standalone_path)
52
+
53
+ def needs_update(self) -> bool:
54
+ if not self.is_installed():
55
+ return True
56
+ if not os.path.exists(self.version_lockfile_path):
57
+ return True
58
+ with open(self.version_lockfile_path) as f:
59
+ locked = f.read().strip()
60
+ return locked != self.get_version_from_config()
61
+
62
+ @staticmethod
63
+ def get_version_from_config() -> str:
64
+ project_root = os.path.dirname(str(PLAIN_TEMP_PATH))
65
+ pyproject = os.path.join(project_root, "pyproject.toml")
66
+ if not os.path.exists(pyproject):
67
+ return ""
68
+ doc = tomlkit.loads(open(pyproject, "rb").read().decode())
69
+ return (
70
+ doc.get("tool", {})
71
+ .get("plain", {})
72
+ .get("code", {})
73
+ .get("oxc", {})
74
+ .get("version", "")
75
+ )
76
+
77
+ @staticmethod
78
+ def set_version_in_config(version: str) -> None:
79
+ project_root = os.path.dirname(str(PLAIN_TEMP_PATH))
80
+ pyproject = os.path.join(project_root, "pyproject.toml")
81
+ if not os.path.exists(pyproject):
82
+ return
83
+ doc = tomlkit.loads(open(pyproject, "rb").read().decode())
84
+ doc.setdefault("tool", {}).setdefault("plain", {}).setdefault(
85
+ "code", {}
86
+ ).setdefault("oxc", {})["version"] = version
87
+ open(pyproject, "w").write(tomlkit.dumps(doc))
88
+
89
+ def detect_platform_slug(self) -> str:
90
+ system = platform.system()
91
+ arch = platform.machine()
92
+ if system == "Windows":
93
+ if arch.lower() in ("arm64", "aarch64"):
94
+ return "aarch64-pc-windows-msvc"
95
+ return "x86_64-pc-windows-msvc"
96
+ if system == "Linux":
97
+ if arch == "aarch64":
98
+ return "aarch64-unknown-linux-gnu"
99
+ return "x86_64-unknown-linux-gnu"
100
+ if system == "Darwin":
101
+ if arch == "arm64":
102
+ return "aarch64-apple-darwin"
103
+ return "x86_64-apple-darwin"
104
+ raise RuntimeError(f"Unsupported platform for Oxc: {system}/{arch}")
105
+
106
+ def download(self, version: str = "") -> str:
107
+ slug = self.detect_platform_slug()
108
+ is_windows = platform.system() == "Windows"
109
+ ext = "zip" if is_windows else "tar.gz"
110
+ asset = f"{self.name}-{slug}.{ext}"
111
+
112
+ if version:
113
+ url = f"https://github.com/oxc-project/oxc/releases/download/{TAG_PREFIX}{version}/{asset}"
114
+ else:
115
+ url = f"https://github.com/oxc-project/oxc/releases/latest/download/{asset}"
116
+
117
+ resp = requests.get(url, stream=True)
118
+ resp.raise_for_status()
119
+
120
+ td = self.target_directory
121
+ if not os.path.isdir(td):
122
+ os.makedirs(td, exist_ok=True)
123
+
124
+ # Download into memory for extraction
125
+ data = io.BytesIO()
126
+ total = int(resp.headers.get("Content-Length", 0))
127
+ if total:
128
+ with click.progressbar(
129
+ length=total,
130
+ label=f"Downloading {self.name}",
131
+ width=0,
132
+ ) as bar:
133
+ for chunk in resp.iter_content(chunk_size=1024 * 1024):
134
+ data.write(chunk)
135
+ bar.update(len(chunk))
136
+ else:
137
+ for chunk in resp.iter_content(chunk_size=1024 * 1024):
138
+ data.write(chunk)
139
+
140
+ data.seek(0)
141
+
142
+ # Extract the binary from the archive
143
+ if is_windows:
144
+ with zipfile.ZipFile(data) as zf:
145
+ # Find the binary inside the archive
146
+ members = zf.namelist()
147
+ binary_name = next(m for m in members if m.startswith(self.name))
148
+ with (
149
+ zf.open(binary_name) as src,
150
+ open(self.standalone_path, "wb") as dst,
151
+ ):
152
+ dst.write(src.read())
153
+ else:
154
+ with tarfile.open(fileobj=data, mode="r:gz") as tf:
155
+ members = tf.getnames()
156
+ binary_name = next(m for m in members if m.startswith(self.name))
157
+ extracted = tf.extractfile(binary_name)
158
+ if extracted is None:
159
+ raise RuntimeError(f"Failed to extract {binary_name} from archive")
160
+ with open(self.standalone_path, "wb") as dst:
161
+ dst.write(extracted.read())
162
+
163
+ os.chmod(self.standalone_path, 0o755)
164
+
165
+ # Determine resolved version for lockfile
166
+ if version:
167
+ resolved = version.lstrip("v")
168
+ else:
169
+ resolved = ""
170
+ if resp.history:
171
+ loc = resp.history[0].headers.get("Location", "")
172
+ if TAG_PREFIX in loc:
173
+ remaining = loc.split(TAG_PREFIX, 1)[-1]
174
+ resolved = remaining.split("/")[0]
175
+
176
+ if not resolved:
177
+ raise RuntimeError("Failed to determine resolved version from redirect")
178
+
179
+ return resolved
180
+
181
+ def invoke(self, *args: str, cwd: str | None = None) -> subprocess.CompletedProcess:
182
+ config_path = os.path.join(
183
+ os.path.dirname(__file__), f"{self.name}_defaults.json"
184
+ )
185
+ extra_args = ["-c", config_path]
186
+ return subprocess.run([self.standalone_path, *extra_args, *args], cwd=cwd)
187
+
188
+
189
+ def install_oxc(version: str = "") -> str:
190
+ """Install both oxlint and oxfmt, return the resolved version."""
191
+ oxlint = OxcTool("oxlint")
192
+ oxfmt = OxcTool("oxfmt")
193
+
194
+ resolved = oxlint.download(version)
195
+ oxfmt.download(resolved)
196
+
197
+ # Write version lockfile once (shared by both tools)
198
+ with open(oxlint.version_lockfile_path, "w") as f:
199
+ f.write(resolved)
200
+
201
+ OxcTool.set_version_in_config(resolved)
202
+ return resolved
@@ -0,0 +1,10 @@
1
+ {
2
+ "ignorePatterns": [
3
+ "**/vendor/**",
4
+ "**/node_modules/**",
5
+ "**/*.min.*",
6
+ "**/htmlcov/**",
7
+ "**/.venv/**",
8
+ "**/.pytest_cache/**"
9
+ ]
10
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "ignorePatterns": [
3
+ "**/vendor/**",
4
+ "**/node_modules/**",
5
+ "**/*.min.*",
6
+ "**/htmlcov/**",
7
+ "**/.venv/**",
8
+ "**/.pytest_cache/**"
9
+ ]
10
+ }
@@ -0,0 +1,23 @@
1
+ target-version = "py311"
2
+
3
+ [lint]
4
+ ignore = [
5
+ "E501", # Never enforce `E501` (line length violations)
6
+ "S101", # pytest use of assert
7
+ "ISC001", # Implicit string concatenation
8
+ ]
9
+ extend-select = [
10
+ "I", # isort
11
+ # # "C90", # mccabe
12
+ # # "N", # pep8-naming
13
+ "UP", # pyupgrade
14
+ # "S", # bandit
15
+ # # "B", # bugbear
16
+ "C4", # flake8-comprehensions
17
+ # # "DTZ", # flake8-datetimez
18
+ "ISC", # flake8-implicit-str-concat
19
+ # # "G", # flake8-logging-format
20
+ # # "T20", # print
21
+ "PT", # pytest
22
+ "B006", # mutable-argument-default
23
+ ]
@@ -1,18 +1,12 @@
1
1
  [project]
2
2
  name = "plain.code"
3
- version = "0.20.2"
3
+ version = "0.21.0"
4
4
  description = "Preconfigured code formatting and linting."
5
- authors = [{name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev"}]
5
+ authors = [{ name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev" }]
6
6
  readme = "README.md"
7
7
  license = "BSD-3-Clause"
8
8
  requires-python = ">=3.13"
9
- dependencies = [
10
- "plain<1.0.0",
11
- "ruff>=0.1.0",
12
- "ty>=0.0.11",
13
- "requests>=2.0.0",
14
- "tomlkit>=0.11.0",
15
- ]
9
+ dependencies = ["plain<1.0.0", "ruff>=0.1.0", "ty>=0.0.11", "requests>=2.0.0", "tomlkit>=0.11.0"]
16
10
 
17
11
  # Make this available as a standalone command
18
12
  # in case plain can't load or something (this can run anyways)
@@ -1,162 +0,0 @@
1
- """
2
- Biome standalone binary management for plain-code.
3
- """
4
-
5
- import os
6
- import platform
7
- import subprocess
8
-
9
- import click
10
- import requests
11
- import tomlkit
12
-
13
- from plain.internal import internalcode
14
- from plain.runtime import PLAIN_TEMP_PATH
15
-
16
-
17
- @internalcode
18
- class Biome:
19
- """Download, install, and invoke the Biome CLI standalone binary."""
20
-
21
- TAG_PREFIX = "@biomejs/biome@"
22
-
23
- @property
24
- def target_directory(self) -> str:
25
- # Directory under .plain to store the binary and lockfile
26
- return str(PLAIN_TEMP_PATH)
27
-
28
- @property
29
- def standalone_path(self) -> str:
30
- # On Windows, use .exe suffix
31
- exe = ".exe" if platform.system() == "Windows" else ""
32
- return os.path.join(self.target_directory, f"biome{exe}")
33
-
34
- @property
35
- def version_lockfile_path(self) -> str:
36
- return os.path.join(self.target_directory, "biome.version")
37
-
38
- def is_installed(self) -> bool:
39
- td = self.target_directory
40
- if not os.path.isdir(td):
41
- os.makedirs(td, exist_ok=True)
42
- return os.path.exists(self.standalone_path)
43
-
44
- def needs_update(self) -> bool:
45
- if not self.is_installed():
46
- return True
47
- if not os.path.exists(self.version_lockfile_path):
48
- return True
49
- with open(self.version_lockfile_path) as f:
50
- locked = f.read().strip()
51
- return locked != self.get_version_from_config()
52
-
53
- def get_version_from_config(self) -> str:
54
- # Read version from pyproject.toml under tool.plain.code.biome
55
- project_root = os.path.dirname(self.target_directory)
56
- pyproject = os.path.join(project_root, "pyproject.toml")
57
- if not os.path.exists(pyproject):
58
- return ""
59
- doc = tomlkit.loads(open(pyproject, "rb").read().decode())
60
- return (
61
- doc.get("tool", {})
62
- .get("plain", {})
63
- .get("code", {})
64
- .get("biome", {})
65
- .get("version", "")
66
- )
67
-
68
- def set_version_in_config(self, version: str) -> None:
69
- # Persist version to pyproject.toml under tool.plain.code.biome
70
- project_root = os.path.dirname(self.target_directory)
71
- pyproject = os.path.join(project_root, "pyproject.toml")
72
- if not os.path.exists(pyproject):
73
- return
74
- doc = tomlkit.loads(open(pyproject, "rb").read().decode())
75
- doc.setdefault("tool", {}).setdefault("plain", {}).setdefault(
76
- "code", {}
77
- ).setdefault("biome", {})["version"] = version
78
- open(pyproject, "w").write(tomlkit.dumps(doc))
79
-
80
- def detect_platform_slug(self) -> str:
81
- # Determine the asset slug for the current OS/arch
82
- system = platform.system()
83
- arch = platform.machine()
84
- if system == "Windows":
85
- # use win32 glibc build
86
- return "win32-arm64.exe" if arch.lower() == "arm64" else "win32-x64.exe"
87
- if system == "Linux":
88
- # prefer glibc builds
89
- return "linux-arm64" if arch == "aarch64" else "linux-x64"
90
- if system == "Darwin":
91
- return "darwin-arm64" if arch == "arm64" else "darwin-x64"
92
- raise RuntimeError(f"Unsupported platform for Biome: {system}/{arch}")
93
-
94
- def download(self, version: str = "") -> str:
95
- # Build download URL based on version (tag: cli/vX.Y.Z) or latest
96
- slug = self.detect_platform_slug()
97
- if version:
98
- url = (
99
- f"https://github.com/biomejs/biome/releases/download/{self.TAG_PREFIX}{version}/"
100
- f"biome-{slug}"
101
- )
102
- else:
103
- url = (
104
- f"https://github.com/biomejs/biome/releases/latest/download/"
105
- f"biome-{slug}"
106
- )
107
-
108
- resp = requests.get(url, stream=True)
109
- resp.raise_for_status()
110
-
111
- # Make sure the target directory exists
112
- td = self.target_directory
113
- if not os.path.isdir(td):
114
- os.makedirs(td, exist_ok=True)
115
-
116
- total = int(resp.headers.get("Content-Length", 0))
117
- with open(self.standalone_path, "wb") as f:
118
- if total:
119
- with click.progressbar(
120
- length=total,
121
- label="Downloading Biome",
122
- width=0,
123
- ) as bar:
124
- for chunk in resp.iter_content(chunk_size=1024 * 1024):
125
- f.write(chunk)
126
- bar.update(len(chunk))
127
- else:
128
- for chunk in resp.iter_content(chunk_size=1024 * 1024):
129
- f.write(chunk)
130
- os.chmod(self.standalone_path, 0o755)
131
-
132
- # Determine resolved version for lockfile
133
- if version:
134
- resolved = version.lstrip("v")
135
- else:
136
- resolved = ""
137
- if resp.history:
138
- # Look for redirect to actual tag version
139
- loc = resp.history[0].headers.get("Location", "")
140
- if self.TAG_PREFIX in loc:
141
- remaining = loc.split(self.TAG_PREFIX, 1)[-1]
142
- resolved = remaining.split("/")[0]
143
-
144
- if not resolved:
145
- raise RuntimeError("Failed to determine resolved version from redirect")
146
-
147
- open(self.version_lockfile_path, "w").write(resolved)
148
-
149
- return resolved
150
-
151
- def install(self, version: str = "") -> str:
152
- v = self.download(version)
153
- self.set_version_in_config(v)
154
- return v
155
-
156
- def invoke(self, *args: str, cwd: str | None = None) -> subprocess.CompletedProcess:
157
- # Run the standalone biome binary with given args
158
- config_path = os.path.abspath(
159
- os.path.join(os.path.dirname(__file__), "biome_defaults.json")
160
- )
161
- args = list(args) + ["--config-path", config_path, "--vcs-root", os.getcwd()]
162
- return subprocess.run([self.standalone_path, *args], cwd=cwd)
@@ -1,23 +0,0 @@
1
- {
2
- "root": true,
3
- "vcs": {
4
- "enabled": true,
5
- "clientKind": "git",
6
- "useIgnoreFile": true
7
- },
8
- "files": {
9
- "includes": [
10
- "**",
11
- "!**/vendor/**",
12
- "!**/node_modules/**",
13
- "!**/*.min.*",
14
- "!**/tests/**",
15
- "!**/htmlcov/**",
16
- "!**/.venv/**",
17
- "!**/.pytest_cache/**"
18
- ]
19
- },
20
- "formatter": {
21
- "indentStyle": "space"
22
- }
23
- }
@@ -1,23 +0,0 @@
1
- target-version = "py311"
2
-
3
- [lint]
4
- ignore = [
5
- "E501", # Never enforce `E501` (line length violations)
6
- "S101", # pytest use of assert
7
- "ISC001", # Implicit string concatenation
8
- ]
9
- extend-select = [
10
- "I", # isort
11
- # # "C90", # mccabe
12
- # # "N", # pep8-naming
13
- "UP", # pyupgrade
14
- # "S", # bandit
15
- # # "B", # bugbear
16
- "C4", # flake8-comprehensions
17
- # # "DTZ", # flake8-datetimez
18
- "ISC", # flake8-implicit-str-concat
19
- # # "G", # flake8-logging-format
20
- # # "T20", # print
21
- "PT", # pytest
22
- "B006", # mutable-argument-default
23
- ]
File without changes
File without changes
File without changes