You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23โ€“26.RSVP โ†’
Socket
Book a DemoSign in
Socket

clean-my-mac-cli

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

clean-my-mac-cli - npm Package Compare versions

Comparing version
1.0.0
to
1.1.0
+925
-331
dist/index.js

@@ -7,4 +7,3 @@ #!/usr/bin/env node

// src/commands/scan.ts
import chalk from "chalk";
import ora from "ora";
import chalk2 from "chalk";

@@ -111,2 +110,18 @@ // src/types.ts

safetyNote: "Review each file carefully before deleting"
},
"node-modules": {
id: "node-modules",
name: "Node Modules",
group: "Development",
description: "Orphaned node_modules in old projects",
safetyLevel: "moderate",
safetyNote: "Projects will need npm install to restore"
},
"duplicates": {
id: "duplicates",
name: "Duplicate Files",
group: "Storage",
description: "Files with identical content",
safetyLevel: "risky",
safetyNote: "Review carefully - keeps newest copy by default"
}

@@ -325,2 +340,234 @@ };

// src/utils/progress.ts
import chalk from "chalk";
var ProgressBar = class {
current = 0;
total;
label;
showPercentage;
showCount;
barWidth;
startTime;
lastRender = "";
constructor(options) {
this.total = options.total;
this.label = options.label ?? "";
this.showPercentage = options.showPercentage ?? true;
this.showCount = options.showCount ?? true;
this.barWidth = options.barWidth ?? 30;
this.startTime = Date.now();
}
update(current, label) {
this.current = current;
if (label) this.label = label;
this.render();
}
increment(label) {
this.update(this.current + 1, label);
}
render() {
const percentage = Math.min(100, Math.round(this.current / this.total * 100));
const filledWidth = Math.round(percentage / 100 * this.barWidth);
const emptyWidth = this.barWidth - filledWidth;
const filled = chalk.green("\u2588".repeat(filledWidth));
const empty = chalk.dim("\u2591".repeat(emptyWidth));
const bar = `${filled}${empty}`;
const parts = [];
if (this.label) {
parts.push(chalk.cyan(this.label.substring(0, 25).padEnd(25)));
}
parts.push(bar);
if (this.showPercentage) {
parts.push(chalk.yellow(`${percentage.toString().padStart(3)}%`));
}
if (this.showCount) {
parts.push(chalk.dim(`(${this.current}/${this.total})`));
}
const elapsed = Date.now() - this.startTime;
if (elapsed > 1e3 && this.current > 0) {
const eta = Math.round(elapsed / this.current * (this.total - this.current) / 1e3);
if (eta > 0) {
parts.push(chalk.dim(`~${eta}s`));
}
}
const output = parts.join(" ");
if (output !== this.lastRender) {
process.stdout.write(`\r${output}`);
this.lastRender = output;
}
}
finish(message) {
process.stdout.write("\r" + " ".repeat(this.lastRender.length) + "\r");
if (message) {
console.log(message);
}
}
};
function createScanProgress(total) {
return new ProgressBar({
total,
label: "Scanning...",
barWidth: 25
});
}
function createCleanProgress(total) {
return new ProgressBar({
total,
label: "Cleaning...",
barWidth: 25
});
}
// src/utils/backup.ts
import { mkdir, rename, readdir as readdir2, stat as stat2, rm as rm2 } from "fs/promises";
import { join as join3, dirname } from "path";
import { homedir as homedir2 } from "os";
var BACKUP_DIR = join3(homedir2(), ".clean-my-mac", "backup");
var BACKUP_RETENTION_DAYS = 7;
async function cleanOldBackups() {
let cleaned = 0;
const now = Date.now();
const maxAge = BACKUP_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
try {
const entries = await readdir2(BACKUP_DIR);
for (const entry of entries) {
const entryPath = join3(BACKUP_DIR, entry);
try {
const stats = await stat2(entryPath);
if (stats.isDirectory() && now - stats.mtime.getTime() > maxAge) {
await rm2(entryPath, { recursive: true, force: true });
cleaned++;
}
} catch {
continue;
}
}
} catch {
}
return cleaned;
}
async function listBackups() {
const backups = [];
try {
const entries = await readdir2(BACKUP_DIR);
for (const entry of entries) {
const entryPath = join3(BACKUP_DIR, entry);
try {
const stats = await stat2(entryPath);
if (stats.isDirectory()) {
const size = await getBackupSize(entryPath);
backups.push({
path: entryPath,
date: stats.mtime,
size
});
}
} catch {
continue;
}
}
} catch {
}
return backups.sort((a, b) => b.date.getTime() - a.date.getTime());
}
async function getBackupSize(dir) {
let size = 0;
try {
const entries = await readdir2(dir, { withFileTypes: true });
for (const entry of entries) {
const entryPath = join3(dir, entry.name);
try {
if (entry.isFile()) {
const stats = await stat2(entryPath);
size += stats.size;
} else if (entry.isDirectory()) {
size += await getBackupSize(entryPath);
}
} catch {
continue;
}
}
} catch {
}
return size;
}
// src/utils/config.ts
import { readFile, writeFile, access as access2 } from "fs/promises";
import { join as join4 } from "path";
import { homedir as homedir3 } from "os";
var CONFIG_PATHS = [
join4(homedir3(), ".cleanmymacrc"),
join4(homedir3(), ".config", "clean-my-mac", "config.json")
];
var DEFAULT_CONFIG = {
downloadsDaysOld: 30,
largeFilesMinSize: 500 * 1024 * 1024,
backupEnabled: false,
backupRetentionDays: 7,
parallelScans: true,
concurrency: 4
};
var cachedConfig = null;
async function loadConfig(configPath) {
if (cachedConfig && !configPath) {
return cachedConfig;
}
const paths = configPath ? [configPath] : CONFIG_PATHS;
for (const path of paths) {
try {
await access2(path);
const content = await readFile(path, "utf-8");
const parsed = JSON.parse(content);
cachedConfig = { ...DEFAULT_CONFIG, ...parsed };
return cachedConfig;
} catch {
continue;
}
}
cachedConfig = DEFAULT_CONFIG;
return cachedConfig;
}
async function configExists() {
for (const path of CONFIG_PATHS) {
try {
await access2(path);
return true;
} catch {
continue;
}
}
return false;
}
async function initConfig() {
const configPath = CONFIG_PATHS[0];
const defaultConfig = {
downloadsDaysOld: 30,
largeFilesMinSize: 500 * 1024 * 1024,
backupEnabled: false,
backupRetentionDays: 7,
parallelScans: true,
concurrency: 4,
extraPaths: {
nodeModules: ["~/Projects", "~/Developer", "~/Code"],
projects: ["~/Projects", "~/Developer", "~/Code"]
}
};
await writeFile(configPath, JSON.stringify(defaultConfig, null, 2));
return configPath;
}
// src/utils/hash.ts
import { createHash } from "crypto";
import { createReadStream } from "fs";
async function getFileHash(filePath, algorithm = "md5") {
return new Promise((resolve, reject) => {
const hash = createHash(algorithm);
const stream = createReadStream(filePath);
stream.on("data", (data) => hash.update(data));
stream.on("end", () => resolve(hash.digest("hex")));
stream.on("error", reject);
});
}
// src/scanners/system-cache.ts

