summaryrefslogtreecommitdiff
path: root/DomainDigCLI.swift
diff options
context:
space:
mode:
Diffstat (limited to 'DomainDigCLI.swift')
-rw-r--r--DomainDigCLI.swift186
1 files changed, 179 insertions, 7 deletions
diff --git a/DomainDigCLI.swift b/DomainDigCLI.swift
index a6d2cf3..8c433d8 100644
--- a/DomainDigCLI.swift
+++ b/DomainDigCLI.swift
@@ -4,14 +4,24 @@ import Foundation
struct DomainDigCLI {
static func main() async {
let arguments = Array(CommandLine.arguments.dropFirst())
+ let wantsJSON = arguments.contains("--json") || arguments.contains("-j")
guard let command = CommandLine.arguments.first else {
- fputs("usage: domaindig <domain> [--json] [--ownership-history] [--dns-history] [--extended-subdomains] [--pricing] [--show-usage]\n", stderr)
+ fputs(usageText, stderr)
Foundation.exit(1)
}
_ = command
- let wantsJSON = arguments.contains("--json") || arguments.contains("-j")
+ if arguments.first == "backup" {
+ runBackupCommand(arguments: Array(arguments.dropFirst()), wantsJSON: wantsJSON)
+ return
+ }
+
+ if arguments.first == "monitor" {
+ await runMonitorCommand(wantsJSON: wantsJSON)
+ return
+ }
+
let wantsOwnershipHistory = arguments.contains("--ownership-history")
let wantsDNSHistory = arguments.contains("--dns-history")
let wantsExtendedSubdomains = arguments.contains("--extended-subdomains")
@@ -24,7 +34,7 @@ struct DomainDigCLI {
.filter { !$0.isEmpty }
guard !requestedDomains.isEmpty else {
- fputs("usage: domaindig <domain> [--json] [--ownership-history] [--dns-history] [--extended-subdomains] [--pricing] [--show-usage]\n", stderr)
+ fputs(usageText, stderr)
Foundation.exit(1)
}
@@ -252,10 +262,172 @@ struct DomainDigCLI {
}
private static func loadHistoryEntries() -> [HistoryEntry] {
- guard let data = UserDefaults.standard.data(forKey: "lookupHistory"),
- let entries = try? JSONDecoder().decode([HistoryEntry].self, from: data) else {
- return []
+ DomainDataPortabilityService.loadHistoryEntries()
+ }
+
+ private static func runBackupCommand(arguments: [String], wantsJSON: Bool) {
+ guard let subcommand = arguments.first else {
+ fputs(usageText, stderr)
+ Foundation.exit(1)
+ }
+
+ switch subcommand {
+ case "export":
+ do {
+ let data = try DomainDataPortabilityService.backupData()
+ let outputPath = arguments.dropFirst().first(where: { !$0.hasPrefix("-") })
+ if let outputPath {
+ try data.write(to: URL(fileURLWithPath: outputPath), options: .atomic)
+ } else {
+ FileHandle.standardOutput.write(data)
+ if data.last != 0x0A {
+ FileHandle.standardOutput.write(Data([0x0A]))
+ }
+ }
+ } catch {
+ fputs("domaindig backup export: \(error.localizedDescription)\n", stderr)
+ Foundation.exit(1)
+ }
+ case "validate":
+ guard let path = arguments.dropFirst().first(where: { !$0.hasPrefix("-") }) else {
+ fputs("usage: domaindig backup validate <path>\n", stderr)
+ Foundation.exit(1)
+ }
+
+ do {
+ let data = try Data(contentsOf: URL(fileURLWithPath: path))
+ let report = try DomainDataPortabilityService.validateBackup(data: data, fileName: URL(fileURLWithPath: path).lastPathComponent)
+ if wantsJSON {
+ let payload = [
+ "warnings": report.warnings,
+ "errors": report.errors
+ ]
+ let encoded = try JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted, .sortedKeys])
+ FileHandle.standardOutput.write(encoded)
+ } else {
+ let lines = [
+ "Warnings: \(report.warnings.count)",
+ "Errors: \(report.errors.count)"
+ ] + report.warnings.map { "warning: \($0)" } + report.errors.map { "error: \($0)" }
+ FileHandle.standardOutput.write(Data(lines.joined(separator: "\n").utf8))
+ }
+ FileHandle.standardOutput.write(Data([0x0A]))
+ if !report.errors.isEmpty {
+ Foundation.exit(1)
+ }
+ } catch {
+ fputs("domaindig backup validate: \(error.localizedDescription)\n", stderr)
+ Foundation.exit(1)
+ }
+ case "import":
+ guard let path = arguments.dropFirst().first(where: { !$0.hasPrefix("-") }) else {
+ fputs("usage: domaindig backup import <path> [--replace]\n", stderr)
+ Foundation.exit(1)
+ }
+
+ let mode: DataPortabilityImportMode = arguments.contains("--replace") ? .replace : .merge
+
+ do {
+ let fileURL = URL(fileURLWithPath: path)
+ let data = try Data(contentsOf: fileURL)
+ let preview = try DomainDataPortabilityService.prepareImport(
+ data: data,
+ fileName: fileURL.lastPathComponent,
+ mode: mode
+ )
+ guard preview.kind == .backup else {
+ fputs("domaindig backup import: expected a full backup file\n", stderr)
+ Foundation.exit(1)
+ }
+ let result = try DomainDataPortabilityService.applyImport(preview, mode: mode)
+ let lines = [result.summary] + result.warnings.map { "warning: \($0)" }
+ FileHandle.standardOutput.write(Data(lines.joined(separator: "\n").utf8))
+ FileHandle.standardOutput.write(Data([0x0A]))
+ } catch {
+ fputs("domaindig backup import: \(error.localizedDescription)\n", stderr)
+ Foundation.exit(1)
+ }
+ default:
+ fputs(usageText, stderr)
+ Foundation.exit(1)
+ }
+ }
+
+ private static func runMonitorCommand(wantsJSON: Bool) async {
+ guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
+ fputs("domaindig monitor: monitoring requires Pro\n", stderr)
+ Foundation.exit(1)
+ }
+
+ let outcome = await DomainMonitoringService.shared.performMonitoring(
+ trigger: .cli,
+ requireEnabledSetting: false
+ )
+
+ guard let log = outcome.log else {
+ fputs("domaindig monitor: \(outcome.message)\n", stderr)
+ Foundation.exit(1)
+ }
+
+ do {
+ let output: Data
+ if wantsJSON {
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
+ output = try encoder.encode(log)
+ } else {
+ output = Data(monitoringTextSummary(for: log).utf8)
+ }
+ FileHandle.standardOutput.write(output)
+ if output.last != 0x0A {
+ FileHandle.standardOutput.write(Data([0x0A]))
+ }
+ if !outcome.success {
+ Foundation.exit(1)
+ }
+ } catch {
+ fputs("domaindig monitor: \(error.localizedDescription)\n", stderr)
+ Foundation.exit(1)
+ }
+ }
+
+ private static func monitoringTextSummary(for log: MonitoringLog) -> String {
+ var lines = [
+ "DomainDig Monitoring",
+ "====================",
+ "Trigger: \(log.trigger.title)",
+ "Timestamp: \(log.timestamp.formatted(date: .abbreviated, time: .shortened))",
+ "Checked: \(log.domainsChecked)",
+ "Changes: \(log.changesFound)",
+ "Alerts: \(log.alertsTriggered)",
+ ""
+ ]
+
+ if log.checkedDomains.isEmpty {
+ lines.append("No domains were checked.")
+ } else {
+ for result in log.checkedDomains {
+ let severity = result.alertSeverity?.title ?? "None"
+ lines.append("\(result.domain): \(result.summaryMessage) [alert: \(severity)]")
+ }
+ }
+
+ if !log.errors.isEmpty {
+ lines.append("")
+ lines.append("Errors:")
+ lines.append(contentsOf: log.errors.map { "- \($0)" })
}
- return entries
+
+ return lines.joined(separator: "\n")
+ }
+
+ private static var usageText: String {
+ """
+ usage: domaindig <domain> [--json] [--ownership-history] [--dns-history] [--extended-subdomains] [--pricing] [--show-usage]
+ domaindig monitor [--json]
+ domaindig backup export [path]
+ domaindig backup import <path> [--replace]
+ domaindig backup validate <path> [--json]
+ """
}
}