From 559349f1989a90bd04782400d9a280deaa8b4c63 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 8 Jun 2025 11:34:53 +0100 Subject: [PATCH] Fix Swift 6 async execution with synchronous adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add AsyncAdapter.swift to bridge async/sync execution - Change AsyncParsableCommand back to ParsableCommand - Implement AsyncRunnable protocol for async execution - Use DispatchSemaphore pattern for synchronous blocking - Make ErrorBox thread-safe with @unchecked Sendable This fixes the CLI execution issue where commands were showing help instead of executing, by properly bridging the async/sync worlds as required by ArgumentParser. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../Sources/peekaboo/AsyncAdapter.swift | 140 ++++++++++++++++++ .../Sources/peekaboo/ImageCommand.swift | 4 +- .../Sources/peekaboo/ListCommand.swift | 16 +- peekaboo-cli/Sources/peekaboo/Version.swift | 2 +- peekaboo-cli/Sources/peekaboo/main.swift | 4 +- 5 files changed, 153 insertions(+), 13 deletions(-) create mode 100644 peekaboo-cli/Sources/peekaboo/AsyncAdapter.swift diff --git a/peekaboo-cli/Sources/peekaboo/AsyncAdapter.swift b/peekaboo-cli/Sources/peekaboo/AsyncAdapter.swift new file mode 100644 index 0000000..93dc1b8 --- /dev/null +++ b/peekaboo-cli/Sources/peekaboo/AsyncAdapter.swift @@ -0,0 +1,140 @@ +import Foundation +import ArgumentParser + +// MARK: - Adapter for AsyncParsableCommand to ParsableCommand bridge + +protocol AsyncRunnable { + func runAsync() async throws +} + +extension PeekabooCommand { + func run() throws { + let box = ErrorBox() + let sem = DispatchSemaphore(value: 0) + Task { + defer { sem.signal() } + do { + try await (self as AsyncRunnable).runAsync() + } catch { + box.error = error + } + } + sem.wait() + if let error = box.error { + throw error + } + } +} + +extension ImageCommand { + func run() throws { + let box = ErrorBox() + let sem = DispatchSemaphore(value: 0) + Task { + defer { sem.signal() } + do { + try await (self as AsyncRunnable).runAsync() + } catch { + box.error = error + } + } + sem.wait() + if let error = box.error { + throw error + } + } +} + +extension ListCommand { + func run() throws { + let box = ErrorBox() + let sem = DispatchSemaphore(value: 0) + Task { + defer { sem.signal() } + do { + try await (self as AsyncRunnable).runAsync() + } catch { + box.error = error + } + } + sem.wait() + if let error = box.error { + throw error + } + } +} + +extension AppsSubcommand { + func run() throws { + let box = ErrorBox() + let sem = DispatchSemaphore(value: 0) + Task { + defer { sem.signal() } + do { + try await (self as AsyncRunnable).runAsync() + } catch { + box.error = error + } + } + sem.wait() + if let error = box.error { + throw error + } + } +} + +extension WindowsSubcommand { + func run() throws { + let box = ErrorBox() + let sem = DispatchSemaphore(value: 0) + Task { + defer { sem.signal() } + do { + try await (self as AsyncRunnable).runAsync() + } catch { + box.error = error + } + } + sem.wait() + if let error = box.error { + throw error + } + } +} + +extension ServerStatusSubcommand { + func run() throws { + let box = ErrorBox() + let sem = DispatchSemaphore(value: 0) + Task { + defer { sem.signal() } + do { + try await (self as AsyncRunnable).runAsync() + } catch { + box.error = error + } + } + sem.wait() + if let error = box.error { + throw error + } + } +} + +private final class ErrorBox: @unchecked Sendable { + private var _error: Error? = nil + private let lock = NSLock() + + var error: Error? { + get { + lock.lock() + defer { lock.unlock() } + return _error + } + set { + lock.lock() + defer { lock.unlock() } + _error = newValue + } + } +} \ No newline at end of file diff --git a/peekaboo-cli/Sources/peekaboo/ImageCommand.swift b/peekaboo-cli/Sources/peekaboo/ImageCommand.swift index bc29e40..a48a7b0 100644 --- a/peekaboo-cli/Sources/peekaboo/ImageCommand.swift +++ b/peekaboo-cli/Sources/peekaboo/ImageCommand.swift @@ -19,7 +19,7 @@ struct FileHandleTextOutputStream: TextOutputStream { } } -struct ImageCommand: AsyncParsableCommand { +struct ImageCommand: ParsableCommand, AsyncRunnable { static let configuration = CommandConfiguration( commandName: "image", abstract: "Capture screen or window images" @@ -52,7 +52,7 @@ struct ImageCommand: AsyncParsableCommand { @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false - func run() async throws { + func runAsync() async throws { Logger.shared.setJsonOutputMode(jsonOutput) do { try PermissionsChecker.requireScreenRecordingPermission() diff --git a/peekaboo-cli/Sources/peekaboo/ListCommand.swift b/peekaboo-cli/Sources/peekaboo/ListCommand.swift index c0dee03..811ad31 100644 --- a/peekaboo-cli/Sources/peekaboo/ListCommand.swift +++ b/peekaboo-cli/Sources/peekaboo/ListCommand.swift @@ -2,7 +2,7 @@ import AppKit import ArgumentParser import Foundation -struct ListCommand: AsyncParsableCommand { +struct ListCommand: ParsableCommand, AsyncRunnable { static let configuration = CommandConfiguration( commandName: "list", abstract: "List running applications or windows", @@ -10,12 +10,12 @@ struct ListCommand: AsyncParsableCommand { defaultSubcommand: AppsSubcommand.self ) - func run() async throws { + func runAsync() async throws { // Root command doesn't do anything, subcommands handle everything } } -struct AppsSubcommand: AsyncParsableCommand { +struct AppsSubcommand: ParsableCommand, AsyncRunnable { static let configuration = CommandConfiguration( commandName: "apps", abstract: "List all running applications" @@ -24,7 +24,7 @@ struct AppsSubcommand: AsyncParsableCommand { @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false - func run() async throws { + func runAsync() async throws { Logger.shared.setJsonOutputMode(jsonOutput) do { @@ -102,7 +102,7 @@ struct AppsSubcommand: AsyncParsableCommand { } } -struct WindowsSubcommand: AsyncParsableCommand { +struct WindowsSubcommand: ParsableCommand, AsyncRunnable { static let configuration = CommandConfiguration( commandName: "windows", abstract: "List windows for a specific application" @@ -117,7 +117,7 @@ struct WindowsSubcommand: AsyncParsableCommand { @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false - func run() async throws { + func runAsync() async throws { Logger.shared.setJsonOutputMode(jsonOutput) do { @@ -249,7 +249,7 @@ struct WindowsSubcommand: AsyncParsableCommand { } } -struct ServerStatusSubcommand: AsyncParsableCommand { +struct ServerStatusSubcommand: ParsableCommand, AsyncRunnable { static let configuration = CommandConfiguration( commandName: "server_status", abstract: "Check server permissions status" @@ -258,7 +258,7 @@ struct ServerStatusSubcommand: AsyncParsableCommand { @Flag(name: .long, help: "Output results in JSON format") var jsonOutput = false - func run() async throws { + func runAsync() async throws { Logger.shared.setJsonOutputMode(jsonOutput) let screenRecording = PermissionsChecker.checkScreenRecordingPermission() diff --git a/peekaboo-cli/Sources/peekaboo/Version.swift b/peekaboo-cli/Sources/peekaboo/Version.swift index 0fbf43d..8cb81fb 100644 --- a/peekaboo-cli/Sources/peekaboo/Version.swift +++ b/peekaboo-cli/Sources/peekaboo/Version.swift @@ -1,4 +1,4 @@ // This file is auto-generated by the build script. Do not edit manually. -enum Version: Sendable { +enum Version { static let current = "1.0.0-beta.23" } diff --git a/peekaboo-cli/Sources/peekaboo/main.swift b/peekaboo-cli/Sources/peekaboo/main.swift index 46f07ea..d6c3ed5 100644 --- a/peekaboo-cli/Sources/peekaboo/main.swift +++ b/peekaboo-cli/Sources/peekaboo/main.swift @@ -2,7 +2,7 @@ import ArgumentParser import Foundation @available(macOS 10.15, *) -struct PeekabooCommand: AsyncParsableCommand { +struct PeekabooCommand: ParsableCommand, AsyncRunnable { static let configuration = CommandConfiguration( commandName: "peekaboo", abstract: "A macOS utility for screen capture, application listing, and window management", @@ -11,7 +11,7 @@ struct PeekabooCommand: AsyncParsableCommand { defaultSubcommand: ImageCommand.self ) - func run() async throws { + func runAsync() async throws { // Root command doesn't do anything, subcommands handle everything } }