178 lines
5.8 KiB
Swift
178 lines
5.8 KiB
Swift
import Foundation
|
|
|
|
struct CLI {
|
|
private static let knownCommands: Set<String> = [
|
|
"scan", "disk-info", "list",
|
|
"help", "--help", "-h",
|
|
"version", "--version", "-v",
|
|
]
|
|
|
|
static func isKnownCommand(_ arg: String) -> Bool {
|
|
knownCommands.contains(arg)
|
|
}
|
|
|
|
static func run() -> Never {
|
|
let args = Array(CommandLine.arguments.dropFirst())
|
|
|
|
guard let command = args.first else {
|
|
printUsage()
|
|
exit(0)
|
|
}
|
|
|
|
switch command {
|
|
case "scan":
|
|
handleScan(args: Array(args.dropFirst()))
|
|
case "disk-info":
|
|
handleDiskInfo()
|
|
case "list":
|
|
handleList()
|
|
case "help", "--help", "-h":
|
|
printUsage()
|
|
case "version", "--version", "-v":
|
|
printVersion()
|
|
default:
|
|
printError("Unknown command: \(command)")
|
|
printUsage()
|
|
exit(1)
|
|
}
|
|
exit(0)
|
|
}
|
|
|
|
// MARK: - Commands
|
|
|
|
private static func handleScan(args: [String]) {
|
|
let json = args.contains("--json")
|
|
let categoryFilter = extractValue(for: "--category", in: args)
|
|
|
|
let engine = ScanEngine()
|
|
let categories: [CleaningCategory]
|
|
|
|
if let filter = categoryFilter {
|
|
guard let cat = CleaningCategory.scannable.first(where: {
|
|
$0.rawValue.lowercased().replacingOccurrences(of: " ", with: "") ==
|
|
filter.lowercased().replacingOccurrences(of: " ", with: "")
|
|
}) else {
|
|
printError("Unknown category: \(filter)")
|
|
print("Available: \(CleaningCategory.scannable.map(\.rawValue).joined(separator: ", "))")
|
|
exit(1)
|
|
}
|
|
categories = [cat]
|
|
} else {
|
|
categories = CleaningCategory.scannable
|
|
}
|
|
|
|
var allResults: [(String, Int, Int64)] = []
|
|
|
|
let group = DispatchGroup()
|
|
let queue = DispatchQueue(label: "cli.scan")
|
|
|
|
for category in categories {
|
|
group.enter()
|
|
Task {
|
|
let result = await engine.scanCategory(category)
|
|
queue.sync {
|
|
allResults.append((category.rawValue, result.itemCount, result.totalSize))
|
|
}
|
|
group.leave()
|
|
}
|
|
}
|
|
group.wait()
|
|
|
|
if json {
|
|
printJSON(allResults)
|
|
} else {
|
|
printTable(allResults)
|
|
}
|
|
}
|
|
|
|
private static func handleDiskInfo() {
|
|
let engine = ScanEngine()
|
|
var info = DiskInfo()
|
|
let group = DispatchGroup()
|
|
group.enter()
|
|
Task {
|
|
info = await engine.getDiskInfo()
|
|
group.leave()
|
|
}
|
|
group.wait()
|
|
|
|
print("Disk Usage:")
|
|
print(" Total: \(info.formattedTotal)")
|
|
print(" Used: \(info.formattedUsed)")
|
|
print(" Free: \(info.formattedFree)")
|
|
if info.purgeableSpace > 0 {
|
|
print(" Purgeable: \(info.formattedPurgeable)")
|
|
}
|
|
}
|
|
|
|
private static func handleList() {
|
|
let apps = AppInfoFetcher.shared.fetchInstalledApps()
|
|
print("Installed Apps (\(apps.count)):")
|
|
for app in apps {
|
|
let size = ByteCountFormatter.string(fromByteCount: app.size, countStyle: .file)
|
|
print(" \(app.appName.padding(toLength: 35, withPad: " ", startingAt: 0)) \(size.padding(toLength: 12, withPad: " ", startingAt: 0)) \(app.bundleIdentifier)")
|
|
}
|
|
}
|
|
|
|
// MARK: - Output
|
|
|
|
private static func printTable(_ results: [(String, Int, Int64)]) {
|
|
var totalSize: Int64 = 0
|
|
var totalItems = 0
|
|
|
|
print("Category Items Size")
|
|
print("---------------------- ----- --------")
|
|
for (name, count, size) in results {
|
|
let sizeStr = ByteCountFormatter.string(fromByteCount: size, countStyle: .file)
|
|
print("\(name.padding(toLength: 22, withPad: " ", startingAt: 0)) \(String(count).padding(toLength: 5, withPad: " ", startingAt: 0)) \(sizeStr)")
|
|
totalSize += size
|
|
totalItems += count
|
|
}
|
|
print("---------------------- ----- --------")
|
|
let totalStr = ByteCountFormatter.string(fromByteCount: totalSize, countStyle: .file)
|
|
print("Total \(String(totalItems).padding(toLength: 5, withPad: " ", startingAt: 0)) \(totalStr)")
|
|
}
|
|
|
|
private static func printJSON(_ results: [(String, Int, Int64)]) {
|
|
var entries: [String] = []
|
|
for (name, count, size) in results {
|
|
entries.append(" {\"category\": \"\(name)\", \"items\": \(count), \"bytes\": \(size)}")
|
|
}
|
|
print("[\n\(entries.joined(separator: ",\n"))\n]")
|
|
}
|
|
|
|
private static func printUsage() {
|
|
print("""
|
|
PureMac CLI
|
|
|
|
Usage: puremac <command> [options]
|
|
|
|
Commands:
|
|
scan Scan all categories
|
|
scan --category <name> Scan a specific category
|
|
scan --json Output as JSON
|
|
disk-info Show disk usage
|
|
list List installed apps
|
|
version Show version
|
|
help Show this help
|
|
|
|
Categories:
|
|
\(CleaningCategory.scannable.map(\.rawValue).joined(separator: ", "))
|
|
""")
|
|
}
|
|
|
|
private static func printVersion() {
|
|
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "2.0.0"
|
|
print("PureMac \(version)")
|
|
}
|
|
|
|
private static func printError(_ message: String) {
|
|
FileHandle.standardError.write(Data("Error: \(message)\n".utf8))
|
|
}
|
|
|
|
private static func extractValue(for flag: String, in args: [String]) -> String? {
|
|
guard let index = args.firstIndex(of: flag), index + 1 < args.count else { return nil }
|
|
return args[index + 1]
|
|
}
|
|
}
|