feat: Adds generate commands that call to pandoc to generate pdf, latex, and html files from a project.
This commit is contained in:
@@ -32,9 +32,6 @@ let package = Package(
|
|||||||
.product(name: "CliDoc", package: "swift-cli-doc"),
|
.product(name: "CliDoc", package: "swift-cli-doc"),
|
||||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
.product(name: "ShellClient", package: "swift-shell-client")
|
.product(name: "ShellClient", package: "swift-shell-client")
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
.plugin(name: "BuildWithVersionPlugin", package: "swift-cli-version")
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
@@ -109,7 +106,10 @@ let package = Package(
|
|||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "PlaybookClientTests",
|
name: "PlaybookClientTests",
|
||||||
dependencies: ["PlaybookClient"]
|
dependencies: [
|
||||||
|
"PlaybookClient",
|
||||||
|
"TestSupport"
|
||||||
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
223
Sources/CliClient/CliClient+PandocCommands.swift
Normal file
223
Sources/CliClient/CliClient+PandocCommands.swift
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,12 @@ public enum CliClientError: Error {
|
|||||||
case brewfileNotFound
|
case brewfileNotFound
|
||||||
case encodingError
|
case encodingError
|
||||||
case playbookDirectoryNotFound
|
case playbookDirectoryNotFound
|
||||||
|
case generate(GenerateError)
|
||||||
case templateDirectoryNotFound
|
case templateDirectoryNotFound
|
||||||
case templateDirectoryOrRepoNotSpecified
|
case templateDirectoryOrRepoNotSpecified
|
||||||
case vaultFileNotFound
|
case vaultFileNotFound
|
||||||
|
|
||||||
|
public enum GenerateError: Sendable {
|
||||||
|
case projectDirectoryNotSpecified
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,52 @@ public struct CliClient: Sendable {
|
|||||||
|
|
||||||
public extension CliClient {
|
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 {
|
struct GenerateJsonOptions: Equatable, Sendable {
|
||||||
let templateDirectory: String?
|
let templateDirectory: String?
|
||||||
let templateRepo: String?
|
let templateRepo: String?
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
// NOTE: When adding items, then the 'hpa.toml' resource file needs to be updated.
|
||||||
|
|
||||||
/// Represents configurable settings for the command line tool.
|
/// Represents configurable settings for the command line tool.
|
||||||
public struct Configuration: Codable, Equatable, Sendable {
|
public struct Configuration: Codable, Equatable, Sendable {
|
||||||
public let args: [String]?
|
public let args: [String]?
|
||||||
public let useVaultArgs: Bool
|
public let useVaultArgs: Bool
|
||||||
|
public let generate: Generate?
|
||||||
public let playbook: Playbook?
|
public let playbook: Playbook?
|
||||||
public let template: Template
|
public let template: Template
|
||||||
public let vault: Vault
|
public let vault: Vault
|
||||||
@@ -11,12 +14,14 @@ public struct Configuration: Codable, Equatable, Sendable {
|
|||||||
public init(
|
public init(
|
||||||
args: [String]? = nil,
|
args: [String]? = nil,
|
||||||
useVaultArgs: Bool = true,
|
useVaultArgs: Bool = true,
|
||||||
|
generate: Generate? = nil,
|
||||||
playbook: Playbook? = nil,
|
playbook: Playbook? = nil,
|
||||||
template: Template = .init(),
|
template: Template = .init(),
|
||||||
vault: Vault = .init()
|
vault: Vault = .init()
|
||||||
) {
|
) {
|
||||||
self.args = args
|
self.args = args
|
||||||
self.useVaultArgs = useVaultArgs
|
self.useVaultArgs = useVaultArgs
|
||||||
|
self.generate = generate
|
||||||
self.playbook = playbook
|
self.playbook = playbook
|
||||||
self.template = template
|
self.template = template
|
||||||
self.vault = vault
|
self.vault = vault
|
||||||
@@ -32,6 +37,40 @@ public struct Configuration: Codable, Equatable, Sendable {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct Generate: Codable, Equatable, Sendable {
|
||||||
|
public let buildDirectory: String?
|
||||||
|
public let files: [String]?
|
||||||
|
public let includeInHeader: [String]?
|
||||||
|
public let outputFileName: String?
|
||||||
|
public let pdfEngine: String?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
buildDirectory: String? = nil,
|
||||||
|
files: [String]? = nil,
|
||||||
|
includeInHeader: [String]? = nil,
|
||||||
|
outputFileName: String? = nil,
|
||||||
|
pdfEngine: String? = nil
|
||||||
|
) {
|
||||||
|
self.buildDirectory = buildDirectory
|
||||||
|
self.files = files
|
||||||
|
self.includeInHeader = includeInHeader
|
||||||
|
self.outputFileName = outputFileName
|
||||||
|
self.pdfEngine = pdfEngine
|
||||||
|
}
|
||||||
|
|
||||||
|
public static let `default` = Self.mock
|
||||||
|
|
||||||
|
public static var mock: Self {
|
||||||
|
.init(
|
||||||
|
buildDirectory: ".build",
|
||||||
|
files: ["Report.md", "Definitions.md"],
|
||||||
|
includeInHeader: ["head.tex", "footer.tex"],
|
||||||
|
outputFileName: "Report",
|
||||||
|
pdfEngine: "xelatex"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct Playbook: Codable, Equatable, Sendable {
|
public struct Playbook: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
public let directory: String?
|
public let directory: String?
|
||||||
|
|||||||
@@ -1,13 +1,42 @@
|
|||||||
|
# NOTE:
|
||||||
# Configuration settings for the `hpa` command line tool.
|
# Configuration settings for the `hpa` command line tool.
|
||||||
#
|
# You can delete settings that are not applicable to your use case.
|
||||||
# Delete settings that are not applicable to your use case.
|
|
||||||
|
|
||||||
# Default arguments / options that get passed into `ansible-playbook` commands.
|
# Default arguments / options that get passed into `ansible-playbook` commands.
|
||||||
|
# WARNING: Do not put arguments / options that contain spaces in the same string,
|
||||||
|
# they should be separate strings, for example do not do something like
|
||||||
|
# ['--tags debug'], instead use ['--tags', 'debug'].
|
||||||
|
#
|
||||||
args = []
|
args = []
|
||||||
|
|
||||||
# Set to true if you want to pass the vault args to `ansible-playbook` commands.
|
# Set to true if you want to pass the vault args to `ansible-playbook` commands.
|
||||||
useVaultArgs = true
|
useVaultArgs = true
|
||||||
|
|
||||||
|
# NOTE:
|
||||||
|
# Configuration for running the generate command(s). This allows custimizations
|
||||||
|
# to the files that get used to generate the final output (generally a pdf).
|
||||||
|
# See `pandoc --help`. Below are the defaults that get used, which only need
|
||||||
|
# adjusted if your template does not follow the default template design or if
|
||||||
|
# you add extra files to your template that need to be included in the final
|
||||||
|
# output. Also be aware that any of the files specified in the `files` or
|
||||||
|
# `includeInHeader` options, need to be inside the `buildDirectory` when generating
|
||||||
|
# the final output file.
|
||||||
|
|
||||||
|
# [generate]
|
||||||
|
# this relative to the project directory.
|
||||||
|
# buildDirectory = '.build'
|
||||||
|
# pdfEngine = 'xelatex'
|
||||||
|
# includeInHeader = [
|
||||||
|
# 'head.tex',
|
||||||
|
# 'footer.tex'
|
||||||
|
# ]
|
||||||
|
# files = [
|
||||||
|
# 'Report.md',
|
||||||
|
# 'Definitions.md'
|
||||||
|
# ]
|
||||||
|
# outputFileName = 'Report'
|
||||||
|
|
||||||
|
# NOTE:
|
||||||
# These are more for local development of the ansible playbook and should not be needed
|
# These are more for local development of the ansible playbook and should not be needed
|
||||||
# in most cases. Uncomment the lines if you want to customize the playbook and use it
|
# in most cases. Uncomment the lines if you want to customize the playbook and use it
|
||||||
# instead of the provided / default playbook.
|
# instead of the provided / default playbook.
|
||||||
@@ -17,6 +46,7 @@ useVaultArgs = true
|
|||||||
#inventory = '/path/to/local/inventory.ini'
|
#inventory = '/path/to/local/inventory.ini'
|
||||||
#version = 'main'
|
#version = 'main'
|
||||||
|
|
||||||
|
# NOTE:
|
||||||
# These are to declare where your template files are either on your local system or
|
# These are to declare where your template files are either on your local system or
|
||||||
# a remote git repository.
|
# a remote git repository.
|
||||||
[template]
|
[template]
|
||||||
@@ -31,6 +61,7 @@ url = 'https://git.example.com/consult-template.git'
|
|||||||
# branch.
|
# branch.
|
||||||
version = '1.0.0'
|
version = '1.0.0'
|
||||||
|
|
||||||
|
# NOTE:
|
||||||
# Holds settings for `ansible-vault` commands.
|
# Holds settings for `ansible-vault` commands.
|
||||||
[vault]
|
[vault]
|
||||||
# Arguments to pass to commands that use `ansible-vault`, such as encrypting or decrypting
|
# Arguments to pass to commands that use `ansible-vault`, such as encrypting or decrypting
|
||||||
|
|||||||
@@ -39,14 +39,13 @@ private func install(config: Configuration.Playbook?) async throws {
|
|||||||
@Dependency(\.logger) var logger
|
@Dependency(\.logger) var logger
|
||||||
@Dependency(\.asyncShellClient) var shell
|
@Dependency(\.asyncShellClient) var shell
|
||||||
|
|
||||||
let path = config?.directory ?? PlaybookClient.Constants.defaultInstallationPath
|
let (path, version) = parsePlaybookPathAndVerion(config)
|
||||||
let version = config?.version ?? PlaybookClient.Constants.playbookRepoVersion
|
|
||||||
|
|
||||||
let parentDirectory = URL(filePath: path)
|
let parentDirectory = URL(filePath: path)
|
||||||
.deletingLastPathComponent()
|
.deletingLastPathComponent()
|
||||||
|
|
||||||
let exists = try await fileClient.isDirectory(parentDirectory)
|
let parentExists = try await fileClient.isDirectory(parentDirectory)
|
||||||
if !exists {
|
if !parentExists {
|
||||||
try await fileClient.createDirectory(parentDirectory)
|
try await fileClient.createDirectory(parentDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,3 +69,10 @@ private func install(config: Configuration.Playbook?) async throws {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func parsePlaybookPathAndVerion(_ configuration: Configuration.Playbook?) -> (path: String, version: String) {
|
||||||
|
return (
|
||||||
|
path: configuration?.directory ?? PlaybookClient.Constants.defaultInstallationPath,
|
||||||
|
version: configuration?.version ?? PlaybookClient.Constants.playbookRepoVersion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,10 +6,6 @@ import Logging
|
|||||||
public protocol TestCase {}
|
public protocol TestCase {}
|
||||||
|
|
||||||
public extension TestCase {
|
public extension TestCase {
|
||||||
// static var logLevel: Logger.Level = {
|
|
||||||
// let levelString = ProcessInfo.processInfo.environment["LOG_LEVEL"] ?? "debug"
|
|
||||||
// return Logger.Level(rawValue: levelString) ?? .debug
|
|
||||||
// }()
|
|
||||||
|
|
||||||
func withTestLogger(
|
func withTestLogger(
|
||||||
key: String,
|
key: String,
|
||||||
|
|||||||
15
Sources/TestSupport/WithTempDir.swift
Normal file
15
Sources/TestSupport/WithTempDir.swift
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// swiftlint:disable force_try
|
||||||
|
public func withTemporaryDirectory(
|
||||||
|
_ operation: @Sendable (URL) async throws -> Void
|
||||||
|
) async rethrows {
|
||||||
|
let temporaryDirectory = FileManager.default
|
||||||
|
.temporaryDirectory
|
||||||
|
.appending(path: UUID().uuidString)
|
||||||
|
try! FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: false)
|
||||||
|
try await operation(temporaryDirectory)
|
||||||
|
try! FileManager.default.removeItem(at: temporaryDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
// swiftlint:enable force_try
|
||||||
@@ -9,9 +9,13 @@ struct Application: AsyncParsableCommand {
|
|||||||
static let configuration = CommandConfiguration(
|
static let configuration = CommandConfiguration(
|
||||||
commandName: Constants.appName,
|
commandName: Constants.appName,
|
||||||
abstract: createAbstract("A utility for working with ansible hpa playbook."),
|
abstract: createAbstract("A utility for working with ansible hpa playbook."),
|
||||||
version: VERSION,
|
version: VERSION ?? "0.0.0",
|
||||||
subcommands: [
|
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 ArgumentParser
|
||||||
import CliClient
|
import CliClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
|
||||||
struct BuildCommand: AsyncParsableCommand {
|
struct BuildCommand: AsyncParsableCommand {
|
||||||
|
|
||||||
@@ -9,16 +10,23 @@ struct BuildCommand: AsyncParsableCommand {
|
|||||||
static let configuration = CommandConfiguration.playbook(
|
static let configuration = CommandConfiguration.playbook(
|
||||||
commandName: commandName,
|
commandName: commandName,
|
||||||
abstract: "Build a home performance assesment project.",
|
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
|
@OptionGroup var globals: GlobalOptions
|
||||||
|
|
||||||
@Argument(
|
@Option(
|
||||||
help: "Path to the project directory.",
|
help: "Path to the project directory.",
|
||||||
completion: .directory
|
completion: .directory
|
||||||
)
|
)
|
||||||
var projectDir: String
|
var projectDirectory: String?
|
||||||
|
|
||||||
@Argument(
|
@Argument(
|
||||||
help: "Extra arguments / options passed to the playbook."
|
help: "Extra arguments / options passed to the playbook."
|
||||||
@@ -27,19 +35,21 @@ struct BuildCommand: AsyncParsableCommand {
|
|||||||
|
|
||||||
mutating func run() async throws {
|
mutating func run() async throws {
|
||||||
try await _run()
|
try await _run()
|
||||||
|
|
||||||
// try await runPlaybook(
|
|
||||||
// commandName: Self.commandName,
|
|
||||||
// globals: globals,
|
|
||||||
// extraArgs: extraArgs,
|
|
||||||
// "--tags", "build-project",
|
|
||||||
// "--extra-vars", "project_dir=\(projectDir)"
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func _run() async throws {
|
private func _run() async throws {
|
||||||
@Dependency(\.cliClient) var cliClient
|
@Dependency(\.cliClient) var cliClient
|
||||||
|
|
||||||
|
let projectDir: String
|
||||||
|
if projectDirectory == nil {
|
||||||
|
guard let pwd = ProcessInfo.processInfo.environment["PWD"] else {
|
||||||
|
throw ProjectDirectoryNotSupplied()
|
||||||
|
}
|
||||||
|
projectDir = pwd
|
||||||
|
} else {
|
||||||
|
projectDir = projectDirectory!
|
||||||
|
}
|
||||||
|
|
||||||
try await cliClient.runPlaybookCommand(
|
try await cliClient.runPlaybookCommand(
|
||||||
globals.playbookOptions(
|
globals.playbookOptions(
|
||||||
arguments: [
|
arguments: [
|
||||||
@@ -52,3 +62,5 @@ struct BuildCommand: AsyncParsableCommand {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ProjectDirectoryNotSupplied: Error {}
|
||||||
|
|||||||
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 CliClient
|
||||||
|
import Dependencies
|
||||||
|
|
||||||
|
// 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(\.cliClient) var cliClient
|
||||||
|
|
||||||
|
try await cliClient.runPandocCommand(
|
||||||
|
globals.pandocOptions(.html),
|
||||||
|
logging: globals.loggingOptions(commandName: Self.commandName)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
23
Sources/hpa/GenerateCommands/GenerateLatexCommand.swift
Normal file
23
Sources/hpa/GenerateCommands/GenerateLatexCommand.swift
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
|
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(\.cliClient) var cliClient
|
||||||
|
|
||||||
|
try await cliClient.runPandocCommand(
|
||||||
|
globals.pandocOptions(.latex),
|
||||||
|
logging: globals.loggingOptions(commandName: Self.commandName)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
87
Sources/hpa/GenerateCommands/GenerateOptions.swift
Normal file
87
Sources/hpa/GenerateCommands/GenerateOptions.swift
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
|
|
||||||
|
@dynamicMemberLookup
|
||||||
|
struct GenerateOptions: ParsableArguments {
|
||||||
|
|
||||||
|
@OptionGroup var basic: BasicGlobalOptions
|
||||||
|
|
||||||
|
@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) -> CliClient.LoggingOptions {
|
||||||
|
basic.loggingOptions(commandName: commandName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pandocOptions(
|
||||||
|
_ fileType: CliClient.PandocOptions.FileType
|
||||||
|
) -> CliClient.PandocOptions {
|
||||||
|
.init(
|
||||||
|
files: files.count > 0 ? files : nil,
|
||||||
|
includeInHeader: includeInHeader.count > 0 ? includeInHeader : nil,
|
||||||
|
outputDirectory: outputDirectory,
|
||||||
|
outputFileName: outputFileName,
|
||||||
|
outputFileType: fileType,
|
||||||
|
projectDirectory: projectDirectory,
|
||||||
|
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 CliClient
|
||||||
|
import Dependencies
|
||||||
|
|
||||||
|
// 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(\.cliClient) var cliClient
|
||||||
|
|
||||||
|
let output = try await cliClient.runPandocCommand(
|
||||||
|
globals.pandocOptions(.pdf(engine: pdfEngine)),
|
||||||
|
logging: globals.loggingOptions(commandName: Self.commandName)
|
||||||
|
)
|
||||||
|
|
||||||
|
print(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,10 +27,10 @@ struct BasicGlobalOptions: ParsableArguments {
|
|||||||
struct GlobalOptions: ParsableArguments {
|
struct GlobalOptions: ParsableArguments {
|
||||||
|
|
||||||
@Option(
|
@Option(
|
||||||
name: .shortAndLong,
|
name: .long,
|
||||||
help: "Optional path to the ansible hpa playbook directory."
|
help: "Optional path to the ansible hpa playbook directory."
|
||||||
)
|
)
|
||||||
var playbookDir: String?
|
var playbookDirectory: String?
|
||||||
|
|
||||||
@Option(
|
@Option(
|
||||||
name: .shortAndLong,
|
name: .shortAndLong,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ extension GlobalOptions {
|
|||||||
arguments: arguments,
|
arguments: arguments,
|
||||||
configuration: configuration,
|
configuration: configuration,
|
||||||
inventoryFilePath: inventoryPath,
|
inventoryFilePath: inventoryPath,
|
||||||
playbookDirectory: playbookDir,
|
playbookDirectory: playbookDirectory,
|
||||||
quiet: quietOnlyPlaybook ? true : basic.quiet,
|
quiet: quietOnlyPlaybook ? true : basic.quiet,
|
||||||
shell: basic.shell
|
shell: basic.shell
|
||||||
)
|
)
|
||||||
|
|||||||
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
|
||||||
@@ -143,23 +143,6 @@ func generateFindEnvironments(file: File) -> [[String: String]] {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable force_try
|
|
||||||
func withTemporaryDirectory(
|
|
||||||
_ operation: @Sendable (URL) async throws -> Void
|
|
||||||
) async rethrows {
|
|
||||||
let dir = FileManager.default.temporaryDirectory
|
|
||||||
let tempDir = dir.appending(path: UUID().uuidString)
|
|
||||||
|
|
||||||
try! FileManager.default.createDirectory(
|
|
||||||
atPath: tempDir.cleanFilePath,
|
|
||||||
withIntermediateDirectories: false
|
|
||||||
)
|
|
||||||
try await operation(tempDir)
|
|
||||||
try! FileManager.default.removeItem(at: tempDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// swiftlint:enable force_try
|
|
||||||
|
|
||||||
func withGeneratedConfigFile(
|
func withGeneratedConfigFile(
|
||||||
named fileName: String,
|
named fileName: String,
|
||||||
client: ConfigurationClient,
|
client: ConfigurationClient,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Foundation
|
|||||||
@_spi(Internal) import PlaybookClient
|
@_spi(Internal) import PlaybookClient
|
||||||
import ShellClient
|
import ShellClient
|
||||||
import Testing
|
import Testing
|
||||||
|
import TestSupport
|
||||||
|
|
||||||
@Suite("PlaybookClientTests")
|
@Suite("PlaybookClientTests")
|
||||||
struct PlaybookClientTests {
|
struct PlaybookClientTests {
|
||||||
@@ -15,18 +16,17 @@ struct PlaybookClientTests {
|
|||||||
$0.fileClient = .liveValue
|
$0.fileClient = .liveValue
|
||||||
$0.asyncShellClient = .liveValue
|
$0.asyncShellClient = .liveValue
|
||||||
} operation: {
|
} operation: {
|
||||||
let tempDirectory = FileManager.default.temporaryDirectory
|
try await withTemporaryDirectory { tempDirectory in
|
||||||
let pathUrl = tempDirectory.appending(path: "playbook")
|
let pathUrl = tempDirectory.appending(path: "playbook")
|
||||||
let playbookClient = PlaybookClient.liveValue
|
let playbookClient = PlaybookClient.liveValue
|
||||||
|
|
||||||
let configuration = Configuration(playbook: .init(directory: pathUrl.cleanFilePath))
|
let configuration = Configuration(playbook: .init(directory: pathUrl.cleanFilePath))
|
||||||
|
|
||||||
try? FileManager.default.removeItem(at: pathUrl)
|
try? FileManager.default.removeItem(at: pathUrl)
|
||||||
try await playbookClient.installPlaybook(configuration)
|
try await playbookClient.installPlaybook(configuration)
|
||||||
let exists = FileManager.default.fileExists(atPath: pathUrl.cleanFilePath)
|
let exists = FileManager.default.fileExists(atPath: pathUrl.cleanFilePath)
|
||||||
#expect(exists)
|
#expect(exists)
|
||||||
|
}
|
||||||
try FileManager.default.removeItem(at: pathUrl)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user