diff --git a/Package.swift b/Package.swift index 21b23ca..d378ec5 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,8 @@ let package = Package( .executable(name: "hpa", targets: ["hpa"]), .library(name: "CliClient", targets: ["CliClient"]), .library(name: "CodersClient", targets: ["CodersClient"]), - .library(name: "ConfigurationClient", targets: ["ConfigurationClient"]) + .library(name: "ConfigurationClient", targets: ["ConfigurationClient"]), + .library(name: "FileClient", targets: ["FileClient"]) ], dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"), @@ -24,6 +25,7 @@ let package = Package( dependencies: [ "CliClient", "ConfigurationClient", + "FileClient", .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "CliDoc", package: "swift-cli-doc"), .product(name: "Dependencies", package: "swift-dependencies"), diff --git a/Sources/CliClient/CliClient.swift b/Sources/CliClient/CliClient.swift index de1146e..cb41972 100644 --- a/Sources/CliClient/CliClient.swift +++ b/Sources/CliClient/CliClient.swift @@ -13,7 +13,6 @@ public extension DependencyValues { @DependencyClient public struct CliClient: Sendable { public var runCommand: @Sendable ([String], Bool, ShellCommand.Shell) async throws -> Void - public var findVaultFileInCurrentDirectory: @Sendable () throws -> String public func runCommand( quiet: Bool, @@ -56,12 +55,6 @@ extension CliClient: DependencyKey { args )) } - } findVaultFileInCurrentDirectory: { - fatalError() -// guard let url = try fileClient.findVaultFileInCurrentDirectory() else { -// throw CliClientError.vaultFileNotFound -// } -// return path(for: url) } } diff --git a/Sources/CliClient/FileClient.swift b/Sources/CliClient/FileClient.swift deleted file mode 100644 index d7adcf5..0000000 --- a/Sources/CliClient/FileClient.swift +++ /dev/null @@ -1,183 +0,0 @@ -// import CodersClient -// import Dependencies -// import DependenciesMacros -// import Foundation -// import TOMLKit -// -// @_spi(Internal) -// public extension DependencyValues { -// var fileClient: FileClient { -// get { self[FileClient.self] } -// set { self[FileClient.self] = newValue } -// } -// } -// -// @_spi(Internal) -// @DependencyClient -// public struct FileClient: Sendable { -// -// /// Load a file as `Data`. -// public var loadFile: @Sendable (URL) throws -> Data -// -// /// Returns the user's home directory path. -// public var homeDir: @Sendable () -> URL = { URL(string: "~/")! } -// -// /// Check if a path is a directory. -// public var isDirectory: @Sendable (URL) -> Bool = { _ in false } -// -// /// Check if a path is a readable. -// public var isReadable: @Sendable (URL) -> Bool = { _ in false } -// -// /// Check if a file exists at the path. -// public var fileExists: @Sendable (String) -> Bool = { _ in false } -// -// public var findVaultFile: @Sendable (String) throws -> URL? -// -// /// Write data to a file. -// public var write: @Sendable (File, Data) throws -> Void -// -// public func findVaultFileInCurrentDirectory() throws -> URL? { -// try findVaultFile(".") -// } -// -// public func loadFile(_ file: File) throws -> Data { -// try loadFile(file.url) -// } -// -// public func loadConfiguration( -// _ file: File -// ) throws -> Configuration { -// @Dependency(\.coders) var coders -// -// let data = try loadFile(file) -// -// switch file { -// case .json: -// return try coders.jsonDecoder().decode(Configuration.self, from: data) -// case .toml: -// guard let string = String(data: data, encoding: .utf8) else { -// throw DecodingError() -// } -// return try coders.tomlDecoder().decode(Configuration.self, from: string) -// } -// } -// } -// -// @_spi(Internal) -// extension FileClient: DependencyKey { -// public static let testValue: FileClient = Self() -// -// public static func live(fileManager: FileManager = .default) -> Self { -// let client = LiveFileClient(fileManager: fileManager) -// return Self( -// loadFile: { try Data(contentsOf: $0) }, -// homeDir: { client.homeDir }, -// isDirectory: { client.isDirectory(url: $0) }, -// isReadable: { client.isReadable(url: $0) }, -// fileExists: { client.fileExists(at: $0) }, -// findVaultFile: { try client.findVaultFile(in: $0) }, -// write: { file, data in -// try data.write(to: file.url) -// } -// ) -// } -// -// public static let liveValue = Self.live() -// } -// -// private struct LiveFileClient: @unchecked Sendable { -// -// private let fileManager: FileManager -// -// init(fileManager: FileManager) { -// self.fileManager = fileManager -// } -// -// var homeDir: URL { fileManager.homeDirectoryForCurrentUser } -// -// func isDirectory(url: URL) -> Bool { -// var isDirectory: ObjCBool = false -// fileManager.fileExists(atPath: path(for: url), isDirectory: &isDirectory) -// return isDirectory.boolValue -// } -// -// func isReadable(url: URL) -> Bool { -// fileManager.isReadableFile(atPath: path(for: url)) -// } -// -// func fileExists(at path: String) -> Bool { -// fileManager.fileExists(atPath: path) -// } -// -// func findVaultFile(in filePath: String) throws -> URL? { -// let urls = try fileManager -// .contentsOfDirectory(at: URL(filePath: filePath), includingPropertiesForKeys: nil) -// -// if let vault = urls.firstVaultFile { -// return vault -// } -// -// // check for folders that end with "vars" and search those next. -// for folder in urls.filter({ $0.absoluteString.hasSuffix("vars/") }) { -// let files = try fileManager.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil) -// if let vault = files.firstVaultFile { -// return vault -// } -// } -// -// // Fallback to check all sub-folders -// for folder in urls.filter({ self.isDirectory(url: $0) && !$0.absoluteString.hasSuffix("vars/") }) { -// let files = try fileManager.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil) -// if let vault = files.firstVaultFile { -// return vault -// } -// } -// return nil -// } -// -// func loadFile( -// at url: URL, -// into env: inout [String: String], -// decoder: JSONDecoder -// ) throws -> Configuration? { -// @Dependency(\.logger) var logger -// logger.trace("Begin load file for: \(path(for: url))") -// -// if url.absoluteString.hasSuffix(".json") { -// // Handle json file. -// let data = try Data(contentsOf: url) -// return try decoder.decode(Configuration.self, from: data) -// } -// -// let string = try String(contentsOfFile: path(for: url), encoding: .utf8) -// -// logger.trace("Loaded file contents: \(string)") -// -// let lines = string.split(separator: "\n") -// for line in lines { -// logger.trace("Line: \(line)") -// let strippedLine = line.trimmingCharacters(in: .whitespacesAndNewlines) -// let splitLine = strippedLine.split(separator: "=").map { -// $0.replacingOccurrences(of: "\"", with: "") -// } -// logger.trace("Split Line: \(splitLine)") -// guard splitLine.count >= 2 else { continue } -// -// if splitLine.count > 2 { -// let rest = splitLine.dropFirst() -// env[String(splitLine[0])] = String(rest.joined(separator: "=")) -// } else { -// env[String(splitLine[0])] = String(splitLine[1]) -// } -// } -// return nil -// } -// } -// -// struct DecodingError: Error {} -// -// private extension Array where Element == URL { -// var firstVaultFile: URL? { -// first { $0.absoluteString.hasSuffix("vault.yml") } -// } -// } diff --git a/Sources/CliClient/Helpers.swift b/Sources/CliClient/Helpers.swift deleted file mode 100644 index a971985..0000000 --- a/Sources/CliClient/Helpers.swift +++ /dev/null @@ -1,116 +0,0 @@ -// import Dependencies -// import Foundation -// import ShellClient -// -// @_spi(Internal) -// public func findConfigurationFiles( -// env: [String: String] = ProcessInfo.processInfo.environment -// ) throws -> File { -// @Dependency(\.logger) var logger -// @Dependency(\.fileClient) var fileClient -// -// logger.debug("Begin find configuration files.") -// logger.trace("Env: \(env)") -// -// // Check for environment variable pointing to a directory that the -// // the configuration lives. -// if let pwd = env["PWD"], -// let file = fileClient.checkUrl(.directory(pwd)) -// { -// logger.debug("Found configuration in current working directory.") -// return file -// } -// -// // Check for environment variable pointing to a file that the -// // the configuration lives. -// if let configFile = env[EnvironmentKey.hpaConfigFileKey], -// let file = fileClient.checkUrl(.file(configFile)) -// { -// logger.debug("Found configuration from hpa config file env var.") -// return file -// } -// -// // Check for environment variable pointing to a directory that the -// // the configuration lives. -// if let configHome = env[EnvironmentKey.hpaConfigDirKey], -// let file = fileClient.checkUrl(.directory(configHome)) -// { -// logger.debug("Found configuration from hpa config home env var.") -// return file -// } -// -// // Check home directory for a `.hparc` file. -// if let file = fileClient.checkUrl(.directory(fileClient.homeDir())) { -// logger.debug("Found configuration in home directory") -// return file -// } -// -// // Check in xdg config home, under an hpa-playbook directory. -// if let xdgConfigHome = env[EnvironmentKey.xdgConfigHomeKey], -// let file = fileClient.checkUrl(.directory(xdgConfigHome, HPAKey.hpaConfigDirectoryName)) -// { -// logger.debug("XDG Config url: \(file.path)") -// return file -// } -// -// // We could not find configuration in any usual places. -// throw ConfigurationError.configurationNotFound -// } -// -// @_spi(Internal) -// public func path(for url: URL) -> String { -// url.absoluteString.replacing("file://", with: "") -// } -// -// enum ConfigurationError: Error { -// case configurationNotFound -// } -// -// private extension FileClient { -// -// static let validFileNames = [ -// "config.json", "config.toml", -// ".hparc.json", ".hparc.toml" -// ] -// -// enum ConfigurationUrlCheck { -// case file(URL) -// case directory(URL) -// -// static func file(_ path: String) -> Self { .file(URL(filePath: path)) } -// -// static func file(_ paths: String...) -> Self { -// var url = URL(filePath: paths[0]) -// url = paths.dropFirst().reduce(into: url) { $0.append(path: $1) } -// return .file(url) -// } -// -// static func directory(_ path: String) -> Self { .directory(URL(filePath: path)) } -// -// static func directory(_ paths: String...) -> Self { -// var url = URL(filePath: paths[0]) -// url = paths.dropFirst().reduce(into: url) { $0.append(path: $1) } -// return .directory(url) -// } -// } -// -// func checkUrl(_ check: ConfigurationUrlCheck) -> File? { -// switch check { -// case let .file(url): -// if isReadable(url) { return .init(url) } -// return nil -// case let .directory(url): -// return findConfigurationInDirectory(url) -// } -// } -// -// func findConfigurationInDirectory(_ url: URL) -> File? { -// for file in Self.validFileNames { -// let fileUrl = url.appending(path: file) -// if isReadable(fileUrl) { -// return .init(fileUrl) -// } -// } -// return nil -// } -// } diff --git a/Sources/ConfigurationClient/ConfigurationClient.swift b/Sources/ConfigurationClient/ConfigurationClient.swift index aa325cf..fd63899 100644 --- a/Sources/ConfigurationClient/ConfigurationClient.swift +++ b/Sources/ConfigurationClient/ConfigurationClient.swift @@ -6,7 +6,7 @@ import Foundation import ShellClient public extension DependencyValues { - var configuration: ConfigurationClient { + var configurationClient: ConfigurationClient { get { self[ConfigurationClient.self] } set { self[ConfigurationClient.self] = newValue } } diff --git a/Sources/FileClient/FileClient.swift b/Sources/FileClient/FileClient.swift index 0d98ed0..64152f0 100644 --- a/Sources/FileClient/FileClient.swift +++ b/Sources/FileClient/FileClient.swift @@ -13,6 +13,7 @@ public extension DependencyValues { public struct FileClient: Sendable { public var copy: @Sendable (URL, URL) async throws -> Void public var fileExists: @Sendable (URL) -> Bool = { _ in true } + public var findVaultFileInCurrentDirectory: @Sendable () async throws -> URL? public var homeDirectory: @Sendable () -> URL = { URL(filePath: "~/") } public var load: @Sendable (URL) async throws -> Data public var write: @Sendable (Data, URL) async throws -> Void @@ -27,6 +28,8 @@ extension FileClient: DependencyKey { try await manager.copy($0, to: $1) } fileExists: { url in manager.fileExists(at: url) + } findVaultFileInCurrentDirectory: { + try await manager.findVaultFileInCurrentDirectory() } homeDirectory: { manager.homeDirectory() } load: { url in @@ -46,13 +49,41 @@ struct LiveFileClient: Sendable { } func fileExists(at url: URL) -> Bool { - manager.fileExists(atPath: url.absoluteString.replacing("file://", with: "")) + manager.fileExists(atPath: url.cleanFilePath) + } + + func findVaultFileInCurrentDirectory() async throws -> URL? { + let urls = try manager.contentsOfDirectory( + at: URL(filePath: "./"), + includingPropertiesForKeys: nil + ) + + // Check the current directory + if let vault = urls.firstVaultFile { return vault } + + let subfolders = urls.filter { isDirectory($0) } + + for folder in subfolders { + let files = try manager.contentsOfDirectory( + at: folder, + includingPropertiesForKeys: nil + ) + if let vault = files.firstVaultFile { return vault } + } + + return nil } func homeDirectory() -> URL { manager.homeDirectoryForCurrentUser } + private func isDirectory(_ url: URL) -> Bool { + var isDirectory: ObjCBool = false + manager.fileExists(atPath: url.cleanFilePath, isDirectory: &isDirectory) + return isDirectory.boolValue + } + func load(from url: URL) async throws -> Data { try Data(contentsOf: url) } @@ -61,3 +92,18 @@ struct LiveFileClient: Sendable { try data.write(to: url) } } + +private extension Array where Element == URL { + var firstVaultFile: URL? { + first { url in + let string = url.absoluteString + return string.hasSuffix("vault.yml") || string.hasSuffix("vault.yaml") + } + } +} + +public extension URL { + var cleanFilePath: String { + absoluteString.replacing("file://", with: "") + } +} diff --git a/Sources/hpa/CreateCommand.swift b/Sources/hpa/CreateCommand.swift index 549d4c6..ce36657 100644 --- a/Sources/hpa/CreateCommand.swift +++ b/Sources/hpa/CreateCommand.swift @@ -64,7 +64,7 @@ struct CreateCommand: AsyncParsableCommand { try await withSetupLogger(commandName: Self.commandName, globals: globals) { @Dependency(\.coders) var coders @Dependency(\.cliClient) var cliClient - @Dependency(\.configuration) var configurationClient + @Dependency(\.configurationClient) var configurationClient @Dependency(\.logger) var logger let encoder = coders.jsonEncoder() diff --git a/Sources/hpa/Internal/RunPlaybook.swift b/Sources/hpa/Internal/RunPlaybook.swift index 00dc61d..d3398ea 100644 --- a/Sources/hpa/Internal/RunPlaybook.swift +++ b/Sources/hpa/Internal/RunPlaybook.swift @@ -16,7 +16,7 @@ func runPlaybook( ) async throws { try await withSetupLogger(commandName: commandName, globals: globals) { @Dependency(\.cliClient) var cliClient - @Dependency(\.configuration) var configurationClient + @Dependency(\.configurationClient) var configurationClient @Dependency(\.logger) var logger logger.debug("Begin run playbook: \(globals)") diff --git a/Sources/hpa/Internal/RunVault.swift b/Sources/hpa/Internal/RunVault.swift index 74510b8..c2acef8 100644 --- a/Sources/hpa/Internal/RunVault.swift +++ b/Sources/hpa/Internal/RunVault.swift @@ -1,5 +1,6 @@ import ConfigurationClient import Dependencies +import FileClient import ShellClient func runVault( @@ -9,7 +10,8 @@ func runVault( ) async throws { try await withSetupLogger(commandName: commandName, globals: options.globals) { @Dependency(\.cliClient) var cliClient - @Dependency(\.configuration) var configurationClient + @Dependency(\.configurationClient) var configurationClient + @Dependency(\.fileClient) var fileClient @Dependency(\.logger) var logger logger.debug("Begin run vault: \(options)") @@ -21,7 +23,10 @@ func runVault( if let file = options.file { path = file } else { - path = try cliClient.findVaultFileInCurrentDirectory() + guard let url = try await fileClient.findVaultFileInCurrentDirectory() else { + throw VaultFileNotFound() + } + path = url.cleanFilePath } logger.debug("Vault path: \(path)") @@ -48,3 +53,5 @@ func runVault( ) } } + +struct VaultFileNotFound: Error {}