import Dependencies import DependenciesMacros import Foundation import ShellClient public extension DependencyValues { var cliClient: CliClient { get { self[CliClient.self] } set { self[CliClient.self] = newValue } } } @DependencyClient public struct CliClient: Sendable { public var decoder: @Sendable () -> JSONDecoder = { .init() } public var encoder: @Sendable () -> JSONEncoder = { .init() } public var loadConfiguration: @Sendable () throws -> Configuration public var runCommand: @Sendable ([String], Bool, ShellCommand.Shell) async throws -> Void public var createConfiguration: @Sendable (_ path: String, _ json: Bool) throws -> Void public var findVaultFileInCurrentDirectory: @Sendable () throws -> String public func runCommand( quiet: Bool, shell: ShellCommand.Shell, _ args: [String] ) async throws { try await runCommand(args, quiet, shell) } public func runCommand( quiet: Bool, shell: ShellCommand.Shell, _ args: String... ) async throws { try await runCommand(args, quiet, shell) } } extension CliClient: DependencyKey { public static func live( decoder: JSONDecoder = .init(), encoder: JSONEncoder = .init(), env: [String: String] ) -> Self { @Dependency(\.fileClient) var fileClient @Dependency(\.logger) var logger return .init { decoder } encoder: { encoder } loadConfiguration: { let url = try findConfigurationFiles(env: env) var env = env logger.trace("Loading configuration from: \(url)") try fileClient.loadFile(url, &env, decoder) return try .fromEnv(env) } runCommand: { args, quiet, shell in @Dependency(\.asyncShellClient) var shellClient if !quiet { try await shellClient.foreground(.init( shell: shell, environment: ProcessInfo.processInfo.environment, in: nil, args )) } try await shellClient.background(.init( shell: shell, environment: ProcessInfo.processInfo.environment, in: nil, args )) } createConfiguration: { path, json in // Early out if a file exists at the path already. guard !fileClient.fileExists(path) else { throw CliClientError.fileExistsAtPath(path) } var path = path let data: Data if !json { // Write the default env template. data = Data(Configuration.fileTemplate.utf8) } else { if !path.contains(".json") { path += ".json" } data = try jsonEncoder.encode(Configuration.mock) } try fileClient.write(path, data) } findVaultFileInCurrentDirectory: { guard let url = try fileClient.findVaultFileInCurrentDirectory() else { throw CliClientError.vaultFileNotFound } return path(for: url) } } public static var liveValue: CliClient { .live(env: ProcessInfo.processInfo.environment) } public static let testValue: CliClient = Self() } enum CliClientError: Error { case fileExistsAtPath(String) case vaultFileNotFound } private let jsonEncoder: JSONEncoder = { var encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes] return encoder }()