197 lines
7.1 KiB
Swift
197 lines
7.1 KiB
Swift
import AsyncXPCConnection
|
|
import ClawdisIPC
|
|
import Foundation
|
|
|
|
private let serviceName = "com.steipete.clawdis.xpc"
|
|
|
|
@objc protocol ClawdisXPCProtocol {
|
|
func handle(_ data: Data, withReply reply: @escaping @Sendable (Data?, Error?) -> Void)
|
|
}
|
|
|
|
@main
|
|
struct ClawdisCLI {
|
|
static func main() async {
|
|
do {
|
|
let request = try parseCommandLine()
|
|
let response = try await send(request: request)
|
|
let payloadString: String? = if let payload = response.payload, let text = String(
|
|
data: payload,
|
|
encoding: .utf8)
|
|
{
|
|
text
|
|
} else {
|
|
nil
|
|
}
|
|
let output: [String: Any] = [
|
|
"ok": response.ok,
|
|
"message": response.message ?? "",
|
|
"payload": payloadString ?? "",
|
|
]
|
|
let json = try JSONSerialization.data(withJSONObject: output, options: [.prettyPrinted])
|
|
FileHandle.standardOutput.write(json)
|
|
FileHandle.standardOutput.write(Data([0x0A]))
|
|
exit(response.ok ? 0 : 1)
|
|
} catch CLIError.help {
|
|
printHelp()
|
|
exit(0)
|
|
} catch CLIError.version {
|
|
printVersion()
|
|
exit(0)
|
|
} catch {
|
|
fputs("clawdis-mac error: \(error)\n", stderr)
|
|
exit(2)
|
|
}
|
|
}
|
|
|
|
// swiftlint:disable cyclomatic_complexity
|
|
private static func parseCommandLine() throws -> Request {
|
|
var args = Array(CommandLine.arguments.dropFirst())
|
|
guard let command = args.first else { throw CLIError.help }
|
|
args = Array(args.dropFirst())
|
|
|
|
switch command {
|
|
case "--help", "-h", "help":
|
|
throw CLIError.help
|
|
case "--version", "-V", "version":
|
|
throw CLIError.version
|
|
|
|
case "notify":
|
|
var title: String?
|
|
var body: String?
|
|
var sound: String?
|
|
while !args.isEmpty {
|
|
let arg = args.removeFirst()
|
|
switch arg {
|
|
case "--title": title = args.popFirst()
|
|
case "--body": body = args.popFirst()
|
|
case "--sound": sound = args.popFirst()
|
|
default: break
|
|
}
|
|
}
|
|
guard let t = title, let b = body else { throw CLIError.help }
|
|
return .notify(title: t, body: b, sound: sound)
|
|
|
|
case "ensure-permissions":
|
|
var caps: [Capability] = []
|
|
var interactive = false
|
|
while !args.isEmpty {
|
|
let arg = args.removeFirst()
|
|
switch arg {
|
|
case "--cap":
|
|
if let val = args.popFirst(), let cap = Capability(rawValue: val) { caps.append(cap) }
|
|
case "--interactive": interactive = true
|
|
default: break
|
|
}
|
|
}
|
|
if caps.isEmpty { caps = Capability.allCases }
|
|
return .ensurePermissions(caps, interactive: interactive)
|
|
|
|
case "screenshot":
|
|
var displayID: UInt32?
|
|
var windowID: UInt32?
|
|
while !args.isEmpty {
|
|
let arg = args.removeFirst()
|
|
switch arg {
|
|
case "--display-id": if let val = args.popFirst(), let num = UInt32(val) { displayID = num }
|
|
case "--window-id": if let val = args.popFirst(), let num = UInt32(val) { windowID = num }
|
|
default: break
|
|
}
|
|
}
|
|
return .screenshot(displayID: displayID, windowID: windowID, format: "png")
|
|
|
|
case "run":
|
|
var cwd: String?
|
|
var env: [String: String] = [:]
|
|
var timeout: Double?
|
|
var needsSR = false
|
|
var cmd: [String] = []
|
|
while !args.isEmpty {
|
|
let arg = args.removeFirst()
|
|
switch arg {
|
|
case "--cwd": cwd = args.popFirst()
|
|
|
|
case "--env":
|
|
if let pair = args.popFirst(), let eq = pair.firstIndex(of: "=") {
|
|
let k = String(pair[..<eq]); let v = String(pair[pair.index(after: eq)...]); env[k] = v
|
|
}
|
|
|
|
case "--timeout": if let val = args.popFirst(), let dbl = Double(val) { timeout = dbl }
|
|
|
|
case "--needs-screen-recording": needsSR = true
|
|
|
|
default:
|
|
cmd.append(arg)
|
|
}
|
|
}
|
|
return .runShell(
|
|
command: cmd,
|
|
cwd: cwd,
|
|
env: env.isEmpty ? nil : env,
|
|
timeoutSec: timeout,
|
|
needsScreenRecording: needsSR)
|
|
|
|
case "status":
|
|
return .status
|
|
|
|
default:
|
|
throw CLIError.help
|
|
}
|
|
}
|
|
|
|
// swiftlint:enable cyclomatic_complexity
|
|
|
|
private static func printHelp() {
|
|
let usage = """
|
|
clawdis-mac — talk to the running Clawdis.app XPC service
|
|
|
|
Usage:
|
|
clawdis-mac notify --title <t> --body <b> [--sound <name>]
|
|
clawdis-mac ensure-permissions [--cap <notifications|accessibility|screenRecording|microphone|speechRecognition>] [--interactive]
|
|
clawdis-mac screenshot [--display-id <u32>] [--window-id <u32>]
|
|
clawdis-mac run [--cwd <path>] [--env KEY=VAL] [--timeout <sec>] [--needs-screen-recording] <command ...>
|
|
clawdis-mac status
|
|
clawdis-mac --help
|
|
|
|
Returns JSON to stdout:
|
|
{"ok":<bool>,"message":"...","payload":"..."}
|
|
"""
|
|
print(usage)
|
|
}
|
|
|
|
private static func printVersion() {
|
|
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown"
|
|
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? ""
|
|
let git = Bundle.main.object(forInfoDictionaryKey: "ClawdisGitCommit") as? String ?? "unknown"
|
|
let ts = Bundle.main.object(forInfoDictionaryKey: "ClawdisBuildTimestamp") as? String ?? "unknown"
|
|
print("clawdis-mac \(version) (\(build)) git:\(git) built:\(ts)")
|
|
}
|
|
|
|
private static func send(request: Request) async throws -> Response {
|
|
let conn = NSXPCConnection(machServiceName: serviceName)
|
|
let interface = NSXPCInterface(with: ClawdisXPCProtocol.self)
|
|
conn.remoteObjectInterface = interface
|
|
conn.resume()
|
|
defer { conn.invalidate() }
|
|
|
|
let data = try JSONEncoder().encode(request)
|
|
|
|
let service = AsyncXPCConnection.RemoteXPCService<ClawdisXPCProtocol>(connection: conn)
|
|
let raw: Data = try await service.withValueErrorCompletion { proxy, completion in
|
|
struct CompletionBox: @unchecked Sendable { let handler: (Data?, Error?) -> Void }
|
|
let box = CompletionBox(handler: completion)
|
|
proxy.handle(data, withReply: { data, error in box.handler(data, error) })
|
|
}
|
|
return try JSONDecoder().decode(Response.self, from: raw)
|
|
}
|
|
}
|
|
|
|
enum CLIError: Error { case help, version }
|
|
|
|
extension [String] {
|
|
mutating func popFirst() -> String? {
|
|
guard let first else { return nil }
|
|
self = Array(self.dropFirst())
|
|
return first
|
|
}
|
|
}
|