Skip to content

Commit d7b33ab

Browse files
committed
feat(detect): recognize oxlint alongside eslint, biome and stylelint
1 parent 2514699 commit d7b33ab

7 files changed

Lines changed: 171 additions & 1 deletion

File tree

‎.changeset/detect-oxlint.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@taskless/cli": patch
3+
---
4+
5+
`taskless detect` recognizes oxlint alongside eslint, biome and stylelint, from `.oxlintrc.json`, `.oxlintrc.jsonc`, `oxlint.config.ts`, `oxlint.config.mts`, or an `oxlint` entry in `package.json`.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
## Why
2+
3+
`taskless detect` recognizes eslint, biome and stylelint for JavaScript and TypeScript, but not oxlint. A repo that has moved to oxlint reports no JS linter, so `detect` output and everything downstream that reads it (`init`, the `route` recipe) treats the project as unlinted. oxlint is where a growing share of JS repos are heading for the static tier, and its plugin architecture cannot do cross-file rules, which makes an oxlint repo exactly the kind of project where Taskless runtime rules add something the linter cannot. Detect needs to see it.
4+
5+
## What Changes
6+
7+
- A new `oxlint` entry in `LINTER_SIGNALS` (`packages/cli/src/detect/scan.ts`), shaped like `biome`: languages JavaScript + TypeScript, config files `.oxlintrc.json`, `.oxlintrc.jsonc`, `oxlint.config.ts`, `oxlint.config.mts`, dependency `oxlint` matched against `package.json` only.
8+
- The config list is the set oxlint auto-discovers per https://oxc.rs/docs/guide/usage/linter/config.html ("Oxlint automatically looks for a `.oxlintrc.json`, `.oxlintrc.jsonc`, `oxlint.config.ts`, or `oxlint.config.mts` in the current working directory"). `oxlint.config.{js,mjs,cjs}` are accepted only through an explicit `-c` flag and are deliberately excluded.
9+
- The `cli-detect` spec's example list for "Linter configs are detected from disk" names `.oxlintrc.json`, and a scenario covers oxlint from either signal.
10+
- Tests: `.oxlintrc.json` alone detects oxlint; `oxlint` only in `devDependencies` detects oxlint; an eslint-only repo does not report oxlint.
11+
12+
Nothing here is **BREAKING**. Pre-1.0, an added linter signal is a `patch`.
13+
14+
## Capabilities
15+
16+
### New Capabilities
17+
18+
None.
19+
20+
### Modified Capabilities
21+
22+
- `cli-detect`: the "Detect scans deterministic repo signals only" requirement's example config list gains `.oxlintrc.json`, and a new scenario states that oxlint is detected from its config file or its `package.json` dependency.
23+
24+
## Impact
25+
26+
- `packages/cli/src/detect/scan.ts`: one new `LinterSignal` entry.
27+
- `packages/cli/test/detect.test.ts`: three new cases.
28+
- `packages/cli/src/agent/detect.md`: untouched; it does not enumerate the JS linters.
29+
30+
## Delivery shape
31+
32+
**Single PR.** The change is one signal entry, its tests, the spec delta and the archive; it fits one reviewable diff and is safe in production on its own.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
## MODIFIED Requirements
2+
3+
### Requirement: Detect scans deterministic repo signals only
4+
5+
The `detect` command SHALL emit only deterministic signals derived from files on
6+
disk: configured linters, detected languages, and the styles of the repo's own
7+
existing rules. It SHALL NOT perform any LLM inference and SHALL NOT match the
8+
request against any catalog of known packaged linter rules.
9+
10+
Detection follows a languages → linters flow: languages are inferred first, and
11+
a linter's dependency evidence is then read from the manifest of that linter's
12+
own language (a node dependency from `package.json`, a Python dependency from
13+
`pyproject.toml`/`requirements.txt`) rather than conflating ecosystems. A
14+
recognized linter config file on disk is honored regardless of the languages
15+
inferred.
16+
17+
#### Scenario: Linter configs are detected from disk
18+
19+
- **WHEN** the working directory contains a recognized linter config (for
20+
example `.eslintrc*`, `eslint.config.js`, `ruff.toml`, a `[tool.ruff]` block in
21+
`pyproject.toml`, `.rubocop.yml`, `biome.json`, `.oxlintrc.json`, or
22+
`stylelint` config)
23+
- **THEN** `detect --json` SHALL report each configured linter it found
24+
25+
#### Scenario: Languages are reported
26+
27+
- **WHEN** `detect --json` runs in a repository
28+
- **THEN** the output SHALL include the languages inferred from manifest and
29+
marker files present on disk and from the linters detected
30+
31+
#### Scenario: A linter dependency is sourced from its own language's manifest
32+
33+
- **WHEN** a dependency-evidenced linter (for example `ruff`) is named only in a
34+
manifest belonging to a different language (for example `package.json`)
35+
- **THEN** `detect --json` SHALL NOT report that linter from the mismatched
36+
manifest
37+
38+
#### Scenario: oxlint is detected from its config or its dependency
39+
40+
- **WHEN** the working directory contains one of the config files oxlint
41+
discovers on its own (`.oxlintrc.json`, `.oxlintrc.jsonc`, `oxlint.config.ts`,
42+
`oxlint.config.mts`), or names `oxlint` as a dependency in `package.json`
43+
- **THEN** `detect --json` SHALL report `oxlint` as a JavaScript/TypeScript linter
44+
- **AND** a repository configured only for eslint SHALL NOT report `oxlint`
45+
46+
#### Scenario: Configs in monorepo sub-packages are detected
47+
48+
- **WHEN** a linter config or language manifest lives in a sub-package rather
49+
than the repository root (for example `packages/api/.eslintrc.json`)
50+
- **THEN** `detect --json` SHALL detect it and SHALL carry the path it was found
51+
at in the linter's evidence
52+
- **AND** the scan SHALL prune a curated set of ignored directories (for example
53+
`node_modules`, `.git`, build output) and SHALL bound traversal depth
54+
55+
#### Scenario: The repo's own rule styles are surfaced
56+
57+
- **WHEN** the working directory contains existing rule definitions (for example
58+
custom linter rules or `.taskless/rules/`)
59+
- **THEN** `detect --json` SHALL surface a description of those existing rule
60+
styles for downstream authoring
61+
62+
#### Scenario: No packaged-rule catalog matching
63+
64+
- **WHEN** `detect --json` runs
65+
- **THEN** the output SHALL NOT claim a request maps to a specific named packaged
66+
rule (such matching is left to the authoring recipe, not the command)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
## 1. Implementation
2+
3+
- [x] 1.1 Verify against https://oxc.rs/docs/guide/usage/linter/config.html which config filenames oxlint discovers on its own; exclude any `oxlint.config.*` variant that is only reachable through `-c`.
4+
- [x] 1.2 Add the `oxlint` entry to `LINTER_SIGNALS` in `packages/cli/src/detect/scan.ts`, shaped like `biome`, with a comment naming the excluded filenames and why.
5+
- [x] 1.3 Add tests in `packages/cli/test/detect.test.ts`: `.oxlintrc.json` alone detects oxlint; `oxlint` in `devDependencies` alone detects oxlint; an eslint-only repo does not report oxlint.
6+
- [x] 1.4 Add `.changeset/detect-oxlint.md` (`patch`).
7+
8+
## 2. Spec
9+
10+
- [x] 2.1 Write the `cli-detect` delta restating the full "Detect scans deterministic repo signals only" requirement with every existing scenario, the updated example list, and the new oxlint scenario.
11+
- [x] 2.2 `pnpm openspec validate --strict`, then the dry-run archive check from CLAUDE.md (scenario count before vs after), then archive for real.
12+
13+
## 3. Verification
14+
15+
- [x] 3.1 `pnpm build`, `pnpm typecheck`, `pnpm lint`, `pnpm --filter @taskless/cli test` all pass.

