@jacob-z/oxlint-gate 1.0.0 → 1.1.0
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.
- package/package.json +3 -3
- package/src/index.ts +291 -81
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jacob-z/oxlint-gate",
|
|
3
|
-
"version": "1.0.0",
|
|
4
3
|
"type": "module",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
4
|
+
"version": "1.1.0",
|
|
5
|
+
"description": "Real-time lint quality gate for OMP — auto-detects ESLint ≥ 9 or falls back to oxlint",
|
|
7
6
|
"author": { "name": "Jacob" },
|
|
7
|
+
"license": "MIT",
|
|
8
8
|
"repository": { "type": "git", "url": "https://github.com/JacobZyy/jacob-omp-collections", "directory": "packages/oxlint-gate" },
|
|
9
9
|
"omp": {
|
|
10
10
|
"extensions": ["./src/index.ts"]
|
package/src/index.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* lint-gate: Real-time lint quality gate for OMP.
|
|
3
3
|
*
|
|
4
|
-
* Intercepts Edit/Write tool calls and checks the target file
|
|
5
|
-
*
|
|
6
|
-
* (e.g
|
|
4
|
+
* Intercepts Edit/Write tool calls and checks the target file after save.
|
|
5
|
+
* Strategy is auto-detected per project:
|
|
6
|
+
* - ESLint ≥ 9 (e.g. antfu config) → project's own eslint --fix
|
|
7
|
+
* - Otherwise → global oxlint as fallback
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
* Logs:
|
|
9
|
+
* Cache: `.omp/lint-strategy.json` in project root.
|
|
10
|
+
* Logs: `~/.omp/logs/oxlint-gate.log`
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
import type { ExtensionAPI, ExtensionFactory } from './omp-types'
|
|
13
|
+
import type { ExtensionAPI, ExtensionFactory, ToolResultEventResult } from './omp-types'
|
|
13
14
|
import { spawnSync } from 'node:child_process'
|
|
14
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from 'node:fs'
|
|
15
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
15
16
|
import { homedir } from 'node:os'
|
|
16
17
|
import { isAbsolute, join, relative, resolve } from 'node:path'
|
|
17
18
|
import process from 'node:process'
|
|
@@ -28,7 +29,7 @@ const WRITE_TOOLS = new Set(['edit', 'write'])
|
|
|
28
29
|
/** Max auto-fix attempts per file per turn. */
|
|
29
30
|
const MAX_FIX_ATTEMPTS = 3
|
|
30
31
|
|
|
31
|
-
/** Max lines of
|
|
32
|
+
/** Max lines of lint output to keep. */
|
|
32
33
|
const MAX_OUTPUT_LINES = 20
|
|
33
34
|
|
|
34
35
|
// ── Types ──────────────────────────────────────────────────────────────────
|
|
@@ -37,6 +38,14 @@ interface OxlintConfig {
|
|
|
37
38
|
ignorePatterns?: string[]
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
type LintStrategy = 'eslint' | 'oxlint'
|
|
42
|
+
|
|
43
|
+
interface LintStrategyCache {
|
|
44
|
+
strategy: LintStrategy
|
|
45
|
+
eslintVersion?: string
|
|
46
|
+
sniffedAt: string
|
|
47
|
+
}
|
|
48
|
+
|
|
40
49
|
// ── Local Logger ───────────────────────────────────────────────────────────
|
|
41
50
|
|
|
42
51
|
function ensureLogDir(): void {
|
|
@@ -58,9 +67,6 @@ function writeLog(level: 'INFO' | 'WARN' | 'DEBUG', msg: string): void {
|
|
|
58
67
|
|
|
59
68
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
60
69
|
|
|
61
|
-
/**
|
|
62
|
-
* Expand ~ to home directory
|
|
63
|
-
*/
|
|
64
70
|
function expandTilde(p: string): string {
|
|
65
71
|
if (p === '~' || p.startsWith('~/')) {
|
|
66
72
|
return join(HOME, p.slice(1))
|
|
@@ -121,7 +127,6 @@ function matchesIgnorePattern(filePath: string, patterns: string[]): boolean {
|
|
|
121
127
|
const rel = relative(process.cwd(), filePath)
|
|
122
128
|
const candidates = [filePath, rel, `./${rel}`]
|
|
123
129
|
|
|
124
|
-
// Simple glob matching without Bun.Glob (for Node.js compatibility)
|
|
125
130
|
for (const pattern of patterns) {
|
|
126
131
|
const regex = globToRegex(pattern)
|
|
127
132
|
if (regex) {
|
|
@@ -136,13 +141,12 @@ function matchesIgnorePattern(filePath: string, patterns: string[]): boolean {
|
|
|
136
141
|
|
|
137
142
|
function globToRegex(glob: string): RegExp | null {
|
|
138
143
|
try {
|
|
139
|
-
// Convert glob to regex
|
|
140
144
|
const regexStr = glob
|
|
141
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
142
|
-
.replace(/\*\*/g, '{{DOUBLE_STAR}}')
|
|
143
|
-
.replace(/\*/g, '[^/]*')
|
|
144
|
-
.replace(/\?/g, '[^/]')
|
|
145
|
-
.replace(/\{\{DOUBLE_STAR\}\}/g, '.*')
|
|
145
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
146
|
+
.replace(/\*\*/g, '{{DOUBLE_STAR}}')
|
|
147
|
+
.replace(/\*/g, '[^/]*')
|
|
148
|
+
.replace(/\?/g, '[^/]')
|
|
149
|
+
.replace(/\{\{DOUBLE_STAR\}\}/g, '.*')
|
|
146
150
|
|
|
147
151
|
return new RegExp(`^${regexStr}$`)
|
|
148
152
|
}
|
|
@@ -151,22 +155,29 @@ function globToRegex(glob: string): RegExp | null {
|
|
|
151
155
|
}
|
|
152
156
|
}
|
|
153
157
|
|
|
158
|
+
function truncateOutput(output: string, maxLines: number = MAX_OUTPUT_LINES): string {
|
|
159
|
+
const lines = output.split('\n')
|
|
160
|
+
if (lines.length <= maxLines)
|
|
161
|
+
return output
|
|
162
|
+
const head = lines.slice(0, 10).join('\n')
|
|
163
|
+
const summary = lines.slice(-5).join('\n')
|
|
164
|
+
return `${head}\n\n... (${lines.length - 15} lines truncated) ...\n\n${summary}`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Oxlint runner ─────────────────────────────────────────────────────
|
|
168
|
+
|
|
154
169
|
function runOxlint(filePath: string, cfgPath: string): { passed: boolean, output: string } {
|
|
155
170
|
const result = spawnSync('oxlint', ['-c', cfgPath, filePath], {
|
|
156
171
|
encoding: 'utf8',
|
|
157
172
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
158
|
-
timeout: 5000,
|
|
173
|
+
timeout: 5000,
|
|
159
174
|
})
|
|
160
175
|
|
|
161
|
-
if (result.error)
|
|
162
|
-
// oxlint not found or spawn error — treat as pass (fail-open)
|
|
176
|
+
if (result.error)
|
|
163
177
|
return { passed: true, output: `oxlint error: ${result.error.message}` }
|
|
164
|
-
}
|
|
165
178
|
|
|
166
179
|
const exitCode = result.status ?? -1
|
|
167
180
|
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim()
|
|
168
|
-
|
|
169
|
-
// exit 0 = pass, exit 1 = violations found, other = tool error (fail-open)
|
|
170
181
|
return { passed: exitCode !== 1, output }
|
|
171
182
|
}
|
|
172
183
|
|
|
@@ -177,35 +188,272 @@ function runOxlintFix(filePath: string, cfgPath: string): { fixed: boolean, rema
|
|
|
177
188
|
timeout: 10000,
|
|
178
189
|
})
|
|
179
190
|
|
|
180
|
-
if (result.error)
|
|
191
|
+
if (result.error)
|
|
181
192
|
return { fixed: false, remaining: -1, output: `oxlint error: ${result.error.message}` }
|
|
182
|
-
}
|
|
183
193
|
|
|
184
194
|
const exitCode = result.status ?? -1
|
|
185
195
|
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim()
|
|
186
|
-
|
|
187
|
-
// exit 0 = all fixed, exit 1 = remaining violations
|
|
188
196
|
return { fixed: exitCode === 0, remaining: exitCode === 1 ? 1 : 0, output }
|
|
189
197
|
}
|
|
190
198
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
199
|
+
// ── ESLint runner ─────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
function runEslintCheck(filePath: string, cwd: string): { passed: boolean, output: string } {
|
|
202
|
+
const result = spawnSync('npx', ['eslint', '--no-warn-ignored', filePath], {
|
|
203
|
+
encoding: 'utf8',
|
|
204
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
205
|
+
timeout: 10000,
|
|
206
|
+
cwd,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
if (result.error)
|
|
210
|
+
return { passed: true, output: `eslint error: ${result.error.message}` }
|
|
211
|
+
|
|
212
|
+
const exitCode = result.status ?? -1
|
|
213
|
+
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim()
|
|
214
|
+
return { passed: exitCode !== 1, output }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function runEslintFix(filePath: string, cwd: string): { fixed: boolean, output: string } {
|
|
218
|
+
// Try project's lint:fix script first, fall back to direct npx eslint --fix
|
|
219
|
+
const result = spawnSync('npx', ['eslint', '--fix', filePath], {
|
|
220
|
+
encoding: 'utf8',
|
|
221
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
222
|
+
timeout: 15000,
|
|
223
|
+
cwd,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
if (result.error)
|
|
227
|
+
return { fixed: false, output: `eslint error: ${result.error.message}` }
|
|
228
|
+
|
|
229
|
+
const exitCode = result.status ?? -1
|
|
230
|
+
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim()
|
|
231
|
+
// eslint: 0 = clean, 1 = lint errors remain, 2 = fatal
|
|
232
|
+
return { fixed: exitCode === 0, output }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Strategy sniffing ─────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
/** In-memory strategy cache, keyed by project cwd. */
|
|
238
|
+
const strategyCache = new Map<string, LintStrategyCache>()
|
|
239
|
+
|
|
240
|
+
function getCachePath(cwd: string): string {
|
|
241
|
+
return join(cwd, '.omp', 'lint-strategy.json')
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Read eslint version from package.json (dependencies + devDependencies).
|
|
246
|
+
* Returns the version string or undefined.
|
|
247
|
+
*/
|
|
248
|
+
function sniffEslintVersion(cwd: string): string | undefined {
|
|
249
|
+
try {
|
|
250
|
+
const pkgPath = join(cwd, 'package.json')
|
|
251
|
+
if (!existsSync(pkgPath))
|
|
252
|
+
return undefined
|
|
253
|
+
|
|
254
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as Record<string, unknown>
|
|
255
|
+
const deps = {
|
|
256
|
+
...(pkg.devDependencies as Record<string, string> | undefined),
|
|
257
|
+
...(pkg.dependencies as Record<string, string> | undefined),
|
|
258
|
+
}
|
|
259
|
+
const raw = deps['eslint']
|
|
260
|
+
if (!raw)
|
|
261
|
+
return undefined
|
|
262
|
+
|
|
263
|
+
// Strip workspace/caret/tilde prefixes: "^9.1.0" → "9.1.0"
|
|
264
|
+
const clean = raw.replace(/^[^0-9]*/, '')
|
|
265
|
+
return clean || undefined
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return undefined
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function isEslintGte(version: string, major: number): boolean {
|
|
273
|
+
const parts = version.split('.')
|
|
274
|
+
const v = Number.parseInt(parts[0] ?? '0', 10)
|
|
275
|
+
return !Number.isNaN(v) && v >= major
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Sniff lint strategy for a project and persist to .omp/lint-strategy.json.
|
|
280
|
+
*/
|
|
281
|
+
function sniffStrategy(cwd: string): LintStrategyCache {
|
|
282
|
+
const eslintVersion = sniffEslintVersion(cwd)
|
|
283
|
+
const strategy: LintStrategy = eslintVersion && isEslintGte(eslintVersion, 9) ? 'eslint' : 'oxlint'
|
|
284
|
+
|
|
285
|
+
const cache: LintStrategyCache = {
|
|
286
|
+
strategy,
|
|
287
|
+
eslintVersion,
|
|
288
|
+
sniffedAt: new Date().toISOString(),
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Persist
|
|
292
|
+
try {
|
|
293
|
+
const cacheDir = join(cwd, '.omp')
|
|
294
|
+
if (!existsSync(cacheDir))
|
|
295
|
+
mkdirSync(cacheDir, { recursive: true })
|
|
296
|
+
writeFileSync(getCachePath(cwd), JSON.stringify(cache, null, 2))
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// Non-critical; we'll re-sniff next session
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
writeLog('INFO', `sniffed strategy for ${cwd}: ${strategy}${eslintVersion ? ` (eslint@${eslintVersion})` : ''}`)
|
|
303
|
+
return cache
|
|
198
304
|
}
|
|
199
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Load strategy from cache or sniff fresh.
|
|
308
|
+
*/
|
|
309
|
+
function loadStrategy(cwd: string): LintStrategyCache {
|
|
310
|
+
const cached = strategyCache.get(cwd)
|
|
311
|
+
if (cached)
|
|
312
|
+
return cached
|
|
313
|
+
|
|
314
|
+
// Try reading persisted cache
|
|
315
|
+
const cachePath = getCachePath(cwd)
|
|
316
|
+
if (existsSync(cachePath)) {
|
|
317
|
+
try {
|
|
318
|
+
const data = JSON.parse(readFileSync(cachePath, 'utf8')) as LintStrategyCache
|
|
319
|
+
if (data.strategy === 'eslint' || data.strategy === 'oxlint') {
|
|
320
|
+
strategyCache.set(cwd, data)
|
|
321
|
+
return data
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
// Corrupt cache; re-sniff
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const result = sniffStrategy(cwd)
|
|
330
|
+
strategyCache.set(cwd, result)
|
|
331
|
+
return result
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── Extension state ───────────────────────────────────────────────────
|
|
335
|
+
|
|
200
336
|
const pendingPaths = new Map<string, { toolName: string, timestamp: number }>()
|
|
201
337
|
const fixCounters = new Map<string, number>()
|
|
202
338
|
|
|
339
|
+
// ── Strategy handlers ─────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
function handleEslintStrategy(
|
|
342
|
+
pi: ExtensionAPI,
|
|
343
|
+
filePath: string,
|
|
344
|
+
cwd: string,
|
|
345
|
+
fixCount: number,
|
|
346
|
+
log: ExtensionAPI['logger'],
|
|
347
|
+
): undefined | ToolResultEventResult {
|
|
348
|
+
// 1. Check first
|
|
349
|
+
const { passed } = runEslintCheck(filePath, cwd)
|
|
350
|
+
if (passed) {
|
|
351
|
+
log.info(`[lint-gate] eslint passed: ${filePath}`)
|
|
352
|
+
writeLog('INFO', `eslint passed: ${filePath}`)
|
|
353
|
+
fixCounters.delete(filePath)
|
|
354
|
+
return undefined
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
log.warn(`[lint-gate] eslint violations in ${filePath}, attempting auto-fix`)
|
|
358
|
+
writeLog('WARN', `eslint violations in ${filePath}, attempting auto-fix`)
|
|
359
|
+
|
|
360
|
+
// 2. Auto-fix
|
|
361
|
+
fixCounters.set(filePath, fixCount + 1)
|
|
362
|
+
const { fixed, output } = runEslintFix(filePath, cwd)
|
|
363
|
+
|
|
364
|
+
if (fixed) {
|
|
365
|
+
log.info(`[lint-gate] eslint auto-fixed: ${filePath}`)
|
|
366
|
+
writeLog('INFO', `eslint auto-fixed: ${filePath}`)
|
|
367
|
+
return {
|
|
368
|
+
content: [{ type: 'text', text: `✅ [lint-gate] eslint auto-fixed ${filePath}` }],
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 3. Remaining issues
|
|
373
|
+
const remaining = truncateOutput(output)
|
|
374
|
+
log.warn(`[lint-gate] eslint partial fix in ${filePath}`)
|
|
375
|
+
writeLog('WARN', `eslint partial fix in ${filePath}`)
|
|
376
|
+
|
|
377
|
+
pi.sendMessage(
|
|
378
|
+
{
|
|
379
|
+
customType: 'lint-gate',
|
|
380
|
+
content: `⚠️ [lint-gate] ${filePath} has remaining eslint issues after auto-fix:\n\n${remaining}`,
|
|
381
|
+
display: true,
|
|
382
|
+
attribution: 'agent',
|
|
383
|
+
},
|
|
384
|
+
{ triggerTurn: false },
|
|
385
|
+
)
|
|
386
|
+
return undefined
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function handleOxlintStrategy(
|
|
390
|
+
pi: ExtensionAPI,
|
|
391
|
+
filePath: string,
|
|
392
|
+
fixCount: number,
|
|
393
|
+
log: ExtensionAPI['logger'],
|
|
394
|
+
): undefined | ToolResultEventResult {
|
|
395
|
+
if (!existsSync(OXLINT_CFG))
|
|
396
|
+
return undefined
|
|
397
|
+
|
|
398
|
+
const ignorePatterns = loadIgnorePatterns(OXLINT_CFG)
|
|
399
|
+
if (matchesIgnorePattern(filePath, ignorePatterns))
|
|
400
|
+
return undefined
|
|
401
|
+
|
|
402
|
+
// 1. Check for violations
|
|
403
|
+
const { passed } = runOxlint(filePath, OXLINT_CFG)
|
|
404
|
+
if (passed) {
|
|
405
|
+
log.info(`[lint-gate] oxlint passed: ${filePath}`)
|
|
406
|
+
writeLog('INFO', `oxlint passed: ${filePath}`)
|
|
407
|
+
fixCounters.delete(filePath)
|
|
408
|
+
return undefined
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
log.warn(`[lint-gate] oxlint violations in ${filePath}, attempting auto-fix`)
|
|
412
|
+
writeLog('WARN', `oxlint violations in ${filePath}, attempting auto-fix`)
|
|
413
|
+
|
|
414
|
+
// 2. Try auto-fix
|
|
415
|
+
fixCounters.set(filePath, fixCount + 1)
|
|
416
|
+
const fixResult = runOxlintFix(filePath, OXLINT_CFG)
|
|
417
|
+
|
|
418
|
+
if (fixResult.fixed) {
|
|
419
|
+
log.info(`[lint-gate] oxlint auto-fixed: ${filePath}`)
|
|
420
|
+
writeLog('INFO', `oxlint auto-fixed: ${filePath}`)
|
|
421
|
+
return {
|
|
422
|
+
content: [{ type: 'text', text: `✅ [lint-gate] auto-fixed lint issues in ${filePath}` }],
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// 3. Some violations remain — report to LLM
|
|
427
|
+
const remaining = truncateOutput(fixResult.output)
|
|
428
|
+
log.warn(`[lint-gate] oxlint partial fix in ${filePath}, remaining issues`)
|
|
429
|
+
writeLog('WARN', `oxlint partial fix in ${filePath}`)
|
|
430
|
+
|
|
431
|
+
pi.sendMessage(
|
|
432
|
+
{
|
|
433
|
+
customType: 'lint-gate',
|
|
434
|
+
content: `⚠️ [lint-gate] ${filePath} has remaining lint issues after auto-fix:\n\n${remaining}`,
|
|
435
|
+
display: true,
|
|
436
|
+
attribution: 'agent',
|
|
437
|
+
},
|
|
438
|
+
{ triggerTurn: false },
|
|
439
|
+
)
|
|
440
|
+
return undefined
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ── Extension factory ─────────────────────────────────────────────────
|
|
444
|
+
|
|
203
445
|
const oxlintGate: ExtensionFactory = (pi: ExtensionAPI): void => {
|
|
204
446
|
const log = pi.logger
|
|
205
447
|
|
|
206
|
-
log.info('[
|
|
448
|
+
log.info('[lint-gate] extension loaded (auto-fix mode)')
|
|
207
449
|
writeLog('INFO', 'extension loaded (auto-fix mode)')
|
|
208
450
|
|
|
451
|
+
// ── session_start: pre-warm strategy cache ──────────────────────
|
|
452
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
453
|
+
const strategy = loadStrategy(ctx.cwd)
|
|
454
|
+
log.info(`[lint-gate] project strategy: ${strategy.strategy}${strategy.eslintVersion ? ` (eslint@${strategy.eslintVersion})` : ''}`)
|
|
455
|
+
})
|
|
456
|
+
|
|
209
457
|
// ── tool_call: record file path, don't block ────────────────────────
|
|
210
458
|
pi.on('tool_call', async (event, ctx) => {
|
|
211
459
|
if (!WRITE_TOOLS.has(event.toolName))
|
|
@@ -248,58 +496,20 @@ const oxlintGate: ExtensionFactory = (pi: ExtensionAPI): void => {
|
|
|
248
496
|
return
|
|
249
497
|
if (!isExistingFile(filePath))
|
|
250
498
|
return
|
|
251
|
-
if (!existsSync(OXLINT_CFG))
|
|
252
|
-
return
|
|
253
499
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return
|
|
500
|
+
// Ensure strategy is loaded (may sniff on first call)
|
|
501
|
+
const { strategy } = loadStrategy(ctx.cwd)
|
|
257
502
|
|
|
258
503
|
const fixCount = fixCounters.get(filePath) ?? 0
|
|
259
504
|
if (fixCount >= MAX_FIX_ATTEMPTS) {
|
|
260
|
-
log.debug(`[
|
|
505
|
+
log.debug(`[lint-gate] max fix attempts (${MAX_FIX_ATTEMPTS}) reached for ${filePath}`)
|
|
261
506
|
return
|
|
262
507
|
}
|
|
263
508
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (passed) {
|
|
267
|
-
log.info(`[oxlint-gate] passed: ${filePath}`)
|
|
268
|
-
writeLog('INFO', `passed: ${filePath}`)
|
|
269
|
-
fixCounters.delete(filePath) // reset counter on clean pass
|
|
270
|
-
return
|
|
509
|
+
if (strategy === 'eslint') {
|
|
510
|
+
return handleEslintStrategy(pi, filePath, ctx.cwd, fixCount, log)
|
|
271
511
|
}
|
|
272
|
-
|
|
273
|
-
log.warn(`[oxlint-gate] violations in ${filePath}, attempting auto-fix`)
|
|
274
|
-
writeLog('WARN', `violations in ${filePath}, attempting auto-fix`)
|
|
275
|
-
|
|
276
|
-
// 2. Try auto-fix
|
|
277
|
-
const fixResult = runOxlintFix(filePath, OXLINT_CFG)
|
|
278
|
-
fixCounters.set(filePath, fixCount + 1)
|
|
279
|
-
|
|
280
|
-
if (fixResult.fixed) {
|
|
281
|
-
log.info(`[oxlint-gate] auto-fixed: ${filePath}`)
|
|
282
|
-
writeLog('INFO', `auto-fixed: ${filePath}`)
|
|
283
|
-
return {
|
|
284
|
-
content: [{ type: 'text', text: `✅ [oxlint-gate] auto-fixed lint issues in ${filePath}` }],
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// 3. Some violations remain — report to LLM
|
|
289
|
-
const remaining = truncateOutput(fixResult.output)
|
|
290
|
-
log.warn(`[oxlint-gate] partial fix in ${filePath}, remaining issues`)
|
|
291
|
-
writeLog('WARN', `partial fix in ${filePath}`)
|
|
292
|
-
|
|
293
|
-
pi.sendMessage(
|
|
294
|
-
{
|
|
295
|
-
customType: 'oxlint-gate',
|
|
296
|
-
content: `⚠️ [oxlint-gate] ${filePath} has remaining lint issues after auto-fix:\n\n${remaining}`,
|
|
297
|
-
display: true,
|
|
298
|
-
attribution: 'agent',
|
|
299
|
-
},
|
|
300
|
-
{ triggerTurn: false },
|
|
301
|
-
)
|
|
302
|
-
return undefined
|
|
512
|
+
return handleOxlintStrategy(pi, filePath, fixCount, log)
|
|
303
513
|
})
|
|
304
514
|
|
|
305
515
|
// ── turn_end: clear pending paths only (keep fixCounters to prevent loops) ──
|