UNPKG

@dreamhorizonorg/sentinel

Version:

Open-source, zero-dependency tool that blocks compromised packages BEFORE download. Built to counter supply chain and credential theft attacks like Shai-Hulud.

870 lines (734 loc) • 32.5 kB
#!/usr/bin/env node /** * Sentinel Package Manager CLI * * Enterprise-grade CLI for validating npm packages against security blacklists * Zero dependencies - uses only Node.js built-in modules */ import fs from 'fs'; import os from 'os'; import path from 'path'; import { fileURLToPath } from 'url'; import { APP_NAME } from '../lib/constants/app.constants.mjs'; import { CLI_BINARY_NAME, CLI_COMMANDS, CLI_SUBCOMMANDS, FULL_COMMANDS, MAX_COMMAND_SUGGESTIONS, VERSION_FLAGS } from '../lib/constants/cli.constants.mjs'; import { ERROR_MESSAGES, INFO_MESSAGES, SUCCESS_MESSAGES } from '../lib/constants/validation.constants.mjs'; import { checkLockfile, checkPackageJson, listCompromisedPackages, scanRepository, validatePackage } from '../lib/scanner.mjs'; import { isHelpCommand, parseArgs, parsePackageSpec } from '../lib/utils/cli.utils.mjs'; import { colors } from '../lib/utils/color.utils.mjs'; import { loadConfig, mergeConfig } from '../lib/utils/config.utils.mjs'; import { isLockfile, isPackageJson, isPackageSpec, pathExists } from '../lib/utils/file.utils.mjs'; import { logError, logInfo, logSuccess } from '../lib/utils/log.utils.mjs'; import { resolveAbsolutePath } from '../lib/utils/path.utils.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Display help message */ const displayHelp = () => { console.log(` ${colors.bold(APP_NAME)} - Zero Dependencies ${colors.bold('Usage:')} ${CLI_BINARY_NAME} <command> [options] ${colors.bold('Commands:')} scan [target] Scan for compromised packages - No argument: scans entire repository - Package: scan package-name@version - Lockfile: scan package-lock.json|yarn.lock|pnpm-lock.yaml - package.json: scan specific package.json file - Directory: scan specific directory list List all compromised packages add aliases Add shell aliases (npm, yarn, pnpm, bun) for automatic package validation on every install/add command remove aliases Remove shell aliases from your shell configuration status Check installation status and verify configuration init Initialize config file (sentinel.config.json) in current directory ${colors.bold('Options:')} --localDataSource=<path> Local file or folder path (default: uses standard locations) - File path: uses exact file (e.g., ./config/custom.json) - Folder path: looks for compromised-packages.json inside - Not provided: uses default locations --remoteDataSource=<url> HTTP/HTTPS endpoint URL for compromised packages JSON --skipNpmAudit=<true|false> Skip npm audit checks (default: false) --logMode=<mode> Output verbosity: "verbose" | "normal" | "quiet" (default: "normal") - verbose: detailed output with all messages - normal: standard output (default) - quiet: minimal output (errors only) ${colors.bold('Provider Options:')} --enableOsv=<true|false> Enable/disable OSV provider (default: true) --enableGitHub=<true|false> Enable/disable GitHub Advisories provider (default: true) --githubToken=<token> GitHub API token (optional, for higher rate limits) --enableSnyk=<true|false> Enable/disable Snyk provider (default: false, requires token) --snykToken=<token> Snyk API token (required if enabling Snyk) -l, --logMode Alias for --logMode -h, --help Show this help message -v, --version Show version number ${colors.bold('Examples:')} ${FULL_COMMANDS.SCAN} # Scan entire repository ${FULL_COMMANDS.SCAN} package-name@1.2.3 # Scan single package ${FULL_COMMANDS.SCAN} package-lock.json # Scan lockfile ${FULL_COMMANDS.SCAN} package.json # Scan package.json ${FULL_COMMANDS.SCAN} ./path/to/repo # Scan directory ${FULL_COMMANDS.LIST} # List all compromised packages ${FULL_COMMANDS.ADD_ALIASES} # Add shell aliases (npm/yarn/pnpm/bun) ${FULL_COMMANDS.REMOVE_ALIASES} # Remove shell aliases ${FULL_COMMANDS.STATUS} # Check installation status ${FULL_COMMANDS.INIT} # Create sentinel.config.json in current directory ${FULL_COMMANDS.VERSION} # Show version number # With options ${FULL_COMMANDS.SCAN} package-name --localDataSource="./config/blacklist.json" ${FULL_COMMANDS.SCAN} package-name --localDataSource="./config" # Folder (auto-finds JSON) ${FULL_COMMANDS.SCAN} --remoteDataSource="https://api.example.com/blacklist.json" ${FULL_COMMANDS.SCAN} --skipNpmAudit=true --logMode=verbose ${FULL_COMMANDS.SCAN} package-name --logMode=quiet # Provider options ${FULL_COMMANDS.SCAN} package-name --enableOsv=false # Disable OSV ${FULL_COMMANDS.SCAN} package-name --enableGitHub=false # Disable GitHub ${FULL_COMMANDS.SCAN} package-name --githubToken="ghp_..." # GitHub token (optional) ${FULL_COMMANDS.SCAN} package-name --enableSnyk=true --snykToken="..." # Enable Snyk `); }; /** * Handle scan command */ const handleScanCommand = async (target, finalConfig) => { // No target: scan entire repository if (!target) { const repoPath = process.cwd(); logInfo(colors.blue(INFO_MESSAGES.SCANNING_REPOSITORY(colors.bold(repoPath))), finalConfig.logMode); const result = await scanRepository(repoPath, finalConfig); if (!result.success) { process.exit(1); } process.exit(0); } const resolvedPath = resolveAbsolutePath(target); // Check if it's a lockfile if (isLockfile(target, resolvedPath)) { if (!pathExists(resolvedPath)) { logError(colors.red(ERROR_MESSAGES.LOCKFILE_NOT_FOUND(resolvedPath))); process.exit(1); } logInfo(colors.blue(INFO_MESSAGES.CHECKING_LOCKFILE(colors.bold(resolvedPath))), finalConfig.logMode); const isValid = await checkLockfile(resolvedPath, finalConfig); if (!isValid) { process.exit(1); } logSuccess(colors.green(SUCCESS_MESSAGES.LOCKFILE_CLEAN), finalConfig.logMode); process.exit(0); } // Check if it's package.json if (isPackageJson(target, resolvedPath)) { if (!pathExists(resolvedPath)) { logError(colors.red(ERROR_MESSAGES.PACKAGE_JSON_NOT_FOUND(resolvedPath))); process.exit(1); } logInfo(colors.blue(INFO_MESSAGES.CHECKING_PACKAGE_JSON(colors.bold(resolvedPath))), finalConfig.logMode); const isValid = await checkPackageJson(resolvedPath, finalConfig); if (!isValid) { process.exit(1); } logSuccess(colors.green(SUCCESS_MESSAGES.PACKAGE_JSON_CLEAN), finalConfig.logMode); process.exit(0); } // Check if it's a package spec const existsAsFile = pathExists(resolvedPath); if (isPackageSpec(target, existsAsFile)) { const { name, version } = parsePackageSpec(target); logInfo(colors.blue(INFO_MESSAGES.VALIDATING_PACKAGE(colors.bold(target))), finalConfig.logMode); const isValid = await validatePackage(name, version, finalConfig); if (!isValid) { process.exit(1); } logSuccess(colors.green(SUCCESS_MESSAGES.PACKAGE_SAFE(colors.bold(target))), finalConfig.logMode); process.exit(0); } // Otherwise, treat as directory path if (!pathExists(resolvedPath)) { logError(colors.red(ERROR_MESSAGES.PATH_NOT_FOUND(resolvedPath))); process.exit(1); } logInfo(colors.blue(INFO_MESSAGES.SCANNING_DIRECTORY(colors.bold(resolvedPath))), finalConfig.logMode); const result = await scanRepository(resolvedPath, finalConfig); if (!result.success) { process.exit(1); } process.exit(0); }; /** * Handle list command */ const handleListCommand = async (finalConfig) => { try { const packages = await listCompromisedPackages(finalConfig); if (packages.length === 0) { console.log(colors.yellow(ERROR_MESSAGES.NO_COMPROMISED_PACKAGES)); process.exit(0); } console.log(colors.bold(INFO_MESSAGES.COMPROMISED_PACKAGES_LIST(packages.length))); packages.forEach((pkg, index) => { const versions = pkg.allVersions ? colors.red('ALL VERSIONS') : pkg.versions.length > 0 ? colors.yellow(pkg.versions.join(', ')) : colors.yellow('ALL VERSIONS'); console.log(`${index + 1}. ${colors.bold(pkg.name)}`); console.log(` Versions: ${versions}\n`); }); process.exit(0); } catch (error) { console.log(colors.red(`\nāŒ Error during scan: ${error.message}\n`)); // Provide helpful context based on error type if (error.message.includes('ENOENT')) { console.log(colors.yellow('šŸ’” This usually means:')); console.log(colors.dim(' • File or directory does not exist')); console.log(colors.dim(' • Check the path and try again\n')); } else if (error.message.includes('EACCES')) { console.log(colors.yellow('šŸ’” This usually means:')); console.log(colors.dim(' • Permission denied')); console.log(colors.dim(' • Try running with appropriate permissions\n')); } else if (error.message.includes('JSON')) { console.log(colors.yellow('šŸ’” This usually means:')); console.log(colors.dim(' • Invalid JSON in package.json or lock file')); console.log(colors.dim(' • Check file syntax and try again\n')); } if (process.env.DEBUG) { console.log(colors.dim('Stack trace:')); console.log(colors.dim(error.stack)); } else { console.log(colors.dim('Run with DEBUG=1 for more details\n')); } process.exit(1); } }; /** * Check if command is version request */ const isVersionCommand = (command) => { return VERSION_FLAGS.includes(command); }; /** * Get similar commands using Levenshtein distance * Suggests commands that are likely typos */ const getSimilarCommands = (input) => { const allCommands = [ 'scan', 'list', 'add aliases', 'remove aliases', 'status', 'init', '--help', '--version' ]; // Simple similarity check based on common typos const suggestions = []; // Check for exact substring matches first allCommands.forEach(cmd => { if (cmd.includes(input) || input.includes(cmd.split(' ')[0])) { suggestions.push(`${CLI_BINARY_NAME} ${cmd}`); } }); // Check for common typos const typoMap = { 'instal': 'add aliases', 'install': 'add aliases', 'uninstal': 'remove aliases', 'uninstall': 'remove aliases', 'delete': 'remove aliases', 'check': 'status', 'verify': 'status', 'setup': 'init', 'config': 'init', 'create': 'init', 'scan-all': 'scan', 'validate': 'scan', 'ls': 'list', 'show': 'list', 'help': '--help', 'version': '--version', 'ver': '--version' }; if (typoMap[input.toLowerCase()]) { const suggestion = `${CLI_BINARY_NAME} ${typoMap[input.toLowerCase()]}`; if (!suggestions.includes(suggestion)) { suggestions.push(suggestion); } } return suggestions.slice(0, MAX_COMMAND_SUGGESTIONS); }; /** * Display version */ const displayVersion = () => { try { const packageJsonPath = path.join(__dirname, '..', 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); console.log(`v${packageJson.version}`); } catch (error) { console.log('Unable to determine version'); } }; /** * Handle remove-aliases command * Removes shell aliases from the user's shell configuration */ const handleRemoveAliasesCommand = () => { console.log(colors.bold(`\nšŸ—‘ļø ${APP_NAME} - Remove Shell Aliases\n`)); try { // Detect shell const shellEnv = process.env.SHELL ?? ''; let shellRc = ''; let shellName = ''; if (shellEnv.includes('zsh')) { shellRc = path.join(os.homedir(), '.zshrc'); shellName = 'zsh'; } else if (shellEnv.includes('bash')) { const bashrc = path.join(os.homedir(), '.bashrc'); const bashProfile = path.join(os.homedir(), '.bash_profile'); if (fs.existsSync(bashrc)) { shellRc = bashrc; } else if (fs.existsSync(bashProfile)) { shellRc = bashProfile; } else { logError(colors.red('āŒ No shell config file found.')); process.exit(1); } shellName = 'bash'; } else { logError(colors.red('āŒ Unsupported shell. Currently supports: bash, zsh')); process.exit(1); } console.log(colors.blue(`šŸ“‹ Detected shell: ${shellName}`)); console.log(colors.blue(`šŸ“„ Config file: ${shellRc}\n`)); // Check if config file exists if (!fs.existsSync(shellRc)) { console.log(colors.yellow('āš ļø Shell config file does not exist.')); console.log(colors.yellow(' Nothing to uninstall.\n')); process.exit(0); } // Read shell config const shellContent = fs.readFileSync(shellRc, 'utf-8'); // Check if aliases exist if (!shellContent.includes(CLI_BINARY_NAME)) { console.log(colors.yellow('āš ļø No Sentinel Package Manager aliases found.')); console.log(colors.yellow(' Nothing to uninstall.\n')); process.exit(0); } // Remove aliases const lines = shellContent.split('\n'); const filteredLines = []; let inSentinelBlock = false; let removedCount = 0; for (const line of lines) { if (line.trim() === '# Sentinel Package Manager') { inSentinelBlock = true; removedCount++; continue; } if (inSentinelBlock && line.includes(CLI_BINARY_NAME)) { removedCount++; continue; } if (inSentinelBlock && line.trim() === '') { inSentinelBlock = false; continue; } filteredLines.push(line); } // Write back to file fs.writeFileSync(shellRc, filteredLines.join('\n'), 'utf-8'); console.log(colors.green('āœ… Shell aliases removed successfully!\n')); console.log(colors.bold('Removed from:')); console.log(colors.blue(` ${shellRc}\n`)); console.log(colors.bold(`Removed ${removedCount} line(s)\n`)); console.log(colors.bold('šŸ“ Next Steps:\n')); console.log(colors.yellow(` 1. Reload your shell to apply changes:`)); console.log(colors.green(` source ${shellRc}\n`)); console.log(colors.yellow(` 2. Or open a new terminal\n`)); console.log(colors.yellow(` 3. (Optional) Uninstall the package:`)); console.log(colors.green(` npm uninstall -g ${CLI_BINARY_NAME}\n`)); console.log(colors.dim('šŸ’” Your package managers (npm, yarn, pnpm, bun) will now work normally without validation.\n')); process.exit(0); } catch (error) { logError(colors.red(`āŒ Failed to remove aliases: ${error.message}`)); if (process.env.DEBUG) { console.error(error.stack); } process.exit(1); } }; /** * Handle status command * Checks installation status and verifies configuration */ const handleStatusCommand = async (config) => { console.log(colors.bold(`\nšŸ” ${APP_NAME} - Status Check\n`)); try { let allGood = true; // 1. Check package installation console.log(colors.bold('šŸ“¦ Package Installation:')); try { const packageJsonPath = path.join(__dirname, '..', 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); console.log(colors.green(` āœ… Installed: v${packageJson.version}`)); } catch (error) { console.log(colors.red(` āŒ Error reading package.json`)); allGood = false; } console.log(''); // 2. Check shell aliases console.log(colors.bold('šŸ”— Shell Aliases:')); const shellEnv = process.env.SHELL ?? ''; let shellRc = ''; if (shellEnv.includes('zsh')) { shellRc = path.join(os.homedir(), '.zshrc'); } else if (shellEnv.includes('bash')) { shellRc = path.join(os.homedir(), '.bashrc'); if (!fs.existsSync(shellRc)) { shellRc = path.join(os.homedir(), '.bash_profile'); } } if (shellRc && fs.existsSync(shellRc)) { const shellContent = fs.readFileSync(shellRc, 'utf-8'); if (shellContent.includes(CLI_BINARY_NAME)) { console.log(colors.green(` āœ… Aliases configured in ${shellRc}`)); console.log(colors.dim(` - npm, yarn, pnpm, bun aliases active`)); } else { console.log(colors.yellow(` āš ļø Aliases not configured`)); console.log(colors.dim(` Run: ${FULL_COMMANDS.ADD_ALIASES}`)); // Aliases are optional - don't fail status check } } else { console.log(colors.yellow(` āš ļø Shell config file not found`)); // Shell config is optional (especially in CI) - don't fail status check } console.log(''); // 3. Check config file console.log(colors.bold('āš™ļø Configuration:')); const cwd = process.cwd(); const localConfigPath = path.join(cwd, 'sentinel.config.json'); if (fs.existsSync(localConfigPath)) { console.log(colors.green(` āœ… Local config found: ${localConfigPath}`)); try { const localConfig = JSON.parse(fs.readFileSync(localConfigPath, 'utf-8')); console.log(colors.dim(` - logMode: ${localConfig.logMode || 'normal'}`)); console.log(colors.dim(` - skipNpmAudit: ${localConfig.skipNpmAudit || false}`)); } catch (error) { console.log(colors.red(` āŒ Error reading config: ${error.message}`)); allGood = false; } } else { console.log(colors.dim(` ā„¹ļø No local config (using defaults)`)); console.log(colors.dim(` Run: ${FULL_COMMANDS.INIT}`)); } console.log(''); // 4. Check blacklist console.log(colors.bold('šŸ›”ļø Security Blacklist:')); try { const packages = await listCompromisedPackages(config); console.log(colors.green(` āœ… Blacklist loaded: ${packages.length} compromised packages`)); } catch (error) { console.log(colors.red(` āŒ Error loading blacklist: ${error.message}`)); allGood = false; } console.log(''); // 5. Check wrapper script console.log(colors.bold('šŸ”§ Wrapper Script:')); const scriptPath = path.resolve(__dirname, '..', 'bin', 'sentinel.sh'); if (fs.existsSync(scriptPath)) { console.log(colors.green(` āœ… Wrapper script found`)); console.log(colors.dim(` Location: ${scriptPath}`)); } else { console.log(colors.red(` āŒ Wrapper script not found`)); allGood = false; } console.log(''); // Summary console.log(colors.bold('═'.repeat(60))); if (allGood) { console.log(colors.green('\nāœ… Everything looks good! Sentinel Package Manager is ready.\n')); } else { console.log(colors.yellow('\nāš ļø Some issues detected. See details above.\n')); } console.log(colors.bold('šŸ’” Quick Commands:\n')); console.log(colors.dim(` ${FULL_COMMANDS.ADD_ALIASES} # Set up shell aliases`)); console.log(colors.dim(` ${FULL_COMMANDS.INIT} # Create config file`)); console.log(colors.dim(` ${FULL_COMMANDS.SCAN} # Scan repository`)); console.log(colors.dim(` ${FULL_COMMANDS.LIST} # List compromised packages\n`)); process.exit(allGood ? 0 : 1); } catch (error) { logError(colors.red(`āŒ Status check failed: ${error.message}`)); if (process.env.DEBUG) { console.error(error.stack); } process.exit(1); } }; /** * Handle init command * Creates a sentinel.config.json file in the current directory */ const handleInitCommand = () => { console.log(colors.bold(`\nšŸš€ ${APP_NAME} - Initialize Configuration\n`)); try { const cwd = process.cwd(); const configPath = path.join(cwd, 'sentinel.config.json'); // Check if config already exists if (fs.existsSync(configPath)) { console.log(colors.yellow(`āš ļø Config file already exists: ${configPath}`)); console.log(colors.yellow(' Skipping creation to avoid overwriting.\n')); console.log(colors.blue('šŸ’” To recreate, delete the existing file and run init again:')); console.log(colors.dim(` rm sentinel.config.json && ${FULL_COMMANDS.INIT}\n`)); process.exit(0); } // Default config template const defaultConfig = { skipNpmAudit: false, logMode: "normal" }; const configWithComments = `{ // Skip npm audit checks (faster, but less secure) // Default: false "skipNpmAudit": false, // Output verbosity: "verbose" | "normal" | "quiet" // - verbose: detailed output with all messages // - normal: standard output (default) // - quiet: minimal output (errors only) "logMode": "normal" // Optional: Local file or folder path for custom blacklist // "dataSourcePath": "./config/compromised-packages.json", // Optional: HTTP/HTTPS endpoint for custom blacklist // "endpoint": "https://example.com/api/compromised-packages.json" } `; // Write config file fs.writeFileSync(configPath, configWithComments, 'utf-8'); console.log(colors.green('āœ… Configuration file created successfully!\n')); console.log(colors.bold('Created file:')); console.log(colors.blue(` ${configPath}\n`)); console.log(colors.bold('šŸ“‹ Default Configuration:\n')); console.log(colors.dim(' {')); console.log(colors.dim(' "skipNpmAudit": false,')); console.log(colors.dim(' "logMode": "normal"')); console.log(colors.dim(' }\n')); console.log(colors.bold('āš™ļø Configuration Options:\n')); console.log(colors.blue(' skipNpmAudit') + ' - Skip npm audit checks (default: false)'); console.log(colors.blue(' logMode') + ' - Output verbosity: "verbose" | "normal" | "quiet"'); console.log(colors.blue(' dataSourcePath') + ' - Local file/folder path for custom blacklist'); console.log(colors.blue(' endpoint') + ' - HTTP/HTTPS URL for custom blacklist\n'); console.log(colors.bold('šŸ“ Next Steps:\n')); console.log(colors.yellow(' 1. Edit the config file to customize behavior:')); console.log(colors.green(` nano sentinel.config.json\n`)); console.log(colors.yellow(' 2. Run scan to test your configuration:')); console.log(colors.green(` ${FULL_COMMANDS.SCAN}\n`)); console.log(colors.yellow(' 3. (Optional) Set up shell aliases for automatic validation:')); console.log(colors.green(` ${FULL_COMMANDS.ADD_ALIASES}\n`)); console.log(colors.dim('šŸ’” The config file is automatically detected and loaded when you run commands.\n')); process.exit(0); } catch (error) { logError(colors.red(`āŒ Failed to create config file: ${error.message}`)); if (process.env.DEBUG) { console.error(error.stack); } process.exit(1); } }; /** * Handle add-aliases command * Creates shell aliases for npm, yarn, pnpm, and bun that intercept commands * and validate packages before installation */ const handleAddAliasesCommand = () => { console.log(colors.bold(`\nšŸ”§ ${APP_NAME} - Add Shell Aliases\n`)); console.log(colors.blue('This command will:')); console.log(colors.blue(' 1. Detect your shell (bash/zsh)')); console.log(colors.blue(' 2. Add aliases to your shell config file (.zshrc, .bashrc, etc.)')); console.log(colors.blue(' 3. Intercept npm/yarn/pnpm/bun commands for automatic validation\n')); try { // Detect shell const shellEnv = process.env.SHELL ?? ''; let shellRc = ''; let shellName = ''; if (shellEnv.includes('zsh')) { shellRc = path.join(os.homedir(), '.zshrc'); shellName = 'zsh'; } else if (shellEnv.includes('bash')) { // Try .bashrc first, then .bash_profile const bashrc = path.join(os.homedir(), '.bashrc'); const bashProfile = path.join(os.homedir(), '.bash_profile'); if (fs.existsSync(bashrc)) { shellRc = bashrc; } else if (fs.existsSync(bashProfile)) { shellRc = bashProfile; } else { // Create .bashrc if neither exists shellRc = bashrc; fs.writeFileSync(shellRc, '# Bash configuration\n', 'utf-8'); } shellName = 'bash'; } else { logError(colors.red('āŒ Unsupported shell. Currently supports: bash, zsh')); console.log(colors.yellow('\nšŸ’” Please add aliases manually to your shell config file.')); process.exit(1); } console.log(colors.blue(`šŸ“‹ Detected shell: ${shellName}`)); console.log(colors.blue(`šŸ“„ Config file: ${shellRc}\n`)); // Determine installation directory // If globally installed via npm, use npm's global node_modules // Otherwise, use the script's current location const scriptPath = path.resolve(__dirname, '..'); const wrapperScript = path.join(scriptPath, 'bin', 'sentinel.sh'); if (!fs.existsSync(wrapperScript)) { logError(colors.red(`āŒ Error: Wrapper script not found at ${wrapperScript}`)); process.exit(1); } console.log(colors.blue(`šŸ“¦ Package location: ${scriptPath}\n`)); // Check if aliases already exist const shellContent = fs.existsSync(shellRc) ? fs.readFileSync(shellRc, 'utf-8') : ''; if (shellContent.includes(CLI_BINARY_NAME)) { console.log(colors.yellow('āš ļø Aliases already exist in your shell config.')); console.log(colors.yellow(' Updating aliases...\n')); // Remove old aliases const lines = shellContent.split('\n'); const filteredLines = lines.filter(line => !line.includes(CLI_BINARY_NAME)); fs.writeFileSync(shellRc, filteredLines.join('\n'), 'utf-8'); } // Add new aliases const aliasBlock = ` # Sentinel Package Manager alias npm='${wrapperScript} npm' alias yarn='${wrapperScript} yarn' alias pnpm='${wrapperScript} pnpm' alias bun='${wrapperScript} bun' `; fs.appendFileSync(shellRc, aliasBlock, 'utf-8'); console.log(colors.green('āœ… Shell aliases installed successfully!\n')); console.log(colors.bold('Created aliases:')); console.log(colors.blue(` npm → Validates packages before npm install/add`)); console.log(colors.blue(` yarn → Validates packages before yarn add`)); console.log(colors.blue(` pnpm → Validates packages before pnpm add`)); console.log(colors.blue(` bun → Validates packages before bun add\n`)); console.log(colors.bold('šŸ“ Next Steps:\n')); console.log(colors.yellow(` 1. Reload your shell to activate aliases:`)); console.log(colors.green(` source ${shellRc}\n`)); console.log(colors.yellow(` 2. Or open a new terminal\n`)); console.log(colors.yellow(` 3. Use package managers normally - protection is automatic!`)); console.log(colors.green(` npm install express # āœ… Validates before installing`)); console.log(colors.green(` yarn add axios # āœ… Validates before installing`)); console.log(colors.green(` pnpm add react # āœ… Validates before installing\n`)); console.log(colors.bold('šŸ›”ļø All package installations will now be automatically validated!\n')); console.log(colors.dim(`Technical: Aliases added to ${shellRc}`)); process.exit(0); } catch (error) { logError(colors.red(`āŒ Setup failed: ${error.message}`)); if (process.env.DEBUG) { console.error(error.stack); } process.exit(1); } }; /** * Main CLI entry point */ const main = async () => { const { command, subcommand, options, positional } = parseArgs(); const config = loadConfig(); const finalConfig = mergeConfig(config, options); if (isHelpCommand(command)) { displayHelp(); process.exit(0); } if (isVersionCommand(command)) { displayVersion(); process.exit(0); } if (command === CLI_COMMANDS.SCAN) { const target = positional[0]; await handleScanCommand(target, finalConfig); } if (command === CLI_COMMANDS.LIST) { await handleListCommand(finalConfig); } if (command === CLI_COMMANDS.ADD) { if (!subcommand) { console.log(colors.red('\nāŒ Error: Missing subcommand for "add"\n')); console.log(colors.yellow('Usage:')); console.log(colors.green(` ${FULL_COMMANDS.ADD_ALIASES}\n`)); console.log(colors.dim('šŸ’” The "add" command requires a subcommand.')); console.log(colors.dim(' Available: aliases\n')); process.exit(1); } if (subcommand === CLI_SUBCOMMANDS.ALIASES) { handleAddAliasesCommand(); } else { console.log(colors.red(`\nāŒ Error: Unknown subcommand "add ${subcommand}"\n`)); console.log(colors.yellow('Did you mean:')); console.log(colors.green(` ${FULL_COMMANDS.ADD_ALIASES}\n`)); console.log(colors.dim('šŸ’” Available subcommands for "add":')); console.log(colors.dim(' • aliases - Add shell aliases for package managers\n')); process.exit(1); } } if (command === CLI_COMMANDS.INIT) { handleInitCommand(); } if (command === CLI_COMMANDS.REMOVE) { if (!subcommand) { console.log(colors.red('\nāŒ Error: Missing subcommand for "remove"\n')); console.log(colors.yellow('Usage:')); console.log(colors.green(` ${FULL_COMMANDS.REMOVE_ALIASES}\n`)); console.log(colors.dim('šŸ’” The "remove" command requires a subcommand.')); console.log(colors.dim(' Available: aliases\n')); process.exit(1); } if (subcommand === CLI_SUBCOMMANDS.ALIASES) { handleRemoveAliasesCommand(); } else { console.log(colors.red(`\nāŒ Error: Unknown subcommand "remove ${subcommand}"\n`)); console.log(colors.yellow('Did you mean:')); console.log(colors.green(` ${FULL_COMMANDS.REMOVE_ALIASES}\n`)); console.log(colors.dim('šŸ’” Available subcommands for "remove":')); console.log(colors.dim(' • aliases - Remove shell aliases for package managers\n')); process.exit(1); } } if (command === CLI_COMMANDS.STATUS) { await handleStatusCommand(finalConfig); } // Unknown command - provide helpful suggestions if (command) { console.log(colors.red(`\nāŒ Error: Unknown command "${command}"\n`)); // Suggest similar commands const suggestions = getSimilarCommands(command); if (suggestions.length > 0) { console.log(colors.yellow('Did you mean:')); suggestions.forEach(cmd => { console.log(colors.green(` ${cmd}`)); }); console.log(''); } console.log(colors.dim('šŸ’” Available commands:')); console.log(colors.dim(' scan [target] - Scan for compromised packages')); console.log(colors.dim(' list - List all compromised packages')); console.log(colors.dim(' add aliases - Add shell aliases')); console.log(colors.dim(' remove aliases - Remove shell aliases')); console.log(colors.dim(' status - Check installation status')); console.log(colors.dim(' init - Initialize config file')); console.log(colors.dim(' --help - Show help message')); console.log(colors.dim(' --version - Show version\n')); } else { console.log(colors.red('\nāŒ Error: No command provided\n')); console.log(colors.yellow('Usage:')); console.log(colors.green(` ${CLI_BINARY_NAME} <command> [options]\n`)); console.log(colors.dim(`šŸ’” Run "${FULL_COMMANDS.HELP}" for available commands\n`)); } process.exit(1); }; main().catch(error => { logError(colors.red(`āŒ Error: ${error.message}`)); if (process.env.DEBUG) { console.error(error.stack); } process.exit(1); });