Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
55 changes: 11 additions & 44 deletions package-lock.json

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

8 changes: 3 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down
125 changes: 89 additions & 36 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* CLI entry point for lostconf
*/

import { Command } from 'commander';
import { parseArgs } from 'node:util';
import fs from 'fs/promises';
Comment on lines +7 to 8
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

This switch to node:util parseArgs effectively requires Node versions where parseArgs is stable/available (noted in the PR description as 18.3+). However, package.json still declares node >=18.0.0, which can lead to runtime failures or unsupported behavior on 18.0–18.2. Consider bumping the engine requirement to >=18.3.0 (or add a small fallback/guard) so the published package matches the CLI’s runtime needs.

Copilot uses AI. Check for mistakes.
import { createEngine } from './core/engine.js';
import { getBuiltinParsers } from './parsers/index.js';
Expand All @@ -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 <fmt>', 'Output format: text, json, sarif', 'text')
.option('-o, --output <file>', 'Write to file instead of stdout')
.option('--include <glob...>', 'Only check matching config files')
.option('--exclude <glob...>', 'Skip matching config files')
.option('--skip-ignore-files', 'Skip .gitignore, .prettierignore, etc. (reduces noise)')
.option('--exclude-parsers <names...>', 'Skip specific parsers (e.g., gitignore prettierignore)')
.option(
'--min-severity <level>',
'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 <fmt> Output format: text, json, sarif (default: "text")
-o, --output <file> Write to file instead of stdout
--include <glob> Only check matching config files (repeatable)
--exclude <glob> Skip matching config files (repeatable)
--skip-ignore-files Skip .gitignore, .prettierignore, etc. (reduces noise)
--exclude-parsers <name> Skip specific parsers (repeatable, e.g., gitignore prettierignore)
--min-severity <level> 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' },
Comment on lines +48 to +52
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

parseArgs with multiple: true makes --include/--exclude/--exclude-parsers repeatable, but it does not accept the previous commander-style variadic form (e.g. --include a b from the README’s --include <glob...>), and extra values will be treated as positionals (scan paths). Either add backwards-compatible parsing for space-separated lists or update the public docs/help to clearly reflect the new required syntax.

Copilot uses AI. Check for mistakes.
'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 }
};
Comment on lines +43 to +58
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

CLI tests cover --include/--exclude with a single value, but there’s no coverage for the new parseArgs multi-value behavior (repeatable flags) or for the legacy --include a b style that commander previously supported. Adding an integration test for repeated --include/--exclude (and, if supported, the space-separated form) would prevent regressions in argument parsing.

Copilot uses AI. Check for mistakes.

let values: Record<string, unknown>;
let positionals: string[];
try {
const parsed = parseArgs({ options, allowPositionals: true, strict: true });
values = parsed.values as Record<string, unknown>;
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;
Expand All @@ -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<void> {
const {
format,
Expand Down Expand Up @@ -194,5 +249,3 @@ function getFormatter(format: string): Formatter {
return createTextFormatter();
}
}

program.parse();
30 changes: 19 additions & 11 deletions src/output/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines +7 to +14
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

The new inline ANSI helpers always emit escape codes, whereas chalk previously disabled color automatically in non-TTY contexts and commonly respects NO_COLOR/TERM=dumb. This can introduce escape sequences in redirected output/files and CI logs. Consider gating coloring behind a simple “color enabled” check (TTY + env vars) and falling back to identity functions when disabled.

Copilot uses AI. Check for mistakes.
import { Severity } from '../core/types.js';
import type { Formatter } from './formatter.js';

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

Expand Down Expand Up @@ -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(
Expand Down
Loading
Loading