@ansospace/sysmon
Version:
A zero-dependency system monitoring CLI tool for listing system info, real-time monitoring, and cleanup tasks.
565 lines (500 loc) • 20 kB
JavaScript
import fs from 'fs';
import os from 'os';
import path from 'path';
import readline from 'readline';
import { execSync } from 'child_process';
const formatBytes = (bytes) => {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let i = 0;
while (bytes >= 1024 && i < units.length - 1) {
bytes /= 1024;
i++;
}
return `${bytes.toFixed(2)} ${units[i]}`;
};
const formatUptime = (seconds) => {
const d = Math.floor(seconds / 86400);
const h = Math.floor((seconds % 86400) / 3600);
const m = Math.floor((seconds % 3600) / 60);
return `${d}d ${h}h ${m}m`;
};
const getPercentBar = (percent, width = 20) => {
const filled = Math.round((percent / 100) * width);
const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
const color = percent > 90 ? '\x1b[31m' : percent > 70 ? '\x1b[33m' : '\x1b[32m';
return `${color}${bar}\x1b[0m ${percent.toFixed(1)}%`;
};
const clearScreen = () => {
process.stdout.write('\x1b[2J\x1b[H');
};
const prompt = (question) => {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim());
});
});
};
const printHeader = (title) => {
console.log('\x1b[36m╔════════════════════════════════════════════════════════════╗\x1b[0m');
console.log(`\x1b[36m║${title.padStart(Math.floor((52 + title.length) / 2)).padEnd(52)}║\x1b[0m`);
console.log('\x1b[36m╚════════════════════════════════════════════════════════════╝\x1b[0m\n');
};
const runBenchmarkStep = async (description, benchmarkFn) => {
console.log(`Running ${description}...`);
const score = await benchmarkFn();
console.log(`${description} Score: ${score}\n`);
return score;
};
class SystemMonitor {
constructor() {
this.running = false;
this.interval = null;
this.history = {
cpu: [],
memory: [],
timestamps: []
};
}
getCPUUsage() {
const cpus = os.cpus();
let totalIdle = 0, totalTick = 0;
cpus.forEach(cpu => {
for (let type in cpu.times) totalTick += cpu.times[type];
totalIdle += cpu.times.idle;
});
return { idle: totalIdle / cpus.length, total: totalTick / cpus.length, usage: 100 - (100 * totalIdle / totalTick) };
}
getMemoryUsage() {
const total = os.totalmem();
const free = os.freemem();
const used = total - free;
return { total, free, used, percent: (used / total) * 100 };
}
getDiskUsage() {
try {
const platform = os.platform();
let output;
if (platform === 'win32') {
output = execSync('wmic logical disk get size,freespace,caption', { encoding: 'utf8' });
const lines = output.split('\n').filter(l => l.trim());
const disks = [];
for (let i = 1; i < lines.length; i++) {
const parts = lines[i].trim().split(/\s+/);
if (parts.length >= 3) {
const [drive, free, total] = parts;
if (total && free) {
const totalBytes = parseInt(total);
const freeBytes = parseInt(free);
disks.push({
mount: drive,
total: totalBytes,
free: freeBytes,
used: totalBytes - freeBytes,
percent: ((totalBytes - freeBytes) / totalBytes) * 100
});
}
}
}
return disks;
} else {
output = execSync('df -k', { encoding: 'utf8' });
const lines = output.split('\n').slice(1);
const disks = [];
lines.forEach(line => {
const parts = line.trim().split(/\s+/);
if (parts.length >= 6) {
const total = parseInt(parts[1]) * 1024;
const used = parseInt(parts[2]) * 1024;
const free = parseInt(parts[3]) * 1024;
const mount = parts[5];
if (total > 0 && mount.startsWith('/')) {
disks.push({
mount,
total,
used,
free,
percent: (used / total) * 100
});
}
}
});
return disks;
}
} catch (err) {
return [{
mount: 'N/A',
total: 0,
used: 0,
free: 0,
percent: 0
}];
}
}
getNetworkInfo() {
const interfaces = os.networkInterfaces();
const networkData = [];
for (const [name, addresses] of Object.entries(interfaces)) {
addresses.forEach(addr => {
if (!addr.internal) networkData.push({ interface: name, address: addr.address, family: addr.family, mac: addr.mac });
});
}
return networkData;
}
getTopProcesses(limit = 5) {
try {
const platform = os.platform();
let output, processes = [];
if (platform === 'win32') {
output = execSync('tasklist /FO CSV /NH', { encoding: 'utf8' });
const lines = output.split('\n').filter(l => l.trim());
processes = lines.slice(0, limit).map(line => {
const parts = line.split(',');
return { name: parts[0].replace(/"/g, ''), pid: parseInt(parts[1].replace(/"/g, '')), memory: parseInt(parts[4].replace(/"/g, '').replace(/,/g, '').replace(' K', '')) * 1024 };
});
} else {
output = execSync('ps aux --sort=-%cpu | head -n 10', { encoding: 'utf8' });
const lines = output.split('\n').slice(1, limit + 1).filter(l => l.trim());
processes = lines.map(line => {
const parts = line.trim().split(/\s+/);
return { name: parts[10] || parts[11] || 'unknown', pid: parseInt(parts[1]), cpu: parseFloat(parts[2]), memory: parseFloat(parts[3]) };
});
}
return processes;
} catch (err) { return []; }
}
displaySnapshot(showNetwork = false, showProcesses = false) {
clearScreen();
printHeader('SYSTEM GUARDIAN - Resource Monitor');
console.log(`\x1b[1mSystem:\x1b[0m ${os.type()} ${os.release()}`);
console.log(`\x1b[1mHostname:\x1b[0m ${os.hostname()}`);
console.log(`\x1b[1mUptime:\x1b[0m ${formatUptime(os.uptime())}\n`);
const cpu = this.getCPUUsage();
console.log(`\x1b[1m🔧 CPU:\x1b[0m ${os.cpus()[0].model}`);
console.log(` Cores: ${os.cpus().length}`);
console.log(` Usage: ${getPercentBar(cpu.usage)}\n`);
const mem = this.getMemoryUsage();
console.log(`\x1b[1m💾 Memory:\x1b[0m`);
console.log(` Total: ${formatBytes(mem.total)}`);
console.log(` Used: ${formatBytes(mem.used)}`);
console.log(` Free: ${formatBytes(mem.free)}`);
console.log(` Usage: ${getPercentBar(mem.percent)}\n`);
console.log(`\x1b[1m📁 Disk Usage:\x1b[0m`);
const disks = this.getDiskUsage();
disks.forEach(disk => { if (disk.total > 0) console.log(` ${disk.mount}: ${formatBytes(disk.used)} / ${formatBytes(disk.total)} ${getPercentBar(disk.percent)}`); });
if (showNetwork) {
console.log(`\n\x1b[1m🌐 Network Interfaces:\x1b[0m`);
const networks = this.getNetworkInfo();
networks.forEach(net => console.log(` ${net.interface}: ${net.address} (${net.family})`));
}
if (showProcesses) {
console.log(`\n\x1b[1m⚙️ Top Processes:\x1b[0m`);
const processes = this.getTopProcesses(5);
processes.forEach((proc, i) => console.log(` ${i + 1}. ${proc.name} (PID: ${proc.pid}) - CPU: ${proc.cpu || 'N/A'}%, MEM: ${proc.memory || 'N/A'}%`));
}
if (cpu.usage > 80) console.log(`\n\x1b[31m⚠️ ALERT: High CPU usage (${cpu.usage.toFixed(1)}%)\x1b[0m`);
if (mem.percent > 90) console.log(`\x1b[31m⚠️ ALERT: High memory usage (${mem.percent.toFixed(1)}%)\x1b[0m`);
console.log('\n\x1b[90mPress Ctrl+C to exit, n for network, p for processes\x1b[0m');
}
async startMonitoring(refreshRate = 2000, exportPath = null) {
this.running = true;
this.showNetwork = false;
this.showProcesses = false;
console.log(`Starting system monitor (refresh every ${refreshRate}ms)...\n`);
this.displaySnapshot(this.showNetwork, this.showProcesses);
this.interval = setInterval(() => {
if (this.running) {
this.displaySnapshot(this.showNetwork, this.showProcesses);
if (exportPath) this.exportSnapshot(exportPath);
}
}, refreshRate);
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.on('data', (key) => {
if (key[0] === 3) { this.stopMonitoring(); process.exit(0); }
else if (key.toString() === 'n') { this.showNetwork = !this.showNetwork; this.displaySnapshot(this.showNetwork, this.showProcesses); }
else if (key.toString() === 'p') { this.showProcesses = !this.showProcesses; this.displaySnapshot(this.showNetwork, this.showProcesses); }
});
process.on('SIGINT', () => { this.stopMonitoring(); process.exit(0); });
}
exportSnapshot(exportPath) {
const snapshot = { timestamp: new Date().toISOString(), system: `${os.type()} ${os.release()}`, hostname: os.hostname(), uptime: os.uptime(), cpu: this.getCPUUsage(), memory: this.getMemoryUsage(), disks: this.getDiskUsage(), network: this.getNetworkInfo(), processes: this.getTopProcesses(5) };
try {
const dir = path.dirname(exportPath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(exportPath, JSON.stringify(snapshot, null, 2));
} catch (err) {
console.error(`Error exporting snapshot: ${err.message}`);
}
}
stopMonitoring() {
this.running = false;
if (this.interval) clearInterval(this.interval);
console.log('\nMonitoring stopped.');
}
}
class DiskAnalyzer {
constructor() {
this.fileTypes = {};
this.largeFiles = [];
this.totalSize = 0;
this.fileCount = 0;
}
analyzeDirectory(dirPath, options = {}) {
const maxDepth = options.maxDepth || 5;
const minSize = options.minSize || 10 * 1024 * 1024; // 10MB
this._scan(dirPath, 0, maxDepth, minSize);
return this._generateReport();
}
_scan(dirPath, depth, maxDepth, minSize) {
if (depth > maxDepth) return;
try {
const items = fs.readdirSync(dirPath);
items.forEach(item => {
const fullPath = path.join(dirPath, item);
try {
const stats = fs.statSync(fullPath);
if (stats.isFile()) {
this.fileCount++;
this.totalSize += stats.size;
const ext = path.extname(item).toLowerCase() || 'no-extension';
this.fileTypes[ext] = (this.fileTypes[ext] || 0) + stats.size;
if (stats.size >= minSize) this.largeFiles.push({ path: fullPath, size: stats.size, modified: stats.mtime });
} else if (stats.isDirectory()) this._scan(fullPath, depth + 1, maxDepth, minSize);
} catch (err) {
console.error(`\x1b[31mError accessing file ${dirPath}: ${err.message}\x1b[0m`);
}
});
} catch (err) { console.error(`\x1b[31mError scanning ${dirPath}: ${err.message}\x1b[0m`); }
}
_generateReport() {
const sortedTypes = Object.entries(this.fileTypes)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
this.largeFiles.sort((a, b) => b.size - a.size);
return {
totalSize: this.totalSize,
fileCount: this.fileCount,
topFileTypes: sortedTypes,
largeFiles: this.largeFiles.slice(0, 20)
};
}
displayReport(report) {
printHeader('DISK ANALYSIS REPORT');
console.log(`\x1b[1mTotal Files:\x1b[0m ${report.fileCount.toLocaleString()}`);
console.log(`\x1b[1mTotal Size:\x1b[0m ${formatBytes(report.totalSize)}\n`);
console.log(`\x1b[1m📊 Top File Types by Size:\x1b[0m`);
report.topFileTypes.forEach(([ext, size], i) => {
const percent = (size / report.totalSize) * 100;
console.log(` ${i + 1}. ${ext.padEnd(15)} ${formatBytes(size).padEnd(12)} ${percent.toFixed(1)}%`);
});
console.log(`\n\x1b[1m📦 Largest Files:\x1b[0m`);
report.largeFiles.slice(0, 10).forEach((file, i) => {
console.log(` ${i + 1}. ${formatBytes(file.size).padEnd(12)} ${file.path}`);
});
}
}
class TempCleaner {
constructor() {
this.tempDirs = [
os.tmpdir(),
path.join(os.homedir(), '.cache'),
path.join(os.homedir(), 'AppData', 'Local', 'Temp')
].filter(dir => {
try {
return fs.existsSync(dir);
} catch {
return false;
}
});
}
async scan(daysOld = 7) {
const cutoffTime = Date.now() - (daysOld * 24 * 60 * 60 * 1000);
const findings = [];
console.log(`\nScanning for files older than ${daysOld} days...\n`);
for (const dir of this.tempDirs) {
console.log(`Checking: ${dir}`);
try {
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
try {
const stats = fs.statSync(fullPath);
if (stats.mtime.getTime() < cutoffTime) {
findings.push({
path: fullPath,
size: stats.size,
modified: stats.mtime,
isDirectory: stats.isDirectory()
});
}
} catch (err) {}
}
} catch (err) {
console.log(` \x1b[33m⚠ Cannot access: ${err.message}\x1b[0m`);
}
}
return findings;
}
async clean(findings, dryRun = true) {
if (findings.length === 0) {
console.log('\n✓ No old temporary files found.');
return { deleted: 0, freed: 0, errors: 0 };
}
const totalSize = findings.reduce((sum, f) => sum + f.size, 0);
console.log(`\n\x1b[1mFound ${findings.length} items (${formatBytes(totalSize)})\x1b[0m`);
if (dryRun) {
console.log('\n\x1b[33m[DRY RUN] No files will be deleted\x1b[0m\n');
findings.slice(0, 10).forEach(f => {
console.log(` ${formatBytes(f.size).padEnd(12)} ${f.path}`);
});
if (findings.length > 10) {
console.log(` ... and ${findings.length - 10} more`);
}
return { deleted: 0, freed: 0, errors: 0 };
}
const confirm = await prompt(`\n\x1b[31mDelete ${findings.length} items? (yes/no): \x1b[0m`);
if (confirm.toLowerCase() !== 'yes') {
console.log('Cancelled.');
return { deleted: 0, freed: 0, errors: 0 };
}
let deleted = 0, freed = 0, errors = 0;
console.log('\nDeleting...');
for (const item of findings) {
try {
if (item.isDirectory) {
fs.rmSync(item.path, { recursive: true, force: true });
} else {
fs.unlinkSync(item.path);
}
deleted++;
freed += item.size;
process.stdout.write(`\r Deleted: ${deleted}/${findings.length}`);
} catch (err) {
errors++;
}
}
console.log(`\n\n✓ Cleaned up ${formatBytes(freed)}`);
if (errors > 0) {
console.log(`\x1b[33m⚠ ${errors} items could not be deleted\x1b[0m`);
}
return { deleted, freed, errors };
}
}
class SystemInfo {
displayInfo() {
printHeader('SYSTEM INFORMATION');
console.log(`OS: ${os.type()} ${os.release()} (${os.arch()})`);
console.log(`Hostname: ${os.hostname()}`);
console.log(`Platform: ${os.platform()}`);
console.log(`Uptime: ${formatUptime(os.uptime())}`);
console.log(`Total Memory: ${formatBytes(os.totalmem())}`);
console.log(`CPU Cores: ${os.cpus().length}`);
console.log(`CPU Model: ${os.cpus()[0].model}`);
console.log(`Home: ${os.homedir()}`);
console.log(`Temp: ${os.tmpdir()}`);
}
}
class Benchmark {
async runBenchmark() {
printHeader('SYSTEM BENCHMARK');
const cpuScore = await runBenchmarkStep('CPU benchmark', this.cpuBenchmark.bind(this));
const memScore = await runBenchmarkStep('memory benchmark', this.memoryBenchmark.bind(this));
const totalScore = Math.round((cpuScore + memScore) / 2);
console.log(`Total System Score: ${totalScore}`);
}
async cpuBenchmark() {
const start = Date.now();
let result = 0;
for (let i = 0; i < 250000; i++) result += Math.sqrt(i) * Math.sin(i);
const time = Date.now() - start;
return Math.max(1, Math.round(1000 / time * 25));
}
memoryBenchmark() {
const arrays = [];
const maxSize = Math.min(25, Math.floor(os.totalmem() / 1024 / 1024 / 40));
for (let i = 0; i < maxSize; i++) arrays.push(new Array(25000).fill(Math.random()));
const score = Math.round(maxSize * 10);
arrays.length = 0;
return score;
}
}
const showHelp = () => {
printHeader('SYSTEM GUARDIAN v1.0.0');
console.log('Professional System Utilities Package\n');
console.log('USAGE: sysmon [command] [options]\n');
console.log('COMMANDS:');
console.log(' monitor Live system resource monitoring');
console.log(' analyze <path> Analyze disk usage in directory');
console.log(' clean [days] Find and clean old temp files (default: 7 days)');
console.log(' info Display detailed system information');
console.log(' benchmark Run system performance benchmark');
console.log(' help Show this help message\n');
console.log('EXAMPLES:');
console.log(' sysmon monitor');
console.log(' sysmon monitor --export reports/snapshot.json');
console.log(' sysmon analyze ~/Documents');
console.log(' sysmon clean 14 --dry-run');
console.log(' sysmon info');
console.log(' sysmon benchmark\n');
console.log('OPTIONS:');
console.log(' --dry-run Preview changes without deleting');
console.log(' --depth <n> Max directory depth for analysis (default: 5)');
console.log(' --min-size <mb> Minimum file size for large file report (default: 10)');
console.log(' --export <path> Export monitor data to JSON file');
};
const main = async () => {
const args = process.argv.slice(2);
const command = args[0];
if (!command || command === 'help') {
showHelp();
return;
}
switch (command) {
case 'monitor': {
const exportIndex = args.indexOf('--export');
const exportPath = exportIndex !== -1 && args[exportIndex + 1] ? args[exportIndex + 1] : null;
const monitor = new SystemMonitor();
await monitor.startMonitoring(2000, exportPath);
break;
}
case 'analyze': {
const targetPath = args[1] || process.cwd();
if (!fs.existsSync(targetPath)) {
console.error(`\x1b[31mError: Path does not exist: ${targetPath}\x1b[0m`);
process.exit(1);
}
console.log(`\nAnalyzing: ${targetPath}\nThis may take a while for large directories...`);
const analyzer = new DiskAnalyzer();
const report = analyzer.analyzeDirectory(targetPath, {
maxDepth: parseInt(args.find(a => a.startsWith('--depth='))?.split('=')[1]) || 5,
minSize: (parseInt(args.find(a => a.startsWith('--min-size='))?.split('=')[1]) || 10) * 1024 * 1024
});
analyzer.displayReport(report);
break;
}
case 'clean': {
const daysOld = parseInt(args[1]) || 7;
const dryRun = args.includes('--dry-run');
const cleaner = new TempCleaner();
const findings = await cleaner.scan(daysOld);
await cleaner.clean(findings, dryRun);
break;
}
case 'info': { const info = new SystemInfo(); info.displayInfo(); break; }
case 'benchmark': { const bench = new Benchmark(); await bench.runBenchmark(); break; }
default:
console.error(`\x1b[31mUnknown command: ${command}\x1b[0m`);
console.log('Run "sysmon help" for usage information.');
process.exit(1);
}
};
main().catch(err => {
console.error(`\x1b[31mFatal error: ${err.message}\x1b[0m`);
process.exit(1);
});