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

@@ -47,7 +47,7 @@ public extension CliClient {
)
let configuration = try await configurationClient.findAndLoad()
try await playbookClient.installPlaybook(configuration)
try await playbookClient.repository.install(configuration)
}
func runPlaybookCommand(
@@ -62,7 +62,7 @@ public extension CliClient {
let configuration = try await configurationClient.ensuredConfiguration(options.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)"
logger.trace("Playbook path: \(playbookPath)")

View File

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

View File

@@ -1,6 +1,8 @@
import Constants
import Dependencies
import DependenciesMacros
import Foundation
import LoggingExtensions
import ShellClient
public extension DependencyValues {
@@ -18,37 +20,93 @@ public struct CommandClient: Sendable {
/// Runs a shell command.
public var runCommand: @Sendable (RunCommandOptions) async throws -> Void
/// Runs a shell command.
/// Runs a shell command and sets up logging.
public func run(
quiet: Bool,
shell: ShellCommand.Shell,
_ arguments: [String]
logging logginOptions: LoggingOptions,
quiet: Bool = false,
shell: String? = nil,
in workingDirectory: String? = nil,
makeArguments: @Sendable @escaping () async throws -> [String]
) 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.
public func run(
quiet: Bool,
shell: ShellCommand.Shell,
quiet: Bool = false,
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...
) 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 let arguments: [String]
public let quiet: Bool
public let shell: ShellCommand.Shell
public let shell: String?
public let workingDirectory: String?
public init(
arguments: [String],
quiet: Bool,
shell: ShellCommand.Shell
shell: String? = nil,
workingDirectory: String? = nil
) {
self.arguments = arguments
self.quiet = quiet
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 var liveValue: CommandClient {
public static func live(
environment: [String: String]
) -> CommandClient {
.init { options in
@Dependency(\.asyncShellClient) var shellClient
if !options.quiet {
try await shellClient.foreground(.init(
shell: options.shell,
environment: ProcessInfo.processInfo.environment,
in: nil,
shell: .init(options.shell),
environment: environment,
in: options.workingDirectory,
options.arguments
))
} else {
try await shellClient.background(.init(
shell: options.shell,
environment: ProcessInfo.processInfo.environment,
in: nil,
shell: .init(options.shell),
environment: environment,
in: options.workingDirectory,
options.arguments
))
}
}
}
public static var liveValue: CommandClient {
.live(environment: ProcessInfo.processInfo.environment)
}
}
@_spi(Internal)
@@ -93,21 +157,32 @@ public extension CommandClient {
/// Captures the arguments / options passed into the command client's run commands.
///
@dynamicMemberLookup
actor CapturingClient: Sendable {
public private(set) var quiet: Bool?
public private(set) var shell: ShellCommand.Shell?
public private(set) var arguments: [String]?
public private(set) var options: RunCommandOptions?
public init() {}
public func set(
_ options: RunCommandOptions
) {
quiet = options.quiet
shell = options.shell
arguments = options.arguments
self.options = options
}
public subscript<T>(dynamicMember keyPath: KeyPath<RunCommandOptions, T>) -> T? {
options?[keyPath: keyPath]
}
}
}
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
public struct PlaybookClient: Sendable {
// TODO: Remove the configuration and have it passed in.
public var installPlaybook: @Sendable (Configuration) async throws -> Void
public var playbookDirectory: @Sendable (Configuration) async throws -> String
public var repository: Repository
}
extension PlaybookClient: DependencyKey {
public static let testValue: PlaybookClient = Self()
public extension PlaybookClient {
public static var liveValue: PlaybookClient {
.init {
try await install(config: $0.playbook)
} playbookDirectory: {
$0.playbook?.directory ?? Constants.defaultInstallationPath
@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)
}
}
}
}
private func install(config: Configuration.Playbook?) async throws {
@Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger
@Dependency(\.asyncShellClient) var shell
extension PlaybookClient: DependencyKey {
public static let testValue: PlaybookClient = Self(repository: Repository())
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]
))
public static var liveValue: PlaybookClient {
.init(
repository: .liveValue
)
}
}
private func parsePlaybookPathAndVerion(_ configuration: Configuration.Playbook?) -> (path: String, version: String) {
return (
path: configuration?.directory ?? PlaybookClient.Constants.defaultInstallationPath,
version: configuration?.version ?? PlaybookClient.Constants.playbookRepoVersion
)
}