Fix Swift 6 async execution with synchronous adapter

- 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 <noreply@anthropic.com>
This commit is contained in:
Peter Steinberger 2025-06-08 11:34:53 +01:00
parent 8b46d11015
commit 559349f198
5 changed files with 153 additions and 13 deletions

View file

@ -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
}
}
}

View file

@ -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()

View file

@ -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()

View file

@ -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"
}

View file

@ -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
}
}