diff --git a/Sources/ConsoleKitCommands/Async/AsyncCommand.swift b/Sources/ConsoleKitCommands/Async/AsyncCommand.swift index eabce96..8eeb8da 100644 --- a/Sources/ConsoleKitCommands/Async/AsyncCommand.swift +++ b/Sources/ConsoleKitCommands/Async/AsyncCommand.swift @@ -80,7 +80,7 @@ import ConsoleKitTerminal /// U ||----w | /// || || /// -public protocol AsyncCommand: AnyAsyncCommand { +public protocol AsyncCommand: Sendable, AnyAsyncCommand { associatedtype Signature: CommandSignature func run(using context: CommandContext, signature: Signature) async throws } @@ -91,7 +91,7 @@ extension AsyncCommand { guard context.input.arguments.isEmpty else { throw CommandError.unknownInput(context.input.arguments.joined(separator: " ")) } - try await self.run(using: context, signature: signature) + try await self.run(using: context, signature: signature) } public func outputAutoComplete(using context: inout CommandContext) { @@ -102,7 +102,10 @@ extension AsyncCommand { } public func outputHelp(using context: inout CommandContext) { - context.console.output("Usage: ".consoleText(.info) + context.input.executable.consoleText() + " ", newLine: false) + context.console.output( + "Usage: ".consoleText(.info) + context.input.executable.consoleText() + " ", + newLine: false + ) Signature().outputHelp(help: self.help, using: &context) } } diff --git a/Sources/ConsoleKitCommands/Async/AsyncCommandGroup.swift b/Sources/ConsoleKitCommands/Async/AsyncCommandGroup.swift index e43254d..a90a87a 100644 --- a/Sources/ConsoleKitCommands/Async/AsyncCommandGroup.swift +++ b/Sources/ConsoleKitCommands/Async/AsyncCommandGroup.swift @@ -18,11 +18,41 @@ import ConsoleKitTerminal public protocol AsyncCommandGroup: AnyAsyncCommand { var commands: [String: any AnyAsyncCommand] { get } var defaultCommand: (any AnyAsyncCommand)? { get } + + /// Merges this group with another group. + /// - Parameters: + /// - group: The group to merge with. + /// - defaultCommand: The new default command to use. + /// - help: The help message to use for the merged group. + /// - Returns: A new `AsyncCommandGroup` with the merged commands. + func merge( + with group: any AsyncCommandGroup, + defaultCommand: (any AnyAsyncCommand)?, + help: String + ) -> any AsyncCommandGroup +} + +public struct MergedAsyncCommandGroup: AsyncCommandGroup { + public let commands: [String: any AnyAsyncCommand] + public let defaultCommand: (any AnyAsyncCommand)? + public var help: String } extension AsyncCommandGroup { - public var defaultCommand: (any AnyAsyncCommand)? { - nil + public var defaultCommand: (any AnyAsyncCommand)? { nil } + + public func merge( + with group: any AsyncCommandGroup, + defaultCommand: (any AnyAsyncCommand)?, + help: String + ) -> any AsyncCommandGroup { + var mergedCommands = self.commands + mergedCommands.merge(group.commands, uniquingKeysWith: { (_, new) in new }) + return MergedAsyncCommandGroup( + commands: mergedCommands, + defaultCommand: defaultCommand, + help: help + ) } } diff --git a/Sources/ConsoleKitCommands/Async/AsyncCommands.swift b/Sources/ConsoleKitCommands/Async/AsyncCommands.swift index e0501a2..76e598a 100644 --- a/Sources/ConsoleKitCommands/Async/AsyncCommands.swift +++ b/Sources/ConsoleKitCommands/Async/AsyncCommands.swift @@ -1,5 +1,5 @@ /// Represents a top-level group of configured commands. This is usually created by calling `resolve(for:)` on `AsyncCommands`. -public struct AsyncCommands { +public struct AsyncCommands: Sendable { /// Top-level available commands, stored by unique name. public var commands: [String: any AnyAsyncCommand] @@ -38,7 +38,7 @@ public struct AsyncCommands { self.defaultCommand = defaultCommand self.enableAutocomplete = enableAutocomplete } - + /// Adds an `AsyncCommand` instance to the config. /// /// var config = AsyncCommands() diff --git a/Sources/ConsoleKitCommands/Base/AnyCommand.swift b/Sources/ConsoleKitCommands/Base/AnyCommand.swift index 28fbe8f..84ab2bb 100644 --- a/Sources/ConsoleKitCommands/Base/AnyCommand.swift +++ b/Sources/ConsoleKitCommands/Base/AnyCommand.swift @@ -1,8 +1,8 @@ /// A type-erased `Command`. -public protocol AnyCommand: Sendable { +public protocol AnyCommand: Sendable, AnyAsyncCommand { /// Text that will be displayed when `--help` is passed. var help: String { get } - + /// Runs the command against the supplied input. func run(using context: inout CommandContext) throws func outputAutoComplete(using context: inout CommandContext) throws @@ -24,4 +24,13 @@ extension AnyCommand { public func renderCompletionFunctions(using context: CommandContext, shell: Shell) -> String { "" } + + // we need to have a sync environment so the compiler uses the sync run method over the async version + private func syncRun(using context: inout CommandContext) throws { + try self.run(using: &context) + } + + public func run(using context: inout CommandContext) async throws { + try self.syncRun(using: &context) + } } diff --git a/Sources/ConsoleKitCommands/Base/CommandContext.swift b/Sources/ConsoleKitCommands/Base/CommandContext.swift index d12696b..7fcbdd5 100644 --- a/Sources/ConsoleKitCommands/Base/CommandContext.swift +++ b/Sources/ConsoleKitCommands/Base/CommandContext.swift @@ -1,15 +1,16 @@ import protocol ConsoleKitTerminal.Console +import struct ConsoleKitTerminal.AnySendableHashable /// A type-erased `CommandContext` -public struct CommandContext { +public struct CommandContext: Sendable { /// The `Console` this command was run on. public var console: any Console - + /// The parsed arguments (according to declared signature). public var input: CommandInput - public var userInfo: [AnyHashable: Any] - + public var userInfo: [AnySendableHashable: any Sendable] + /// Create a new `AnyCommandContext`. public init( console: any Console, diff --git a/Sources/ConsoleKitCommands/Base/CommandGroup.swift b/Sources/ConsoleKitCommands/Base/CommandGroup.swift index 3be82c7..b838590 100644 --- a/Sources/ConsoleKitCommands/Base/CommandGroup.swift +++ b/Sources/ConsoleKitCommands/Base/CommandGroup.swift @@ -15,7 +15,7 @@ import ConsoleKitTerminal /// try console.run(group, with: context) /// /// You can create your own `CommandGroup` if you want to support custom `CommandOptions`. -public protocol CommandGroup: AnyCommand { +public protocol CommandGroup: AnyCommand, AsyncCommandGroup { var commands: [String: any AnyCommand] { get } var defaultCommand: (any AnyCommand)? { get } } @@ -24,6 +24,12 @@ extension CommandGroup { public var defaultCommand: (any AnyCommand)? { nil } + + public var commands: [String: any AnyAsyncCommand] { + // make the compiler happy + let castedCommands: [String: any AnyCommand] = commands + return castedCommands + } } extension CommandGroup {