@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
JavaScript
/**
* 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);
});