‎openspec/specs/cli-detect/spec.md‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ inferred.
3636

3737
- **WHEN** the working directory contains a recognized linter config (for
3838
example `.eslintrc*`, `eslint.config.js`, `ruff.toml`, a `[tool.ruff]` block in
39-
`pyproject.toml`, `.rubocop.yml`, `biome.json`, or `stylelint` config)
39+
`pyproject.toml`, `.rubocop.yml`, `biome.json`, `.oxlintrc.json`, or
40+
`stylelint` config)
4041
- **THEN** `detect --json` SHALL report each configured linter it found
4142

4243
#### Scenario: Languages are reported
@@ -52,6 +53,14 @@ inferred.
5253
- **THEN** `detect --json` SHALL NOT report that linter from the mismatched
5354
manifest
5455

56+
#### Scenario: oxlint is detected from its config or its dependency
57+
58+
- **WHEN** the working directory contains one of the config files oxlint
59+
discovers on its own (`.oxlintrc.json`, `.oxlintrc.jsonc`, `oxlint.config.ts`,
60+
`oxlint.config.mts`), or names `oxlint` as a dependency in `package.json`
61+
- **THEN** `detect --json` SHALL report `oxlint` as a JavaScript/TypeScript linter
62+
- **AND** a repository configured only for eslint SHALL NOT report `oxlint`
63+
5564
#### Scenario: Configs in monorepo sub-packages are detected
5665

