This commit is contained in:
@@ -9,8 +9,13 @@ struct Application: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: Constants.appName,
|
||||
abstract: createAbstract("A utility for working with ansible hpa playbook."),
|
||||
version: VERSION ?? "0.0.0",
|
||||
subcommands: [
|
||||
BuildCommand.self, CreateCommand.self, VaultCommand.self, UtilsCommand.self
|
||||
BuildCommand.self,
|
||||
CreateCommand.self,
|
||||
GenerateCommand.self,
|
||||
VaultCommand.self,
|
||||
UtilsCommand.self
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import ArgumentParser
|
||||
import CliClient
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import PlaybookClient
|
||||
|
||||
struct BuildCommand: AsyncParsableCommand {
|
||||
|
||||
@@ -9,29 +10,38 @@ struct BuildCommand: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration.playbook(
|
||||
commandName: commandName,
|
||||
abstract: "Build a home performance assesment project.",
|
||||
examples: (label: "Build Project", example: "\(commandName) /path/to/project")
|
||||
examples: (
|
||||
label: "Build project when in the project directory.",
|
||||
example: "\(commandName)"
|
||||
),
|
||||
(
|
||||
label: "Build project from outside the project directory.",
|
||||
example: "\(commandName) --project-directory /path/to/project"
|
||||
)
|
||||
)
|
||||
|
||||
@OptionGroup var globals: GlobalOptions
|
||||
|
||||
@Argument(
|
||||
@Option(
|
||||
help: "Path to the project directory.",
|
||||
completion: .directory
|
||||
)
|
||||
var projectDir: String
|
||||
var projectDirectory: String?
|
||||
|
||||
@Argument(
|
||||
help: "Extra arguments passed to the playbook."
|
||||
help: "Extra arguments / options passed to the playbook."
|
||||
)
|
||||
var extraArgs: [String] = []
|
||||
var extraOptions: [String] = []
|
||||
|
||||
mutating func run() async throws {
|
||||
try await runPlaybook(
|
||||
commandName: Self.commandName,
|
||||
globals: globals,
|
||||
extraArgs: extraArgs,
|
||||
"--tags", "build-project",
|
||||
"--extra-vars", "project_dir=\(projectDir)"
|
||||
)
|
||||
@Dependency(\.playbookClient) var playbookClient
|
||||
|
||||
try await playbookClient.run.buildProject(.init(
|
||||
projectDirectory: projectDirectory,
|
||||
shared: globals.sharedPlaybookRunOptions(
|
||||
commandName: Self.commandName,
|
||||
extraOptions: extraOptions
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import ArgumentParser
|
||||
import CliClient
|
||||
import ConfigurationClient
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import Logging
|
||||
import PlaybookClient
|
||||
|
||||
struct CreateCommand: AsyncParsableCommand {
|
||||
|
||||
static let commandName = "create"
|
||||
|
||||
static let configuration = CommandConfiguration.playbook2(
|
||||
static let configuration = CommandConfiguration.playbook(
|
||||
commandName: commandName,
|
||||
abstract: "Create a home performance assesment project.",
|
||||
examples: (
|
||||
@@ -53,118 +54,19 @@ struct CreateCommand: AsyncParsableCommand {
|
||||
@Argument(
|
||||
help: "Extra arguments passed to the playbook."
|
||||
)
|
||||
var extraArgs: [String] = []
|
||||
var extraOptions: [String] = []
|
||||
|
||||
mutating func run() async throws {
|
||||
try await _run()
|
||||
}
|
||||
|
||||
private func _run() async throws {
|
||||
try await withSetupLogger(commandName: Self.commandName, globals: globals) {
|
||||
@Dependency(\.cliClient) var cliClient
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
let encoder = cliClient.encoder()
|
||||
|
||||
let configuration = try cliClient.loadConfiguration()
|
||||
|
||||
logger.debug("Configuration: \(configuration)")
|
||||
|
||||
let jsonData = try parseOptions(
|
||||
command: self,
|
||||
configuration: configuration,
|
||||
logger: logger,
|
||||
encoder: encoder
|
||||
)
|
||||
|
||||
guard let jsonString = String(data: jsonData, encoding: .utf8) else {
|
||||
throw CreateError.encodingError
|
||||
}
|
||||
|
||||
logger.debug("JSON string: \(jsonString)")
|
||||
|
||||
try await runPlaybook(
|
||||
@Dependency(\.playbookClient) var playbookClient
|
||||
try await playbookClient.run.createProject(.init(
|
||||
projectDirectory: projectDir,
|
||||
shared: globals.sharedPlaybookRunOptions(
|
||||
commandName: Self.commandName,
|
||||
globals: self.globals,
|
||||
configuration: configuration,
|
||||
extraArgs: extraArgs,
|
||||
"--tags", "setup-project",
|
||||
"--extra-vars", "project_dir=\(self.projectDir)",
|
||||
"--extra-vars", "'\(jsonString)'"
|
||||
)
|
||||
}
|
||||
extraOptions: extraOptions
|
||||
),
|
||||
template: .init(directory: templateDir),
|
||||
useLocalTemplateDirectory: localTemplateDir
|
||||
))
|
||||
print(projectDir)
|
||||
}
|
||||
}
|
||||
|
||||
private func parseOptions(
|
||||
command: CreateCommand,
|
||||
configuration: Configuration,
|
||||
logger: Logger,
|
||||
encoder: JSONEncoder
|
||||
) throws -> Data {
|
||||
let templateDir = command.templateDir ?? configuration.templateDir
|
||||
let templateRepo = command.repo ?? configuration.templateRepo
|
||||
let version = (command.branch ?? configuration.templateRepoVersion) ?? "main"
|
||||
|
||||
logger.debug("""
|
||||
(\(command.localTemplateDir), \(String(describing: templateDir)), \(String(describing: templateRepo)))
|
||||
""")
|
||||
|
||||
switch (command.localTemplateDir, 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 CreateError.templateDirNotFound
|
||||
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 CreateError.templateDirOrRepoNotSpecified
|
||||
}
|
||||
}
|
||||
|
||||
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: repo, version: version ?? "main")
|
||||
}
|
||||
|
||||
struct Template: Encodable {
|
||||
let repo: String
|
||||
let version: String
|
||||
}
|
||||
}
|
||||
|
||||
enum CreateError: Error {
|
||||
case encodingError
|
||||
case templateDirNotFound
|
||||
case templateDirOrRepoNotSpecified
|
||||
}
|
||||
|
||||
14
Sources/hpa/GenerateCommands/GenerateCommand.swift
Normal file
14
Sources/hpa/GenerateCommands/GenerateCommand.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
import ArgumentParser
|
||||
|
||||
struct GenerateCommand: AsyncParsableCommand {
|
||||
static let commandName = "generate"
|
||||
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: commandName,
|
||||
subcommands: [
|
||||
GeneratePdfCommand.self,
|
||||
GenerateLatexCommand.self,
|
||||
GenerateHtmlCommand.self
|
||||
]
|
||||
)
|
||||
}
|
||||
24
Sources/hpa/GenerateCommands/GenerateHtmlCommand.swift
Normal file
24
Sources/hpa/GenerateCommands/GenerateHtmlCommand.swift
Normal file
@@ -0,0 +1,24 @@
|
||||
import ArgumentParser
|
||||
import Dependencies
|
||||
import PandocClient
|
||||
|
||||
// TODO: Need to add a step to build prior to generating file.
|
||||
struct GenerateHtmlCommand: AsyncParsableCommand {
|
||||
static let commandName = "html"
|
||||
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: commandName
|
||||
)
|
||||
|
||||
@OptionGroup var globals: GenerateOptions
|
||||
|
||||
mutating func run() async throws {
|
||||
@Dependency(\.pandocClient) var pandocClient
|
||||
|
||||
let output = try await pandocClient.run.generateHtml(
|
||||
globals.pandocRunOptions(commandName: Self.commandName)
|
||||
)
|
||||
print(output)
|
||||
}
|
||||
|
||||
}
|
||||
22
Sources/hpa/GenerateCommands/GenerateLatexCommand.swift
Normal file
22
Sources/hpa/GenerateCommands/GenerateLatexCommand.swift
Normal file
@@ -0,0 +1,22 @@
|
||||
import ArgumentParser
|
||||
import Dependencies
|
||||
|
||||
// TODO: Need to add a step to build prior to generating file.
|
||||
struct GenerateLatexCommand: AsyncParsableCommand {
|
||||
static let commandName = "latex"
|
||||
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: commandName
|
||||
)
|
||||
|
||||
@OptionGroup var globals: GenerateOptions
|
||||
|
||||
mutating func run() async throws {
|
||||
@Dependency(\.pandocClient) var pandocClient
|
||||
|
||||
let output = try await pandocClient.run.generateLatex(
|
||||
globals.pandocRunOptions(commandName: Self.commandName)
|
||||
)
|
||||
print(output)
|
||||
}
|
||||
}
|
||||
95
Sources/hpa/GenerateCommands/GenerateOptions.swift
Normal file
95
Sources/hpa/GenerateCommands/GenerateOptions.swift
Normal file
@@ -0,0 +1,95 @@
|
||||
import ArgumentParser
|
||||
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.",
|
||||
completion: .file()
|
||||
)
|
||||
var files: [String] = []
|
||||
|
||||
@Option(
|
||||
name: [.customShort("H"), .long],
|
||||
help: "Files to include in the header, can be specified multiple times.",
|
||||
completion: .file()
|
||||
)
|
||||
var includeInHeader: [String] = []
|
||||
|
||||
@Flag(
|
||||
help: "Do not build the project prior to generating the output."
|
||||
)
|
||||
var noBuild: Bool = false
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The project directory.",
|
||||
completion: .directory
|
||||
)
|
||||
var projectDirectory: String?
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The output directory",
|
||||
completion: .directory
|
||||
)
|
||||
var outputDirectory: String?
|
||||
|
||||
@Option(
|
||||
name: [.customShort("n"), .customLong("name")],
|
||||
help: "Name of the output file."
|
||||
)
|
||||
var outputFileName: String?
|
||||
|
||||
// NOTE: This must be last, both here and in the commands, so if the commands have options of their
|
||||
// own, they must be declared ahead of using the global options.
|
||||
|
||||
@Argument(
|
||||
help: "Extra arguments / options to pass to the underlying pandoc command."
|
||||
)
|
||||
var extraOptions: [String] = []
|
||||
|
||||
subscript<T>(dynamicMember keyPath: WritableKeyPath<BasicGlobalOptions, T>) -> T {
|
||||
get { basic[keyPath: keyPath] }
|
||||
set { basic[keyPath: keyPath] = newValue }
|
||||
}
|
||||
|
||||
subscript<T>(dynamicMember keyPath: KeyPath<BasicGlobalOptions, T>) -> T {
|
||||
basic[keyPath: keyPath]
|
||||
}
|
||||
}
|
||||
|
||||
extension GenerateOptions {
|
||||
|
||||
func loggingOptions(commandName: String) -> LoggingOptions {
|
||||
basic.loggingOptions(commandName: commandName)
|
||||
}
|
||||
|
||||
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,
|
||||
projectDirectory: projectDirectory,
|
||||
outputFileName: outputFileName,
|
||||
quiet: basic.quiet,
|
||||
shell: basic.shell,
|
||||
shouldBuild: !noBuild
|
||||
)
|
||||
}
|
||||
}
|
||||
32
Sources/hpa/GenerateCommands/GeneratePdfCommand.swift
Normal file
32
Sources/hpa/GenerateCommands/GeneratePdfCommand.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
import ArgumentParser
|
||||
import Dependencies
|
||||
import PandocClient
|
||||
|
||||
// TODO: Need to add a step to build prior to generating file.
|
||||
|
||||
struct GeneratePdfCommand: AsyncParsableCommand {
|
||||
static let commandName = "pdf"
|
||||
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: commandName
|
||||
)
|
||||
|
||||
@Option(
|
||||
name: [.customShort("e"), .customLong("engine")],
|
||||
help: "The pdf engine to use."
|
||||
)
|
||||
var pdfEngine: String?
|
||||
|
||||
@OptionGroup var globals: GenerateOptions
|
||||
|
||||
mutating func run() async throws {
|
||||
@Dependency(\.pandocClient) var pandocClient
|
||||
|
||||
let output = try await pandocClient.run.generatePdf(
|
||||
globals.pandocRunOptions(commandName: Self.commandName),
|
||||
pdfEngine: pdfEngine
|
||||
)
|
||||
|
||||
print(output)
|
||||
}
|
||||
}
|
||||
@@ -4,66 +4,17 @@ import Rainbow
|
||||
|
||||
extension CommandConfiguration {
|
||||
|
||||
static func create(
|
||||
commandName: String,
|
||||
abstract: String,
|
||||
usesExtraArgs: Bool = true,
|
||||
discussion: Discussion
|
||||
) -> Self {
|
||||
.init(
|
||||
commandName: commandName,
|
||||
abstract: createAbstract(abstract),
|
||||
discussion: discussion.render()
|
||||
)
|
||||
}
|
||||
|
||||
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 playbook2(
|
||||
commandName: String,
|
||||
abstract: String,
|
||||
parentCommand: String? = nil,
|
||||
examples: (label: String, example: String)...
|
||||
) -> Self {
|
||||
.init(
|
||||
commandName: commandName,
|
||||
abstract: Abstract { abstract.blue },
|
||||
usage: """
|
||||
\(Constants.appName)\(parentCommand != nil ? " \(parentCommand!)" : "") \(commandName)
|
||||
""".blue.bold.italic
|
||||
+ " [OPTIONS]".green
|
||||
+ " [ARGUMENTS]".cyan
|
||||
+ " --" + " [EXTRA-OPTIONS...]".magenta,
|
||||
discussion: CliDoc.Discussion.playbook(examples: examples)
|
||||
)
|
||||
}
|
||||
|
||||
static func playbook(
|
||||
commandName: String,
|
||||
abstract: String,
|
||||
parentCommand: String? = nil,
|
||||
examples: (label: String, example: String)...
|
||||
examples: Example...
|
||||
) -> Self {
|
||||
.create(
|
||||
.init(
|
||||
commandName: commandName,
|
||||
abstract: abstract,
|
||||
discussion: .default(
|
||||
usesExtraArgs: true,
|
||||
parentCommand: parentCommand,
|
||||
examples: examples,
|
||||
seeAlso: .seeAlso(label: "Ansible playbook options.", command: "ansible-playbook")
|
||||
)
|
||||
abstract: Abstract { abstract.blue },
|
||||
usage: Usage.default(commandName: commandName, parentCommand: parentCommand),
|
||||
discussion: Discussion.playbook(examples: examples)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -72,272 +23,39 @@ func createAbstract(_ string: String) -> String {
|
||||
"\(string.blue)"
|
||||
}
|
||||
|
||||
// TODO: Remove
|
||||
struct Discussion {
|
||||
let nodes: [Node]
|
||||
extension Usage where Content == AnyTextNode {
|
||||
|
||||
static func `default`(
|
||||
usesExtraArgs: Bool,
|
||||
commandName: String,
|
||||
parentCommand: String?,
|
||||
examples: [(label: String, example: String)],
|
||||
seeAlso: Node?
|
||||
usesArguments: Bool = true,
|
||||
usesOptions: Bool = true,
|
||||
usesExtraArguments: Bool = true
|
||||
) -> Self {
|
||||
var nodes = Array.defaultNodes + examples.nodes(parentCommand)
|
||||
if let seeAlso { nodes.append(seeAlso) }
|
||||
if usesExtraArgs && examples.count > 0 { nodes.append(.important(label: Constants.importantExtraArgsNote)) }
|
||||
return .init(
|
||||
nodes: nodes,
|
||||
usesExtraArgs: usesExtraArgs
|
||||
)
|
||||
}
|
||||
|
||||
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) {
|
||||
guard case let .example(.container(exampleNodes, _)) = nodes[lastExampleIndex] else {
|
||||
// this should never happen.
|
||||
print(nodes[lastExampleIndex])
|
||||
fatalError()
|
||||
.init {
|
||||
HStack {
|
||||
HStack {
|
||||
"\(Constants.appName)"
|
||||
if let parentCommand {
|
||||
parentCommand
|
||||
}
|
||||
commandName
|
||||
}
|
||||
.color(.blue).bold()
|
||||
|
||||
// Get the last element of the example container, which will be the
|
||||
// text for the example.
|
||||
guard let exampleNode = exampleNodes.last else {
|
||||
print(exampleNodes)
|
||||
fatalError()
|
||||
if usesOptions {
|
||||
"[OPTIONS]".color(.green)
|
||||
}
|
||||
if usesArguments {
|
||||
"[ARGUMENTS]".color(.cyan)
|
||||
}
|
||||
if usesExtraArguments {
|
||||
"--"
|
||||
"[EXTRA-OPTIONS]".color(.magenta)
|
||||
}
|
||||
|
||||
// replace the first element, which is the header so that we can
|
||||
// replace it with a new header.
|
||||
nodes.insert(
|
||||
.example(.exampleContainer(
|
||||
"Passing extra args.",
|
||||
"\(exampleNode.render().replacingOccurrences(of: " $ ", with: ""))"
|
||||
+ " -- --vault-id \"myId@$SCRIPTS/vault-gopass-client\""
|
||||
)),
|
||||
at: nodes.index(after: lastExampleIndex)
|
||||
)
|
||||
}
|
||||
}
|
||||
self.nodes = nodes
|
||||
}
|
||||
|
||||
func render() -> String { nodes.render() }
|
||||
}
|
||||
|
||||
enum Node {
|
||||
|
||||
struct Separator {
|
||||
|
||||
let string: String
|
||||
let count: Int
|
||||
|
||||
init(_ string: String, repeating count: Int = 1) {
|
||||
self.string = string
|
||||
self.count = count
|
||||
}
|
||||
|
||||
var value: String { string.repeating(count) }
|
||||
|
||||
static var empty: Self { .init("") }
|
||||
|
||||
static func space(_ count: Int = 1) -> Self {
|
||||
.init(" ", repeating: count)
|
||||
}
|
||||
|
||||
static func newLine(_ count: Int = 1) -> Self {
|
||||
.init("\n", repeating: count)
|
||||
}
|
||||
|
||||
static func tab(_ count: Int) -> Self {
|
||||
.init("\t", repeating: count)
|
||||
}
|
||||
}
|
||||
|
||||
/// A container that holds onto other nodes to be rendered.
|
||||
indirect case container(_ nodes: [Node], separator: Separator = .newLine())
|
||||
|
||||
/// A container to identify example nodes, so some other nodes can get injected
|
||||
/// when this is rendered.
|
||||
///
|
||||
/// NOTE: Things blow up if this is not used correctly, at least when using the `Discussion`
|
||||
/// to render the nodes, so this is not safe to create aside from the static methods that
|
||||
/// create an example node. If not using in the context of a `Discussion` then it's fine to
|
||||
/// use, as it doesn't do anything except stand as an type identifier.
|
||||
indirect case example(_ node: Node)
|
||||
|
||||
/// A root node that renders the given string without modification.
|
||||
case text(_ text: String)
|
||||
|
||||
static func container(separator: Separator = .newLine(), _ nodes: Node...) -> Self {
|
||||
.container(nodes, separator: separator)
|
||||
}
|
||||
|
||||
static func container(_ lhs: Node, _ rhs: Node, separator: Separator = .newLine()) -> Self {
|
||||
.container([lhs, rhs], separator: separator)
|
||||
}
|
||||
|
||||
static func styledText(_ text: String, color: NamedColor? = nil, styles: [Style]? = nil) -> Self {
|
||||
var string = text
|
||||
if let color {
|
||||
string = string.applyingColor(color)
|
||||
}
|
||||
if let styles {
|
||||
for style in styles {
|
||||
string = string.applyingStyle(style)
|
||||
}
|
||||
}
|
||||
return .text(string)
|
||||
}
|
||||
|
||||
static func styledText(_ text: String, color: NamedColor? = nil, style: Style) -> Self {
|
||||
.styledText(text, color: color, styles: [style])
|
||||
}
|
||||
|
||||
static func boldYellowText(_ text: String) -> Self {
|
||||
.styledText(text, color: .yellow, styles: [.bold])
|
||||
}
|
||||
|
||||
static func section(header: Node, content: Node, separator: Separator = .newLine()) -> Self {
|
||||
.container([header, content], separator: separator)
|
||||
}
|
||||
|
||||
static func section(header: String, label: Node, separator: Separator = .newLine()) -> Self {
|
||||
.section(header: .boldYellowText(header), content: label, separator: separator)
|
||||
}
|
||||
|
||||
static func important(label: String) -> Self {
|
||||
.section(
|
||||
header: .text("IMPORTANT NOTE:".red.bold.underline),
|
||||
content: .text(label.italic),
|
||||
separator: .newLine()
|
||||
)
|
||||
}
|
||||
|
||||
static func note(_ text: String, inline: Bool = true) -> Self {
|
||||
.section(header: "NOTE:", label: .text(text.italic), separator: inline ? .space() : .newLine())
|
||||
}
|
||||
|
||||
static func shellCommand(_ text: String, symbol: String = " $") -> Self {
|
||||
.text("\(symbol) \(text)")
|
||||
}
|
||||
|
||||
static func seeAlso(label: String, command: String, appendHelpToCommand: Bool = true) -> Self {
|
||||
.container([
|
||||
.container(.boldYellowText("See Also:"), .text(label.italic), separator: .space()),
|
||||
.shellCommand("\(appendHelpToCommand ? command + " --help" : command)")
|
||||
])
|
||||
}
|
||||
|
||||
static var exampleHeading: Self {
|
||||
.section(header: "Examples:", label: .text("Some common examples."), separator: .space())
|
||||
}
|
||||
|
||||
var isExampleNode: Bool {
|
||||
if case .example = self { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
fileprivate static func exampleContainer(_ label: String, _ command: String) -> Self {
|
||||
.container(
|
||||
.exampleLabel("\(label)\n"),
|
||||
.shellCommand(command, symbol: " $"),
|
||||
separator: .empty
|
||||
)
|
||||
}
|
||||
|
||||
/// Renders an example usage of the command.
|
||||
static func example(label: String, example: String, parentCommand: String?) -> Self {
|
||||
let string: String
|
||||
if let parent = parentCommand {
|
||||
string = "\(Constants.appName) \(parent) \(example)"
|
||||
} else {
|
||||
string = "\(Constants.appName) \(example)"
|
||||
}
|
||||
return .example(.exampleContainer(label, string))
|
||||
}
|
||||
|
||||
static func exampleLabel(_ label: String) -> Node {
|
||||
.text(" \(label.green.italic)")
|
||||
}
|
||||
|
||||
func render() -> String {
|
||||
switch self {
|
||||
case let .container(nodes, separator):
|
||||
return nodes.render(separator: separator)
|
||||
case let .text(text):
|
||||
return text
|
||||
case let .example(node):
|
||||
return node.render()
|
||||
.eraseToAnyTextNode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Node: ExpressibleByStringLiteral {
|
||||
|
||||
typealias StringLiteralType = String
|
||||
|
||||
init(stringLiteral value: String) {
|
||||
self = .text(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension Node: ExpressibleByArrayLiteral {
|
||||
|
||||
typealias ArrayLiteralElement = Node
|
||||
|
||||
init(arrayLiteral elements: Node...) {
|
||||
self = .container(elements)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Array where Element == (label: String, example: String) {
|
||||
func nodes(_ parentCommand: String?) -> [Node] {
|
||||
map { .example(label: $0.label, example: $0.example, parentCommand: parentCommand) }
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == Node {
|
||||
|
||||
static var defaultNodes: Self {
|
||||
[.note("Most options are not required if you have a configuration file setup.")]
|
||||
}
|
||||
|
||||
fileprivate func render(separator: Node.Separator = .newLine()) -> String {
|
||||
map {
|
||||
// Strip of any new-line characters from the last section of the rendered string
|
||||
// of the node. This allows us to have a consistent single new-line between each
|
||||
// rendered node.
|
||||
var string = $0.render()[...]
|
||||
while string.hasSuffix("\n") {
|
||||
string = string.dropLast()
|
||||
}
|
||||
return String(string)
|
||||
}
|
||||
.joined(separator: separator.value)
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
func repeating(_ count: Int) -> Self {
|
||||
guard count > 0 else { return self }
|
||||
var count = count
|
||||
var output = self
|
||||
|
||||
while count > 0 {
|
||||
output = "\(output)\(self)"
|
||||
count -= 1
|
||||
}
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = """
|
||||
|
||||
@@ -1,86 +1,146 @@
|
||||
import CliDoc
|
||||
|
||||
extension CliDoc.Discussion {
|
||||
extension CliDoc.Discussion where Content == AnyTextNode {
|
||||
static func playbook(
|
||||
examples: [(label: String, example: String)]
|
||||
examples: [Example]
|
||||
) -> Self {
|
||||
.init {
|
||||
Section {
|
||||
VStack {
|
||||
Note.mostOptionsNotRequired
|
||||
Examples(examples: examples)
|
||||
ExampleSection.default(examples: examples)
|
||||
SeeAlso(label: "Ansible playbook options.", command: "ansible-playbook --help")
|
||||
ImportantNote.passingExtraArgs
|
||||
}
|
||||
.labeledContentStyle(.custom("foo:"))
|
||||
.separator(.newLine(count: 2))
|
||||
.eraseToAnyTextNode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ShellCommand {
|
||||
extension Array where Element == Example {
|
||||
func addingPassingOptions(command: String) -> Self {
|
||||
var output = self
|
||||
output.append((
|
||||
label: "Passing extra arguments / options",
|
||||
example: "\(command) -- --vaultId=myId@$SCRIPTS/vault-gopass-client"
|
||||
))
|
||||
return output
|
||||
}
|
||||
|
||||
func addingPipeToOtherCommand(command: String) -> Self {
|
||||
var output = self
|
||||
output.append((
|
||||
label: "Piping output to other command.",
|
||||
example: "\(command) --quiet | xargs open"
|
||||
))
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
extension ExampleSection where Header == String, Label == String {
|
||||
static func `default`(
|
||||
examples: [Example],
|
||||
usesPassingExtraOptions: Bool = true,
|
||||
usesPipeToOtherCommand: Bool = true
|
||||
) -> some TextNode {
|
||||
var examples = examples
|
||||
|
||||
if let first = examples.first {
|
||||
if usesPassingExtraOptions {
|
||||
examples = examples.addingPassingOptions(command: first.example)
|
||||
}
|
||||
if usesPipeToOtherCommand {
|
||||
examples = examples.addingPipeToOtherCommand(command: first.example)
|
||||
}
|
||||
}
|
||||
return Self(examples: examples) {
|
||||
"EXAMPLES:"
|
||||
} label: {
|
||||
"Some common usage examples."
|
||||
}
|
||||
.exampleStyle(HPAExampleStyle())
|
||||
}
|
||||
}
|
||||
|
||||
struct HPAExampleStyle: ExampleStyle {
|
||||
|
||||
func render(content: ExampleConfiguration) -> some TextNode {
|
||||
VStack {
|
||||
content.examples.map { example in
|
||||
VStack {
|
||||
example.label.color(.green).bold()
|
||||
ShellCommand.hpaCommand(example.example)
|
||||
}
|
||||
}
|
||||
}
|
||||
.separator(.newLine(count: 2))
|
||||
}
|
||||
}
|
||||
|
||||
extension ShellCommand where Content == String, Symbol == String {
|
||||
static func hpaCommand(_ command: String) -> Self {
|
||||
.init(command: "\(Constants.appName) \(command)")
|
||||
.init { "\(Constants.appName) \(command)" } symbol: { "$" }
|
||||
}
|
||||
}
|
||||
|
||||
struct SeeAlso: NodeRepresentable {
|
||||
struct SeeAlso<Label: TextNode, Content: TextNode>: TextNode {
|
||||
|
||||
let node: any NodeRepresentable
|
||||
|
||||
init(label: String, command: String) {
|
||||
self.node = Group(separator: "\n") {
|
||||
Note("SEE ALSO:") {
|
||||
label
|
||||
}
|
||||
ShellCommand(command: command)
|
||||
}
|
||||
}
|
||||
|
||||
func render() -> String {
|
||||
node.render()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct Examples: NodeRepresentable {
|
||||
typealias Example = (label: String, example: String)
|
||||
let examples: [Example]
|
||||
|
||||
func render() -> String {
|
||||
Group(separator: "\n") {
|
||||
Note("EXAMPLES:") { "Common usage examples." }
|
||||
for (label, command) in examples {
|
||||
LabeledContent("\t\(label.green.bold)", separator: "\n") {
|
||||
ShellCommand.hpaCommand(command)
|
||||
}
|
||||
}
|
||||
if let first = examples.first {
|
||||
LabeledContent("\n\tPassing extra options.".green.bold, separator: "\n") {
|
||||
ShellCommand.hpaCommand("\(first.example) -- --vault-id \"myId@$SCRIPTS/vault-gopass-client\"")
|
||||
}
|
||||
}
|
||||
}.render()
|
||||
}
|
||||
}
|
||||
|
||||
struct Note: NodeRepresentable {
|
||||
|
||||
let node: any NodeRepresentable
|
||||
let title = "SEE ALSO:"
|
||||
let label: Label
|
||||
let content: Content
|
||||
|
||||
init(
|
||||
_ label: String = "NOTE:",
|
||||
@NodeBuilder _ content: () -> any NodeRepresentable
|
||||
@TextBuilder content: () -> Content,
|
||||
@TextBuilder label: () -> Label
|
||||
) {
|
||||
self.node = LabeledContent(
|
||||
separator: " ",
|
||||
label: { label.yellow.bold },
|
||||
content: { content().style(.italic) }
|
||||
)
|
||||
self.content = content()
|
||||
self.label = label()
|
||||
}
|
||||
|
||||
func render() -> String {
|
||||
node.render()
|
||||
var body: some TextNode {
|
||||
VStack {
|
||||
LabeledContent {
|
||||
label.italic()
|
||||
} label: {
|
||||
title.color(.yellow).bold().underline()
|
||||
}
|
||||
ShellCommand { content } symbol: { "$" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SeeAlso where Content == String, Label == Empty {
|
||||
init(command: String) {
|
||||
self.init { "\(Constants.appName) \(command)" } label: { Empty() }
|
||||
}
|
||||
}
|
||||
|
||||
extension SeeAlso where Content == String, Label == String {
|
||||
init(label: String, command: String) {
|
||||
self.init { "\(Constants.appName) \(command)" } label: { label }
|
||||
}
|
||||
}
|
||||
|
||||
struct Note<Content: TextNode>: TextNode {
|
||||
|
||||
let label: String = "NOTE:"
|
||||
let content: Content
|
||||
|
||||
init(
|
||||
@TextBuilder _ content: () -> Content
|
||||
) {
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
var body: some TextNode {
|
||||
HStack {
|
||||
label.color(.yellow).bold()
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Note where Content == String {
|
||||
static var mostOptionsNotRequired: Self {
|
||||
.init {
|
||||
"Most options are not required if you have a configuration file setup."
|
||||
@@ -88,26 +148,27 @@ struct Note: NodeRepresentable {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Fix the text.
|
||||
struct ImportantNote: NodeRepresentable {
|
||||
struct ImportantNote<Content: TextNode>: TextNode {
|
||||
|
||||
let node: any NodeRepresentable
|
||||
let label: String = "IMPORTANT NOTE:"
|
||||
let content: Content
|
||||
|
||||
init(
|
||||
_ label: String = "IMPORTANT NOTE:",
|
||||
@NodeBuilder _ content: () -> any NodeRepresentable
|
||||
@TextBuilder _ content: () -> Content
|
||||
) {
|
||||
self.node = LabeledContent(
|
||||
separator: "\n",
|
||||
label: { label.red.bold.underline },
|
||||
content: { content().style(.italic) }
|
||||
)
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
func render() -> String {
|
||||
node.render()
|
||||
var body: some TextNode {
|
||||
HStack {
|
||||
label.color(.red).bold().underline()
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ImportantNote where Content == String {
|
||||
static var passingExtraArgs: Self {
|
||||
.init {
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import ArgumentParser
|
||||
import CommandClient
|
||||
import ConfigurationClient
|
||||
import PlaybookClient
|
||||
|
||||
struct BasicGlobalOptions: ParsableArguments {
|
||||
@Flag(
|
||||
@@ -26,10 +29,10 @@ struct BasicGlobalOptions: ParsableArguments {
|
||||
struct GlobalOptions: ParsableArguments {
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
name: .long,
|
||||
help: "Optional path to the ansible hpa playbook directory."
|
||||
)
|
||||
var playbookDir: String?
|
||||
var playbookDirectory: String?
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
@@ -55,3 +58,39 @@ struct GlobalOptions: ParsableArguments {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension GlobalOptions {
|
||||
func loggingOptions(commandName: String) -> LoggingOptions {
|
||||
.init(
|
||||
commandName: commandName,
|
||||
logLevel: .init(globals: basic, quietOnlyPlaybook: quietOnlyPlaybook)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension BasicGlobalOptions {
|
||||
func loggingOptions(commandName: String) -> LoggingOptions {
|
||||
.init(
|
||||
commandName: commandName,
|
||||
logLevel: .init(globals: self, quietOnlyPlaybook: false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension GlobalOptions {
|
||||
func sharedPlaybookRunOptions(
|
||||
commandName: String,
|
||||
extraOptions: [String]?
|
||||
) -> PlaybookClient.RunPlaybook.SharedRunOptions {
|
||||
return .init(
|
||||
extraOptions: extraOptions,
|
||||
inventoryFilePath: inventoryPath,
|
||||
loggingOptions: .init(
|
||||
commandName: commandName,
|
||||
logLevel: .init(globals: basic, quietOnlyPlaybook: quietOnlyPlaybook)
|
||||
),
|
||||
quiet: basic.quiet,
|
||||
shell: basic.shell
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Dependencies
|
||||
import Logging
|
||||
|
||||
extension Logger.Level {
|
||||
@@ -21,34 +20,3 @@ extension Logger.Level {
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import ArgumentParser
|
||||
import CliClient
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import Logging
|
||||
import Rainbow
|
||||
import ShellClient
|
||||
|
||||
func runPlaybook(
|
||||
commandName: String,
|
||||
globals: GlobalOptions,
|
||||
configuration: Configuration? = nil,
|
||||
extraArgs: [String],
|
||||
_ args: [String]
|
||||
) async throws {
|
||||
try await withSetupLogger(commandName: commandName, globals: globals) {
|
||||
@Dependency(\.cliClient) var cliClient
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
logger.debug("Begin run playbook: \(globals)")
|
||||
|
||||
let configuration = try cliClient.ensuredConfiguration(configuration)
|
||||
|
||||
logger.debug("Configuration: \(configuration)")
|
||||
|
||||
let playbookDir = try ensureString(
|
||||
globals: globals,
|
||||
configuration: configuration,
|
||||
globalsKeyPath: \.playbookDir,
|
||||
configurationKeyPath: \.playbookDir
|
||||
)
|
||||
let playbook = "\(playbookDir)/\(Constants.playbookFileName)"
|
||||
|
||||
let inventory = (try? ensureString(
|
||||
globals: globals,
|
||||
configuration: configuration,
|
||||
globalsKeyPath: \.inventoryPath,
|
||||
configurationKeyPath: \.inventoryPath
|
||||
)) ?? "\(playbookDir)/\(Constants.inventoryFileName)"
|
||||
|
||||
let defaultArgs = (configuration.defaultPlaybookArgs ?? [])
|
||||
+ (configuration.defaultVaultArgs ?? [])
|
||||
|
||||
try await cliClient.runCommand(
|
||||
quiet: globals.quietOnlyPlaybook ? true : globals.quiet,
|
||||
shell: globals.shellOrDefault,
|
||||
[
|
||||
"ansible-playbook", playbook,
|
||||
"--inventory", inventory
|
||||
] + args
|
||||
+ extraArgs
|
||||
+ defaultArgs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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<GlobalOptions, String?>,
|
||||
configurationKeyPath: KeyPath<Configuration, String?>
|
||||
) 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
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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 ?? []
|
||||
|
||||
var vaultArgs = ["ansible-vault"]
|
||||
+ args
|
||||
+ defaultArgs
|
||||
+ options.extraArgs
|
||||
+ [path]
|
||||
|
||||
if args.contains("encrypt"),
|
||||
!vaultArgs.contains("--encrypt-vault-id"),
|
||||
let id = configuration.defaultVaultEncryptId
|
||||
{
|
||||
vaultArgs.append(contentsOf: ["--encrypt-vault-id", id])
|
||||
}
|
||||
|
||||
try await cliClient.runCommand(
|
||||
quiet: options.quiet,
|
||||
shell: options.shellOrDefault,
|
||||
vaultArgs
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import ArgumentParser
|
||||
|
||||
struct CreateProjectTemplateCommand: AsyncParsableCommand {
|
||||
|
||||
static let commandName = "create-template"
|
||||
|
||||
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")
|
||||
)
|
||||
|
||||
@OptionGroup var globals: GlobalOptions
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "Customize the directory where template variables are stored."
|
||||
)
|
||||
var templateVars: String?
|
||||
|
||||
@Flag(
|
||||
name: .long,
|
||||
help: "Do not generate ansible-vault variables file."
|
||||
)
|
||||
var noVault: Bool = false
|
||||
|
||||
@Argument(
|
||||
help: "Path to the project template directory.",
|
||||
completion: .directory
|
||||
)
|
||||
var path: String
|
||||
|
||||
@Argument(
|
||||
help: "Extra arguments passed to the playbook."
|
||||
)
|
||||
var extraArgs: [String] = []
|
||||
|
||||
mutating func run() async throws {
|
||||
let varsDir = templateVars != nil
|
||||
? ["--extra-vars", "repo_vars_dir=\(templateVars!)"]
|
||||
: []
|
||||
|
||||
let vault = noVault ? ["--extra-vars", "use_vault=false"] : []
|
||||
|
||||
try await runPlaybook(
|
||||
commandName: Self.commandName,
|
||||
globals: globals,
|
||||
extraArgs: extraArgs,
|
||||
[
|
||||
"--tags", "repo-template",
|
||||
"--extra-vars", "output_dir=\(path)"
|
||||
] + varsDir
|
||||
+ vault
|
||||
)
|
||||
}
|
||||
}
|
||||
27
Sources/hpa/UtilsCommands/DumpConfigCommand.swift
Normal file
27
Sources/hpa/UtilsCommands/DumpConfigCommand.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
import ArgumentParser
|
||||
import CliDoc
|
||||
import ConfigurationClient
|
||||
import CustomDump
|
||||
import Dependencies
|
||||
|
||||
struct DumpConfigCommand: AsyncParsableCommand {
|
||||
static let commandName = "dump-config"
|
||||
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: commandName,
|
||||
abstract: createAbstract("Show the current configuration."),
|
||||
usage: .default(commandName: commandName, parentCommand: "utils", usesArguments: false, usesExtraArguments: false),
|
||||
discussion: Discussion {
|
||||
"Useful to debug your configuration settings / make sure they load properly."
|
||||
}
|
||||
)
|
||||
|
||||
func run() async throws {
|
||||
@Dependency(\.configurationClient) var configurationClient
|
||||
|
||||
let configuration = try await configurationClient.findAndLoad()
|
||||
|
||||
customDump(configuration)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import ArgumentParser
|
||||
import CliClient
|
||||
import CliDoc
|
||||
import CommandClient
|
||||
import ConfigurationClient
|
||||
import Dependencies
|
||||
|
||||
struct GenerateConfigurationCommand: AsyncParsableCommand {
|
||||
@@ -9,18 +11,23 @@ struct GenerateConfigurationCommand: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: commandName,
|
||||
abstract: createAbstract("Generate a local configuration file."),
|
||||
discussion: """
|
||||
|
||||
\("NOTE:".yellow) If a directory is not supplied then a configuration file will be created
|
||||
at \("'~/.config/hpa-playbook/config'".yellow).
|
||||
|
||||
\("Example:".yellow)
|
||||
|
||||
\("Create a directory and generate the configuration file.".green)
|
||||
$ mkdir -p ~/.config/hpa-playbook
|
||||
$ hpa generate-config --path ~/.config/hpa-playbook
|
||||
|
||||
"""
|
||||
discussion: Discussion {
|
||||
VStack {
|
||||
Note {
|
||||
"""
|
||||
If a directory is not supplied then a configuration file will be created
|
||||
at \("'~/.config/hpa/config.toml'".yellow).
|
||||
"""
|
||||
}
|
||||
VStack {
|
||||
"EXAMPLE:".yellow.bold
|
||||
"Create a directory and generate the configuration".green
|
||||
ShellCommand("mkdir -p ~/.config/hpa")
|
||||
ShellCommand("hpa generate-config --path ~/.config/hpa")
|
||||
}
|
||||
}
|
||||
.separator(.newLine(count: 2))
|
||||
}
|
||||
)
|
||||
|
||||
@Option(
|
||||
@@ -32,10 +39,16 @@ 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
|
||||
|
||||
@Flag(
|
||||
name: .shortAndLong,
|
||||
help: "Force generation, overwriting a file if it exists."
|
||||
)
|
||||
var force: Bool = false
|
||||
|
||||
@OptionGroup var globals: BasicGlobalOptions
|
||||
|
||||
mutating func run() async throws {
|
||||
@@ -43,24 +56,18 @@ struct GenerateConfigurationCommand: AsyncParsableCommand {
|
||||
}
|
||||
|
||||
private func _run() async throws {
|
||||
try await withSetupLogger(commandName: Self.commandName, globals: globals) {
|
||||
@Dependency(\.cliClient) var cliClient
|
||||
@Dependency(\.configurationClient) var configurationClient
|
||||
|
||||
let actualPath: String
|
||||
try await globals.loggingOptions(commandName: Self.commandName).withLogger {
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
if let path {
|
||||
actualPath = "\(path)/config"
|
||||
} else {
|
||||
let path = "~/.config/hpa-playbook/"
|
||||
try await cliClient.runCommand(
|
||||
quiet: false,
|
||||
shell: globals.shellOrDefault,
|
||||
"mkdir", "-p", path
|
||||
)
|
||||
actualPath = "\(path)/config"
|
||||
}
|
||||
let output = try await configurationClient.generate(.init(
|
||||
force: force,
|
||||
json: json,
|
||||
path: path != nil ? .directory(path!) : nil
|
||||
))
|
||||
|
||||
try cliClient.createConfiguration(actualPath, json)
|
||||
print(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import ArgumentParser
|
||||
import Dependencies
|
||||
import PlaybookClient
|
||||
|
||||
struct GenerateProjectTemplateCommand: AsyncParsableCommand {
|
||||
|
||||
static let commandName = "generate-template"
|
||||
|
||||
static let configuration = CommandConfiguration.playbook(
|
||||
commandName: commandName,
|
||||
abstract: "Generate a home performance assesment project template.",
|
||||
parentCommand: UtilsCommand.commandName,
|
||||
examples: (label: "Generate Template", example: "\(commandName) /path/to/project-template")
|
||||
)
|
||||
|
||||
@OptionGroup var globals: GlobalOptions
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "Customize the directory where template variables are stored."
|
||||
)
|
||||
var templateVars: String?
|
||||
|
||||
@Flag(
|
||||
name: .long,
|
||||
help: "Do not generate ansible-vault variables file."
|
||||
)
|
||||
var noVault: Bool = false
|
||||
|
||||
@Argument(
|
||||
help: "Path to the project template directory.",
|
||||
completion: .directory
|
||||
)
|
||||
var path: String
|
||||
|
||||
@Argument(
|
||||
help: "Extra arguments / options passed to the playbook."
|
||||
)
|
||||
var extraOptions: [String] = []
|
||||
|
||||
mutating func run() async throws {
|
||||
@Dependency(\.playbookClient) var playbookClient
|
||||
|
||||
let output = try await playbookClient.run.generateTemplate(.init(
|
||||
shared: globals.sharedPlaybookRunOptions(
|
||||
commandName: Self.commandName,
|
||||
extraOptions: extraOptions
|
||||
),
|
||||
templateDirectory: path,
|
||||
templateVarsDirectory: templateVars,
|
||||
useVault: !noVault
|
||||
))
|
||||
|
||||
print(output)
|
||||
}
|
||||
}
|
||||
52
Sources/hpa/UtilsCommands/InstallDependenciesCommand.swift
Normal file
52
Sources/hpa/UtilsCommands/InstallDependenciesCommand.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
import ArgumentParser
|
||||
import CliDoc
|
||||
import CommandClient
|
||||
import Dependencies
|
||||
|
||||
struct InstallDependenciesCommand: AsyncParsableCommand {
|
||||
static let commandName: String = "install-dependencies"
|
||||
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: commandName,
|
||||
abstract: createAbstract("Ensure required dependencies are installed"),
|
||||
usage: .default(commandName: commandName, parentCommand: "utils", usesArguments: false),
|
||||
discussion: Discussion {
|
||||
VStack {
|
||||
Note {
|
||||
"Homebrew is required to install dependencies."
|
||||
}
|
||||
HStack {
|
||||
"See Also:".yellow.bold.underline
|
||||
"https://brew.sh"
|
||||
}
|
||||
ImportantNote.passingExtraArgs
|
||||
}
|
||||
.separator(.newLine(count: 2))
|
||||
}
|
||||
)
|
||||
|
||||
@OptionGroup var globals: BasicGlobalOptions
|
||||
|
||||
@Argument(
|
||||
help: "Extra arguments / options to pass to the homebrew command."
|
||||
)
|
||||
var extraOptions: [String] = []
|
||||
|
||||
mutating func run() async throws {
|
||||
@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,
|
||||
arguments
|
||||
)
|
||||
|
||||
try await playbookClient.repository.install()
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,15 @@ struct UtilsCommand: AsyncParsableCommand {
|
||||
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: commandName,
|
||||
abstract: "\("Utility commands.".blue)",
|
||||
abstract: createAbstract("Utility commands."),
|
||||
discussion: """
|
||||
These are commands that are generally only run on occasion / less frequently used.
|
||||
""",
|
||||
subcommands: [
|
||||
CreateProjectTemplateCommand.self, GenerateConfigurationCommand.self
|
||||
DumpConfigCommand.self,
|
||||
GenerateProjectTemplateCommand.self,
|
||||
GenerateConfigurationCommand.self,
|
||||
InstallDependenciesCommand.self
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import ArgumentParser
|
||||
import Dependencies
|
||||
import VaultClient
|
||||
|
||||
struct DecryptCommand: AsyncParsableCommand {
|
||||
|
||||
@@ -9,23 +11,22 @@ struct DecryptCommand: AsyncParsableCommand {
|
||||
abstract: createAbstract("Decrypt a vault file.")
|
||||
)
|
||||
|
||||
@OptionGroup var options: VaultOptions
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "Output file."
|
||||
)
|
||||
var output: String?
|
||||
|
||||
@OptionGroup var options: VaultOptions
|
||||
|
||||
mutating func run() async throws {
|
||||
var args = ["decrypt"]
|
||||
if let output {
|
||||
args.append(contentsOf: ["--output", output])
|
||||
}
|
||||
try await runVault(
|
||||
@Dependency(\.vaultClient) var vaultClient
|
||||
|
||||
let output = try await vaultClient.run.decrypt(options.runOptions(
|
||||
commandName: Self.commandName,
|
||||
options: options,
|
||||
args
|
||||
)
|
||||
outputFilePath: output
|
||||
))
|
||||
|
||||
print(output)
|
||||
}
|
||||
}
|
||||
|
||||
1
Sources/hpa/VaultCommands/EditCommand.swift
Normal file
1
Sources/hpa/VaultCommands/EditCommand.swift
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import ArgumentParser
|
||||
import Dependencies
|
||||
import VaultClient
|
||||
|
||||
struct EncryptCommand: AsyncParsableCommand {
|
||||
|
||||
@@ -9,23 +11,22 @@ struct EncryptCommand: AsyncParsableCommand {
|
||||
abstract: createAbstract("Encrypt a vault file.")
|
||||
)
|
||||
|
||||
@OptionGroup var options: VaultOptions
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "Output file."
|
||||
)
|
||||
var output: String?
|
||||
|
||||
@OptionGroup var options: VaultOptions
|
||||
|
||||
mutating func run() async throws {
|
||||
var args = ["encrypt"]
|
||||
if let output {
|
||||
args.append(contentsOf: ["--output", output])
|
||||
}
|
||||
try await runVault(
|
||||
@Dependency(\.vaultClient) var vaultClient
|
||||
|
||||
let output = try await vaultClient.run.encrypt(options.runOptions(
|
||||
commandName: Self.commandName,
|
||||
options: options,
|
||||
args
|
||||
)
|
||||
outputFilePath: output
|
||||
))
|
||||
|
||||
print(output)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ArgumentParser
|
||||
import CliDoc
|
||||
|
||||
struct VaultCommand: AsyncParsableCommand {
|
||||
|
||||
@@ -7,14 +8,23 @@ struct VaultCommand: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: commandName,
|
||||
abstract: createAbstract("Vault commands."),
|
||||
discussion: Discussion(
|
||||
.text("""
|
||||
Allows you to run `ansible-vault` commands on your project or project-template.
|
||||
"""),
|
||||
.seeAlso(label: "Ansible Vault", command: "ansible-vault")
|
||||
).render(),
|
||||
discussion: Discussion {
|
||||
VStack {
|
||||
"""
|
||||
Allows you to run `ansible-vault` commands on your project or project-template.
|
||||
|
||||
Ansible-vault allows you to store sensitive variables in an encrypted format.
|
||||
"""
|
||||
SeeAlso {
|
||||
"ansible-vault --help"
|
||||
} label: {
|
||||
"Ansible Vault"
|
||||
}
|
||||
}
|
||||
.separator(.newLine(count: 2))
|
||||
},
|
||||
subcommands: [
|
||||
EncryptCommand.self, DecryptCommand.self
|
||||
DecryptCommand.self, EncryptCommand.self
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import ArgumentParser
|
||||
import CommandClient
|
||||
import VaultClient
|
||||
|
||||
// Holds the common options for vault commands, as they all share the
|
||||
// same structure.
|
||||
@@ -17,7 +19,7 @@ struct VaultOptions: ParsableArguments {
|
||||
@Argument(
|
||||
help: "Extra arguments to pass to the vault command."
|
||||
)
|
||||
var extraArgs: [String] = []
|
||||
var extraOptions: [String] = []
|
||||
|
||||
subscript<T>(dynamicMember keyPath: WritableKeyPath<BasicGlobalOptions, T>) -> T {
|
||||
get { globals[keyPath: keyPath] }
|
||||
@@ -29,3 +31,23 @@ struct VaultOptions: ParsableArguments {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension VaultOptions {
|
||||
|
||||
func runOptions(
|
||||
commandName: String,
|
||||
outputFilePath: String? = nil
|
||||
) -> VaultClient.RunOptions {
|
||||
.init(
|
||||
extraOptions: extraOptions,
|
||||
loggingOptions: .init(
|
||||
commandName: commandName,
|
||||
logLevel: .init(globals: globals, quietOnlyPlaybook: false)
|
||||
),
|
||||
outputFilePath: outputFilePath,
|
||||
quiet: globals.quiet,
|
||||
shell: globals.shell,
|
||||
vaultFilePath: file
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
2
Sources/hpa/Version.swift
Normal file
2
Sources/hpa/Version.swift
Normal file
@@ -0,0 +1,2 @@
|
||||
// Do not set this variable, it is set during the build process.
|
||||
let VERSION: String? = nil
|
||||
Reference in New Issue
Block a user