From 983f0a2b8942df6674115b039d9cdd8d177db76b Mon Sep 17 00:00:00 2001 From: Maciej Mensfeld Date: Thu, 26 Mar 2026 21:02:14 +0100 Subject: [PATCH] Reduce runtime dependencies from 6 to 4 Replace chalk with inline ANSI helpers, commander with node:util parseArgs, and micromatch with picomatch (already a transitive dep via fast-glob). Bumps version to 0.4.1. --- CHANGELOG.md | 11 ++- package-lock.json | 55 +++------------ package.json | 8 +-- src/cli.ts | 125 ++++++++++++++++++++++++---------- src/output/text.ts | 30 +++++--- src/validator/glob.ts | 9 +-- tests/integration/cli.test.ts | 2 +- 7 files changed, 138 insertions(+), 102 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f4a3f5..1b9327b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.4.1] - 2026-03-26 + +### Changed +- Replaced `chalk` dependency with inline ANSI color helpers (zero-dependency text coloring) +- Replaced `commander` dependency with Node.js built-in `node:util parseArgs` (stable since Node 18.3) +- Replaced `micromatch` dependency with `picomatch` (already a transitive dependency via `fast-glob`) +- Reduced runtime dependencies from 6 to 4: `fast-glob`, `picomatch`, `smol-toml`, `yaml` + ## [0.4.0] - 2026-01-27 ### Added @@ -130,7 +138,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Examples for CI/CD integration - Contributing guidelines -[Unreleased]: https://github.com/mensfeld/lostconf/compare/v0.4.0...HEAD +[Unreleased]: https://github.com/mensfeld/lostconf/compare/v0.4.1...HEAD +[0.4.1]: https://github.com/mensfeld/lostconf/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/mensfeld/lostconf/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/mensfeld/lostconf/compare/v0.2.1...v0.3.0 [0.2.1]: https://github.com/mensfeld/lostconf/compare/v0.2.0...v0.2.1 diff --git a/package-lock.json b/package-lock.json index cc2181f..b647a21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,16 @@ { "name": "lostconf", - "version": "0.3.0", + "version": "0.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lostconf", - "version": "0.3.0", + "version": "0.4.1", "license": "MIT", "dependencies": { - "chalk": "^5.3.0", - "commander": "^12.1.0", "fast-glob": "^3.3.2", - "micromatch": "^4.0.8", + "picomatch": "^2.3.1", "smol-toml": "^1.3.0", "yaml": "^2.5.0" }, @@ -21,8 +19,8 @@ }, "devDependencies": { "@eslint/js": "^9.39.2", - "@types/micromatch": "^4.0.9", "@types/node": "^22.5.0", + "@types/picomatch": "^2.3.4", "eslint": "^9.9.0", "prettier": "^3.3.3", "typescript": "^5.5.4", @@ -1012,13 +1010,6 @@ "win32" ] }, - "node_modules/@types/braces": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.5.tgz", - "integrity": "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1033,16 +1024,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/micromatch": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.10.tgz", - "integrity": "sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/braces": "*" - } - }, "node_modules/@types/node": { "version": "22.19.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", @@ -1053,6 +1034,13 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/picomatch": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.4.tgz", + "integrity": "sha512-0so8lU8O5zatZS/2Fi4zrwks+vZv7e0dygrgEZXljODXBig97l4cPQD+9LabXfGJOWwoRkTVz6Q4edZvD12UOA==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.53.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz", @@ -1562,18 +1550,6 @@ "node": ">=18" } }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/check-error": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", @@ -1604,15 +1580,6 @@ "dev": true, "license": "MIT" }, - "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", diff --git a/package.json b/package.json index 049e3e8..bea0bcd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lostconf", - "version": "0.4.0", + "version": "0.4.1", "description": "A meta-linter that detects stale references in configuration files", "type": "module", "main": "dist/index.js", @@ -55,16 +55,14 @@ "node": ">=18.0.0" }, "dependencies": { - "chalk": "^5.3.0", - "commander": "^12.1.0", "fast-glob": "^3.3.2", - "micromatch": "^4.0.8", + "picomatch": "^2.3.1", "smol-toml": "^1.3.0", "yaml": "^2.5.0" }, "devDependencies": { "@eslint/js": "^9.39.2", - "@types/micromatch": "^4.0.9", + "@types/picomatch": "^2.3.4", "@types/node": "^22.5.0", "eslint": "^9.9.0", "prettier": "^3.3.3", diff --git a/src/cli.ts b/src/cli.ts index 3c0beb3..d6d1d6d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,7 +4,7 @@ * CLI entry point for lostconf */ -import { Command } from 'commander'; +import { parseArgs } from 'node:util'; import fs from 'fs/promises'; import { createEngine } from './core/engine.js'; import { getBuiltinParsers } from './parsers/index.js'; @@ -14,39 +14,70 @@ import { createSarifFormatter } from './output/sarif.js'; import type { Formatter } from './output/formatter.js'; import { Severity } from './core/types.js'; -const program = new Command(); - -program - .name('lostconf') - .description('A meta-linter that detects stale references in configuration files') - .version('0.4.0') - .argument('[paths...]', 'Paths to scan (default: current directory)') - .option('-f, --format ', 'Output format: text, json, sarif', 'text') - .option('-o, --output ', 'Write to file instead of stdout') - .option('--include ', 'Only check matching config files') - .option('--exclude ', 'Skip matching config files') - .option('--skip-ignore-files', 'Skip .gitignore, .prettierignore, etc. (reduces noise)') - .option('--exclude-parsers ', 'Skip specific parsers (e.g., gitignore prettierignore)') - .option( - '--min-severity ', - 'Minimum severity to show: low, medium, high (default: medium)', - 'medium' - ) - .option('--show-all', 'Show all findings including low severity (same as --min-severity=low)') - .option('--fail-on-stale', 'Exit code 1 if stale patterns found') - .option('-q, --quiet', 'Suppress non-error output') - .option('-v, --verbose', 'Show debug info') - .option('--no-progress', 'Disable progress indicator') - .action(async (paths: string[], options) => { - try { - await run(paths, options); - } catch (err) { - if (!options.quiet) { - console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); - } - process.exit(2); - } - }); +const VERSION = '0.4.1'; + +const HELP = `Usage: lostconf [options] [paths...] + +A meta-linter that detects stale references in configuration files + +Arguments: + paths Paths to scan (default: current directory) + +Options: + -V, --version output the version number + -f, --format Output format: text, json, sarif (default: "text") + -o, --output Write to file instead of stdout + --include Only check matching config files (repeatable) + --exclude Skip matching config files (repeatable) + --skip-ignore-files Skip .gitignore, .prettierignore, etc. (reduces noise) + --exclude-parsers Skip specific parsers (repeatable, e.g., gitignore prettierignore) + --min-severity Minimum severity to show: low, medium, high (default: "medium") + --show-all Show all findings including low severity (same as --min-severity=low) + --fail-on-stale Exit code 1 if stale patterns found + -q, --quiet Suppress non-error output + -v, --verbose Show debug info + --no-progress Disable progress indicator + -h, --help display help for command +`; + +const options = { + version: { type: 'boolean' as const, short: 'V' }, + help: { type: 'boolean' as const, short: 'h' }, + format: { type: 'string' as const, short: 'f', default: 'text' }, + output: { type: 'string' as const, short: 'o' }, + include: { type: 'string' as const, multiple: true }, + exclude: { type: 'string' as const, multiple: true }, + 'skip-ignore-files': { type: 'boolean' as const, default: false }, + 'exclude-parsers': { type: 'string' as const, multiple: true }, + 'min-severity': { type: 'string' as const, default: 'medium' }, + 'show-all': { type: 'boolean' as const, default: false }, + 'fail-on-stale': { type: 'boolean' as const, default: false }, + quiet: { type: 'boolean' as const, short: 'q', default: false }, + verbose: { type: 'boolean' as const, short: 'v', default: false }, + progress: { type: 'boolean' as const, default: true } +}; + +let values: Record; +let positionals: string[]; +try { + const parsed = parseArgs({ options, allowPositionals: true, strict: true }); + values = parsed.values as Record; + positionals = parsed.positionals; +} catch (err) { + console.error(`error: ${err instanceof Error ? err.message : String(err)}`); + console.error(`Try 'lostconf --help' for more information.`); + process.exit(2); +} + +if (values.help) { + process.stdout.write(HELP); + process.exit(0); +} + +if (values.version) { + console.log(VERSION); + process.exit(0); +} interface CliOptions { format: string; @@ -63,6 +94,30 @@ interface CliOptions { progress?: boolean; } +const cliOptions: CliOptions = { + format: (values.format as string | undefined) ?? 'text', + output: values.output as string | undefined, + include: values.include as string[] | undefined, + exclude: values.exclude as string[] | undefined, + skipIgnoreFiles: values['skip-ignore-files'] as boolean | undefined, + excludeParsers: values['exclude-parsers'] as string[] | undefined, + minSeverity: values['min-severity'] as string | undefined, + showAll: values['show-all'] as boolean | undefined, + failOnStale: values['fail-on-stale'] as boolean | undefined, + quiet: values.quiet as boolean | undefined, + verbose: values.verbose as boolean | undefined, + progress: values.progress as boolean | undefined +}; + +try { + await run(positionals, cliOptions); +} catch (err) { + if (!cliOptions.quiet) { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + } + process.exit(2); +} + async function run(paths: string[], options: CliOptions): Promise { const { format, @@ -194,5 +249,3 @@ function getFormatter(format: string): Formatter { return createTextFormatter(); } } - -program.parse(); diff --git a/src/output/text.ts b/src/output/text.ts index bed88a6..fef896e 100644 --- a/src/output/text.ts +++ b/src/output/text.ts @@ -2,8 +2,16 @@ * Text output formatter */ -import chalk from 'chalk'; import type { ValidationResult, Finding, StaleReason } from '../core/types.js'; + +/** Inline ANSI color helpers (replaces chalk) */ +const ansi = (code: number, close: number) => (s: string) => `\x1b[${code}m${s}\x1b[${close}m`; +const gray = ansi(90, 39); +const yellow = ansi(33, 39); +const red = ansi(31, 39); +const cyan = ansi(36, 39); +const dim = ansi(2, 22); +const green = ansi(32, 39); import { Severity } from '../core/types.js'; import type { Formatter } from './formatter.js'; @@ -23,20 +31,20 @@ function formatReason(reason: StaleReason): string { function getSeverityIcon(severity: Severity): string { switch (severity) { case Severity.LOW: - return chalk.gray('○'); + return gray('○'); case Severity.MEDIUM: - return chalk.yellow('●'); + return yellow('●'); case Severity.HIGH: - return chalk.red('●'); + return red('●'); } } /** Format a single finding */ function formatFinding(finding: Finding): string { const severityIcon = getSeverityIcon(finding.severity); - const location = chalk.cyan(`${finding.file}:${finding.line}`); - const pattern = chalk.yellow(finding.pattern); - const reason = chalk.dim(formatReason(finding.reason)); + const location = cyan(`${finding.file}:${finding.line}`); + const pattern = yellow(finding.pattern); + const reason = dim(formatReason(finding.reason)); // Calculate padding for alignment const locationStr = `${finding.file}:${finding.line}`; @@ -51,7 +59,7 @@ export const textFormatter: Formatter = { const lines: string[] = []; if (result.findings.length === 0) { - lines.push(chalk.green('No stale patterns found')); + lines.push(green('No stale patterns found')); return lines.join('\n'); } @@ -84,13 +92,13 @@ export const textFormatter: Formatter = { const parts = []; if (severityCounts[Severity.HIGH] > 0) { - parts.push(chalk.red(`${severityCounts[Severity.HIGH]} high`)); + parts.push(red(`${severityCounts[Severity.HIGH]} high`)); } if (severityCounts[Severity.MEDIUM] > 0) { - parts.push(chalk.yellow(`${severityCounts[Severity.MEDIUM]} medium`)); + parts.push(yellow(`${severityCounts[Severity.MEDIUM]} medium`)); } if (severityCounts[Severity.LOW] > 0) { - parts.push(chalk.gray(`${severityCounts[Severity.LOW]} low`)); + parts.push(gray(`${severityCounts[Severity.LOW]} low`)); } lines.push( diff --git a/src/validator/glob.ts b/src/validator/glob.ts index fe83c12..70bf93d 100644 --- a/src/validator/glob.ts +++ b/src/validator/glob.ts @@ -1,8 +1,8 @@ /** - * Glob matching utilities using micromatch + * Glob matching utilities using picomatch */ -import micromatch from 'micromatch'; +import picomatch from 'picomatch'; export interface GlobMatchOptions { /** Base directory for relative patterns */ @@ -33,10 +33,11 @@ export function globMatches( } try { - return micromatch(files, normalizedPattern, { + const isMatch = picomatch(normalizedPattern, { dot, matchBase: !normalizedPattern.includes('/') }); + return files.filter((file) => isMatch(file)); } catch { // Invalid pattern return []; @@ -47,7 +48,7 @@ export function globMatches( export function isValidGlob(pattern: string): boolean { try { // Try to compile the pattern - micromatch.makeRe(pattern); + picomatch.makeRe(pattern); return true; } catch { return false; diff --git a/tests/integration/cli.test.ts b/tests/integration/cli.test.ts index 1b2631a..3e5f5a7 100644 --- a/tests/integration/cli.test.ts +++ b/tests/integration/cli.test.ts @@ -38,7 +38,7 @@ describe('CLI Integration Tests', () => { it('should show version with --version', async () => { const { stdout } = await execAsync(`node ${cliPath} --version`); - expect(stdout).toContain('0.4.0'); + expect(stdout).toContain('0.4.1'); }); it('should output text format by default', async () => {