Skip to content

Use commander package to replace current implementation of CLI options #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 7, 2025
80 changes: 70 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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",
Expand Down
61 changes: 36 additions & 25 deletions src/bin/lint.ts
Original file line number Diff line number Diff line change
@@ -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>', 'Path to explicit ESLint config file')
.option('--eslint <pat...>', 'Glob(s) to pass to ESLint')
.option('--shell <pat...>', '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<CLIOptions>();

Comment on lines 31 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't how we use Commander. The main code needs to be in the .action(async () => {}) callback of the program. https://www.npmjs.com/package/commander#action-handler

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;
Expand All @@ -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));

Expand Down
10 changes: 9 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
44 changes: 32 additions & 12 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down