feat: Removes cli-client

This commit is contained in:
2024-12-16 17:14:25 -05:00
parent 8c402f3f5f
commit 85b285347b
32 changed files with 837 additions and 1580 deletions

View File

@@ -1,189 +0,0 @@
import ConfigurationClient
import Dependencies
import DependenciesMacros
import FileClient
import Foundation
import PlaybookClient
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 {
@Dependency(\.playbookClient) var playbookClient
@Dependency(\.configurationClient) var configurationClient
var arguments = [
"brew", "install"
] + Constants.brewPackages
if let extraArgs {
arguments.append(contentsOf: extraArgs)
}
try await runCommand(
quiet: quiet,
shell: shell.orDefault,
arguments
)
let configuration = try await configurationClient.findAndLoad()
try await playbookClient.repository.install(configuration)
}
func runPlaybookCommand(
_ options: PlaybookOptions,
logging loggingOptions: LoggingOptions
) async throws {
try await withLogger(loggingOptions) {
@Dependency(\.configurationClient) var configurationClient
@Dependency(\.logger) var logger
@Dependency(\.playbookClient) var playbookClient
let configuration = try await configurationClient.ensuredConfiguration(options.configuration)
logger.trace("Configuration: \(configuration)")
let playbookDirectory = try await playbookClient.repository.directory(configuration)
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 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 {}

View File

@@ -1,223 +0,0 @@
import ConfigurationClient
import Dependencies
import Foundation
import ShellClient
// TODO: Need to parse / ensure that header includes and files are in the build directory, not sure
// exactly how to handle if they're not, but it seems reasonable to potentially allow files that are
// outside of the build directory to be included.
public extension CliClient {
@discardableResult
func runPandocCommand(
_ options: PandocOptions,
logging loggingOptions: LoggingOptions
) async throws -> String {
try await withLogger(loggingOptions) {
@Dependency(\.configurationClient) var configurationClient
@Dependency(\.logger) var logger
let configuration = try await configurationClient.findAndLoad()
logger.trace("Configuration: \(configuration)")
let ensuredOptions = try await ensurePandocOptions(
configuration: configuration,
options: options
)
let projectDirectory = options.projectDirectory ?? ProcessInfo.processInfo.environment["PWD"]
guard let projectDirectory else {
throw CliClientError.generate(.projectDirectoryNotSpecified)
}
let outputDirectory = options.outputDirectory ?? projectDirectory
let outputPath = "\(outputDirectory)/\(ensuredOptions.ensuredExtensionFileName)"
var arguments = [
"pandoc"
]
arguments += ensuredOptions.includeInHeader.map {
"--include-in-header=\(projectDirectory)/\(ensuredOptions.buildDirectory)/\($0)"
}
if let pdfEngine = ensuredOptions.pdfEngine {
arguments.append("--pdf-engine=\(pdfEngine)")
}
arguments.append("--output=\(outputPath)")
arguments += ensuredOptions.files.map {
"\(projectDirectory)/\(ensuredOptions.buildDirectory)/\($0)"
}
if options.shouldBuild {
logger.trace("Building project...")
try await runPlaybookCommand(
.init(
arguments: [
"--tags", "build-project",
"--extra-vars", "project_dir=\(projectDirectory)"
],
configuration: configuration,
quiet: options.quiet,
shell: options.shell
),
logging: loggingOptions
)
}
logger.trace("Running pandoc with arguments: \(arguments)")
try await runCommand(
quiet: options.quiet,
shell: options.shell.orDefault,
arguments
)
return outputPath
}
}
}
@_spi(Internal)
public struct EnsuredPandocOptions: Equatable, Sendable {
public let buildDirectory: String
public let files: [String]
public let includeInHeader: [String]
public let outputFileName: String
public let outputFileType: CliClient.PandocOptions.FileType
public let pdfEngine: String?
public var ensuredExtensionFileName: String {
let extensionString: String
switch outputFileType {
case .html:
extensionString = ".html"
case .latex:
extensionString = ".tex"
case .pdf:
extensionString = ".pdf"
}
if !outputFileName.hasSuffix(extensionString) {
return outputFileName + extensionString
}
return outputFileName
}
}
@_spi(Internal)
public func ensurePandocOptions(
configuration: Configuration,
options: CliClient.PandocOptions
) async throws -> EnsuredPandocOptions {
let defaults = Configuration.Generate.default
let pdfEngine = parsePdfEngine(configuration.generate, defaults, options)
return .init(
buildDirectory: parseBuildDirectory(configuration.generate, defaults, options),
files: parseFiles(configuration.generate, defaults, options),
includeInHeader: parseIncludeInHeader(configuration.generate, defaults, options),
outputFileName: parseOutputFileName(configuration.generate, defaults, options),
outputFileType: options.outputFileType,
pdfEngine: pdfEngine
)
}
private func parsePdfEngine(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate,
_ options: CliClient.PandocOptions
) -> String? {
switch options.outputFileType {
case .html, .latex:
return nil
case let .pdf(engine: engine):
if let engine {
return engine
} else if let engine = configuration?.pdfEngine {
return engine
} else if let engine = defaults.pdfEngine {
return engine
} else {
return "xelatex"
}
}
}
private func parseFiles(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate,
_ options: CliClient.PandocOptions
) -> [String] {
@Dependency(\.logger) var logger
if let files = options.files {
return files
} else if let files = configuration?.files {
return files
} else if let files = defaults.files {
return files
} else {
logger.warning("Files not specified, this could lead to errors.")
return []
}
}
private func parseIncludeInHeader(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate,
_ options: CliClient.PandocOptions
) -> [String] {
@Dependency(\.logger) var logger
if let files = options.includeInHeader {
return files
} else if let files = configuration?.includeInHeader {
return files
} else if let files = defaults.includeInHeader {
return files
} else {
logger.warning("Include in header files not specified, this could lead to errors.")
return []
}
}
private func parseOutputFileName(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate,
_ options: CliClient.PandocOptions
) -> String {
@Dependency(\.logger) var logger
if let output = options.outputFileName {
return output
} else if let output = configuration?.outputFileName {
return output
} else if let output = defaults.outputFileName {
return output
} else {
logger.warning("Output file name not specified, this could lead to errors.")
return "Report"
}
}
private func parseBuildDirectory(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate,
_ options: CliClient.PandocOptions
) -> String {
@Dependency(\.logger) var logger
if let output = options.buildDirectory {
return output
} else if let output = configuration?.buildDirectory {
return output
} else if let output = defaults.buildDirectory {
return output
} else {
logger.warning("Output file name not specified, this could lead to errors.")
return ".build"
}
}

View File

@@ -1,216 +0,0 @@
import ConfigurationClient
import Dependencies
import Foundation
import PlaybookClient
extension CliClient.RunPlaybook {
static func makeCommonArguments(
configuration: Configuration,
inventoryFilePath: String?
) async throws -> [String] {
@Dependency(\.logger) var logger
@Dependency(\.playbookClient) var playbookClient
let playbookDirectory = try await playbookClient.repository.directory(configuration)
let playbookPath = "\(playbookDirectory)/\(Constants.playbookFileName)"
logger.trace("Playbook path: \(playbookPath)")
let inventoryPath = ensuredInventoryPath(
inventoryFilePath,
configuration: configuration,
playbookDirectory: playbookDirectory
)
logger.trace("Inventory path: \(inventoryPath)")
var arguments = [
Constants.playbookCommand, playbookPath,
"--inventory", inventoryPath
]
if let defaultArgs = configuration.args {
arguments.append(contentsOf: defaultArgs)
}
if configuration.useVaultArgs, let vaultArgs = configuration.vault.args {
arguments.append(contentsOf: vaultArgs)
}
logger.trace("Common arguments: \(inventoryPath)")
return arguments
}
}
extension CliClient.RunPlaybook.BuildOptions {
private func applyArguments(
to arguments: inout [String],
configuration: Configuration
) throws {
let projectDirectory = projectDirectory
?? ProcessInfo.processInfo.environment["PWD"]
guard let projectDirectory else {
throw CliClientError.projectDirectoryNotFound
}
arguments.append(contentsOf: [
"--tags", "build-project",
"--extra-vars", "project_dir=\(projectDirectory)"
])
if let extraOptions = extraOptions {
arguments.append(contentsOf: extraOptions)
}
}
}
extension CliClient.RunPlaybook.CreateOptions {
private func applyArguments(
to arguments: inout [String],
configuration: Configuration
) throws {
let json = try createJSONData(configuration: configuration)
arguments.append(contentsOf: [
"--tags", "setup-project",
"--extra-vars", "project_dir=\(projectDirectory)",
"--extra-vars", "'\(json)'"
])
if let extraOptions {
arguments.append(contentsOf: extraOptions)
}
}
}
extension CliClient.PlaybookOptions.Route {
private func parseInventoryPath(
_ configuration: Configuration,
_ playbookDirectory: String
) -> String {
let inventoryFilePath: String?
switch self {
case let .build(options):
inventoryFilePath = options.inventoryFilePath
case let .create(options):
inventoryFilePath = options.inventoryFilePath
}
return ensuredInventoryPath(
inventoryFilePath,
configuration: configuration,
playbookDirectory: playbookDirectory
)
}
func makeArguments(configuration: Configuration) async throws -> [String] {
@Dependency(\.logger) var logger
@Dependency(\.playbookClient) var playbookClient
let playbookDirectory = try await playbookClient.repository.directory(configuration)
let playbookPath = "\(playbookDirectory)/\(Constants.playbookFileName)"
logger.trace("Playbook path: \(playbookPath)")
let inventoryPath = parseInventoryPath(configuration, playbookDirectory)
logger.trace("Inventory path: \(inventoryPath)")
var arguments = [
Constants.playbookCommand, playbookPath,
"--inventory", inventoryPath
]
if let defaultArgs = configuration.args {
arguments.append(contentsOf: defaultArgs)
}
if configuration.useVaultArgs, let vaultArgs = configuration.vault.args {
arguments.append(contentsOf: vaultArgs)
}
// try applyArguments(to: &arguments, configuration: configuration)
return arguments
}
}
// NOTE: We're not using the `Coders` client because we generally do not
// want the output to be `prettyPrinted` or anything, unless we're running
// tests, so we use a supplied json encoder.
extension CliClient.RunPlaybook.CreateOptions {
func createJSONData(
configuration: Configuration,
encoder: JSONEncoder = .init()
) throws -> Data {
@Dependency(\.logger) var logger
let templateDir = template.directory ?? configuration.template.directory
let templateRepo = template.url ?? configuration.template.url
let version = template.version ?? configuration.template.version
logger.debug("""
(\(useLocalTemplateDirectory), \(String(describing: templateDir)), \(String(describing: templateRepo)))
""")
switch (useLocalTemplateDirectory, templateDir, templateRepo) {
case (true, .none, _):
// User supplied they wanted to use a local template directory, but we could not find
// the path set from command line or in configuration.
throw CliClientError.templateDirectoryNotFound
case let (false, _, .some(repo)):
// User did not supply they wanted to use a local template directory, and we found a repo url that was
// either set by the command line or found in the configuration.
logger.debug("Using repo.")
return try encoder.encode(TemplateRepo(repo: repo, version: version))
case let (true, .some(templateDir), _):
// User supplied they wanted to use a local template directory, and we found the template directory
// either set by the command line or in the configuration.
logger.debug("Using template directory.")
return try encoder.encode(TemplateDirJson(path: templateDir))
case let (false, .some(templateDir), _):
// User supplied they did not wanted to use a local template directory, and we found the template directory
// either set by the command line or in the configuration, and no repo was found / handled previously.
logger.debug("Using template directory.")
return try encoder.encode(TemplateDirJson(path: templateDir))
case (_, .none, .none):
// We could not find a repo or template directory.
throw CliClientError.templateDirectoryOrRepoNotSpecified
}
}
}
private struct TemplateDirJson: Encodable {
let template: Template
init(path: String) {
self.template = .init(path: path)
}
struct Template: Encodable {
let path: String
}
}
private struct TemplateRepo: Encodable {
let template: Template
init(repo: String, version: String?) {
self.template = .init(repo: .init(url: repo, version: version ?? "main"))
}
struct Template: Encodable {
let repo: Repo
}
struct Repo: Encodable {
let url: String
let version: String
}
}

View File

@@ -1,16 +0,0 @@
import Foundation
public enum CliClientError: Error {
case brewfileNotFound
case encodingError
case playbookDirectoryNotFound
case projectDirectoryNotFound
case generate(GenerateError)
case templateDirectoryNotFound
case templateDirectoryOrRepoNotSpecified
case vaultFileNotFound
public enum GenerateError: Sendable {
case projectDirectoryNotSpecified
}
}

View File

@@ -1,11 +0,0 @@
enum Constants {
static let executableName = "hpa"
static let playbookBundleDirectoryName = "ansible-hpa-playbook"
static let playbookCommand = "ansible-playbook"
static let playbookFileName = "main.yml"
static let inventoryFileName = "inventory.ini"
static let vaultCommand = "ansible-vault"
static let brewPackages = [
"ansible", "imagemagick", "pandoc", "texLive"
]
}

View File

@@ -1,85 +0,0 @@
import ConfigurationClient
import Dependencies
import Foundation
// NOTE: We're not using the `Coders` client because we generally do not
// want the output to be `prettyPrinted` or anything, unless we're running
// tests, so we use a supplied json encoder.
// TODO: Remove.
func createJSONData(
_ options: CliClient.GenerateJsonOptions,
logging loggingOptions: CliClient.LoggingOptions,
encoder: JSONEncoder = .init()
) async throws -> Data {
try await CliClient.withLogger(loggingOptions) {
@Dependency(\.logger) var logger
@Dependency(\.configurationClient) var configurationClient
let configuration = try await configurationClient.findAndLoad()
let templateDir = options.templateDirectory ?? configuration.template.directory
let templateRepo = options.templateRepo ?? configuration.template.url
let version = options.version ?? configuration.template.version
logger.debug("""
(\(options.useLocalTemplateDirectory), \(String(describing: templateDir)), \(String(describing: templateRepo)))
""")
switch (options.useLocalTemplateDirectory, templateDir, templateRepo) {
case (true, .none, _):
// User supplied they wanted to use a local template directory, but we could not find
// the path set from command line or in configuration.
throw CliClientError.templateDirectoryNotFound
case let (false, _, .some(repo)):
// User did not supply they wanted to use a local template directory, and we found a repo url that was
// either set by the command line or found in the configuration.
logger.debug("Using repo.")
return try encoder.encode(TemplateRepo(repo: repo, version: version))
case let (true, .some(templateDir), _):
// User supplied they wanted to use a local template directory, and we found the template directory
// either set by the command line or in the configuration.
logger.debug("Using template directory.")
return try encoder.encode(TemplateDirJson(path: templateDir))
case let (false, .some(templateDir), _):
// User supplied they did not wanted to use a local template directory, and we found the template directory
// either set by the command line or in the configuration, and no repo was found / handled previously.
logger.debug("Using template directory.")
return try encoder.encode(TemplateDirJson(path: templateDir))
case (_, .none, .none):
// We could not find a repo or template directory.
throw CliClientError.templateDirectoryOrRepoNotSpecified
}
}
}
private struct TemplateDirJson: Encodable {
let template: Template
init(path: String) {
self.template = .init(path: path)
}
struct Template: Encodable {
let path: String
}
}
private struct TemplateRepo: Encodable {
let template: Template
init(repo: String, version: String?) {
self.template = .init(repo: .init(url: repo, version: version ?? "main"))
}
struct Template: Encodable {
let repo: Repo
}
struct Repo: Encodable {
let url: String
let version: String
}
}

View File

@@ -1,329 +0,0 @@
import ConfigurationClient
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 runCommand: @Sendable (RunCommandOptions) async throws -> Void
public var generateJSON: @Sendable (GenerateJsonOptions, LoggingOptions, JSONEncoder) async throws -> String
public func generateJSON(
_ options: GenerateJsonOptions,
logging loggingOptions: LoggingOptions,
encoder jsonEncoder: JSONEncoder = .init()
) async throws -> String {
try await generateJSON(options, loggingOptions, jsonEncoder)
}
}
public extension CliClient {
@DependencyClient
struct RunPlaybook: Sendable {
public var buildProject: @Sendable (RunOptions, BuildOptions) async throws -> Void
public var createProject: @Sendable (RunOptions, CreateOptions) async throws -> String
public struct RunOptions: Equatable, Sendable {
public let loggingOptions: CliClient.LoggingOptions
public let quiet: Bool
public let shell: String?
}
public struct BuildOptions: Equatable, Sendable {
public let extraOptions: [String]?
public let inventoryFilePath: String?
public let projectDirectory: String?
public init(
extraOptions: [String]?,
inventoryFilePath: String?,
projectDirectory: String
) {
self.extraOptions = extraOptions
self.inventoryFilePath = inventoryFilePath
self.projectDirectory = projectDirectory
}
}
public struct CreateOptions: Equatable, Sendable {
public let extraOptions: [String]?
public let inventoryFilePath: String?
public let projectDirectory: String
public let template: Configuration.Template
public let useLocalTemplateDirectory: Bool
public init(
extraOptions: [String]?,
inventoryFilePath: String?,
projectDirectory: String,
template: Configuration.Template,
useLocalTemplateDirectory: Bool
) {
self.extraOptions = extraOptions
self.inventoryFilePath = inventoryFilePath
self.projectDirectory = projectDirectory
self.template = template
self.useLocalTemplateDirectory = useLocalTemplateDirectory
}
}
}
}
public extension CliClient {
struct PandocOptions: Equatable, Sendable {
let buildDirectory: String?
let files: [String]?
let includeInHeader: [String]?
let outputDirectory: String?
let outputFileName: String?
let outputFileType: FileType
let projectDirectory: String?
let quiet: Bool
let shell: String?
let shouldBuild: Bool
public init(
buildDirectory: String? = nil,
files: [String]? = nil,
includeInHeader: [String]? = nil,
outputDirectory: String? = nil,
outputFileName: String? = nil,
outputFileType: FileType,
projectDirectory: String?,
quiet: Bool,
shell: String? = nil,
shouldBuild: Bool
) {
self.buildDirectory = buildDirectory
self.files = files
self.includeInHeader = includeInHeader
self.outputDirectory = outputDirectory
self.outputFileName = outputFileName
self.outputFileType = outputFileType
self.projectDirectory = projectDirectory
self.quiet = quiet
self.shell = shell
self.shouldBuild = shouldBuild
}
// swiftlint:disable nesting
public enum FileType: Equatable, Sendable {
case html
case latex
case pdf(engine: String?)
}
// swiftlint:enable nesting
}
struct GenerateJsonOptions: Equatable, Sendable {
let templateDirectory: String?
let templateRepo: String?
let version: String?
let useLocalTemplateDirectory: Bool
public init(
templateDirectory: String?,
templateRepo: String?,
version: String?,
useLocalTemplateDirectory: Bool
) {
self.templateDirectory = templateDirectory
self.templateRepo = templateRepo
self.version = version
self.useLocalTemplateDirectory = useLocalTemplateDirectory
}
}
struct LoggingOptions: Equatable, Sendable {
let commandName: String
let logLevel: Logger.Level
public init(commandName: String, logLevel: Logger.Level) {
self.commandName = commandName
self.logLevel = logLevel
}
}
struct PlaybookOptions: Sendable, Equatable {
let arguments: [String]
let configuration: Configuration?
let inventoryFilePath: String?
let playbookDirectory: String?
let quiet: Bool
let shell: String?
public init(
arguments: [String],
configuration: Configuration? = nil,
inventoryFilePath: String? = nil,
playbookDirectory: String? = nil,
quiet: Bool,
shell: String? = nil
) {
self.arguments = arguments
self.configuration = configuration
self.inventoryFilePath = inventoryFilePath
self.playbookDirectory = playbookDirectory
self.quiet = quiet
self.shell = shell
}
public enum Route {
case build(BuildOption)
case create(CreateOption)
public struct BuildOption: Equatable, Sendable {
public let extraOptions: [String]?
public let inventoryFilePath: String?
public let projectDirectory: String?
public init(
extraOptions: [String]?,
inventoryFilePath: String?,
projectDirectory: String
) {
self.extraOptions = extraOptions
self.inventoryFilePath = inventoryFilePath
self.projectDirectory = projectDirectory
}
}
public struct CreateOption: Equatable, Sendable {
public let extraOptions: [String]?
public let inventoryFilePath: String?
public let projectDirectory: String
public let template: Configuration.Template
public let useLocalTemplateDirectory: Bool
public init(
extraOptions: [String]?,
inventoryFilePath: String?,
projectDirectory: String,
template: Configuration.Template,
useLocalTemplateDirectory: Bool
) {
self.extraOptions = extraOptions
self.inventoryFilePath = inventoryFilePath
self.projectDirectory = projectDirectory
self.template = template
self.useLocalTemplateDirectory = useLocalTemplateDirectory
}
}
}
}
struct RunCommandOptions: Sendable, Equatable {
public let arguments: [String]
public let quiet: Bool
public let shell: ShellCommand.Shell
public init(
arguments: [String],
quiet: Bool,
shell: ShellCommand.Shell
) {
self.arguments = arguments
self.quiet = quiet
self.shell = shell
}
}
struct VaultOptions: Equatable, Sendable {
let arguments: [String]
let configuration: Configuration?
let quiet: Bool
let shell: String?
let vaultFilePath: String?
public init(
arguments: [String],
configuration: Configuration? = nil,
quiet: Bool,
shell: String?,
vaultFilePath: String? = nil
) {
self.arguments = arguments
self.configuration = configuration
self.quiet = quiet
self.shell = shell
self.vaultFilePath = vaultFilePath
}
}
}
extension CliClient: DependencyKey {
public static func live(
env: [String: String]
) -> Self {
@Dependency(\.logger) var logger
return .init { options in
@Dependency(\.asyncShellClient) var shellClient
if !options.quiet {
try await shellClient.foreground(.init(
shell: options.shell,
environment: ProcessInfo.processInfo.environment,
in: nil,
options.arguments
))
} else {
try await shellClient.background(.init(
shell: options.shell,
environment: ProcessInfo.processInfo.environment,
in: nil,
options.arguments
))
}
} generateJSON: { options, loggingOptions, encoder in
let data = try await createJSONData(options, logging: loggingOptions, encoder: encoder)
guard let string = String(data: data, encoding: .utf8) else {
throw CliClientError.encodingError
}
return string
}
}
public static var liveValue: CliClient {
.live(env: ProcessInfo.processInfo.environment)
}
public static let testValue: CliClient = Self()
public static func capturing(_ client: CapturingClient) -> Self {
.init { options in
await client.set(options)
} generateJSON: {
try await Self().generateJSON($0, $1, $2)
}
}
public actor CapturingClient: Sendable {
public private(set) var quiet: Bool?
public private(set) var shell: ShellCommand.Shell?
public private(set) var arguments: [String]?
public init() {}
public func set(
_ options: RunCommandOptions
) {
quiet = options.quiet
shell = options.shell
arguments = options.arguments
}
}
}

View File

@@ -1,29 +0,0 @@
import Dependencies
import Logging
import ShellClient
// TODO: Remove.
public extension CliClient {
@discardableResult
func withLogger<T>(
_ options: LoggingOptions,
operation: @Sendable @escaping () async throws -> T
) async rethrows -> T {
try await Self.withLogger(options, operation: operation)
}
@discardableResult
static func withLogger<T>(
_ options: LoggingOptions,
operation: @Sendable @escaping () async throws -> T
) async rethrows -> T {
try await withDependencies {
$0.logger = .init(label: "\(Constants.executableName)")
$0.logger.logLevel = options.logLevel
$0.logger[metadataKey: "command"] = "\(options.commandName.blue)"
} operation: {
try await operation()
}
}
}

View File

@@ -15,7 +15,7 @@ public extension DependencyValues {
@DependencyClient
public struct ConfigurationClient: Sendable {
public var find: @Sendable () async throws -> File
var generate: @Sendable (File, Bool) async throws -> Void
public var generate: @Sendable (GenerateOptions) async throws -> String
public var load: @Sendable (File?) async throws -> Configuration
var write: @Sendable (File, Configuration, Bool) async throws -> Void
@@ -24,13 +24,6 @@ public struct ConfigurationClient: Sendable {
return try await load(file)
}
public func generate(
at file: File,
force: Bool = false
) async throws {
try await generate(file, force)
}
public func write(
_ configuration: Configuration,
to file: File,
@@ -38,6 +31,28 @@ public struct ConfigurationClient: Sendable {
) async throws {
try await write(file, configuration, force)
}
public struct GenerateOptions: Equatable, Sendable {
public let force: Bool
public let json: Bool
public let path: Path?
public init(
force: Bool = false,
json: Bool = false,
path: Path? = nil
) {
self.force = force
self.json = json
self.path = path
}
public enum Path: Equatable, Sendable {
case file(File)
case directory(String)
}
}
}
extension ConfigurationClient: DependencyKey {
@@ -47,8 +62,8 @@ extension ConfigurationClient: DependencyKey {
let liveClient = LiveConfigurationClient(environment: environment)
return .init {
try await liveClient.find()
} generate: { file, force in
try await liveClient.generate(at: file, force: force)
} generate: {
try await liveClient.generate($0)
} load: { file in
try await liveClient.load(file: file)
} write: { file, configuration, force in
@@ -126,10 +141,24 @@ struct LiveConfigurationClient {
throw ConfigurationError.configurationNotFound
}
func generate(at file: File, force: Bool) async throws {
func generate(_ options: ConfigurationClient.GenerateOptions) async throws -> String {
@Dependency(\.logger) var logger
logger.debug("Begin generating configuration: \(file.path), force: \(force)")
let file: File
if let path = options.path {
switch path {
case let .file(requestedFile):
file = requestedFile
case let .directory(directory):
file = .init("\(directory)/\(HPAKey.defaultFileNameWithoutExtension).\(options.json ? "json" : "toml")")!
}
} else {
let configDir = "\(environment.xdgConfigHome)/\(HPAKey.configDirName)"
file = .init("\(configDir)/\(HPAKey.defaultFileName)")!
}
logger.debug("Begin generating configuration: \(file.path), force: \(options.force)")
let expandedPath = file.path.replacingOccurrences(
of: "~",
@@ -138,7 +167,7 @@ struct LiveConfigurationClient {
let fileUrl = URL(filePath: expandedPath)
if !force {
if !options.force {
guard !fileManager.fileExists(fileUrl) else {
throw ConfigurationError.fileExists(path: file.path)
}
@@ -166,8 +195,10 @@ struct LiveConfigurationClient {
} else {
// Json does not allow comments, so we write the mock configuration
// to the file path.
try await write(.mock, to: File(fileUrl)!, force: force)
try await write(.mock, to: File(fileUrl)!, force: options.force)
}
return fileUrl.cleanFilePath
}
func load(file: File?) async throws -> Configuration {

View File

@@ -11,6 +11,7 @@ public enum HPAKey {
public static let resourceFileName = "hpa"
public static let resourceFileExtension = "toml"
public static let defaultFileName = "config.toml"
public static let defaultFileNameWithoutExtension = "config.toml"
}
extension [String: String] {

View File

@@ -0,0 +1,7 @@
extension PandocClient {
enum Constants {
static let pandocCommand = "pandoc"
static let defaultOutputFileName = "Report"
static let defaultPdfEngine = "xelatex"
}
}

View File

@@ -0,0 +1,241 @@
import CommandClient
import ConfigurationClient
import Dependencies
import Foundation
import PlaybookClient
extension PandocClient.RunOptions {
func run(_ fileType: PandocClient.FileType) async throws -> String {
@Dependency(\.commandClient) var commandClient
@Dependency(\.logger) var logger
@Dependency(\.playbookClient) var playbookClient
return try await commandClient.run(logging: loggingOptions, quiet: quiet, shell: shell) {
let ensuredOptions = try await self.ensuredOptions(fileType)
let projectDirectory = self.projectDirectory ?? ProcessInfo.processInfo.environment["PWD"]
guard let projectDirectory else {
throw ProjectDirectoryNotSpecified()
}
if shouldBuild {
logger.debug("Building project...")
try await playbookClient.run.buildProject(.init(
projectDirectory: projectDirectory,
shared: .init(
extraOptions: nil,
inventoryFilePath: nil,
loggingOptions: loggingOptions,
quiet: quiet,
shell: shell
)
))
}
let outputDirectory = self.outputDirectory ?? projectDirectory
let outputPath = "\(outputDirectory)/\(ensuredOptions.ensuredExtensionFileName)"
let arguments = ensuredOptions.makeArguments(
outputPath: outputPath,
projectDirectory: projectDirectory
)
logger.debug("Pandoc arguments: \(arguments)")
return (arguments, outputPath)
}
}
func ensuredOptions(
_ fileType: PandocClient.FileType
) async throws -> EnsuredPandocOptions {
@Dependency(\.configurationClient) var configurationClient
@Dependency(\.logger) var logger
let configuration = try await configurationClient.findAndLoad()
logger.debug("Configuration: \(configuration)")
return try await ensurePandocOptions(
configuration: configuration,
fileType: fileType,
options: self
)
}
}
@_spi(Internal)
public struct EnsuredPandocOptions: Equatable, Sendable {
public let buildDirectory: String
public let extraOptions: [String]?
public let files: [String]
public let includeInHeader: [String]
public let outputFileName: String
public let outputFileType: PandocClient.FileType
public let pdfEngine: String?
public var ensuredExtensionFileName: String {
let extensionString: String
switch outputFileType {
case .html:
extensionString = ".html"
case .latex:
extensionString = ".tex"
case .pdf:
extensionString = ".pdf"
}
if !outputFileName.hasSuffix(extensionString) {
return outputFileName + extensionString
}
return outputFileName
}
func makeArguments(
outputPath: String,
projectDirectory: String
) -> [String] {
var arguments = [PandocClient.Constants.pandocCommand]
arguments += includeInHeader.map {
"--include-in-header=\(projectDirectory)/\(buildDirectory)/\($0)"
}
if let pdfEngine {
arguments.append("--pdf-engine=\(pdfEngine)")
}
arguments.append("--output=\(outputPath)")
if let extraOptions {
arguments.append(contentsOf: extraOptions)
}
arguments += files.map {
"\(projectDirectory)/\(buildDirectory)/\($0)"
}
return arguments
}
}
@_spi(Internal)
public func ensurePandocOptions(
configuration: Configuration,
fileType: PandocClient.FileType,
options: PandocClient.RunOptions
) async throws -> EnsuredPandocOptions {
let defaults = Configuration.Generate.default
return .init(
buildDirectory: options.parseBuildDirectory(configuration.generate, defaults),
extraOptions: options.extraOptions,
files: options.parseFiles(configuration.generate, defaults),
includeInHeader: options.parseIncludeInHeader(configuration.generate, defaults),
outputFileName: options.parseOutputFileName(configuration.generate, defaults),
outputFileType: fileType,
pdfEngine: fileType.parsePdfEngine(configuration.generate, defaults)
)
}
@_spi(Internal)
public extension PandocClient.FileType {
func parsePdfEngine(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate
) -> String? {
switch self {
case .html, .latex:
return nil
case let .pdf(engine: engine):
if let engine {
return engine
} else if let engine = configuration?.pdfEngine {
return engine
} else if let engine = defaults.pdfEngine {
return engine
} else {
return PandocClient.Constants.defaultPdfEngine
}
}
}
}
@_spi(Internal)
public extension PandocClient.RunOptions {
func parseFiles(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate
) -> [String] {
@Dependency(\.logger) var logger
if let files = files {
return files
} else if let files = configuration?.files {
return files
} else if let files = defaults.files {
return files
} else {
logger.warning("Files not specified, this could lead to errors.")
return []
}
}
func parseIncludeInHeader(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate
) -> [String] {
@Dependency(\.logger) var logger
if let files = includeInHeader {
return files
} else if let files = configuration?.includeInHeader {
return files
} else if let files = defaults.includeInHeader {
return files
} else {
logger.warning("Include in header files not specified, this could lead to errors.")
return []
}
}
func parseOutputFileName(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate
) -> String {
@Dependency(\.logger) var logger
if let output = outputFileName {
return output
} else if let output = configuration?.outputFileName {
return output
} else if let output = defaults.outputFileName {
return output
} else {
logger.warning("Output file name not specified, this could lead to errors.")
return PandocClient.Constants.defaultOutputFileName
}
}
func parseBuildDirectory(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate
) -> String {
@Dependency(\.logger) var logger
if let output = buildDirectory {
return output
} else if let output = configuration?.buildDirectory {
return output
} else if let output = defaults.buildDirectory {
return output
} else {
logger.warning("Output file name not specified, this could lead to errors.")
return ".build"
}
}
}
struct ProjectDirectoryNotSpecified: Error {}

View File

@@ -0,0 +1,95 @@
import CommandClient
import ConfigurationClient
import Dependencies
import DependenciesMacros
public extension DependencyValues {
var pandocClient: PandocClient {
get { self[PandocClient.self] }
set { self[PandocClient.self] = newValue }
}
}
@DependencyClient
public struct PandocClient: Sendable {
public var run: Run
@DependencyClient
public struct Run: Sendable {
public var generateLatex: @Sendable (RunOptions) async throws -> String
public var generateHtml: @Sendable (RunOptions) async throws -> String
public var generatePdf: @Sendable (RunOptions, String?) async throws -> String
public func generatePdf(
_ options: RunOptions,
pdfEngine: String? = nil
) async throws -> String {
try await generatePdf(options, pdfEngine)
}
}
public struct RunOptions: Equatable, Sendable {
let buildDirectory: String?
let extraOptions: [String]?
let files: [String]?
let loggingOptions: LoggingOptions
let includeInHeader: [String]?
let outputDirectory: String?
let outputFileName: String?
let projectDirectory: String?
let quiet: Bool
let shell: String?
let shouldBuild: Bool
public init(
buildDirectory: String? = nil,
extraOptions: [String]? = nil,
files: [String]? = nil,
loggingOptions: LoggingOptions,
includeInHeader: [String]? = nil,
outputDirectory: String? = nil,
projectDirectory: String? = nil,
outputFileName: String? = nil,
quiet: Bool = false,
shell: String? = nil,
shouldBuild: Bool = true
) {
self.buildDirectory = buildDirectory
self.extraOptions = extraOptions
self.files = files
self.loggingOptions = loggingOptions
self.includeInHeader = includeInHeader
self.outputDirectory = outputDirectory
self.outputFileName = outputFileName
self.projectDirectory = projectDirectory
self.quiet = quiet
self.shell = shell
self.shouldBuild = shouldBuild
}
}
@_spi(Internal)
public enum FileType: Equatable, Sendable {
case html
case latex
case pdf(engine: String?)
}
}
extension PandocClient: DependencyKey {
public static let testValue: PandocClient = Self(run: Run())
public static var liveValue: PandocClient {
.init(
run: Run(
generateLatex: { try await $0.run(.latex) },
generateHtml: { try await $0.run(.html) },
generatePdf: { try await $0.run(.pdf(engine: $1)) }
)
)
}
}

View File

@@ -1,5 +1,4 @@
import ArgumentParser
import CliClient
import Dependencies
import Foundation
import PlaybookClient

View File

@@ -1,5 +1,4 @@
import ArgumentParser
import CliClient
import ConfigurationClient
import Dependencies
import Foundation
@@ -71,14 +70,3 @@ struct CreateCommand: AsyncParsableCommand {
print(projectDir)
}
}
private extension CreateCommand {
var generateJsonOptions: CliClient.GenerateJsonOptions {
.init(
templateDirectory: templateDir,
templateRepo: repo,
version: branch,
useLocalTemplateDirectory: localTemplateDir
)
}
}

View File

@@ -1,6 +1,6 @@
import ArgumentParser
import CliClient
import Dependencies
import PandocClient
// TODO: Need to add a step to build prior to generating file.
struct GenerateHtmlCommand: AsyncParsableCommand {
@@ -13,12 +13,12 @@ struct GenerateHtmlCommand: AsyncParsableCommand {
@OptionGroup var globals: GenerateOptions
mutating func run() async throws {
@Dependency(\.cliClient) var cliClient
@Dependency(\.pandocClient) var pandocClient
try await cliClient.runPandocCommand(
globals.pandocOptions(.html),
logging: globals.loggingOptions(commandName: Self.commandName)
let output = try await pandocClient.run.generateHtml(
globals.pandocRunOptions(commandName: Self.commandName)
)
print(output)
}
}

View File

@@ -1,5 +1,4 @@
import ArgumentParser
import CliClient
import Dependencies
// TODO: Need to add a step to build prior to generating file.
@@ -13,11 +12,11 @@ struct GenerateLatexCommand: AsyncParsableCommand {
@OptionGroup var globals: GenerateOptions
mutating func run() async throws {
@Dependency(\.cliClient) var cliClient
@Dependency(\.pandocClient) var pandocClient
try await cliClient.runPandocCommand(
globals.pandocOptions(.latex),
logging: globals.loggingOptions(commandName: Self.commandName)
let output = try await pandocClient.run.generateLatex(
globals.pandocRunOptions(commandName: Self.commandName)
)
print(output)
}
}

View File

@@ -1,11 +1,19 @@
import ArgumentParser
import CliClient
import CommandClient
import PandocClient
@dynamicMemberLookup
struct GenerateOptions: ParsableArguments {
@OptionGroup var basic: BasicGlobalOptions
@Option(
name: .shortAndLong,
help: "Custom build directory path.",
completion: .directory
)
var buildDirectory: String?
@Option(
name: [.short, .customLong("file")],
help: "Files used to generate the output, can be specified multiple times.",
@@ -65,20 +73,20 @@ struct GenerateOptions: ParsableArguments {
extension GenerateOptions {
func loggingOptions(commandName: String) -> CliClient.LoggingOptions {
func loggingOptions(commandName: String) -> LoggingOptions {
basic.loggingOptions(commandName: commandName)
}
func pandocOptions(
_ fileType: CliClient.PandocOptions.FileType
) -> CliClient.PandocOptions {
func pandocRunOptions(commandName: String) -> PandocClient.RunOptions {
.init(
buildDirectory: buildDirectory,
extraOptions: extraOptions.count > 0 ? extraOptions : nil,
files: files.count > 0 ? files : nil,
loggingOptions: .init(commandName: commandName, logLevel: .init(globals: basic, quietOnlyPlaybook: false)),
includeInHeader: includeInHeader.count > 0 ? includeInHeader : nil,
outputDirectory: outputDirectory,
outputFileName: outputFileName,
outputFileType: fileType,
projectDirectory: projectDirectory,
outputFileName: outputFileName,
quiet: basic.quiet,
shell: basic.shell,
shouldBuild: !noBuild

View File

@@ -1,6 +1,6 @@
import ArgumentParser
import CliClient
import Dependencies
import PandocClient
// TODO: Need to add a step to build prior to generating file.
@@ -20,11 +20,11 @@ struct GeneratePdfCommand: AsyncParsableCommand {
@OptionGroup var globals: GenerateOptions
mutating func run() async throws {
@Dependency(\.cliClient) var cliClient
@Dependency(\.pandocClient) var pandocClient
let output = try await cliClient.runPandocCommand(
globals.pandocOptions(.pdf(engine: pdfEngine)),
logging: globals.loggingOptions(commandName: Self.commandName)
let output = try await pandocClient.run.generatePdf(
globals.pandocRunOptions(commandName: Self.commandName),
pdfEngine: pdfEngine
)
print(output)

View File

@@ -3,6 +3,9 @@ import Rainbow
// Constant string values.
enum Constants {
static let appName = "hpa"
static let brewPackages = [
"ansible", "imagemagick", "pandoc", "texLive"
]
static let playbookFileName = "main.yml"
static let inventoryFileName = "inventory.ini"
static let importantExtraArgsNote = """

View File

@@ -1,5 +1,5 @@
import ArgumentParser
import CliClient
import CommandClient
import ConfigurationClient
import PlaybookClient
@@ -59,10 +59,8 @@ struct GlobalOptions: ParsableArguments {
}
// TODO: Update these to use CommandClient.LoggingOptions
extension GlobalOptions {
func loggingOptions(commandName: String) -> CliClient.LoggingOptions {
func loggingOptions(commandName: String) -> LoggingOptions {
.init(
commandName: commandName,
logLevel: .init(globals: basic, quietOnlyPlaybook: quietOnlyPlaybook)
@@ -71,7 +69,7 @@ extension GlobalOptions {
}
extension BasicGlobalOptions {
func loggingOptions(commandName: String) -> CliClient.LoggingOptions {
func loggingOptions(commandName: String) -> LoggingOptions {
.init(
commandName: commandName,
logLevel: .init(globals: self, quietOnlyPlaybook: false)
@@ -79,24 +77,6 @@ extension BasicGlobalOptions {
}
}
// TODO: Remove
extension GlobalOptions {
func playbookOptions(
arguments: [String],
configuration: Configuration?
) -> CliClient.PlaybookOptions {
.init(
arguments: arguments,
configuration: configuration,
inventoryFilePath: inventoryPath,
playbookDirectory: playbookDirectory,
quiet: quietOnlyPlaybook ? true : basic.quiet,
shell: basic.shell
)
}
}
extension GlobalOptions {
func sharedPlaybookRunOptions(
commandName: String,

View File

@@ -1,18 +1,17 @@
import CliClient
import ConfigurationClient
extension VaultOptions {
func vaultOptions(
arguments: [String],
configuration: Configuration?
) -> CliClient.VaultOptions {
.init(
arguments: arguments,
configuration: configuration,
quiet: globals.quiet,
shell: globals.shell,
vaultFilePath: file
)
}
}
// import ConfigurationClient
//
// extension VaultOptions {
//
// func vaultOptions(
// arguments: [String],
// configuration: Configuration?
// ) -> CliClient.VaultOptions {
// .init(
// arguments: arguments,
// configuration: configuration,
// quiet: globals.quiet,
// shell: globals.shell,
// vaultFilePath: file
// )
// }
// }

View File

@@ -1,6 +1,6 @@
import ArgumentParser
import CliClient
import CliDoc
import CommandClient
import ConfigurationClient
import Dependencies
@@ -16,14 +16,14 @@ struct GenerateConfigurationCommand: AsyncParsableCommand {
Note {
"""
If a directory is not supplied then a configuration file will be created
at \("'~/.config/hpa-playbook/config'".yellow).
at \("'~/.config/hpa/config.toml'".yellow).
"""
}
VStack {
"EXAMPLE:".yellow.bold
"Create a directory and generate the configuration".green
ShellCommand("mkdir -p ~/.config/hpa-playbook")
ShellCommand("hpa generate-config --path ~/.config/hpa-playbook")
ShellCommand("mkdir -p ~/.config/hpa")
ShellCommand("hpa generate-config --path ~/.config/hpa")
}
}
.separator(.newLine(count: 2))
@@ -39,7 +39,7 @@ struct GenerateConfigurationCommand: AsyncParsableCommand {
@Flag(
name: .shortAndLong,
help: "Generate a json file, instead of default env style"
help: "Generate a json file, instead of the default toml style"
)
var json: Bool = false
@@ -56,26 +56,18 @@ struct GenerateConfigurationCommand: AsyncParsableCommand {
}
private func _run() async throws {
@Dependency(\.cliClient) var cliClient
@Dependency(\.configurationClient) var configurationClient
try await cliClient.withLogger(globals.loggingOptions(commandName: Self.commandName)) {
try await globals.loggingOptions(commandName: Self.commandName).withLogger {
@Dependency(\.logger) var logger
let actualPath: File
let output = try await configurationClient.generate(.init(
force: force,
json: json,
path: path != nil ? .directory(path!) : nil
))
if let path, let file = File("\(path)/config.\(json ? "json" : "toml")") {
actualPath = file
} else {
actualPath = .default
}
logger.debug("Generating config at path: \(actualPath.path)")
try await configurationClient.generate(
at: actualPath,
force: force
)
print(output)
}
}
}

View File

@@ -1,5 +1,4 @@
import ArgumentParser
import CliClient
import Dependencies
import PlaybookClient

View File

@@ -1,6 +1,6 @@
import ArgumentParser
import CliClient
import CliDoc
import CommandClient
import Dependencies
struct InstallDependenciesCommand: AsyncParsableCommand {
@@ -33,11 +33,20 @@ struct InstallDependenciesCommand: AsyncParsableCommand {
var extraOptions: [String] = []
mutating func run() async throws {
@Dependency(\.cliClient) var cliClient
try await cliClient.installDependencies(
@Dependency(\.commandClient) var commandClient
@Dependency(\.playbookClient) var playbookClient
let arguments = [
"brew", "install"
] + Constants.brewPackages
+ extraOptions
try await commandClient.run(
quiet: globals.quiet,
shell: globals.shell,
extraArgs: extraOptions
arguments
)
try await playbookClient.repository.install()
}
}

View File

@@ -1,5 +1,4 @@
import ArgumentParser
import CliClient
import Dependencies
import VaultClient

View File

@@ -1,5 +1,4 @@
import ArgumentParser
import CliClient
import Dependencies
import VaultClient

View File

@@ -1,5 +1,5 @@
import ArgumentParser
import CliClient
import CommandClient
import VaultClient
// Holds the common options for vault commands, as they all share the
@@ -32,12 +32,6 @@ struct VaultOptions: ParsableArguments {
}
extension VaultOptions {
func loggingOptions(commandName: String) -> CliClient.LoggingOptions {
globals.loggingOptions(commandName: commandName)
}
}
extension VaultOptions {
func runOptions(