diff --git a/package-lock.json b/package-lock.json index 802bd25..e5f350a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@typescript-eslint/eslint-plugin": "^8.27.0", "@typescript-eslint/parser": "^8.27.0", "@typescript-eslint/utils": "^8.26.1", - "eslint": "^9.18.0", + "eslint": ">=9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.2", @@ -31,9 +31,11 @@ "@swc/jest": "^0.2.29", "@types/jest": "^29.5.2", "@types/node": "^20.5.7", + "commander": "^13.1.0", "jest": "^29.6.2", "jest-extended": "^4.0.2", "jest-junit": "^16.0.0", + "minimatch": "^10.0.1", "prettier": "^3.0.0", "shx": "^0.3.4", "tsconfig-paths": "^3.9.0", @@ -2900,6 +2902,21 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/utils": { "version": "8.27.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.27.0.tgz", @@ -3646,13 +3663,13 @@ "license": "MIT" }, "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, "license": "MIT", - "peer": true, "engines": { - "node": ">= 6" + "node": ">=18" } }, "node_modules/concat-map": { @@ -7921,15 +7938,16 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -9536,6 +9554,16 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/sucrase/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -9557,6 +9585,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9902,6 +9946,22 @@ "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x" } }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/typescript": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", diff --git a/package.json b/package.json index 61dbb27..27c9bcf 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "build": "shx rm -rf ./dist && tsc -p ./tsconfig.build.json && shx chmod +x dist/bin/lint.js", "postversion": "npm install --package-lock-only --ignore-scripts --silent", "tsx": "tsx", - "lint": "test -f ./dist/bin/lint.js || npm run build && ./dist/bin/lint.js", - "lintfix": "sh -c 'test -f ./dist/bin/lint.js || npm run build && ./dist/bin/lint.js --fix'", + "lint": "test -f ./dist/bin/lint.js || npm run build && ./dist/bin/lint.js --eslint \"{src,scripts,tests}/**/*.{js,mjs,ts,mts,jsx,tsx}\" --shell src scripts tests", + "lintfix": "sh -c 'test -f ./dist/bin/lint.js || npm run build && ./dist/bin/lint.js --fix --eslint \"{src,scripts,tests}/**/*.{js,mjs,ts,mts,jsx,tsx}\"' --shell src scripts tests", "lint-shell": "find ./src ./tests ./scripts -type f -regextype posix-extended -regex '.*\\.(sh)' -exec shellcheck {} +", "docs": "shx rm -rf ./docs && typedoc --entryPointStrategy expand --gitRevision master --tsconfig ./tsconfig.build.json --out ./docs src", "test": "node ./scripts/test.mjs" @@ -38,11 +38,11 @@ "@typescript-eslint/eslint-plugin": "^8.27.0", "@typescript-eslint/parser": "^8.27.0", "@typescript-eslint/utils": "^8.26.1", - "eslint": "^9.18.0", + "eslint": ">=9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-tailwindcss": "^3.18.0" @@ -52,9 +52,11 @@ "@swc/jest": "^0.2.29", "@types/jest": "^29.5.2", "@types/node": "^20.5.7", + "commander": "^13.1.0", "jest": "^29.6.2", "jest-extended": "^4.0.2", "jest-junit": "^16.0.0", + "minimatch": "^10.0.1", "prettier": "^3.0.0", "shx": "^0.3.4", "tsconfig-paths": "^3.9.0", diff --git a/src/bin/lint.ts b/src/bin/lint.ts index 033c3a6..4c38fa0 100644 --- a/src/bin/lint.ts +++ b/src/bin/lint.ts @@ -1,39 +1,45 @@ #!/usr/bin/env node +import type { CLIOptions } from '../types.js'; import os from 'node:os'; import path from 'node:path'; import process from 'node:process'; import childProcess from 'node:child_process'; import fs from 'node:fs'; +import { Command } from 'commander'; import * as utils from '../utils.js'; const platform = os.platform(); +const program = new Command(); +const DEFAULT_SHELLCHECK_SEARCH_ROOTS = ['./src', './scripts', './tests']; + +program + .name('matrixai-lint') + .description( + 'Lint source files, scripts, and markdown with configured rules.', + ) + .option('-f, --fix', 'Automatically fix problems') + .option( + '--user-config', + 'Use user-provided ESLint config instead of built-in one', + ) + .option('--config ', 'Path to explicit ESLint config file') + .option('--eslint ', 'Glob(s) to pass to ESLint') + .option('--shell ', 'Glob(s) to pass to shell-check') + .allowUnknownOption(true); // Optional: force rejection of unknown flags /* eslint-disable no-console */ async function main(argv = process.argv) { - argv = argv.slice(2); + await program.parseAsync(argv); + const options = program.opts(); + + const fix = Boolean(options.fix); + const useUserConfig = Boolean(options.userConfig); + const explicitConfigPath: string | undefined = options.config; + + const eslintPatterns: string[] | undefined = options.eslint; + const shellPatterns: string[] | undefined = options.shell; let hadFailure = false; - let fix = false; - let useUserConfig = false; - let explicitConfigPath: string | undefined; - const restArgs: string[] = []; - - while (argv.length > 0) { - const option = argv.shift()!; - switch (option) { - case '--fix': - fix = true; - break; - case '--user-config': - useUserConfig = true; - break; - case '--config': - explicitConfigPath = argv.shift(); // Grab the next token - break; - default: - restArgs.push(option); - } - } // Resolve which config file to use let chosenConfig: string | undefined; @@ -59,14 +65,19 @@ async function main(argv = process.argv) { } try { - await utils.runESLint({ fix, configPath: chosenConfig }); + await utils.runESLint({ + fix, + configPath: chosenConfig, + explicitGlobs: eslintPatterns, + }); } catch (err) { console.error(`ESLint failed: \n${err}`); hadFailure = true; } - const shellcheckDefaultSearchRoots = ['./src', './scripts', './tests']; - const searchRoots = shellcheckDefaultSearchRoots + const searchRoots = ( + shellPatterns?.length ? shellPatterns : DEFAULT_SHELLCHECK_SEARCH_ROOTS + ) .map((p) => path.resolve(process.cwd(), p)) .filter((p) => fs.existsSync(p)); diff --git a/src/types.ts b/src/types.ts index 6b25cc4..70e98bc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,4 +8,12 @@ type RawMatrixCfg = Partial<{ forceInclude: unknown; }>; // “might have these two keys, values are unknown” -export type { MatrixAILintCfg, RawMatrixCfg }; +type CLIOptions = { + fix: boolean; + userConfig: boolean; + config?: string; + eslint?: string[]; + shell?: string[]; +}; + +export type { MatrixAILintCfg, RawMatrixCfg, CLIOptions }; diff --git a/src/utils.ts b/src/utils.ts index c110d78..f094343 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -11,52 +11,72 @@ import { ESLint } from 'eslint'; async function runESLint({ fix, configPath, + explicitGlobs, }: { fix: boolean; configPath?: string; + explicitGlobs?: string[]; }) { const dirname = path.dirname(url.fileURLToPath(import.meta.url)); const defaultConfigPath = path.resolve(dirname, './configs/js.js'); - const matrixaiLintConfig = resolveMatrixConfig(); - const forceInclude = matrixaiLintConfig.forceInclude; - const tsconfigPaths = matrixaiLintConfig.tsconfigPaths; + // PATH A – user supplied explicit globs + if (explicitGlobs?.length) { + console.log('Linting with explicit patterns:'); + explicitGlobs.forEach((g) => console.log(' ' + g)); + + const eslint = new ESLint({ + overrideConfigFile: configPath || defaultConfigPath, + fix, + errorOnUnmatchedPattern: false, + warnIgnored: false, + ignorePatterns: [], // Trust caller entirely + }); + + await lintAndReport(eslint, explicitGlobs, fix); + return; + } + + // PATH B – default behaviour (tsconfig + matrix config) + const { forceInclude, tsconfigPaths } = resolveMatrixConfig(); if (tsconfigPaths.length === 0) { console.error('[matrixai-lint] ⚠ No tsconfig.json files found.'); } console.log(`Found ${tsconfigPaths.length} tsconfig.json files:`); - tsconfigPaths.forEach((tsconfigPath) => console.log(' ' + tsconfigPath)); + tsconfigPaths.forEach((p) => console.log(' ' + p)); - const { files: lintFiles, ignore } = buildPatterns( + const { files: patterns, ignore: ignorePats } = buildPatterns( tsconfigPaths[0], forceInclude, ); console.log('Linting files:'); - lintFiles.forEach((file) => console.log(' ' + file)); + patterns.forEach((p) => console.log(' ' + p)); const eslint = new ESLint({ overrideConfigFile: configPath || defaultConfigPath, fix, errorOnUnmatchedPattern: false, warnIgnored: false, - ignorePatterns: ignore, + ignorePatterns: ignorePats, }); - const results = await eslint.lintFiles(lintFiles); + await lintAndReport(eslint, patterns, fix); +} + +async function lintAndReport(eslint: ESLint, patterns: string[], fix: boolean) { + const results = await eslint.lintFiles(patterns); if (fix) { await ESLint.outputFixes(results); } const formatter = await eslint.loadFormatter('stylish'); - const resultText = formatter.format(results); - console.log(resultText); - - /* eslint-enable no-console */ + console.log(formatter.format(results)); } +/* eslint-enable no-console */ /** * Find the user's ESLint config file in the current working directory.