@@ -360,4 +607,4 @@ var SystemCacheScanner = class extends BaseScanner {

// src/scanners/temp-files.ts
import { readdir as readdir2 } from "fs/promises";
import { join as join3 } from "path";
import { readdir as readdir3 } from "fs/promises";
import { join as join5 } from "path";
var TempFilesScanner = class extends BaseScanner {

@@ -383,9 +630,9 @@ category = CATEGORIES["temp-files"];

try {
const level1 = await readdir2(PATHS.varFolders);
const level1 = await readdir3(PATHS.varFolders);
for (const dir1 of level1) {
const path1 = join3(PATHS.varFolders, dir1);
const path1 = join5(PATHS.varFolders, dir1);
try {
const level2 = await readdir2(path1);
const level2 = await readdir3(path1);
for (const dir2 of level2) {
const tempPath = join3(path1, dir2, "T");
const tempPath = join5(path1, dir2, "T");
if (await exists(tempPath)) {

@@ -437,3 +684,3 @@ const tempItems = await getDirectoryItems(tempPath);

// src/scanners/browser-cache.ts
import { stat as stat2 } from "fs/promises";
import { stat as stat3 } from "fs/promises";
var BrowserCacheScanner = class extends BaseScanner {

@@ -453,3 +700,3 @@ category = CATEGORIES["browser-cache"];

const size = await getSize(browser.path);
const stats = await stat2(browser.path);
const stats = await stat3(browser.path);
items.push({

@@ -472,3 +719,3 @@ path: browser.path,

// src/scanners/dev-cache.ts
import { stat as stat3 } from "fs/promises";
import { stat as stat4 } from "fs/promises";
var DevCacheScanner = class extends BaseScanner {

@@ -492,3 +739,3 @@ category = CATEGORIES["dev-cache"];

if (size > 0) {
const stats = await stat3(dev.path);
const stats = await stat4(dev.path);
items.push({

@@ -520,3 +767,3 @@ path: dev.path,

if (size > 0) {
const stats = await stat3(PATHS.xcodeArchives);
const stats = await stat4(PATHS.xcodeArchives);
items.push({

@@ -540,3 +787,3 @@ path: PATHS.xcodeArchives,

import { promisify } from "util";
import { stat as stat4 } from "fs/promises";
import { stat as stat5 } from "fs/promises";
var execAsync = promisify(exec);

@@ -553,3 +800,3 @@ var HomebrewScanner = class extends BaseScanner {

if (size > 0) {
const stats = await stat4(brewCache);
const stats = await stat5(brewCache);
items.push({

@@ -695,4 +942,4 @@ path: brewCache,

// src/scanners/language-files.ts
import { readdir as readdir3, stat as stat5 } from "fs/promises";
import { join as join4 } from "path";
import { readdir as readdir4, stat as stat6 } from "fs/promises";
import { join as join6 } from "path";
var KEEP_LANGUAGES = ["en", "en_US", "en_GB", "pt", "pt_BR", "pt_PT", "Base"];

@@ -704,9 +951,9 @@ var LanguageFilesScanner = class extends BaseScanner {

if (await exists(PATHS.applications)) {
const apps = await readdir3(PATHS.applications);
const apps = await readdir4(PATHS.applications);
for (const app of apps) {
if (!app.endsWith(".app")) continue;
const resourcesPath = join4(PATHS.applications, app, "Contents", "Resources");
const resourcesPath = join6(PATHS.applications, app, "Contents", "Resources");
if (await exists(resourcesPath)) {
try {
const resources = await readdir3(resourcesPath);
const resources = await readdir4(resourcesPath);
const lprojDirs = resources.filter(

@@ -716,6 +963,6 @@ (r) => r.endsWith(".lproj") && !KEEP_LANGUAGES.includes(r.replace(".lproj", ""))

for (const lproj of lprojDirs) {
const lprojPath = join4(resourcesPath, lproj);
const lprojPath = join6(resourcesPath, lproj);
try {
const size = await getSize(lprojPath);
const stats = await stat5(lprojPath);
const stats = await stat6(lprojPath);
items.push({

@@ -743,4 +990,4 @@ path: lprojPath,

// src/scanners/large-files.ts
import { readdir as readdir4, stat as stat6 } from "fs/promises";
import { join as join5 } from "path";
import { readdir as readdir5, stat as stat7 } from "fs/promises";
import { join as join7 } from "path";
var LargeFilesScanner = class extends BaseScanner {

@@ -765,9 +1012,9 @@ category = CATEGORIES["large-files"];

try {
const entries = await readdir4(dirPath, { withFileTypes: true });
const entries = await readdir5(dirPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith(".")) continue;
const fullPath = join5(dirPath, entry.name);
const fullPath = join7(dirPath, entry.name);
try {
if (entry.isFile()) {
const stats = await stat6(fullPath);
const stats = await stat7(fullPath);
if (stats.size >= minSize) {

@@ -796,2 +1043,185 @@ items.push({

// src/scanners/node-modules.ts
import { readdir as readdir6, stat as stat8, access as access3 } from "fs/promises";
import { join as join8 } from "path";
import { homedir as homedir4 } from "os";
var DEFAULT_SEARCH_PATHS = [
join8(homedir4(), "Projects"),
join8(homedir4(), "Developer"),
join8(homedir4(), "Code"),
join8(homedir4(), "dev"),
join8(homedir4(), "workspace"),
join8(homedir4(), "repos")
];
var DEFAULT_DAYS_OLD = 30;
var NodeModulesScanner = class extends BaseScanner {
category = CATEGORIES["node-modules"];
async scan(options) {
const items = [];
const daysOld = options?.daysOld ?? DEFAULT_DAYS_OLD;
for (const searchPath of DEFAULT_SEARCH_PATHS) {
if (await exists(searchPath)) {
const found = await this.findNodeModules(searchPath, daysOld, 4);
items.push(...found);
}
}
items.sort((a, b) => b.size - a.size);
return this.createResult(items);
}
async findNodeModules(dir, daysOld, maxDepth, currentDepth = 0) {
const items = [];
if (currentDepth > maxDepth) return items;
try {
const entries = await readdir6(dir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (entry.name.startsWith(".")) continue;
const fullPath = join8(dir, entry.name);
if (entry.name === "node_modules") {
const parentDir = dir;
const packageJsonPath = join8(parentDir, "package.json");
try {
await access3(packageJsonPath);
const parentStats = await stat8(parentDir);
const daysSinceModified = (Date.now() - parentStats.mtime.getTime()) / (1e3 * 60 * 60 * 24);
if (daysSinceModified >= daysOld) {
const size = await getSize(fullPath);
const stats = await stat8(fullPath);
items.push({
path: fullPath,
size,
name: `${this.getProjectName(parentDir)} (${Math.floor(daysSinceModified)}d old)`,
isDirectory: true,
modifiedAt: stats.mtime
});
}
} catch {
const size = await getSize(fullPath);
if (size > 0) {
const stats = await stat8(fullPath);
items.push({
path: fullPath,
size,
name: `${this.getProjectName(parentDir)} (orphaned)`,
isDirectory: true,
modifiedAt: stats.mtime
});
}
}
} else {
const subItems = await this.findNodeModules(fullPath, daysOld, maxDepth, currentDepth + 1);
items.push(...subItems);
}
}
} catch {
}
return items;
}
getProjectName(projectPath) {
const parts = projectPath.split("/");
return parts[parts.length - 1] || projectPath;
}
};
// src/scanners/duplicates.ts
import { readdir as readdir7, stat as stat9 } from "fs/promises";
import { join as join9 } from "path";
import { homedir as homedir5 } from "os";
var DEFAULT_SEARCH_PATHS2 = [
join9(homedir5(), "Downloads"),
join9(homedir5(), "Documents"),
join9(homedir5(), "Desktop")
];
var MIN_FILE_SIZE = 1024 * 1024;
var MAX_DEPTH = 5;
var DuplicatesScanner = class extends BaseScanner {
category = CATEGORIES["duplicates"];
async scan(options) {
const minSize = options?.minSize ?? MIN_FILE_SIZE;
const filesBySize = /* @__PURE__ */ new Map();
for (const searchPath of DEFAULT_SEARCH_PATHS2) {
if (await exists(searchPath)) {
await this.collectFiles(searchPath, filesBySize, minSize, MAX_DEPTH);
}
}
const duplicates = await this.findDuplicates(filesBySize);
const items = this.convertToCleanableItems(duplicates);
return this.createResult(items);
}
async collectFiles(dir, filesBySize, minSize, maxDepth, currentDepth = 0) {
if (currentDepth > maxDepth) return;
try {
const entries = await readdir7(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith(".")) continue;
const fullPath = join9(dir, entry.name);
try {
if (entry.isFile()) {
const stats = await stat9(fullPath);
if (stats.size >= minSize) {
const files = filesBySize.get(stats.size) ?? [];
files.push({
path: fullPath,
size: stats.size,
modifiedAt: stats.mtime
});
filesBySize.set(stats.size, files);
}
} else if (entry.isDirectory()) {
await this.collectFiles(fullPath, filesBySize, minSize, maxDepth, currentDepth + 1);
}
} catch {
continue;
}
}
} catch {
}
}
async findDuplicates(filesBySize) {
const duplicates = /* @__PURE__ */ new Map();
for (const [, files] of filesBySize) {
if (files.length < 2) continue;
const filesByHash = /* @__PURE__ */ new Map();
for (const file of files) {
try {
const hash = await getFileHash(file.path);
const hashFiles = filesByHash.get(hash) ?? [];
hashFiles.push(file);
filesByHash.set(hash, hashFiles);
} catch {
continue;
}
}
for (const [hash, hashFiles] of filesByHash) {
if (hashFiles.length >= 2) {
duplicates.set(hash, hashFiles);
}
}
}
return duplicates;
}
convertToCleanableItems(duplicates) {
const items = [];
for (const [, files] of duplicates) {
files.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
const [newest, ...older] = files;
for (const file of older) {
items.push({
path: file.path,
size: file.size,
name: `${this.getFileName(file.path)} (dup of ${this.getFileName(newest.path)})`,
isDirectory: false,
modifiedAt: file.modifiedAt
});
}
}
items.sort((a, b) => b.size - a.size);
return items;
}
getFileName(path) {
const parts = path.split("/");
return parts[parts.length - 1] || path;
}
};
// src/scanners/index.ts

@@ -811,3 +1241,5 @@ var ALL_SCANNERS = {

"language-files": new LanguageFilesScanner(),
"large-files": new LargeFilesScanner()
"large-files": new LargeFilesScanner(),
"node-modules": new NodeModulesScanner(),
"duplicates": new DuplicatesScanner()
};

@@ -820,33 +1252,53 @@ function getScanner(categoryId) {

}
async function runAllScans(options, onProgress) {
async function runWithConcurrency(tasks, concurrency) {
const results = [];
const scanners = getAllScanners();
for (const scanner of scanners) {
const result = await scanner.scan(options);
results.push(result);
onProgress?.(scanner, result);
const executing = [];
for (const task of tasks) {
const p = task().then((result) => {
results.push(result);
});
executing.push(p);
if (executing.length >= concurrency) {
await Promise.race(executing);
executing.splice(
executing.findIndex((e) => e === p),
1
);
}
}
const totalSize = results.reduce((sum, r) => sum + r.totalSize, 0);
const totalItems = results.reduce((sum, r) => sum + r.items.length, 0);
return {
results,
totalSize,
totalItems
};
await Promise.all(executing);
return results;
}
async function runScans(categoryIds, options, onProgress) {
const results = [];
for (const categoryId of categoryIds) {
const scanner = getScanner(categoryId);
const result = await scanner.scan(options);
results.push(result);
onProgress?.(scanner, result);
async function runAllScans(options, onProgress) {
const scanners = getAllScanners();
const parallel = options?.parallel ?? true;
const concurrency = options?.concurrency ?? 4;
let completed = 0;
const total = scanners.length;
if (parallel) {
const tasks = scanners.map((scanner) => async () => {
const result = await scanner.scan(options);
completed++;
options?.onProgress?.(completed, total, scanner, result);
onProgress?.(scanner, result);
return { scanner, result };
});
const scanResults = await runWithConcurrency(tasks, concurrency);
const results = scanResults.map((r) => r.result);
const totalSize = results.reduce((sum, r) => sum + r.totalSize, 0);
const totalItems = results.reduce((sum, r) => sum + r.items.length, 0);
return { results, totalSize, totalItems };
} else {
const results = [];
for (const scanner of scanners) {
const result = await scanner.scan(options);
results.push(result);
completed++;
options?.onProgress?.(completed, total, scanner, result);
onProgress?.(scanner, result);
}
const totalSize = results.reduce((sum, r) => sum + r.totalSize, 0);
const totalItems = results.reduce((sum, r) => sum + r.items.length, 0);
return { results, totalSize, totalItems };
}
const totalSize = results.reduce((sum, r) => sum + r.totalSize, 0);
const totalItems = results.reduce((sum, r) => sum + r.items.length, 0);
return {
results,
totalSize,
totalItems
};
}

@@ -856,74 +1308,10 @@

var SAFETY_ICONS = {
safe: chalk.green("\u25CF"),
moderate: chalk.yellow("\u25CF"),
risky: chalk.red("\u25CF")
safe: chalk2.green("\u25CF"),
moderate: chalk2.yellow("\u25CF"),
risky: chalk2.red("\u25CF")
};
async function scanCommand(options) {
const spinner = ora("Scanning your Mac...").start();
let summary;
if (options.category) {
summary = await runScans([options.category], { verbose: options.verbose });
} else {
summary = await runAllScans({ verbose: options.verbose });
}
spinner.stop();
printScanResults(summary, options.verbose);
return summary;
}
function printScanResults(summary, verbose = false) {
console.log();
console.log(chalk.bold("Scan Results"));
console.log(chalk.dim("\u2500".repeat(60)));
const groupedResults = groupResultsByCategory(summary.results);
for (const [group, results] of Object.entries(groupedResults)) {
const groupTotal = results.reduce((sum, r) => sum + r.totalSize, 0);
if (groupTotal === 0) continue;
console.log();
console.log(chalk.bold.cyan(group));
for (const result of results) {
if (result.totalSize === 0) continue;
const sizeStr = formatSize(result.totalSize);
const itemCount = result.items.length;
const safetyIcon = SAFETY_ICONS[result.category.safetyLevel];
console.log(
` ${safetyIcon} ${result.category.name.padEnd(28)} ${chalk.yellow(sizeStr.padStart(10))} ${chalk.dim(`(${itemCount} items)`)}`
);
if (verbose && result.items.length > 0) {
const topItems = result.items.sort((a, b) => b.size - a.size).slice(0, 5);
for (const item of topItems) {
console.log(chalk.dim(` \u2514\u2500 ${item.name.padEnd(24)} ${formatSize(item.size).padStart(10)}`));
}
if (result.items.length > 5) {
console.log(chalk.dim(` \u2514\u2500 ... and ${result.items.length - 5} more`));
}
}
}
}
console.log();
console.log(chalk.dim("\u2500".repeat(60)));
console.log(
chalk.bold(`Total: ${chalk.green(formatSize(summary.totalSize))} can be cleaned (${summary.totalItems} items)`)
);
console.log();
console.log(chalk.dim("Safety: ") + `${SAFETY_ICONS.safe} safe ${SAFETY_ICONS.moderate} moderate ${SAFETY_ICONS.risky} risky (use --unsafe)`);
console.log();
}
function groupResultsByCategory(results) {
const groups = {
"System Junk": [],
"Development": [],
"Storage": [],
"Browsers": [],
"Large Files": []
};
for (const result of results) {
const group = result.category.group;
groups[group].push(result);
}
return groups;
}
function listCategories() {
console.log();
console.log(chalk.bold("Available Categories"));
console.log(chalk.dim("\u2500".repeat(70)));
console.log(chalk2.bold("Available Categories"));
console.log(chalk2.dim("\u2500".repeat(70)));
const groupedCategories = {

@@ -941,8 +1329,8 @@ "System Junk": [],

console.log();
console.log(chalk.bold.cyan(group));
console.log(chalk2.bold.cyan(group));
for (const category of categories) {
const safetyIcon = SAFETY_ICONS[category.safetyLevel];
console.log(` ${safetyIcon} ${chalk.yellow(category.id.padEnd(18))} ${chalk.dim(category.description)}`);
console.log(` ${safetyIcon} ${chalk2.yellow(category.id.padEnd(18))} ${chalk2.dim(category.description)}`);
if (category.safetyNote) {
console.log(` ${chalk.dim.italic(`\u26A0 ${category.safetyNote}`)}`);
console.log(` ${chalk2.dim.italic(`\u26A0 ${category.safetyNote}`)}`);
}

@@ -952,3 +1340,3 @@ }

console.log();
console.log(chalk.dim("Safety: ") + `${SAFETY_ICONS.safe} safe ${SAFETY_ICONS.moderate} moderate ${SAFETY_ICONS.risky} risky (requires --unsafe)`);
console.log(chalk2.dim("Safety: ") + `${SAFETY_ICONS.safe} safe ${SAFETY_ICONS.moderate} moderate ${SAFETY_ICONS.risky} risky (requires --unsafe)`);
console.log();

@@ -958,16 +1346,301 @@ }

// src/commands/clean.ts
import chalk2 from "chalk";
import ora2 from "ora";
import inquirer from "inquirer";
import chalk3 from "chalk";
import { confirm, checkbox } from "@inquirer/prompts";
var SAFETY_ICONS2 = {
safe: chalk2.green("\u25CF"),
moderate: chalk2.yellow("\u25CF"),
risky: chalk2.red("\u25CF")
safe: chalk3.green("\u25CF"),
moderate: chalk3.yellow("\u25CF"),
risky: chalk3.red("\u25CF")
};
async function cleanCommand(options) {
const spinner = ora2("Scanning your Mac...").start();
const summary = await runAllScans();
spinner.stop();
// src/commands/maintenance.ts
import chalk4 from "chalk";
import ora from "ora";
// src/maintenance/dns-cache.ts
import { exec as exec3 } from "child_process";
import { promisify as promisify3 } from "util";
var execAsync3 = promisify3(exec3);
async function flushDnsCache() {
try {
await execAsync3("sudo dscacheutil -flushcache");
await execAsync3("sudo killall -HUP mDNSResponder");
return {
success: true,
message: "DNS cache flushed successfully"
};
} catch (error) {
return {
success: false,
message: "Failed to flush DNS cache",
error: error instanceof Error ? error.message : String(error)
};
}
}
// src/maintenance/purgeable.ts
import { exec as exec4 } from "child_process";
import { promisify as promisify4 } from "util";
var execAsync4 = promisify4(exec4);
async function freePurgeableSpace() {
try {
await execAsync4("purge");
return {
success: true,
message: "Purgeable space freed successfully"
};
} catch (error) {
return {
success: false,
message: "Failed to free purgeable space",
error: error instanceof Error ? error.message : String(error)
};
}
}
// src/commands/maintenance.ts
async function maintenanceCommand(options) {
const tasks = [];
if (options.dns) {
tasks.push({ name: "Flush DNS Cache", fn: flushDnsCache });
}
if (options.purgeable) {
tasks.push({ name: "Free Purgeable Space", fn: freePurgeableSpace });
}
if (tasks.length === 0) {
console.log(chalk4.yellow("\nNo maintenance tasks specified."));
console.log(chalk4.dim("Use --dns to flush DNS cache or --purgeable to free purgeable space.\n"));
return;
}
console.log();
console.log(chalk4.bold("Running Maintenance Tasks"));
console.log(chalk4.dim("\u2500".repeat(50)));
for (const task of tasks) {
const spinner = ora(task.name).start();
const result = await task.fn();
if (result.success) {
spinner.succeed(chalk4.green(result.message));
} else {
spinner.fail(chalk4.red(result.message));
if (result.error) {
console.log(chalk4.dim(` ${result.error}`));
}
}
}
console.log();
}
// src/commands/uninstall.ts
import chalk5 from "chalk";
import { confirm as confirm2, checkbox as checkbox2 } from "@inquirer/prompts";
import { readdir as readdir8, stat as stat10, rm as rm3 } from "fs/promises";
import { join as join10, basename } from "path";
import { homedir as homedir6 } from "os";
var APPLICATIONS_DIR = "/Applications";
var USER_APPLICATIONS_DIR = join10(homedir6(), "Applications");
var RELATED_PATHS_TEMPLATES = [
join10(homedir6(), "Library", "Application Support", "{APP_NAME}"),
join10(homedir6(), "Library", "Preferences", "{BUNDLE_ID}.plist"),
join10(homedir6(), "Library", "Preferences", "{APP_NAME}.plist"),
join10(homedir6(), "Library", "Caches", "{APP_NAME}"),
join10(homedir6(), "Library", "Caches", "{BUNDLE_ID}"),
join10(homedir6(), "Library", "Logs", "{APP_NAME}"),
join10(homedir6(), "Library", "Saved Application State", "{BUNDLE_ID}.savedState"),
join10(homedir6(), "Library", "WebKit", "{APP_NAME}"),
join10(homedir6(), "Library", "HTTPStorages", "{BUNDLE_ID}"),
join10(homedir6(), "Library", "Containers", "{BUNDLE_ID}"),
join10(homedir6(), "Library", "Group Containers", "*.{APP_NAME}")
];
async function uninstallCommand(options) {
console.log(chalk5.cyan("\nScanning installed applications...\n"));
const apps = await getInstalledApps();
if (apps.length === 0) {
console.log(chalk5.yellow("No applications found.\n"));
return;
}
const choices = apps.map((app) => ({
name: `${app.name.padEnd(35)} ${chalk5.yellow(formatSize(app.totalSize).padStart(10))} ${chalk5.dim(`(+${app.relatedPaths.length} related)`)}`,
value: app.name,
checked: false
}));
const selectedApps = await checkbox2({
message: "Select applications to uninstall:",
choices,
pageSize: 15
});
if (selectedApps.length === 0) {
console.log(chalk5.yellow("\nNo applications selected.\n"));
return;
}
const appsToRemove = apps.filter((a) => selectedApps.includes(a.name));
const totalSize = appsToRemove.reduce((sum, a) => sum + a.totalSize, 0);
const totalPaths = appsToRemove.reduce((sum, a) => sum + 1 + a.relatedPaths.length, 0);
console.log();
console.log(chalk5.bold("Applications to uninstall:"));
for (const app of appsToRemove) {
console.log(` ${chalk5.red("\u2717")} ${app.name} (${formatSize(app.totalSize)})`);
for (const related of app.relatedPaths) {
console.log(chalk5.dim(` \u2514\u2500 ${related.replace(homedir6(), "~")}`));
}
}
console.log();
console.log(chalk5.bold(`Total: ${formatSize(totalSize)} will be freed (${totalPaths} items)`));
console.log();
if (options.dryRun) {
console.log(chalk5.cyan("[DRY RUN] Would uninstall the above applications.\n"));
return;
}
if (!options.yes) {
const proceed = await confirm2({
message: "Proceed with uninstallation?",
default: false
});
if (!proceed) {
console.log(chalk5.yellow("\nUninstallation cancelled.\n"));
return;
}
}
const showProgress = !options.noProgress && process.stdout.isTTY;
const progress = showProgress ? createCleanProgress(appsToRemove.length) : null;
let uninstalledCount = 0;
let freedSpace = 0;
const errors = [];
for (let i = 0; i < appsToRemove.length; i++) {
const app = appsToRemove[i];
progress?.update(i, `Uninstalling ${app.name}...`);
try {
await rm3(app.path, { recursive: true, force: true });
freedSpace += app.size;
for (const relatedPath of app.relatedPaths) {
try {
await rm3(relatedPath, { recursive: true, force: true });
const size = await getSize(relatedPath).catch(() => 0);
freedSpace += size;
} catch {
}
}
uninstalledCount++;
} catch (error) {
errors.push(`${app.name}: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
progress?.finish();
console.log();
console.log(chalk5.bold.green("\u2713 Uninstallation Complete"));
console.log(chalk5.dim("\u2500".repeat(50)));
console.log(` Apps uninstalled: ${uninstalledCount}`);
console.log(` Space freed: ${chalk5.green(formatSize(freedSpace))}`);
if (errors.length > 0) {
console.log();
console.log(chalk5.red("Errors:"));
for (const error of errors) {
console.log(chalk5.red(` \u2717 ${error}`));
}
}
console.log();
}
async function getInstalledApps() {
const apps = [];
for (const appDir of [APPLICATIONS_DIR, USER_APPLICATIONS_DIR]) {
if (!await exists(appDir)) continue;
try {
const entries = await readdir8(appDir);
for (const entry of entries) {
if (!entry.endsWith(".app")) continue;
const appPath = join10(appDir, entry);
const appName = basename(entry, ".app");
try {
const stats = await stat10(appPath);
if (!stats.isDirectory()) continue;
const appSize = await getSize(appPath);
const relatedPaths = await findRelatedPaths(appName);
let totalRelatedSize = 0;
for (const path of relatedPaths) {
totalRelatedSize += await getSize(path).catch(() => 0);
}
apps.push({
name: appName,
path: appPath,
size: appSize,
relatedPaths,
totalSize: appSize + totalRelatedSize
});
} catch {
continue;
}
}
} catch {
continue;
}
}
return apps.sort((a, b) => b.totalSize - a.totalSize);
}
async function findRelatedPaths(appName) {
const paths = [];
const bundleId = appName.toLowerCase().replace(/\s+/g, ".");
for (const template of RELATED_PATHS_TEMPLATES) {
const variations = [
template.replace("{APP_NAME}", appName).replace("{BUNDLE_ID}", bundleId),
template.replace("{APP_NAME}", appName.toLowerCase()).replace("{BUNDLE_ID}", bundleId),
template.replace("{APP_NAME}", appName.replace(/\s+/g, "")).replace("{BUNDLE_ID}", bundleId)
];
for (const path of variations) {
if (path.includes("*")) {
const dir = path.substring(0, path.lastIndexOf("/"));
const pattern = path.substring(path.lastIndexOf("/") + 1);
if (await exists(dir)) {
try {
const entries = await readdir8(dir);
for (const entry of entries) {
if (matchPattern(entry, pattern)) {
const fullPath = join10(dir, entry);
if (!paths.includes(fullPath)) {
paths.push(fullPath);
}
}
}
} catch {
continue;
}
}
} else if (await exists(path)) {
if (!paths.includes(path)) {
paths.push(path);
}
}
}
}
return paths;
}
function matchPattern(str, pattern) {
const regexPattern = pattern.replace(/\*/g, ".*").replace(/\./g, "\\.");
return new RegExp(`^${regexPattern}$`, "i").test(str);
}
// src/commands/interactive.ts
import chalk6 from "chalk";
import { confirm as confirm3, checkbox as checkbox3 } from "@inquirer/prompts";
var SAFETY_ICONS3 = {
safe: chalk6.green("\u25CF"),
moderate: chalk6.yellow("\u25CF"),
risky: chalk6.red("\u25CF")
};
async function interactiveCommand(options = {}) {
console.log();
console.log(chalk6.bold.cyan("\u{1F9F9} Clean My Mac"));
console.log(chalk6.dim("\u2500".repeat(50)));
console.log();
const showProgress = !options.noProgress && process.stdout.isTTY;
const scanners = getAllScanners();
const scanProgress = showProgress ? createScanProgress(scanners.length) : null;
console.log(chalk6.cyan("Scanning your Mac for cleanable files...\n"));
const summary = await runAllScans({
parallel: true,
concurrency: 4,
onProgress: (completed, _total, scanner) => {
scanProgress?.update(completed, `Scanning ${scanner.category.name}...`);
}
});
scanProgress?.finish();
if (summary.totalSize === 0) {
console.log(chalk2.green("\n\u2713 Your Mac is already clean!\n"));
console.log(chalk6.green("\u2713 Your Mac is already clean! Nothing to remove.\n"));
return null;

@@ -978,30 +1651,23 @@ }

const safeResults = resultsWithItems.filter((r) => r.category.safetyLevel !== "risky");
if (!options.unsafe && riskyResults.length > 0) {
if (!options.includeRisky && riskyResults.length > 0) {
const riskySize = riskyResults.reduce((sum, r) => sum + r.totalSize, 0);
console.log();
console.log(chalk2.yellow("\u26A0 Skipping risky categories (use --unsafe to include):"));
console.log(chalk6.yellow("\u26A0 Hiding risky categories:"));
for (const result of riskyResults) {
console.log(chalk2.dim(` ${SAFETY_ICONS2.risky} ${result.category.name}: ${formatSize(result.totalSize)}`));
if (result.category.safetyNote) {
console.log(chalk2.dim.italic(` ${result.category.safetyNote}`));
}
console.log(chalk6.dim(` ${SAFETY_ICONS3.risky} ${result.category.name}: ${formatSize(result.totalSize)}`));
}
console.log(chalk2.dim(` Total skipped: ${formatSize(riskySize)}`));
console.log(chalk6.dim(` Total hidden: ${formatSize(riskySize)}`));
console.log(chalk6.dim(" Run with --risky to include these categories"));
resultsWithItems = safeResults;
}
if (resultsWithItems.length === 0) {
console.log(chalk2.green("\n\u2713 Nothing safe to clean!\n"));
console.log(chalk6.green("\n\u2713 Nothing safe to clean!\n"));
return null;
}
let selectedItems = [];
if (options.all) {
selectedItems = resultsWithItems.map((r) => ({
categoryId: r.category.id,
items: r.items
}));
} else {
selectedItems = await selectItemsInteractively(resultsWithItems, options.unsafe);
}
console.log();
console.log(chalk6.bold(`Found ${chalk6.green(formatSize(summary.totalSize))} that can be cleaned:`));
console.log();
const selectedItems = await selectItemsInteractively(resultsWithItems, options.includeRisky);
if (selectedItems.length === 0) {
console.log(chalk2.yellow("\nNo items selected for cleaning.\n"));
console.log(chalk6.yellow("\nNo items selected. Nothing to clean.\n"));
return null;

@@ -1011,29 +1677,16 @@ }

const totalItems = selectedItems.reduce((sum, s) => sum + s.items.length, 0);
if (!options.yes && !options.dryRun) {
const confirm = await inquirer.prompt([
{
type: "confirm",
name: "proceed",
message: `Delete ${totalItems} items (${formatSize(totalToClean)})?`,
default: false
}
]);
if (!confirm.proceed) {
console.log(chalk2.yellow("\nCleaning cancelled.\n"));
return null;
}
}
if (options.dryRun) {
console.log(chalk2.cyan("\n[DRY RUN] Would clean the following:"));
for (const { categoryId, items } of selectedItems) {
const scanner = getScanner(categoryId);
const size = items.reduce((sum, i) => sum + i.size, 0);
console.log(` ${scanner.category.name}: ${items.length} items (${formatSize(size)})`);
}
console.log(chalk2.cyan(`
[DRY RUN] Would free ${formatSize(totalToClean)}
`));
console.log();
console.log(chalk6.bold("Summary:"));
console.log(` Items to delete: ${chalk6.yellow(totalItems.toString())}`);
console.log(` Space to free: ${chalk6.green(formatSize(totalToClean))}`);
console.log();
const proceed = await confirm3({
message: `Proceed with cleaning?`,
default: true
});
if (!proceed) {
console.log(chalk6.yellow("\nCleaning cancelled.\n"));
return null;
}
const cleanSpinner = ora2("Cleaning...").start();
const cleanProgress = showProgress ? createCleanProgress(selectedItems.length) : null;
const cleanResults = {

@@ -1045,6 +1698,7 @@ results: [],

};
let cleanedCount = 0;
for (const { categoryId, items } of selectedItems) {
const scanner = getScanner(categoryId);
cleanSpinner.text = `Cleaning ${scanner.category.name}...`;
const result = await scanner.clean(items, options.dryRun);
cleanProgress?.update(cleanedCount, `Cleaning ${scanner.category.name}...`);
const result = await scanner.clean(items);
cleanResults.results.push(result);

@@ -1054,36 +1708,27 @@ cleanResults.totalFreedSpace += result.freedSpace;

cleanResults.totalErrors += result.errors.length;
cleanedCount++;
}
cleanSpinner.stop();
cleanProgress?.finish();
printCleanResults(cleanResults);
return cleanResults;
}
async function selectItemsInteractively(results, _unsafe = false) {
console.log();
console.log(chalk2.bold("Select categories to clean:"));
console.log();
async function selectItemsInteractively(results, _includeRisky = false) {
const choices = results.map((r) => {
const safetyIcon = SAFETY_ICONS2[r.category.safetyLevel];
const safetyIcon = SAFETY_ICONS3[r.category.safetyLevel];
const isRisky = r.category.safetyLevel === "risky";
return {
name: `${safetyIcon} ${r.category.name.padEnd(28)} ${chalk2.yellow(formatSize(r.totalSize).padStart(10))} ${chalk2.dim(`(${r.items.length} items)`)}`,
name: `${safetyIcon} ${r.category.name.padEnd(28)} ${chalk6.yellow(formatSize(r.totalSize).padStart(10))} ${chalk6.dim(`(${r.items.length} items)`)}`,
value: r.category.id,
checked: !isRisky && r.category.safetyLevel !== "risky",
size: r.totalSize,
items: r.items
checked: !isRisky
};
});
const { categories } = await inquirer.prompt([
{
type: "checkbox",
name: "categories",
message: "Categories",
choices: choices.map((c) => ({
name: c.name,
value: c.value,
checked: c.checked
})),
pageSize: 15
}
]);
const selectedCategories = categories;
const selectedCategories = await checkbox3({
message: "Select categories to clean (space to toggle, enter to confirm):",
choices: choices.map((c) => ({
name: c.name,
value: c.value,
checked: c.checked
})),
pageSize: 15
});
const selectedResults = results.filter((r) => selectedCategories.includes(r.category.id));

@@ -1093,22 +1738,18 @@ const selectedItems = [];

const isRisky = result.category.safetyLevel === "risky";
if (isRisky || result.category.id === "large-files" || result.category.id === "ios-backups") {
const needsItemSelection = isRisky || result.category.id === "large-files" || result.category.id === "ios-backups";
if (needsItemSelection) {
if (isRisky && result.category.safetyNote) {
console.log();
console.log(chalk2.red(`\u26A0 WARNING: ${result.category.safetyNote}`));
console.log(chalk6.red(`\u26A0 WARNING: ${result.category.safetyNote}`));
}
const itemChoices = result.items.map((item) => ({
name: `${item.name.substring(0, 40).padEnd(40)} ${chalk2.yellow(formatSize(item.size).padStart(10))}`,
name: `${item.name.substring(0, 40).padEnd(40)} ${chalk6.yellow(formatSize(item.size).padStart(10))}`,
value: item.path,
checked: false
}));
const { items } = await inquirer.prompt([
{
type: "checkbox",
name: "items",
message: `Select items from ${result.category.name}:`,
choices: itemChoices,
pageSize: 10
}
]);
const selectedPaths = items;
const selectedPaths = await checkbox3({
message: `Select items from ${result.category.name}:`,
choices: itemChoices,
pageSize: 10
});
const selectedItemsList = result.items.filter((i) => selectedPaths.includes(i.path));

@@ -1132,20 +1773,20 @@ if (selectedItemsList.length > 0) {

console.log();
console.log(chalk2.bold.green("\u2713 Cleaning Complete"));
console.log(chalk2.dim("\u2500".repeat(50)));
console.log(chalk6.bold.green("\u2713 Cleaning Complete!"));
console.log(chalk6.dim("\u2500".repeat(50)));
for (const result of summary.results) {
if (result.cleanedItems > 0) {
console.log(
` ${result.category.name.padEnd(30)} ${chalk2.green("\u2713")} ${formatSize(result.freedSpace)} freed`
` ${result.category.name.padEnd(30)} ${chalk6.green("\u2713")} ${formatSize(result.freedSpace)} freed`
);
}
for (const error of result.errors) {
console.log(` ${result.category.name.padEnd(30)} ${chalk2.red("\u2717")} ${error}`);
console.log(` ${result.category.name.padEnd(30)} ${chalk6.red("\u2717")} ${error}`);
}
}
console.log();
console.log(chalk2.dim("\u2500".repeat(50)));
console.log(chalk2.bold(`Freed: ${chalk2.green(formatSize(summary.totalFreedSpace))}`));
console.log(chalk2.dim(`Cleaned ${summary.totalCleanedItems} items`));
console.log(chalk6.dim("\u2500".repeat(50)));
console.log(chalk6.bold(`\u{1F389} Freed ${chalk6.green(formatSize(summary.totalFreedSpace))} of disk space!`));
console.log(chalk6.dim(` Cleaned ${summary.totalCleanedItems} items`));
if (summary.totalErrors > 0) {
console.log(chalk2.red(`Errors: ${summary.totalErrors}`));
console.log(chalk6.red(` Errors: ${summary.totalErrors}`));
}

@@ -1155,112 +1796,18 @@ console.log();

// src/commands/maintenance.ts
import chalk3 from "chalk";
import ora3 from "ora";
// src/maintenance/dns-cache.ts
import { exec as exec3 } from "child_process";
import { promisify as promisify3 } from "util";
var execAsync3 = promisify3(exec3);
async function flushDnsCache() {
try {
await execAsync3("sudo dscacheutil -flushcache");
await execAsync3("sudo killall -HUP mDNSResponder");
return {
success: true,
message: "DNS cache flushed successfully"
};
} catch (error) {
return {
success: false,
message: "Failed to flush DNS cache",
error: error instanceof Error ? error.message : String(error)
};
}
}
// src/maintenance/purgeable.ts
import { exec as exec4 } from "child_process";
import { promisify as promisify4 } from "util";
var execAsync4 = promisify4(exec4);
async function freePurgeableSpace() {
try {
await execAsync4("purge");
return {
success: true,
message: "Purgeable space freed successfully"
};
} catch (error) {
return {
success: false,
message: "Failed to free purgeable space",
error: error instanceof Error ? error.message : String(error)
};
}
}
// src/commands/maintenance.ts
async function maintenanceCommand(options) {
const tasks = [];
if (options.dns) {
tasks.push({ name: "Flush DNS Cache", fn: flushDnsCache });
}
if (options.purgeable) {
tasks.push({ name: "Free Purgeable Space", fn: freePurgeableSpace });
}
if (tasks.length === 0) {
console.log(chalk3.yellow("\nNo maintenance tasks specified."));
console.log(chalk3.dim("Use --dns to flush DNS cache or --purgeable to free purgeable space.\n"));
return;
}
console.log();
console.log(chalk3.bold("Running Maintenance Tasks"));
console.log(chalk3.dim("\u2500".repeat(50)));
for (const task of tasks) {
const spinner = ora3(task.name).start();
const result = await task.fn();
if (result.success) {
spinner.succeed(chalk3.green(result.message));
} else {
spinner.fail(chalk3.red(result.message));
if (result.error) {
console.log(chalk3.dim(` ${result.error}`));
}
}
}
console.log();
}
// src/index.ts
var program = new Command();
program.name("clean-my-mac").description("Open source CLI tool to clean your Mac").version("1.0.0");
program.command("scan").description("Scan your Mac for cleanable files").option("-c, --category <category>", "Scan specific category").option("-v, --verbose", "Show detailed output").option("-l, --list", "List available categories").action(async (options) => {
if (options.list) {
listCategories();
return;
}
if (options.category && !CATEGORIES[options.category]) {
console.error(`Invalid category: ${options.category}`);
console.error("Use --list to see available categories");
process.exit(1);
}
await scanCommand({
category: options.category,
verbose: options.verbose
program.name("clean-my-mac").description("Open source CLI tool to clean your Mac").version("1.1.0").option("-r, --risky", "Include risky categories (downloads, iOS backups, etc)").option("--no-progress", "Disable progress bar").action(async (options) => {
await interactiveCommand({
includeRisky: options.risky,
noProgress: !options.progress
});
});
program.command("clean").description("Clean selected files from your Mac").option("-a, --all", "Clean all categories (safe and moderate only by default)").option("-y, --yes", "Skip confirmation prompts").option("-d, --dry-run", "Show what would be cleaned without actually cleaning").option("-c, --category <category>", "Clean specific category").option("-u, --unsafe", "Include risky categories (downloads, iOS backups, etc)").action(async (options) => {
if (options.category && !CATEGORIES[options.category]) {
console.error(`Invalid category: ${options.category}`);
console.error('Use "clean-my-mac scan --list" to see available categories');
process.exit(1);
}
await cleanCommand({
all: options.all,
program.command("uninstall").description("Uninstall applications and their related files").option("-y, --yes", "Skip confirmation prompts").option("-d, --dry-run", "Show what would be uninstalled without actually uninstalling").option("--no-progress", "Disable progress bar").action(async (options) => {
await uninstallCommand({
yes: options.yes,
dryRun: options.dryRun,
category: options.category,
unsafe: options.unsafe
noProgress: !options.progress
});
});
program.command("maintenance").description("Run maintenance tasks").option("--dns", "Flush DNS cache").option("--purgeable", "Free purgeable space").action(async (options) => {
program.command("maintenance").description("Run maintenance tasks (DNS flush, free purgeable space)").option("--dns", "Flush DNS cache").option("--purgeable", "Free purgeable space").action(async (options) => {
await maintenanceCommand({

@@ -1271,2 +1818,49 @@ dns: options.dns,

});
program.command("categories").description("List all available categories").action(() => {
listCategories();
});
program.command("config").description("Manage configuration").option("--init", "Create default configuration file").option("--show", "Show current configuration").action(async (options) => {
if (options.init) {
const exists2 = await configExists();
if (exists2) {
console.log("Configuration file already exists.");
return;
}
const path = await initConfig();
console.log(`Created configuration file at: ${path}`);
return;
}
if (options.show) {
const exists2 = await configExists();
if (!exists2) {
console.log('No configuration file found. Run "clean-my-mac config --init" to create one.');
return;
}
const config = await loadConfig();
console.log(JSON.stringify(config, null, 2));
return;
}
console.log("Use --init to create config or --show to display current config.");
});
program.command("backup").description("Manage backups").option("--list", "List all backups").option("--clean", "Clean old backups (older than 7 days)").action(async (options) => {
if (options.list) {
const backups = await listBackups();
if (backups.length === 0) {
console.log("No backups found.");
return;
}
console.log("\nBackups:");
for (const backup of backups) {
console.log(` ${backup.date.toLocaleDateString()} - ${formatSize(backup.size)}`);
console.log(` ${backup.path}`);
}
return;
}
if (options.clean) {
const cleaned = await cleanOldBackups();
console.log(`Cleaned ${cleaned} old backups.`);
return;
}
console.log("Use --list to show backups or --clean to remove old ones.");
});
program.parse();
{
"name": "clean-my-mac-cli",
"version": "1.0.0",
"version": "1.1.0",
"description": "Open source CLI tool to clean your Mac - similar to CleanMyMac",

@@ -19,3 +19,3 @@ "type": "module",

"build": "tsup src/index.ts --format esm --dts --clean",
"lint": "eslint src --ext .ts",
"lint": "eslint src",
"typecheck": "tsc --noEmit",

@@ -50,21 +50,20 @@ "test": "vitest run",

"devDependencies": {
"@types/inquirer": "^9.0.9",
"@eslint/js": "^9.28.0",
"@types/node": "^20.10.0",
"@typescript-eslint/eslint-plugin": "^6.13.0",
"@typescript-eslint/parser": "^6.13.0",
"@vitest/coverage-v8": "^1.0.4",
"eslint": "^8.55.0",
"@vitest/coverage-v8": "^4.0.15",
"eslint": "^9.28.0",
"tsup": "^8.0.1",
"tsx": "^4.6.2",
"typescript": "^5.3.2",
"vitest": "^1.0.4"
"typescript-eslint": "^8.48.1",
"vitest": "^4.0.15"
},
"dependencies": {
"@inquirer/prompts": "^7.5.1",
"chalk": "^5.3.0",
"commander": "^11.1.0",
"inquirer": "^9.2.12",
"ora": "^8.0.1"
"commander": "^14.0.2",
"ora": "^9.0.0"
},
"engines": {
"node": ">=18"
"node": ">=20"
},

@@ -71,0 +70,0 @@ "os": [

+83
-59

@@ -5,69 +5,81 @@ # Clean My Mac CLI

## Installation
[![npm version](https://badge.fury.io/js/clean-my-mac-cli.svg)](https://www.npmjs.com/package/clean-my-mac-cli)
```bash
npm install -g clean-my-mac-cli
```
## Quick Start
Or run directly with npx:
Just run one command - no installation required:
```bash
npx clean-my-mac-cli scan
npx clean-my-mac-cli
```
That's it! The CLI will:
1. ๐Ÿ” Scan your Mac for cleanable files
2. ๐Ÿ“‹ Show you what was found
3. โœ… Let you select what to clean
4. ๐Ÿ—‘๏ธ Clean the selected items
## Features
- **Smart Scanning**: Automatically detects cleanable files across multiple categories
- **Safety Levels**: Items are classified as safe, moderate, or risky to prevent accidental data loss
- **Interactive Selection**: Choose exactly what to clean with an interactive checkbox interface
- **Dry Run Mode**: Preview what would be cleaned without actually deleting anything
- **Multiple Categories**: Clean system caches, logs, browser data, development files, and more
- **One Command**: Just run `npx clean-my-mac-cli` - no complex flags to remember
- **Interactive**: Select exactly what you want to clean with checkboxes
- **Safe by Default**: Risky items are hidden unless you explicitly include them
- **Smart Scanning**: Finds caches, logs, development files, browser data, and more
- **App Uninstaller**: Remove apps completely with all their associated files
- **Maintenance Tasks**: Flush DNS cache, free purgeable space
## Usage
### Scan for Cleanable Files
### Basic Usage (Recommended)
```bash
# Full scan
clean-my-mac scan
# Interactive mode - scan, select, and clean
npx clean-my-mac-cli
# Detailed scan with file breakdown
clean-my-mac scan --verbose
# Include risky categories (downloads, iOS backups, large files)
npx clean-my-mac-cli --risky
```
# Scan specific category
clean-my-mac scan --category dev-cache
### Uninstall Apps
# List all available categories
clean-my-mac scan --list
Remove applications completely, including their preferences, caches, and support files:
```bash
npx clean-my-mac-cli uninstall
```
### Clean Files
### Maintenance Tasks
```bash
# Interactive cleaning (recommended)
clean-my-mac clean
# Flush DNS cache (may require sudo)
npx clean-my-mac-cli maintenance --dns
# Preview what would be cleaned
clean-my-mac clean --dry-run
# Clean all safe and moderate categories
clean-my-mac clean --all --yes
# Include risky categories (downloads, iOS backups, etc)
clean-my-mac clean --all --yes --unsafe
# Free purgeable space
npx clean-my-mac-cli maintenance --purgeable
```
### Maintenance Tasks
### Other Commands
```bash
# Flush DNS cache
clean-my-mac maintenance --dns
# List all available categories
npx clean-my-mac-cli categories
# Free purgeable space
clean-my-mac maintenance --purgeable
# Manage configuration
npx clean-my-mac-cli config --init
npx clean-my-mac-cli config --show
# Run both
clean-my-mac maintenance --dns --purgeable
# Manage backups
npx clean-my-mac-cli backup --list
npx clean-my-mac-cli backup --clean
```
## Global Installation (Optional)
If you use this tool frequently, install it globally:
```bash
npm install -g clean-my-mac-cli
clean-my-mac
```
## Categories

@@ -91,2 +103,3 @@

| `docker` | ๐ŸŸข Safe | Unused Docker images, containers, volumes |
| `node-modules` | ๐ŸŸก Moderate | Orphaned node_modules in old projects |

@@ -101,2 +114,3 @@ ### Storage

| `mail-attachments` | ๐Ÿ”ด Risky | Downloaded email attachments |
| `duplicates` | ๐Ÿ”ด Risky | Duplicate files (keeps newest) |

@@ -119,30 +133,40 @@ ### Browsers

- ๐ŸŸก **Moderate**: Generally safe, but may cause minor inconvenience (e.g., apps rebuilding cache).
- ๐Ÿ”ด **Risky**: May contain important data. Requires `--unsafe` flag and individual file selection.
- ๐Ÿ”ด **Risky**: May contain important data. Hidden by default, use `--risky` to include.
## Example Output
## Example
```
Scanning your Mac...
$ npx clean-my-mac-cli
Scan Results
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
๐Ÿงน Clean My Mac
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
System Junk
๐ŸŸก User Cache Files 15.5 GB (118 items)
๐ŸŸก System Log Files 102.4 MB (80 items)
๐ŸŸข Temporary Files 549.2 MB (622 items)
๐Ÿ”ด Language Files 68.9 MB (535 items)
Scanning your Mac for cleanable files...
Development
๐ŸŸก Development Cache 21.9 GB (14 items)
๐ŸŸข Homebrew Cache 225.6 MB (1 items)
๐ŸŸข Docker 4.9 GB (3 items)
Found 44.8 GB that can be cleaned:
Browsers
๐ŸŸข Browser Cache 1.5 GB (3 items)
? Select categories to clean (space to toggle, enter to confirm):
โ—‰ ๐ŸŸข Trash 2.1 GB (45 items)
โ—‰ ๐ŸŸข Browser Cache 1.5 GB (3 items)
โ—‰ ๐ŸŸข Temporary Files 549.2 MB (622 items)
โ—‰ ๐ŸŸก User Cache Files 15.5 GB (118 items)
โ—‰ ๐ŸŸก Development Cache 21.9 GB (14 items)
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Total: 44.8 GB can be cleaned (1377 items)
Summary:
Items to delete: 802
Space to free: 41.5 GB
Safety: ๐ŸŸข safe ๐ŸŸก moderate ๐Ÿ”ด risky (use --unsafe)
? Proceed with cleaning? (Y/n)
โœ“ Cleaning Complete!
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Trash โœ“ 2.1 GB freed
Browser Cache โœ“ 1.5 GB freed
Temporary Files โœ“ 549.2 MB freed
User Cache Files โœ“ 15.5 GB freed
Development Cache โœ“ 21.9 GB freed
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
๐ŸŽ‰ Freed 41.5 GB of disk space!
Cleaned 802 items
```

@@ -161,3 +185,3 @@

# Run in development mode
npm run dev -- scan
npm run dev

@@ -193,2 +217,2 @@ # Run tests

This tool deletes files from your system. While we've implemented safety measures, always use `--dry-run` first to preview changes, and ensure you have backups of important data. Use at your own risk.
This tool deletes files from your system. While we've implemented safety measures, always ensure you have backups of important data. Use at your own risk.