import Foundation struct MonitorCLI { static func run(_ arguments: [String]) async throws { var args = arguments, binary = ContainerService.defaultBinary, demo = false, project = "" while let first = args.first, first.hasPrefix("--") { args.removeFirst() switch first { case "--help": print(help); return case "--version": print("container-compose monitor Swift 1.1.0"); return case "--demo": demo = true case "--container", "--project": guard !args.isEmpty else { throw ServiceError.message("\(first) needs a value") }; let value = args.removeFirst(); if first == "--container" { binary = value } else { project = value } default: throw ServiceError.message("Unknown option \(first). Use --help.") } } let command = args.isEmpty ? "help" : args.removeFirst() let service = ContainerService(binary: binary, demo: demo) var native: [String] switch command { case "help": print(help); return case "app": let executable = URL(fileURLWithPath: CommandLine.arguments[0]).standardizedFileURL let adjacent = executable.deletingLastPathComponent().deletingLastPathComponent().appendingPathComponent("Container Compose.app") let embedded = executable.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent() let target = embedded.pathExtension == "app" ? embedded.path : FileManager.default.fileExists(atPath: adjacent.path) ? adjacent.path : "Container Compose" let p = Process(); p.executableURL = URL(fileURLWithPath: "/usr/bin/open"); p.arguments = ["-a", target] + (demo ? ["--args", "--demo"] : []); try p.run(); p.waitUntilExit(); if p.terminationStatus != 0 { throw ServiceError.message("Install Container Compose.app in Applications, or launch it directly.") }; return case "watch", "monitor": try await watch(args, service: service); return case "volume", "network", "image": guard let verb = args.first, ["list", "inspect", "create", "delete", "prune", "pull"].contains(verb) else { throw ServiceError.message("Use container-compose monitor \(command) list|inspect|create|delete|prune [native arguments]") } if command == "volume", verb == "list", args.contains("--all-mounts") { try await printVolumeInventory(service); return } if verb == "prune" { guard args.contains("--yes") else { throw ServiceError.message("Resource pruning requires --yes") }; args.removeAll { $0 == "--yes" } } if verb == "list", args.count == 1 { args += ["--format", "json"] } native = [command] + args case "ps": native = ["list", "--all", "--format", "json"] + args case "volumes" where args.contains("--all-mounts"): try await printVolumeInventory(service); return case "images", "networks", "volumes": native = [String(command.dropLast()), "list", "--format", "json"] + args case "projects": let resources = try await service.list(.projects, project: project).filter { !$0.project.isEmpty && (project.isEmpty || $0.project == project) } let rows = resources.map { ["id": $0.id, "project": $0.project, "service": $0.service, "status": $0.state] }; print(ContainerService.pretty(rows)); return case "logs", "inspect", "start", "stop", "delete", "kill": guard !args.isEmpty else { throw ServiceError.message("\(command) needs a target") }; native = [command] + args case "stats" where args.contains("--watch"): args.removeAll { $0 == "--watch" }; try await watch(args, service: service); return case "stats": native = ["stats", "--no-stream", "--format", "json"] + args case "restart": guard args.count == 1, let id = args.first, !id.isEmpty, !id.hasPrefix("-") else { throw ServiceError.message("restart requires exactly one container ID") } let resources = try await service.list(.containers) guard let resource = resources.first(where: { $0.id == id }) else { throw ServiceError.message("Container not found: \(id)") } print(try await service.action(kind: .containers, action: "restart", resource: resource)); return case "prune": guard args.contains("--yes") else { throw ServiceError.message("Prune requires --yes. It affects ALL unused resources of the chosen type, regardless of --project.") } let kinds = args.filter { $0 != "--yes" }; guard kinds.count <= 1, let kind = ResourceKind(rawValue: kinds.first ?? "containers"), kind != .projects, kind != .monitor else { throw ServiceError.message("Usage: container-compose monitor prune [containers|images|networks|volumes] --yes") }; native = ContainerService.actionArguments(kind: kind, action: "prune", id: "") default: throw ServiceError.message("Unknown command: \(command). Use --help.") } if command == "logs", !demo { let p = Process(); p.executableURL = URL(fileURLWithPath: binary); p.arguments = native; p.standardInput = FileHandle.standardInput; p.standardOutput = FileHandle.standardOutput; p.standardError = FileHandle.standardError try p.run(); p.waitUntilExit(); if p.terminationStatus != 0 { exit(p.terminationStatus) }; return } print(try await service.run(native)) } static func printVolumeInventory(_ service: ContainerService) async throws { let volumes = try await service.list(.volumes), containers = try await service.list(.containers) let rows = VolumeInventory.combine(volumes: volumes, containers: containers).map { r in ["id": r.id, "name": r.name, "type": r.storageKind, "capacity": r.capacity, "driver": r.driver, "format": r.format, "source": r.source, "created": r.created, "usedBy": r.usedBy, "inspectOnly": r.readOnlyStorage ? "true" : "false"] } print(ContainerService.pretty(rows)) } static func watch(_ arguments: [String], service: ContainerService) async throws { var args = arguments, interval = 1.0, count: Int?, json = false, ids: [String] = [] while !args.isEmpty { let option = args.removeFirst() switch option { case "--interval": guard !args.isEmpty, let value = Double(args.removeFirst()), value >= 0.25, value.isFinite else { throw ServiceError.message("--interval needs a finite number >= 0.25 seconds") }; interval = value case "--count": guard !args.isEmpty, let value = Int(args.removeFirst()), value > 0 else { throw ServiceError.message("--count needs a positive number") }; count = value case "--json": json = true default: guard !option.hasPrefix("-") else { throw ServiceError.message("Unknown watch option: \(option)") }; ids.append(option) } } var tracker = NetworkRateTracker(), history: [Double] = [], samples = 0 while !Task.isCancelled { let start = Date() var rows = try await service.list(.containers) if !ids.isEmpty { rows = rows.filter { ids.contains($0.id) }; guard rows.count == Set(ids).count else { throw ServiceError.message("One or more requested containers were not found") } } rows = try await service.withStats(rows) let time = Date(), rates = tracker.sample(rows, at: time), cpu = rows.reduce(0) { $0 + ($1.cpuFraction ?? 0) * 100 } history.append(cpu); history = Array(history.suffix(50)); samples += 1 if json { let resourceRows: [[String: Any]] = rows.map { ["id": $0.id, "state": $0.state, "cpuPercent": $0.cpuFraction.map { $0 * 100 } as Any? ?? NSNull(), "memoryBytes": $0.memoryBytes as Any? ?? NSNull(), "networkRxBytes": $0.rxBytes as Any? ?? NSNull(), "networkTxBytes": $0.txBytes as Any? ?? NSNull()] } let value: [String: Any] = ["timestamp": ISO8601DateFormatter().string(from: time), "containers": resourceRows, "networkRxBytesPerSecond": rates.rx, "networkTxBytesPerSecond": rates.tx, "demo": service.demo] let data = try JSONSerialization.data(withJSONObject: value, options: [.sortedKeys]); print(String(decoding: data, as: UTF8.self)) } else { if isatty(STDOUT_FILENO) != 0 { print("\u{001B}[2J\u{001B}[H", terminator: "") } print("container-compose monitor live monitor" + (service.demo ? " · DEMO READ-ONLY" : " · Apple Containers")) print(time.formatted(date: .omitted, time: .standard) + " · " + String(rows.filter { $0.state == "running" }.count) + " running · CPU " + String(format: "%.2f%%",cpu)) print("Network ↓ " + ContainerService.bytes(rates.rx) + "/s ↑ " + ContainerService.bytes(rates.tx) + "/s") let glyphs = Array("▁▂▃▄▅▆▇█"), peak = max(history.max() ?? 0, 1) print("CPU " + history.map { String(glyphs[min(7, Int(max(0,$0) / peak * 7))]) }.joined()) print("NAME".padding(toLength: 32, withPad: " ", startingAt: 0) + "STATE CPU MEMORY NET RX / TX") for r in rows { print(String(r.name.prefix(30)).padding(toLength: 32, withPad: " ", startingAt: 0) + r.state.padding(toLength: 12, withPad: " ", startingAt: 0) + r.cpu.padding(toLength: 13, withPad: " ", startingAt: 0) + r.memory.padding(toLength: 13, withPad: " ", startingAt: 0) + r.network) } print("Ctrl+C to exit · native CPU samples take about 2s · no terminal input mode changes") } fflush(stdout) if let count, samples >= count { return } let remaining = interval - Date().timeIntervalSince(start) if remaining > 0 { try await Task.sleep(for: .seconds(remaining)) } } } static let help = """ container-compose monitor Swift 1.1.0 — Apple Container command-line companion Usage: container-compose monitor [--container PATH] [--demo] [--project NAME] COMMAND [ARGS] app Open the native macOS app watch [--interval S] [--count N] [--json] [ID...] Live terminal table + CPU sparkline; --json emits NDJSON monitor Alias for watch ps All containers as JSON projects Recognized Compose members as JSON volume list [--all-mounts] Named volumes; --all-mounts includes host mounts/references volume inspect NAME... Volume details volume create [-s SIZE] NAME volume delete NAME... Delete named volumes volume prune --yes Prune unreferenced named volumes network|image SUBCOMMAND Native resource commands images | networks | volumes Resource list as JSON logs [-n N] [--follow] ID Logs; native streaming has no timeout inspect ID... Container configuration as JSON stats [ID...] JSON snapshot; --watch enables live monitoring start ID... | stop ID... | delete ID... | kill ID... restart ID Start if stopped, otherwise stop then start prune [containers|images|networks|volumes] --yes help | --help | --version Global options precede COMMAND. Native options follow it. Commands use Apple's container CLI; Docker is not required. Non-streaming commands time out after 30s; captured output is capped at 4 MiB. """ }