feat(cleaner): 支持开发工具缓存清理
No files matched your search
@@ -29,12 +29,18 @@ public enum CleanEngine {
|
||||
} catch {
|
||||
return RuleScan(rule: rule, items: [], skippedCount: 0, blocked: .needsFullDiskAccess)
|
||||
}
|
||||
if !candidates.isEmpty, let tool = rule.requiredTool, !environment.isToolAvailable(tool) {
|
||||
return RuleScan(rule: rule, items: [], skippedCount: 0, blocked: .toolUnavailable(tool))
|
||||
}
|
||||
|
||||
var items: [CleanItem] = []
|
||||
var skipped = 0
|
||||
for url in candidates {
|
||||
if Task.isCancelled { break }
|
||||
if skipReason(for: url, rule: rule, running: running, environment: environment, safety: safety) != nil {
|
||||
// 工具托管的缓存由工具自身处理锁与内部目录;这里的路径只用于只读计量,不能套用
|
||||
// “应用数据保护词”过滤,否则名为 xstats 等普通包会被误报为正在使用。
|
||||
if !rule.usesToolCleaner,
|
||||
skipReason(for: url, rule: rule, running: running, environment: environment, safety: safety) != nil {
|
||||
skipped += 1
|
||||
continue
|
||||
}
|
||||
@@ -99,6 +105,28 @@ public enum CleanEngine {
|
||||
report.skippedCount += scan.items.count
|
||||
continue
|
||||
}
|
||||
if let cleanWithTool = scan.rule.cleanWithTool {
|
||||
if let tool = scan.rule.requiredTool, !environment.isToolAvailable(tool) {
|
||||
report.failures.append(tr("\(scan.rule.title):未找到 \(tool),无法安全清理"))
|
||||
continue
|
||||
}
|
||||
do {
|
||||
try await cleanWithTool(environment)
|
||||
for item in scan.items {
|
||||
let remaining = allocatedSize(of: item.url)
|
||||
log?.record(item: item, rule: scan.rule, action: "tool", detail: scan.rule.requiredTool)
|
||||
guard remaining < item.size else { continue }
|
||||
report.freedBytes += item.size - remaining
|
||||
report.removedCount += 1
|
||||
}
|
||||
} catch {
|
||||
report.failures.append(tr("\(scan.rule.title):\(error.localizedDescription)"))
|
||||
for item in scan.items {
|
||||
log?.record(item: item, rule: scan.rule, action: "fail", detail: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 废纸篓规则本身必须永久删除;其他可再生内容按用户偏好决定
|
||||
let useTrash = scan.rule.id != RuleCatalog.trash.id && (scan.rule.policy == .trash || preferTrash)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ public enum DeletionPolicy: Sendable {
|
||||
public enum SkipReason: Sendable, Equatable, Hashable {
|
||||
case appRunning(String)
|
||||
case needsFullDiskAccess
|
||||
case toolUnavailable(String)
|
||||
case recentlyModified
|
||||
case protected
|
||||
|
||||
@@ -34,6 +35,7 @@ public enum SkipReason: Sendable, Equatable, Hashable {
|
||||
switch self {
|
||||
case .appRunning(let name): tr("\(name) 正在运行")
|
||||
case .needsFullDiskAccess: tr("需要完全磁盘访问权限")
|
||||
case .toolUnavailable(let tool): tr("未找到 \(tool),无法安全清理")
|
||||
case .recentlyModified: tr("刚刚被使用")
|
||||
case .protected: tr("受保护")
|
||||
}
|
||||
@@ -45,13 +47,19 @@ public struct CleanEnvironment: Sendable {
|
||||
public var home: String
|
||||
public var runningBundleIdentifiers: @Sendable () -> Set<String>
|
||||
public var now: @Sendable () -> Date
|
||||
public var isToolAvailable: @Sendable (String) -> Bool
|
||||
public var runTool: @Sendable (String, [String]) async throws -> ToolRunResult
|
||||
|
||||
public init(home: String = NSHomeDirectory(),
|
||||
runningBundleIdentifiers: @escaping @Sendable () -> Set<String>,
|
||||
now: @escaping @Sendable () -> Date = Date.init) {
|
||||
now: @escaping @Sendable () -> Date = Date.init,
|
||||
isToolAvailable: (@Sendable (String) -> Bool)? = nil,
|
||||
runTool: (@Sendable (String, [String]) async throws -> ToolRunResult)? = nil) {
|
||||
self.home = home
|
||||
self.runningBundleIdentifiers = runningBundleIdentifiers
|
||||
self.now = now
|
||||
self.isToolAvailable = isToolAvailable ?? { DeveloperToolRunner.executable(named: $0, home: home) != nil }
|
||||
self.runTool = runTool ?? { try await DeveloperToolRunner.run($0, arguments: $1, home: home) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,13 +77,16 @@ public struct CleanRule: Sendable, Identifiable {
|
||||
public let checksOwnerApp: Bool
|
||||
/// 最近修改过的条目视为正在使用
|
||||
public let minimumAge: TimeInterval
|
||||
public let requiredTool: String?
|
||||
let cleanWithTool: (@Sendable (CleanEnvironment) async throws -> Void)?
|
||||
/// 列出候选条目(顶层文件或目录)
|
||||
let locate: @Sendable (CleanEnvironment) throws -> [URL]
|
||||
|
||||
init(id: String, category: CleanCategory, title: String, detail: String, symbol: String,
|
||||
selectedByDefault: Bool = true, policy: DeletionPolicy = .delete,
|
||||
blockingApps: [(bundleID: String, name: String)] = [], checksOwnerApp: Bool = false,
|
||||
minimumAge: TimeInterval = 120,
|
||||
minimumAge: TimeInterval = 120, requiredTool: String? = nil,
|
||||
cleanWithTool: (@Sendable (CleanEnvironment) async throws -> Void)? = nil,
|
||||
locate: @escaping @Sendable (CleanEnvironment) throws -> [URL]) {
|
||||
self.id = id
|
||||
self.category = category
|
||||
@@ -87,8 +98,12 @@ public struct CleanRule: Sendable, Identifiable {
|
||||
self.blockingApps = blockingApps
|
||||
self.checksOwnerApp = checksOwnerApp
|
||||
self.minimumAge = minimumAge
|
||||
self.requiredTool = requiredTool
|
||||
self.cleanWithTool = cleanWithTool
|
||||
self.locate = locate
|
||||
}
|
||||
|
||||
public var usesToolCleaner: Bool { cleanWithTool != nil }
|
||||
}
|
||||
|
||||
public struct CleanItem: Sendable, Identifiable, Hashable {
|
||||
@@ -103,7 +118,7 @@ public struct RuleScan: Sendable, Identifiable {
|
||||
/// 按大小降序
|
||||
public let items: [CleanItem]
|
||||
public let skippedCount: Int
|
||||
/// 整条规则被阻止的原因(浏览器运行中、缺少权限)
|
||||
/// 整条规则被阻止的原因(浏览器运行中、缺少权限或对应工具不可用)
|
||||
public let blocked: SkipReason?
|
||||
|
||||
public var totalSize: UInt64 { items.reduce(0) { $0 + $1.size } }
|
||||
|
||||
@@ -4,7 +4,9 @@ import Localization
|
||||
/// 清理规则。目录清单参考了 Mole 的清理范围(仅参考范围,代码为独立实现)。
|
||||
public enum RuleCatalog {
|
||||
public static func rules() -> [CleanRule] {
|
||||
[userCaches, logs] + browsers.map(browserRule) + [xcodeDerivedData, simulatorCaches, npmCache, xcodeArchives,
|
||||
[userCaches, logs] + browsers.map(browserRule) + [xcodeDerivedData, simulatorCaches,
|
||||
npmCache, yarnCache, pnpmCache, bunCache,
|
||||
goCache, rustCache, uvCache, xcodeArchives,
|
||||
incompleteDownloads, installers, trash]
|
||||
}
|
||||
|
||||
@@ -127,12 +129,92 @@ public enum RuleCatalog {
|
||||
|
||||
static let npmCache = CleanRule(
|
||||
id: "developer.npm", category: .developer, title: tr("npm 缓存"),
|
||||
detail: tr("~/.npm/_cacache,安装依赖时自动重新下载"), symbol: "shippingbox"
|
||||
detail: tr("npm 下载缓存,由 npm 自行清理"), symbol: "tool-npm",
|
||||
selectedByDefault: false, minimumAge: 0,
|
||||
requiredTool: "npm", cleanWithTool: { env in
|
||||
_ = try await env.runTool("npm", ["cache", "clean", "--force"])
|
||||
}
|
||||
) { env in
|
||||
let path = env.home + "/.npm/_cacache"
|
||||
return FileManager.default.fileExists(atPath: path) ? [URL(fileURLWithPath: path)] : []
|
||||
}
|
||||
|
||||
static let yarnCache = CleanRule(
|
||||
id: "developer.yarn", category: .developer, title: tr("Yarn 缓存"),
|
||||
detail: tr("Yarn 全局与离线镜像缓存,由 Yarn 自行清理"), symbol: "tool-yarn",
|
||||
selectedByDefault: false, minimumAge: 0,
|
||||
requiredTool: "yarn", cleanWithTool: { env in
|
||||
let version = try await env.runTool("yarn", ["--version"]).output
|
||||
let major = Int(version.split(separator: ".").first ?? "1") ?? 1
|
||||
let arguments = major >= 2 ? ["cache", "clean", "--all"] : ["cache", "clean"]
|
||||
_ = try await env.runTool("yarn", arguments)
|
||||
}
|
||||
) { env in
|
||||
try childrenIfExists(of: env.home + "/Library/Caches/Yarn")
|
||||
+ childrenIfExists(of: env.home + "/.yarn/berry/cache")
|
||||
}
|
||||
|
||||
static let pnpmCache = CleanRule(
|
||||
id: "developer.pnpm", category: .developer, title: tr("pnpm 缓存"),
|
||||
detail: tr("pnpm 内容寻址存储,仅清理未被引用的包"), symbol: "tool-pnpm",
|
||||
selectedByDefault: false, minimumAge: 0,
|
||||
requiredTool: "pnpm", cleanWithTool: { env in
|
||||
_ = try await env.runTool("pnpm", ["store", "prune"])
|
||||
}
|
||||
) { env in
|
||||
try childrenIfExists(of: env.home + "/Library/pnpm/store")
|
||||
+ childrenIfExists(of: env.home + "/.pnpm-store")
|
||||
}
|
||||
|
||||
static let bunCache = CleanRule(
|
||||
id: "developer.bun", category: .developer, title: tr("Bun 缓存"),
|
||||
detail: tr("Bun 下载的包缓存,由 Bun 自行清理"), symbol: "tool-bun",
|
||||
selectedByDefault: false, minimumAge: 0,
|
||||
requiredTool: "bun", cleanWithTool: { env in
|
||||
_ = try await env.runTool("bun", ["pm", "cache", "rm"])
|
||||
}
|
||||
) { env in
|
||||
try childrenIfExists(of: env.home + "/.bun/install/cache")
|
||||
}
|
||||
|
||||
static let goCache = CleanRule(
|
||||
id: "developer.go", category: .developer, title: tr("Go 缓存"),
|
||||
detail: tr("Go 编译与完整模块缓存,由 go clean 清理"), symbol: "tool-go",
|
||||
selectedByDefault: false, minimumAge: 0,
|
||||
requiredTool: "go", cleanWithTool: { env in
|
||||
_ = try await env.runTool("go", ["clean", "-cache", "-modcache"])
|
||||
}
|
||||
) { env in
|
||||
let fileManager = FileManager.default
|
||||
let buildCache = URL(fileURLWithPath: env.home + "/Library/Caches/go-build")
|
||||
var urls = fileManager.fileExists(atPath: buildCache.path) ? [buildCache] : []
|
||||
urls += try childrenIfExists(of: env.home + "/go/pkg/mod")
|
||||
return urls
|
||||
}
|
||||
|
||||
static let rustCache = CleanRule(
|
||||
id: "developer.rust", category: .developer, title: tr("Rust 缓存"),
|
||||
detail: tr("Cargo 注册表与 Git 依赖缓存,需要 cargo-cache"), symbol: "tool-rust",
|
||||
selectedByDefault: false, minimumAge: 0,
|
||||
requiredTool: "cargo-cache", cleanWithTool: { env in
|
||||
_ = try await env.runTool("cargo-cache", ["-r", "all"])
|
||||
}
|
||||
) { env in
|
||||
try childrenIfExists(of: env.home + "/.cargo/registry")
|
||||
+ childrenIfExists(of: env.home + "/.cargo/git")
|
||||
}
|
||||
|
||||
static let uvCache = CleanRule(
|
||||
id: "developer.uv", category: .developer, title: tr("uv 缓存"),
|
||||
detail: tr("uv 下载的 Python 包与构建缓存,由 uv 自行清理"), symbol: "tool-uv",
|
||||
selectedByDefault: false, minimumAge: 0,
|
||||
requiredTool: "uv", cleanWithTool: { env in
|
||||
_ = try await env.runTool("uv", ["cache", "clean"])
|
||||
}
|
||||
) { env in
|
||||
try childrenIfExists(of: env.home + "/.cache/uv")
|
||||
}
|
||||
|
||||
static let xcodeArchives = CleanRule(
|
||||
id: "developer.archives", category: .developer, title: tr("Xcode 归档"),
|
||||
detail: tr("打包生成的 .xcarchive,删除后无法重新符号化旧版本崩溃日志"), symbol: "archivebox.fill",
|
||||
|
||||
@@ -36,6 +36,14 @@ public struct SafetyGuard: Sendable {
|
||||
"Library/Developer/Xcode/Archives",
|
||||
"Library/Developer/CoreSimulator/Caches",
|
||||
".npm",
|
||||
"go/pkg/mod",
|
||||
".cargo/registry",
|
||||
".cargo/git",
|
||||
".cache/uv",
|
||||
".yarn/berry/cache",
|
||||
"Library/pnpm/store",
|
||||
".pnpm-store",
|
||||
".bun/install/cache",
|
||||
"Downloads",
|
||||
".Trash",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright (C) 2026 ysicing
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import Foundation
|
||||
import Localization
|
||||
|
||||
public struct ToolRunResult: Sendable {
|
||||
public let status: Int32
|
||||
public let output: String
|
||||
}
|
||||
|
||||
public enum ToolExecutionError: Error, LocalizedError, Sendable {
|
||||
case unavailable(String)
|
||||
case failed(tool: String, status: Int32, output: String)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .unavailable(let tool):
|
||||
tr("未找到 \(tool),无法安全清理")
|
||||
case .failed(let tool, let status, let output):
|
||||
output.isEmpty
|
||||
? tr("\(tool) 清理失败(退出码 \(status))")
|
||||
: tr("\(tool) 清理失败(退出码 \(status)):\(output)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum DeveloperToolRunner {
|
||||
/// 只从常见的用户工具目录和系统工具目录解析,避免依赖 GUI 进程通常不完整的 PATH。
|
||||
static func executable(named name: String, home: String) -> URL? {
|
||||
guard !name.isEmpty, !name.contains("/") else { return nil }
|
||||
return searchDirectories(home: home)
|
||||
.map { $0.appendingPathComponent(name) }
|
||||
.first { FileManager.default.isExecutableFile(atPath: $0.path) }
|
||||
}
|
||||
|
||||
/// 外部工具可能执行较久;放到并发执行器,避免阻塞调用它的主 Actor。
|
||||
@concurrent
|
||||
static func run(_ name: String, arguments: [String], home: String) async throws -> ToolRunResult {
|
||||
guard let executable = executable(named: name, home: home) else {
|
||||
throw ToolExecutionError.unavailable(name)
|
||||
}
|
||||
|
||||
let process = Process()
|
||||
process.executableURL = executable
|
||||
process.arguments = arguments
|
||||
process.currentDirectoryURL = FileManager.default.temporaryDirectory
|
||||
|
||||
var environment = ProcessInfo.processInfo.environment
|
||||
environment["HOME"] = home
|
||||
environment["PATH"] = ([executable.deletingLastPathComponent()] + searchDirectories(home: home))
|
||||
.map(\.path).uniqued().joined(separator: ":")
|
||||
environment["NO_COLOR"] = "1"
|
||||
environment["TERM"] = "dumb"
|
||||
process.environment = environment
|
||||
|
||||
let pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
process.standardError = pipe
|
||||
do {
|
||||
try process.run()
|
||||
} catch {
|
||||
throw ToolExecutionError.failed(tool: name, status: -1, output: error.localizedDescription)
|
||||
}
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
process.waitUntilExit()
|
||||
let output = String(decoding: data, as: UTF8.self)
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard process.terminationStatus == 0 else {
|
||||
throw ToolExecutionError.failed(tool: name, status: process.terminationStatus,
|
||||
output: String(output.prefix(2_000)))
|
||||
}
|
||||
return ToolRunResult(status: process.terminationStatus, output: output)
|
||||
}
|
||||
|
||||
private static func searchDirectories(home: String) -> [URL] {
|
||||
let homeURL = URL(fileURLWithPath: home, isDirectory: true)
|
||||
var directories = [
|
||||
homeURL.appendingPathComponent(".local/bin"),
|
||||
homeURL.appendingPathComponent(".cargo/bin"),
|
||||
homeURL.appendingPathComponent(".bun/bin"),
|
||||
homeURL.appendingPathComponent(".volta/bin"),
|
||||
homeURL.appendingPathComponent(".asdf/shims"),
|
||||
homeURL.appendingPathComponent(".local/share/mise/shims"),
|
||||
]
|
||||
|
||||
let nvmRoot = homeURL.appendingPathComponent(".nvm/versions/node")
|
||||
let nvmVersions = ((try? FileManager.default.contentsOfDirectory(
|
||||
at: nvmRoot, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]
|
||||
)) ?? []).sorted {
|
||||
$0.lastPathComponent.compare($1.lastPathComponent, options: .numeric) == .orderedDescending
|
||||
}
|
||||
directories += nvmVersions.map { $0.appendingPathComponent("bin") }
|
||||
directories += ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"]
|
||||
.map { URL(fileURLWithPath: $0, isDirectory: true) }
|
||||
return directories.uniqued()
|
||||
}
|
||||
}
|
||||
|
||||
private extension Sequence where Element: Hashable {
|
||||
func uniqued() -> [Element] {
|
||||
var seen = Set<Element>()
|
||||
return filter { seen.insert($0).inserted }
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,25 @@ Xcode 编译缓存 ذاكرة بناء Xcode المؤقتة
|
||||
cleanip.io 对这次评分的把握:各数据源结论越一致越高 مدى ثقة cleanip.io في التقييم: ترتفع كلما زاد اتفاق المصادر
|
||||
macOS 系统目录(系统组件) مجلد نظام macOS (مكوّن نظام)
|
||||
npm 缓存 ذاكرة npm المؤقتة
|
||||
Go 缓存 ذاكرة Go المؤقتة
|
||||
Rust 缓存 ذاكرة Rust المؤقتة
|
||||
uv 缓存 ذاكرة uv المؤقتة
|
||||
Yarn 缓存 ذاكرة Yarn المؤقتة
|
||||
pnpm 缓存 ذاكرة pnpm المؤقتة
|
||||
Bun 缓存 ذاكرة Bun المؤقتة
|
||||
npm 下载缓存,由 npm 自行清理 ذاكرة تنزيل npm المؤقتة، ينظفها npm بنفسه
|
||||
Yarn 全局与离线镜像缓存,由 Yarn 自行清理 ذاكرة Yarn العامة ومرآة العمل دون اتصال، ينظفها Yarn بنفسه
|
||||
pnpm 内容寻址存储,仅清理未被引用的包 مخزن pnpm المعنون بالمحتوى؛ تزال الحزم غير المشار إليها فقط
|
||||
Bun 下载的包缓存,由 Bun 自行清理 ذاكرة تنزيل حزم Bun المؤقتة، ينظفها Bun بنفسه
|
||||
Go 编译与完整模块缓存,由 go clean 清理 ذاكرة بناء Go والوحدات الكاملة، ينظفها go clean
|
||||
Cargo 注册表与 Git 依赖缓存,需要 cargo-cache ذاكرة سجل Cargo وتبعيات Git المؤقتة؛ تتطلب cargo-cache
|
||||
uv 下载的 Python 包与构建缓存,由 uv 自行清理 ذاكرة حزم Python والبناء التي نزّلها uv، ينظفها uv بنفسه
|
||||
未找到 {},无法安全清理 لم يتم العثور على {}، لذلك لا يمكن تنظيفه بأمان
|
||||
{} 清理失败(退出码 {}) فشل تنظيف {} (رمز الخروج {})
|
||||
{} 清理失败(退出码 {}):{} فشل تنظيف {} (رمز الخروج {}): {}
|
||||
{}:未找到 {},无法安全清理 {}: لم يتم العثور على {}، لذلك لا يمكن تنظيفه بأمان
|
||||
支持的内容先移到废纸篓;工具缓存由对应命令直接清理。 تُنقل العناصر المدعومة إلى سلة المهملات أولًا؛ وتُنظف ذاكرة الأدوات مباشرة بأوامرها.
|
||||
支持的缓存可以恢复;工具缓存始终由对应命令直接清理 يمكن استعادة الذاكرة المؤقتة المدعومة؛ وتُنظف ذاكرة الأدوات دائمًا مباشرة بأوامرها
|
||||
{} {} 英寸 {} بحجم {} بوصة
|
||||
{} · {} 个 {} · {}
|
||||
{} · {} 核 {} · {} نوى
|
||||
|
||||
|
@@ -95,6 +95,25 @@ Xcode 编译缓存 Xcode-Build-Cache
|
||||
cleanip.io 对这次评分的把握:各数据源结论越一致越高 Zuverlässigkeit der Bewertung von cleanip.io: steigt mit der Übereinstimmung der Datenquellen
|
||||
macOS 系统目录(系统组件) macOS-Systemordner (Systemkomponente)
|
||||
npm 缓存 npm-Cache
|
||||
Go 缓存 Go-Cache
|
||||
Rust 缓存 Rust-Cache
|
||||
uv 缓存 uv-Cache
|
||||
Yarn 缓存 Yarn-Cache
|
||||
pnpm 缓存 pnpm-Cache
|
||||
Bun 缓存 Bun-Cache
|
||||
npm 下载缓存,由 npm 自行清理 npm-Download-Cache; wird von npm selbst bereinigt
|
||||
Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Globaler Yarn- und Offline-Mirror-Cache; wird von Yarn selbst bereinigt
|
||||
pnpm 内容寻址存储,仅清理未被引用的包 Inhaltsadressierter pnpm-Speicher; nur nicht referenzierte Pakete werden entfernt
|
||||
Bun 下载的包缓存,由 Bun 自行清理 Bun-Paketdownload-Cache; wird von Bun selbst bereinigt
|
||||
Go 编译与完整模块缓存,由 go clean 清理 Go-Build- und vollständiger Modul-Cache; wird mit go clean bereinigt
|
||||
Cargo 注册表与 Git 依赖缓存,需要 cargo-cache Cargo-Registry- und Git-Abhängigkeits-Cache; benötigt cargo-cache
|
||||
uv 下载的 Python 包与构建缓存,由 uv 自行清理 Von uv geladener Python-Paket- und Build-Cache; wird von uv selbst bereinigt
|
||||
未找到 {},无法安全清理 {} wurde nicht gefunden und kann daher nicht sicher bereinigt werden
|
||||
{} 清理失败(退出码 {}) Bereinigung von {} fehlgeschlagen (Exit-Code {})
|
||||
{} 清理失败(退出码 {}):{} Bereinigung von {} fehlgeschlagen (Exit-Code {}): {}
|
||||
{}:未找到 {},无法安全清理 {}: {} wurde nicht gefunden und kann daher nicht sicher bereinigt werden
|
||||
支持的内容先移到废纸篓;工具缓存由对应命令直接清理。 Unterstützte Inhalte werden zuerst in den Papierkorb verschoben; Tool-Caches werden direkt durch ihre Befehle bereinigt.
|
||||
支持的缓存可以恢复;工具缓存始终由对应命令直接清理 Unterstützte Caches können wiederhergestellt werden; Tool-Caches werden immer direkt durch ihre Befehle bereinigt
|
||||
{} {} 英寸 {} {} Zoll
|
||||
{} · {} 个 {} · {}
|
||||
{} · {} 核 {} · {} Kerne
|
||||
|
||||
|
@@ -95,6 +95,25 @@ Xcode 编译缓存 Caché de compilación de Xcode
|
||||
cleanip.io 对这次评分的把握:各数据源结论越一致越高 Confianza de cleanip.io en la puntuación: aumenta cuanto más coinciden sus fuentes
|
||||
macOS 系统目录(系统组件) Carpeta del sistema macOS (componente del sistema)
|
||||
npm 缓存 Caché de npm
|
||||
Go 缓存 Caché de Go
|
||||
Rust 缓存 Caché de Rust
|
||||
uv 缓存 Caché de uv
|
||||
Yarn 缓存 Caché de Yarn
|
||||
pnpm 缓存 Caché de pnpm
|
||||
Bun 缓存 Caché de Bun
|
||||
npm 下载缓存,由 npm 自行清理 Caché de descargas de npm, limpiada por npm
|
||||
Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Caché global y espejo sin conexión de Yarn, limpiadas por Yarn
|
||||
pnpm 内容寻址存储,仅清理未被引用的包 Almacén direccionado por contenido de pnpm; solo se eliminan paquetes sin referencias
|
||||
Bun 下载的包缓存,由 Bun 自行清理 Caché de descarga de paquetes de Bun, limpiada por Bun
|
||||
Go 编译与完整模块缓存,由 go clean 清理 Cachés de compilación y módulos completos de Go, limpiadas con go clean
|
||||
Cargo 注册表与 Git 依赖缓存,需要 cargo-cache Cachés del registro de Cargo y dependencias Git; requiere cargo-cache
|
||||
uv 下载的 Python 包与构建缓存,由 uv 自行清理 Cachés de paquetes de Python y compilación descargadas por uv, limpiadas por uv
|
||||
未找到 {},无法安全清理 No se encontró {} y no se puede limpiar de forma segura
|
||||
{} 清理失败(退出码 {}) Falló la limpieza de {} (código de salida {})
|
||||
{} 清理失败(退出码 {}):{} Falló la limpieza de {} (código de salida {}): {}
|
||||
{}:未找到 {},无法安全清理 {}: no se encontró {} y no se puede limpiar de forma segura
|
||||
支持的内容先移到废纸篓;工具缓存由对应命令直接清理。 Los elementos compatibles se mueven primero a la papelera; las cachés de herramientas se limpian directamente con sus comandos.
|
||||
支持的缓存可以恢复;工具缓存始终由对应命令直接清理 Las cachés compatibles se pueden restaurar; las cachés de herramientas siempre se limpian directamente con sus comandos
|
||||
{} {} 英寸 {} de {} pulgadas
|
||||
{} · {} 个 {} · {}
|
||||
{} · {} 核 {} · {} núcleos
|
||||
|
||||
|
@@ -95,6 +95,25 @@ Xcode 编译缓存 Cache de compilation Xcode
|
||||
cleanip.io 对这次评分的把握:各数据源结论越一致越高 Fiabilité du score selon cleanip.io : plus les sources concordent, plus elle est élevée
|
||||
macOS 系统目录(系统组件) Dossier système macOS (composant système)
|
||||
npm 缓存 Cache npm
|
||||
Go 缓存 Cache Go
|
||||
Rust 缓存 Cache Rust
|
||||
uv 缓存 Cache uv
|
||||
Yarn 缓存 Cache Yarn
|
||||
pnpm 缓存 Cache pnpm
|
||||
Bun 缓存 Cache Bun
|
||||
npm 下载缓存,由 npm 自行清理 Cache de téléchargement npm, nettoyé par npm
|
||||
Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Caches global et miroir hors ligne de Yarn, nettoyés par Yarn
|
||||
pnpm 内容寻址存储,仅清理未被引用的包 Stockage adressé par contenu de pnpm ; seuls les paquets non référencés sont supprimés
|
||||
Bun 下载的包缓存,由 Bun 自行清理 Cache de téléchargement des paquets Bun, nettoyé par Bun
|
||||
Go 编译与完整模块缓存,由 go clean 清理 Caches de compilation et de modules complets Go, nettoyés par go clean
|
||||
Cargo 注册表与 Git 依赖缓存,需要 cargo-cache Caches du registre Cargo et des dépendances Git ; nécessite cargo-cache
|
||||
uv 下载的 Python 包与构建缓存,由 uv 自行清理 Caches de paquets Python et de compilation téléchargés par uv, nettoyés par uv
|
||||
未找到 {},无法安全清理 {} est introuvable et ne peut pas être nettoyé en toute sécurité
|
||||
{} 清理失败(退出码 {}) Échec du nettoyage de {} (code de sortie {})
|
||||
{} 清理失败(退出码 {}):{} Échec du nettoyage de {} (code de sortie {}) : {}
|
||||
{}:未找到 {},无法安全清理 {} : {} est introuvable et ne peut pas être nettoyé en toute sécurité
|
||||
支持的内容先移到废纸篓;工具缓存由对应命令直接清理。 Les éléments pris en charge sont d’abord placés dans la corbeille ; les caches d’outils sont nettoyés directement par leurs commandes.
|
||||
支持的缓存可以恢复;工具缓存始终由对应命令直接清理 Les caches pris en charge peuvent être restaurés ; les caches d’outils sont toujours nettoyés directement par leurs commandes
|
||||
{} {} 英寸 {} {} pouces
|
||||
{} · {} 个 {} · {}
|
||||
{} · {} 核 {} · {} cœurs
|
||||
|
||||
|
@@ -95,6 +95,25 @@ Xcode 编译缓存 Xcodeビルドキャッシュ
|
||||
cleanip.io 对这次评分的把握:各数据源结论越一致越高 cleanip.ioの評価の確度:情報源の結果が一致するほど高くなります
|
||||
macOS 系统目录(系统组件) macOSシステムフォルダ(システムコンポーネント)
|
||||
npm 缓存 npmキャッシュ
|
||||
Go 缓存 Goキャッシュ
|
||||
Rust 缓存 Rustキャッシュ
|
||||
uv 缓存 uvキャッシュ
|
||||
Yarn 缓存 Yarnキャッシュ
|
||||
pnpm 缓存 pnpmキャッシュ
|
||||
Bun 缓存 Bunキャッシュ
|
||||
npm 下载缓存,由 npm 自行清理 npmのダウンロードキャッシュ。npmでクリーンアップします
|
||||
Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Yarnのグローバルキャッシュとオフラインミラー。Yarnでクリーンアップします
|
||||
pnpm 内容寻址存储,仅清理未被引用的包 pnpmのコンテンツアドレスストア。参照されていないパッケージのみ削除します
|
||||
Bun 下载的包缓存,由 Bun 自行清理 Bunのパッケージダウンロードキャッシュ。Bunでクリーンアップします
|
||||
Go 编译与完整模块缓存,由 go clean 清理 Goのビルドキャッシュと完全なモジュールキャッシュ。go cleanでクリーンアップします
|
||||
Cargo 注册表与 Git 依赖缓存,需要 cargo-cache CargoのレジストリとGit依存関係キャッシュ。cargo-cacheが必要です
|
||||
uv 下载的 Python 包与构建缓存,由 uv 自行清理 uvのPythonパッケージとビルドキャッシュ。uvでクリーンアップします
|
||||
未找到 {},无法安全清理 {}が見つからないため、安全にクリーンアップできません
|
||||
{} 清理失败(退出码 {}) {}のクリーンアップに失敗しました(終了コード{})
|
||||
{} 清理失败(退出码 {}):{} {}のクリーンアップに失敗しました(終了コード{}):{}
|
||||
{}:未找到 {},无法安全清理 {}:{}が見つからないため、安全にクリーンアップできません
|
||||
支持的内容先移到废纸篓;工具缓存由对应命令直接清理。 対応する項目は先にゴミ箱へ移動し、ツールキャッシュは各コマンドで直接削除します。
|
||||
支持的缓存可以恢复;工具缓存始终由对应命令直接清理 対応するキャッシュは復元できますが、ツールキャッシュは常に各コマンドで直接削除します
|
||||
{} {} 英寸 {} {}インチ
|
||||
{} · {} 个 {} · {}個
|
||||
{} · {} 核 {} · {}コア
|
||||
|
||||
|
@@ -95,6 +95,25 @@ Xcode 编译缓存 Xcode 빌드 캐시
|
||||
cleanip.io 对这次评分的把握:各数据源结论越一致越高 cleanip.io의 점수 확신도: 정보 출처의 결과가 일치할수록 높습니다
|
||||
macOS 系统目录(系统组件) macOS 시스템 폴더(시스템 구성 요소)
|
||||
npm 缓存 npm 캐시
|
||||
Go 缓存 Go 캐시
|
||||
Rust 缓存 Rust 캐시
|
||||
uv 缓存 uv 캐시
|
||||
Yarn 缓存 Yarn 캐시
|
||||
pnpm 缓存 pnpm 캐시
|
||||
Bun 缓存 Bun 캐시
|
||||
npm 下载缓存,由 npm 自行清理 npm 다운로드 캐시이며 npm이 직접 정리합니다
|
||||
Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Yarn 전역 및 오프라인 미러 캐시이며 Yarn이 직접 정리합니다
|
||||
pnpm 内容寻址存储,仅清理未被引用的包 pnpm 콘텐츠 주소 지정 저장소이며 참조되지 않은 패키지만 정리합니다
|
||||
Bun 下载的包缓存,由 Bun 自行清理 Bun 패키지 다운로드 캐시이며 Bun이 직접 정리합니다
|
||||
Go 编译与完整模块缓存,由 go clean 清理 Go 빌드 및 전체 모듈 캐시이며 go clean으로 정리합니다
|
||||
Cargo 注册表与 Git 依赖缓存,需要 cargo-cache Cargo 레지스트리 및 Git 종속성 캐시이며 cargo-cache가 필요합니다
|
||||
uv 下载的 Python 包与构建缓存,由 uv 自行清理 uv의 Python 패키지 및 빌드 캐시이며 uv가 직접 정리합니다
|
||||
未找到 {},无法安全清理 {}을(를) 찾지 못해 안전하게 정리할 수 없습니다
|
||||
{} 清理失败(退出码 {}) {} 정리에 실패했습니다(종료 코드 {})
|
||||
{} 清理失败(退出码 {}):{} {} 정리에 실패했습니다(종료 코드 {}): {}
|
||||
{}:未找到 {},无法安全清理 {}: {}을(를) 찾지 못해 안전하게 정리할 수 없습니다
|
||||
支持的内容先移到废纸篓;工具缓存由对应命令直接清理。 지원되는 항목은 먼저 휴지통으로 이동하고 도구 캐시는 해당 명령으로 직접 정리합니다.
|
||||
支持的缓存可以恢复;工具缓存始终由对应命令直接清理 지원되는 캐시는 복원할 수 있으며 도구 캐시는 항상 해당 명령으로 직접 정리합니다
|
||||
{} {} 英寸 {} {}인치
|
||||
{} · {} 个 {} · {}개
|
||||
{} · {} 核 {} · {}코어
|
||||
|
||||
|
@@ -96,6 +96,25 @@ Xcode 编译缓存 Xcode 編譯緩存
|
||||
cleanip.io 对这次评分的把握:各数据源结论越一致越高 cleanip.io 對這次評分的把握:各數據源結論越一致越高
|
||||
macOS 系统目录(系统组件) macOS 系統目錄(系統組件)
|
||||
npm 缓存 npm 緩存
|
||||
Go 缓存 Go 緩存
|
||||
Rust 缓存 Rust 緩存
|
||||
uv 缓存 uv 緩存
|
||||
Yarn 缓存 Yarn 緩存
|
||||
pnpm 缓存 pnpm 緩存
|
||||
Bun 缓存 Bun 緩存
|
||||
npm 下载缓存,由 npm 自行清理 npm 下載緩存,由 npm 自行清理
|
||||
Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Yarn 全局與離線鏡像緩存,由 Yarn 自行清理
|
||||
pnpm 内容寻址存储,仅清理未被引用的包 pnpm 內容尋址存儲,僅清理未被引用的包
|
||||
Bun 下载的包缓存,由 Bun 自行清理 Bun 下載的包緩存,由 Bun 自行清理
|
||||
Go 编译与完整模块缓存,由 go clean 清理 Go 編譯與完整模組緩存,由 go clean 清理
|
||||
Cargo 注册表与 Git 依赖缓存,需要 cargo-cache Cargo 註冊表與 Git 依賴緩存,需要 cargo-cache
|
||||
uv 下载的 Python 包与构建缓存,由 uv 自行清理 uv 下載的 Python 包與構建緩存,由 uv 自行清理
|
||||
未找到 {},无法安全清理 未找到 {},無法安全清理
|
||||
{} 清理失败(退出码 {}) {} 清理失敗(退出碼 {})
|
||||
{} 清理失败(退出码 {}):{} {} 清理失敗(退出碼 {}):{}
|
||||
{}:未找到 {},无法安全清理 {}:未找到 {},無法安全清理
|
||||
支持的内容先移到废纸篓;工具缓存由对应命令直接清理。 支持的內容先移到廢紙簍;工具緩存由對應命令直接清理。
|
||||
支持的缓存可以恢复;工具缓存始终由对应命令直接清理 支持的緩存可以恢復;工具緩存始終由對應命令直接清理
|
||||
{} {} 英寸 {} {} 英吋
|
||||
{} · {} 个 {} · {} 個
|
||||
{} · {} 核 {} · {} 核
|
||||
|
||||
|
@@ -100,6 +100,25 @@ Xcode 编译缓存 Xcode Build Cache
|
||||
cleanip.io 对这次评分的把握:各数据源结论越一致越高 How sure cleanip.io is about this score: the more its sources agree, the higher
|
||||
macOS 系统目录(系统组件) macOS system folder (system component)
|
||||
npm 缓存 npm Cache
|
||||
Go 缓存 Go Cache
|
||||
Rust 缓存 Rust Cache
|
||||
uv 缓存 uv Cache
|
||||
Yarn 缓存 Yarn Cache
|
||||
pnpm 缓存 pnpm Cache
|
||||
Bun 缓存 Bun Cache
|
||||
npm 下载缓存,由 npm 自行清理 npm download cache; cleaned by npm
|
||||
Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Yarn global and offline mirror caches; cleaned by Yarn
|
||||
pnpm 内容寻址存储,仅清理未被引用的包 pnpm content-addressable store; only unreferenced packages are removed
|
||||
Bun 下载的包缓存,由 Bun 自行清理 Bun package download cache; cleaned by Bun
|
||||
Go 编译与完整模块缓存,由 go clean 清理 Go build and complete module caches; cleaned by go clean
|
||||
Cargo 注册表与 Git 依赖缓存,需要 cargo-cache Cargo registry and Git dependency caches; requires cargo-cache
|
||||
uv 下载的 Python 包与构建缓存,由 uv 自行清理 Python package and build caches downloaded by uv; cleaned by uv
|
||||
未找到 {},无法安全清理 {} was not found, so it cannot be cleaned safely
|
||||
{} 清理失败(退出码 {}) {} cleanup failed (exit code {})
|
||||
{} 清理失败(退出码 {}):{} {} cleanup failed (exit code {}): {}
|
||||
{}:未找到 {},无法安全清理 {}: {} was not found, so it cannot be cleaned safely
|
||||
支持的内容先移到废纸篓;工具缓存由对应命令直接清理。 Supported items are moved to Trash first; tool caches are cleaned directly by their commands.
|
||||
支持的缓存可以恢复;工具缓存始终由对应命令直接清理 Supported caches can be restored; tool caches are always cleaned directly by their commands
|
||||
{} {} 英寸 {} {}-inch
|
||||
{} · {} 个 {} · {}
|
||||
{} · {} 核 {} · {} cores
|
||||
|
||||
@@ -134,7 +134,9 @@ private struct SummaryCard: View {
|
||||
private var confirmationText: String {
|
||||
let cleaner = model.cleaner
|
||||
let count = cleaner.selectedScans.reduce(0) { $0 + $1.items.count }
|
||||
let trashNote = model.settings.cleanPrefersTrash ? tr("所有内容先移到废纸篓。") : tr("缓存与日志直接删除,下载内容移到废纸篓。")
|
||||
let trashNote = model.settings.cleanPrefersTrash
|
||||
? tr("支持的内容先移到废纸篓;工具缓存由对应命令直接清理。")
|
||||
: tr("缓存与日志直接删除,下载内容移到废纸篓。")
|
||||
return tr("将清理 \(count) 个项目,共 \(Format.bytes(cleaner.selectedBytes, base: .decimal))。\(trashNote)")
|
||||
}
|
||||
|
||||
@@ -266,10 +268,7 @@ private struct RuleRow: View {
|
||||
HStack(spacing: DS.Space.s2) {
|
||||
DSCheckbox(isOn: selected) { cleaner.toggle(scan) }
|
||||
.disabled(!scan.isCleanable || cleaner.isBusy)
|
||||
Image(systemName: scan.rule.symbol)
|
||||
.font(.system(size: DS.TextSize.sm.rawValue))
|
||||
.foregroundStyle(DS.Palette.textSecondary)
|
||||
.frame(width: DS.Size.iconStandalone)
|
||||
RuleIcon(name: scan.rule.symbol)
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(scan.rule.title)
|
||||
.dsFont(.sm, weight: .medium)
|
||||
@@ -335,6 +334,27 @@ private struct RuleRow: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct RuleIcon: View {
|
||||
let name: String
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let image = LogoCache.shared.image(named: name, template: true) {
|
||||
Image(nsImage: image)
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.scaledToFit()
|
||||
.frame(width: DS.Size.iconStandalone, height: DS.TextSize.sm.rawValue)
|
||||
} else {
|
||||
Image(systemName: name)
|
||||
.font(.system(size: DS.TextSize.sm.rawValue))
|
||||
}
|
||||
}
|
||||
.foregroundStyle(DS.Palette.textSecondary)
|
||||
.frame(width: DS.Size.iconStandalone, height: DS.Size.iconStandalone)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 底部
|
||||
|
||||
private struct CleanerFooter: View {
|
||||
@@ -347,7 +367,8 @@ private struct CleanerFooter: View {
|
||||
DSToggle(isOn: $settings.cleanPrefersTrash, label: tr("缓存也先移到废纸篓"))
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(tr("缓存也先移到废纸篓")).dsFont(.sm, weight: .medium).foregroundStyle(DS.Palette.textPrimary)
|
||||
Text(tr("可以恢复,但清空废纸篓前不会释放空间")).dsFont(.xs).foregroundStyle(DS.Palette.textTertiary)
|
||||
Text(tr("支持的缓存可以恢复;工具缓存始终由对应命令直接清理"))
|
||||
.dsFont(.xs).foregroundStyle(DS.Palette.textTertiary)
|
||||
}
|
||||
Spacer()
|
||||
Button(tr("查看清理日志")) { model.cleaner.revealLog() }
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<!-- Simple Icons Bun, CC0-1.0, source commit b86d5c9a0bdd4f3f5c30898a63654dd32f39fd76. -->
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Bun</title><path d="M12 22.596c6.628 0 12-4.338 12-9.688 0-3.318-2.057-6.248-5.219-7.986-1.286-.715-2.297-1.357-3.139-1.89C14.058 2.025 13.08 1.404 12 1.404c-1.097 0-2.334.785-3.966 1.821a49.92 49.92 0 0 1-2.816 1.697C2.057 6.66 0 9.59 0 12.908c0 5.35 5.372 9.687 12 9.687v.001ZM10.599 4.715c.334-.759.503-1.58.498-2.409 0-.145.202-.187.23-.029.658 2.783-.902 4.162-2.057 4.624-.124.048-.199-.121-.103-.209a5.763 5.763 0 0 0 1.432-1.977Zm2.058-.102a5.82 5.82 0 0 0-.782-2.306v-.016c-.069-.123.086-.263.185-.172 1.962 2.111 1.307 4.067.556 5.051-.082.103-.23-.003-.189-.126a5.85 5.85 0 0 0 .23-2.431Zm1.776-.561a5.727 5.727 0 0 0-1.612-1.806v-.014c-.112-.085-.024-.274.114-.218 2.595 1.087 2.774 3.18 2.459 4.407a.116.116 0 0 1-.049.071.11.11 0 0 1-.153-.026.122.122 0 0 1-.022-.083 5.891 5.891 0 0 0-.737-2.331Zm-5.087.561c-.617.546-1.282.76-2.063 1-.117 0-.195-.078-.156-.181 1.752-.909 2.376-1.649 2.999-2.778 0 0 .155-.118.188.085 0 .304-.349 1.329-.968 1.874Zm4.945 11.237a2.957 2.957 0 0 1-.937 1.553c-.346.346-.8.565-1.286.62a2.178 2.178 0 0 1-1.327-.62 2.955 2.955 0 0 1-.925-1.553.244.244 0 0 1 .064-.198.234.234 0 0 1 .193-.069h3.965a.226.226 0 0 1 .19.07c.05.053.073.125.063.197Zm-5.458-2.176a1.862 1.862 0 0 1-2.384-.245 1.98 1.98 0 0 1-.233-2.447c.207-.319.503-.566.848-.713a1.84 1.84 0 0 1 1.092-.11c.366.075.703.261.967.531a1.98 1.98 0 0 1 .408 2.114 1.931 1.931 0 0 1-.698.869v.001Zm8.495.005a1.86 1.86 0 0 1-2.381-.253 1.964 1.964 0 0 1-.547-1.366c0-.384.11-.76.32-1.079.207-.319.503-.567.849-.713a1.844 1.844 0 0 1 1.093-.108c.367.076.704.262.968.534a1.98 1.98 0 0 1 .4 2.117 1.932 1.932 0 0 1-.702.868Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,2 @@
|
||||
<!-- Simple Icons Go, CC0-1.0, source commit b86d5c9a0bdd4f3f5c30898a63654dd32f39fd76. -->
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Go</title><path d="M1.811 10.231c-.047 0-.058-.023-.035-.059l.246-.315c.023-.035.081-.058.128-.058h4.172c.046 0 .058.035.035.07l-.199.303c-.023.036-.082.07-.117.07zM.047 11.306c-.047 0-.059-.023-.035-.058l.245-.316c.023-.035.082-.058.129-.058h5.328c.047 0 .07.035.058.07l-.093.28c-.012.047-.058.07-.105.07zm2.828 1.075c-.047 0-.059-.035-.035-.07l.163-.292c.023-.035.07-.07.117-.07h2.337c.047 0 .07.035.07.082l-.023.28c0 .047-.047.082-.082.082zm12.129-2.36c-.736.187-1.239.327-1.963.514-.176.046-.187.058-.34-.117-.174-.199-.303-.327-.548-.444-.737-.362-1.45-.257-2.115.175-.795.514-1.204 1.274-1.192 2.22.011.935.654 1.706 1.577 1.835.795.105 1.46-.175 1.987-.77.105-.13.198-.27.315-.434H10.47c-.245 0-.304-.152-.222-.35.152-.362.432-.97.596-1.274a.315.315 0 01.292-.187h4.253c-.023.316-.023.631-.07.947a4.983 4.983 0 01-.958 2.29c-.841 1.11-1.94 1.8-3.33 1.986-1.145.152-2.209-.07-3.143-.77-.865-.655-1.356-1.52-1.484-2.595-.152-1.274.222-2.419.993-3.424.83-1.086 1.928-1.776 3.272-2.02 1.098-.2 2.15-.07 3.096.571.62.41 1.063.97 1.356 1.648.07.105.023.164-.117.2m3.868 6.461c-1.064-.024-2.034-.328-2.852-1.029a3.665 3.665 0 01-1.262-2.255c-.21-1.32.152-2.489.947-3.529.853-1.122 1.881-1.706 3.272-1.95 1.192-.21 2.314-.095 3.33.595.923.63 1.496 1.484 1.648 2.605.198 1.578-.257 2.863-1.344 3.962-.771.783-1.718 1.273-2.805 1.495-.315.06-.63.07-.934.106zm2.78-4.72c-.011-.153-.011-.27-.034-.387-.21-1.157-1.274-1.81-2.384-1.554-1.087.245-1.788.935-2.045 2.033-.21.912.234 1.835 1.075 2.21.643.28 1.285.244 1.905-.07.923-.48 1.425-1.228 1.484-2.233z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,2 @@
|
||||
<!-- Simple Icons npm, CC0-1.0, source commit b86d5c9a0bdd4f3f5c30898a63654dd32f39fd76. -->
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>npm</title><path d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z"/></svg>
|
||||
|
After Width: | Height: | Size: 410 B |
@@ -0,0 +1,2 @@
|
||||
<!-- Simple Icons pnpm, CC0-1.0, source commit b86d5c9a0bdd4f3f5c30898a63654dd32f39fd76. -->
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>pnpm</title><path d="M0 0v7.5h7.5V0zm8.25 0v7.5h7.498V0zm8.25 0v7.5H24V0zM2 2h3.5v3.5H2zm8.25 0h3.498v3.5H10.25zm8.25 0H22v3.5h-3.5zM8.25 8.25v7.5h7.498v-7.5zm8.25 0v7.5H24v-7.5zm2 2H22v3.5h-3.5zM0 16.5V24h7.5v-7.5zm8.25 0V24h7.498v-7.5zm8.25 0V24H24v-7.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 437 B |
@@ -0,0 +1,2 @@
|
||||
<!-- Simple Icons Rust, CC0-1.0, source commit b86d5c9a0bdd4f3f5c30898a63654dd32f39fd76. -->
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Rust</title><path d="M23.8346 11.7033l-1.0073-.6236a13.7268 13.7268 0 00-.0283-.2936l.8656-.8069a.3483.3483 0 00-.1154-.578l-1.1066-.414a8.4958 8.4958 0 00-.087-.2856l.6904-.9587a.3462.3462 0 00-.2257-.5446l-1.1663-.1894a9.3574 9.3574 0 00-.1407-.2622l.49-1.0761a.3437.3437 0 00-.0274-.3361.3486.3486 0 00-.3006-.154l-1.1845.0416a6.7444 6.7444 0 00-.1873-.2268l.2723-1.153a.3472.3472 0 00-.417-.4172l-1.1532.2724a14.0183 14.0183 0 00-.2278-.1873l.0415-1.1845a.3442.3442 0 00-.49-.328l-1.076.491c-.0872-.0476-.1742-.0952-.2623-.1407l-.1903-1.1673A.3483.3483 0 0016.256.955l-.9597.6905a8.4867 8.4867 0 00-.2855-.086l-.414-1.1066a.3483.3483 0 00-.5781-.1154l-.8069.8666a9.2936 9.2936 0 00-.2936-.0284L12.2946.1683a.3462.3462 0 00-.5892 0l-.6236 1.0073a13.7383 13.7383 0 00-.2936.0284L9.9803.3374a.3462.3462 0 00-.578.1154l-.4141 1.1065c-.0962.0274-.1903.0567-.2855.086L7.744.955a.3483.3483 0 00-.5447.2258L7.009 2.348a9.3574 9.3574 0 00-.2622.1407l-1.0762-.491a.3462.3462 0 00-.49.328l.0416 1.1845a7.9826 7.9826 0 00-.2278.1873L3.8413 3.425a.3472.3472 0 00-.4171.4171l.2713 1.1531c-.0628.075-.1255.1509-.1863.2268l-1.1845-.0415a.3462.3462 0 00-.328.49l.491 1.0761a9.167 9.167 0 00-.1407.2622l-1.1662.1894a.3483.3483 0 00-.2258.5446l.6904.9587a13.303 13.303 0 00-.087.2855l-1.1065.414a.3483.3483 0 00-.1155.5781l.8656.807a9.2936 9.2936 0 00-.0283.2935l-1.0073.6236a.3442.3442 0 000 .5892l1.0073.6236c.008.0982.0182.1964.0283.2936l-.8656.8079a.3462.3462 0 00.1155.578l1.1065.4141c.0273.0962.0567.1914.087.2855l-.6904.9587a.3452.3452 0 00.2268.5447l1.1662.1893c.0456.088.0922.1751.1408.2622l-.491 1.0762a.3462.3462 0 00.328.49l1.1834-.0415c.0618.0769.1235.1528.1873.2277l-.2713 1.1541a.3462.3462 0 00.4171.4161l1.153-.2713c.075.0638.151.1255.2279.1863l-.0415 1.1845a.3442.3442 0 00.49.327l1.0761-.49c.087.0486.1741.0951.2622.1407l.1903 1.1662a.3483.3483 0 00.5447.2268l.9587-.6904a9.299 9.299 0 00.2855.087l.414 1.1066a.3452.3452 0 00.5781.1154l.8079-.8656c.0972.0111.1954.0203.2936.0294l.6236 1.0073a.3472.3472 0 00.5892 0l.6236-1.0073c.0982-.0091.1964-.0183.2936-.0294l.8069.8656a.3483.3483 0 00.578-.1154l.4141-1.1066a8.4626 8.4626 0 00.2855-.087l.9587.6904a.3452.3452 0 00.5447-.2268l.1903-1.1662c.088-.0456.1751-.0931.2622-.1407l1.0762.49a.3472.3472 0 00.49-.327l-.0415-1.1845a6.7267 6.7267 0 00.2267-.1863l1.1531.2713a.3472.3472 0 00.4171-.416l-.2713-1.1542c.0628-.0749.1255-.1508.1863-.2278l1.1845.0415a.3442.3442 0 00.328-.49l-.49-1.076c.0475-.0872.0951-.1742.1407-.2623l1.1662-.1893a.3483.3483 0 00.2258-.5447l-.6904-.9587.087-.2855 1.1066-.414a.3462.3462 0 00.1154-.5781l-.8656-.8079c.0101-.0972.0202-.1954.0283-.2936l1.0073-.6236a.3442.3442 0 000-.5892zm-6.7413 8.3551a.7138.7138 0 01.2986-1.396.714.714 0 11-.2997 1.396zm-.3422-2.3142a.649.649 0 00-.7715.5l-.3573 1.6685c-1.1035.501-2.3285.7795-3.6193.7795a8.7368 8.7368 0 01-3.6951-.814l-.3574-1.6684a.648.648 0 00-.7714-.499l-1.473.3158a8.7216 8.7216 0 01-.7613-.898h7.1676c.081 0 .1356-.0141.1356-.088v-2.536c0-.074-.0536-.0881-.1356-.0881h-2.0966v-1.6077h2.2677c.2065 0 1.1065.0587 1.394 1.2088.0901.3533.2875 1.5044.4232 1.8729.1346.413.6833 1.2381 1.2685 1.2381h3.5716a.7492.7492 0 00.1296-.0131 8.7874 8.7874 0 01-.8119.9526zM6.8369 20.024a.714.714 0 11-.2997-1.396.714.714 0 01.2997 1.396zM4.1177 8.9972a.7137.7137 0 11-1.304.5791.7137.7137 0 011.304-.579zm-.8352 1.9813l1.5347-.6824a.65.65 0 00.33-.8585l-.3158-.7147h1.2432v5.6025H3.5669a8.7753 8.7753 0 01-.2834-3.348zm6.7343-.5437V8.7836h2.9601c.153 0 1.0792.1772 1.0792.8697 0 .575-.7107.7815-1.2948.7815zm10.7574 1.4862c0 .2187-.008.4363-.0243.651h-.9c-.09 0-.1265.0586-.1265.1477v.413c0 .973-.5487 1.1846-1.0296 1.2382-.4576.0517-.9648-.1913-1.0275-.4717-.2704-1.5186-.7198-1.8436-1.4305-2.4034.8817-.5599 1.799-1.386 1.799-2.4915 0-1.1936-.819-1.9458-1.3769-2.3153-.7825-.5163-1.6491-.6195-1.883-.6195H5.4682a8.7651 8.7651 0 014.907-2.7699l1.0974 1.151a.648.648 0 00.9182.0213l1.227-1.1743a8.7753 8.7753 0 016.0044 4.2762l-.8403 1.8982a.652.652 0 00.33.8585l1.6178.7188c.0283.2875.0425.577.0425.8717zm-9.3006-9.5993a.7128.7128 0 11.984 1.0316.7137.7137 0 01-.984-1.0316zm8.3389 6.71a.7107.7107 0 01.9395-.3625.7137.7137 0 11-.9405.3635z"/></svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,2 @@
|
||||
<!-- Simple Icons uv, CC0-1.0, source commit b86d5c9a0bdd4f3f5c30898a63654dd32f39fd76. -->
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>uv</title><path d="m0 .1058.0504 11.9496.0403 9.5597c.0055 1.3199 1.08 2.3854 2.4 2.3798l9.5596-.0403 5.9749-.0252.6075-.0026c1.316-.0056 2.3799-1.0963 2.3799-2.4123h1.0946v2.3894L24 23.9042 23.8992.005 12.9056.0513l.0463 9.5245v5.9637h-1.9583L11.04 9.584 10.9936.0594Z"/></svg>
|
||||
|
After Width: | Height: | Size: 448 B |
@@ -0,0 +1,2 @@
|
||||
<!-- Simple Icons Yarn, CC0-1.0, source commit b86d5c9a0bdd4f3f5c30898a63654dd32f39fd76. -->
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Yarn</title><path d="M12 0C5.375 0 0 5.375 0 12s5.375 12 12 12 12-5.375 12-12S18.625 0 12 0zm.768 4.105c.183 0 .363.053.525.157.125.083.287.185.755 1.154.31-.088.468-.042.551-.019.204.056.366.19.463.375.477.917.542 2.553.334 3.605-.241 1.232-.755 2.029-1.131 2.576.324.329.778.899 1.117 1.825.278.774.31 1.478.273 2.015a5.51 5.51 0 0 0 .602-.329c.593-.366 1.487-.917 2.553-.931.714-.009 1.269.445 1.353 1.103a1.23 1.23 0 0 1-.945 1.362c-.649.158-.95.278-1.821.843-1.232.797-2.539 1.242-3.012 1.39a1.686 1.686 0 0 1-.704.343c-.737.181-3.266.315-3.466.315h-.046c-.783 0-1.214-.241-1.45-.491-.658.329-1.51.19-2.122-.134a1.078 1.078 0 0 1-.58-1.153 1.243 1.243 0 0 1-.153-.195c-.162-.25-.528-.936-.454-1.946.056-.723.556-1.367.88-1.71a5.522 5.522 0 0 1 .408-2.256c.306-.727.885-1.348 1.32-1.737-.32-.537-.644-1.367-.329-2.21.227-.602.412-.936.82-1.08h-.005c.199-.074.389-.153.486-.259a3.418 3.418 0 0 1 2.298-1.103c.037-.093.079-.185.125-.283.31-.658.639-1.029 1.024-1.168a.94.94 0 0 1 .328-.06zm.006.7c-.507.016-1.001 1.519-1.001 1.519s-1.27-.204-2.266.871c-.199.218-.468.334-.746.44-.079.028-.176.023-.417.672-.371.991.625 2.094.625 2.094s-1.186.839-1.626 1.881c-.486 1.144-.338 2.261-.338 2.261s-.843.732-.899 1.487c-.051.663.139 1.2.343 1.515.227.343.51.176.51.176s-.561.653-.037.931c.477.25 1.283.394 1.71-.037.31-.31.371-1.001.486-1.283.028-.065.12.111.209.199.097.093.264.195.264.195s-.755.324-.445 1.066c.102.246.468.403 1.066.398.222-.005 2.664-.139 3.313-.296.375-.088.505-.283.505-.283s1.566-.431 2.998-1.357c.917-.598 1.293-.76 2.034-.936.612-.148.57-1.098-.241-1.084-.839.009-1.575.44-2.196.825-1.163.718-1.742.672-1.742.672l-.018-.032c-.079-.13.371-1.293-.134-2.678-.547-1.515-1.413-1.881-1.344-1.997.297-.5 1.038-1.297 1.334-2.78.176-.899.13-2.377-.269-3.151-.074-.144-.732.241-.732.241s-.616-1.371-.788-1.483a.271.271 0 0 0-.157-.046z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -499,6 +499,16 @@ public final class AppSettings {
|
||||
public var cleanPrefersTrash: Bool {
|
||||
didSet { defaults.set(cleanPrefersTrash, forKey: Keys.cleanPrefersTrash) }
|
||||
}
|
||||
/// 清理页的规则选择;nil 表示用户从未调整过,使用规则默认值。空集合表示用户明确全部取消。
|
||||
public var cleanSelectedRuleIDs: Set<String>? {
|
||||
didSet {
|
||||
if let cleanSelectedRuleIDs {
|
||||
defaults.set(cleanSelectedRuleIDs.sorted(), forKey: Keys.cleanSelectedRuleIDs)
|
||||
} else {
|
||||
defaults.removeObject(forKey: Keys.cleanSelectedRuleIDs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static let refreshOptions = [1, 2, 3, 5]
|
||||
public static let batteryFloorOptions = [10, 20, 30, 40]
|
||||
@@ -532,6 +542,7 @@ public final class AppSettings {
|
||||
showDockIcon = defaults.bool(forKey: Keys.showDockIcon)
|
||||
speedTestBudget = defaults.string(forKey: Keys.speedTestBudget).flatMap(SpeedTestBudget.init(rawValue:)) ?? .full
|
||||
cleanPrefersTrash = defaults.bool(forKey: Keys.cleanPrefersTrash)
|
||||
cleanSelectedRuleIDs = defaults.stringArray(forKey: Keys.cleanSelectedRuleIDs).map(Set.init)
|
||||
menuBarLayout = defaults.string(forKey: Keys.menuBarLayout).flatMap(MenuBarLayout.init(rawValue:)) ?? .separate
|
||||
expandedPopoverSections = Set((defaults.stringArray(forKey: Keys.expandedPopoverSections) ?? []).compactMap(PopoverSection.init(rawValue:)))
|
||||
hiddenPopoverSections = defaults.stringArray(forKey: Keys.hiddenPopoverSections)
|
||||
@@ -604,6 +615,7 @@ public final class AppSettings {
|
||||
static let showDockIcon = "showDockIcon"
|
||||
static let speedTestBudget = "speedTestBudget"
|
||||
static let cleanPrefersTrash = "cleanPrefersTrash"
|
||||
static let cleanSelectedRuleIDs = "cleanSelectedRuleIDs"
|
||||
static let menuBarLayout = "menuBarLayout"
|
||||
static let hiddenPopoverSections = "hiddenPopoverSections"
|
||||
static let expandedPopoverSections = "expandedPopoverSections"
|
||||
|
||||
@@ -14,7 +14,9 @@ public final class CleanerController {
|
||||
public private(set) var scans: [RuleScan] = []
|
||||
public private(set) var lastScan: Date?
|
||||
public private(set) var report: CleanReport?
|
||||
public var selection: Set<String>
|
||||
public var selection: Set<String> {
|
||||
didSet { settings.cleanSelectedRuleIDs = selection }
|
||||
}
|
||||
public var isConfirming = false
|
||||
|
||||
@ObservationIgnored private let settings: AppSettings
|
||||
@@ -23,7 +25,9 @@ public final class CleanerController {
|
||||
|
||||
init(settings: AppSettings) {
|
||||
self.settings = settings
|
||||
selection = Set(rules.filter(\.selectedByDefault).map(\.id))
|
||||
let validIDs = Set(rules.map(\.id))
|
||||
let initial = settings.cleanSelectedRuleIDs ?? Set(rules.filter(\.selectedByDefault).map(\.id))
|
||||
selection = initial.intersection(validIDs)
|
||||
}
|
||||
|
||||
var isBusy: Bool { phase == .scanning || phase == .cleaning }
|
||||
|
||||
@@ -26,8 +26,17 @@ private struct FakeHome {
|
||||
return url
|
||||
}
|
||||
|
||||
func environment(running: Set<String> = []) -> CleanEnvironment {
|
||||
CleanEnvironment(home: path, runningBundleIdentifiers: { running })
|
||||
@discardableResult
|
||||
func executable(_ name: String, script: String) throws -> URL {
|
||||
let url = root.appendingPathComponent(".local/bin/\(name)")
|
||||
try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||
try Data(script.utf8).write(to: url)
|
||||
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: url.path)
|
||||
return url
|
||||
}
|
||||
|
||||
func environment(running: Set<String> = [], now: Date = Date()) -> CleanEnvironment {
|
||||
CleanEnvironment(home: path, runningBundleIdentifiers: { running }, now: { now })
|
||||
}
|
||||
|
||||
func remove() { try? FileManager.default.removeItem(at: root) }
|
||||
@@ -41,13 +50,25 @@ struct SafetyGuardTests {
|
||||
try guardian.validate(URL(fileURLWithPath: "/Users/tester/Library/Caches/com.example.app"))
|
||||
try guardian.validate(URL(fileURLWithPath: "/Users/tester/Library/Logs/DiagnosticReports"))
|
||||
try guardian.validate(URL(fileURLWithPath: "/Users/tester/Library/Application Support/Google/Chrome/Default/Code Cache"))
|
||||
try guardian.validate(URL(fileURLWithPath: "/Users/tester/go/pkg/mod/cache/download/example.zip"))
|
||||
try guardian.validate(URL(fileURLWithPath: "/Users/tester/go/pkg/mod/github.com/example/module"))
|
||||
try guardian.validate(URL(fileURLWithPath: "/Users/tester/.cargo/registry/cache/index.crates.io"))
|
||||
try guardian.validate(URL(fileURLWithPath: "/Users/tester/.cargo/git/db/example"))
|
||||
try guardian.validate(URL(fileURLWithPath: "/Users/tester/.cache/uv/archive-v0"))
|
||||
}
|
||||
|
||||
@Test func rejectsRootsAndOutsidePaths() {
|
||||
#expect(throws: SafetyError.isRoot) { try guardian.validate(URL(fileURLWithPath: "/Users/tester/Library/Caches")) }
|
||||
#expect(throws: SafetyError.isRoot) { try guardian.validate(URL(fileURLWithPath: "/Users/tester/go/pkg/mod")) }
|
||||
#expect(throws: SafetyError.isRoot) { try guardian.validate(URL(fileURLWithPath: "/Users/tester/.cargo/registry")) }
|
||||
#expect(throws: SafetyError.isRoot) { try guardian.validate(URL(fileURLWithPath: "/Users/tester/.cargo/git")) }
|
||||
#expect(throws: SafetyError.isRoot) { try guardian.validate(URL(fileURLWithPath: "/Users/tester/.cache/uv")) }
|
||||
#expect(throws: SafetyError.outsideAllowedRoots) { try guardian.validate(URL(fileURLWithPath: "/Users/tester/Documents/a")) }
|
||||
#expect(throws: SafetyError.outsideAllowedRoots) { try guardian.validate(URL(fileURLWithPath: "/System/Library/Caches/x")) }
|
||||
#expect(throws: SafetyError.outsideAllowedRoots) { try guardian.validate(URL(fileURLWithPath: "/Users/other/Library/Caches/x")) }
|
||||
#expect(throws: SafetyError.outsideAllowedRoots) { try guardian.validate(URL(fileURLWithPath: "/Users/tester/go/src/project")) }
|
||||
#expect(throws: SafetyError.outsideAllowedRoots) { try guardian.validate(URL(fileURLWithPath: "/Users/tester/.cargo/bin/cargo")) }
|
||||
#expect(throws: SafetyError.outsideAllowedRoots) { try guardian.validate(URL(fileURLWithPath: "/Users/tester/.cache/pip/http-v2")) }
|
||||
}
|
||||
|
||||
@Test func rejectsTraversalAndControlCharacters() {
|
||||
@@ -95,6 +116,194 @@ struct SafetyGuardTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("开发工具缓存规则")
|
||||
struct DeveloperCacheRuleTests {
|
||||
@Test func catalogIncludesPackageManagerCaches() {
|
||||
let rules = Dictionary(uniqueKeysWithValues: RuleCatalog.rules().map { ($0.id, $0) })
|
||||
|
||||
for id in ["developer.npm", "developer.yarn", "developer.pnpm", "developer.bun",
|
||||
"developer.go", "developer.rust", "developer.uv"] {
|
||||
#expect(rules[id]?.category == .developer)
|
||||
#expect(rules[id]?.selectedByDefault == false)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func packageManagerCachesUseTheirBrandLogos() {
|
||||
let rules = Dictionary(uniqueKeysWithValues: RuleCatalog.rules().map { ($0.id, $0) })
|
||||
let expected = [
|
||||
"developer.npm": "tool-npm",
|
||||
"developer.yarn": "tool-yarn",
|
||||
"developer.pnpm": "tool-pnpm",
|
||||
"developer.bun": "tool-bun",
|
||||
"developer.go": "tool-go",
|
||||
"developer.rust": "tool-rust",
|
||||
"developer.uv": "tool-uv",
|
||||
]
|
||||
|
||||
for (id, logo) in expected {
|
||||
#expect(rules[id]?.symbol == logo)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func rulesOnlyLocateRegenerableCacheDirectories() throws {
|
||||
let home = try FakeHome()
|
||||
defer { home.remove() }
|
||||
try home.file("Library/Caches/go-build/compile-cache/action")
|
||||
try home.file("go/pkg/mod/cache/download/example.zip")
|
||||
try home.file("go/pkg/mod/github.com/example/module@v1.0.0/source.go")
|
||||
try home.file("go/src/project/main.go")
|
||||
try home.file(".cargo/registry/cache/index.crates.io/package.crate")
|
||||
try home.file(".cargo/registry/src/index.crates.io/package/src/lib.rs")
|
||||
try home.file(".cargo/git/db/example/objects/pack/data")
|
||||
try home.file(".cargo/git/checkouts/example/revision/src/lib.rs")
|
||||
try home.file(".cargo/bin/cargo")
|
||||
try home.file(".cache/uv/archive-v0/package/module.py")
|
||||
try home.file(".cache/pip/http-v2/response")
|
||||
try home.file("Library/Caches/Yarn/v6/npm-example/package.tgz")
|
||||
try home.file(".yarn/berry/cache/example.zip")
|
||||
try home.file("Library/pnpm/store/v11/files/example")
|
||||
try home.file(".pnpm-store/v3/files/example")
|
||||
try home.file(".bun/install/cache/example/package.tgz")
|
||||
|
||||
let rules = Dictionary(uniqueKeysWithValues: RuleCatalog.rules().map { ($0.id, $0) })
|
||||
let goRule = try #require(rules["developer.go"])
|
||||
let rustRule = try #require(rules["developer.rust"])
|
||||
let uvRule = try #require(rules["developer.uv"])
|
||||
let yarnRule = try #require(rules["developer.yarn"])
|
||||
let pnpmRule = try #require(rules["developer.pnpm"])
|
||||
let bunRule = try #require(rules["developer.bun"])
|
||||
|
||||
#expect(try relativePaths(goRule.locate(home.environment()), home: home) == [
|
||||
"Library/Caches/go-build", "go/pkg/mod/cache", "go/pkg/mod/github.com",
|
||||
])
|
||||
#expect(try relativePaths(rustRule.locate(home.environment()), home: home) == [
|
||||
".cargo/git/checkouts", ".cargo/git/db", ".cargo/registry/cache", ".cargo/registry/src",
|
||||
])
|
||||
#expect(try relativePaths(uvRule.locate(home.environment()), home: home) == [
|
||||
".cache/uv/archive-v0",
|
||||
])
|
||||
#expect(try relativePaths(yarnRule.locate(home.environment()), home: home) == [
|
||||
".yarn/berry/cache/example.zip", "Library/Caches/Yarn/v6",
|
||||
])
|
||||
#expect(try relativePaths(pnpmRule.locate(home.environment()), home: home) == [
|
||||
".pnpm-store/v3", "Library/pnpm/store/v11",
|
||||
])
|
||||
#expect(try relativePaths(bunRule.locate(home.environment()), home: home) == [
|
||||
".bun/install/cache/example",
|
||||
])
|
||||
}
|
||||
|
||||
@Test func packageManagerCachesUseToolsInsteadOfDirectDeletion() async throws {
|
||||
let home = try FakeHome()
|
||||
defer { home.remove() }
|
||||
let marker = home.root.appendingPathComponent("tool-invocations.txt")
|
||||
let script = """
|
||||
#!/bin/sh
|
||||
if [ "$1" = "--version" ]; then
|
||||
echo "1.22.22"
|
||||
exit 0
|
||||
fi
|
||||
printf '%s|%s\\n' "$(basename "$0")" "$*" >> "\(marker.path)"
|
||||
"""
|
||||
for tool in ["npm", "yarn", "pnpm", "bun", "go", "cargo-cache", "uv"] {
|
||||
try home.executable(tool, script: script)
|
||||
}
|
||||
|
||||
let cacheFiles = [
|
||||
try home.file(".npm/_cacache/content/data"),
|
||||
try home.file("Library/Caches/Yarn/v6/npm-example/package.tgz"),
|
||||
try home.file("Library/pnpm/store/v11/files/data"),
|
||||
try home.file(".bun/install/cache/example/package.tgz"),
|
||||
try home.file("Library/Caches/go-build/action/data"),
|
||||
try home.file("go/pkg/mod/cache/download/data"),
|
||||
try home.file(".cargo/registry/cache/index/package.crate"),
|
||||
try home.file(".cache/uv/archive-v0/package/data"),
|
||||
]
|
||||
let ids = Set(["developer.npm", "developer.yarn", "developer.pnpm", "developer.bun",
|
||||
"developer.go", "developer.rust", "developer.uv"])
|
||||
let rules = RuleCatalog.rules().filter { ids.contains($0.id) }
|
||||
let environment = home.environment(now: Date().addingTimeInterval(86_400))
|
||||
let scans = await CleanEngine.scan(rules, environment: environment)
|
||||
|
||||
let report = await CleanEngine.clean(scans, selected: ids, preferTrash: false,
|
||||
environment: environment, log: nil)
|
||||
|
||||
#expect(report.failures.isEmpty)
|
||||
#expect(cacheFiles.allSatisfy { FileManager.default.fileExists(atPath: $0.path) })
|
||||
let invocations = try String(contentsOf: marker, encoding: .utf8)
|
||||
.split(separator: "\n").map(String.init).sorted()
|
||||
#expect(invocations == [
|
||||
"bun|pm cache rm",
|
||||
"cargo-cache|-r all",
|
||||
"go|clean -cache -modcache",
|
||||
"npm|cache clean --force",
|
||||
"pnpm|store prune",
|
||||
"uv|cache clean",
|
||||
"yarn|cache clean",
|
||||
])
|
||||
}
|
||||
|
||||
@Test func missingToolBlocksCacheWithoutDeletingIt() async throws {
|
||||
let home = try FakeHome()
|
||||
defer { home.remove() }
|
||||
let cache = try home.file(".npm/_cacache/content/data")
|
||||
let environment = CleanEnvironment(
|
||||
home: home.path,
|
||||
runningBundleIdentifiers: { [] },
|
||||
isToolAvailable: { _ in false },
|
||||
runTool: { _, _ in throw TestError.unexpectedToolRun }
|
||||
)
|
||||
|
||||
let scans = await CleanEngine.scan([RuleCatalog.npmCache], environment: environment)
|
||||
|
||||
#expect(scans.first?.blocked == .toolUnavailable("npm"))
|
||||
#expect(FileManager.default.fileExists(atPath: cache.path))
|
||||
}
|
||||
|
||||
@Test func failedToolCommandReportsFailureWithoutDeletingCache() async throws {
|
||||
let home = try FakeHome()
|
||||
defer { home.remove() }
|
||||
try home.executable("npm", script: "#!/bin/sh\necho 'simulated failure' >&2\nexit 7\n")
|
||||
let cache = try home.file(".npm/_cacache/content/data")
|
||||
let environment = home.environment(now: Date().addingTimeInterval(86_400))
|
||||
let scans = await CleanEngine.scan([RuleCatalog.npmCache], environment: environment)
|
||||
|
||||
let report = await CleanEngine.clean(scans, selected: [RuleCatalog.npmCache.id], preferTrash: false,
|
||||
environment: environment, log: nil)
|
||||
|
||||
#expect(report.failures.count == 1)
|
||||
#expect(report.failures.first?.contains("simulated failure") == true)
|
||||
#expect(report.failures.first?.contains("7") == true)
|
||||
#expect(FileManager.default.fileExists(atPath: cache.path))
|
||||
}
|
||||
|
||||
@Test func toolManagedCacheDoesNotTreatPackageNamesAsProtectedAppData() async throws {
|
||||
let home = try FakeHome()
|
||||
defer { home.remove() }
|
||||
try home.executable("bun", script: "#!/bin/sh\nexit 0\n")
|
||||
try home.file(".bun/install/cache/xstats-package/data")
|
||||
|
||||
let scans = await CleanEngine.scan([RuleCatalog.bunCache], environment: home.environment())
|
||||
|
||||
#expect(scans.first?.skippedCount == 0)
|
||||
#expect(scans.first?.items.map(\.url.lastPathComponent) == ["xstats-package"])
|
||||
}
|
||||
|
||||
private func relativePaths(_ urls: [URL], home: FakeHome) throws -> [String] {
|
||||
let prefix = home.path + "/"
|
||||
return try urls.map { url in
|
||||
let path = url.standardizedFileURL.path
|
||||
guard path.hasPrefix(prefix) else { throw TestError.outsideHome }
|
||||
return String(path.dropFirst(prefix.count))
|
||||
}.sorted()
|
||||
}
|
||||
|
||||
private enum TestError: Error {
|
||||
case outsideHome
|
||||
case unexpectedToolRun
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("扫描与清理", .serialized)
|
||||
struct CleanEngineTests {
|
||||
@Test func scanSkipsRunningAppsAndAppleCaches() async throws {
|
||||
|
||||
@@ -11,6 +11,18 @@ private func isolatedDefaults() -> UserDefaults {
|
||||
return defaults
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Suite struct ToolBrandLogoTests {
|
||||
@Test(arguments: ["tool-npm", "tool-yarn", "tool-pnpm", "tool-bun",
|
||||
"tool-go", "tool-rust", "tool-uv"])
|
||||
func packagedSVGCanBeRenderedAsTemplate(_ name: String) throws {
|
||||
let image = try #require(LogoCache.shared.image(named: name, template: true))
|
||||
#expect(image.isTemplate)
|
||||
#expect(image.size.width > 0)
|
||||
#expect(image.size.height > 0)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Suite struct BluetoothDeviceCacheTests {
|
||||
private func device(_ name: String, percent: Int) -> BluetoothDevice {
|
||||
@@ -170,6 +182,41 @@ private func isolatedDefaults() -> UserDefaults {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Suite struct CleanerSelectionTests {
|
||||
private let toolRuleIDs: Set<String> = [
|
||||
"developer.npm", "developer.yarn", "developer.pnpm", "developer.bun",
|
||||
"developer.go", "developer.rust", "developer.uv",
|
||||
]
|
||||
|
||||
@Test func toolCachesStartUnselected() {
|
||||
let controller = CleanerController(settings: AppSettings(defaults: isolatedDefaults()))
|
||||
|
||||
#expect(controller.selection.isDisjoint(with: toolRuleIDs))
|
||||
#expect(controller.selection.contains("system.caches"))
|
||||
}
|
||||
|
||||
@Test func userSelectionSurvivesControllerAndSettingsRecreation() {
|
||||
let defaults = isolatedDefaults()
|
||||
let controller = CleanerController(settings: AppSettings(defaults: defaults))
|
||||
controller.selection.insert("developer.pnpm")
|
||||
|
||||
let reloaded = CleanerController(settings: AppSettings(defaults: defaults))
|
||||
|
||||
#expect(reloaded.selection.contains("developer.pnpm"))
|
||||
}
|
||||
|
||||
@Test func explicitlyClearingSelectionDoesNotRestoreDefaults() {
|
||||
let defaults = isolatedDefaults()
|
||||
let controller = CleanerController(settings: AppSettings(defaults: defaults))
|
||||
controller.selection.removeAll()
|
||||
|
||||
let reloaded = CleanerController(settings: AppSettings(defaults: defaults))
|
||||
|
||||
#expect(reloaded.selection.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
@Suite struct PanelTabTests {
|
||||
@Test func sidebarGroupsCoverEveryTabOnce() {
|
||||
let grouped = PanelTab.monitors + PanelTab.tools + PanelTab.settings
|
||||
|
||||
@@ -158,11 +158,13 @@ unclean exit restores them at the next boot.
|
||||
## Cleanup
|
||||
|
||||
- **What it scans.** App caches, logs and crash reports, browser caches (Chrome, Edge,
|
||||
Brave, Arc, Firefox, Safari), Xcode DerivedData, simulator caches, the npm cache, Xcode
|
||||
archives, unfinished downloads, installers and the Trash.
|
||||
Brave, Arc, Firefox, Safari), Xcode DerivedData, simulator caches, npm, Yarn, pnpm, Bun,
|
||||
Go, Rust and uv caches, Xcode archives, unfinished downloads, installers and the Trash.
|
||||
- **Review first.** Sizes by category, every rule expandable to its items, a second click to
|
||||
confirm. Caches and logs are deleted outright so the space comes back at once; downloads go
|
||||
to the Trash. One switch sends everything to the Trash instead.
|
||||
to the Trash. Developer-tool caches are cleaned by their own commands rather than by
|
||||
deleting internal folders; the Trash preference applies only to folder-based caches.
|
||||
Developer-tool caches start unselected and remember their local selection afterwards.
|
||||
- **Safety.** Only allow-listed folders are ever touched. Keychains, password managers,
|
||||
VPNs, cookies and history are off limits. Caches of running apps are skipped, browsers must
|
||||
be quit first, and every item is checked again right before it goes. Each action is logged
|
||||
|
||||
@@ -138,8 +138,8 @@ XStats の終了時やクラッシュ時には自動制御へ戻り、蓋を閉
|
||||
|
||||
## クリーンアップ
|
||||
|
||||
- **対象**:アプリやブラウザーのキャッシュ、ログ、クラッシュレポート、Xcode・シミュレーター・npm のキャッシュ、Xcode アーカイブ、未完了ダウンロード、インストーラー、ゴミ箱。
|
||||
- **削除前に確認**:種類別の容量とファイルを表示し、実行前に再確認します。通常、キャッシュとログは直接削除し、ダウンロードはゴミ箱へ移動します。すべてゴミ箱へ移動する設定もあります。
|
||||
- **対象**:アプリやブラウザーのキャッシュ、ログ、クラッシュレポート、Xcode・シミュレーター・npm・Yarn・pnpm・Bun・Go・Rust・uv のキャッシュ、Xcode アーカイブ、未完了ダウンロード、インストーラー、ゴミ箱。
|
||||
- **削除前に確認**:種類別の容量とファイルを表示し、実行前に再確認します。開発ツールのキャッシュは内部フォルダーを直接削除せず、各ツールのコマンドでクリーンアップします。初回は未選択で、以後はこの Mac での選択状態を記憶します。ゴミ箱設定はフォルダー型キャッシュにのみ適用されます。
|
||||
- **保護**:許可されたディレクトリのみを処理します。キーチェーン、パスワード管理、VPN、Cookie、履歴などを保護し、実行中アプリのキャッシュはスキップします。各項目を削除直前に再検証します。
|
||||
- **記録**:操作ログは ~/Library/Logs/XStats/cleanup.log に保存します。
|
||||
- **メンテナンス**:DNS キャッシュ削除とメモリ解放。ヘルパーがなければ管理者認証を求めます。
|
||||
|
||||
@@ -138,8 +138,8 @@ XStats를 종료하거나 앱이 비정상 종료되면 팬이 자동 모드로
|
||||
|
||||
## 정리
|
||||
|
||||
- **대상**: 앱·브라우저 캐시, 로그, 충돌 보고서, Xcode·시뮬레이터·npm 캐시, Xcode 아카이브, 미완료 다운로드, 설치 파일, 휴지통.
|
||||
- **삭제 전 확인**: 유형별 용량과 개별 파일을 표시하고 실행 전에 다시 확인합니다. 기본적으로 캐시와 로그는 바로 삭제하고 다운로드는 휴지통으로 옮깁니다. 모두 휴지통으로 이동하도록 설정할 수도 있습니다.
|
||||
- **대상**: 앱·브라우저 캐시, 로그, 충돌 보고서, Xcode·시뮬레이터·npm·Yarn·pnpm·Bun·Go·Rust·uv 캐시, Xcode 아카이브, 미완료 다운로드, 설치 파일, 휴지통.
|
||||
- **삭제 전 확인**: 유형별 용량과 개별 파일을 표시하고 실행 전에 다시 확인합니다. 개발 도구 캐시는 내부 디렉터리를 직접 삭제하지 않고 각 도구의 명령으로 정리합니다. 처음에는 선택되지 않으며 이후 이 Mac의 선택 상태를 기억합니다. 휴지통 설정은 디렉터리 기반 캐시에만 적용됩니다.
|
||||
- **보호**: 허용된 디렉터리만 처리합니다. 키체인, 비밀번호 관리자, VPN, 쿠키, 기록 등을 보호하고 실행 중인 앱의 캐시는 건너뜁니다. 삭제 직전에 각 항목을 다시 검사합니다.
|
||||
- **기록**: 작업 로그는 ~/Library/Logs/XStats/cleanup.log에 저장합니다.
|
||||
- **유지 관리**: DNS 캐시 비우기와 메모리 확보를 지원합니다. 보조 프로그램이 없으면 관리자 인증을 요청합니다.
|
||||
|
||||
@@ -156,9 +156,10 @@ XStats 暂无官网。自动更新与现有 GeoIP 服务保持不变;账号登
|
||||
## 清理
|
||||
|
||||
- **扫描范围**:应用缓存、日志与崩溃报告、浏览器缓存(Chrome、Edge、Brave、Arc、Firefox、Safari)、
|
||||
Xcode 编译缓存、模拟器缓存、npm 缓存、Xcode 归档、未完成的下载、安装包和废纸篓。
|
||||
Xcode 编译缓存、模拟器缓存、npm / Yarn / pnpm / Bun / Go / Rust / uv 缓存、Xcode 归档、未完成的下载、安装包和废纸篓。
|
||||
- **先预览**:按类别显示大小,每条规则可展开查看具体项目,清理前再确认一次。缓存与日志直接删除,
|
||||
空间立即释放;下载目录的内容移到废纸篓。也可以一键改为全部先移到废纸篓。
|
||||
空间立即释放;下载目录的内容移到废纸篓。开发工具缓存由对应工具命令清理,不直接删除内部目录;
|
||||
“缓存也先移到废纸篓”仅作用于目录型缓存。开发工具缓存首次不勾选,之后会记住本机的勾选状态。
|
||||
- **安全**:只处理白名单目录。钥匙串、密码管理器、VPN、Cookie、历史记录一律不碰。正在运行的应用的缓存
|
||||
会跳过,浏览器需要先退出,删除前每一项都会再校验一次。所有操作记录在 `~/Library/Logs/XStats/cleanup.log`。
|
||||
- **系统维护**:刷新 DNS 缓存、释放内存。已安装辅助工具时直接执行,否则请求一次管理员授权。
|
||||
|
||||
@@ -32,6 +32,12 @@ XStats 基于 [gentpan/OpenStats](https://github.com/gentpan/OpenStats) 开发
|
||||
`google.svg` 是 Google 的品牌标志,仅按其品牌规范用于“使用 Google 登录”按钮。Apple 标志使用系统 SF Symbols 的 `apple.logo`。
|
||||
GitHub、Google 与 Apple 的名称和标志均为各自公司的商标。
|
||||
|
||||
## 开发工具品牌图标
|
||||
|
||||
`Packages/XStatsKit/Sources/XStatsUI/Resources/Logos/tool-{npm,yarn,pnpm,bun,go,rust,uv}.svg` 来自
|
||||
[Simple Icons](https://github.com/simple-icons/simple-icons/tree/b86d5c9a0bdd4f3f5c30898a63654dd32f39fd76),
|
||||
使用 CC0-1.0。npm、Yarn、pnpm、Bun、Go、Rust、uv 的名称与标志均为各自权利人的商标,XStats 仅用这些图标标识对应工具的缓存。
|
||||
|
||||
## exelban/stats
|
||||
|
||||
XStats 的以下部分移植自 [exelban/stats](https://github.com/exelban/stats):
|
||||
|
||||
@@ -240,7 +240,7 @@ struct CleanRule: Identifiable, Sendable {
|
||||
| 开机自启 | `SMAppService.mainApp.register()` | 不需要单独的 LaunchAtLogin helper |
|
||||
| 自动更新 | Sparkle 2 | 通过 SPM 引入 |
|
||||
| 快捷键 | sindresorhus/KeyboardShortcuts(MIT) | 可选 |
|
||||
| 图标 | SF Symbols(唯一图标集) | 16pt 行内 / 20pt 独立 |
|
||||
| 图标 | 默认使用 SF Symbols;工具和网站可使用有来源声明的品牌标志 | 16pt 行内 / 20pt 独立 |
|
||||
| 日志 | `os.Logger` + 清理操作 JSONL 文件 | |
|
||||
| 测试 | Swift Testing;清理用临时目录做沙箱测试 | SafetyGuard 必须覆盖符号链接、`..`、保护路径等用例 |
|
||||
| CI | GitHub Actions `macos-latest`:构建、测试、签名、公证、生成 DMG | 用 `xcrun notarytool` |
|
||||
|
||||