#!/usr/bin/env bun // Fails when a GUI package file this branch adds or edits has any oxlint problem, including warn-level rules (anti-slop): // a change that touches a file leaves the whole file clean. Untouched files and other packages are not checked. // Usage: bun script/lint-changed.ts [base-ref] (default: merge base with upstream/dev, origin/dev, or dev) import { $ } from "bun" type Diagnostic = { message: string code: string filename: string labels: { span: { line: number; column: number } }[] } const base = await resolveBase(process.argv[2] ?? process.env.LINT_BASE) // -M100% reports only exact moves as renames, so a moved file is linted only once its content changes. const tracked = (await $`git diff --name-only -M100% --diff-filter=ACM ${base}`.text()).split("\n") const untracked = (await $`git ls-files --others --exclude-standard`.text()).split("\n") // The packages .oxlintrc.json applies the anti-slop rules to. const packages = [ "packages/app/", "packages/desktop/", "packages/gui-plugins/", "packages/plugin/src/gui/", "packages/ui/", "packages/session-ui/", ] // oxlint skips declaration files and prints a non-JSON message when given only those. const files = [...new Set([...tracked, ...untracked])].filter( (file) => /\.(ts|tsx)$/.test(file) && !file.endsWith(".d.ts") && packages.some((prefix) => file.startsWith(prefix)) && Bun.file(file).size > 0, ) if (files.length === 0) { console.log(`lint:changed: no changed TypeScript files since ${base.slice(0, 10)}`) process.exit(0) } // In chunks: a renamed package lists hundreds of files, more than one Windows command line holds. const chunks = Array.from({ length: Math.ceil(files.length / 100) }, (_, index) => files.slice(index * 100, index * 100 + 100)) const report: { diagnostics: Diagnostic[] } = { diagnostics: [] } for (const chunk of chunks) { const output = JSON.parse(await $`bunx oxlint --format json ${chunk}`.nothrow().quiet().text()) as { diagnostics: Diagnostic[] } report.diagnostics.push(...output.diagnostics) } report.diagnostics.forEach((diagnostic) => { const span = diagnostic.labels[0]?.span console.log(`${diagnostic.filename}:${span?.line}:${span?.column} ${diagnostic.code} ${diagnostic.message}`) }) const dirty = new Set(report.diagnostics.map((diagnostic) => diagnostic.filename)) console.log( `lint:changed: ${report.diagnostics.length} problem(s) in ${dirty.size} of ${files.length} changed files since ${base.slice(0, 10)}`, ) process.exit(report.diagnostics.length > 0 ? 1 : 0) async function resolveBase(explicit: string | undefined) { if (explicit) return (await $`git rev-parse ${explicit}`.text()).trim() const refs = ["upstream/dev", "origin/dev", "dev"] const found = await Promise.all( refs.map(async (ref) => (await $`git rev-parse --verify --quiet ${ref}`.nothrow().quiet()).exitCode === 0), ) const ref = refs.find((_, index) => found[index]) if (!ref) throw new Error("lint:changed: no v2 ref found; pass a base ref") return (await $`git merge-base HEAD ${ref}`.text()).trim() }