diff --git a/Sources/CliClient/CliClient.swift b/Sources/CliClient/CliClient.swift index c7dda6b..3a00939 100644 --- a/Sources/CliClient/CliClient.swift +++ b/Sources/CliClient/CliClient.swift @@ -17,6 +17,7 @@ public struct CliClient: Sendable { 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, @@ -95,6 +96,11 @@ extension CliClient: DependencyKey { } try fileClient.write(path, data) + } findVaultFileInCurrentDirectory: { + guard let url = try fileClient.findVaultFileInCurrentDirectory() else { + throw CliClientError.vaultFileNotFound + } + return path(for: url) } } @@ -107,6 +113,7 @@ extension CliClient: DependencyKey { enum CliClientError: Error { case fileExistsAtPath(String) + case vaultFileNotFound } private let jsonEncoder: JSONEncoder = { diff --git a/Sources/CliClient/Configuration.swift b/Sources/CliClient/Configuration.swift index 76ee76f..f466505 100644 --- a/Sources/CliClient/Configuration.swift +++ b/Sources/CliClient/Configuration.swift @@ -3,7 +3,7 @@ import Foundation import ShellClient /// Represents the configuration. -public struct Configuration: Codable { +public struct Configuration: Codable, Sendable { public let playbookDir: String? public let inventoryPath: String? @@ -11,6 +11,7 @@ public struct Configuration: Codable { public let templateRepoVersion: String? public let templateDir: String? public let defaultPlaybookArgs: [String]? + public let defaultVaultArgs: [String]? fileprivate enum CodingKeys: String, CodingKey { case playbookDir = "HPA_PLAYBOOK_DIR" @@ -19,6 +20,7 @@ public struct Configuration: Codable { case templateRepoVersion = "HPA_TEMPLATE_VERSION" case templateDir = "HPA_TEMPLATE_DIR" case defaultPlaybookArgs = "HPA_DEFAULT_PLAYBOOK_ARGS" + case defaultVaultArgs = "HPA_DEFAULT_VAULT_ARGS" } public static func fromEnv( @@ -31,16 +33,14 @@ public struct Configuration: Codable { let hpaValues: [String: String] = env.filter { $0.key.contains("HPA") } logger.debug("HPA env vars: \(hpaValues)") - let defaultArgs: [String]? = hpaValues.value(key: .defaultPlaybookArgs) - .flatMap { $0.split(separator: ",").map(String.init) } - return Configuration( - playbookDir: hpaValues.value(key: .playbookDir), - inventoryPath: hpaValues.value(key: .inventoryPath), - templateRepo: hpaValues.value(key: .templateRepo), - templateRepoVersion: hpaValues.value(key: .templateRepoVersion), - templateDir: hpaValues.value(key: .templateDir), - defaultPlaybookArgs: defaultArgs + playbookDir: hpaValues.value(for: .playbookDir), + inventoryPath: hpaValues.value(for: .inventoryPath), + templateRepo: hpaValues.value(for: .templateRepo), + templateRepoVersion: hpaValues.value(for: .templateRepoVersion), + templateDir: hpaValues.value(for: .templateDir), + defaultPlaybookArgs: hpaValues.array(for: .defaultPlaybookArgs), + defaultVaultArgs: hpaValues.array(for: .defaultVaultArgs) ) } @@ -51,7 +51,8 @@ public struct Configuration: Codable { templateRepo: "https://git.example.com/consult-template.git", templateRepoVersion: "main", templateDir: "/path/to/local/template", - defaultPlaybookArgs: ["--vault-id=myId@$SCRIPTS/vault-gopass-client"] + defaultPlaybookArgs: ["--tags", "debug"], + defaultVaultArgs: ["--vault-id=myId@$SCRIPTS/vault-gopass-client"] ) } @@ -82,7 +83,11 @@ public struct Configuration: Codable { } extension [String: String] { - fileprivate func value(key codingKey: Configuration.CodingKeys) -> String? { + fileprivate func value(for codingKey: Configuration.CodingKeys) -> String? { self[codingKey.rawValue] } + + fileprivate func array(for codingKey: Configuration.CodingKeys) -> [String]? { + value(for: codingKey).flatMap { $0.split(separator: ",").map(String.init) } + } } diff --git a/Sources/CliClient/FileClient.swift b/Sources/CliClient/FileClient.swift index 8bacb1e..cb5139e 100644 --- a/Sources/CliClient/FileClient.swift +++ b/Sources/CliClient/FileClient.swift @@ -18,6 +18,7 @@ public struct FileClient: Sendable { public var isDirectory: @Sendable (URL) -> Bool = { _ in false } public var isReadable: @Sendable (URL) -> Bool = { _ in false } public var fileExists: @Sendable (String) -> Bool = { _ in false } + public var findVaultFileInCurrentDirectory: @Sendable () throws -> URL? public var write: @Sendable (String, Data) throws -> Void } @@ -33,6 +34,7 @@ extension FileClient: DependencyKey { isDirectory: { client.isDirectory(url: $0) }, isReadable: { client.isReadable(url: $0) }, fileExists: { client.fileExists(at: $0) }, + findVaultFileInCurrentDirectory: { try client.findVaultFileInCurrentDirectory() }, write: { path, data in try data.write(to: URL(filePath: path)) } @@ -66,6 +68,31 @@ private struct LiveFileClient: @unchecked Sendable { fileManager.fileExists(atPath: path) } + func findVaultFileInCurrentDirectory() throws -> URL? { + let urls = try fileManager.contentsOfDirectory(at: URL(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 { @Dependency(\.logger) var logger logger.trace("Begin load file for: \(path(for: url))") @@ -101,3 +128,9 @@ private struct LiveFileClient: @unchecked Sendable { } } } + +private extension Array where Element == URL { + var firstVaultFile: URL? { + first { $0.absoluteString.hasSuffix("vault.yml") } + } +} diff --git a/Sources/hpa/Application.swift b/Sources/hpa/Application.swift index 7bbbe62..ba17e65 100644 --- a/Sources/hpa/Application.swift +++ b/Sources/hpa/Application.swift @@ -7,10 +7,10 @@ import ShellClient struct Application: AsyncParsableCommand { static let configuration = CommandConfiguration( - commandName: "hpa", - abstract: "\("A utility for working with ansible hpa playbook.".blue)", + commandName: Constants.appName, + abstract: createAbstract("A utility for working with ansible hpa playbook."), subcommands: [ - BuildCommand.self, CreateCommand.self, UtilsCommand.self + BuildCommand.self, CreateCommand.self, VaultCommand.self, UtilsCommand.self ] ) diff --git a/Sources/hpa/BuildCommand.swift b/Sources/hpa/BuildCommand.swift index e33f05c..e278848 100644 --- a/Sources/hpa/BuildCommand.swift +++ b/Sources/hpa/BuildCommand.swift @@ -6,7 +6,7 @@ struct BuildCommand: AsyncParsableCommand { static let commandName = "build" - static let configuration = CommandConfiguration.playbookCommandConfiguration( + static let configuration = CommandConfiguration.playbook( commandName: commandName, abstract: "Build a home performance assesment project.", examples: (label: "Build Project", example: "\(commandName) /path/to/project") diff --git a/Sources/hpa/CreateCommand.swift b/Sources/hpa/CreateCommand.swift index 8adf4b0..638b984 100644 --- a/Sources/hpa/CreateCommand.swift +++ b/Sources/hpa/CreateCommand.swift @@ -8,7 +8,7 @@ struct CreateCommand: AsyncParsableCommand { static let commandName = "create" - static let configuration = CommandConfiguration.playbookCommandConfiguration( + static let configuration = CommandConfiguration.playbook( commandName: commandName, abstract: "Create a home performance assesment project.", examples: ( diff --git a/Sources/hpa/Helpers/CommandConfigurationExtensions.swift b/Sources/hpa/Helpers/CommandConfigurationExtensions.swift deleted file mode 100644 index f76ba6d..0000000 --- a/Sources/hpa/Helpers/CommandConfigurationExtensions.swift +++ /dev/null @@ -1,35 +0,0 @@ -import ArgumentParser - -extension CommandConfiguration { - static func playbookCommandConfiguration( - commandName: String, - abstract: String, - examples: (label: String, example: String)... - ) -> Self { - guard examples.count > 0 else { - fatalError("Did not supply any examples.") - } - return Self( - commandName: commandName, - abstract: "\(abstract.blue)", - discussion: """ - \("NOTE:".yellow) Most options are not required if you have a configuration file setup. - - \("Examples:".yellow) - - \(examples.map { "\($0.label.green.italic)\n $ hpa \($0.example)" }.joined(separator: "\n")) - - \("Passing extra args to the playbook.".green.italic) - $ hpa \(examples[0].example) -- --vault-id "myId@$SCRIPTS/vault-gopass-client" - - \("See Also:".yellow) \("Ansible playbook options.".italic) - - $ ansible-playbook --help - - \("IMPORTANT NOTE:".red) Any extra arguments to pass to the playbook invocation have to - be at the end with `--` before any arguments otherwise there will - be an "Unkown option" error. See examples above. - """ - ) - } -} diff --git a/Sources/hpa/Internal/CommandConfigurationExtensions.swift b/Sources/hpa/Internal/CommandConfigurationExtensions.swift new file mode 100644 index 0000000..35b1ecc --- /dev/null +++ b/Sources/hpa/Internal/CommandConfigurationExtensions.swift @@ -0,0 +1,108 @@ +import ArgumentParser +import Rainbow + +extension CommandConfiguration { + + static func create( + commandName: String, + abstract: String, + usesExtraArgs: Bool = true, + discussion nodes: [Node] + ) -> Self { + .init( + commandName: commandName, + abstract: createAbstract(abstract), + discussion: Discussion(nodes: nodes, usesExtraArgs: usesExtraArgs).render() + ) + } + + static func playbook( + commandName: String, + abstract: String, + parentCommand: String? = nil, + examples: (label: String, example: String)... + ) -> Self { + .create( + commandName: commandName, + abstract: abstract, + discussion: [.note(label: "Most options are not required if you have a configuration file setup.")] + + examples.nodes(parentCommand) + + [.seeAlso(label: "Ansible playbook options.", command: "ansible-playbook")] + ) + } +} + +func createAbstract(_ string: String) -> String { + "\(string.blue)" +} + +struct Discussion { + var nodes: [Node] + + init(usesExtraArgs: Bool = true, _ nodes: Node...) { + self.init(nodes: nodes, usesExtraArgs: usesExtraArgs) + } + + init(nodes: [Node], usesExtraArgs: Bool = true) { + var nodes = nodes + + if let firstExampleIndex = nodes.firstIndex(where: \.isExampleNode) { + nodes.insert(.exampleHeading, at: firstExampleIndex) + } + + if usesExtraArgs { + if let lastExampleIndex = nodes.lastIndex(where: \.isExampleNode) { + let last = nodes[lastExampleIndex] + if case let .example(_, example, parent) = last { + nodes.insert( + .example(label: "Passing extra args.", example: example, parentCommand: parent), + at: nodes.index(after: lastExampleIndex) + ) + } + } + } + self.nodes = nodes + } + + func render() -> String { + nodes.map { $0.render() }.joined(separator: "\n") + } +} + +enum Node: Equatable { + case note(header: String = "NOTE:", label: String, appendNewLine: Bool = true) + case example(label: String, example: String, parentCommand: String?) + case seeAlso(label: String, command: String, appendHelpToCommand: Bool = true) + + static var exampleHeading: Self { .note(header: "Examples:", label: "Some common examples.", appendNewLine: false) } + + var isExampleNode: Bool { + if case .example = self { return true } + return false + } + + func render() -> String { + switch self { + case let .note(header, note, appendNewLine): + return "\(header.yellow) \(note.italic)\(appendNewLine ? "\n" : "")" + case let .example(label: label, example: example, parentCommand: parent): + return """ + \(label.green.italic) + $ \(Constants.appName) \(parent ?? "")\(parent != nil ? " \(example)" : example) + + """ + case let .seeAlso(label: label, command: command, appendHelpToCommand: appendHelp): + return """ + \("See Also:".yellow) \(label.italic) + $ \(command)\(appendHelp ? " --help" : "") + + """ + } + } +} + +private extension Array where Element == (label: String, example: String) { + func nodes(_ parentCommand: String?) -> [Node] { + map { .example(label: $0.label, example: $0.example, parentCommand: parentCommand) } + } +} diff --git a/Sources/hpa/Internal/Constants.swift b/Sources/hpa/Internal/Constants.swift new file mode 100644 index 0000000..7976bd9 --- /dev/null +++ b/Sources/hpa/Internal/Constants.swift @@ -0,0 +1,17 @@ +import Rainbow + +// Constant string values. +enum Constants { + static let appName = "hpa" + static let playbookFileName = "main.yml" + static let inventoryFileName = "inventory.ini" + static let importantExtraArgsNote = """ + + \("IMPORTANT NOTE".bold.underline): \(""" + Any extra arguments to pass to the underlying command invocation have to + be at the end with `--` before any arguments otherwise there will + be an "Unkown option" error. See examples above. + """.italic + ) + """.red +} diff --git a/Sources/hpa/GlobalOptions.swift b/Sources/hpa/Internal/GlobalOptions.swift similarity index 90% rename from Sources/hpa/GlobalOptions.swift rename to Sources/hpa/Internal/GlobalOptions.swift index a830180..c33a253 100644 --- a/Sources/hpa/GlobalOptions.swift +++ b/Sources/hpa/Internal/GlobalOptions.swift @@ -50,4 +50,8 @@ struct GlobalOptions: ParsableArguments { set { basic[keyPath: keyPath] = newValue } } + subscript(dynamicMember keyPath: KeyPath) -> T { + basic[keyPath: keyPath] + } + } diff --git a/Sources/hpa/Internal/LoggingExtensions.swift b/Sources/hpa/Internal/LoggingExtensions.swift new file mode 100644 index 0000000..6706cf7 --- /dev/null +++ b/Sources/hpa/Internal/LoggingExtensions.swift @@ -0,0 +1,53 @@ +import Dependencies +import Logging + +extension Logger.Level { + + /// Set the log level based on the user's options supplied. + init(globals: BasicGlobalOptions, quietOnlyPlaybook: Bool) { + if quietOnlyPlaybook || !globals.quiet { + switch globals.verbose { + case 0: + self = .info + case 1: + self = .debug + case 2...: + self = .trace + default: + self = .info + } + } + self = .info + } +} + +func withSetupLogger( + commandName: String, + globals: BasicGlobalOptions, + quietOnlyPlaybook: Bool = false, + dependencies setupDependencies: (inout DependencyValues) -> Void = { _ in }, + operation: @escaping () async throws -> Void +) async rethrows { + try await withDependencies { + $0.logger = .init(label: "\("hpa".yellow)") + $0.logger.logLevel = .init(globals: globals, quietOnlyPlaybook: quietOnlyPlaybook) + $0.logger[metadataKey: "command"] = "\(commandName.blue)" + } operation: { + try await operation() + } +} + +func withSetupLogger( + commandName: String, + globals: GlobalOptions, + dependencies setupDependencies: (inout DependencyValues) -> Void = { _ in }, + operation: @escaping () async throws -> Void +) async rethrows { + try await withSetupLogger( + commandName: commandName, + globals: globals.basic, + quietOnlyPlaybook: globals.quietOnlyPlaybook, + dependencies: setupDependencies, + operation: operation + ) +} diff --git a/Sources/hpa/Helpers/Helpers.swift b/Sources/hpa/Internal/RunPlaybook.swift similarity index 61% rename from Sources/hpa/Helpers/Helpers.swift rename to Sources/hpa/Internal/RunPlaybook.swift index 118b738..ccdfce7 100644 --- a/Sources/hpa/Helpers/Helpers.swift +++ b/Sources/hpa/Internal/RunPlaybook.swift @@ -6,101 +6,6 @@ import Logging import Rainbow import ShellClient -extension BasicGlobalOptions { - - var shellOrDefault: ShellCommand.Shell { - guard let shell else { return .zsh(useDashC: true) } - return .custom(path: shell, useDashC: true) - } -} - -extension GlobalOptions { - - var shellOrDefault: ShellCommand.Shell { basic.shellOrDefault } -} - -func ensureString( - globals: GlobalOptions, - configuration: Configuration, - globalsKeyPath: KeyPath, - configurationKeyPath: KeyPath -) throws -> String { - if let global = globals[keyPath: globalsKeyPath] { - return global - } - guard let configuration = configuration[keyPath: configurationKeyPath] else { - throw RunPlaybookError.playbookNotFound - } - return configuration -} - -func withSetupLogger( - commandName: String, - globals: BasicGlobalOptions, - quietOnlyPlaybook: Bool = false, - dependencies setupDependencies: (inout DependencyValues) -> Void = { _ in }, - operation: @escaping () async throws -> Void -) async rethrows { - try await withDependencies { - $0.logger = .init(label: "\("hpa".yellow)") - if quietOnlyPlaybook || !globals.quiet { - switch globals.verbose { - case 0: - $0.logger.logLevel = .info - case 1: - $0.logger.logLevel = .debug - case 2...: - $0.logger.logLevel = .trace - default: - $0.logger.logLevel = .info - } - } - $0.logger[metadataKey: "command"] = "\(commandName.blue)" - } operation: { - try await operation() - } -} - -func withSetupLogger( - commandName: String, - globals: GlobalOptions, - dependencies setupDependencies: (inout DependencyValues) -> Void = { _ in }, - operation: @escaping () async throws -> Void -) async rethrows { - try await withSetupLogger( - commandName: commandName, - globals: globals.basic, - quietOnlyPlaybook: globals.quietOnlyPlaybook, - dependencies: setupDependencies, - operation: operation - ) -} - -func runPlaybook( - commandName: String, - globals: GlobalOptions, - configuration: Configuration? = nil, - extraArgs: [String], - _ args: String... -) async throws { - try await runPlaybook( - commandName: commandName, - globals: globals, - configuration: configuration, - extraArgs: extraArgs, - args - ) -} - -private extension CliClient { - func ensuredConfiguration(_ configuration: Configuration?) throws -> Configuration { - guard let configuration else { - return try loadConfiguration() - } - return configuration - } -} - func runPlaybook( commandName: String, globals: GlobalOptions, @@ -124,19 +29,20 @@ func runPlaybook( globalsKeyPath: \.playbookDir, configurationKeyPath: \.playbookDir ) - let playbook = "\(playbookDir)/main.yml" + let playbook = "\(playbookDir)/\(Constants.playbookFileName)" let inventory = (try? ensureString( globals: globals, configuration: configuration, globalsKeyPath: \.inventoryPath, configurationKeyPath: \.inventoryPath - )) ?? "\(playbookDir)/inventory.ini" + )) ?? "\(playbookDir)/\(Constants.inventoryFileName)" - let defaultArgs = configuration.defaultPlaybookArgs ?? [] + let defaultArgs = (configuration.defaultPlaybookArgs ?? []) + + (configuration.defaultVaultArgs ?? []) try await cliClient.runCommand( - quiet: globals.quietOnlyPlaybook ? true : globals.basic.quiet, + quiet: globals.quietOnlyPlaybook ? true : globals.quiet, shell: globals.shellOrDefault, [ "ansible-playbook", playbook, @@ -148,6 +54,54 @@ func runPlaybook( } } +func runPlaybook( + commandName: String, + globals: GlobalOptions, + configuration: Configuration? = nil, + extraArgs: [String], + _ args: String... +) async throws { + try await runPlaybook( + commandName: commandName, + globals: globals, + configuration: configuration, + extraArgs: extraArgs, + args + ) +} + +extension CliClient { + func ensuredConfiguration(_ configuration: Configuration?) throws -> Configuration { + guard let configuration else { + return try loadConfiguration() + } + return configuration + } +} + +extension BasicGlobalOptions { + + var shellOrDefault: ShellCommand.Shell { + guard let shell else { return .zsh(useDashC: true) } + return .custom(path: shell, useDashC: true) + } +} + +func ensureString( + globals: GlobalOptions, + configuration: Configuration, + globalsKeyPath: KeyPath, + configurationKeyPath: KeyPath +) throws -> String { + if let global = globals[keyPath: globalsKeyPath] { + return global + } + guard let configuration = configuration[keyPath: configurationKeyPath] else { + throw RunPlaybookError.playbookNotFound + } + return configuration +} + enum RunPlaybookError: Error { case playbookNotFound case configurationError diff --git a/Sources/hpa/Internal/RunVault.swift b/Sources/hpa/Internal/RunVault.swift new file mode 100644 index 0000000..9416eef --- /dev/null +++ b/Sources/hpa/Internal/RunVault.swift @@ -0,0 +1,41 @@ +import Dependencies +import ShellClient + +func runVault( + commandName: String, + options: VaultOptions, + _ args: [String] +) async throws { + try await withSetupLogger(commandName: commandName, globals: options.globals) { + @Dependency(\.cliClient) var cliClient + @Dependency(\.logger) var logger + + logger.debug("Begin run vault: \(options)") + + let configuration = try cliClient.ensuredConfiguration(nil) + logger.debug("Configuration: \(configuration)") + + let path: String + if let file = options.file { + path = file + } else { + path = try cliClient.findVaultFileInCurrentDirectory() + } + + logger.debug("Vault path: \(path)") + + let defaultArgs = configuration.defaultVaultArgs ?? [] + + try await cliClient.runCommand( + quiet: options.quiet, + shell: options.shellOrDefault, + ["ansible-vault"] + + args + + defaultArgs + + options.extraArgs + + [path] + ) + + fatalError() + } +} diff --git a/Sources/hpa/UtilsCommands/CreateTemplateCommand.swift b/Sources/hpa/UtilsCommands/CreateTemplateCommand.swift index afbd968..7fb51d5 100644 --- a/Sources/hpa/UtilsCommands/CreateTemplateCommand.swift +++ b/Sources/hpa/UtilsCommands/CreateTemplateCommand.swift @@ -4,9 +4,10 @@ struct CreateProjectTemplateCommand: AsyncParsableCommand { static let commandName = "create-template" - static let configuration = CommandConfiguration.playbookCommandConfiguration( + static let configuration = CommandConfiguration.playbook( commandName: commandName, abstract: "Create a home performance assesment project template.", + parentCommand: UtilsCommand.commandName, examples: (label: "Create Template", example: "\(commandName) /path/to/project-template") ) diff --git a/Sources/hpa/UtilsCommands/GenerateConfigCommand.swift b/Sources/hpa/UtilsCommands/GenerateConfigCommand.swift index 4660f90..f7fc403 100644 --- a/Sources/hpa/UtilsCommands/GenerateConfigCommand.swift +++ b/Sources/hpa/UtilsCommands/GenerateConfigCommand.swift @@ -8,7 +8,7 @@ struct GenerateConfigurationCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: commandName, - abstract: "\("Generate a local configuration file.".blue)", + abstract: createAbstract("Generate a local configuration file."), discussion: """ \("NOTE:".yellow) If a directory is not supplied then a configuration file will be created diff --git a/Sources/hpa/VaultCommands/DecryptCommand.swift b/Sources/hpa/VaultCommands/DecryptCommand.swift new file mode 100644 index 0000000..b06ffd9 --- /dev/null +++ b/Sources/hpa/VaultCommands/DecryptCommand.swift @@ -0,0 +1,23 @@ +import ArgumentParser + +struct DecryptCommand: AsyncParsableCommand { + + static let commandName = "decrypt" + + static let configuration = CommandConfiguration( + commandName: commandName, + abstract: createAbstract("Decrypt a vault file.") + ) + + @OptionGroup var options: VaultOptions + + @Option( + name: .shortAndLong, + help: "Output file." + ) + var output: String? + + mutating func run() async throws { + fatalError() + } +} diff --git a/Sources/hpa/VaultCommands/EncryptCommand.swift b/Sources/hpa/VaultCommands/EncryptCommand.swift new file mode 100644 index 0000000..19880f4 --- /dev/null +++ b/Sources/hpa/VaultCommands/EncryptCommand.swift @@ -0,0 +1,23 @@ +import ArgumentParser + +struct EncryptCommand: AsyncParsableCommand { + + static let commandName = "encrypt" + + static let configuration = CommandConfiguration( + commandName: commandName, + abstract: createAbstract("Encrypt a vault file.") + ) + + @OptionGroup var options: VaultOptions + + @Option( + name: .shortAndLong, + help: "Output file." + ) + var output: String? + + mutating func run() async throws { + fatalError() + } +} diff --git a/Sources/hpa/VaultCommands/VaultCommand.swift b/Sources/hpa/VaultCommands/VaultCommand.swift new file mode 100644 index 0000000..fe9697c --- /dev/null +++ b/Sources/hpa/VaultCommands/VaultCommand.swift @@ -0,0 +1,14 @@ +import ArgumentParser + +struct VaultCommand: AsyncParsableCommand { + + static let commandName = "vault" + + static let configuration = CommandConfiguration( + commandName: commandName, + abstract: createAbstract("Vault commands."), + subcommands: [ + EncryptCommand.self, DecryptCommand.self + ] + ) +} diff --git a/Sources/hpa/VaultCommands/VaultOptions.swift b/Sources/hpa/VaultCommands/VaultOptions.swift new file mode 100644 index 0000000..bce4c9d --- /dev/null +++ b/Sources/hpa/VaultCommands/VaultOptions.swift @@ -0,0 +1,31 @@ +import ArgumentParser + +// Holds the common options for vault commands, as they all share the +// same structure. +@dynamicMemberLookup +struct VaultOptions: ParsableArguments { + + @OptionGroup var globals: BasicGlobalOptions + + @Option( + name: .shortAndLong, + help: "The vault file path.", + completion: .file() + ) + var file: String? + + @Argument( + help: "Extra arguments to pass to the vault command." + ) + var extraArgs: [String] = [] + + subscript(dynamicMember keyPath: WritableKeyPath) -> T { + get { globals[keyPath: keyPath] } + set { globals[keyPath: keyPath] = newValue } + } + + subscript(dynamicMember keyPath: KeyPath) -> T { + globals[keyPath: keyPath] + } + +}