import ConfigurationClient import Dependencies import DependenciesMacros import FileClient import Foundation import ShellClient public extension CliClient { func runCommand( quiet: Bool, shell: ShellCommand.Shell, _ args: [String] ) async throws { try await runCommand(.init(arguments: args, quiet: quiet, shell: shell)) } func runCommand( quiet: Bool, shell: ShellCommand.Shell, _ args: String... ) async throws { try await runCommand(quiet: quiet, shell: shell, args) } func installDependencies( quiet: Bool = false, shell: String? = nil, extraArgs: [String]? = nil ) async throws { guard let url = Bundle.module.url(forResource: "Brewfile", withExtension: nil) else { throw CliClientError.brewfileNotFound } var arguments = [ "brew", "bundle", "--file", url.cleanFilePath ] if let extraArgs { arguments.append(contentsOf: extraArgs) } try await runCommand( quiet: quiet, shell: shell.orDefault, arguments ) } func runPlaybookCommand( _ options: PlaybookOptions, logging loggingOptions: LoggingOptions ) async throws { try await withLogger(loggingOptions) { @Dependency(\.configurationClient) var configurationClient @Dependency(\.logger) var logger let configuration = try await configurationClient.ensuredConfiguration(options.configuration) logger.trace("Configuration: \(configuration)") let playbookDirectory = try configuration.ensuredPlaybookDirectory(options.playbookDirectory) let playbookPath = "\(playbookDirectory)/\(Constants.playbookFileName)" logger.trace("Playbook path: \(playbookPath)") let inventoryPath = ensuredInventoryPath( options.inventoryFilePath, configuration: configuration, playbookDirectory: playbookDirectory ) logger.trace("Inventory path: \(inventoryPath)") var arguments = [ Constants.playbookCommand, playbookPath, "--inventory", inventoryPath ] + options.arguments if let defaultArgs = configuration.args { arguments.append(contentsOf: defaultArgs) } if configuration.useVaultArgs, let vaultArgs = configuration.vault.args { arguments.append(contentsOf: vaultArgs) } logger.trace("Running playbook command with arguments: \(arguments)") try await runCommand( quiet: options.quiet, shell: options.shell.orDefault, arguments ) } } func runVaultCommand( _ options: VaultOptions, logging loggingOptions: LoggingOptions ) async throws { try await withLogger(loggingOptions) { @Dependency(\.configurationClient) var configurationClient @Dependency(\.fileClient) var fileClient @Dependency(\.logger) var logger let configuration = try await configurationClient.ensuredConfiguration(options.configuration) logger.trace("Configuration: \(configuration)") let vaultFilePath = try await fileClient.ensuredVaultFilePath(options.vaultFilePath) logger.trace("Vault file: \(vaultFilePath)") var arguments = [ Constants.vaultCommand ] + options.arguments if let defaultArgs = configuration.vault.args { arguments.append(contentsOf: defaultArgs) } if arguments.contains("encrypt"), !arguments.contains("--encrypt-vault-id"), let id = configuration.vault.encryptId { arguments.append(contentsOf: ["--encrypt-vault-id", id]) } arguments.append(vaultFilePath) logger.trace("Running vault command with arguments: \(arguments)") try await runCommand( quiet: options.quiet, shell: options.shell.orDefault, arguments ) } } } @_spi(Internal) public extension ConfigurationClient { func ensuredConfiguration(_ optionalConfig: Configuration?) async throws -> Configuration { guard let config = optionalConfig else { return try await findAndLoad() } return config } } @_spi(Internal) public extension Configuration { func defaultPlaybookDirectory() throws -> String { let playbookDirectory = Bundle.module.url( forResource: Constants.playbookBundleDirectoryName, withExtension: nil ) guard let playbookDirectory else { throw CliClientError.playbookDirectoryNotFound } return playbookDirectory.cleanFilePath } func ensuredPlaybookDirectory(_ optionalDirectory: String?) throws -> String { guard let directory = optionalDirectory else { guard let directory = playbook?.directory else { return try defaultPlaybookDirectory() } return directory } return directory } } @_spi(Internal) public extension Optional where Wrapped == String { var orDefault: ShellCommand.Shell { guard let shell = self else { return .zsh(useDashC: true) } return .custom(path: shell, useDashC: true) } } @_spi(Internal) public func ensuredInventoryPath( _ optionalInventoryPath: String?, configuration: Configuration, playbookDirectory: String ) -> String { guard let path = optionalInventoryPath else { guard let path = configuration.playbook?.inventory else { return "\(playbookDirectory)/\(Constants.inventoryFileName)" } return path } return path } @_spi(Internal) public extension FileClient { func ensuredVaultFilePath(_ optionalPath: String?) async throws -> String { guard let path = optionalPath else { guard let url = try await findVaultFileInCurrentDirectory() else { throw CliClientError.vaultFileNotFound } return url.cleanFilePath } return path } } extension ShellCommand.Shell: @retroactive @unchecked Sendable {}