@umbrelladocs/rdformat-validator
Version:
Validate and fix Reviewdog Diagnostic Format (RD Format) - A comprehensive library and CLI tool for validating JSON data against the Reviewdog Diagnostic Format specification
554 lines • 23.6 kB
JavaScript
;
/**
* CLI Module for RDFormat Validator
* Provides command-line interface functionality for validating RDFormat data
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CLI = void 0;
exports.createCLI = createCLI;
exports.main = main;
const commander_1 = require("commander");
const index_1 = require("../index");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
/**
* ANSI color codes for terminal output
*/
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
gray: '\x1b[90m',
bold: '\x1b[1m'
};
/**
* Utility function to colorize text for terminal output
*/
function colorize(text, color) {
// Only colorize if stdout is a TTY (terminal)
if (process.stdout.isTTY) {
return `${colors[color]}${text}${colors.reset}`;
}
return text;
}
/**
* Main CLI class that handles command-line operations
*/
class CLI {
constructor() {
this.program = new commander_1.Command();
this.validator = new index_1.RDFormatValidator();
this.setupCommands();
}
/**
* Set up the command-line interface structure and options
*/
setupCommands() {
this.program
.name('rdformat-validator')
.description('Validate JSON data against the Reviewdog Diagnostic Format specification')
.version('1.0.0')
.argument('[files...]', 'JSON files to validate (reads from stdin if no files specified)')
.option('-f, --fix', 'attempt to automatically fix common issues', false)
.option('-o, --output <file>', 'output file (default: stdout)')
.option('-v, --verbose', 'enable verbose output', false)
.option('-s, --silent', 'suppress non-error output', false)
.option('--format <format>', 'output format: json or text', 'text')
.option('--strict', 'enable strict validation mode', false)
.option('--no-extra-fields', 'disallow extra fields not in specification', false)
.option('--fix-level <level>', 'fix level: basic or aggressive', 'basic')
.action(async (files, options) => {
try {
const cliOptions = this.parseOptions(files, options);
const exitCode = await this.run(cliOptions);
process.exit(exitCode);
}
catch (error) {
console.error('Fatal error:', error instanceof Error ? error.message : 'Unknown error');
process.exit(1);
}
});
// Add help examples
this.program.addHelpText('after', `
Examples:
$ rdformat-validator file.json # Validate a single file
$ rdformat-validator file1.json file2.json # Validate multiple files
$ cat file.json | rdformat-validator # Validate from stdin
$ rdformat-validator --fix file.json # Validate and fix issues
$ rdformat-validator --format json file.json # Output in JSON format
$ rdformat-validator --output result.json file.json # Save output to file
$ rdformat-validator --strict --no-extra-fields file.json # Strict validation
`);
}
/**
* Parse command-line arguments and convert to CLIOptions
*/
parseOptions(files, options) {
return {
files: files.length > 0 ? files : undefined,
fix: options.fix || false,
output: options.output,
verbose: options.verbose || false,
silent: options.silent || false,
format: options.format === 'json' ? 'json' : 'text',
strictMode: options.strict || false,
allowExtraFields: options.extraFields !== false, // Default true unless --no-extra-fields
fixLevel: options.fixLevel === 'aggressive' ? 'aggressive' : 'basic'
};
}
/**
* Main CLI execution method
* @param options - Parsed CLI options
* @returns Exit code (0 for success, non-zero for failure)
*/
async run(options) {
try {
// Update validator options
this.validator.setOptions({
strictMode: options.strictMode,
allowExtraFields: options.allowExtraFields,
fixLevel: options.fixLevel
});
let result;
if (options.files && options.files.length > 0) {
// Process files
result = await this.processFiles(options.files, options);
}
else {
// Process stdin
result = await this.processStdin(options);
}
// Output results
await this.outputResult(result, options);
// Return appropriate exit code
return result.success ? 0 : 1;
}
catch (error) {
if (!options.silent) {
console.error('Error:', error instanceof Error ? error.message : 'Unknown error');
}
return 1;
}
}
/**
* Process multiple files
*/
async processFiles(files, options) {
const result = {
success: true,
filesProcessed: 0,
validFiles: 0,
invalidFiles: 0,
totalErrors: 0,
totalWarnings: 0,
totalFixes: 0,
fileResults: []
};
for (const file of files) {
try {
if (!options.silent && options.verbose) {
console.log(`Processing: ${file}`);
}
const fileResult = await this.processFile(file, options);
result.fileResults.push(fileResult);
result.filesProcessed++;
if (fileResult.valid) {
result.validFiles++;
}
else {
result.invalidFiles++;
result.success = false;
}
result.totalErrors += fileResult.errors;
result.totalWarnings += fileResult.warnings;
result.totalFixes += fileResult.fixes;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
result.fileResults.push({
file,
valid: false,
errors: 1,
warnings: 0,
fixes: 0
});
result.filesProcessed++;
result.invalidFiles++;
result.totalErrors++;
result.success = false;
if (!options.silent) {
console.error(`Error processing ${file}: ${errorMessage}`);
}
}
}
return result;
}
/**
* Process a single file
*/
async processFile(filePath, options) {
// Check if file exists and is readable
try {
const stats = fs.statSync(filePath);
if (!stats.isFile()) {
throw new Error(`Path is not a file: ${filePath}`);
}
// Check if file is readable
fs.accessSync(filePath, fs.constants.R_OK);
}
catch (error) {
if (error instanceof Error && 'code' in error) {
const nodeError = error;
if (nodeError.code === 'ENOENT') {
throw new Error(`File not found: ${filePath}`);
}
else if (nodeError.code === 'EACCES') {
throw new Error(`Permission denied: ${filePath}`);
}
}
throw new Error(`Cannot access file: ${filePath} - ${error instanceof Error ? error.message : 'Unknown error'}`);
}
// Validate the file
const validationResult = await this.validator.validateFile(filePath, options.fix || false);
// If fixing was requested and fixes were applied, optionally write back to file
if (options.fix && validationResult.fixedData && validationResult.appliedFixes && validationResult.appliedFixes.length > 0) {
if (!options.silent && options.verbose) {
console.log(`Applied ${validationResult.appliedFixes.length} fixes to ${filePath}`);
}
// Note: We don't automatically overwrite the original file for safety
// Users can use --output to specify where to save fixed data
}
const result = {
file: filePath,
valid: validationResult.valid,
errors: validationResult.errors.length,
warnings: validationResult.warnings.length,
fixes: validationResult.appliedFixes?.length || 0
};
// Add detailed error information if verbose or JSON format
if (options.verbose || options.format === 'json') {
if (validationResult.errors.length > 0) {
result.errorDetails = validationResult.errors.map(error => ({
path: error.path,
message: error.message,
code: error.code
}));
}
if (validationResult.warnings.length > 0) {
result.warningDetails = validationResult.warnings.map(warning => ({
path: warning.path,
message: warning.message,
code: warning.code
}));
}
if (validationResult.appliedFixes && validationResult.appliedFixes.length > 0) {
result.fixDetails = validationResult.appliedFixes.map(fix => ({
path: fix.path,
message: fix.message
}));
}
}
return result;
}
/**
* Process input from stdin
*/
async processStdin(options) {
return new Promise((resolve, reject) => {
let input = '';
let hasData = false;
// Set up stdin reading with timeout for empty input
process.stdin.setEncoding('utf8');
// Set a timeout to detect if no input is provided
const timeout = setTimeout(() => {
if (!hasData) {
reject(new Error('No input provided via stdin. Use --help for usage information.'));
}
}, 100); // 100ms timeout for detecting empty stdin
process.stdin.on('data', (chunk) => {
hasData = true;
clearTimeout(timeout);
input += chunk;
});
process.stdin.on('end', async () => {
clearTimeout(timeout);
try {
// Check for empty input
if (!input.trim()) {
reject(new Error('Empty input provided via stdin'));
return;
}
if (!options.silent && options.verbose) {
console.log('Processing stdin...');
}
const validationResult = await this.validator.validateString(input, options.fix || false);
// If fixing was requested and fixes were applied, log the information
if (options.fix && validationResult.fixedData && validationResult.appliedFixes && validationResult.appliedFixes.length > 0) {
if (!options.silent && options.verbose) {
console.log(`Applied ${validationResult.appliedFixes.length} fixes to stdin input`);
}
}
const fileResult = {
file: '<stdin>',
valid: validationResult.valid,
errors: validationResult.errors.length,
warnings: validationResult.warnings.length,
fixes: validationResult.appliedFixes?.length || 0
};
// Add detailed error information if verbose or JSON format
if (options.verbose || options.format === 'json') {
if (validationResult.errors.length > 0) {
fileResult.errorDetails = validationResult.errors.map(error => ({
path: error.path,
message: error.message,
code: error.code
}));
}
if (validationResult.warnings.length > 0) {
fileResult.warningDetails = validationResult.warnings.map(warning => ({
path: warning.path,
message: warning.message,
code: warning.code
}));
}
if (validationResult.appliedFixes && validationResult.appliedFixes.length > 0) {
fileResult.fixDetails = validationResult.appliedFixes.map(fix => ({
path: fix.path,
message: fix.message
}));
}
}
const result = {
success: validationResult.valid,
filesProcessed: 1,
validFiles: validationResult.valid ? 1 : 0,
invalidFiles: validationResult.valid ? 0 : 1,
totalErrors: validationResult.errors.length,
totalWarnings: validationResult.warnings.length,
totalFixes: validationResult.appliedFixes?.length || 0,
fileResults: [fileResult]
};
resolve(result);
}
catch (error) {
reject(error);
}
});
process.stdin.on('error', (error) => {
clearTimeout(timeout);
reject(new Error(`Error reading from stdin: ${error.message}`));
});
// Check if stdin is a TTY (interactive terminal)
if (process.stdin.isTTY) {
clearTimeout(timeout);
reject(new Error('No input provided. Please provide input via stdin or specify files to validate.'));
return;
}
// Start reading
process.stdin.resume();
});
}
/**
* Output the validation results
*/
async outputResult(result, options) {
let output;
if (options.format === 'json') {
output = JSON.stringify(result, null, 2);
}
else {
output = this.formatTextOutput(result, options);
}
if (options.output) {
// Write to file
try {
const outputDir = path.dirname(options.output);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
fs.writeFileSync(options.output, output, 'utf8');
if (!options.silent) {
console.log(`Results written to: ${options.output}`);
}
}
catch (error) {
throw new Error(`Failed to write output file: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
else {
// Write to stdout
console.log(output);
}
}
/**
* Write fixed data to output file when fixing is enabled
*/
async writeFixedData(fixedData, outputPath, options) {
try {
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const fixedJson = JSON.stringify(fixedData, null, 2);
fs.writeFileSync(outputPath, fixedJson, 'utf8');
if (!options.silent) {
console.log(`Fixed data written to: ${outputPath}`);
}
}
catch (error) {
throw new Error(`Failed to write fixed data: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Format results as human-readable text
*/
formatTextOutput(result, options) {
const lines = [];
if (!options.silent) {
// Summary
lines.push(colorize('RDFormat Validation Results', 'bold'));
lines.push(colorize('==========================', 'bold'));
lines.push('');
lines.push(`Files processed: ${colorize(result.filesProcessed.toString(), 'cyan')}`);
lines.push(`Valid files: ${colorize(result.validFiles.toString(), 'green')}`);
lines.push(`Invalid files: ${colorize(result.invalidFiles.toString(), result.invalidFiles > 0 ? 'red' : 'green')}`);
lines.push(`Total errors: ${colorize(result.totalErrors.toString(), result.totalErrors > 0 ? 'red' : 'green')}`);
lines.push(`Total warnings: ${colorize(result.totalWarnings.toString(), result.totalWarnings > 0 ? 'yellow' : 'green')}`);
if (result.totalFixes > 0) {
lines.push(`Total fixes applied: ${colorize(result.totalFixes.toString(), 'blue')}`);
}
lines.push('');
// Per-file results
if (result.fileResults.length > 1 || options.verbose) {
lines.push(colorize('File Results:', 'bold'));
lines.push(colorize('-------------', 'bold'));
for (const fileResult of result.fileResults) {
const status = fileResult.valid ? colorize('✓', 'green') : colorize('✗', 'red');
let line = `${status} ${fileResult.file}`;
if (!fileResult.valid || options.verbose) {
const details = [];
if (fileResult.errors > 0)
details.push(colorize(`${fileResult.errors} errors`, 'red'));
if (fileResult.warnings > 0)
details.push(colorize(`${fileResult.warnings} warnings`, 'yellow'));
if (fileResult.fixes > 0)
details.push(colorize(`${fileResult.fixes} fixes`, 'blue'));
if (details.length > 0) {
line += ` (${details.join(', ')})`;
}
}
lines.push(line);
// Add detailed error, warning, and fix information in verbose mode
if (options.verbose) {
// Show errors
if (fileResult.errorDetails && fileResult.errorDetails.length > 0) {
lines.push(colorize(' Errors:', 'red'));
for (const error of fileResult.errorDetails) {
const pathStr = error.path ? `${colorize(error.path, 'gray')}: ` : '';
lines.push(` ${colorize('✗', 'red')} ${pathStr}${error.message} ${colorize(`(${error.code})`, 'gray')}`);
}
}
// Show warnings
if (fileResult.warningDetails && fileResult.warningDetails.length > 0) {
lines.push(colorize(' Warnings:', 'yellow'));
for (const warning of fileResult.warningDetails) {
const pathStr = warning.path ? `${colorize(warning.path, 'gray')}: ` : '';
lines.push(` ${colorize('⚠', 'yellow')} ${pathStr}${warning.message} ${colorize(`(${warning.code})`, 'gray')}`);
}
}
// Show fixes
if (fileResult.fixDetails && fileResult.fixDetails.length > 0) {
lines.push(colorize(' Fixes Applied:', 'blue'));
for (const fix of fileResult.fixDetails) {
const pathStr = fix.path ? `${colorize(fix.path, 'gray')}: ` : '';
lines.push(` ${colorize('✓', 'blue')} ${pathStr}${fix.message}`);
}
}
// Add spacing between files if there are multiple
if (result.fileResults.length > 1) {
lines.push('');
}
}
}
if (!options.verbose) {
lines.push('');
}
}
// Overall result
if (result.success) {
lines.push(colorize('✓ All files are valid RDFormat', 'green'));
}
else {
lines.push(colorize('✗ Some files have validation errors', 'red'));
}
}
return lines.join('\n');
}
/**
* Parse command-line arguments and execute
*/
async parseAndExecute(args) {
try {
await this.program.parseAsync(args);
return 0; // If we get here, the action handler should have called process.exit
}
catch (error) {
console.error('Error parsing arguments:', error instanceof Error ? error.message : 'Unknown error');
return 1;
}
}
}
exports.CLI = CLI;
/**
* Create and return a new CLI instance
*/
function createCLI() {
return new CLI();
}
/**
* Main entry point for CLI execution
* @param args - Command-line arguments (defaults to process.argv)
*/
async function main(args = process.argv) {
const cli = createCLI();
return cli.parseAndExecute(args);
}
//# sourceMappingURL=index.js.map