feat: Preparing to move items out of cli-client module into playbook-client

This commit is contained in:
2024-12-14 10:06:30 -05:00
parent b5afc77428
commit 303cdef84b
7 changed files with 211 additions and 112 deletions

View File

@@ -38,6 +38,7 @@ let package = Package(
.target( .target(
name: "CliClient", name: "CliClient",
dependencies: [ dependencies: [
"CommandClient",
"CodersClient", "CodersClient",
"ConfigurationClient", "ConfigurationClient",
"PlaybookClient", "PlaybookClient",
@@ -64,6 +65,7 @@ let package = Package(
.target( .target(
name: "CommandClient", name: "CommandClient",
dependencies: [ dependencies: [
"Constants",
.product(name: "Dependencies", package: "swift-dependencies"), .product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies"), .product(name: "DependenciesMacros", package: "swift-dependencies"),
.product(name: "ShellClient", package: "swift-shell-client") .product(name: "ShellClient", package: "swift-shell-client")
@@ -104,16 +106,10 @@ let package = Package(
.product(name: "DependenciesMacros", package: "swift-dependencies") .product(name: "DependenciesMacros", package: "swift-dependencies")
] ]
), ),
.target(
name: "LoggingExtensions",
dependencies: [
"Constants",
.product(name: "ShellClient", package: "swift-shell-client")
]
),
.target( .target(
name: "PlaybookClient", name: "PlaybookClient",
dependencies: [ dependencies: [
"CommandClient",
"ConfigurationClient", "ConfigurationClient",
"FileClient", "FileClient",
.product(name: "Dependencies", package: "swift-dependencies"), .product(name: "Dependencies", package: "swift-dependencies"),

View File

@@ -47,7 +47,7 @@ public extension CliClient {
) )
let configuration = try await configurationClient.findAndLoad() let configuration = try await configurationClient.findAndLoad()
try await playbookClient.installPlaybook(configuration) try await playbookClient.repository.install(configuration)
} }
func runPlaybookCommand( func runPlaybookCommand(
@@ -62,7 +62,7 @@ public extension CliClient {
let configuration = try await configurationClient.ensuredConfiguration(options.configuration) let configuration = try await configurationClient.ensuredConfiguration(options.configuration)
logger.trace("Configuration: \(configuration)") logger.trace("Configuration: \(configuration)")
let playbookDirectory = try await playbookClient.playbookDirectory(configuration) let playbookDirectory = try await playbookClient.repository.directory(configuration)
let playbookPath = "\(playbookDirectory)/\(Constants.playbookFileName)" let playbookPath = "\(playbookDirectory)/\(Constants.playbookFileName)"
logger.trace("Playbook path: \(playbookPath)") logger.trace("Playbook path: \(playbookPath)")

View File

@@ -12,7 +12,7 @@ extension CliClient.RunPlaybook {
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
@Dependency(\.playbookClient) var playbookClient @Dependency(\.playbookClient) var playbookClient
let playbookDirectory = try await playbookClient.playbookDirectory(configuration) let playbookDirectory = try await playbookClient.repository.directory(configuration)
let playbookPath = "\(playbookDirectory)/\(Constants.playbookFileName)" let playbookPath = "\(playbookDirectory)/\(Constants.playbookFileName)"
logger.trace("Playbook path: \(playbookPath)") logger.trace("Playbook path: \(playbookPath)")
@@ -111,7 +111,7 @@ extension CliClient.PlaybookOptions.Route {
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
@Dependency(\.playbookClient) var playbookClient @Dependency(\.playbookClient) var playbookClient
let playbookDirectory = try await playbookClient.playbookDirectory(configuration) let playbookDirectory = try await playbookClient.repository.directory(configuration)
let playbookPath = "\(playbookDirectory)/\(Constants.playbookFileName)" let playbookPath = "\(playbookDirectory)/\(Constants.playbookFileName)"
logger.trace("Playbook path: \(playbookPath)") logger.trace("Playbook path: \(playbookPath)")

View File

@@ -1,6 +1,8 @@
import Constants
import Dependencies import Dependencies
import DependenciesMacros import DependenciesMacros
import Foundation import Foundation
import LoggingExtensions
import ShellClient import ShellClient
public extension DependencyValues { public extension DependencyValues {
@@ -18,37 +20,93 @@ public struct CommandClient: Sendable {
/// Runs a shell command. /// Runs a shell command.
public var runCommand: @Sendable (RunCommandOptions) async throws -> Void public var runCommand: @Sendable (RunCommandOptions) async throws -> Void
/// Runs a shell command. /// Runs a shell command and sets up logging.
public func run( public func run(
quiet: Bool, logging logginOptions: LoggingOptions,
shell: ShellCommand.Shell, quiet: Bool = false,
_ arguments: [String] shell: String? = nil,
in workingDirectory: String? = nil,
makeArguments: @Sendable @escaping () async throws -> [String]
) async throws { ) async throws {
try await runCommand(.init(arguments: arguments, quiet: quiet, shell: shell)) try await logginOptions.withLogger {
let arguments = try await makeArguments()
try await self.run(
quiet: quiet,
shell: shell,
arguments
)
}
} }
/// Runs a shell command. /// Runs a shell command.
public func run( public func run(
quiet: Bool, quiet: Bool = false,
shell: ShellCommand.Shell, shell: String? = nil,
in workingDirectory: String? = nil,
_ arguments: [String]
) async throws {
try await runCommand(.init(
arguments: arguments,
quiet: quiet,
shell: shell,
workingDirectory: workingDirectory
))
}
/// Runs a shell command.
public func run(
quiet: Bool = false,
shell: String? = nil,
in workingDirectory: String? = nil,
_ arguments: String... _ arguments: String...
) async throws { ) async throws {
try await run(quiet: quiet, shell: shell, arguments) try await run(
quiet: quiet,
shell: shell,
in: workingDirectory,
arguments
)
} }
public struct RunCommandOptions: Sendable, Equatable { public struct RunCommandOptions: Sendable, Equatable {
public let arguments: [String] public let arguments: [String]
public let quiet: Bool public let quiet: Bool
public let shell: ShellCommand.Shell public let shell: String?
public let workingDirectory: String?
public init( public init(
arguments: [String], arguments: [String],
quiet: Bool, quiet: Bool,
shell: ShellCommand.Shell shell: String? = nil,
workingDirectory: String? = nil
) { ) {
self.arguments = arguments self.arguments = arguments
self.quiet = quiet self.quiet = quiet
self.shell = shell self.shell = shell
self.workingDirectory = workingDirectory
}
}
}
public struct LoggingOptions: Equatable, Sendable {
public let commandName: String
public let logLevel: Logger.Level
public init(commandName: String, logLevel: Logger.Level) {
self.commandName = commandName
self.logLevel = logLevel
}
@discardableResult
public func withLogger<T>(
operation: @Sendable @escaping () async throws -> T
) async rethrows -> T {
try await withDependencies {
$0.logger = .init(label: "\(Constants.executableName)")
$0.logger.logLevel = logLevel
$0.logger[metadataKey: "command"] = "\(commandName.blue)"
} operation: {
try await operation()
} }
} }
} }
@@ -57,26 +115,32 @@ extension CommandClient: DependencyKey {
public static let testValue: CommandClient = Self() public static let testValue: CommandClient = Self()
public static var liveValue: CommandClient { public static func live(
environment: [String: String]
) -> CommandClient {
.init { options in .init { options in
@Dependency(\.asyncShellClient) var shellClient @Dependency(\.asyncShellClient) var shellClient
if !options.quiet { if !options.quiet {
try await shellClient.foreground(.init( try await shellClient.foreground(.init(
shell: options.shell, shell: .init(options.shell),
environment: ProcessInfo.processInfo.environment, environment: environment,
in: nil, in: options.workingDirectory,
options.arguments options.arguments
)) ))
} else { } else {
try await shellClient.background(.init( try await shellClient.background(.init(
shell: options.shell, shell: .init(options.shell),
environment: ProcessInfo.processInfo.environment, environment: environment,
in: nil, in: options.workingDirectory,
options.arguments options.arguments
)) ))
} }
} }
} }
public static var liveValue: CommandClient {
.live(environment: ProcessInfo.processInfo.environment)
}
} }
@_spi(Internal) @_spi(Internal)
@@ -93,21 +157,32 @@ public extension CommandClient {
/// Captures the arguments / options passed into the command client's run commands. /// Captures the arguments / options passed into the command client's run commands.
/// ///
@dynamicMemberLookup
actor CapturingClient: Sendable { actor CapturingClient: Sendable {
public private(set) var quiet: Bool? public private(set) var options: RunCommandOptions?
public private(set) var shell: ShellCommand.Shell?
public private(set) var arguments: [String]?
public init() {} public init() {}
public func set( public func set(
_ options: RunCommandOptions _ options: RunCommandOptions
) { ) {
quiet = options.quiet self.options = options
shell = options.shell }
arguments = options.arguments
public subscript<T>(dynamicMember keyPath: KeyPath<RunCommandOptions, T>) -> T? {
options?[keyPath: keyPath]
} }
} }
} }
extension ShellCommand.Shell: @retroactive @unchecked Sendable {} extension ShellCommand.Shell: @retroactive @unchecked Sendable {}
extension ShellCommand.Shell {
init(_ path: String?) {
if let path {
self = .custom(path: path, useDashC: true)
} else {
self = .zsh(useDashC: true)
}
}
}

View File

@@ -1,27 +0,0 @@
import Constants
import Dependencies
import Foundation
import ShellClient
public struct LoggingOptions: Equatable, Sendable {
public let commandName: String
public let logLevel: Logger.Level
public init(commandName: String, logLevel: Logger.Level) {
self.commandName = commandName
self.logLevel = logLevel
}
@discardableResult
public func withLogger<T>(
operation: @Sendable @escaping () async throws -> T
) async rethrows -> T {
try await withDependencies {
$0.logger = .init(label: "\(Constants.executableName)")
$0.logger.logLevel = logLevel
$0.logger[metadataKey: "command"] = "\(commandName.blue)"
} operation: {
try await operation()
}
}
}

View File

@@ -0,0 +1,79 @@
import CommandClient
import ConfigurationClient
import Dependencies
import FileClient
import Foundation
import ShellClient
extension PlaybookClient.Repository {
static func findDirectory(
configuration: Configuration? = nil
) async throws -> String {
@Dependency(\.configurationClient) var configurationClient
var configuration: Configuration! = configuration
if configuration == nil {
configuration = try await configurationClient.findAndLoad()
}
return configuration.playbook?.directory
?? PlaybookClient.Constants.defaultInstallationPath
}
static func installPlaybook(
configuration: Configuration?
) async throws {
@Dependency(\.commandClient) var commandClient
@Dependency(\.configurationClient) var configurationClient
@Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger
var configuration: Configuration! = configuration
if configuration == nil {
configuration = try await configurationClient.findAndLoad()
}
let (path, version) = parsePlaybookPathAndVerion(
configuration?.playbook
)
let parentDirectory = URL(filePath: path)
.deletingLastPathComponent()
let parentExists = try await fileClient.isDirectory(parentDirectory)
if !parentExists {
try await fileClient.createDirectory(parentDirectory)
}
let playbookExists = try await fileClient.isDirectory(URL(filePath: path))
if !playbookExists {
logger.debug("Cloning playbook to: \(path)")
try await commandClient.run(
"git", "clone", "--branch", version,
PlaybookClient.Constants.playbookRepoUrl, path
)
} else {
logger.debug("Playbook exists, ensuring it's up to date.")
try await commandClient.run(
in: path,
"git", "pull", "--tags"
)
try await commandClient.run(
in: path,
"git", "checkout", version
)
}
}
static func parsePlaybookPathAndVerion(
_ configuration: Configuration.Playbook?
) -> (path: String, version: String) {
return (
path: configuration?.directory ?? PlaybookClient.Constants.defaultInstallationPath,
version: configuration?.version ?? PlaybookClient.Constants.playbookRepoVersion
)
}
}

View File

@@ -16,64 +16,40 @@ public extension DependencyValues {
@DependencyClient @DependencyClient
public struct PlaybookClient: Sendable { public struct PlaybookClient: Sendable {
public var repository: Repository
}
// TODO: Remove the configuration and have it passed in. public extension PlaybookClient {
public var installPlaybook: @Sendable (Configuration) async throws -> Void
public var playbookDirectory: @Sendable (Configuration) async throws -> String
@DependencyClient
struct Repository: Sendable {
public var install: @Sendable (Configuration?) async throws -> Void
public var directory: @Sendable (Configuration?) async throws -> String
public func install() async throws {
try await install(nil)
}
public func directory() async throws -> String {
try await directory(nil)
}
public static var liveValue: Self {
.init {
try await installPlaybook(configuration: $0)
} directory: {
try await findDirectory(configuration: $0)
}
}
}
} }
extension PlaybookClient: DependencyKey { extension PlaybookClient: DependencyKey {
public static let testValue: PlaybookClient = Self() public static let testValue: PlaybookClient = Self(repository: Repository())
public static var liveValue: PlaybookClient { public static var liveValue: PlaybookClient {
.init { .init(
try await install(config: $0.playbook) repository: .liveValue
} playbookDirectory: {
$0.playbook?.directory ?? Constants.defaultInstallationPath
}
}
}
private func install(config: Configuration.Playbook?) async throws {
@Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger
@Dependency(\.asyncShellClient) var shell
let (path, version) = parsePlaybookPathAndVerion(config)
let parentDirectory = URL(filePath: path)
.deletingLastPathComponent()
let parentExists = try await fileClient.isDirectory(parentDirectory)
if !parentExists {
try await fileClient.createDirectory(parentDirectory)
}
let playbookExists = try await fileClient.isDirectory(URL(filePath: path))
if !playbookExists {
try await shell.foreground(.init([
"git", "clone",
"--branch", version,
PlaybookClient.Constants.playbookRepoUrl, path
]))
} else {
logger.debug("Playbook exists, ensuring it's up to date.")
try await shell.foreground(.init(
in: path,
["git", "pull", "--tags"]
))
try await shell.foreground(.init(
in: path,
["git", "checkout", version]
))
}
}
private func parsePlaybookPathAndVerion(_ configuration: Configuration.Playbook?) -> (path: String, version: String) {
return (
path: configuration?.directory ?? PlaybookClient.Constants.defaultInstallationPath,
version: configuration?.version ?? PlaybookClient.Constants.playbookRepoVersion
) )
} }
}