Compare commits
31 Commits
v0.1.1
...
8c402f3f5f
| Author | SHA1 | Date | |
|---|---|---|---|
|
8c402f3f5f
|
|||
|
1429c51821
|
|||
|
1302b15ee2
|
|||
|
da810d0a45
|
|||
|
35d9422f07
|
|||
|
601869d457
|
|||
|
6d0108da0c
|
|||
|
bc0b740f95
|
|||
|
303cdef84b
|
|||
|
b5afc77428
|
|||
|
3f56dda568
|
|||
|
d1b3379815
|
|||
|
b557a60fa3
|
|||
|
5f4ef3b5b5
|
|||
|
ba1e61d99e
|
|||
|
bd56660683
|
|||
|
a480e942bc
|
|||
|
2b265a4ca5
|
|||
|
56a0bca00c
|
|||
|
7b30b78b67
|
|||
|
ce6eb3ec2f
|
|||
|
8302ede99e
|
|||
|
d9e91538fb
|
|||
|
c1a14ea855
|
|||
|
ddb5e6767a
|
|||
|
9c784d4dcb
|
|||
|
92cd6afa2b
|
|||
|
87390c4b63
|
|||
|
a8d35fed37
|
|||
|
357914ba93
|
|||
|
a39536364b
|
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
name: CI
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["main", "dev"]
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: Run Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: https://git.housh.dev/actions/setup-just@v1
|
|
||||||
- name: Setup QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
- name: Setup Docker buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: Run Test
|
|
||||||
run: just test-docker
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "bc31b11e5e7d488e0a9c1bf91cb572d29f782bfd8e43f44157036f8f3d282893",
|
"originHash" : "a3b90cdeec7e11e73a0a47c1f8dffa472c22f0c6385282eb11096210e3e7ad8b",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "combine-schedulers",
|
"identity" : "combine-schedulers",
|
||||||
@@ -64,15 +64,6 @@
|
|||||||
"version" : "1.3.0"
|
"version" : "1.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"identity" : "swift-custom-dump",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pointfreeco/swift-custom-dump.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1",
|
|
||||||
"version" : "1.3.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"identity" : "swift-dependencies",
|
"identity" : "swift-dependencies",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -7,17 +7,16 @@ let package = Package(
|
|||||||
platforms: [.macOS(.v14)],
|
platforms: [.macOS(.v14)],
|
||||||
products: [
|
products: [
|
||||||
.executable(name: "hpa", targets: ["hpa"]),
|
.executable(name: "hpa", targets: ["hpa"]),
|
||||||
|
.library(name: "CliClient", targets: ["CliClient"]),
|
||||||
.library(name: "CodersClient", targets: ["CodersClient"]),
|
.library(name: "CodersClient", targets: ["CodersClient"]),
|
||||||
.library(name: "CommandClient", targets: ["CommandClient"]),
|
.library(name: "CommandClient", targets: ["CommandClient"]),
|
||||||
.library(name: "ConfigurationClient", targets: ["ConfigurationClient"]),
|
.library(name: "ConfigurationClient", targets: ["ConfigurationClient"]),
|
||||||
.library(name: "FileClient", targets: ["FileClient"]),
|
.library(name: "FileClient", targets: ["FileClient"]),
|
||||||
.library(name: "PlaybookClient", targets: ["PlaybookClient"]),
|
.library(name: "PlaybookClient", targets: ["PlaybookClient"]),
|
||||||
.library(name: "PandocClient", targets: ["PandocClient"]),
|
|
||||||
.library(name: "VaultClient", targets: ["VaultClient"])
|
.library(name: "VaultClient", targets: ["VaultClient"])
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
|
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
|
||||||
.package(url: "https://github.com/pointfreeco/swift-custom-dump.git", from: "1.3.3"),
|
|
||||||
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.5.2"),
|
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.5.2"),
|
||||||
.package(url: "https://github.com/m-housh/swift-shell-client.git", from: "0.1.0"),
|
.package(url: "https://github.com/m-housh/swift-shell-client.git", from: "0.1.0"),
|
||||||
.package(url: "https://git.housh.dev/michael/swift-cli-doc.git", from: "0.2.0"),
|
.package(url: "https://git.housh.dev/michael/swift-cli-doc.git", from: "0.2.0"),
|
||||||
@@ -28,18 +27,36 @@ let package = Package(
|
|||||||
.executableTarget(
|
.executableTarget(
|
||||||
name: "hpa",
|
name: "hpa",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
"CliClient",
|
||||||
"ConfigurationClient",
|
"ConfigurationClient",
|
||||||
"FileClient",
|
"FileClient",
|
||||||
"PandocClient",
|
|
||||||
"PlaybookClient",
|
"PlaybookClient",
|
||||||
"VaultClient",
|
"VaultClient",
|
||||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||||
.product(name: "CliDoc", package: "swift-cli-doc"),
|
.product(name: "CliDoc", package: "swift-cli-doc"),
|
||||||
.product(name: "CustomDump", package: "swift-custom-dump"),
|
|
||||||
.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")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "CliClient",
|
||||||
|
dependencies: [
|
||||||
|
"CommandClient",
|
||||||
|
"CodersClient",
|
||||||
|
"ConfigurationClient",
|
||||||
|
"PlaybookClient",
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||||
|
.product(name: "ShellClient", package: "swift-shell-client")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "CliClientTests",
|
||||||
|
dependencies: [
|
||||||
|
"CliClient",
|
||||||
|
"TestSupport"
|
||||||
|
]
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "CodersClient",
|
name: "CodersClient",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
@@ -93,29 +110,11 @@ let package = Package(
|
|||||||
.product(name: "DependenciesMacros", package: "swift-dependencies")
|
.product(name: "DependenciesMacros", package: "swift-dependencies")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.testTarget(
|
|
||||||
name: "FileClientTests",
|
|
||||||
dependencies: ["FileClient", "TestSupport"]
|
|
||||||
),
|
|
||||||
.target(
|
|
||||||
name: "PandocClient",
|
|
||||||
dependencies: [
|
|
||||||
"CommandClient",
|
|
||||||
"ConfigurationClient",
|
|
||||||
"PlaybookClient",
|
|
||||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
|
||||||
.product(name: "DependenciesMacros", package: "swift-dependencies")
|
|
||||||
]
|
|
||||||
),
|
|
||||||
.testTarget(
|
|
||||||
name: "PandocClientTests",
|
|
||||||
dependencies: ["PandocClient", "TestSupport"]
|
|
||||||
),
|
|
||||||
.target(
|
.target(
|
||||||
name: "PlaybookClient",
|
name: "PlaybookClient",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"CommandClient",
|
|
||||||
"CodersClient",
|
"CodersClient",
|
||||||
|
"CommandClient",
|
||||||
"ConfigurationClient",
|
"ConfigurationClient",
|
||||||
"FileClient",
|
"FileClient",
|
||||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
|||||||
189
Sources/CliClient/CliClient+Commands.swift
Normal file
189
Sources/CliClient/CliClient+Commands.swift
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
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 {}
|
||||||
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
216
Sources/CliClient/CliClient+RunPlaybook.swift
Normal file
216
Sources/CliClient/CliClient+RunPlaybook.swift
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Sources/CliClient/CliClientError.swift
Normal file
16
Sources/CliClient/CliClientError.swift
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Sources/CliClient/Constants.swift
Normal file
11
Sources/CliClient/Constants.swift
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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"
|
||||||
|
]
|
||||||
|
}
|
||||||
85
Sources/CliClient/GenerateJson.swift
Normal file
85
Sources/CliClient/GenerateJson.swift
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
329
Sources/CliClient/Interface.swift
Normal file
329
Sources/CliClient/Interface.swift
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
Sources/CliClient/LoggingExtensions.swift
Normal file
29
Sources/CliClient/LoggingExtensions.swift
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,6 @@ import Foundation
|
|||||||
import TOMLKit
|
import TOMLKit
|
||||||
|
|
||||||
public extension DependencyValues {
|
public extension DependencyValues {
|
||||||
|
|
||||||
/// Holds onto decoders and encoders for json and toml files.
|
|
||||||
var coders: Coders {
|
var coders: Coders {
|
||||||
get { self[Coders.self] }
|
get { self[Coders.self] }
|
||||||
set { self[Coders.self] = newValue }
|
set { self[Coders.self] = newValue }
|
||||||
|
|||||||
@@ -110,33 +110,15 @@ public struct CommandClient: Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents logger setup options used when running commands.
|
|
||||||
///
|
|
||||||
/// This set's up metadata tags and keys appropriately for the command being run,
|
|
||||||
/// which can aid in debugging.
|
|
||||||
///
|
|
||||||
public struct LoggingOptions: Equatable, Sendable {
|
public struct LoggingOptions: Equatable, Sendable {
|
||||||
|
|
||||||
/// The command name / key that the logger is running for.
|
|
||||||
public let commandName: String
|
public let commandName: String
|
||||||
|
|
||||||
/// The log level.
|
|
||||||
public let logLevel: Logger.Level
|
public let logLevel: Logger.Level
|
||||||
|
|
||||||
/// Create the logging options.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - commandName: The command name / key that the logger is running for.
|
|
||||||
/// - logLevel: The log level.
|
|
||||||
public init(commandName: String, logLevel: Logger.Level) {
|
public init(commandName: String, logLevel: Logger.Level) {
|
||||||
self.commandName = commandName
|
self.commandName = commandName
|
||||||
self.logLevel = logLevel
|
self.logLevel = logLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform an operation with a setup logger.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - operation: The operation to perform with the setup logger.
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
public func withLogger<T>(
|
public func withLogger<T>(
|
||||||
operation: @Sendable @escaping () async throws -> T
|
operation: @Sendable @escaping () async throws -> T
|
||||||
@@ -222,12 +204,7 @@ extension ShellCommand.Shell {
|
|||||||
if let path {
|
if let path {
|
||||||
self = .custom(path: path, useDashC: true)
|
self = .custom(path: path, useDashC: true)
|
||||||
} else {
|
} else {
|
||||||
#if os(macOS)
|
|
||||||
self = .zsh(useDashC: true)
|
self = .zsh(useDashC: true)
|
||||||
#else
|
|
||||||
// Generally we're in a docker container when this occurs, which does not have `zsh` installed.
|
|
||||||
self = .sh(useDashC: true)
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,28 +2,13 @@ import Foundation
|
|||||||
|
|
||||||
// NOTE: When adding items, then the 'hpa.toml' resource file needs to be updated.
|
// NOTE: When adding items, then the 'hpa.toml' resource file needs to be updated.
|
||||||
|
|
||||||
/// Represents configurable settings for the application.
|
/// Represents configurable settings for the command line tool.
|
||||||
public struct Configuration: Codable, Equatable, Sendable {
|
public struct Configuration: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
/// Default arguments / options that can get passed into
|
|
||||||
/// ansible-playbook commands.
|
|
||||||
public let args: [String]?
|
public let args: [String]?
|
||||||
|
|
||||||
/// Whether to use the vault arguments as defaults that get passed into
|
|
||||||
/// the ansible-playbook commands.
|
|
||||||
public let useVaultArgs: Bool
|
public let useVaultArgs: Bool
|
||||||
|
|
||||||
/// Configuration for when generating files from templated directories.
|
|
||||||
public let generate: Generate?
|
public let generate: Generate?
|
||||||
|
|
||||||
/// Configuration of the ansible-playbook, these are more for developing the
|
|
||||||
/// playbook locally and generally not needed by the user.
|
|
||||||
public let playbook: Playbook?
|
public let playbook: Playbook?
|
||||||
|
|
||||||
/// Template configuration options.
|
|
||||||
public let template: Template
|
public let template: Template
|
||||||
|
|
||||||
/// Ansible-vault configuration options.
|
|
||||||
public let vault: Vault
|
public let vault: Vault
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@@ -52,34 +37,11 @@ public struct Configuration: Codable, Equatable, Sendable {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration options for running `pandoc` commands that generate files from
|
|
||||||
/// a templated directory.
|
|
||||||
///
|
|
||||||
/// ## NOTE: Most of these can also be set by the template repository variables.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
public struct Generate: Codable, Equatable, Sendable {
|
public struct Generate: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
/// Specifiy the name of the build directory, generally `.build`.
|
|
||||||
public let buildDirectory: String?
|
public let buildDirectory: String?
|
||||||
|
|
||||||
/// Specifiy the files used in the `pandoc` command to generate a final output file.
|
|
||||||
///
|
|
||||||
/// - SeeAlso: `pandoc --help`
|
|
||||||
public let files: [String]?
|
public let files: [String]?
|
||||||
|
|
||||||
/// Specifiy the files used in the `pandoc` command to include in the header.
|
|
||||||
///
|
|
||||||
/// - SeeAlso: `pandoc --help`
|
|
||||||
public let includeInHeader: [String]?
|
public let includeInHeader: [String]?
|
||||||
|
|
||||||
/// The default name of the output file, this does not require the file extension as we will
|
|
||||||
/// add it based on the command that is called. Generally this is 'Report'.
|
|
||||||
///
|
|
||||||
public let outputFileName: String?
|
public let outputFileName: String?
|
||||||
|
|
||||||
/// The default pdf engine to use when generating pdf files using `pandoc`. Generally
|
|
||||||
/// this is 'xelatex'.
|
|
||||||
public let pdfEngine: String?
|
public let pdfEngine: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@@ -96,10 +58,8 @@ public struct Configuration: Codable, Equatable, Sendable {
|
|||||||
self.pdfEngine = pdfEngine
|
self.pdfEngine = pdfEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents the default configuration for generating files using `pandoc`.
|
|
||||||
public static let `default` = Self.mock
|
public static let `default` = Self.mock
|
||||||
|
|
||||||
/// Represents mock configuration for generating files using `pandoc`.
|
|
||||||
public static var mock: Self {
|
public static var mock: Self {
|
||||||
.init(
|
.init(
|
||||||
buildDirectory: ".build",
|
buildDirectory: ".build",
|
||||||
@@ -111,22 +71,10 @@ public struct Configuration: Codable, Equatable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration options for the ansible-hpa-playbook. The playbook is
|
|
||||||
/// the primary driver of templating files, generating repository templates, and building
|
|
||||||
/// projects.
|
|
||||||
///
|
|
||||||
/// ## NOTE: These are generally only used for local development of the playbook.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
public struct Playbook: Codable, Equatable, Sendable {
|
public struct Playbook: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
/// The directory location of the ansible playbook.
|
|
||||||
public let directory: String?
|
public let directory: String?
|
||||||
|
|
||||||
/// The inventory file name / location.
|
|
||||||
public let inventory: String?
|
public let inventory: String?
|
||||||
|
|
||||||
/// The playbook version, branch, or tag.
|
|
||||||
public let version: String?
|
public let version: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@@ -142,28 +90,9 @@ public struct Configuration: Codable, Equatable, Sendable {
|
|||||||
public static var mock: Self { .init() }
|
public static var mock: Self { .init() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration settings for the user's template repository or directory.
|
|
||||||
///
|
|
||||||
/// A template is what is used to create projects for a user. Generally they setup
|
|
||||||
/// their own template by customizing the default template to their needs. This template
|
|
||||||
/// can then be stored as a git repository or on the local file system.
|
|
||||||
///
|
|
||||||
/// The template will hold variables and files that are used to generate the final output
|
|
||||||
/// files using `pandoc`. Generally the template will hold files that may only need setup once,
|
|
||||||
/// such as header files, footer files, and definitions.
|
|
||||||
///
|
|
||||||
/// The project directory contains dynamic variables / files that need edited in order
|
|
||||||
/// to generate the final output. This allows the project directory to remain smaller so the user
|
|
||||||
/// can more easily focus on the tasks required to generate the final output files.
|
|
||||||
public struct Template: Codable, Equatable, Sendable {
|
public struct Template: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
/// A url of the template when it is a remote git repository.
|
|
||||||
public let url: String?
|
public let url: String?
|
||||||
|
|
||||||
/// The version, branch, or tag of the remote repository.
|
|
||||||
public let version: String?
|
public let version: String?
|
||||||
|
|
||||||
/// The local directory that contains the template on the local file system.
|
|
||||||
public let directory: String?
|
public let directory: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@@ -185,23 +114,8 @@ public struct Configuration: Codable, Equatable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration for `ansible-vault` commands. Ansible vault is used to encrypt and
|
|
||||||
/// decrypt sensitive data. This allows for variables, such as customer name and address
|
|
||||||
/// to be stored along with the project in an encrypted file so that it is safer to store them.
|
|
||||||
///
|
|
||||||
/// These may also be used in general `ansible-playbook` commands if the `useVaultArgs` is set
|
|
||||||
/// to `true` in a users configuration.
|
|
||||||
///
|
|
||||||
public struct Vault: Codable, Equatable, Sendable {
|
public struct Vault: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
/// A list of arguments / options that get passed to the `ansible-vault` command.
|
|
||||||
///
|
|
||||||
/// - SeeAlso: `ansible-vault --help`
|
|
||||||
public let args: [String]?
|
public let args: [String]?
|
||||||
|
|
||||||
/// An id that is used during encrypting `ansible-vault` files.
|
|
||||||
///
|
|
||||||
/// - SeeAlso: `ansible-vault encrypt --help`
|
|
||||||
public let encryptId: String?
|
public let encryptId: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
|
|||||||
@@ -6,119 +6,37 @@ import Foundation
|
|||||||
import ShellClient
|
import ShellClient
|
||||||
|
|
||||||
public extension DependencyValues {
|
public extension DependencyValues {
|
||||||
|
|
||||||
/// Interacts with the user's configuration.
|
|
||||||
var configurationClient: ConfigurationClient {
|
var configurationClient: ConfigurationClient {
|
||||||
get { self[ConfigurationClient.self] }
|
get { self[ConfigurationClient.self] }
|
||||||
set { self[ConfigurationClient.self] = newValue }
|
set { self[ConfigurationClient.self] = newValue }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents actions that can be taken on user's configuration files.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
@DependencyClient
|
@DependencyClient
|
||||||
public struct ConfigurationClient: Sendable {
|
public struct ConfigurationClient: Sendable {
|
||||||
|
|
||||||
/// Find the user's configuration, searches in the current directory and default
|
|
||||||
/// locations where configuration can be stored. An error is thrown if no configuration
|
|
||||||
/// is found.
|
|
||||||
public var find: @Sendable () async throws -> File
|
public var find: @Sendable () async throws -> File
|
||||||
|
var generate: @Sendable (File, Bool) async throws -> Void
|
||||||
/// Generate a configuration file for the user.
|
|
||||||
public var generate: @Sendable (GenerateOptions) async throws -> String
|
|
||||||
|
|
||||||
/// Load a configuration file from the given file location. If the file is
|
|
||||||
/// not provided then we return an empty configuraion item.
|
|
||||||
public var load: @Sendable (File?) async throws -> Configuration
|
public var load: @Sendable (File?) async throws -> Configuration
|
||||||
|
var write: @Sendable (File, Configuration, Bool) async throws -> Void
|
||||||
|
|
||||||
/// Write the configuration to the given file, optionally forcing an overwrite of
|
|
||||||
/// the file.
|
|
||||||
///
|
|
||||||
/// ## NOTE: This uses the `fileClient.write` under the hood, so if you need to control
|
|
||||||
/// what happens during tests, then you can customize the behavior of the fileClient.
|
|
||||||
///
|
|
||||||
public var write: @Sendable (WriteOptions) async throws -> Void
|
|
||||||
|
|
||||||
/// Find the user's configuration and load it.
|
|
||||||
public func findAndLoad() async throws -> Configuration {
|
public func findAndLoad() async throws -> Configuration {
|
||||||
let file = try? await find()
|
let file = try? await find()
|
||||||
return try await load(file)
|
return try await load(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write the configuration to the given file, optionally forcing an overwrite of
|
public func generate(
|
||||||
/// the file. If a file already exists and force is not supplied then we will create
|
at file: File,
|
||||||
/// a backup of the existing file.
|
force: Bool = false
|
||||||
///
|
) async throws {
|
||||||
/// ## NOTE: This uses the `fileClient.write` under the hood, so if you need to control
|
try await generate(file, force)
|
||||||
/// what happens during tests, then you can customize the behavior of the fileClient.
|
}
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - configuration: The configuration to save.
|
|
||||||
/// - file: The file location and type to save.
|
|
||||||
/// - force: Force overwritting if a file already exists.
|
|
||||||
public func write(
|
public func write(
|
||||||
_ configuration: Configuration,
|
_ configuration: Configuration,
|
||||||
to file: File,
|
to file: File,
|
||||||
force: Bool = false
|
force: Bool = false
|
||||||
) async throws {
|
) async throws {
|
||||||
try await write(.init(configuration, to: file, force: force))
|
try await write(file, configuration, force)
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the options to generate a configuration file for a user.
|
|
||||||
public struct GenerateOptions: Equatable, Sendable {
|
|
||||||
|
|
||||||
/// Force generation, overwritting existing file if it exists.
|
|
||||||
public let force: Bool
|
|
||||||
|
|
||||||
/// Generate a `json` file instead of the default `toml` file.
|
|
||||||
public let json: Bool
|
|
||||||
|
|
||||||
/// The path to generate a file in.
|
|
||||||
public let path: Path?
|
|
||||||
|
|
||||||
public init(
|
|
||||||
force: Bool = false,
|
|
||||||
json: Bool = false,
|
|
||||||
path: Path? = nil
|
|
||||||
) {
|
|
||||||
self.force = force
|
|
||||||
self.json = json
|
|
||||||
self.path = path
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the path option for generating a configuration file for a user.
|
|
||||||
///
|
|
||||||
/// This can either be a full file path or a directory. If a directory is supplied
|
|
||||||
/// then we will use the default file name of 'config' and add the extension dependning
|
|
||||||
/// on if the caller wants a `json` or `toml` file.
|
|
||||||
public enum Path: Equatable, Sendable {
|
|
||||||
case file(File)
|
|
||||||
case directory(String)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the options required to write a configuration file.
|
|
||||||
public struct WriteOptions: Equatable, Sendable {
|
|
||||||
|
|
||||||
/// The configuration to wrtie to the file path.
|
|
||||||
public let configuration: Configuration
|
|
||||||
|
|
||||||
/// The file path to write the configuration to.
|
|
||||||
public let file: File
|
|
||||||
|
|
||||||
/// Force overwritting an existing file, if it exists.
|
|
||||||
public let force: Bool
|
|
||||||
|
|
||||||
public init(
|
|
||||||
_ configuration: Configuration,
|
|
||||||
to file: File,
|
|
||||||
force: Bool
|
|
||||||
) {
|
|
||||||
self.configuration = configuration
|
|
||||||
self.file = file
|
|
||||||
self.force = force
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,12 +45,15 @@ extension ConfigurationClient: DependencyKey {
|
|||||||
|
|
||||||
public static func live(environment: [String: String]) -> Self {
|
public static func live(environment: [String: String]) -> Self {
|
||||||
let liveClient = LiveConfigurationClient(environment: environment)
|
let liveClient = LiveConfigurationClient(environment: environment)
|
||||||
return .init(
|
return .init {
|
||||||
find: { try await liveClient.find() },
|
try await liveClient.find()
|
||||||
generate: { try await liveClient.generate($0) },
|
} generate: { file, force in
|
||||||
load: { try await liveClient.load(file: $0) },
|
try await liveClient.generate(at: file, force: force)
|
||||||
write: { try await liveClient.write($0) }
|
} load: { file in
|
||||||
)
|
try await liveClient.load(file: file)
|
||||||
|
} write: { file, configuration, force in
|
||||||
|
try await liveClient.write(configuration, to: file, force: force)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var liveValue: Self {
|
public static var liveValue: Self {
|
||||||
@@ -205,24 +126,10 @@ struct LiveConfigurationClient {
|
|||||||
throw ConfigurationError.configurationNotFound
|
throw ConfigurationError.configurationNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func generate(_ options: ConfigurationClient.GenerateOptions) async throws -> String {
|
func generate(at file: File, force: Bool) async throws {
|
||||||
@Dependency(\.logger) var logger
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
let file: File
|
logger.debug("Begin generating configuration: \(file.path), force: \(force)")
|
||||||
|
|
||||||
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(
|
let expandedPath = file.path.replacingOccurrences(
|
||||||
of: "~",
|
of: "~",
|
||||||
@@ -231,10 +138,10 @@ struct LiveConfigurationClient {
|
|||||||
|
|
||||||
let fileUrl = URL(filePath: expandedPath)
|
let fileUrl = URL(filePath: expandedPath)
|
||||||
|
|
||||||
let exists = fileManager.fileExists(fileUrl)
|
if !force {
|
||||||
|
guard !fileManager.fileExists(fileUrl) else {
|
||||||
if !options.force, exists {
|
throw ConfigurationError.fileExists(path: file.path)
|
||||||
try await createBackup(file.url)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let fileDirectory = fileUrl.deletingLastPathComponent()
|
let fileDirectory = fileUrl.deletingLastPathComponent()
|
||||||
@@ -245,8 +152,6 @@ struct LiveConfigurationClient {
|
|||||||
try await fileManager.createDirectory(fileDirectory)
|
try await fileManager.createDirectory(fileDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: The hpa file needs to be copied somewhere on the system during install and
|
|
||||||
// not use bundle, as it only works if the tool was built on the users system.
|
|
||||||
if case .toml = file {
|
if case .toml = file {
|
||||||
// In the case of toml, we copy the internal resource that includes
|
// In the case of toml, we copy the internal resource that includes
|
||||||
// usage comments in the file.
|
// usage comments in the file.
|
||||||
@@ -261,10 +166,8 @@ struct LiveConfigurationClient {
|
|||||||
} else {
|
} else {
|
||||||
// Json does not allow comments, so we write the mock configuration
|
// Json does not allow comments, so we write the mock configuration
|
||||||
// to the file path.
|
// to the file path.
|
||||||
try await write(.init(.mock, to: File(fileUrl)!, force: options.force))
|
try await write(.mock, to: File(fileUrl)!, force: force)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileUrl.cleanFilePath
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func load(file: File?) async throws -> Configuration {
|
func load(file: File?) async throws -> Configuration {
|
||||||
@@ -283,16 +186,14 @@ struct LiveConfigurationClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func write(
|
func write(
|
||||||
_ options: ConfigurationClient.WriteOptions
|
_ configuration: Configuration,
|
||||||
|
to file: File,
|
||||||
|
force: Bool
|
||||||
) async throws {
|
) async throws {
|
||||||
let configuration = options.configuration
|
if !force {
|
||||||
let file = options.file
|
guard !fileManager.fileExists(file.url) else {
|
||||||
let force = options.force
|
throw ConfigurationError.fileExists(path: file.path)
|
||||||
|
}
|
||||||
let exists = fileManager.fileExists(file.url)
|
|
||||||
|
|
||||||
if !force, exists {
|
|
||||||
try await createBackup(file.url)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: Data
|
let data: Data
|
||||||
@@ -308,13 +209,6 @@ struct LiveConfigurationClient {
|
|||||||
try await fileManager.write(data, file.url)
|
try await fileManager.write(data, file.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createBackup(_ url: URL) async throws {
|
|
||||||
let backupUrl = url.appendingPathExtension("back")
|
|
||||||
logger.warning("File exists, creating a backup of the existing file at: \(backupUrl.cleanFilePath)")
|
|
||||||
try await fileManager.copy(url, backupUrl)
|
|
||||||
try await fileManager.delete(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func findInDirectory(_ directory: URL) async -> File? {
|
private func findInDirectory(_ directory: URL) async -> File? {
|
||||||
for file in validFileNames {
|
for file in validFileNames {
|
||||||
let url = directory.appending(path: file)
|
let url = directory.appending(path: file)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
/// Represents keys in the environment that can be used to locate a user's
|
|
||||||
/// configuration file.
|
|
||||||
@_spi(Internal)
|
@_spi(Internal)
|
||||||
public enum EnvironmentKey {
|
public enum EnvironmentKey {
|
||||||
static let xdgConfigHome = "XDG_CONFIG_HOME"
|
static let xdgConfigHome = "XDG_CONFIG_HOME"
|
||||||
@@ -7,14 +5,12 @@ public enum EnvironmentKey {
|
|||||||
static let hpaConfigFile = "HPA_CONFIG_FILE"
|
static let hpaConfigFile = "HPA_CONFIG_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents keys that are used internally for directory names, file names, etc.
|
|
||||||
@_spi(Internal)
|
@_spi(Internal)
|
||||||
public enum HPAKey {
|
public enum HPAKey {
|
||||||
public static let configDirName = "hpa"
|
public static let configDirName = "hpa"
|
||||||
public static let resourceFileName = "hpa"
|
public static let resourceFileName = "hpa"
|
||||||
public static let resourceFileExtension = "toml"
|
public static let resourceFileExtension = "toml"
|
||||||
public static let defaultFileName = "config.toml"
|
public static let defaultFileName = "config.toml"
|
||||||
public static let defaultFileNameWithoutExtension = "config"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension [String: String] {
|
extension [String: String] {
|
||||||
|
|||||||
@@ -2,21 +2,11 @@ import FileClient
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Represents a file location and type on disk for a configuration file.
|
/// Represents a file location and type on disk for a configuration file.
|
||||||
///
|
|
||||||
/// Currently the supported formats are `json` or `toml`. Toml is the default /
|
|
||||||
/// preferred format because it allows comments in the file and is easy to understand.
|
|
||||||
///
|
|
||||||
public enum File: Equatable, Sendable {
|
public enum File: Equatable, Sendable {
|
||||||
|
|
||||||
case json(URL)
|
case json(URL)
|
||||||
case toml(URL)
|
case toml(URL)
|
||||||
|
|
||||||
/// Attempts to create a file with the given url.
|
|
||||||
///
|
|
||||||
/// ## NOTE: There are no checks on if a file / path actually exists or not.
|
|
||||||
///
|
|
||||||
/// If the file does not have a suffix of `json` or `toml` then
|
|
||||||
/// we will return `nil`.
|
|
||||||
public init?(_ url: URL) {
|
public init?(_ url: URL) {
|
||||||
if url.cleanFilePath.hasSuffix("json") {
|
if url.cleanFilePath.hasSuffix("json") {
|
||||||
self = .json(url)
|
self = .json(url)
|
||||||
@@ -27,17 +17,18 @@ public enum File: Equatable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to create a file with the given path.
|
|
||||||
///
|
|
||||||
/// ## NOTE: There are no checks on if a file / path actually exists or not.
|
|
||||||
///
|
|
||||||
/// If the file does not have a suffix of `json` or `toml` then
|
|
||||||
/// we will return `nil`.
|
|
||||||
public init?(_ path: String) {
|
public init?(_ path: String) {
|
||||||
self.init(URL(filePath: path))
|
self.init(URL(filePath: path))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the url of the file.
|
public static func json(_ path: String) -> Self {
|
||||||
|
.json(URL(filePath: path))
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func toml(_ path: String) -> Self {
|
||||||
|
.toml(URL(filePath: path))
|
||||||
|
}
|
||||||
|
|
||||||
public var url: URL {
|
public var url: URL {
|
||||||
switch self {
|
switch self {
|
||||||
case let .json(url): return url
|
case let .json(url): return url
|
||||||
@@ -45,14 +36,10 @@ public enum File: Equatable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the path string of the file.
|
|
||||||
public var path: String {
|
public var path: String {
|
||||||
url.cleanFilePath
|
url.cleanFilePath
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents the default file path for a user's configuration.
|
|
||||||
///
|
|
||||||
/// Which is `~/.config/hpa/config.toml`
|
|
||||||
public static var `default`: Self {
|
public static var `default`: Self {
|
||||||
let fileUrl = FileManager.default
|
let fileUrl = FileManager.default
|
||||||
.homeDirectoryForCurrentUser
|
.homeDirectoryForCurrentUser
|
||||||
|
|||||||
@@ -3,53 +3,22 @@ import DependenciesMacros
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public extension DependencyValues {
|
public extension DependencyValues {
|
||||||
/// Represents interactions with the file system.
|
|
||||||
///
|
|
||||||
var fileClient: FileClient {
|
var fileClient: FileClient {
|
||||||
get { self[FileClient.self] }
|
get { self[FileClient.self] }
|
||||||
set { self[FileClient.self] = newValue }
|
set { self[FileClient.self] = newValue }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents interactions with the file system.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
@DependencyClient
|
@DependencyClient
|
||||||
public struct FileClient: Sendable {
|
public struct FileClient: Sendable {
|
||||||
|
|
||||||
/// Copy an item from one location to another.
|
|
||||||
public var copy: @Sendable (URL, URL) async throws -> Void
|
public var copy: @Sendable (URL, URL) async throws -> Void
|
||||||
|
|
||||||
/// Create a directory at the given location.
|
|
||||||
public var createDirectory: @Sendable (URL) async throws -> Void
|
public var createDirectory: @Sendable (URL) async throws -> Void
|
||||||
|
|
||||||
/// Delete the item at the given location.
|
|
||||||
public var delete: @Sendable (URL) async throws -> Void
|
|
||||||
|
|
||||||
/// Check if a file exists at the given location.
|
|
||||||
public var fileExists: @Sendable (URL) -> Bool = { _ in true }
|
public var fileExists: @Sendable (URL) -> Bool = { _ in true }
|
||||||
|
public var findVaultFileInCurrentDirectory: @Sendable () async throws -> URL?
|
||||||
/// Find an ansible-vault file in the given location, checking up to 1 level deep
|
|
||||||
/// in subfolders.
|
|
||||||
public var findVaultFile: @Sendable (URL) async throws -> URL?
|
|
||||||
|
|
||||||
/// Return the user's home directory.
|
|
||||||
public var homeDirectory: @Sendable () -> URL = { URL(filePath: "~/") }
|
public var homeDirectory: @Sendable () -> URL = { URL(filePath: "~/") }
|
||||||
|
|
||||||
/// Check if an item is a directory or not.
|
|
||||||
public var isDirectory: @Sendable (URL) async throws -> Bool
|
public var isDirectory: @Sendable (URL) async throws -> Bool
|
||||||
|
|
||||||
/// Load a file from the given location.
|
|
||||||
public var load: @Sendable (URL) async throws -> Data
|
public var load: @Sendable (URL) async throws -> Data
|
||||||
|
|
||||||
/// Write data to a file at the given location.
|
|
||||||
public var write: @Sendable (Data, URL) async throws -> Void
|
public var write: @Sendable (Data, URL) async throws -> Void
|
||||||
|
|
||||||
/// Find an ansible-vault file in the current directory, checking up to 1 level
|
|
||||||
/// deep in subfolders.
|
|
||||||
public func findVaultFileInCurrentDirectory() async throws -> URL? {
|
|
||||||
try await findVaultFile(URL(filePath: "./"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FileClient: DependencyKey {
|
extension FileClient: DependencyKey {
|
||||||
@@ -57,17 +26,23 @@ extension FileClient: DependencyKey {
|
|||||||
|
|
||||||
public static var liveValue: Self {
|
public static var liveValue: Self {
|
||||||
let manager = LiveFileClient()
|
let manager = LiveFileClient()
|
||||||
return .init(
|
return .init {
|
||||||
copy: { try await manager.copy($0, to: $1) },
|
try await manager.copy($0, to: $1)
|
||||||
createDirectory: { try await manager.creatDirectory($0) },
|
} createDirectory: {
|
||||||
delete: { try await manager.delete($0) },
|
try await manager.creatDirectory($0)
|
||||||
fileExists: { manager.fileExists(at: $0) },
|
} fileExists: { url in
|
||||||
findVaultFile: { try await manager.findVaultFile(in: $0) },
|
manager.fileExists(at: url)
|
||||||
homeDirectory: { manager.homeDirectory() },
|
} findVaultFileInCurrentDirectory: {
|
||||||
isDirectory: { manager.isDirectory($0) },
|
try await manager.findVaultFileInCurrentDirectory()
|
||||||
load: { try await manager.load(from: $0) },
|
} homeDirectory: {
|
||||||
write: { try await manager.write($0, to: $1) }
|
manager.homeDirectory()
|
||||||
)
|
} isDirectory: {
|
||||||
|
manager.isDirectory($0)
|
||||||
|
} load: { url in
|
||||||
|
try await manager.load(from: url)
|
||||||
|
} write: { data, url in
|
||||||
|
try await manager.write(data, to: url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,37 +58,32 @@ struct LiveFileClient: Sendable {
|
|||||||
try manager.createDirectory(at: url, withIntermediateDirectories: true)
|
try manager.createDirectory(at: url, withIntermediateDirectories: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func delete(_ url: URL) async throws {
|
|
||||||
try manager.removeItem(at: url)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileExists(at url: URL) -> Bool {
|
func fileExists(at url: URL) -> Bool {
|
||||||
manager.fileExists(atPath: url.cleanFilePath)
|
manager.fileExists(atPath: url.cleanFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func findVaultFile(in url: URL) async throws -> URL? {
|
func findVaultFileInCurrentDirectory() async throws -> URL? {
|
||||||
guard isDirectory(url) else { return nil }
|
let urls = try manager.contentsOfDirectory(
|
||||||
let urls = try manager.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)
|
at: URL(filePath: "./"),
|
||||||
|
includingPropertiesForKeys: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check the current directory
|
||||||
|
if let vault = urls.firstVaultFile { return vault }
|
||||||
|
|
||||||
guard let vault = urls.firstVaultFile else {
|
|
||||||
// check subfolders, 1 layer deep.
|
|
||||||
let subfolders = urls.filter { isDirectory($0) }
|
let subfolders = urls.filter { isDirectory($0) }
|
||||||
|
|
||||||
for folder in subfolders {
|
for folder in subfolders {
|
||||||
let vault = try manager.contentsOfDirectory(
|
let files = try manager.contentsOfDirectory(
|
||||||
at: folder,
|
at: folder,
|
||||||
includingPropertiesForKeys: nil
|
includingPropertiesForKeys: nil
|
||||||
)
|
)
|
||||||
.firstVaultFile
|
if let vault = files.firstVaultFile { return vault }
|
||||||
|
|
||||||
if let vault { return vault }
|
|
||||||
}
|
}
|
||||||
// Didn't find a file.
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return vault
|
|
||||||
}
|
|
||||||
|
|
||||||
func homeDirectory() -> URL {
|
func homeDirectory() -> URL {
|
||||||
manager.homeDirectoryForCurrentUser
|
manager.homeDirectoryForCurrentUser
|
||||||
}
|
}
|
||||||
@@ -144,8 +114,6 @@ private extension Array where Element == URL {
|
|||||||
|
|
||||||
public extension URL {
|
public extension URL {
|
||||||
var cleanFilePath: String {
|
var cleanFilePath: String {
|
||||||
absoluteString
|
absoluteString.replacing("file://", with: "")
|
||||||
.replacing("file://", with: "")
|
|
||||||
.replacing("/private", with: "")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
extension PandocClient {
|
|
||||||
|
|
||||||
/// Represents constant string values needed internally.
|
|
||||||
enum Constants {
|
|
||||||
static let pandocCommand = "pandoc"
|
|
||||||
static let defaultOutputFileName = "Report"
|
|
||||||
static let defaultPdfEngine = "xelatex"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
import CommandClient
|
|
||||||
import ConfigurationClient
|
|
||||||
import Dependencies
|
|
||||||
import Foundation
|
|
||||||
import PlaybookClient
|
|
||||||
|
|
||||||
extension PandocClient.RunOptions {
|
|
||||||
|
|
||||||
func run(
|
|
||||||
_ fileType: PandocClient.FileType,
|
|
||||||
_ environment: [String: String]
|
|
||||||
) 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 ?? environment["PWD"]
|
|
||||||
|
|
||||||
guard let projectDirectory else {
|
|
||||||
throw ProjectDirectoryNotSpecified()
|
|
||||||
}
|
|
||||||
|
|
||||||
if shouldBuildProject {
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PandocClient.FileType {
|
|
||||||
var fileExtension: String {
|
|
||||||
switch self {
|
|
||||||
case .html: return "html"
|
|
||||||
case .latex: return "tex"
|
|
||||||
case .pdf: return "pdf"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@_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 = ".\(outputFileType.fileExtension)"
|
|
||||||
|
|
||||||
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 {}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import CommandClient
|
|
||||||
import ConfigurationClient
|
|
||||||
import Dependencies
|
|
||||||
import DependenciesMacros
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
public extension DependencyValues {
|
|
||||||
|
|
||||||
/// Represents interactions with the `pandoc` command line application.
|
|
||||||
///
|
|
||||||
/// The `pandoc` command line application is used to generate the final output
|
|
||||||
/// documents from a home performance assessment project.
|
|
||||||
///
|
|
||||||
var pandocClient: PandocClient {
|
|
||||||
get { self[PandocClient.self] }
|
|
||||||
set { self[PandocClient.self] = newValue }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@DependencyClient
|
|
||||||
public struct PandocClient: Sendable {
|
|
||||||
|
|
||||||
/// Run a pandoc command.
|
|
||||||
public var run: Run
|
|
||||||
|
|
||||||
/// Represents the pandoc commands that we can run.
|
|
||||||
@DependencyClient
|
|
||||||
public struct Run: Sendable {
|
|
||||||
|
|
||||||
/// Generate a latex file from the given options, returning the path the
|
|
||||||
/// generated file was written to.
|
|
||||||
public var generateLatex: @Sendable (RunOptions) async throws -> String
|
|
||||||
|
|
||||||
/// Generate an html file from the given options, returning the path the
|
|
||||||
/// generated file was written to.
|
|
||||||
public var generateHtml: @Sendable (RunOptions) async throws -> String
|
|
||||||
|
|
||||||
/// Generate a pdf file from the given options, returning the path the
|
|
||||||
/// generated file was written to.
|
|
||||||
public var generatePdf: @Sendable (RunOptions, String?) async throws -> String
|
|
||||||
|
|
||||||
/// Generate a pdf file from the given options, returning the path the
|
|
||||||
/// generated file was written to.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - options: The shared run options.
|
|
||||||
/// - pdfEngine: The pdf-engine to use to generate the pdf file.
|
|
||||||
public func generatePdf(
|
|
||||||
_ options: RunOptions,
|
|
||||||
pdfEngine: String? = nil
|
|
||||||
) async throws -> String {
|
|
||||||
try await generatePdf(options, pdfEngine)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the shared options used to run a `pandoc` command.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
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 shouldBuildProject: Bool
|
|
||||||
|
|
||||||
/// Create the shared run options.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - buildDirectory: Specify the build directory of the project.
|
|
||||||
/// - extraOptions: Extra arguments / options passed to the `pandoc` command.
|
|
||||||
/// - file: Files used to build the final output.
|
|
||||||
/// - loggingOptions: The logging options used when running the command.
|
|
||||||
/// - includeInHeader: Files to include in the header of the final output document.
|
|
||||||
/// - outputDirectory: Optional output directory for the output file, defaults to current working directory.
|
|
||||||
/// - projectDirectory: Optional project directory, defaults to current working directory.
|
|
||||||
/// - outputFileName: Override the output file name.
|
|
||||||
/// - quiet: Don't log output from the command.
|
|
||||||
/// - shell: Optional shell to use when calling the pandoc command.
|
|
||||||
/// - shouldBuildProject: Build the project prior to generating the output file.
|
|
||||||
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 shouldBuildProject: 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.shouldBuildProject = shouldBuildProject
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the file types that we can generate output files for.
|
|
||||||
@_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 func live(environment: [String: String]) -> PandocClient {
|
|
||||||
.init(
|
|
||||||
run: Run(
|
|
||||||
generateLatex: { try await $0.run(.latex, environment) },
|
|
||||||
generateHtml: { try await $0.run(.html, environment) },
|
|
||||||
generatePdf: { try await $0.run(.pdf(engine: $1), environment) }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public static var liveValue: PandocClient {
|
|
||||||
.live(environment: ProcessInfo.processInfo.environment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
// TODO: Use an actual version tag for playbook repo.
|
// TODO: Use an actuall version tag for playbook repo.
|
||||||
// TODO: Use an externally public url for the playbook repo.
|
|
||||||
|
|
||||||
public extension PlaybookClient {
|
public extension PlaybookClient {
|
||||||
@_spi(Internal)
|
@_spi(Internal)
|
||||||
enum Constants {
|
enum Constants {
|
||||||
|
|||||||
@@ -9,11 +9,6 @@ import ShellClient
|
|||||||
// TODO: Add update checks and pull for the playbook.
|
// TODO: Add update checks and pull for the playbook.
|
||||||
|
|
||||||
public extension DependencyValues {
|
public extension DependencyValues {
|
||||||
|
|
||||||
/// Manages interactions with the playbook repository as well as `ansible-playbook`
|
|
||||||
/// command line application.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
var playbookClient: PlaybookClient {
|
var playbookClient: PlaybookClient {
|
||||||
get { self[PlaybookClient.self] }
|
get { self[PlaybookClient.self] }
|
||||||
set { self[PlaybookClient.self] = newValue }
|
set { self[PlaybookClient.self] = newValue }
|
||||||
@@ -22,36 +17,21 @@ public extension DependencyValues {
|
|||||||
|
|
||||||
@DependencyClient
|
@DependencyClient
|
||||||
public struct PlaybookClient: Sendable {
|
public struct PlaybookClient: Sendable {
|
||||||
|
|
||||||
/// Manages interactions with the playbook repository.
|
|
||||||
public var repository: Repository
|
public var repository: Repository
|
||||||
|
|
||||||
/// Run the `ansible-playbook` command line application.
|
|
||||||
public var run: RunPlaybook
|
public var run: RunPlaybook
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension PlaybookClient {
|
public extension PlaybookClient {
|
||||||
|
|
||||||
/// Manages interactions with the `ansible-hpa-playbook` repository, which is
|
|
||||||
/// used to build and generate home performance assessment projects.
|
|
||||||
@DependencyClient
|
@DependencyClient
|
||||||
struct Repository: Sendable {
|
struct Repository: Sendable {
|
||||||
|
|
||||||
/// Install the repository based on the given configuration. If configuration is
|
|
||||||
/// not supplied then the default location for the playbook repository is
|
|
||||||
/// `~/.local/share/hpa/playbook`
|
|
||||||
public var install: @Sendable (Configuration?) async throws -> Void
|
public var install: @Sendable (Configuration?) async throws -> Void
|
||||||
|
|
||||||
/// Get the current directory of the playbook repository based on the given
|
|
||||||
/// configuration.
|
|
||||||
public var directory: @Sendable (Configuration?) async throws -> String
|
public var directory: @Sendable (Configuration?) async throws -> String
|
||||||
|
|
||||||
/// Install the playbook in the default location.
|
|
||||||
public func install() async throws {
|
public func install() async throws {
|
||||||
try await install(nil)
|
try await install(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current directory path of the playbook repository.
|
|
||||||
public func directory() async throws -> String {
|
public func directory() async throws -> String {
|
||||||
try await directory(nil)
|
try await directory(nil)
|
||||||
}
|
}
|
||||||
@@ -60,56 +40,23 @@ public extension PlaybookClient {
|
|||||||
|
|
||||||
public extension PlaybookClient {
|
public extension PlaybookClient {
|
||||||
|
|
||||||
/// Runs the `ansible-playbook` command line application with the `ansible-hpa-playbook`.
|
|
||||||
///
|
|
||||||
/// This is used to build and create home performance projects. It can also generate a
|
|
||||||
/// template for a user to customize to their use case.
|
|
||||||
@DependencyClient
|
@DependencyClient
|
||||||
struct RunPlaybook: Sendable {
|
struct RunPlaybook: Sendable {
|
||||||
|
|
||||||
/// Build a home performance assesment project with the given options.
|
|
||||||
public var buildProject: @Sendable (BuildOptions) async throws -> Void
|
public var buildProject: @Sendable (BuildOptions) async throws -> Void
|
||||||
|
|
||||||
/// Create a new home performance assesment project with the given options.
|
|
||||||
public var createProject: @Sendable (CreateOptions, JSONEncoder?) async throws -> Void
|
public var createProject: @Sendable (CreateOptions, JSONEncoder?) async throws -> Void
|
||||||
|
|
||||||
/// Generate a user's template from the default home performance template.
|
|
||||||
public var generateTemplate: @Sendable (GenerateTemplateOptions) async throws -> String
|
public var generateTemplate: @Sendable (GenerateTemplateOptions) async throws -> String
|
||||||
|
|
||||||
/// Create a new home performance assesment project with the given options.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - options: The options used to create the project.
|
|
||||||
public func createProject(_ options: CreateOptions) async throws {
|
public func createProject(_ options: CreateOptions) async throws {
|
||||||
try await createProject(options, nil)
|
try await createProject(options, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents options that are shared for all the `ansible-playbook` commands.
|
|
||||||
public struct SharedRunOptions: Equatable, Sendable {
|
public struct SharedRunOptions: Equatable, Sendable {
|
||||||
|
public let extraOptions: [String]?
|
||||||
|
public let inventoryFilePath: String?
|
||||||
|
public let loggingOptions: LoggingOptions
|
||||||
|
public let quiet: Bool
|
||||||
|
public let shell: String?
|
||||||
|
|
||||||
/// Extra arguments / options passed to the `ansible-playbook` command.
|
|
||||||
let extraOptions: [String]?
|
|
||||||
|
|
||||||
/// Specify the inventory file path.
|
|
||||||
let inventoryFilePath: String?
|
|
||||||
|
|
||||||
/// The logging options used when running the command.
|
|
||||||
let loggingOptions: LoggingOptions
|
|
||||||
|
|
||||||
/// Disables log output of the command.
|
|
||||||
let quiet: Bool
|
|
||||||
|
|
||||||
/// Optional shell to use when running the command.
|
|
||||||
let shell: String?
|
|
||||||
|
|
||||||
/// Create the shared options.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - extraOptions: The extra arguments / options to pass to the command.
|
|
||||||
/// - inventoryFilePath: Specify the inventory file path.
|
|
||||||
/// - loggingOptions: The logging options used when running the command.
|
|
||||||
/// - quiet: Disable log output from the command.
|
|
||||||
/// - shell: Specify a shell used when running the command.
|
|
||||||
public init(
|
public init(
|
||||||
extraOptions: [String]? = nil,
|
extraOptions: [String]? = nil,
|
||||||
inventoryFilePath: String? = nil,
|
inventoryFilePath: String? = nil,
|
||||||
@@ -125,21 +72,11 @@ public extension PlaybookClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Options require when calling the build project command.
|
|
||||||
@dynamicMemberLookup
|
@dynamicMemberLookup
|
||||||
public struct BuildOptions: Equatable, Sendable {
|
public struct BuildOptions: Equatable, Sendable {
|
||||||
|
public let projectDirectory: String?
|
||||||
|
public let shared: SharedRunOptions
|
||||||
|
|
||||||
/// An optional project directory, if not supplied then we will use the current working directory.
|
|
||||||
let projectDirectory: String?
|
|
||||||
|
|
||||||
/// The shared run options.
|
|
||||||
let shared: SharedRunOptions
|
|
||||||
|
|
||||||
/// Create new build options.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - projectDirectory: The optional project directory to build, if not supplied then we'll use the current working directory.
|
|
||||||
/// - shared: The shared run options.
|
|
||||||
public init(
|
public init(
|
||||||
projectDirectory: String? = nil,
|
projectDirectory: String? = nil,
|
||||||
shared: SharedRunOptions
|
shared: SharedRunOptions
|
||||||
@@ -153,25 +90,13 @@ public extension PlaybookClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Options required when creating a new home performance assessment project.
|
|
||||||
@dynamicMemberLookup
|
@dynamicMemberLookup
|
||||||
public struct CreateOptions: Equatable, Sendable {
|
public struct CreateOptions: Equatable, Sendable {
|
||||||
/// The directory to generate the new project in.
|
public let projectDirectory: String
|
||||||
let projectDirectory: String
|
public let shared: SharedRunOptions
|
||||||
/// Shared run options.
|
public let template: Configuration.Template?
|
||||||
let shared: SharedRunOptions
|
public let useLocalTemplateDirectory: Bool
|
||||||
/// Custom template configuration to use.
|
|
||||||
let template: Configuration.Template?
|
|
||||||
/// Specify whether we should only use a local template directory to create the project from.
|
|
||||||
let useLocalTemplateDirectory: Bool
|
|
||||||
|
|
||||||
/// Create new create options.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - projectDirectory: The directory to generate the project in.
|
|
||||||
/// - shared: The shared run options.
|
|
||||||
/// - template: Custom template configuration used when generating the project.
|
|
||||||
/// - useLocalTemplateDirectory: Whether to use a local template directory, not a template repository.
|
|
||||||
public init(
|
public init(
|
||||||
projectDirectory: String,
|
projectDirectory: String,
|
||||||
shared: SharedRunOptions,
|
shared: SharedRunOptions,
|
||||||
@@ -189,25 +114,13 @@ public extension PlaybookClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Options required when generating a new template repository / directory.
|
|
||||||
@dynamicMemberLookup
|
@dynamicMemberLookup
|
||||||
public struct GenerateTemplateOptions: Equatable, Sendable {
|
public struct GenerateTemplateOptions: Equatable, Sendable {
|
||||||
/// The shared run options.
|
public let shared: SharedRunOptions
|
||||||
let shared: SharedRunOptions
|
public let templateDirectory: String
|
||||||
/// The path to generate the template in.
|
public let templateVarsDirectory: String?
|
||||||
let templateDirectory: String
|
public let useVault: Bool
|
||||||
/// Specify the name of the directory for template variables.
|
|
||||||
let templateVarsDirectory: String?
|
|
||||||
/// Specify whether to use `ansible-vault` encrypted variables.
|
|
||||||
let useVault: Bool
|
|
||||||
|
|
||||||
/// Create new generate template options.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - shared: The shared run options
|
|
||||||
/// - templateDirectory: The path to generate the template in.
|
|
||||||
/// - templateVarsDirectory: Specify the name of the directory for template variables.
|
|
||||||
/// - useVault: Specify wheter to use `ansible-vault` encrypted variables.
|
|
||||||
public init(
|
public init(
|
||||||
shared: SharedRunOptions,
|
shared: SharedRunOptions,
|
||||||
templateDirectory: String,
|
templateDirectory: String,
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import Dependencies
|
|||||||
import DependenciesMacros
|
import DependenciesMacros
|
||||||
import FileClient
|
import FileClient
|
||||||
|
|
||||||
|
// TODO: Add edit / view routes, possibly create?
|
||||||
|
|
||||||
public extension DependencyValues {
|
public extension DependencyValues {
|
||||||
/// Manages interactions with `ansible-vault` command line application.
|
|
||||||
var vaultClient: VaultClient {
|
var vaultClient: VaultClient {
|
||||||
get { self[VaultClient.self] }
|
get { self[VaultClient.self] }
|
||||||
set { self[VaultClient.self] = newValue }
|
set { self[VaultClient.self] = newValue }
|
||||||
@@ -14,41 +15,23 @@ public extension DependencyValues {
|
|||||||
|
|
||||||
@DependencyClient
|
@DependencyClient
|
||||||
public struct VaultClient: Sendable {
|
public struct VaultClient: Sendable {
|
||||||
/// Run an `ansible-vault` command.
|
|
||||||
public var run: Run
|
public var run: Run
|
||||||
|
|
||||||
@DependencyClient
|
@DependencyClient
|
||||||
public struct Run: Sendable {
|
public struct Run: Sendable {
|
||||||
/// Decrypt an `ansible-vault` file.
|
|
||||||
public var decrypt: @Sendable (RunOptions) async throws -> String
|
public var decrypt: @Sendable (RunOptions) async throws -> String
|
||||||
/// Encrypt an `ansible-vault` file.
|
|
||||||
public var encrypt: @Sendable (RunOptions) async throws -> String
|
public var encrypt: @Sendable (RunOptions) async throws -> String
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct RunOptions: Equatable, Sendable {
|
public struct RunOptions: Equatable, Sendable {
|
||||||
|
|
||||||
/// Extra arguments / options passed to the `ansible-vault` command.
|
public let extraOptions: [String]?
|
||||||
let extraOptions: [String]?
|
public let loggingOptions: LoggingOptions
|
||||||
/// Logging options to use while running the command.
|
public let outputFilePath: String?
|
||||||
let loggingOptions: LoggingOptions
|
public let quiet: Bool
|
||||||
/// The optional output file path.
|
public let shell: String?
|
||||||
let outputFilePath: String?
|
public let vaultFilePath: String?
|
||||||
/// Disable logging output.
|
|
||||||
let quiet: Bool
|
|
||||||
/// Optional shell used to call the command.
|
|
||||||
let shell: String?
|
|
||||||
/// The path to the vault file, if not supplied we will search the current directory for it.
|
|
||||||
let vaultFilePath: String?
|
|
||||||
|
|
||||||
/// Create new run options.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - extraOptions: Extra arguments / options passed to the command.
|
|
||||||
/// - loggingOptions: The logging options used while running the command.
|
|
||||||
/// - outputFilePath: The optional output file path.
|
|
||||||
/// - quiet: Disable logging output of the command.
|
|
||||||
/// - shell: The shell used to call the command.
|
|
||||||
/// - vaultFilePath: The vault file, if not supplied we will search in the current working directory.
|
|
||||||
public init(
|
public init(
|
||||||
extraOptions: [String]? = nil,
|
extraOptions: [String]? = nil,
|
||||||
loggingOptions: LoggingOptions,
|
loggingOptions: LoggingOptions,
|
||||||
@@ -68,8 +51,8 @@ public struct VaultClient: Sendable {
|
|||||||
|
|
||||||
@_spi(Internal)
|
@_spi(Internal)
|
||||||
public enum Route: String, Equatable, Sendable {
|
public enum Route: String, Equatable, Sendable {
|
||||||
case decrypt
|
|
||||||
case encrypt
|
case encrypt
|
||||||
|
case decrypt
|
||||||
|
|
||||||
public var verb: String { rawValue }
|
public var verb: String { rawValue }
|
||||||
}
|
}
|
||||||
@@ -82,8 +65,12 @@ extension VaultClient: DependencyKey {
|
|||||||
public static var liveValue: VaultClient {
|
public static var liveValue: VaultClient {
|
||||||
.init(
|
.init(
|
||||||
run: .init(
|
run: .init(
|
||||||
decrypt: { try await $0.run(route: .decrypt) },
|
decrypt: {
|
||||||
encrypt: { try await $0.run(route: .encrypt) }
|
try await $0.run(route: .decrypt)
|
||||||
|
},
|
||||||
|
encrypt: {
|
||||||
|
try await $0.run(route: .encrypt)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import Foundation
|
import Foundation
|
||||||
import PlaybookClient
|
import PlaybookClient
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
import ConfigurationClient
|
import ConfigurationClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -70,3 +71,14 @@ struct CreateCommand: AsyncParsableCommand {
|
|||||||
print(projectDir)
|
print(projectDir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension CreateCommand {
|
||||||
|
var generateJsonOptions: CliClient.GenerateJsonOptions {
|
||||||
|
.init(
|
||||||
|
templateDirectory: templateDir,
|
||||||
|
templateRepo: repo,
|
||||||
|
version: branch,
|
||||||
|
useLocalTemplateDirectory: localTemplateDir
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import PandocClient
|
|
||||||
|
|
||||||
// TODO: Need to add a step to build prior to generating file.
|
// TODO: Need to add a step to build prior to generating file.
|
||||||
struct GenerateHtmlCommand: AsyncParsableCommand {
|
struct GenerateHtmlCommand: AsyncParsableCommand {
|
||||||
@@ -13,12 +13,12 @@ struct GenerateHtmlCommand: AsyncParsableCommand {
|
|||||||
@OptionGroup var globals: GenerateOptions
|
@OptionGroup var globals: GenerateOptions
|
||||||
|
|
||||||
mutating func run() async throws {
|
mutating func run() async throws {
|
||||||
@Dependency(\.pandocClient) var pandocClient
|
@Dependency(\.cliClient) var cliClient
|
||||||
|
|
||||||
let output = try await pandocClient.run.generateHtml(
|
try await cliClient.runPandocCommand(
|
||||||
globals.pandocRunOptions(commandName: Self.commandName)
|
globals.pandocOptions(.html),
|
||||||
|
logging: globals.loggingOptions(commandName: Self.commandName)
|
||||||
)
|
)
|
||||||
print(output)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
|
|
||||||
// TODO: Need to add a step to build prior to generating file.
|
// TODO: Need to add a step to build prior to generating file.
|
||||||
@@ -12,11 +13,11 @@ struct GenerateLatexCommand: AsyncParsableCommand {
|
|||||||
@OptionGroup var globals: GenerateOptions
|
@OptionGroup var globals: GenerateOptions
|
||||||
|
|
||||||
mutating func run() async throws {
|
mutating func run() async throws {
|
||||||
@Dependency(\.pandocClient) var pandocClient
|
@Dependency(\.cliClient) var cliClient
|
||||||
|
|
||||||
let output = try await pandocClient.run.generateLatex(
|
try await cliClient.runPandocCommand(
|
||||||
globals.pandocRunOptions(commandName: Self.commandName)
|
globals.pandocOptions(.latex),
|
||||||
|
logging: globals.loggingOptions(commandName: Self.commandName)
|
||||||
)
|
)
|
||||||
print(output)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,11 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
import CommandClient
|
import CliClient
|
||||||
import PandocClient
|
|
||||||
|
|
||||||
@dynamicMemberLookup
|
@dynamicMemberLookup
|
||||||
struct GenerateOptions: ParsableArguments {
|
struct GenerateOptions: ParsableArguments {
|
||||||
|
|
||||||
@OptionGroup var basic: BasicGlobalOptions
|
@OptionGroup var basic: BasicGlobalOptions
|
||||||
|
|
||||||
@Option(
|
|
||||||
name: .shortAndLong,
|
|
||||||
help: "Custom build directory path.",
|
|
||||||
completion: .directory
|
|
||||||
)
|
|
||||||
var buildDirectory: String?
|
|
||||||
|
|
||||||
@Option(
|
@Option(
|
||||||
name: [.short, .customLong("file")],
|
name: [.short, .customLong("file")],
|
||||||
help: "Files used to generate the output, can be specified multiple times.",
|
help: "Files used to generate the output, can be specified multiple times.",
|
||||||
@@ -73,20 +65,20 @@ struct GenerateOptions: ParsableArguments {
|
|||||||
|
|
||||||
extension GenerateOptions {
|
extension GenerateOptions {
|
||||||
|
|
||||||
func loggingOptions(commandName: String) -> LoggingOptions {
|
func loggingOptions(commandName: String) -> CliClient.LoggingOptions {
|
||||||
basic.loggingOptions(commandName: commandName)
|
basic.loggingOptions(commandName: commandName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func pandocRunOptions(commandName: String) -> PandocClient.RunOptions {
|
func pandocOptions(
|
||||||
|
_ fileType: CliClient.PandocOptions.FileType
|
||||||
|
) -> CliClient.PandocOptions {
|
||||||
.init(
|
.init(
|
||||||
buildDirectory: buildDirectory,
|
|
||||||
extraOptions: extraOptions.count > 0 ? extraOptions : nil,
|
|
||||||
files: files.count > 0 ? files : nil,
|
files: files.count > 0 ? files : nil,
|
||||||
loggingOptions: .init(commandName: commandName, logLevel: .init(globals: basic, quietOnlyPlaybook: false)),
|
|
||||||
includeInHeader: includeInHeader.count > 0 ? includeInHeader : nil,
|
includeInHeader: includeInHeader.count > 0 ? includeInHeader : nil,
|
||||||
outputDirectory: outputDirectory,
|
outputDirectory: outputDirectory,
|
||||||
projectDirectory: projectDirectory,
|
|
||||||
outputFileName: outputFileName,
|
outputFileName: outputFileName,
|
||||||
|
outputFileType: fileType,
|
||||||
|
projectDirectory: projectDirectory,
|
||||||
quiet: basic.quiet,
|
quiet: basic.quiet,
|
||||||
shell: basic.shell,
|
shell: basic.shell,
|
||||||
shouldBuild: !noBuild
|
shouldBuild: !noBuild
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import PandocClient
|
|
||||||
|
|
||||||
// TODO: Need to add a step to build prior to generating file.
|
// TODO: Need to add a step to build prior to generating file.
|
||||||
|
|
||||||
@@ -20,11 +20,11 @@ struct GeneratePdfCommand: AsyncParsableCommand {
|
|||||||
@OptionGroup var globals: GenerateOptions
|
@OptionGroup var globals: GenerateOptions
|
||||||
|
|
||||||
mutating func run() async throws {
|
mutating func run() async throws {
|
||||||
@Dependency(\.pandocClient) var pandocClient
|
@Dependency(\.cliClient) var cliClient
|
||||||
|
|
||||||
let output = try await pandocClient.run.generatePdf(
|
let output = try await cliClient.runPandocCommand(
|
||||||
globals.pandocRunOptions(commandName: Self.commandName),
|
globals.pandocOptions(.pdf(engine: pdfEngine)),
|
||||||
pdfEngine: pdfEngine
|
logging: globals.loggingOptions(commandName: Self.commandName)
|
||||||
)
|
)
|
||||||
|
|
||||||
print(output)
|
print(output)
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ import Rainbow
|
|||||||
// Constant string values.
|
// Constant string values.
|
||||||
enum Constants {
|
enum Constants {
|
||||||
static let appName = "hpa"
|
static let appName = "hpa"
|
||||||
static let brewPackages = [
|
|
||||||
"ansible", "imagemagick", "pandoc", "texLive"
|
|
||||||
]
|
|
||||||
static let playbookFileName = "main.yml"
|
static let playbookFileName = "main.yml"
|
||||||
static let inventoryFileName = "inventory.ini"
|
static let inventoryFileName = "inventory.ini"
|
||||||
static let importantExtraArgsNote = """
|
static let importantExtraArgsNote = """
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
import CommandClient
|
import CliClient
|
||||||
import ConfigurationClient
|
import ConfigurationClient
|
||||||
import PlaybookClient
|
import PlaybookClient
|
||||||
|
|
||||||
@@ -59,8 +59,10 @@ struct GlobalOptions: ParsableArguments {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Update these to use CommandClient.LoggingOptions
|
||||||
|
|
||||||
extension GlobalOptions {
|
extension GlobalOptions {
|
||||||
func loggingOptions(commandName: String) -> LoggingOptions {
|
func loggingOptions(commandName: String) -> CliClient.LoggingOptions {
|
||||||
.init(
|
.init(
|
||||||
commandName: commandName,
|
commandName: commandName,
|
||||||
logLevel: .init(globals: basic, quietOnlyPlaybook: quietOnlyPlaybook)
|
logLevel: .init(globals: basic, quietOnlyPlaybook: quietOnlyPlaybook)
|
||||||
@@ -69,7 +71,7 @@ extension GlobalOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension BasicGlobalOptions {
|
extension BasicGlobalOptions {
|
||||||
func loggingOptions(commandName: String) -> LoggingOptions {
|
func loggingOptions(commandName: String) -> CliClient.LoggingOptions {
|
||||||
.init(
|
.init(
|
||||||
commandName: commandName,
|
commandName: commandName,
|
||||||
logLevel: .init(globals: self, quietOnlyPlaybook: false)
|
logLevel: .init(globals: self, quietOnlyPlaybook: false)
|
||||||
@@ -77,6 +79,24 @@ 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 {
|
extension GlobalOptions {
|
||||||
func sharedPlaybookRunOptions(
|
func sharedPlaybookRunOptions(
|
||||||
commandName: String,
|
commandName: String,
|
||||||
|
|||||||
18
Sources/hpa/Internal/VaultOptions+Globals.swift
Normal file
18
Sources/hpa/Internal/VaultOptions+Globals.swift
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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,6 +1,6 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
import CliDoc
|
import CliDoc
|
||||||
import CommandClient
|
|
||||||
import ConfigurationClient
|
import ConfigurationClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
|
|
||||||
@@ -16,14 +16,14 @@ struct GenerateConfigurationCommand: AsyncParsableCommand {
|
|||||||
Note {
|
Note {
|
||||||
"""
|
"""
|
||||||
If a directory is not supplied then a configuration file will be created
|
If a directory is not supplied then a configuration file will be created
|
||||||
at \("'~/.config/hpa/config.toml'".yellow).
|
at \("'~/.config/hpa-playbook/config'".yellow).
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
VStack {
|
VStack {
|
||||||
"EXAMPLE:".yellow.bold
|
"EXAMPLE:".yellow.bold
|
||||||
"Create a directory and generate the configuration".green
|
"Create a directory and generate the configuration".green
|
||||||
ShellCommand("mkdir -p ~/.config/hpa")
|
ShellCommand("mkdir -p ~/.config/hpa-playbook")
|
||||||
ShellCommand("hpa generate-config --path ~/.config/hpa")
|
ShellCommand("hpa generate-config --path ~/.config/hpa-playbook")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.separator(.newLine(count: 2))
|
.separator(.newLine(count: 2))
|
||||||
@@ -39,7 +39,7 @@ struct GenerateConfigurationCommand: AsyncParsableCommand {
|
|||||||
|
|
||||||
@Flag(
|
@Flag(
|
||||||
name: .shortAndLong,
|
name: .shortAndLong,
|
||||||
help: "Generate a json file, instead of the default toml style"
|
help: "Generate a json file, instead of default env style"
|
||||||
)
|
)
|
||||||
var json: Bool = false
|
var json: Bool = false
|
||||||
|
|
||||||
@@ -56,18 +56,26 @@ struct GenerateConfigurationCommand: AsyncParsableCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func _run() async throws {
|
private func _run() async throws {
|
||||||
|
@Dependency(\.cliClient) var cliClient
|
||||||
@Dependency(\.configurationClient) var configurationClient
|
@Dependency(\.configurationClient) var configurationClient
|
||||||
|
|
||||||
try await globals.loggingOptions(commandName: Self.commandName).withLogger {
|
try await cliClient.withLogger(globals.loggingOptions(commandName: Self.commandName)) {
|
||||||
@Dependency(\.logger) var logger
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
let output = try await configurationClient.generate(.init(
|
let actualPath: File
|
||||||
force: force,
|
|
||||||
json: json,
|
|
||||||
path: path != nil ? .directory(path!) : nil
|
|
||||||
))
|
|
||||||
|
|
||||||
print(output)
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import PlaybookClient
|
import PlaybookClient
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
import CliDoc
|
import CliDoc
|
||||||
import CommandClient
|
|
||||||
import Dependencies
|
import Dependencies
|
||||||
|
|
||||||
struct InstallDependenciesCommand: AsyncParsableCommand {
|
struct InstallDependenciesCommand: AsyncParsableCommand {
|
||||||
@@ -33,20 +33,11 @@ struct InstallDependenciesCommand: AsyncParsableCommand {
|
|||||||
var extraOptions: [String] = []
|
var extraOptions: [String] = []
|
||||||
|
|
||||||
mutating func run() async throws {
|
mutating func run() async throws {
|
||||||
@Dependency(\.commandClient) var commandClient
|
@Dependency(\.cliClient) var cliClient
|
||||||
@Dependency(\.playbookClient) var playbookClient
|
try await cliClient.installDependencies(
|
||||||
|
|
||||||
let arguments = [
|
|
||||||
"brew", "install"
|
|
||||||
] + Constants.brewPackages
|
|
||||||
+ extraOptions
|
|
||||||
|
|
||||||
try await commandClient.run(
|
|
||||||
quiet: globals.quiet,
|
quiet: globals.quiet,
|
||||||
shell: globals.shell,
|
shell: globals.shell,
|
||||||
arguments
|
extraArgs: extraOptions
|
||||||
)
|
)
|
||||||
|
|
||||||
try await playbookClient.repository.install()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ struct UtilsCommand: AsyncParsableCommand {
|
|||||||
These are commands that are generally only run on occasion / less frequently used.
|
These are commands that are generally only run on occasion / less frequently used.
|
||||||
""",
|
""",
|
||||||
subcommands: [
|
subcommands: [
|
||||||
DumpConfigCommand.self,
|
|
||||||
GenerateProjectTemplateCommand.self,
|
GenerateProjectTemplateCommand.self,
|
||||||
GenerateConfigurationCommand.self,
|
GenerateConfigurationCommand.self,
|
||||||
InstallDependenciesCommand.self
|
InstallDependenciesCommand.self
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import VaultClient
|
import VaultClient
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import VaultClient
|
import VaultClient
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ struct VaultCommand: AsyncParsableCommand {
|
|||||||
.separator(.newLine(count: 2))
|
.separator(.newLine(count: 2))
|
||||||
},
|
},
|
||||||
subcommands: [
|
subcommands: [
|
||||||
DecryptCommand.self, EncryptCommand.self
|
EncryptCommand.self, DecryptCommand.self
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
import CommandClient
|
import CliClient
|
||||||
import VaultClient
|
import VaultClient
|
||||||
|
|
||||||
// Holds the common options for vault commands, as they all share the
|
// Holds the common options for vault commands, as they all share the
|
||||||
@@ -32,6 +32,12 @@ struct VaultOptions: ParsableArguments {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension VaultOptions {
|
||||||
|
func loggingOptions(commandName: String) -> CliClient.LoggingOptions {
|
||||||
|
globals.loggingOptions(commandName: commandName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension VaultOptions {
|
extension VaultOptions {
|
||||||
|
|
||||||
func runOptions(
|
func runOptions(
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
// Do not set this variable, it is set during the build process.
|
// Do not set this variable, it is set during the build process.
|
||||||
let VERSION: String? = "0.1.1"
|
let VERSION: String? = nil
|
||||||
|
|||||||
333
Tests/CliClientTests/CliClientTests.swift
Normal file
333
Tests/CliClientTests/CliClientTests.swift
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
@_spi(Internal) import CliClient
|
||||||
|
import ConfigurationClient
|
||||||
|
import Dependencies
|
||||||
|
import FileClient
|
||||||
|
import Foundation
|
||||||
|
import PlaybookClient
|
||||||
|
import ShellClient
|
||||||
|
import Testing
|
||||||
|
import TestSupport
|
||||||
|
|
||||||
|
@Suite("CliClientTests")
|
||||||
|
struct CliClientTests: TestCase {
|
||||||
|
|
||||||
|
static let loggingOptions: CliClient.LoggingOptions = {
|
||||||
|
let levelString = ProcessInfo.processInfo.environment["LOGGING_LEVEL"] ?? "debug"
|
||||||
|
let logLevel = Logger.Level(rawValue: levelString) ?? .debug
|
||||||
|
return .init(commandName: "CliClientTests", logLevel: logLevel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func capturingClient() async throws {
|
||||||
|
let captured = CliClient.CapturingClient()
|
||||||
|
let client = CliClient.capturing(captured)
|
||||||
|
try await client.runCommand(quiet: false, shell: .zsh(), "foo", "bar")
|
||||||
|
|
||||||
|
let quiet = await captured.quiet!
|
||||||
|
#expect(quiet == false)
|
||||||
|
|
||||||
|
let shell = await captured.shell
|
||||||
|
#expect(shell == .zsh())
|
||||||
|
|
||||||
|
let arguments = await captured.arguments!
|
||||||
|
#expect(arguments == ["foo", "bar"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(arguments: ["encrypt", "decrypt"])
|
||||||
|
func runVault(argument: String) async throws {
|
||||||
|
let captured = CliClient.CapturingClient()
|
||||||
|
try await withMockConfiguration(captured, key: "runVault") {
|
||||||
|
$0.fileClient.findVaultFileInCurrentDirectory = { URL(filePath: "vault.yml") }
|
||||||
|
} operation: {
|
||||||
|
@Dependency(\.cliClient) var cliClient
|
||||||
|
let configuration = Configuration.mock
|
||||||
|
|
||||||
|
try await cliClient.runVaultCommand(
|
||||||
|
.init(arguments: [argument], quiet: false, shell: nil),
|
||||||
|
logging: Self.loggingOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
let shell = await captured.shell
|
||||||
|
#expect(shell == .zsh(useDashC: true))
|
||||||
|
|
||||||
|
let vaultPath = URL(filePath: #file)
|
||||||
|
.deletingLastPathComponent()
|
||||||
|
.deletingLastPathComponent()
|
||||||
|
.appending(path: "vault.yml")
|
||||||
|
|
||||||
|
var encryptArgs: [String] = []
|
||||||
|
if argument == "encrypt", let id = configuration.vault.encryptId {
|
||||||
|
encryptArgs = ["--encrypt-vault-id", id]
|
||||||
|
}
|
||||||
|
|
||||||
|
let expectedArguments = [
|
||||||
|
"ansible-vault", argument
|
||||||
|
] + configuration.vault.args!
|
||||||
|
+ encryptArgs
|
||||||
|
+ [vaultPath.cleanFilePath]
|
||||||
|
|
||||||
|
let arguments = await captured.arguments
|
||||||
|
#expect(arguments == expectedArguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(arguments: [
|
||||||
|
Configuration(
|
||||||
|
args: ["--tags", "debug"],
|
||||||
|
useVaultArgs: true,
|
||||||
|
playbook: .init(directory: "playbook", inventory: nil),
|
||||||
|
vault: .mock
|
||||||
|
)
|
||||||
|
])
|
||||||
|
func runPlaybook(configuration: Configuration) async throws {
|
||||||
|
let captured = CliClient.CapturingClient()
|
||||||
|
try await withMockConfiguration(captured, configuration: configuration, key: "runPlaybook") {
|
||||||
|
@Dependency(\.cliClient) var cliClient
|
||||||
|
|
||||||
|
try await cliClient.runPlaybookCommand(
|
||||||
|
.init(
|
||||||
|
arguments: [],
|
||||||
|
quiet: false,
|
||||||
|
shell: nil
|
||||||
|
),
|
||||||
|
logging: Self.loggingOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
let expectedArguments = [
|
||||||
|
"ansible-playbook", "playbook/main.yml",
|
||||||
|
"--inventory", "playbook/inventory.ini",
|
||||||
|
"--tags", "debug",
|
||||||
|
"--vault-id=myId@$SCRIPTS/vault-gopass-client"
|
||||||
|
]
|
||||||
|
|
||||||
|
let arguments = await captured.arguments
|
||||||
|
#expect(arguments == expectedArguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Test
|
||||||
|
// func ensuredPlaybookDirectory() throws {
|
||||||
|
// let configuration = Configuration.mock
|
||||||
|
// let playbookDir = try configuration.ensuredPlaybookDirectory("playbook")
|
||||||
|
// #expect(playbookDir == "playbook")
|
||||||
|
//
|
||||||
|
// do {
|
||||||
|
// _ = try configuration.ensuredPlaybookDirectory(nil)
|
||||||
|
// #expect(Bool(false))
|
||||||
|
// } catch {
|
||||||
|
// #expect(Bool(true))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func shellOrDefault() {
|
||||||
|
var shell: String? = "/bin/bash"
|
||||||
|
#expect(shell.orDefault == .custom(path: "/bin/bash", useDashC: true))
|
||||||
|
|
||||||
|
shell = nil
|
||||||
|
#expect(shell.orDefault == .zsh(useDashC: true))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func testEnsuredInventoryPath() {
|
||||||
|
let configuration = Configuration(playbook: .init(inventory: "inventory.ini"))
|
||||||
|
let playbookDir = "playbook"
|
||||||
|
let inventoryPath = "inventory.ini"
|
||||||
|
|
||||||
|
var output = ensuredInventoryPath(
|
||||||
|
inventoryPath,
|
||||||
|
configuration: configuration,
|
||||||
|
playbookDirectory: playbookDir
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(output == "inventory.ini")
|
||||||
|
|
||||||
|
output = ensuredInventoryPath(
|
||||||
|
nil,
|
||||||
|
configuration: configuration,
|
||||||
|
playbookDirectory: playbookDir
|
||||||
|
)
|
||||||
|
#expect(output == "inventory.ini")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func vaultFilePath() async throws {
|
||||||
|
var fileClient = FileClient.testValue
|
||||||
|
fileClient.findVaultFileInCurrentDirectory = { URL(string: "vault.yml") }
|
||||||
|
|
||||||
|
var output = try await fileClient.ensuredVaultFilePath("vault.yml")
|
||||||
|
#expect(output == "vault.yml")
|
||||||
|
|
||||||
|
output = try await fileClient.ensuredVaultFilePath(nil)
|
||||||
|
|
||||||
|
fileClient.findVaultFileInCurrentDirectory = { nil }
|
||||||
|
do {
|
||||||
|
_ = try await fileClient.ensuredVaultFilePath(nil)
|
||||||
|
#expect(Bool(false))
|
||||||
|
} catch {
|
||||||
|
#expect(Bool(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(
|
||||||
|
arguments: [
|
||||||
|
GenerateJsonTestOption(
|
||||||
|
options: .init(
|
||||||
|
templateDirectory: nil,
|
||||||
|
templateRepo: nil,
|
||||||
|
version: nil,
|
||||||
|
useLocalTemplateDirectory: true
|
||||||
|
),
|
||||||
|
configuration: nil,
|
||||||
|
expectation: .failing
|
||||||
|
),
|
||||||
|
GenerateJsonTestOption(
|
||||||
|
options: .init(
|
||||||
|
templateDirectory: nil,
|
||||||
|
templateRepo: nil,
|
||||||
|
version: nil,
|
||||||
|
useLocalTemplateDirectory: false
|
||||||
|
),
|
||||||
|
configuration: nil,
|
||||||
|
expectation: .failing
|
||||||
|
),
|
||||||
|
GenerateJsonTestOption(
|
||||||
|
options: .init(
|
||||||
|
templateDirectory: "template",
|
||||||
|
templateRepo: nil,
|
||||||
|
version: nil,
|
||||||
|
useLocalTemplateDirectory: true
|
||||||
|
),
|
||||||
|
configuration: nil,
|
||||||
|
expectation: .success("""
|
||||||
|
{
|
||||||
|
"template" : {
|
||||||
|
"path" : "template"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
),
|
||||||
|
GenerateJsonTestOption(
|
||||||
|
options: .init(
|
||||||
|
templateDirectory: nil,
|
||||||
|
templateRepo: nil,
|
||||||
|
version: nil,
|
||||||
|
useLocalTemplateDirectory: false
|
||||||
|
),
|
||||||
|
configuration: .init(template: .init(directory: "template")),
|
||||||
|
expectation: .success("""
|
||||||
|
{
|
||||||
|
"template" : {
|
||||||
|
"path" : "template"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
),
|
||||||
|
GenerateJsonTestOption(
|
||||||
|
options: .init(
|
||||||
|
templateDirectory: nil,
|
||||||
|
templateRepo: "https://git.example.com/template.git",
|
||||||
|
version: nil,
|
||||||
|
useLocalTemplateDirectory: false
|
||||||
|
),
|
||||||
|
configuration: nil,
|
||||||
|
expectation: .success("""
|
||||||
|
{
|
||||||
|
"template" : {
|
||||||
|
"repo" : {
|
||||||
|
"url" : "https://git.example.com/template.git",
|
||||||
|
"version" : "main"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
),
|
||||||
|
GenerateJsonTestOption(
|
||||||
|
options: .init(
|
||||||
|
templateDirectory: nil,
|
||||||
|
templateRepo: nil,
|
||||||
|
version: nil,
|
||||||
|
useLocalTemplateDirectory: false
|
||||||
|
),
|
||||||
|
configuration: .init(template: .init(url: "https://git.example.com/template.git", version: "v0.1.0")),
|
||||||
|
expectation: .success("""
|
||||||
|
{
|
||||||
|
"template" : {
|
||||||
|
"repo" : {
|
||||||
|
"url" : "https://git.example.com/template.git",
|
||||||
|
"version" : "v0.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
func generateJson(input: GenerateJsonTestOption) async {
|
||||||
|
await withTestLogger(key: "generateJson") {
|
||||||
|
$0.configurationClient = .mock(input.configuration ?? .init())
|
||||||
|
$0.cliClient = .liveValue
|
||||||
|
} operation: {
|
||||||
|
@Dependency(\.cliClient) var cliClient
|
||||||
|
|
||||||
|
let json = try? await cliClient.generateJSON(
|
||||||
|
input.options,
|
||||||
|
logging: Self.loggingOptions,
|
||||||
|
encoder: jsonEncoder
|
||||||
|
)
|
||||||
|
|
||||||
|
switch input.expectation {
|
||||||
|
case let .success(expected):
|
||||||
|
#expect(json == expected)
|
||||||
|
case .failing:
|
||||||
|
#expect(json == nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withMockConfiguration(
|
||||||
|
_ capturing: CliClient.CapturingClient,
|
||||||
|
configuration: Configuration = .mock,
|
||||||
|
key: String,
|
||||||
|
logLevel: Logger.Level = .trace,
|
||||||
|
depednencies setupDependencies: @escaping (inout DependencyValues) -> Void = { _ in },
|
||||||
|
operation: @Sendable @escaping () async throws -> Void
|
||||||
|
) async rethrows {
|
||||||
|
try await withTestLogger(key: key, logLevel: logLevel) {
|
||||||
|
$0.configurationClient = .mock(configuration)
|
||||||
|
$0.cliClient = .capturing(capturing)
|
||||||
|
$0.playbookClient = .liveValue
|
||||||
|
setupDependencies(&$0)
|
||||||
|
} operation: {
|
||||||
|
try await operation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GenerateJsonTestOption: Sendable {
|
||||||
|
let options: CliClient.GenerateJsonOptions
|
||||||
|
let configuration: Configuration?
|
||||||
|
let expectation: GenerateJsonExpectation
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GenerateJsonExpectation: Sendable {
|
||||||
|
case failing
|
||||||
|
case success(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConfigurationClient {
|
||||||
|
static func mock(_ configuration: Configuration) -> Self {
|
||||||
|
var mock = Self.testValue
|
||||||
|
mock.find = { throw TestError() }
|
||||||
|
mock.load = { _ in configuration }
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestError: Error {}
|
||||||
|
|
||||||
|
let jsonEncoder: JSONEncoder = {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes, .sortedKeys]
|
||||||
|
return encoder
|
||||||
|
}()
|
||||||
@@ -18,7 +18,6 @@ struct ConfigurationClientTests: TestCase {
|
|||||||
@Test(arguments: ["config.toml", "config.json"])
|
@Test(arguments: ["config.toml", "config.json"])
|
||||||
func generateConfigFile(fileName: String) async throws {
|
func generateConfigFile(fileName: String) async throws {
|
||||||
try await withTestLogger(key: "generateConfigFile") {
|
try await withTestLogger(key: "generateConfigFile") {
|
||||||
$0.coders = .liveValue
|
|
||||||
$0.fileClient = .liveValue
|
$0.fileClient = .liveValue
|
||||||
} operation: {
|
} operation: {
|
||||||
@Dependency(\.logger) var logger
|
@Dependency(\.logger) var logger
|
||||||
@@ -27,14 +26,17 @@ struct ConfigurationClientTests: TestCase {
|
|||||||
|
|
||||||
try await withTemporaryDirectory { tempDir in
|
try await withTemporaryDirectory { tempDir in
|
||||||
let tempFile = tempDir.appending(path: fileName)
|
let tempFile = tempDir.appending(path: fileName)
|
||||||
let output = try await configuration.generate(.init(
|
try await configuration.generate(at: File(tempFile)!, force: false)
|
||||||
force: false,
|
|
||||||
json: fileName.hasSuffix("json"),
|
|
||||||
path: .file(File(tempFile)!)
|
|
||||||
))
|
|
||||||
#expect(FileManager.default.fileExists(atPath: tempFile.cleanFilePath))
|
#expect(FileManager.default.fileExists(atPath: tempFile.cleanFilePath))
|
||||||
#expect(fileClient.fileExists(tempFile))
|
#expect(fileClient.fileExists(tempFile))
|
||||||
#expect(output == tempFile.cleanFilePath)
|
|
||||||
|
// Ensure that we do not overwrite files if they exist.
|
||||||
|
do {
|
||||||
|
try await configuration.generate(at: File(tempFile)!, force: false)
|
||||||
|
#expect(Bool(false))
|
||||||
|
} catch {
|
||||||
|
#expect(Bool(true))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,7 +44,6 @@ struct ConfigurationClientTests: TestCase {
|
|||||||
@Test(arguments: ["config.toml", "config.json", nil])
|
@Test(arguments: ["config.toml", "config.json", nil])
|
||||||
func loadConfigFile(fileName: String?) async throws {
|
func loadConfigFile(fileName: String?) async throws {
|
||||||
try await withTestLogger(key: "generateConfigFile") {
|
try await withTestLogger(key: "generateConfigFile") {
|
||||||
$0.coders = .liveValue
|
|
||||||
$0.fileClient = .liveValue
|
$0.fileClient = .liveValue
|
||||||
} operation: {
|
} operation: {
|
||||||
@Dependency(\.logger) var logger
|
@Dependency(\.logger) var logger
|
||||||
@@ -129,27 +130,6 @@ struct ConfigurationClientTests: TestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
func writeCreatesBackupFile() async throws {
|
|
||||||
try await withDependencies {
|
|
||||||
$0.fileClient = .liveValue
|
|
||||||
} operation: {
|
|
||||||
let client = ConfigurationClient.liveValue
|
|
||||||
|
|
||||||
try await withGeneratedConfigFile(named: "config.toml", client: client) { configFile in
|
|
||||||
@Dependency(\.fileClient) var fileClient
|
|
||||||
|
|
||||||
let backupUrl = configFile.url.appendingPathExtension("back")
|
|
||||||
#expect(fileClient.fileExists(backupUrl) == false)
|
|
||||||
|
|
||||||
let config = Configuration()
|
|
||||||
try await client.write(config, to: configFile)
|
|
||||||
|
|
||||||
#expect(fileClient.fileExists(backupUrl))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateFindEnvironments(file: File) -> [[String: String]] {
|
func generateFindEnvironments(file: File) -> [[String: String]] {
|
||||||
@@ -170,11 +150,7 @@ func withGeneratedConfigFile(
|
|||||||
) async rethrows {
|
) async rethrows {
|
||||||
try await withTemporaryDirectory { tempDir in
|
try await withTemporaryDirectory { tempDir in
|
||||||
let file = File(tempDir.appending(path: fileName))!
|
let file = File(tempDir.appending(path: fileName))!
|
||||||
_ = try await client.generate(.init(
|
try await client.generate(at: file)
|
||||||
force: true,
|
|
||||||
json: fileName.hasSuffix("json"),
|
|
||||||
path: .file(file)
|
|
||||||
))
|
|
||||||
try await operation(file)
|
try await operation(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,11 +167,7 @@ func withGeneratedXDGConfigFile(
|
|||||||
withIntermediateDirectories: false
|
withIntermediateDirectories: false
|
||||||
)
|
)
|
||||||
let file = File(xdgDir.appending(path: fileName))!
|
let file = File(xdgDir.appending(path: fileName))!
|
||||||
_ = try await client.generate(.init(
|
try await client.generate(at: file)
|
||||||
force: true,
|
|
||||||
json: fileName.hasSuffix("json"),
|
|
||||||
path: .file(file)
|
|
||||||
))
|
|
||||||
try await operation(file, tempDir)
|
try await operation(file, tempDir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import FileClient
|
|
||||||
import Foundation
|
|
||||||
import Testing
|
|
||||||
import TestSupport
|
|
||||||
|
|
||||||
@Suite("FileClientTests")
|
|
||||||
struct FileClientTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
func createDirectory() async throws {
|
|
||||||
try await withTemporaryDirectory { url in
|
|
||||||
let fileClient = FileClient.liveValue
|
|
||||||
let tempDir = url.appending(path: "temp")
|
|
||||||
try await fileClient.createDirectory(tempDir)
|
|
||||||
let isDirectory = try await fileClient.isDirectory(tempDir)
|
|
||||||
#expect(isDirectory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(arguments: ["vault.yml", "vault.yaml"])
|
|
||||||
func findVaultFile(fileName: String) async throws {
|
|
||||||
try await withTemporaryDirectory { url in
|
|
||||||
let fileClient = FileClient.liveValue
|
|
||||||
|
|
||||||
let vaultFilePath = url.appending(path: fileName)
|
|
||||||
FileManager.default.createFile(atPath: vaultFilePath.cleanFilePath, contents: nil)
|
|
||||||
let output = try await fileClient.findVaultFile(url)!
|
|
||||||
|
|
||||||
#expect(output.cleanFilePath == vaultFilePath.cleanFilePath)
|
|
||||||
|
|
||||||
let nilWhenFileNotDirectory = try await fileClient.findVaultFile(vaultFilePath)
|
|
||||||
#expect(nilWhenFileNotDirectory == nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(arguments: ["vault.yml", "vault.yaml"])
|
|
||||||
func findVaultFileNestedInSubfolder(fileName: String) async throws {
|
|
||||||
try await withTemporaryDirectory { url in
|
|
||||||
|
|
||||||
let fileClient = FileClient.liveValue
|
|
||||||
let subDir = url.appending(path: "sub")
|
|
||||||
|
|
||||||
try await fileClient.createDirectory(subDir)
|
|
||||||
|
|
||||||
let vaultFilePath = subDir.appending(path: fileName)
|
|
||||||
FileManager.default.createFile(atPath: vaultFilePath.cleanFilePath, contents: nil)
|
|
||||||
let output = try await fileClient.findVaultFile(url)!
|
|
||||||
|
|
||||||
#expect(output.cleanFilePath == vaultFilePath.cleanFilePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
func findVaultFileReturnsNil() async throws {
|
|
||||||
try await withTemporaryDirectory { url in
|
|
||||||
|
|
||||||
let fileClient = FileClient.liveValue
|
|
||||||
let subDir = url.appending(path: "sub")
|
|
||||||
|
|
||||||
try await fileClient.createDirectory(subDir)
|
|
||||||
|
|
||||||
let output = try await fileClient.findVaultFile(url)
|
|
||||||
|
|
||||||
#expect(output == nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,333 +0,0 @@
|
|||||||
@_spi(Internal) import ConfigurationClient
|
|
||||||
@_spi(Internal) import PandocClient
|
|
||||||
import PlaybookClient
|
|
||||||
import Testing
|
|
||||||
import TestSupport
|
|
||||||
|
|
||||||
@Suite("PandocClientTests")
|
|
||||||
struct PandocClientTests: TestCase {
|
|
||||||
|
|
||||||
static let outputDirectory = "/output"
|
|
||||||
static let projectDirectory = "/project"
|
|
||||||
static let defaultFileName = "Report"
|
|
||||||
|
|
||||||
static let expectedIncludeInHeaders = [
|
|
||||||
"--include-in-header=/project/.build/head.tex",
|
|
||||||
"--include-in-header=/project/.build/footer.tex"
|
|
||||||
]
|
|
||||||
|
|
||||||
static let expectedFiles = [
|
|
||||||
"/project/.build/Report.md",
|
|
||||||
"/project/.build/Definitions.md"
|
|
||||||
]
|
|
||||||
|
|
||||||
static var sharedRunOptions: PandocClient.RunOptions {
|
|
||||||
.init(
|
|
||||||
buildDirectory: nil,
|
|
||||||
files: nil,
|
|
||||||
loggingOptions: loggingOptions,
|
|
||||||
includeInHeader: nil,
|
|
||||||
outputDirectory: outputDirectory,
|
|
||||||
projectDirectory: projectDirectory,
|
|
||||||
outputFileName: nil,
|
|
||||||
quiet: false,
|
|
||||||
shell: nil,
|
|
||||||
shouldBuild: true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
func generateLatex() async throws {
|
|
||||||
try await withCapturingCommandClient("generateLatex") {
|
|
||||||
$0.configurationClient = .mock()
|
|
||||||
$0.playbookClient.run.buildProject = { _ in }
|
|
||||||
$0.pandocClient = .liveValue
|
|
||||||
} run: {
|
|
||||||
@Dependency(\.pandocClient) var pandocClient
|
|
||||||
|
|
||||||
let output = try await pandocClient.run.generateLatex(Self.sharedRunOptions)
|
|
||||||
#expect(output == "\(Self.outputDirectory)/\(Self.defaultFileName).tex")
|
|
||||||
|
|
||||||
} assert: { output in
|
|
||||||
let expected = ["pandoc"]
|
|
||||||
+ Self.expectedIncludeInHeaders
|
|
||||||
+ ["--output=\(Self.outputDirectory)/\(Self.defaultFileName).tex"]
|
|
||||||
+ Self.expectedFiles
|
|
||||||
|
|
||||||
#expect(output.arguments == expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
func generateHtml() async throws {
|
|
||||||
try await withCapturingCommandClient("generateHtml") {
|
|
||||||
$0.configurationClient = .mock()
|
|
||||||
$0.playbookClient.run.buildProject = { _ in }
|
|
||||||
$0.pandocClient = .liveValue
|
|
||||||
} run: {
|
|
||||||
@Dependency(\.pandocClient) var pandocClient
|
|
||||||
|
|
||||||
let output = try await pandocClient.run.generateHtml(Self.sharedRunOptions)
|
|
||||||
#expect(output == "\(Self.outputDirectory)/\(Self.defaultFileName).html")
|
|
||||||
|
|
||||||
} assert: { output in
|
|
||||||
let expected = ["pandoc"]
|
|
||||||
+ Self.expectedIncludeInHeaders
|
|
||||||
+ ["--output=\(Self.outputDirectory)/\(Self.defaultFileName).html"]
|
|
||||||
+ Self.expectedFiles
|
|
||||||
|
|
||||||
#expect(output.arguments == expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(
|
|
||||||
arguments: [
|
|
||||||
nil,
|
|
||||||
"lualatex"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
func generatePdf(pdfEngine: String?) async throws {
|
|
||||||
try await withCapturingCommandClient("generatePdf") {
|
|
||||||
$0.configurationClient = .mock()
|
|
||||||
$0.playbookClient.run.buildProject = { _ in }
|
|
||||||
$0.pandocClient = .liveValue
|
|
||||||
} run: {
|
|
||||||
@Dependency(\.pandocClient) var pandocClient
|
|
||||||
|
|
||||||
let output = try await pandocClient.run.generatePdf(Self.sharedRunOptions, pdfEngine: pdfEngine)
|
|
||||||
#expect(output == "\(Self.outputDirectory)/\(Self.defaultFileName).pdf")
|
|
||||||
|
|
||||||
} assert: { output in
|
|
||||||
let expected = ["pandoc"]
|
|
||||||
+ Self.expectedIncludeInHeaders
|
|
||||||
+ ["--pdf-engine=\(pdfEngine ?? "xelatex")"]
|
|
||||||
+ ["--output=\(Self.outputDirectory)/\(Self.defaultFileName).pdf"]
|
|
||||||
+ Self.expectedFiles
|
|
||||||
|
|
||||||
#expect(output.arguments == expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(arguments: TestPdfEngine.testCases)
|
|
||||||
func parsePdfEngine(input: TestPdfEngine) {
|
|
||||||
#expect(input.engine == input.expectedEngine)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(arguments: TestParseFiles.testCases)
|
|
||||||
func parseFiles(input: TestParseFiles) {
|
|
||||||
#expect(input.parsedFiles == input.expectedFiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(arguments: TestParseIncludeInHeaderFiles.testCases)
|
|
||||||
func parseInclueInHeaderFiles(input: TestParseIncludeInHeaderFiles) {
|
|
||||||
#expect(input.parsedFiles == input.expectedHeaderFiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(arguments: TestParseOutputFileName.testCases)
|
|
||||||
func parseOutputFileName(input: TestParseOutputFileName) {
|
|
||||||
#expect(input.parsedFileName == input.expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(arguments: TestParseBuildDirectory.testCases)
|
|
||||||
func parseBuildDirectory(input: TestParseBuildDirectory) {
|
|
||||||
#expect(input.parsedBuildDirectory == input.expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TestPdfEngine: Sendable {
|
|
||||||
let fileType: PandocClient.FileType
|
|
||||||
let expectedEngine: String?
|
|
||||||
let configuration: Configuration
|
|
||||||
let defaults: Configuration.Generate
|
|
||||||
|
|
||||||
var engine: String? {
|
|
||||||
fileType.parsePdfEngine(configuration.generate, defaults)
|
|
||||||
}
|
|
||||||
|
|
||||||
static let testCases: [Self] = [
|
|
||||||
.init(fileType: .html, expectedEngine: nil, configuration: .init(), defaults: .default),
|
|
||||||
.init(fileType: .latex, expectedEngine: nil, configuration: .init(), defaults: .default),
|
|
||||||
.init(fileType: .pdf(engine: "lualatex"), expectedEngine: "lualatex", configuration: .init(), defaults: .default),
|
|
||||||
.init(fileType: .pdf(engine: nil), expectedEngine: "xelatex", configuration: .init(), defaults: .default),
|
|
||||||
.init(fileType: .pdf(engine: nil), expectedEngine: "xelatex", configuration: .init(), defaults: .init()),
|
|
||||||
.init(fileType: .pdf(engine: nil), expectedEngine: "xelatex", configuration: .init(generate: .default), defaults: .init())
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TestParseFiles: Sendable {
|
|
||||||
|
|
||||||
let expectedFiles: [String]
|
|
||||||
let configuration: Configuration
|
|
||||||
let defaults: Configuration.Generate
|
|
||||||
let runOptions: PandocClient.RunOptions?
|
|
||||||
|
|
||||||
init(
|
|
||||||
expectedFiles: [String],
|
|
||||||
configuration: Configuration = .init(),
|
|
||||||
defaults: Configuration.Generate = .default,
|
|
||||||
runOptions: PandocClient.RunOptions? = nil
|
|
||||||
) {
|
|
||||||
self.expectedFiles = expectedFiles
|
|
||||||
self.configuration = configuration
|
|
||||||
self.defaults = defaults
|
|
||||||
self.runOptions = runOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
var parsedFiles: [String] {
|
|
||||||
let runOptions = self.runOptions ?? PandocClient.RunOptions(
|
|
||||||
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug),
|
|
||||||
projectDirectory: nil,
|
|
||||||
quiet: true,
|
|
||||||
shouldBuild: false
|
|
||||||
)
|
|
||||||
|
|
||||||
return runOptions.parseFiles(configuration.generate, defaults)
|
|
||||||
}
|
|
||||||
|
|
||||||
static let testCases: [Self] = [
|
|
||||||
.init(expectedFiles: ["Report.md", "Definitions.md"]),
|
|
||||||
.init(expectedFiles: ["Report.md", "Definitions.md"], configuration: .init(generate: .default), defaults: .init()),
|
|
||||||
.init(expectedFiles: [], defaults: .init()),
|
|
||||||
.init(
|
|
||||||
expectedFiles: ["custom.md"],
|
|
||||||
configuration: .init(),
|
|
||||||
defaults: .init(),
|
|
||||||
runOptions: .init(
|
|
||||||
files: ["custom.md"],
|
|
||||||
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug),
|
|
||||||
projectDirectory: nil,
|
|
||||||
quiet: true,
|
|
||||||
shouldBuild: false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TestParseIncludeInHeaderFiles: Sendable {
|
|
||||||
|
|
||||||
let expectedHeaderFiles: [String]
|
|
||||||
let configuration: Configuration
|
|
||||||
let defaults: Configuration.Generate
|
|
||||||
let runOptions: PandocClient.RunOptions?
|
|
||||||
|
|
||||||
init(
|
|
||||||
expectedHeaderFiles: [String],
|
|
||||||
configuration: Configuration = .init(),
|
|
||||||
defaults: Configuration.Generate = .default,
|
|
||||||
runOptions: PandocClient.RunOptions? = nil
|
|
||||||
) {
|
|
||||||
self.expectedHeaderFiles = expectedHeaderFiles
|
|
||||||
self.configuration = configuration
|
|
||||||
self.defaults = defaults
|
|
||||||
self.runOptions = runOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
var parsedFiles: [String] {
|
|
||||||
let runOptions = self.runOptions ?? PandocClient.RunOptions(
|
|
||||||
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug)
|
|
||||||
)
|
|
||||||
|
|
||||||
return runOptions.parseIncludeInHeader(configuration.generate, defaults)
|
|
||||||
}
|
|
||||||
|
|
||||||
static let testCases: [Self] = [
|
|
||||||
.init(expectedHeaderFiles: ["head.tex", "footer.tex"]),
|
|
||||||
.init(expectedHeaderFiles: ["head.tex", "footer.tex"], configuration: .init(generate: .default), defaults: .init()),
|
|
||||||
.init(expectedHeaderFiles: [], defaults: .init()),
|
|
||||||
.init(
|
|
||||||
expectedHeaderFiles: ["custom.tex"],
|
|
||||||
configuration: .init(),
|
|
||||||
defaults: .init(),
|
|
||||||
runOptions: .init(
|
|
||||||
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug),
|
|
||||||
includeInHeader: ["custom.tex"]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TestParseOutputFileName: Sendable {
|
|
||||||
|
|
||||||
let expected: String
|
|
||||||
let configuration: Configuration
|
|
||||||
let defaults: Configuration.Generate
|
|
||||||
let runOptions: PandocClient.RunOptions?
|
|
||||||
|
|
||||||
init(
|
|
||||||
expected: String,
|
|
||||||
configuration: Configuration = .init(),
|
|
||||||
defaults: Configuration.Generate = .default,
|
|
||||||
runOptions: PandocClient.RunOptions? = nil
|
|
||||||
) {
|
|
||||||
self.expected = expected
|
|
||||||
self.configuration = configuration
|
|
||||||
self.defaults = defaults
|
|
||||||
self.runOptions = runOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
var parsedFileName: String {
|
|
||||||
let runOptions = self.runOptions ?? PandocClient.RunOptions(
|
|
||||||
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug)
|
|
||||||
)
|
|
||||||
|
|
||||||
return runOptions.parseOutputFileName(configuration.generate, defaults)
|
|
||||||
}
|
|
||||||
|
|
||||||
static let testCases: [Self] = [
|
|
||||||
.init(expected: "Report"),
|
|
||||||
.init(expected: "Report", configuration: .init(generate: .default), defaults: .init()),
|
|
||||||
.init(expected: "Report", defaults: .init()),
|
|
||||||
.init(
|
|
||||||
expected: "custom",
|
|
||||||
configuration: .init(),
|
|
||||||
defaults: .init(),
|
|
||||||
runOptions: .init(
|
|
||||||
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug),
|
|
||||||
outputFileName: "custom"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TestParseBuildDirectory: Sendable {
|
|
||||||
|
|
||||||
let expected: String
|
|
||||||
let configuration: Configuration
|
|
||||||
let defaults: Configuration.Generate
|
|
||||||
let runOptions: PandocClient.RunOptions?
|
|
||||||
|
|
||||||
init(
|
|
||||||
expected: String = ".build",
|
|
||||||
configuration: Configuration = .init(),
|
|
||||||
defaults: Configuration.Generate = .default,
|
|
||||||
runOptions: PandocClient.RunOptions? = nil
|
|
||||||
) {
|
|
||||||
self.expected = expected
|
|
||||||
self.configuration = configuration
|
|
||||||
self.defaults = defaults
|
|
||||||
self.runOptions = runOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
var parsedBuildDirectory: String {
|
|
||||||
let runOptions = self.runOptions ?? PandocClient.RunOptions(
|
|
||||||
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug)
|
|
||||||
)
|
|
||||||
|
|
||||||
return runOptions.parseBuildDirectory(configuration.generate, defaults)
|
|
||||||
}
|
|
||||||
|
|
||||||
static let testCases: [Self] = [
|
|
||||||
.init(),
|
|
||||||
.init(configuration: .init(generate: .default), defaults: .init()),
|
|
||||||
.init(defaults: .init()),
|
|
||||||
.init(
|
|
||||||
expected: "custom",
|
|
||||||
configuration: .init(),
|
|
||||||
defaults: .init(),
|
|
||||||
runOptions: .init(
|
|
||||||
buildDirectory: "custom",
|
|
||||||
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -22,14 +22,12 @@ struct PlaybookClientTests: TestCase {
|
|||||||
|
|
||||||
@Test(.tags(.repository))
|
@Test(.tags(.repository))
|
||||||
func repositoryInstallation() async throws {
|
func repositoryInstallation() async throws {
|
||||||
try await withTestLogger(key: "repositoryInstallation") {
|
try await withDependencies {
|
||||||
$0.fileClient = .liveValue
|
$0.fileClient = .liveValue
|
||||||
$0.asyncShellClient = .liveValue
|
$0.asyncShellClient = .liveValue
|
||||||
$0.commandClient = .liveValue
|
$0.commandClient = .liveValue
|
||||||
} operation: {
|
} operation: {
|
||||||
try await withTemporaryDirectory { tempDirectory in
|
try await withTemporaryDirectory { tempDirectory in
|
||||||
@Dependency(\.fileClient) var fileClient
|
|
||||||
@Dependency(\.logger) var logger
|
|
||||||
let pathUrl = tempDirectory.appending(path: "playbook")
|
let pathUrl = tempDirectory.appending(path: "playbook")
|
||||||
let playbookClient = PlaybookClient.liveValue
|
let playbookClient = PlaybookClient.liveValue
|
||||||
|
|
||||||
@@ -37,8 +35,7 @@ struct PlaybookClientTests: TestCase {
|
|||||||
|
|
||||||
try? FileManager.default.removeItem(at: pathUrl)
|
try? FileManager.default.removeItem(at: pathUrl)
|
||||||
try await playbookClient.repository.install(configuration)
|
try await playbookClient.repository.install(configuration)
|
||||||
logger.debug("Done cloning playbook")
|
let exists = FileManager.default.fileExists(atPath: pathUrl.cleanFilePath)
|
||||||
let exists = try await fileClient.isDirectory(pathUrl)
|
|
||||||
#expect(exists)
|
#expect(exists)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,6 +61,8 @@ struct PlaybookClientTests: TestCase {
|
|||||||
try await withMockConfiguration(captured, key: "runBuildProject") {
|
try await withMockConfiguration(captured, key: "runBuildProject") {
|
||||||
@Dependency(\.playbookClient) var playbookClient
|
@Dependency(\.playbookClient) var playbookClient
|
||||||
|
|
||||||
|
let configuration = Configuration.mock
|
||||||
|
|
||||||
try await playbookClient.run.buildProject(.init(projectDirectory: "/foo", shared: Self.sharedRunOptions))
|
try await playbookClient.run.buildProject(.init(projectDirectory: "/foo", shared: Self.sharedRunOptions))
|
||||||
|
|
||||||
let arguments = await captured.options!.arguments
|
let arguments = await captured.options!.arguments
|
||||||
@@ -93,6 +92,8 @@ struct PlaybookClientTests: TestCase {
|
|||||||
@Dependency(\.logger) var logger
|
@Dependency(\.logger) var logger
|
||||||
@Dependency(\.playbookClient) var playbookClient
|
@Dependency(\.playbookClient) var playbookClient
|
||||||
|
|
||||||
|
let configuration = Configuration.mock
|
||||||
|
|
||||||
try await playbookClient.run.createProject(
|
try await playbookClient.run.createProject(
|
||||||
.init(
|
.init(
|
||||||
projectDirectory: "/project",
|
projectDirectory: "/project",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ struct VaultClientTests: TestCase {
|
|||||||
func decrypt(input: TestOptions) async throws {
|
func decrypt(input: TestOptions) async throws {
|
||||||
try await withCapturingCommandClient("decrypt") {
|
try await withCapturingCommandClient("decrypt") {
|
||||||
$0.configurationClient = .mock(input.configuration)
|
$0.configurationClient = .mock(input.configuration)
|
||||||
$0.fileClient.findVaultFile = { _ in URL(filePath: "/vault.yml") }
|
$0.fileClient.findVaultFileInCurrentDirectory = { URL(filePath: "/vault.yml") }
|
||||||
$0.vaultClient = .liveValue
|
$0.vaultClient = .liveValue
|
||||||
} run: {
|
} run: {
|
||||||
@Dependency(\.vaultClient) var vaultClient
|
@Dependency(\.vaultClient) var vaultClient
|
||||||
@@ -45,7 +45,7 @@ struct VaultClientTests: TestCase {
|
|||||||
func encrypt(input: TestOptions) async throws {
|
func encrypt(input: TestOptions) async throws {
|
||||||
try await withCapturingCommandClient("decrypt") {
|
try await withCapturingCommandClient("decrypt") {
|
||||||
$0.configurationClient = .mock(input.configuration)
|
$0.configurationClient = .mock(input.configuration)
|
||||||
$0.fileClient.findVaultFile = { _ in URL(filePath: "/vault.yml") }
|
$0.fileClient.findVaultFileInCurrentDirectory = { URL(filePath: "/vault.yml") }
|
||||||
$0.vaultClient = .liveValue
|
$0.vaultClient = .liveValue
|
||||||
} run: {
|
} run: {
|
||||||
@Dependency(\.vaultClient) var vaultClient
|
@Dependency(\.vaultClient) var vaultClient
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
# Used this to build the release version of the image.
|
|
||||||
# Build the executable
|
|
||||||
ARG SWIFT_IMAGE_VERSION="6.0.3"
|
|
||||||
|
|
||||||
FROM swift:${SWIFT_IMAGE_VERSION} AS build
|
|
||||||
WORKDIR /build
|
|
||||||
COPY ./Package.* ./
|
|
||||||
RUN swift package resolve
|
|
||||||
COPY . .
|
|
||||||
RUN swift build -c release -Xswiftc -g
|
|
||||||
|
|
||||||
# Run image
|
|
||||||
FROM swift:${SWIFT_IMAGE_VERSION}-slim
|
|
||||||
|
|
||||||
RUN export DEBIAN_FRONTEND=nointeractive DEBCONF_NOINTERACTIVE_SEEN=true && apt-get -q update && \
|
|
||||||
apt-get -q install -y \
|
|
||||||
ansible \
|
|
||||||
pandoc \
|
|
||||||
texlive \
|
|
||||||
&& rm -r /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY --from=build /build/.build/release/hpa /usr/local/bin
|
|
||||||
CMD ["/bin/bash", "-xc", "/usr/local/bin/hpa"]
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# Used to build a test image.
|
|
||||||
ARG SWIFT_IMAGE_VERSION="6.0.3"
|
|
||||||
FROM swift:${SWIFT_IMAGE_VERSION}
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY ./Package.* ./
|
|
||||||
RUN swift package resolve
|
|
||||||
COPY . .
|
|
||||||
RUN swift build
|
|
||||||
CMD ["/bin/bash", "-xc", "swift", "test"]
|
|
||||||
20
justfile
20
justfile
@@ -1,30 +1,14 @@
|
|||||||
docker_image_name := "swift-hpa"
|
|
||||||
install_path := "~/.local/share/bin/hpa"
|
|
||||||
completion_path := "~/.local/share/zsh/completions/_hpa"
|
|
||||||
|
|
||||||
build mode="debug":
|
build mode="debug":
|
||||||
swift build -c {{mode}}
|
swift build -c {{mode}}
|
||||||
|
|
||||||
alias b := build
|
alias b := build
|
||||||
|
|
||||||
build-docker file="Dockerfile" tag="latest":
|
|
||||||
@docker build \
|
|
||||||
--file docker/{{file}} \
|
|
||||||
--tag {{docker_image_name}}:{{tag}} .
|
|
||||||
|
|
||||||
build-docker-test: (build-docker "Dockerfile.test" "test")
|
|
||||||
|
|
||||||
test *ARGS:
|
test *ARGS:
|
||||||
swift test {{ARGS}}
|
swift test {{ARGS}}
|
||||||
|
|
||||||
alias t := test
|
alias t := test
|
||||||
|
|
||||||
test-docker *ARGS: (build-docker-test)
|
|
||||||
@docker run --rm \
|
|
||||||
--network host \
|
|
||||||
{{docker_image_name}}:test \
|
|
||||||
swift test {{ARGS}}
|
|
||||||
|
|
||||||
run *ARGS:
|
run *ARGS:
|
||||||
swift run hpa {{ARGS}}
|
swift run hpa {{ARGS}}
|
||||||
|
|
||||||
@@ -39,7 +23,3 @@ update-version:
|
|||||||
--allow-writing-to-package-directory \
|
--allow-writing-to-package-directory \
|
||||||
update-version \
|
update-version \
|
||||||
hpa
|
hpa
|
||||||
|
|
||||||
install: (build "release")
|
|
||||||
@cp .build/release/hpa {{install_path}}
|
|
||||||
@{{install_path}} --generate-completion-script zsh > {{completion_path}}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user