5766
- **WHEN** a linter config or language manifest lives in a sub-package rather

‎packages/cli/src/detect/scan.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@ const LINTER_SIGNALS: readonly LinterSignal[] = [
120120
configFiles: ["biome.json", "biome.jsonc"],
121121
deps: ["@biomejs/biome"],
122122
},
123+
{
124+
name: "oxlint",
125+
languages: ["JavaScript", "TypeScript"],
126+
// The four names oxlint discovers on its own (oxc.rs, "Configuration").
127+
// `oxlint.config.{js,mjs,cjs}` are accepted only via `-c`, never found by
128+
// the walk, so listing them would be dead weight in the sweep.
129+
configFiles: [
130+
".oxlintrc.json",
131+
".oxlintrc.jsonc",
132+
"oxlint.config.ts",
133+
"oxlint.config.mts",
134+
],
135+
deps: ["oxlint"],
136+
},
123137
{
124138
name: "stylelint",
125139
languages: ["JavaScript", "TypeScript"],

‎packages/cli/test/detect.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,35 @@ describe("taskless detect", () => {
127127
expect(linterNames(result)).toContain("biome");
128128
});
129129

130+
it("detects oxlint from .oxlintrc.json", async () => {
131+
await writeFile(join(cwd, ".oxlintrc.json"), "{}", "utf8");
132+
const result = await detect(cwd);
133+
expect(linterNames(result)).toContain("oxlint");
134+
expect(linterNames(result)).not.toContain("eslint");
135+
});
136+
137+
it("detects oxlint from a package.json devDependency", async () => {
138+
await writeFile(
139+
join(cwd, "package.json"),
140+
JSON.stringify({ devDependencies: { oxlint: "^1.0.0" } }),
141+
"utf8"
142+
);
143+
const result = await detect(cwd);
144+
expect(linterNames(result)).toContain("oxlint");
145+
});
146+
147+
it("does not report oxlint for an eslint-only repo", async () => {
148+
await writeFile(join(cwd, ".eslintrc.json"), "{}", "utf8");
149+
await writeFile(
150+
join(cwd, "package.json"),
151+
JSON.stringify({ devDependencies: { eslint: "^9.0.0" } }),
152+
"utf8"
153+
);
154+
const result = await detect(cwd);
155+
expect(linterNames(result)).toContain("eslint");
156+
expect(linterNames(result)).not.toContain("oxlint");
157+
});
158+
130159
it("detects stylelint from a package.json devDependency", async () => {
131160
await writeFile(
132161
join(cwd, "package.json"),

0 commit comments

Comments
 (0)