knip-guard
Version:
Baseline / no-new-issues guard for Knip
141 lines • 5.92 kB
JavaScript
import process from 'node:process';
import { Command } from 'commander';
import { diffBaseline, extractIssueKeys, loadBaseline, runKnipJson, loadKnipReportFromFile, saveBaseline } from './index.js';
/**
* Gets the current set of issue keys from either a provided report or by running knip
* @param options - CLI options with baseline, report, and command paths
* @returns A Set of issue key strings
*/
async function getCurrentIssueKeys(options) {
let report;
if (options.report) {
report = await loadKnipReportFromFile(options.report);
}
else {
const command = options.command ?? 'npx knip';
report = await runKnipJson(command);
}
return extractIssueKeys(report);
}
/**
* Initializes a new baseline from the current Knip report
* @param options - CLI options with baseline path and report source
* @returns Exit code (0 = success, 1 = failure)
*/
async function cmdInit(options) {
const issueKeys = await getCurrentIssueKeys(options);
const existingBaseline = await loadBaseline(options.baseline);
if (existingBaseline) {
console.warn(`Baseline already exists at ${options.baseline}. It will be overwritten with current issues.`);
}
await saveBaseline(options.baseline, issueKeys, existingBaseline, options.command);
console.log(`Baseline initialized at ${options.baseline} with ${issueKeys.size} issues recorded.`);
return 0;
}
/**
* Checks if there are any new issues compared to the baseline
* Fails with exit code 1 if new issues are detected
* @param options - CLI options with baseline path and report source
* @returns Exit code (0 = no new issues, 1 = new issues detected or baseline missing)
*/
async function cmdCheck(options) {
const baseline = await loadBaseline(options.baseline);
if (!baseline) {
console.error(`No baseline found at ${options.baseline}. Run "knip-guard init" first in a clean state.`);
return 1;
}
const issueKeys = await getCurrentIssueKeys(options);
const { newIssues, resolvedIssues } = diffBaseline(baseline, issueKeys);
if (resolvedIssues.length > 0) {
console.log(`Resolved issues since baseline: ${resolvedIssues.length}`);
}
if (newIssues.length === 0) {
console.log('No new Knip issues compared to baseline. ✅');
return 0;
}
console.error(`New Knip issues detected since baseline: ${newIssues.length}`);
for (const key of newIssues) {
console.error(` - ${key}`);
}
return 1;
}
/**
* Accepts the current issues and updates the baseline
* Useful after reviewing and intentionally accepting new issues
* @param options - CLI options with baseline path and report source
* @returns Exit code (0 = success, 1 = failure)
*/
async function cmdAccept(options) {
const baseline = await loadBaseline(options.baseline);
if (!baseline) {
console.warn(`No existing baseline at ${options.baseline}. This will create a new one from current issues.`);
}
const issueKeys = await getCurrentIssueKeys(options);
const newBaseline = await saveBaseline(options.baseline, issueKeys, baseline, options.command);
console.log(`Baseline updated at ${options.baseline}. Recorded ${newBaseline.issues.length} issues.`);
return 0;
}
/**
* Core entry point for CLI and programmatic use
* Sets up all available commands (init, check, accept) and parses arguments
* @param argv - Command line arguments (typically process.argv.slice(2))
* @returns Exit code (0 = success, non-zero = failure)
* @example
* const code = await run(['check', '--baseline', '.knip-baseline.json']);
* process.exitCode = code;
*/
export async function run(argv) {
const program = new Command();
program
.name('knip-guard')
.description('Baseline / no-new-issues guard for Knip')
.version('1.0.0');
program
.command('init')
.description('Initialize baseline from current Knip report')
.option('-b, --baseline <path>', 'Path to baseline file', '.knip-baseline.json')
.option('-r, --report <path>', 'Path to existing Knip JSON report (otherwise run knip)')
.option('-c, --command <command>', 'Command to run knip, e.g. "npm run knip"')
.action(async (options) => {
const code = await cmdInit(options);
process.exitCode = code;
});
program
.command('check')
.description('Compare current report with baseline and fail on NEW issues')
.option('-b, --baseline <path>', 'Path to baseline file', '.knip-baseline.json')
.option('-r, --report <path>', 'Path to existing Knip JSON report (otherwise run knip)')
.option('-c, --command <command>', 'Command to run knip, e.g. "npm run knip"')
.action(async (options) => {
const code = await cmdCheck(options);
process.exitCode = code;
});
program
.command('accept')
.description('Update baseline to current issues (after review)')
.option('-b, --baseline <path>', 'Path to baseline file', '.knip-baseline.json')
.option('-r, --report <path>', 'Path to existing Knip JSON report (otherwise run knip)')
.option('-c, --command <command>', 'Command to run knip, e.g. "npm run knip"')
.action(async (options) => {
const code = await cmdAccept(options);
process.exitCode = code;
});
await program.parseAsync(argv, { from: 'user' });
return process.exitCode ?? 0;
}
/**
* Auto-run when executed as a script (not when imported as a module)
* Only works in ESM builds
*/
if (import.meta.url === `file://${process.argv[1]}`) {
run(process.argv.slice(2))
.then((code) => {
process.exitCode = code;
})
.catch((err) => {
console.error('Unexpected error in knip-guard:', err);
process.exitCode = 1;
});
}
//# sourceMappingURL=cli.js.map