agentsqripts
Version:
Comprehensive static code analysis toolkit for identifying technical debt, security vulnerabilities, performance issues, and code quality problems
268 lines (242 loc) • 8.42 kB
JavaScript
/**
* @file Centralized command-line argument parser for CLI tool consistency
* @description Single responsibility: Provide unified argument parsing across all CLI interfaces
*
* This module implements a centralized argument parsing system that ensures consistent
* command-line interfaces across all AgentSqripts CLI tools. It provides common options,
* validation patterns, and parsing logic while allowing tool-specific customization
* through option merging and override capabilities.
*
* Design rationale:
* - Centralized parsing prevents CLI inconsistencies across tools
* - Common options reduce learning curve for users switching between tools
* - Extensible design allows tool-specific options while maintaining consistency
* - Type coercion and validation prevent common CLI input errors
* - Default value system provides sensible out-of-box behavior
*/
const path = require('path');
/**
* Common argument parser configuration with validation and type coercion
*
* Configuration structure rationale:
* - flags: Multiple flag variants (long and short) improve user experience
* - parser: Type coercion functions ensure correct data types and validation
* - default: Sensible defaults reduce required CLI arguments
* - description: Consistent help text across all tools
* - boolean: Special handling for boolean flags that don't require values
*
* Common options design philosophy:
* - Help options using standard conventions (--help, -h)
* - Output format standardization across all analysis tools
* - File extension filtering with intelligent defaults for JavaScript projects
* - Verbosity control for debugging and detailed analysis
* - Performance controls (max files) for large projects
* - Result limiting (max top) for focused output
*/
const COMMON_OPTIONS = {
help: {
flags: ['--help', '-h'],
description: 'Show help message'
},
outputFormat: {
flags: ['--output-format', '-f'],
values: ['json', 'summary', 'detailed'],
default: 'summary',
description: 'Output format'
},
extensions: {
flags: ['--extensions', '--ext'],
parser: (value) => value.split(',').map(ext => ext.trim()),
default: ['.js', '.ts', '.jsx', '.tsx'],
description: 'File extensions to analyze'
},
verbose: {
flags: ['--verbose', '-v'],
boolean: true,
description: 'Show detailed output'
},
maxFiles: {
flags: ['--max-files'],
parser: parseInt,
description: 'Maximum number of files to analyze'
},
maxTop: {
flags: ['--max-top'],
parser: parseInt,
default: 10,
description: 'Maximum number of top issues to show'
}
};
/**
* Parse command line arguments with validation and error handling
* @param {Array} args - Process arguments array (including node, script.js, ...)
* @param {Object} config - Configuration object with defaults and flags
* @returns {Object} { options, targetPath, error } - Parsed results or error
*/
function parseArgs(args = [], config = {}) {
const { defaults = {}, flags = {} } = config;
const options = { ...defaults };
let targetPath = null;
let error = null;
// Helper function to convert kebab-case to camelCase
function kebabToCamel(str) {
return str.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
}
// Handle both process.argv format and direct arguments format
let cleanArgs;
if (args.length >= 2 && (args[0].includes('node') || args[1].includes('.js'))) {
// process.argv format: ['node', 'script.js', ...]
cleanArgs = args.slice(2);
} else {
// Direct arguments format: ['.', '--flag', ...]
cleanArgs = args;
}
for (let i = 0; i < cleanArgs.length; i++) {
const arg = cleanArgs[i];
// Handle flags
if (arg.startsWith('--')) {
const rawFlagName = arg.replace('--', '');
const flagName = kebabToCamel(rawFlagName);
const flagConfig = flags[flagName];
if (flagConfig) {
if (flagConfig.type === 'boolean') {
options[flagName] = true;
} else if (i + 1 < cleanArgs.length) {
const value = cleanArgs[++i];
// Handle list type
if (flagConfig.type === 'list') {
options[flagName] = value.split(',').map(item => item.trim());
} else {
options[flagName] = value;
}
// Validate if validator provided
if (flagConfig.validate && !flagConfig.validate(options[flagName])) {
error = `Invalid value for ${flagName}: ${value}`;
break;
}
} else {
error = `Missing value for flag: ${arg}`;
break;
}
} else {
// Unknown flag, ignore for now
if (i + 1 < cleanArgs.length && !cleanArgs[i + 1].startsWith('--')) {
i++; // Skip the value too
}
}
} else if (!arg.startsWith('-')) {
// Non-flag argument is the target path
if (!targetPath) {
targetPath = arg;
}
}
}
// Check if target path is required when no arguments or only flags provided
// But don't require path if help flag is present
const hasHelpFlag = cleanArgs.some(arg => arg === '--help' || arg === '-h');
if (!targetPath && cleanArgs.length === 0 && !hasHelpFlag) {
error = 'Missing required target path argument';
}
return {
options,
targetPath,
error
};
}
/**
* Legacy parseArgs function for backward compatibility
* @param {Array} args - Process arguments (process.argv.slice(2))
* @param {Object} toolOptions - Tool-specific options to merge with common ones
* @returns {Object} Parsed options and target path
*/
function parseArgsLegacy(args = [], toolOptions = {}) {
const allOptions = { ...COMMON_OPTIONS, ...toolOptions };
const options = {};
// Set defaults
Object.entries(allOptions).forEach(([key, config]) => {
if (config.default !== undefined) {
options[key] = config.default;
}
if (config.boolean) {
options[key] = false;
}
});
let targetPath = '.';
let mode = 'project';
for (let i = 0; i < args.length; i++) {
const arg = args[i];
let handled = false;
// Check each option
for (const [key, config] of Object.entries(allOptions)) {
if (config.flags && config.flags.includes(arg)) {
if (config.boolean) {
options[key] = true;
} else if (i + 1 < args.length) {
const value = args[++i];
if (config.parser) {
options[key] = config.parser(value);
} else if (config.values && config.values.includes(value)) {
options[key] = value;
} else {
options[key] = value;
}
}
handled = true;
break;
}
}
// If not an option, it's the target path
if (!handled && !arg.startsWith('-')) {
targetPath = arg;
}
}
// Determine mode based on target
if (targetPath && targetPath !== '.') {
try {
const stats = require('fs').statSync(targetPath);
mode = stats.isDirectory() ? 'project' : 'file';
} catch (error) {
// If path doesn't exist yet, default to project mode
mode = 'project';
}
}
return {
targetPath: path.resolve(targetPath),
mode,
options
};
}
/**
* Build help text from options configuration
* @param {Object} toolOptions - Tool-specific options
* @param {Object} toolInfo - Tool information (name, description, examples)
* @returns {string} Help text
*/
function buildHelpText(toolOptions = {}, toolInfo = {}) {
const allOptions = { ...COMMON_OPTIONS, ...toolOptions };
const { name = 'analyze', description = '', examples = [] } = toolInfo;
let help = `Usage: ${name} [path] [options]\n\n`;
if (description) {
help += `${description}\n\n`;
}
help += 'Options:\n';
Object.entries(allOptions).forEach(([key, config]) => {
const flags = config.flags.join(', ');
const desc = config.description || '';
const defaultVal = config.default ? ` (default: ${JSON.stringify(config.default)})` : '';
help += ` ${flags.padEnd(25)} ${desc}${defaultVal}\n`;
});
if (examples.length > 0) {
help += '\nExamples:\n';
examples.forEach(example => {
help += ` ${example}\n`;
});
}
return help;
}
module.exports = {
parseArgs,
parseArgsLegacy,
buildHelpText,
COMMON_OPTIONS
};