feat: Working create command.
This commit is contained in:
@@ -12,7 +12,6 @@ let package = Package(
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
|
||||
.package(url: "https://github.com/apple/swift-log", from: "1.6.0"),
|
||||
.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")
|
||||
],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import Foundation
|
||||
import ShellClient
|
||||
|
||||
public extension DependencyValues {
|
||||
var cliClient: CliClient {
|
||||
@@ -14,6 +15,23 @@ public struct CliClient: Sendable {
|
||||
public var decoder: @Sendable () -> JSONDecoder = { .init() }
|
||||
public var encoder: @Sendable () -> JSONEncoder = { .init() }
|
||||
public var loadConfiguration: @Sendable () throws -> Configuration
|
||||
public var runCommand: @Sendable ([String], Bool, ShellCommand.Shell) async throws -> Void
|
||||
|
||||
public func runCommand(
|
||||
quiet: Bool,
|
||||
shell: ShellCommand.Shell,
|
||||
_ args: [String]
|
||||
) async throws {
|
||||
try await runCommand(args, quiet, shell)
|
||||
}
|
||||
|
||||
public func runCommand(
|
||||
quiet: Bool,
|
||||
shell: ShellCommand.Shell,
|
||||
_ args: String...
|
||||
) async throws {
|
||||
try await runCommand(args, quiet, shell)
|
||||
}
|
||||
}
|
||||
|
||||
extension CliClient: DependencyKey {
|
||||
@@ -41,6 +59,22 @@ extension CliClient: DependencyKey {
|
||||
}
|
||||
|
||||
return try .fromEnv(env, encoder: encoder, decoder: decoder)
|
||||
} runCommand: { args, quiet, shell in
|
||||
@Dependency(\.asyncShellClient) var shellClient
|
||||
if !quiet {
|
||||
try await shellClient.foreground(.init(
|
||||
shell: shell,
|
||||
environment: ProcessInfo.processInfo.environment,
|
||||
in: nil,
|
||||
args
|
||||
))
|
||||
}
|
||||
try await shellClient.background(.init(
|
||||
shell: shell,
|
||||
environment: ProcessInfo.processInfo.environment,
|
||||
in: nil,
|
||||
args
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,13 +66,13 @@ private struct LiveFileClient: @unchecked Sendable {
|
||||
@Dependency(\.logger) var logger
|
||||
logger.trace("Begin load file for: \(path(for: url))")
|
||||
|
||||
// if url.absoluteString.split(separator: ".json").count > 0 {
|
||||
// // Handle json file.
|
||||
// let data = try Data(contentsOf: url)
|
||||
// let dict = (try? decoder.decode([String: String].self, from: data)) ?? [:]
|
||||
// env.merge(dict, uniquingKeysWith: { $1 })
|
||||
// return
|
||||
// }
|
||||
if url.absoluteString.contains(".json") {
|
||||
// Handle json file.
|
||||
let data = try Data(contentsOf: url)
|
||||
let dict = (try? decoder.decode([String: String].self, from: data)) ?? [:]
|
||||
env.merge(dict, uniquingKeysWith: { $1 })
|
||||
return
|
||||
}
|
||||
|
||||
let string = try String(contentsOfFile: path(for: url), encoding: .utf8)
|
||||
|
||||
@@ -82,7 +82,9 @@ private struct LiveFileClient: @unchecked Sendable {
|
||||
for line in lines {
|
||||
logger.trace("Line: \(line)")
|
||||
let strippedLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let splitLine = strippedLine.split(separator: "=")
|
||||
let splitLine = strippedLine.split(separator: "=").map {
|
||||
$0.replacingOccurrences(of: "\"", with: "")
|
||||
}
|
||||
logger.trace("Split Line: \(splitLine)")
|
||||
guard splitLine.count >= 2 else { continue }
|
||||
|
||||
|
||||
@@ -8,13 +8,14 @@ struct BuildCommand: AsyncParsableCommand {
|
||||
|
||||
static let configuration = CommandConfiguration.playbookCommandConfiguration(
|
||||
commandName: commandName,
|
||||
abstract: "Build a home performance assesment project."
|
||||
abstract: "Build a home performance assesment project.",
|
||||
examples: (label: "Build Project", example: "\(commandName) /path/to/project")
|
||||
)
|
||||
|
||||
@OptionGroup var globals: GlobalOptions
|
||||
|
||||
@Argument(
|
||||
help: "The project directory.",
|
||||
help: "Path to the project directory.",
|
||||
completion: .directory
|
||||
)
|
||||
var projectDir: String
|
||||
@@ -25,11 +26,12 @@ struct BuildCommand: AsyncParsableCommand {
|
||||
var extraArgs: [String] = []
|
||||
|
||||
mutating func run() async throws {
|
||||
let args = [
|
||||
try await runPlaybook(
|
||||
commandName: Self.commandName,
|
||||
globals: globals,
|
||||
extraArgs: extraArgs,
|
||||
"--tags", "build-project",
|
||||
"--extra-vars", "project_dir=\(projectDir)"
|
||||
] + extraArgs
|
||||
|
||||
try await runPlaybook(commandName: Self.commandName, globals: globals, args: args)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,170 @@
|
||||
import ArgumentParser
|
||||
import CliClient
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import Logging
|
||||
|
||||
struct CreateProjectCommand: AsyncParsableCommand {
|
||||
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "create-project",
|
||||
abstract: "Create a home performance assesment project."
|
||||
static let commandName = "create"
|
||||
|
||||
static let configuration = CommandConfiguration.playbookCommandConfiguration(
|
||||
commandName: commandName,
|
||||
abstract: "Create a home performance assesment project.",
|
||||
examples: (
|
||||
label: "Create Assesment",
|
||||
example: "\(commandName) /new/assement/path"
|
||||
)
|
||||
)
|
||||
|
||||
@OptionGroup var globals: GlobalOptions
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The template repository to use."
|
||||
)
|
||||
var repo: String?
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The repo branch or version to use."
|
||||
)
|
||||
var branch: String?
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "Path to local template directory to use.",
|
||||
completion: .directory
|
||||
)
|
||||
var templateDir: String?
|
||||
|
||||
@Flag(
|
||||
name: .shortAndLong,
|
||||
help: "Force using a local template directory."
|
||||
)
|
||||
var localTemplateDir = false
|
||||
|
||||
@Argument(
|
||||
help: "Path to the project directory.",
|
||||
completion: .directory
|
||||
)
|
||||
var projectDir: String
|
||||
|
||||
@Argument(
|
||||
help: "Extra arguments passed to the playbook."
|
||||
)
|
||||
var extraArgs: [String] = []
|
||||
|
||||
mutating func run() async throws {
|
||||
fatalError()
|
||||
try await _run()
|
||||
}
|
||||
|
||||
private func _run() async throws {
|
||||
try await withSetupLogger(commandName: Self.commandName, globals: globals) {
|
||||
@Dependency(\.cliClient) var cliClient
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
let encoder = cliClient.encoder()
|
||||
|
||||
let configuration = try cliClient.loadConfiguration()
|
||||
|
||||
logger.debug("Configuration: \(configuration)")
|
||||
|
||||
let jsonData = try parseOptions(
|
||||
command: self,
|
||||
configuration: configuration,
|
||||
logger: logger,
|
||||
encoder: encoder
|
||||
)
|
||||
|
||||
guard let jsonString = String(data: jsonData, encoding: .utf8) else {
|
||||
throw CreateError.encodingError
|
||||
}
|
||||
|
||||
logger.debug("JSON string: \(jsonString)")
|
||||
|
||||
try await runPlaybook(
|
||||
commandName: Self.commandName,
|
||||
globals: self.globals,
|
||||
configuration: configuration,
|
||||
extraArgs: extraArgs,
|
||||
"--tags", "setup-project",
|
||||
"--extra-vars", "project_dir=\(self.projectDir)",
|
||||
"--extra-vars", "'\(jsonString)'"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func parseOptions(
|
||||
command: CreateProjectCommand,
|
||||
configuration: Configuration,
|
||||
logger: Logger,
|
||||
encoder: JSONEncoder
|
||||
) throws -> Data {
|
||||
let templateDir = command.templateDir ?? configuration.templateDir
|
||||
let templateRepo = command.repo ?? configuration.templateRepo
|
||||
let version = (command.branch ?? configuration.templateRepoVersion) ?? "main"
|
||||
|
||||
logger.debug("""
|
||||
(\(command.localTemplateDir), \(String(describing: templateDir)), \(String(describing: templateRepo)))
|
||||
""")
|
||||
|
||||
switch (command.localTemplateDir, templateDir, templateRepo) {
|
||||
case (true, .none, _):
|
||||
// User supplied they wanted to use a local template directory, but we could not find
|
||||
// the path set from command line or in configuration.
|
||||
throw CreateError.templateDirNotFound
|
||||
case let (false, _, .some(repo)):
|
||||
// User did not supply they wanted to use a local template directory, and we found a repo url that was
|
||||
// either set by the command line or found in the configuration.
|
||||
logger.debug("Using repo.")
|
||||
return try encoder.encode(TemplateRepo(repo: repo, version: version))
|
||||
case let (true, .some(templateDir), _):
|
||||
// User supplied they wanted to use a local template directory, and we found the template directory
|
||||
// either set by the command line or in the configuration.
|
||||
logger.debug("Using template directory.")
|
||||
return try encoder.encode(TemplateDirJson(path: templateDir))
|
||||
case let (false, .some(templateDir), _):
|
||||
// User supplied they did not wanted to use a local template directory, and we found the template directory
|
||||
// either set by the command line or in the configuration, and no repo was found / handled previously.
|
||||
logger.debug("Using template directory.")
|
||||
return try encoder.encode(TemplateDirJson(path: templateDir))
|
||||
case (_, .none, .none):
|
||||
// We could not find a repo or template directory.
|
||||
throw CreateError.templateDirOrRepoNotSpecified
|
||||
}
|
||||
}
|
||||
|
||||
private struct TemplateDirJson: Encodable {
|
||||
|
||||
let template: Template
|
||||
|
||||
init(path: String) {
|
||||
self.template = .init(path: path)
|
||||
}
|
||||
|
||||
struct Template: Encodable {
|
||||
let path: String
|
||||
}
|
||||
}
|
||||
|
||||
private struct TemplateRepo: Encodable {
|
||||
|
||||
let template: Template
|
||||
|
||||
init(repo: String, version: String?) {
|
||||
self.template = .init(repo: repo, version: version ?? "main")
|
||||
}
|
||||
|
||||
struct Template: Encodable {
|
||||
let repo: String
|
||||
let version: String
|
||||
}
|
||||
}
|
||||
|
||||
enum CreateError: Error {
|
||||
case encodingError
|
||||
case templateDirNotFound
|
||||
case templateDirOrRepoNotSpecified
|
||||
}
|
||||
|
||||
@@ -15,8 +15,28 @@ struct GlobalOptions: ParsableArguments {
|
||||
var inventoryPath: String?
|
||||
|
||||
@Flag(
|
||||
name: .long,
|
||||
help: "Increase logging level."
|
||||
name: .shortAndLong,
|
||||
help: "Increase logging level (can be passed multiple times)."
|
||||
)
|
||||
var verbose: Int
|
||||
|
||||
@Flag(
|
||||
name: .shortAndLong,
|
||||
help: "Supress logging."
|
||||
)
|
||||
var quiet = false
|
||||
|
||||
@Flag(
|
||||
name: .long,
|
||||
help: "Supress only playbook logging."
|
||||
)
|
||||
var quietOnlyPlaybook = false
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "Optional shell to use when calling shell commands.",
|
||||
completion: .file()
|
||||
)
|
||||
var shell: String?
|
||||
|
||||
}
|
||||
|
||||
@@ -7,30 +7,47 @@ import Rainbow
|
||||
import ShellClient
|
||||
|
||||
extension CommandConfiguration {
|
||||
static func playbookCommandConfiguration(commandName: String, abstract: String) -> Self {
|
||||
Self(
|
||||
static func playbookCommandConfiguration(
|
||||
commandName: String,
|
||||
abstract: String,
|
||||
examples: (label: String, example: String)...
|
||||
) -> Self {
|
||||
guard examples.count > 0 else {
|
||||
fatalError("Did not supply any examples.")
|
||||
}
|
||||
return Self(
|
||||
commandName: commandName,
|
||||
abstract: "\(abstract.blue)",
|
||||
discussion: """
|
||||
\("IMPORTANT NOTE:".red) Any extra arguments to pass to the playbook invocation have to
|
||||
be at the end with `--` before any arguments otherwise there will
|
||||
be an "Unkown option" error.
|
||||
\("NOTE:".yellow) Most options are not required if you have a configuration file setup.
|
||||
|
||||
\("Example of passing extra args to the playbook:".yellow)
|
||||
\("Examples:".yellow)
|
||||
|
||||
$ hpa \(commandName) /my/project -- --vault-id "myId@$SCRIPTS/vault-gopass-client"
|
||||
\(examples.map { "\($0.label.green.italic)\n $ hpa \($0.example)" }.joined(separator: "\n"))
|
||||
|
||||
\("See Also:".yellow)
|
||||
\("Passing extra args to the playbook.".green.italic)
|
||||
$ hpa \(examples[0].example) -- --vault-id "myId@$SCRIPTS/vault-gopass-client"
|
||||
|
||||
You can run the following command to see the options that can be passed to the playbook
|
||||
invocation.
|
||||
\("See Also:".yellow) \("Ansible playbook options.".italic)
|
||||
|
||||
$ ansible-playbook --help
|
||||
|
||||
\("IMPORTANT NOTE:".red) Any extra arguments to pass to the playbook invocation have to
|
||||
be at the end with `--` before any arguments otherwise there will
|
||||
be an "Unkown option" error. See examples above.
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension GlobalOptions {
|
||||
|
||||
var shellOrDefault: ShellCommand.Shell {
|
||||
guard let shell else { return .zsh(useDashC: true) }
|
||||
return .custom(path: shell, useDashC: true)
|
||||
}
|
||||
}
|
||||
|
||||
func ensureString(
|
||||
globals: GlobalOptions,
|
||||
configuration: Configuration,
|
||||
@@ -41,37 +58,78 @@ func ensureString(
|
||||
return global
|
||||
}
|
||||
guard let configuration = configuration[keyPath: configurationKeyPath] else {
|
||||
throw PlaybookNotFound()
|
||||
throw RunPlaybookError.playbookNotFound
|
||||
}
|
||||
return configuration
|
||||
}
|
||||
|
||||
func withSetupLogger(
|
||||
commandName: String,
|
||||
globals: GlobalOptions,
|
||||
dependencies setupDependencies: (inout DependencyValues) -> Void = { _ in },
|
||||
operation: @escaping () async throws -> Void
|
||||
) async rethrows {
|
||||
try await withDependencies {
|
||||
$0.logger = .init(label: "\("hpa".yellow)")
|
||||
if globals.quietOnlyPlaybook || !globals.quiet {
|
||||
switch globals.verbose {
|
||||
case 0:
|
||||
$0.logger.logLevel = .info
|
||||
case 1:
|
||||
$0.logger.logLevel = .debug
|
||||
case 2...:
|
||||
$0.logger.logLevel = .trace
|
||||
default:
|
||||
$0.logger.logLevel = .info
|
||||
}
|
||||
}
|
||||
$0.logger[metadataKey: "command"] = "\(commandName.blue)"
|
||||
} operation: {
|
||||
try await operation()
|
||||
}
|
||||
}
|
||||
|
||||
func runPlaybook(
|
||||
commandName: String,
|
||||
globals: GlobalOptions,
|
||||
args: [String]
|
||||
configuration: Configuration? = nil,
|
||||
extraArgs: [String],
|
||||
_ args: String...
|
||||
) async throws {
|
||||
try await withDependencies {
|
||||
$0.logger = .init(label: "\("hpa".yellow)")
|
||||
switch globals.verbose {
|
||||
case 0:
|
||||
$0.logger.logLevel = .info
|
||||
case 1:
|
||||
$0.logger.logLevel = .debug
|
||||
case 2:
|
||||
$0.logger.logLevel = .trace
|
||||
default:
|
||||
$0.logger.logLevel = .info
|
||||
try await runPlaybook(
|
||||
commandName: commandName,
|
||||
globals: globals,
|
||||
configuration: configuration,
|
||||
extraArgs: extraArgs,
|
||||
args
|
||||
)
|
||||
}
|
||||
|
||||
private extension CliClient {
|
||||
func ensuredConfiguration(_ configuration: Configuration?) throws -> Configuration {
|
||||
guard let configuration else {
|
||||
return try loadConfiguration()
|
||||
}
|
||||
$0.logger[metadataKey: "command"] = "\(commandName.blue)"
|
||||
} operation: {
|
||||
return configuration
|
||||
}
|
||||
}
|
||||
|
||||
func runPlaybook(
|
||||
commandName: String,
|
||||
globals: GlobalOptions,
|
||||
configuration: Configuration? = nil,
|
||||
extraArgs: [String],
|
||||
_ args: [String]
|
||||
) async throws {
|
||||
try await withSetupLogger(commandName: commandName, globals: globals) {
|
||||
@Dependency(\.cliClient) var cliClient
|
||||
@Dependency(\.logger) var logger
|
||||
@Dependency(\.asyncShellClient) var shellClient
|
||||
|
||||
logger.debug("Begin run playbook: \(globals)")
|
||||
let configuration = try cliClient.loadConfiguration()
|
||||
logger.debug("Loaded configuration: \(configuration)")
|
||||
|
||||
let configuration = try cliClient.ensuredConfiguration(configuration)
|
||||
|
||||
logger.debug("Configuration: \(configuration)")
|
||||
|
||||
let playbookDir = try ensureString(
|
||||
globals: globals,
|
||||
@@ -91,19 +149,21 @@ func runPlaybook(
|
||||
var playbookArgs = [
|
||||
"ansible-playbook", playbook,
|
||||
"--inventory", inventory
|
||||
] + args
|
||||
]
|
||||
|
||||
if let defaultArgs = configuration.defaultPlaybookArgs {
|
||||
playbookArgs.append(defaultArgs)
|
||||
}
|
||||
|
||||
try await shellClient.foreground(.init(
|
||||
shell: .zsh(useDashC: true),
|
||||
environment: ProcessInfo.processInfo.environment,
|
||||
in: nil,
|
||||
playbookArgs
|
||||
))
|
||||
try await cliClient.runCommand(
|
||||
quiet: globals.quietOnlyPlaybook ? true : globals.quiet,
|
||||
shell: globals.shellOrDefault,
|
||||
playbookArgs + args + extraArgs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct PlaybookNotFound: Error {}
|
||||
enum RunPlaybookError: Error {
|
||||
case playbookNotFound
|
||||
case configurationError
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ func testFindConfigPaths() throws {
|
||||
|
||||
@Test
|
||||
func loadConfiguration() throws {
|
||||
try withTestLogger(key: "loadConfiguration", logLevel: .trace) {
|
||||
try withTestLogger(key: "loadConfiguration", logLevel: .debug) {
|
||||
$0.cliClient = .liveValue
|
||||
$0.fileClient = .liveValue
|
||||
} operation: {
|
||||
@@ -37,7 +37,7 @@ func withTestLogger(
|
||||
) rethrows {
|
||||
try withDependencies {
|
||||
$0.logger = .init(label: label)
|
||||
$0.logger[metadataKey: "instance"] = "\(key)"
|
||||
$0.logger[metadataKey: "test"] = "\(key)"
|
||||
$0.logger.logLevel = logLevel
|
||||
} operation: {
|
||||
try operation()
|
||||
@@ -53,7 +53,7 @@ func withTestLogger(
|
||||
) rethrows {
|
||||
try withDependencies {
|
||||
$0.logger = .init(label: label)
|
||||
$0.logger[metadataKey: "instance"] = "\(key)"
|
||||
$0.logger[metadataKey: "test"] = "\(key)"
|
||||
$0.logger.logLevel = logLevel
|
||||
setupDependencies(&$0)
|
||||
} operation: {
|
||||
|
||||
Reference in New Issue
Block a user