diff --git a/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE/issue_template.md similarity index 100% rename from ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE/issue_template.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fe1b7a..46bf3bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [2018.05.18] + +### Updated +- Rewrote whole CLI with Console 3, Manifest, and the vapor-community/PackageCatalogAPI. + ## [1.10.0] 2018-02-18 ### Added diff --git a/Package.resolved b/Package.resolved index 6fc2eaa..5dfdf09 100644 --- a/Package.resolved +++ b/Package.resolved @@ -2,57 +2,165 @@ "object": { "pins": [ { - "package": "Bits", - "repositoryURL": "https://github.com/vapor/bits.git", + "package": "Console", + "repositoryURL": "https://github.com/vapor/console.git", + "state": { + "branch": null, + "revision": "5b9796d39f201b3dd06800437abd9d774a455e57", + "version": "3.0.2" + } + }, + { + "package": "Core", + "repositoryURL": "https://github.com/vapor/core.git", + "state": { + "branch": null, + "revision": "a909eccc41941faac6fb9e511cdb9a5cb30a05de", + "version": "3.1.7" + } + }, + { + "package": "Crypto", + "repositoryURL": "https://github.com/vapor/crypto.git", + "state": { + "branch": null, + "revision": "1b8c2ba5a42f1adf2aa812204678d8b16466fa59", + "version": "3.1.2" + } + }, + { + "package": "DatabaseKit", + "repositoryURL": "https://github.com/vapor/database-kit.git", + "state": { + "branch": null, + "revision": "0db303439e5ef8b6df50a2b6c4029edddee90cb0", + "version": "1.0.1" + } + }, + { + "package": "HTTP", + "repositoryURL": "https://github.com/vapor/http.git", + "state": { + "branch": null, + "revision": "5e766f72d81ef5fe8805d704efdffd17e4906134", + "version": "3.0.6" + } + }, + { + "package": "Manifest", + "repositoryURL": "https://github.com/Ether-CLI/Manifest.git", + "state": { + "branch": null, + "revision": "a200f9a6af998f787ac4ea7b92802f7b139d499f", + "version": "0.4.5" + } + }, + { + "package": "Multipart", + "repositoryURL": "https://github.com/vapor/multipart.git", "state": { "branch": null, - "revision": "c32f5e6ae2007dccd21a92b7e33eba842dd80d2f", + "revision": "7778dcb62f3efa845e8e2808937bb347575ba7ce", + "version": "3.0.1" + } + }, + { + "package": "Routing", + "repositoryURL": "https://github.com/vapor/routing.git", + "state": { + "branch": null, + "revision": "3219e328491b0853b8554c5a694add344d2c6cfb", + "version": "3.0.1" + } + }, + { + "package": "Service", + "repositoryURL": "https://github.com/vapor/service.git", + "state": { + "branch": null, + "revision": "281a70b69783891900be31a9e70051b6fe19e146", + "version": "1.0.0" + } + }, + { + "package": "swift-nio", + "repositoryURL": "https://github.com/apple/swift-nio.git", + "state": { + "branch": null, + "revision": "bad7c297427b5efedb96c4044f9e57b42881e9ea", + "version": "1.7.0" + } + }, + { + "package": "swift-nio-ssl", + "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", + "state": { + "branch": null, + "revision": "38955a5f806a952daf2b16fbfe9aa529749cf1dd", "version": "1.1.0" } }, { - "package": "Console", - "repositoryURL": "https://github.com/vapor/console.git", + "package": "swift-nio-ssl-support", + "repositoryURL": "https://github.com/apple/swift-nio-ssl-support.git", "state": { "branch": null, - "revision": "df9eb9a6afd03851abcb3d8204d04c368729776e", - "version": "2.3.0" + "revision": "c02eec4e0e6d351cd092938cf44195a8e669f555", + "version": "1.0.0" } }, { - "package": "Core", - "repositoryURL": "https://github.com/vapor/core.git", + "package": "swift-nio-zlib-support", + "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git", + "state": { + "branch": null, + "revision": "37760e9a52030bb9011972c5213c3350fa9d41fd", + "version": "1.0.0" + } + }, + { + "package": "TemplateKit", + "repositoryURL": "https://github.com/vapor/template-kit.git", + "state": { + "branch": null, + "revision": "43b57b5861d5181b906ac6411d28645e980bb638", + "version": "1.0.1" + } + }, + { + "package": "URLEncodedForm", + "repositoryURL": "https://github.com/vapor/url-encoded-form.git", "state": { "branch": null, - "revision": "f9f3a585ab0ea5764b46d7a36d9c0d9d508b9c63", - "version": "2.2.0" + "revision": "57cf7fb9c1a1014c50bc05123684a9139ad44127", + "version": "1.0.3" } }, { - "package": "Debugging", - "repositoryURL": "https://github.com/vapor/debugging.git", + "package": "Validation", + "repositoryURL": "https://github.com/vapor/validation.git", "state": { "branch": null, - "revision": "fc5a27d6eb236141dc24e5f14eedaa2e035ae7b3", - "version": "1.1.1" + "revision": "ab6c5a352d97c8687b91ed4963aef8e7cfe0795b", + "version": "2.0.0" } }, { - "package": "JSON", - "repositoryURL": "https://github.com/vapor/json.git", + "package": "Vapor", + "repositoryURL": "https://github.com/vapor/vapor.git", "state": { "branch": null, - "revision": "735800d8f2e75ebe3be25559eb6a781f4666dcfc", - "version": "2.2.1" + "revision": "39b4d3fa36e58c6f7415c9da6c65a703bec34cea", + "version": "3.0.3" } }, { - "package": "Node", - "repositoryURL": "https://github.com/vapor/node.git", + "package": "WebSocket", + "repositoryURL": "https://github.com/vapor/websocket.git", "state": { "branch": null, - "revision": "c4ff32f07657aec849677e5aecb657bb6c85098d", - "version": "2.1.4" + "revision": "141cb4d3814dc8062cb0b2f43e72801b5dfcf272", + "version": "1.0.1" } } ] diff --git a/Package.swift b/Package.swift index c74e697..380ba27 100644 --- a/Package.swift +++ b/Package.swift @@ -5,13 +5,14 @@ import PackageDescription let package = Package( name: "Ether", dependencies: [ - .package(url: "https://github.com/vapor/console.git", .exact("2.3.0")), - .package(url: "https://github.com/vapor/json.git", .exact("2.2.1")), - .package(url: "https://github.com/vapor/core.git", .exact("2.2.0")) + .package(url: "https://github.com/vapor/vapor.git", from: "3.0.3"), + .package(url: "https://github.com/vapor/console.git", from: "3.0.2"), + .package(url: "https://github.com/vapor/core.git", from: "3.1.7"), + .package(url: "https://github.com/Ether-CLI/Manifest.git", from: "0.4.4") ], targets: [ - .target(name: "Helpers", dependencies: ["Core", "JSON"]), - .target(name: "Ether", dependencies: ["Helpers", "Console", "JSON"]), - .target(name: "Executable", dependencies: ["Ether"]) + .target(name: "Helpers", dependencies: ["Core", "Console"]), + .target(name: "Ether", dependencies: ["Vapor", "Helpers", "Console", "Command", "Manifest", "Core"]), + .target(name: "Executable", dependencies: ["Vapor", "Ether", "Console"]) ] ) diff --git a/README.md b/README.md index 2cc6cc5..a65f855 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ # Ether +[![Mentioned in Awesome Vapor](https://awesome.re/mentioned-badge.svg)](https://github.com/Cellane/awesome-vapor) + **Notice!** Ether is currently out of comission because the service that was used to fetch package data from (IBM's package catalog) is [no longer hosted](https://packagecatalog.com/). There is work going on for a [replacement](https://github.com/vapor-community/PackageCatalogAPI), but development is moving slowly. If you wold like to pitch in, pop on over to the [Vapor Slack](https://vapor.team/) and ping me @calebkleveter! diff --git a/Sources/Ether/CleanManifest.swift b/Sources/Ether/CleanManifest.swift deleted file mode 100644 index b749610..0000000 --- a/Sources/Ether/CleanManifest.swift +++ /dev/null @@ -1,42 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2017 Caleb Kleveter -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import Console -import Helpers - -public class CleanManifest: Command { - public let id: String = "clean-manifest" - public let console: ConsoleProtocol - - public let help: [String] = [ - "This command is for internal purposes only" - ] - - public init(console: ConsoleProtocol) { - self.console = console - } - - public func run(arguments: [String]) throws { - console.output("Cleaning Manifest...", style: .info, newLine: true) - try Manifest.current.clean() - } -} diff --git a/Sources/Ether/Configuration.swift b/Sources/Ether/Configuration.swift index 7bfa1aa..5bd99be 100644 --- a/Sources/Ether/Configuration.swift +++ b/Sources/Ether/Configuration.swift @@ -22,149 +22,64 @@ import Foundation import Console +import Command import Helpers -import JSON +import Core +import Bits public class Configuration: Command { - public let id: String = "config" - public let configPath = "/Library/Application Support/Ether/config.json" - - public let signature: [Argument] = [ - Value(name: "key", help: [ - "The configuration JSON key to set" - ]), - Value(name: "value", help: [ - "The new value for the key passed in" - ]) + public var arguments: [CommandArgument] = [ + CommandArgument.argument(name: "key", help: ["The configuration JSON key to set"]), + CommandArgument.argument(name: "value", help: ["The new value for the key passed in"]) ] - public let help: [String] = [ - "Configure custom actions to occure when a command is run", - "Run `config help` to get information on expected data for the command options" - ] + public var options: [CommandOption] = [] - public let console: ConsoleProtocol + public var help: [String] = ["Configure custom actions to occure when a command is run"] - public init(console: ConsoleProtocol) { - self.console = console - } + public init() {} - public func run(arguments: [String]) throws { - let setBar = console.loadingBar(title: "Setting Configuration Key") - setBar.start() + public func run(using context: CommandContext) throws -> EventLoopFuture { + let setter = context.console.loadingBar(title: "Setting Configuration Key") + _ = setter.start(on: context.container) - let fileManager = FileManager.default - let key = try value("key", from: arguments) - let val: String + let key = try context.argument("key") + let value = try context.argument("value") + let user = try Process.execute("whoami") - do { - val = try value("value", from: arguments) - } catch { - if key == "help" { printHelp() } - return - } + var configuration = try Configuration.get() - guard let jsonPath = ConfigurationKey.getKey(from: key)?.jsonPath else { - throw fail(bar: setBar, with: "Unable to get JSON path for specified key") - } - guard let configURL = URL(string: "file:\(fileManager.currentDirectoryPath)\(configPath)") else { - throw fail(bar: setBar, with: "Unable to create path to config file") + guard let property = Config.properties[key] else { + throw EtherError(identifier: "noSettingWithName", reason: "No configuration setting found with name '\(key)'") } - let jsonData = try Data(contentsOf: configURL).makeBytes() - var json = try JSON(bytes: jsonData) - try self.set(jsonPath, with: val, in: &json) + configuration[keyPath: property] = value - try Data(bytes: json.makeBytes()).write(to: configURL) + try JSONEncoder().encode(configuration).write(to: URL(string: "file:/Users/\(user)/Library/Application%20Support/Ether/config.json")!) - setBar.finish() - } - - fileprivate func printHelp() { - let help = """ - Below are the keys, values, and expected types for the configuration JSON. - - id | key | value-type | description - ---+----------------+------------+------------------------------------------------------------- - 0 | use-git | Bool | Wheather to run git commands when a project is written to - 1 | install-commit | String | The message to use when committing after an installation - 2 | remove-commit | String | The message to use when committing after a package removal - 3 | latest-commit | String | The message to use when all packages are updated to their - | | | latest versions - 4 | new-commit | String | The message to use when committing a newly generated project - - When a commit is made, there are variables that can be replaced for more specific messages. - Below are the variables, their values, and the config ID that they belong to: - - id | var | description - ---+-----+-------------------- - 1 | $0 | The package name - 1 | $1 | The package version - 2 | $0 | The package name - 4 | $0 | The project name - 4 | $1 | The package type - """ - console.output(help, style: .plain, newLine: true) + setter.succeed() + return context.container.eventLoop.newSucceededFuture(result: ()) } - fileprivate func set(_ path: [String], with val: Any?, `in` json: inout JSON)throws { - var jsons: [(key: String, json: JSON)] = [] - var top: JSON = JSON() - var sub: JSON = JSON() + public static func get()throws -> Config { + let user = try Process.execute("whoami") + let configuration: Data - if path.count < 1 { return } - for key in path { - try jsons.append((key: key, json: json.get(key))) - } - if jsons.count == 0 { return } - else if jsons.count == 1 { - top = jsons[0].json - try top.set(path[0], val) - json = top - return + let contents = try Data(contentsOf: URL(string: "file:/Users/\(user)/Library/Application%20Support/Ether/config.json")!) + if contents.count > 0 { + configuration = contents + } else { + configuration = Data([.leftCurlyBracket, .rightCurlyBracket]) } - for index in Array(0...jsons.count-1).reversed() { - sub = jsons[index].json - - if index == jsons.count-1 { - // Force-unwrapping always succedes because we tested for the path count earlier. - try sub.set(path.last!, val) - } else if index > 0 { - top = jsons[index].json - try top.set(jsons[index].key, sub) - } else { - json = top - } - } + return try JSONDecoder().decode(Config.self, from: configuration) } } -fileprivate enum ConfigurationKey { - case useGit - case gitInstallMessage - case gitRemoveMessage - case gitLatestMessage - case gitNewMessage +public struct Config: Codable, Reflectable { + public var accessToken: String? - var jsonPath: [String] { - switch self { - case .useGit: return ["git", "use"] - case .gitInstallMessage: return ["git", "commit-messages", "install"] - case .gitRemoveMessage: return ["git", "commit-message", "remove"] - case .gitLatestMessage: return ["git", "commit-message", "version-latest"] - case .gitNewMessage: return ["git", "commit-message", "new"] - } - } - - static func getKey(from string: String) -> ConfigurationKey? { - switch string.lowercased() { - case "use-git": return .useGit - case "install-commit": return .gitInstallMessage - case "remove-commit": return .gitRemoveMessage - case "latest-commit": return .gitLatestMessage - case "new-commit": return .gitNewMessage - default: return nil - } - } + static let properties: [String: WritableKeyPath] = [ + "access-token": \.accessToken + ] } diff --git a/Sources/Ether/FixInstall.swift b/Sources/Ether/FixInstall.swift index beec04a..10579bc 100644 --- a/Sources/Ether/FixInstall.swift +++ b/Sources/Ether/FixInstall.swift @@ -20,41 +20,28 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Console +import Command -public class FixInstall: Command { - public let id: String = "fix-install" +public final class FixInstall: Command { + public var arguments: [CommandArgument] = [] + public var options: [CommandOption] = [] - public let signature: [Argument] = [ - Option(name: "no-build", help: [ - "Skips the rebuilding proccess after clearing the project cache" - ]) - ] + public var help: [String] = ["Fixes fetching errors that occur during package install"] - public var help: [String] = [ - "Fixes fetching errors that occur during package install" - ] + public init() {} - public let console: ConsoleProtocol - - public init(console: ConsoleProtocol) { - self.console = console - } - - public func run(arguments: [String]) throws { - let fixBar = console.loadingBar(title: "Fixing Installation") + public func run(using context: CommandContext) throws -> EventLoopFuture { + context.console.output("This may take some time...", style: .info) - console.output("This may take some time...", style: .info, newLine: true) - fixBar.start() + let fixing = context.console.loadingBar(title: "Fixing Instillation") + _ = fixing.start(on: context.container) - _ = try console.backgroundExecute(program: "rm", arguments: ["-rf", ".build"]) - _ = try console.backgroundExecute(program: "swift", arguments: ["package", "update"]) - _ = try console.backgroundExecute(program: "swift", arguments: ["package", "resolve"]) + _ = try Process.execute("rm", ["--rf", ".build"]) + _ = try Process.execute("swift", ["package", "update"]) + _ = try Process.execute("swift", ["package", "resolve"]) - if arguments.option("no-build") == nil { - _ = try console.backgroundExecute(program: "swift", arguments: ["build"]) - } + fixing.succeed() - fixBar.finish() + return context.container.eventLoop.newSucceededFuture(result: ()) } } diff --git a/Sources/Ether/Install.swift b/Sources/Ether/Install.swift index 89d7eaf..2559e10 100644 --- a/Sources/Ether/Install.swift +++ b/Sources/Ether/Install.swift @@ -20,142 +20,87 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import Manifest +import Command import Helpers -import Console -import Foundation -import Core +import Vapor public final class Install: Command { - public let id = "install" - public let baseURL = "https://packagecatalog.com/data/package/" - - public let signature: [Argument] = [ - Value(name: "name", help: [ - "The name of the package that will be installed" - ]), - Option(name: "url", short: "u", help: [ - "The URL for the package" - ]), - Option(name: "version", short: "v", help: [ - "The desired version for the package", - "This defaults to the latest version" - ]), - Option(name: "xcode", short: "x", help: [ - "Regenerate the Xcode project after the install is complete" - ]) + public var arguments: [CommandArgument] = [ + CommandArgument.argument(name: "name", help: ["The name of the package that will be installed"]) ] - public var help: [String] = [ - "Installs a package into the current project" + public var options: [CommandOption] = [ + CommandOption.value(name: "url", short: "u", help: ["The URL for the package"]), + CommandOption.value(name: "version", short: "v", help: [ + "The desired version for the package", + "This defaults to the latest version" + ]), + CommandOption.flag(name: "xcode", short: "x", help: ["Regenerate the Xcode project after the install is complete"]) ] - public let console: ConsoleProtocol - public let client = PackageJSONFetcher() + public var help: [String] = ["Installs a package into the current project"] - public init(console: ConsoleProtocol) { - self.console = console - } + public init() {} - public func run(arguments: [String]) throws { - console.output("Reading Package Targets...", style: .info, newLine: true) + public func run(using context: CommandContext) throws -> Future { + context.console.info("Reading Package Targets...") + let targets = try Manifest.current.targets().map { $0.name } + let approvedTargets = self.inquireFor(targets: targets, in: context) - let fileManager = FileManager.default - let name = try value("name", from: arguments) - let installBar = console.loadingBar(title: "Installing Dependency") + let installing = context.console.loadingBar(title: "Installing Dependency") - // Get package manifest and JSON data - guard let manifestURL = URL(string: "file:\(fileManager.currentDirectoryPath)/Package.swift") else { - throw EtherError.fail("Bad path to package manifest. Make sure you are in the project root.") - } - guard let resolvedURL = URL(string: "file:\(fileManager.currentDirectoryPath)/Package.resolved") else { - throw EtherError.fail("Bad path to package data. Make sure you are in the project root.") - } - let packageManifest = try String(contentsOf: manifestURL) - let mutablePackageManifest = NSMutableString(string: packageManifest) - let pinsCount: Int + let oldPinCount: Int do { - let packageData = try Data(contentsOf: resolvedURL).json() - guard let object = packageData?["object"] as? APIJSON, - let pins = object["pins"] as? [APIJSON] else { return } - pinsCount = pins.count - } catch { - pinsCount = 0 - } - - // Get the names of the targets to add the dependency to - let targets = try Manifest.current.getTargets() - let useTargets: [String] = inquireFor(targets: targets) - - installBar.start() - - let packageInstenceRegex = try NSRegularExpression(pattern: "(\\.package([\\w\\s\\d\\,\\:\\(\\)\\@\\-\\\"\\/\\.])+\\)),?(?:\\R?)", options: .anchorsMatchLines) - let dependenciesRegex = try NSRegularExpression(pattern: "products: *\\[(?s:.*?)\\],\\s*dependencies: *\\[", options: .anchorsMatchLines) - - // Get the data for the package to install - let newPackageData = try Manifest.current.getPackageData(for: name) - let packageVersion = arguments.options["version"] ?? newPackageData.version - let packageUrl = arguments.options["url"] ?? newPackageData.url - - let packageInstance = "$1,\n .package(url: \"\(packageUrl)\", .exact(\"\(packageVersion)\"))\n" - let depPackageInstance = "$0\n .package(url: \"\(packageUrl)\", .exact(\"\(packageVersion)\"))" - - // Add the new package instance to the Package dependencies array. - if packageInstenceRegex.matches(in: packageManifest, options: [], range: NSMakeRange(0, packageManifest.utf8.count)).count > 0 { - packageInstenceRegex.replaceMatches(in: mutablePackageManifest, options: [], range: NSMakeRange(0, mutablePackageManifest.length), withTemplate: packageInstance) - } else { - dependenciesRegex.replaceMatches(in: mutablePackageManifest, options: [], range: NSMakeRange(0, mutablePackageManifest.length), withTemplate: depPackageInstance) - } - - // Write the new package manifest to the Package.swift file - try String(mutablePackageManifest).data(using: .utf8)?.write(to: URL(string: "file:\(fileManager.currentDirectoryPath)/Package.swift")!) - - // Update the packages. - _ = try console.backgroundExecute(program: "swift", arguments: ["package", "update"]) - _ = try console.backgroundExecute(program: "swift", arguments: ["package", "resolve"]) - - // Get the new package name and add it to the previously accepted targets. - let dependencyName = try Manifest.current.getPackageName(for: newPackageData.url) - for target in useTargets { - try mutablePackageManifest.addDependency(dependencyName, to: target) - } - - // Write the Package.swift file again - try String(mutablePackageManifest).data(using: .utf8)?.write(to: URL(string: "file:\(fileManager.currentDirectoryPath)/Package.swift")!) - - // Calculate the number of package that where installed and output it. - let packageData = try Data(contentsOf: resolvedURL).json() - guard let object = packageData?["object"] as? APIJSON, - let pins = object["pins"] as? [APIJSON] else { return } - - let newPackageCount = pins.count - pinsCount - - installBar.finish() - - if let _ = arguments.options["xcode"] { - let xcodeBar = console.loadingBar(title: "Generating Xcode Project") - xcodeBar.start() - _ = try console.backgroundExecute(program: "swift", arguments: ["package", "generate-xcodeproj"]) - xcodeBar.finish() - try console.execute(program: "/bin/sh", arguments: ["-c", "open *.xcodeproj"], input: nil, output: nil, error: nil) + oldPinCount = try Manifest.current.resolved().object.pins.count + } catch { oldPinCount = 0 } + + let name = try context.argument("name") + + context.console.info("Fetching Package Data...") + return try self.package(with: name, on: context).map(to: Void.self) { package in + _ = installing.start(on: context.container) + + let dependency = Dependency(url: package.url, version: .from(package.version)) + try dependency.save() + + try approvedTargets.forEach { name in + guard let target = try Manifest.current.target(withName: name) else { return } + target.dependencies.append(contentsOf: package.products) + try target.save() + } + _ = try Process.execute("swift", "package", "update") + let newPinCount = try Manifest.current.resolved().object.pins.count + + installing.succeed() + + if let _ = context.options["xcode"] { + let xcodeBar = context.console.loadingBar(title: "Generating Xcode Project") + _ = xcodeBar.start(on: context.container) + + _ = try Process.execute("swift", "package", "generate-xcodeproj") + xcodeBar.succeed() + _ = try Process.execute("sh", "-c", "open *.xcodeproj") + } + + context.console.output("📦 \(newPinCount - oldPinCount) packages installed", style: .plain, newLine: true) } - - console.output("📦 \(newPackageCount) packages installed", style: .plain, newLine: true) } /// Asks the user if they want to add a dependency to the targets in the package manifest. /// /// - Parameter targets: The names of the targets available. /// - Returns: The names of the targets that where accepted. - fileprivate func inquireFor(targets: [String]) -> [String] { + fileprivate func inquireFor(targets: [String], in context: CommandContext) -> [String] { var acceptedTargets: [String] = [] var index = 0 - + if targets.count > 1 { targetFetch: while index < targets.count { let target = targets[index] - let response = console.ask("Would you like to add the package to the target '\(target)'? (y,n,q,?)") - + let response = context.console.ask(ConsoleText(stringLiteral: "Would you like to add the package to the target '\(target)'? (y,n,q,?)")) + switch response { case "y": acceptedTargets.append(target) @@ -164,7 +109,7 @@ public final class Install: Command { index += 1 case "q": break targetFetch - default: console.output(""" + default: context.console.output(""" y: Add the package as a dependency to the target. n: Do not add the package as a dependency to the target. q: Do not add the package as a dependency to the current target or any of the following targets. @@ -175,17 +120,116 @@ public final class Install: Command { } else { acceptedTargets.append(targets[0]) } - + return acceptedTargets } + + func package(with name: String, on context: CommandContext)throws -> Future<(url: String, version: String, products: [String])> { + let client = try context.container.make(Client.self) + guard let token = try Configuration.get().accessToken else { + throw EtherError( + identifier: "noAccessToken", + reason: "No access token in configuration. Run `ether config access-token `. The token should have permissions to access public repositories" + ) + } + + let fullName: Future + if name.contains("/") { + let url = "https://package.vapor.cloud/packages/\(name)" + fullName = client.get(url).flatMap(to: String.self) { response in + response.content.get(String.self, at: "full_name") + } + } else { + let search = "https://package.vapor.cloud/packages/search?name=\(name)" + fullName = client.get(search, headers: ["Authorization": "Bearer \(token)"]).flatMap(to: String.self) { response in + response.content.get(String.self, at: "repositories", 0, "nameWithOwner") + } + } + + let version = fullName.flatMap(to: String.self) { fullName in + let names = fullName.split(separator: "/").map(String.init) + return try self.version(owner: names[0], repo: names[1], token: token, on: context) + }.map(to: String.self) { version in + if version.first == "v" { return String(version.dropFirst()) } + return version + } + + let products = fullName.flatMap(to: [String].self) { fullName in + let names = fullName.split(separator: "/").map(String.init) + return try self.products(owner: names[0], repo: names[1], token: token, on: context) + } + + return map(to: (url: String, version: String, products: [String]).self, fullName, version, products) { name, version, products in + let url = "https://github.com/\(name).git" + return (url, version, products) + } + } + + fileprivate func version(owner: String, repo: String, token: String, on context: CommandContext)throws -> Future { + let client = try context.container.make(Client.self) + return client.get("https://package.vapor.cloud/packages/\(owner)/\(repo)/releases", headers: ["Authorization":"Bearer \(token)"]).flatMap(to: [String].self) { response in + return try response.content.decode([String].self) + }.map(to: String.self) { releases in + guard let first = releases.first else { + throw EtherError( + identifier: "noReleases", + reason: "No tags where found for the selected package. You might want to open an issue on the package requesting a release." + ) + } + + if first.lowercased().contains("rc") || first.lowercased().contains("beta") || first.lowercased().contains("alpha") { + let majorVersion = Int(String(first.first ?? "0")) ?? 0 + if majorVersion > 0 && releases.count > 1 { + var answer: String = "replace" + + while true { + answer = context.console.ask( + ConsoleText(stringLiteral:"The latest version found (\(first)) is a pre-release. Would you like to use an earlier stable release? (y/n)") + ).lowercased() + if answer == "y" || answer == "n" { break } + } + + if answer == "y" { + return releases.filter { Int(String($0.first ?? "0")) ?? 0 != majorVersion }.first ?? first + } else { + return first + } + } else { + return first + } + } else { + return first + } + } + } + + fileprivate func products(owner: String, repo: String, token: String, on context: CommandContext)throws -> Future<[String]> { + let client = try context.container.make(Client.self) + return client.get("https://package.vapor.cloud/packages/\(owner)/\(repo)/manifest", headers: ["Authorization":"Bearer \(token)"]).flatMap(to: [Product].self) { response in + return response.content.get([Product].self, at: "products") + }.map(to: [String].self) { products in + if let index = products.index(where: { $0.name.lowercased() == repo.lowercased() }) { + return [products[index].name] + } + if products.count < 1 { return [repo] } + + var allowed: [String]? = nil + + repeat { + let options = products.enumerated().map { return "\($0.offset). \($0.element)" } + let question = ["Unable to automatically detect product to add to target(s). Answer with comma seperated list of products to add"] + options + let seletions = context.console.ask(ConsoleText(stringLiteral: question.joined(separator: "\n"))) + let indexes = seletions.split(separator: "\n").map(String.init).map { $0.trimmingCharacters(in: .whitespaces) }.compactMap(Int.init) + + let selected = products.enumerated().filter { indexes.contains($0.offset) }.map { $0.element.name } + if selected.count > 0 { allowed = selected } + } while allowed == nil + + return allowed! + } + } } - - - - - - - - - +struct Product: Content { + let name: String +} diff --git a/Sources/Ether/New.swift b/Sources/Ether/New.swift index 8fa43b0..cec15f0 100644 --- a/Sources/Ether/New.swift +++ b/Sources/Ether/New.swift @@ -20,82 +20,74 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Console -import Helpers import Foundation +import Manifest +import Helpers +import Command public final class New: Command { - public let id = "new" - - public let help: [String] = [ - "Create new projects" + public var arguments: [CommandArgument] = [ + CommandArgument.argument(name: "name", help: ["The name of the new project"]) ] - public let signature: [Argument] = [ - Value(name: "name", help: [ - "The name of the new project" - ]), - Option(name: "executable", short: "e", help: [ - "Creates an executable SPM project" - ]), - Option(name: "package", short: "p", help: [ - "Creates an SPM package" - ]), - Option(name: "template", help: [ - "Creates a project starting with a previously saved template" - ]) + public var options: [CommandOption] = [ + CommandOption.flag(name: "executable", short: "e", help: ["Creates an executable SPM project"]), + CommandOption.flag(name: "package", short: "p", help: ["(default) Creates an SPM package"]), + CommandOption.value(name: "template", help: ["Creates a project with a previously saved template"]) ] - public let console: ConsoleProtocol + public var help: [String] = ["Creates a new project"] - public init(console: ConsoleProtocol) { - self.console = console - } + public init() {} - public func run(arguments: [String]) throws { - let newProjectBar = console.loadingBar(title: "Generating Project") - newProjectBar.start() - - let executable = try newExecutable(arguments: arguments) - let template = try newFromTemplate(arguments: arguments) + public func run(using context: CommandContext) throws -> EventLoopFuture { + let newProject = context.console.loadingBar(title: "Generating Project") + _ = newProject.start(on: context.container) + + let executable = try newExecutable(from: context) + let template = try newFromTemplate(using: context) if !executable && !template { - try newPackage(arguments: arguments) + try newPackage(from: context) } - - newProjectBar.finish() + + newProject.succeed() + return context.container.eventLoop.newSucceededFuture(result: ()) } - func newExecutable(arguments: [String]) throws -> Bool { - if let _ = arguments.option("executable") { - let name = try value("name", from: arguments) - let script = "mkdir \(name); cd \(name); swift package init --type=executable; ether clean-manifest" - _ = try console.backgroundExecute(program: "bash", arguments: ["-c", script]) + func newExecutable(from context: CommandContext) throws -> Bool { + if let _ = context.options["executable"] { + let name = try context.argument("name") + let script = "mkdir \(name); cd \(name); swift package init --type=executable" + _ = try Process.execute("bash", ["-c", script]) + + try Manifest.current.reset() return true } return false } - func newFromTemplate(arguments: [String]) throws -> Bool { - if let template = arguments.option("template") { - let name = try value("name", from: arguments) + func newFromTemplate(using context: CommandContext) throws -> Bool { + if let template = context.options["template"] { + let name = try context.argument("name") let manager = FileManager.default - + if #available(OSX 10.12, *) { let directoryName = manager.homeDirectoryForCurrentUser.absoluteString let templatePath = String("\(directoryName)Library/Application Support/Ether/Templates/\(template)".dropFirst(7)) let current = manager.currentDirectoryPath - shell(command: "/bin/cp", "-a", "\(templatePath)", "\(current)/\(name)") + _ = try Process.execute("cp", ["-a", "\(templatePath)", "\(current)/\(name)"]) } else { - throw EtherError.fail("This command is not supported in macOS versions older then 10.12") + throw EtherError(identifier: "unsupportedOS", reason: "This command is not supported in macOS versions older then 10.12") } return true } return false } - func newPackage(arguments: [String]) throws { - let name = try value("name", from: arguments) - let script = "mkdir \(name); cd \(name); swift package init; ether clean-manifest" - _ = try console.backgroundExecute(program: "bash", arguments: ["-c", script]) + func newPackage(from context: CommandContext) throws { + let name = try context.argument("name") + let script = "mkdir \(name); cd \(name); swift package init" + _ = try Process.execute("bash", ["-c", script]) + try Manifest.current.reset() } } diff --git a/Sources/Ether/Remove.swift b/Sources/Ether/Remove.swift index c4eaa33..f866e6b 100644 --- a/Sources/Ether/Remove.swift +++ b/Sources/Ether/Remove.swift @@ -20,75 +20,58 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import Manifest import Helpers -import Console -import Foundation +import Command public final class Remove: Command { - public let id = "remove" - - public var help: [String] = [ - "Removes and uninstalls a package" + public var arguments: [CommandArgument] = [ + CommandArgument.argument(name: "name", help: ["The name of the package that will be removed"]) ] - public var signature: [Argument] = [ - Value(name: "name", help: [ - "The name of the package that will be removed" - ]), - Option(name: "xcode", short: "x", help: [ - "Regenerate the Xcode project after removing the package" - ]) + public var options: [CommandOption] = [ + CommandOption.flag(name: "xcode", short: "x", help: ["Regenerate the Xcode project after removing the package"]) ] - public let console: ConsoleProtocol + public var help: [String] = ["Removes a package from the manifest and uninstalls it"] - public init(console: ConsoleProtocol) { - self.console = console - } + public init() {} - public func run(arguments: [String]) throws { - let removingProgressBar = console.loadingBar(title: "Removing Dependency") - removingProgressBar.start() - - let manager = FileManager.default - let name = try value("name", from: arguments) - let url = try Manifest.current.getPackageUrl(for: name) + public func run(using context: CommandContext) throws -> EventLoopFuture { + let removing = context.console.loadingBar(title: "Removing Dependency") + _ = removing.start(on: context.container) - let regex = try NSRegularExpression(pattern: "(\\,?\\n *\\.package\\(url: *\"\(url)\", *)(.*?)(?=,?\n)", options: .caseInsensitive) - let oldPins = try Manifest.current.getPins() + let name = try context.argument("name") + let pinCount = try Manifest.current.resolved().object.pins.count - let packageString = try Manifest.current.get() - let mutableString = NSMutableString(string: packageString) - - if regex.matches(in: packageString, options: [], range: NSMakeRange(0, packageString.utf8.count)).count == 0 { - throw fail(bar: removingProgressBar, with: "No packages matching the name passed in where found") + guard let pin = try Manifest.current.resolved().object.pins.filter({ $0.package == name }).first else { + throw EtherError(identifier: "pinNotFound", reason: "No package was found with the name '\(name)'") } - regex.replaceMatches(in: mutableString, options: [], range: NSMakeRange(0, mutableString.length), withTemplate: "") - try mutableString.removeDependency(name) - - do { - try String(mutableString).data(using: .utf8)?.write(to: URL(string: "file:\(manager.currentDirectoryPath)/Package.swift")!) - _ = try console.backgroundExecute(program: "swift", arguments: ["package", "update"]) - _ = try console.backgroundExecute(program: "swift", arguments: ["package", "resolve"]) - } catch let error { - removingProgressBar.fail() - throw error + try Manifest.current.dependency(withURL: pin.repositoryURL)?.delete() + try Manifest.current.targets().filter { $0.dependencies.contains(pin.package) }.forEach { target in + if let index = target.dependencies.index(of: pin.package) { + target.dependencies.remove(at: index) + try target.save() + } } - let pins = try Manifest.current.getPins() - let pinsCount = oldPins.count - pins.count + _ = try Process.execute("swift", ["package", "update"]) + _ = try Process.execute("swift", ["package", "resolve"]) - removingProgressBar.finish() + let removed = try pinCount - Manifest.current.resolved().object.pins.count + removing.succeed() - if let _ = arguments.options["xcode"] { - let xcodeBar = console.loadingBar(title: "Generating Xcode Project") - xcodeBar.start() - _ = try console.backgroundExecute(program: "swift", arguments: ["package", "generate-xcodeproj"]) - xcodeBar.finish() - try console.execute(program: "/bin/sh", arguments: ["-c", "open *.xcodeproj"], input: nil, output: nil, error: nil) + if context.options["xcode"] != nil { + let xcodeBar = context.console.loadingBar(title: "Generating Xcode Project") + _ = xcodeBar.start(on: context.container) + + _ = try Process.execute("swift", ["package", "generate-xcodeproj"]) + xcodeBar.succeed() + _ = try Process.execute("bash", ["-c", "open *.xcodeproj"]) } - console.output("📦 \(pinsCount) packages removed", style: .custom(.white), newLine: true) + context.console.print("📦 \(removed) packages removed") + return context.container.eventLoop.newSucceededFuture(result: ()) } } diff --git a/Sources/Ether/Search.swift b/Sources/Ether/Search.swift index 9c9b48c..90f3f51 100644 --- a/Sources/Ether/Search.swift +++ b/Sources/Ether/Search.swift @@ -21,96 +21,78 @@ // SOFTWARE. import Helpers +import Command import Console -import Foundation -import Core +import Vapor public final class Search: Command { - public let id = "search" - - public let baseURL = "https://packagecatalog.com/api/search/" - public let sort = "chart" - public let results = "items" - - public var help: [String] = [ - "Searches for availible packages." + public var arguments: [CommandArgument] = [ + CommandArgument.argument(name: "name", help: ["The name of the package to search for."]) ] - public var signature: [Argument] = [ - Value(name: "name", help: [ - "The name of the package to search for." - ]), - Option(name: "max-results", help: [ - "The maximum number of results that will be returned.", - "This defaults to 20." - ]), - Option(name: "sort", help: [ - "The sorting method to use:", - "moststarred (Most Starred)", - "leaststarred (Least Starred)", - "mostrecent (Most Recent)", - "leastrecent (Least Recent)", - "The default value is moststarred." - ]) + public var options: [CommandOption] = [ + CommandOption.value(name: "max-results", default: "20", help: [ + "The maximum number of results that will be returned.", + "This defaults to 20." + ]) ] - public let console: ConsoleProtocol - public let client = PackageJSONFetcher() + public var help: [String] = ["Searches for availible packages."] - public init(console: ConsoleProtocol) { - self.console = console - } + public init() {} - public func run(arguments: [String]) throws { - let searchingBar = console.loadingBar(title: "Searching") - searchingBar.start() + public func run(using context: CommandContext) throws -> EventLoopFuture { + let searching = context.console.loadingBar(title: "Searching") + _ = searching.start(on: context.container) - let name = try value("name", from: arguments) - let maxResults = arguments.options["max-results"] ?? "20" - let sortMethod = arguments.options["sort"] ?? "moststarred" + let client = try context.container.make(Client.self) + let name = try context.argument("name") + let maxResults = context.options["max-results"] ?? "20" + let config = try Configuration.get() - func fail(_ message: String) -> Error { - searchingBar.fail() - return EtherError.fail(message) + guard let max = Int(maxResults), max <= 100 && max > 0 else { + throw EtherError(identifier: "badMaxResults", reason: "`max-results` value must be an integer, less than or equal to 100, and greater than 0") } - - var totalResults: Int? - var maxedResults: Bool? - var packages: [(name: String?, description: String?)]? - - let json = try self.client.get(from: self.baseURL + name, withParameters: [self.sort: sortMethod, self.results: maxResults]) - - guard let data = json["data"] as? APIJSON else { throw fail("Bad JSON key") } - guard let hits = data["hits"] as? APIJSON else { throw fail("Bad JSON key") } - guard let results = hits["hits"] as? [APIJSON] else { throw fail("Bad JSON key") } - - packages = try results.map { (result) -> (name: String?, description: String?) in - guard let source = result["_source"] as? APIJSON else { throw fail("Bad JSON key") } - return (name: source["package_full_name"] as? String, description: source["description"] as? String) + guard let token = config.accessToken else { + throw EtherError( + identifier: "noAccessToken", + reason: "No access token in configuration. Run `ether config access-token `. The token should have permissions to access public repositories" + ) } - maxedResults = Int(String(describing: hits["total"] ?? 0 as AnyObject))! > Int(maxResults)! - totalResults = Int(String(describing: hits["total"] ?? 0 as AnyObject)) - - searchingBar.finish() - - self.console.output("Total results: \(totalResults ?? 0)", style: .info, newLine: true) - - if let maxedResults = maxedResults { - if maxedResults { - self.console.output("Not all results are shown.", style: .info, newLine: true) + let response = client.get("https://package.vapor.cloud/packages/search?name=\(name)&limit=\(max)", headers: ["Authorization": "Bearer \(token)"]) + return response.flatMap(to: [PackageDescription].self) { response in + searching.succeed() + return response.content.get([PackageDescription].self, at: "repositories") + }.map(to: Void.self) { packages in + packages.forEach { package in + package.print(on: context) + context.console.print() } } - if (totalResults ?? 0) > 0 { - console.output(String(repeating: "-", count: console.size.width), style: .info, newLine: true) - console.output("", style: .info, newLine: true) + } +} + +struct PackageDescription: Codable { + let nameWithOwner: String + let description: String? + let license: String? + let stargazers: Int? + + func print(on context: CommandContext) { + if let description = self.description { + context.console.info(nameWithOwner + ": ", newLine: false) + context.console.print(description) + } else { + context.console.info(self.nameWithOwner) } - if let packages = packages { - for package in packages { - self.console.output("\(package.name ?? "N/A"): ", style: .custom(.green), newLine: false) - self.console.output("\(package.description ?? "N/A")", style: .custom(.white), newLine: true) - console.output("", style: .info, newLine: true) - } + + if let license = self.license { + context.console.print("License: " + license) + } + + if let stars = self.stargazers { + context.console.print("Stars: " + String(stars)) } } } diff --git a/Sources/Ether/Template.swift b/Sources/Ether/Template.swift index 8eacd15..efb1dda 100644 --- a/Sources/Ether/Template.swift +++ b/Sources/Ether/Template.swift @@ -20,70 +20,52 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -// Path: ~/Library/Application\ Support/Ether - -import Console import Foundation -import Core import Helpers +import Command -final public class Template: Command { - public let id = "template" - - public let signature: [Argument] = [ - Value(name: "template-name", help: [ - "The name used to identify the template" - ]), - Option(name: "github", help: [ - "Creates a GitHub repo and pushes the template to it (Un-implimented)" - ]), - Option(name: "remove", short: "r", help: [ - "Deletes the template" - ]) +public final class Template: Command { + public var arguments: [CommandArgument] = [ + CommandArgument.argument(name: "name", help: ["The name used to identify the template"]) ] - public let help: [String] = [ - "Creates and stores a template for use as the starting point of a project." + public var options: [CommandOption] = [ + CommandOption.flag(name: "remove", short: "r", help: ["Deletes the template"]) + // TODO: Add `github` flag to create remote repo and push. ] - public let console: ConsoleProtocol + public var help: [String] = ["Creates and stores a template for use as the starting point of a project."] - public init(console: ConsoleProtocol) { - self.console = console - } + public init() {} - public func run(arguments: [String]) throws { - let name = try value("template-name", from: arguments) - let useGitHub = arguments.option("github") != nil ? true : false - let removeTemplate = arguments.option("remove") != nil ? true : false + public func run(using context: CommandContext) throws -> EventLoopFuture { + let name = try context.argument("name") + let removeTemplate = context.options["remove"] == nil ? false : true let manager = FileManager.default - let loadingBarTitle = removeTemplate ? "Deleting Template" : "Saving Template" + let barTitle = removeTemplate ? "Deleting Template" : "Saving Template" - let savingBar = console.loadingBar(title: loadingBarTitle) - savingBar.start() + let temapletBar = context.console.loadingBar(title: barTitle) + _ = temapletBar.start(on: context.container) if #available(OSX 10.12, *) { var isDir : ObjCBool = true let directoryName = manager.homeDirectoryForCurrentUser.absoluteString let defaultPath = String("\(directoryName)Library/Application Support/Ether/Templates".dropFirst(7)) let directoryExists = manager.fileExists(atPath: "\(defaultPath)/\(name)", isDirectory: &isDir) - + if removeTemplate { - if !directoryExists { throw fail(bar: savingBar, with: "No template with that name exists") } - shell(command: "/bin/rm", "-rf", "\(defaultPath)/\(name)") + if !directoryExists { throw EtherError(identifier: "templateNotFound", reason: "No template with the name '\(name)' was found") } + _ = try Process.execute("rm", ["-rf", "\(defaultPath)/\(name)"]) } else { - if directoryExists { throw fail(bar: savingBar, with: "A template with that name already exists") } + if directoryExists { throw EtherError(identifier: "templateAlreadyExists", reason: "A template with the name '\(name)' was found") } let current = manager.currentDirectoryPath + "/." - shell(command: "/bin/cp", "-a", "\(current)", "\(defaultPath)/\(name)") + _ = try Process.execute("cp", ["-a", "\(current)", "\(defaultPath)/\(name)"]) } } else { - throw fail(bar: savingBar, with: "This command is not supported in macOS versions older then 10.12") - } - savingBar.finish() - - if useGitHub { - console.output("The GitHub flag is currently not implimented", style: .warning, newLine: true) + throw EtherError(identifier: "unsupportedOS", reason: "This command is not supported in macOS versions older then 10.12") } + temapletBar.succeed() + return context.container.eventLoop.newSucceededFuture(result: ()) } } diff --git a/Sources/Ether/Update.swift b/Sources/Ether/Update.swift index edc1f3b..ce31cdc 100644 --- a/Sources/Ether/Update.swift +++ b/Sources/Ether/Update.swift @@ -20,59 +20,55 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Console +import Command public final class Update: Command { - public let id = "update" - - public let signature: [Argument] = [ - Option(name: "self", short: "s", help: [ - "Updates Ether" - ]), - Option(name: "xcode", short: "x", help: [ - "Regenerate and open the Xcode project after update its packages" - ]) - ] - - public let help: [String] = [ - "Updates your dependencies." + public var arguments: [CommandArgument] = [] + + public var options: [CommandOption] = [ + CommandOption.flag(name: "ether", short: "e", help: ["Updates Ether CLI"]), + CommandOption.flag(name: "xcode", short: "x", help: ["Regenerate and open the Xcode project after updating packages"]) ] - - public let console: ConsoleProtocol - - public init(console: ConsoleProtocol) { - self.console = console - } - - public func run(arguments: [String]) throws { - if let _ = arguments.option("self") { - let updateBar = console.loadingBar(title: "Updating Ether") - updateBar.start() - _ = try console.backgroundExecute(program: "/bin/sh", arguments: ["-c", "curl https://raw.githubusercontent.com/calebkleveter/Ether/master/install.sh | bash"]) - updateBar.finish() - self.printEtherArt() + + public var help: [String] = ["Updates a project's dependencies."] + + public init() {} + + public func run(using context: CommandContext) throws -> EventLoopFuture { + if context.options["ether"] != nil { + let updating = context.console.loadingBar(title: "Updating Ether") + _ = updating.start(on: context.container) + + _ = try Process.execute("bash", ["-c", "curl https://raw.githubusercontent.com/calebkleveter/Ether/master/install.sh | bash"]) + + updating.succeed() + self.printEtherArt(with: context.console) } else { - console.output("This may take some time...", style: .info, newLine: true) + context.console.output("This may take some time...", style: .info, newLine: true) + + let updating = context.console.loadingBar(title: "Updating Packages") + _ = updating.start(on: context.container) - let updateBar = console.loadingBar(title: "Updating Packages") - updateBar.start() - _ = try console.backgroundExecute(program: "rm", arguments: ["-rf", ".build"]) - _ = try console.backgroundExecute(program: "swift", arguments: ["package", "update"]) - _ = try console.backgroundExecute(program: "swift", arguments: ["package", "resolve"]) - _ = try console.backgroundExecute(program: "swift", arguments: ["build"]) - updateBar.finish() + _ = try Process.execute("swift", ["package", "update"]) + _ = try Process.execute("swift", ["package", "resolve"]) - if let _ = arguments.options["xcode"] { - let xcodeBar = console.loadingBar(title: "Generating Xcode Project") - xcodeBar.start() - _ = try console.backgroundExecute(program: "swift", arguments: ["package", "generate-xcodeproj"]) - xcodeBar.finish() - try console.execute(program: "/bin/sh", arguments: ["-c", "open *.xcodeproj"], input: nil, output: nil, error: nil) + updating.succeed() + + if context.options["xcode"] != nil { + let xcode = context.console.loadingBar(title: "Generating Xcode Project") + _ = xcode.start(on: context.container) + + _ = try Process.execute("swift", ["package", "generate-xcodeproj"]) + + xcode.succeed() + _ = try Process.execute("/bin/sh", ["-c", "open *.xcodeproj"]) } } + + return context.container.eventLoop.newSucceededFuture(result: ()) } - - private func printEtherArt() { + + private func printEtherArt(with console: Console) { let etherArt = """ | • | | • | @@ -89,7 +85,7 @@ public final class Update: Command { let style: ConsoleStyle if let color = characterColors[character] { - style = .custom(color) + style = ConsoleStyle(color: color) } else { style = .plain } @@ -101,5 +97,4 @@ public final class Update: Command { console.print() console.output(console.center("Thanks for Updating Ether!"), style: .plain, newLine: true) } - } diff --git a/Sources/Ether/VersionAll.swift b/Sources/Ether/VersionAll.swift index 880a913..984551d 100644 --- a/Sources/Ether/VersionAll.swift +++ b/Sources/Ether/VersionAll.swift @@ -20,36 +20,36 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Console -import Foundation -import Helpers +import Manifest +import Command public final class VersionAll: Command { - public let id = "all" + public var arguments: [CommandArgument] = [] - public var help: [String] = [ - "Outputs the name of each package installed and its version" - ] + public var options: [CommandOption] = [] - public var signature: [Argument] = [] + public var help: [String] = ["Outputs the name of each package installed and its version"] - public let console: ConsoleProtocol + public init() {} - public init(console: ConsoleProtocol) { - self.console = console - } - - public func run(arguments: [String]) throws { - let fetchingDataBar = console.loadingBar(title: "Getting Package Data") - fetchingDataBar.start() + public func run(using context: CommandContext) throws -> EventLoopFuture { + let pins = try Manifest.current.resolved().object.pins - let pins = try Manifest.current.getPins() - fetchingDataBar.finish() pins.forEach { package in - console.output("\(package["package"] ?? "N/A" as AnyObject): ", style: .success, newLine: false) - if let state = package["state"] as? [String: AnyObject] { - console.output("v\(state["version"] ?? "N/A" as AnyObject)", style: .plain, newLine: true) + context.console.output(package.package + ": ", style: .success, newLine: false) + let version: String + + if let number = package.state.version { + version = "v\(number)" + } else if let branch = package.state.branch { + version = branch + } else { + version = package.state.revision } + + context.console.print(version) } + + return context.container.eventLoop.newSucceededFuture(result: ()) } } diff --git a/Sources/Ether/VersionLatest.swift b/Sources/Ether/VersionLatest.swift index 47342c5..e44b3a0 100644 --- a/Sources/Ether/VersionLatest.swift +++ b/Sources/Ether/VersionLatest.swift @@ -20,69 +20,77 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -// REGEX: \\.Package\\(url\\:\\s?\\\"https\\:\\/\\/github\\.com([\\d\\w\\:\\/\\.\\@\\-]+)\\.git\\\"\\,([\\d\\w\\s\\:])+\\)\\,? - -import Console -import Helpers import Foundation -import Core +import Manifest +import Helpers +import Command +import Vapor public final class VersionLatest: Command { - public let id = "latest" - public let baseURL = "https://packagecatalog.com/data/package" - - public var help: [String] = [ - "Updates all packeges to the latest version" - ] + public var arguments: [CommandArgument] = [] - public var signature: [Argument] = [ - Option(name: "xcode", short: "x", help: [ - "Regenerate Xcode project after updating package versions" - ]) + public var options: [CommandOption] = [ + CommandOption.flag(name: "xcode", short: "x", help: ["Regenerate Xcode project after updating package versions"]) ] - public let console: ConsoleProtocol - public let client = PackageJSONFetcher() + public var help: [String] = ["Updates all packeges to the latest version"] - public init(console: ConsoleProtocol) { - self.console = console - } + public init() {} - public func run(arguments: [String]) throws { - let updateBar = console.loadingBar(title: "Updating Package Versions") - updateBar.start() + public func run(using context: CommandContext) throws -> EventLoopFuture { + let updating = context.console.loadingBar(title: "Updating Version Versions") + _ = updating.start(on: context.container) - let fileManager = FileManager.default - let manifest = try Manifest.current.get() - let nsManifest = NSMutableString(string: manifest) - let versionPattern = try NSRegularExpression(pattern: "(.package\\(url:\\s*\".*?\\.com\\/(.*?)\\.git\",\\s*)(.*?)(\\),?\\n)", options: []) - let matches = versionPattern.matches(in: manifest, options: [], range: NSMakeRange(0, manifest.utf8.count)) - let packageNames = matches.map { match -> String in - let name = versionPattern.replacementString(for: match, in: manifest, offset: 0, template: "$2") - return name - } - let packageVersions = try packageNames.map { name -> String in - return try Manifest.current.getPackageData(for: name).version - } + let namePattern = try NSRegularExpression(pattern: ".*?\\.com\\/(.*?)\\.git", options: []) + let tagPattern = try NSRegularExpression(pattern: "v?\\d+(?:\\.\\d+)?(?:\\.\\d+)?", options: []) + let client = try context.container.make(Client.self) - try zip(packageVersions, packageNames).forEach { (arg) in - let (version, name) = arg - let pattern = try NSRegularExpression(pattern: "(.package\\(url:\\s*\".*?\\.com\\/\(name)\\.git\",\\s*)(\\.?\\w+(\\(|:)\\s*\"[\\w\\.]+\"\\)?)(\\))", options: []) - pattern.replaceMatches(in: nsManifest, options: [], range: NSMakeRange(0, nsManifest.length), withTemplate: "$1.exact(\"\(version)\"))") + guard let token = try Configuration.get().accessToken else { + throw EtherError( + identifier: "noAccessToken", + reason: "No access token in configuration. Run `ether config access-token `. The token should have permissions to access public repositories" + ) } - try String(nsManifest).data(using: .utf8)?.write(to: URL(string: "file:\(fileManager.currentDirectoryPath)/Package.swift")!) - _ = try console.backgroundExecute(program: "swift", arguments: ["package", "update"]) - _ = try console.backgroundExecute(program: "swift", arguments: ["package", "resolve"]) - - updateBar.finish() + let packageNames = try Manifest.current.dependencies().compactMap { dependency -> (fullName: String, url: String)? in + guard let result = namePattern.firstMatch(in: dependency.url, options: [], range: NSMakeRange(0, dependency.url.utf8.count)) else { return nil } + return (namePattern.replacementString(for: result, in: dependency.url, offset: 0, template: "$1"), dependency.url) + } + let versions = packageNames.map { $0.fullName }.map { name in + return client.get("https://package.vapor.cloud/packages/\(name)/releases", headers: ["Authorization": "Bearer \(token)"]).flatMap { response in + return try response.content.decode([String].self) + }.map { releases -> String? in + for tag in releases { + let tagRange = NSMakeRange(0, tag.utf8.count) + if tagRange == tagPattern.rangeOfFirstMatch(in: tag, options: [], range: tagRange) { + return tag + } + } + return nil + } + }.flatten(on: context.container) - if let _ = arguments.options["xcode"] { - let xcodeBar = console.loadingBar(title: "Generating Xcode Project") - xcodeBar.start() - _ = try console.backgroundExecute(program: "swift", arguments: ["package", "generate-xcodeproj"]) - xcodeBar.finish() - try console.execute(program: "/bin/sh", arguments: ["-c", "open *.xcodeproj"], input: nil, output: nil, error: nil) + return versions.map(to: Void.self) { versions in + try zip(packageNames, versions).forEach { packageVersion in + let (names, version) = packageVersion + let dependency = try Manifest.current.dependency(withURL: names.url) + if let version = version { + dependency?.version = .from(version) + } + try dependency?.save() + } + + _ = try Process.execute("swift", "package", "update") + updating.succeed() + + if let _ = context.options["xcode"] { + let xcodeBar = context.console.loadingBar(title: "Generating Xcode Project") + _ = xcodeBar.start(on: context.container) + + _ = try Process.execute("swift", "package", "generate-xcodeproj") + xcodeBar.succeed() + _ = try Process.execute("sh", "-c", "open *.xcodeproj") + } } } } diff --git a/Sources/Ether/VersionSet.swift b/Sources/Ether/VersionSet.swift index a75b746..ada67e5 100644 --- a/Sources/Ether/VersionSet.swift +++ b/Sources/Ether/VersionSet.swift @@ -20,98 +20,86 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Console +import Manifest +import Command import Helpers -import Foundation -public final class VersonSet: Command { - public let id: String = "set" - - public var signature: [Argument] = [ - Value(name: "name", help: [ - "The name of the package to change the version for" - ]), - Value(name: "version", help: [ - "The value for the new version. The format varies depending on the version type used" - ]), - Option(name: "xcode", short: "x", help: [ - "Regenerate the Xcode project after updating a package's version" - ]), - Option(name: "from", short: "f", help: [ - "Sets the dependency version argument to `from: VERSION`" - ]), - Option(name: "up-to-next-major", short: "u", help: [ - "Sets the dependency version argument to `.upToNextMinor(from: \"VERSION\")`" - ]), - Option(name: "exact", short: "e", help: [ - "(Default) Sets the dependency version argument to `.exact(\"VERSION\")`" - ]), - Option(name: "range", short: "r", help: [ - "Sets the dependency version argument to `VERSION`" - ]), - Option(name: "branch", short: "b", help: [ - "Sets the dependency version argument to `.branch(\"VERSION\")`" - ]), - Option(name: "revision", help: [ - "Sets the dependency version argument to `.revision(\"VERSION\")`" - ]) +public final class VersionSet: Command { + public var arguments: [CommandArgument] = [ + CommandArgument.argument(name: "name", help: ["The name of the package to change the version for"]), + CommandArgument.argument(name: "version", help: ["The version to set the specified package to. The format varies depending on the version type used"]) ] - public var help: [String] = [ - "Changes the version of a single dependency" + public var options: [CommandOption] = [ + CommandOption.flag(name: "from", short: "f", help: ["(default) Sets the dependency version argument to `from: VERSION`"]), + CommandOption.flag(name: "up-to-next-major", short: "u", help: ["Sets the dependency version argument to `.upToNextMinor(from: \"VERSION\")`"]), + CommandOption.flag(name: "exact", short: "e", help: ["Sets the dependency version argument to `.exact(\"VERSION\")`"]), + CommandOption.flag(name: "range", short: "r", help: ["Sets the dependency version argument to `VERSION`"]), + CommandOption.flag(name: "branch", short: "b", help: ["Sets the dependency version argument to `.branch(\"VERSION\")`"]), + CommandOption.flag(name: "revision", help: ["Sets the dependency version argument to `.revision(\"VERSION\")`"]), + CommandOption.flag(name: "xcode", short: "x", help: ["Regenerate the Xcode project after updating a package's version"]) ] - public let console: ConsoleProtocol + public var help: [String] = ["Changes the version of a single dependency"] - public init(console: ConsoleProtocol) { - self.console = console - } + public init() {} - public func run(arguments: [String]) throws { - let updateBar = console.loadingBar(title: "Updating Package Version") - updateBar.start() - - let package = try value("name", from: arguments) - let version = try value("version", from: arguments) - let versionLitteral = versionOption(from: arguments, with: version) - - let url = try Manifest.current.getPackageUrl(for: package) - let manifest = try NSMutableString(string: Manifest.current.get()) - let pattern = try NSRegularExpression( - pattern: "(\\,?\\n *\\.package\\(url: *\"\(url)\", *)(.*?)(\\),?\\n)", - options: [] - ) - pattern.replaceMatches(in: manifest, options: [], range: NSMakeRange(0, manifest.length), withTemplate: "$1\(versionLitteral)$3") - try Manifest.current.write(String(manifest)) + public func run(using context: CommandContext) throws -> EventLoopFuture { + let updating = context.console.loadingBar(title: "Updating Package Version") + _ = updating.start(on: context.container) - _ = try console.backgroundExecute(program: "swift", arguments: ["package", "update"]) - _ = try console.backgroundExecute(program: "swift", arguments: ["package", "resolve"]) + let package = try context.argument("name") + let version = try context.argument("version") + let versionLitteral = try self.version(from: context.options, with: version) - updateBar.finish() + guard let url = try Manifest.current.resolved().object.pins.filter({ $0.package == package }).first?.repositoryURL else { + throw EtherError(identifier: "pinNotFound", reason: "No pin entry found for package name '\(package)'") + } + guard let dependency = try Manifest.current.dependency(withURL: url) else { + throw EtherError(identifier: "packageNotFound", reason: "No package found with URL '\(url)'") + } + dependency.version = versionLitteral + try dependency.save() - if let _ = arguments.options["xcode"] { - let xcodeBar = console.loadingBar(title: "Generating Xcode Project") - xcodeBar.start() - _ = try console.backgroundExecute(program: "swift", arguments: ["package", "generate-xcodeproj"]) - xcodeBar.finish() - try console.execute(program: "/bin/sh", arguments: ["-c", "open *.xcodeproj"], input: nil, output: nil, error: nil) + _ = try Process.execute("swift", "package", "update") + updating.succeed() + + if let _ = context.options["xcode"] { + let xcodeBar = context.console.loadingBar(title: "Generating Xcode Project") + _ = xcodeBar.start(on: context.container) + _ = try Process.execute("swift", "package", "generate-xcodeproj") + xcodeBar.succeed() + _ = try Process.execute("/bin/sh", "-c", "open *.xcodeproj") } - console.output("\(package) version was updated", style: .plain, newLine: true) + return context.container.eventLoop.newSucceededFuture(result: ()) } - private func versionOption(from arguments: [String], with version: String) -> String { - if arguments.option("from") != nil { - return "from: \"\(version)\"" - } else if arguments.option("up-to-next-major") != nil { - return ".upToNextMajor(from: \"\(version)\")" - } else if arguments.option("range") != nil { - return "\"\(version.dropLast(8))\"\(String(version.dropFirst(5)).dropLast(5))\"\(version.dropFirst(8))\"" - } else if arguments.option("branch") != nil { - return ".branch(\"\(version)\")" - } else if arguments.option("revision") != nil { - return ".revision(\"\(version)\")" + private func version(from options: [String: String], with version: String)throws -> DependencyVersionType { + if options["exact"] != nil { + return .exact(version) + + } else if options["up-to-next-major"] != nil { + return .upToNextMajor(version) + + } else if options["branch"] != nil { + return .branch(version) + + } else if options["revision"] != nil { + return .revision(version) + + } else if options["range"] != nil { + let pattern = try NSRegularExpression(pattern: "(.*?)(\\.\\.(?:\\.|<))(.*)", options: []) + guard let match = pattern.firstMatch(in: version, options: [], range: version.range) else { + throw EtherError(identifier: "badVersionStructure", reason: "The '--range' flag was passed in, but the version is not structured as a range") + } + let open = version.substring(at: match.range(at: 1))! + let `operator` = version.substring(at: match.range(at: 2))! + let close = version.substring(at: match.range(at: 3))! + + return .range("\"\(open)\"\(`operator`)\"\(close)\"") } - return ".exact(\"\(version)\")" + + return .from(version) } } diff --git a/Sources/Executable/main.swift b/Sources/Executable/main.swift index 172104e..c5fdc95 100644 --- a/Sources/Executable/main.swift +++ b/Sources/Executable/main.swift @@ -20,78 +20,49 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Foundation import Console +import Vapor import Ether -import Helpers -import libc -// The current version of Ether. This string should be updated with each release. -let version = "1.10.0" -var arguments = CommandLine.arguments -let terminal = Terminal(arguments: arguments) -var iterator = arguments.makeIterator() - -guard let executable = iterator.next() else { - throw ConsoleError.noExecutable -} +let version = "2018.05.18" +let arguments = CommandLine.arguments if arguments.count == 2, arguments[1] == "--version" || arguments[1] == "-v" { + let terminal = Terminal() terminal.output("Ether Version: \(version)", style: .info, newLine: true) exit(0) } -let date = Date() -let formatter = DateFormatter() -formatter.dateFormat = "YYYY" -let currentYear = formatter.string(from: date) +var services = Services.default() + +let versions = Commands( + commands: [ + "all": VersionAll(), + "latest": VersionLatest(), + "set": VersionSet() + ], + defaultCommand: "all" +).group(help: [ + "For interacting with dependency versions" +]) + +var commands = CommandConfig() +commands.use(Configuration(), as: "config") +commands.use(FixInstall(), as: "fix-install") +commands.use(Install(), as: "install") +commands.use(New(), as: "new") +commands.use(Remove(), as: "remove") +commands.use(Search(), as: "search") +commands.use(Template(), as: "template") +commands.use(Update(), as: "update") +commands.use(versions, as: "version") + +services.register(commands) do { - let commands: [Runnable] = [ - Search(console: terminal), - Install(console: terminal), - Update(console: terminal), - Remove(console: terminal), - Template(console: terminal), - New(console: terminal), - FixInstall(console: terminal), - Group(id: "version", commands: [ - VersionLatest(console: terminal), - VersionAll(console: terminal), - VersonSet(console: terminal) - ], help: ["For interacting with dependency versions"]), - CleanManifest(console: terminal) - ] - - try terminal.run(executable: executable, commands: commands, arguments: Array(iterator), help: [ - "MIT \(currentYear) Caleb Kleveter.", - "If you are getting errors, open an issue on GitHub.", - "If you want to help, submit a PR." - ]) -} catch ConsoleError.insufficientArguments { - terminal.error("Error: ", newLine: false) - terminal.print("Insufficient arguments.") -} catch ConsoleError.help { - exit(0) -} catch ConsoleError.cancelled { - print("Cancelled") - exit(2) -} catch ConsoleError.noCommand { - terminal.error("Error: ", newLine: false) - terminal.print("No command supplied.") -} catch let ConsoleError.commandNotFound(id) { - terminal.error("Error: ", newLine: false) - terminal.print("Command \"\(id)\" not found.") -} catch let EtherError.fail(message) { - let err = "Error: " - var output = message.split(separator: "\n").map({ return String(repeating: " ", count: err.count) + $0 }) - output[0] = output[0].trim() - - terminal.error("Error: ", newLine: false) - terminal.print(output.joined(separator: "\n")) - exit(1) + try Application.asyncBoot(services: services).wait().run() } catch { - terminal.error("Error: ", newLine: false) - terminal.print("\(error)") + print("Error:", error) exit(1) } + diff --git a/Sources/Helpers/APIClient.swift b/Sources/Helpers/APIClient.swift deleted file mode 100644 index 5b67350..0000000 --- a/Sources/Helpers/APIClient.swift +++ /dev/null @@ -1,100 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2017 Caleb Kleveter -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import Foundation -import Core - -public let CKNetworkingErrorDomain = "com.caleb-kleveter.Ether.NetworkingError" -public let MissingHTTPResponseError: Int = 0 - -public typealias APIJSON = [String: AnyObject] -public typealias FetchCompletion = (APIJSON?, HTTPURLResponse?, DataTaskError?) -> Void - -public enum DataTaskError: Error { - case badStatusCode(Int) - case cannotCastToHTTPURLResponse(NSError) - case dataTaskError(Error) - case noData - case jsonSerializationError(Error) - case noJson -} - -public protocol JSONInitable { - init(json: APIJSON) -} - -public protocol APIClient { - var configuration: URLSessionConfiguration { get } - var session: URLSession { get } - - func dataTask(with request: URLRequest, endingWith completion: @escaping FetchCompletion) -> URLSessionDataTask -} - -extension APIClient { - public func dataTask(with request: URLRequest, endingWith completion: @escaping FetchCompletion) -> URLSessionDataTask { - let task = try! Portal.open({ (portal) in - let task = self.session.dataTask(with: request) { (data, response, error) in - - guard let resp = response as? HTTPURLResponse else { - let userInfo = [ - NSLocalizedDescriptionKey: NSLocalizedString("Missing HTTP Response", comment: "") - ] - - let error = NSError(domain: CKNetworkingErrorDomain, code: MissingHTTPResponseError, userInfo: userInfo) - completion(nil, nil, DataTaskError.cannotCastToHTTPURLResponse(error)) - return - } - - if resp.statusCode >= 200 && resp.statusCode < 300 { - if error == nil { - if data != nil { - do { - let json = try JSONSerialization.jsonObject(with: data!, options: []) as? APIJSON - if let json = json { - completion(json, resp, nil) - } else { - completion(nil, nil, .noJson) - return - } - } catch let error { - completion(nil, nil, .jsonSerializationError(error)) - return - } - return - } else { - completion(nil, nil, .noData) - return - } - } else { - completion(nil, nil, .dataTaskError(error!)) - return - } - } else { - completion(nil, nil, .badStatusCode(resp.statusCode)) - return - } - } - portal.close(with: task) - }) - return task - } -} diff --git a/Sources/Helpers/CommandLine.swift b/Sources/Helpers/CommandLine.swift deleted file mode 100644 index dc52c1d..0000000 --- a/Sources/Helpers/CommandLine.swift +++ /dev/null @@ -1,34 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2017 Caleb Kleveter -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import Foundation - -// ATTRIBUTION: - @rintaro | https://stackoverflow.com/questions/26971240/how-do-i-run-an-terminal-command-in-a-swift-script-e-g-xcodebuild -@discardableResult -public func shell(command: String, _ args: String...) -> Int32 { - let task = Process() - task.launchPath = command - task.arguments = args - task.launch() - task.waitUntilExit() - return task.terminationStatus -} diff --git a/Sources/Helpers/EtherError.swift b/Sources/Helpers/EtherError.swift index 89a7ac9..53660fb 100644 --- a/Sources/Helpers/EtherError.swift +++ b/Sources/Helpers/EtherError.swift @@ -20,13 +20,14 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Console +import Debugging -public enum EtherError: Error { - case fail(String) -} - -public func fail(bar: LoadingBar, with message: String) -> Error { - bar.fail() - return EtherError.fail(message) +public struct EtherError: Error, Debuggable { + public let identifier: String + public let reason: String + + public init(identifier: String, reason: String) { + self.identifier = identifier + self.reason = reason + } } diff --git a/Sources/Helpers/JSON.swift b/Sources/Helpers/JSON.swift deleted file mode 100644 index 869d69c..0000000 --- a/Sources/Helpers/JSON.swift +++ /dev/null @@ -1,29 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2017 Caleb Kleveter -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import Foundation - -public extension Data { - public func json()throws -> APIJSON? { - return try JSONSerialization.jsonObject(with: self, options: []) as? APIJSON - } -} diff --git a/Sources/Helpers/Manifest.swift b/Sources/Helpers/Manifest.swift deleted file mode 100644 index b4ff0e6..0000000 --- a/Sources/Helpers/Manifest.swift +++ /dev/null @@ -1,291 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2017 Caleb Kleveter -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import Foundation -import Console -import Bits - -public class Manifest { - public static let current = Manifest() - private let fileManager = FileManager.default - private let client = PackageJSONFetcher() - - private init() {} - - /// Gets the package manifest for the current project. - /// - /// - Returns: The manifest data. - /// - Throws: If a package manifest is not found in the current directory. - public func get()throws -> String { - guard let resolvedURL = URL(string: "file:\(fileManager.currentDirectoryPath)/Package.swift") else { - throw EtherError.fail("Unable to create URL for package manifest file.") - } - if !fileManager.fileExists(atPath: "\(fileManager.currentDirectoryPath)/Package.swift") { - throw EtherError.fail("Bad path to package manifest. Make sure you are in the project root.") - } - - return try String(contentsOf: resolvedURL) - } - - /// Rewrites the package manifest file with a string. - /// - /// - Parameter string: The string the rewrite the manifest with. - /// - Throws: Any errors that occur when createing the URL to the manifest file or in writing the manifest. - public func write(_ string: String)throws { - guard let manifestURL = URL(string: "file:\(fileManager.currentDirectoryPath)/Package.swift") else { - throw EtherError.fail("Unable to create URL for package manifest file.") - } - try string.data(using: .utf8)?.write(to: manifestURL) - } - - /// Gets the package manifest data in JSON format. - /// - /// - Parameter console: The `ConsoleProtocol` instance to use to run `swift package dump-package`. - /// - Returns: The JSON data representing the package manifest. - /// - Throws: `EtherError.fail` if the data returned from the command cannot be converted to JSON. - public func getJSON(withConsole console: ConsoleProtocol)throws -> APIJSON { - guard let json = try (console.backgroundExecute(program: "swift", arguments: ["package", "dump-package"]) as Data).json() else { - throw EtherError.fail("Unable to convert package data to JSON") - } - return json - } - - /// Gets the name of the package that has a specefied URL by reading the `Package.resolved` file data. - /// - /// - Parameter url: The URL of the package that the name is to get fetched from. - /// - Returns: The name of the package that was found. - /// - Throws: An error is thrown if either, 1) The data in the Package.resolved file is corrupted, or 2) A package does not exist with the URL passed in - public func getPackageName(`for` url: String)throws -> String { - guard let resolvedURL = URL(string: "file:\(fileManager.currentDirectoryPath)/Package.resolved") else { - throw EtherError.fail("Bad path to package data. Make sure you are in the project root.") - } - let packageData = try Data(contentsOf: resolvedURL).json() - - guard let object = packageData?["object"] as? APIJSON, - let pins = object["pins"] as? [APIJSON] else { throw EtherError.fail("Unable to read Package.resolved") } - - guard let package = try pins.filter({ (json) -> Bool in - guard let repoURL = json["repositoryURL"] as? String else { - throw EtherError.fail("Unable to read Package.resolved") - } - return repoURL == url - }).first else { - throw EtherError.fail("Unable to read Package.resolved") - } - - guard let name = package["package"] as? String else { - throw EtherError.fail("Unable to read Package.resolved") - } - - return name - } - - /// Gets the URL of the package that has a specefied name by reading the `Package.resolved` file data. - /// - /// - Parameter name: The ame of the package that the URL is to get fetched from. - /// - Returns: The URL of the package that was found. - /// - Throws: An error is thrown if either, 1) The data in the Package.resolved file is corrupted, or 2) A package does not exist with the name passed in - public func getPackageUrl(`for` name: String)throws -> String { - guard let resolvedURL = URL(string: "file:\(fileManager.currentDirectoryPath)/Package.resolved") else { - throw EtherError.fail("Bad path to package data. Make sure you are in the project root.") - } - let packageData = try Data(contentsOf: resolvedURL).json() - - guard let object = packageData?["object"] as? APIJSON, - let pins = object["pins"] as? [APIJSON] else { throw EtherError.fail("Unable to read Package.resolved") } - - guard let package = try pins.filter({ (json) -> Bool in - guard let repoURL = json["package"] as? String else { - throw EtherError.fail("Unable to read Package.resolved") - } - return repoURL == name - }).first else { - throw EtherError.fail("No package data found for name '\(name)'") - } - - guard let url = package["repositoryURL"] as? String else { - throw EtherError.fail("Unable to read repo URL for package with name '\(name)'") - } - - return url - } - - /// Gets that names of all the current projects targets. - /// - /// - Parameter packageData: The contents of the package manifest file. - /// - Returns: All the target names. - /// - Throws: Any errors that occur while creating an `NSRegularExpression` to match targets against. - public func getTargets()throws -> [String] { - guard let resolvedURL = URL(string: "file:\(fileManager.currentDirectoryPath)/Package.swift") else { - throw EtherError.fail("Bad path to package data. Make sure you are in the project root.") - } - let packageData = try String(contentsOf: resolvedURL) - - let targetPattern = try NSRegularExpression(pattern: "\\.(testT|t)arget\\(\\s*name:\\s\"(.*?)\".*?(\\)|\\])\\)", options: NSRegularExpression.Options.dotMatchesLineSeparators) - let targetMatches = targetPattern.matches(in: packageData, options: [], range: NSMakeRange(0, packageData.utf8.count)) - - let targetNames = targetMatches.map { (match) in - return targetPattern.replacementString(for: match, in: packageData, offset: 0, template: "$2") - } - - return targetNames - } - - /// Gets the pins from `Package.resolved`. - /// - /// - Returns: The projects package pins. - /// - Throws: An Ether error if a `Package.resolved` file is not found, or the JSON it contains is malformed. - public func getPins()throws -> [APIJSON] { - guard let resolvedURL = URL(string: "file:\(fileManager.currentDirectoryPath)/Package.resolved") else { - throw EtherError.fail("Bad path to package data. Make sure you are in the project root.") - } - let packageData = try Data(contentsOf: resolvedURL).json() - - guard let object = packageData?["object"] as? APIJSON, - let pins = object["pins"] as? [APIJSON] else { - throw EtherError.fail("Unable to read Package.resolved") - } - - return pins - } - - /// Removes extra comments and white space from a package manifest. - /// - /// - Throws: Errors from creating maifest URL, NSRegularExpression objects, or re-writing the maifest. - public func clean()throws { - let manifest = try self.get() - let lines = manifest.split(separator: "\n").map(String.init) - - let comment = try NSRegularExpression(pattern: " *\\/\\/ +(?!swift-tools-version).*", options: []) - let collapse = try NSRegularExpression(pattern: " *\\.(?:library|(?:testT|t)arget)\\(", options: []) - - var newManifest: [String] = [] - var currentLine = "" - var lineIndex = 0 - - while lineIndex < lines.count { - var line = lines[lineIndex] - if comment.matches(in: line, options: [], range: NSMakeRange(0, line.count)).count > 0 { - lineIndex += 1 - } else if collapse.matches(in: line, options: [], range: NSMakeRange(0, line.count)).count > 0 { - currentLine = "" - while !line.contains(")") { - if currentLine.last != nil, currentLine.last! == "(" { - currentLine.append(line.trim()) - } else if currentLine == "" { - currentLine.append(line) - } else { - currentLine.append(" " + line.trim()) - } - lineIndex += 1 - line = lines[lineIndex] - } - currentLine.append(" " + line.trim()) - lineIndex += 1 - line = lines[lineIndex] - - newManifest.append(currentLine) - } else { - newManifest.append(line) - lineIndex += 1 - } - } - - try self.write(newManifest.joined(separator: "\n")) - } - - /// Gets the URL and version of a package from the IBM package catalog API on a search URL. - /// - /// - Parameter name: The name of the package to get data for. If it contains a forward slash, the data will be fetched for the matching package, if it does not contain a forward slash, a search will be preformed and the first result will be used. - /// - Returns: The URL and version of the package found. - /// - Throws: Any errors that occur while fetching the JSON, or unwrapping the package data. - public func getPackageData(for name: String)throws -> (url: String, version: String) { - let packageUrl: String - let version: String - - if name.contains("/") { - let clientUrl = "https://packagecatalog.com/data/package/\(name)" - let json = try client.get(from: clientUrl, withParameters: [:]) - guard let ghUrl = json["ghUrl"] as? String, - let packageVersion = json["version"] as? String else { - throw EtherError.fail("Bad JSON") - } - - packageUrl = ghUrl - version = packageVersion - } else { - let clientUrl = "https://packagecatalog.com/api/search/\(name)" - let json = try client.get(from: clientUrl, withParameters: ["items": "1", "chart": "moststarred"]) - guard let data = json["data"] as? APIJSON, - let hits = data["hits"] as? APIJSON, - let results = hits["hits"] as? [APIJSON], - let source = results[0]["_source"] as? APIJSON else { - throw EtherError.fail("Bad JSON") - } - - packageUrl = String(describing: source["git_clone_url"]!) - version = String(describing: source["latest_version"]!) - } - - return (url: packageUrl, version: version) - } -} - -extension NSMutableString { - - /// Adds a package dependency to a target in a package manifest file. - /// - /// - Parameters: - /// - dependency: The name of the dependency that will be added to a target. - /// - target: The target the dependency will be added to. - /// - packageData: The contents of the package manifest file. - /// - Returns: The package manifest with the dependency added to the target. - /// - Throws: Any errors that originate when creating an `NSRegularExpression`. - public func addDependency(_ dependency: String, to target: String)throws { - let targetPattern = try NSRegularExpression(pattern: "\\.(testT|t)arget\\(\\s*name:\\s\"(.*?)\".*?(\\)|\\])\\)", options: .dotMatchesLineSeparators) - let dependenciesPattern = try NSRegularExpression(pattern: "(dependencies:\\s*\\[\\n?(\\s*).*?(\"|\\))),?\\s*\\]", options: .dotMatchesLineSeparators) - let targetMatches = targetPattern.matches(in: String(self), options: [], range: NSMakeRange(0, self.length)) - - guard let targetRange: NSRange = targetMatches.map({ (match) -> (name: String, range: NSRange) in - let name = targetPattern.replacementString(for: match, in: self as String, offset: 0, template: "$2") - let range = match.range - return (name: name, range: range) - }).filter({ (name: String, range: NSRange) -> Bool in - return name == target - }).first?.1 else { throw EtherError.fail("Attempted to add a dependency to a non-existent target") } - - dependenciesPattern.replaceMatches(in: self, options: [], range: targetRange, withTemplate: "$1, \"\(dependency)\"]") - } - - /// Removes a package dependency from all targets in a package manifest file. - /// - /// - Parameters: - /// - dependency: The name of the dependency that will be removed from all targets. - /// - Returns: The package manifest with the dependency added to the target. - /// - Throws: Any errors that originate when creating an `NSRegularExpression`. - public func removeDependency(_ dependency: String)throws { - let dependenciesPattern = try NSRegularExpression(pattern: "(dependencies: *\\[)((\\s*(\\.\\w+)?\"\\w+\",?\\s*)*)\"\(dependency)\",?\\s*((\\s*(\\.\\w+)?\"\\w+\",?\\s*)*)(\\])", options: .dotMatchesLineSeparators) - let range = NSMakeRange(0, self.length) - - dependenciesPattern.replaceMatches(in: self, options: [], range: range, withTemplate: "$1$2$5$8") - } -} diff --git a/Sources/Helpers/PackageJSONFetcher.swift b/Sources/Helpers/PackageJSONFetcher.swift deleted file mode 100644 index d817db4..0000000 --- a/Sources/Helpers/PackageJSONFetcher.swift +++ /dev/null @@ -1,78 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2017 Caleb Kleveter -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import Foundation -import Core - -public enum GetJSONError: Error { - case badURL(String) - case noJSON -} - -public final class PackageJSONFetcher: APIClient { - - public let session: URLSession - - public let configuration: URLSessionConfiguration - - public init() { - self.configuration = URLSessionConfiguration.default - self.session = URLSession(configuration: configuration) - } - - /// Gets the data from a URL in semi-asynchronusly. - /// - /// - Parameters: - /// - urlString: The URL the data will be fetched from. - /// - parameters: The URL's parameters. - /// - completion: The completion handler where either the JSON or Error can be accessed. - public func get(from urlString: String, withParameters parameters: [String: String], _ completion: @escaping (APIJSON?, Error?)->()) { - let parameterString = parameters.map({ return "\($0)=\($1)"}).joined(separator:"&") - if let url = URL(string: urlString + "?" + parameterString) { - let request = URLRequest(url: url) - do { - let (json, error) = try Portal<(APIJSON?,Error?)>.open({ (portal) in - self.dataTask(with: request, endingWith: { (json, reponse, error) in - portal.close(with: (json, error)) - }).resume() - }) - completion(json, error) - } catch {} - } else { completion(nil, GetJSONError.badURL(urlString + "?" + parameterString)) } - } - - /// Synchronously fetches data from a URL - /// - /// - Parameters: - /// - url: The URL the data will be fetched from. - /// - parameters: The paramters for the URL. - /// - Returns: The JSON is returned from the network request. - /// - Throws: Any errors that occur in the Portal or in the network request. - public func get(from url: String, withParameters parameters: [String: String])throws -> APIJSON { - let requestResult = try Portal<(APIJSON?,Error?)>.open({ (portal) in - self.get(from: url, withParameters: parameters, { (json, error) in portal.close(with: (json,error)) }) - }) - if let error = requestResult.1 { throw error } - if requestResult.0 == nil { throw GetJSONError.noJSON } - return requestResult.0! - } -} diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..f631f03 --- /dev/null +++ b/circle.yml @@ -0,0 +1,23 @@ +version: 2 + +jobs: + linux: + docker: + - image: codevapor/swift:4.1 + steps: + - checkout + - run: + name: Compile code + command: swift build + - run: + name: Run unit tests + command: swift test + + linux-release: + docker: + - image: codevapor/swift:4.1 + steps: + - checkout + - run: + name: Compile code with optimizations + command: swift build -c release