diff --git a/Package.resolved b/Package.resolved index 9e562b2..a8ee643 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,17 +6,8 @@ "repositoryURL": "https://github.com/PromiseKit/Foundation.git", "state": { "branch": null, - "revision": "0a59b0d89f469979aecacdf9046df14f99d1cc6b", - "version": "3.3.2" - } - }, - { - "package": "Guaka", - "repositoryURL": "https://github.com/nsomar/Guaka.git", - "state": { - "branch": null, - "revision": "6fb29b2378166a30d72120980e1c099c664598de", - "version": "0.4.1" + "revision": "1a276e598dac59489ed904887e0740fa75e571e0", + "version": "3.3.4" } }, { @@ -24,8 +15,8 @@ "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git", "state": { "branch": null, - "revision": "0046db4ab8fd7be31a59e57912dbb96639ad8ce9", - "version": "3.2.0" + "revision": "8d33ffd6f74b3bcfc99af759d4204c6395a3f918", + "version": "3.2.1" } }, { @@ -33,8 +24,8 @@ "repositoryURL": "https://github.com/mxcl/LegibleError.git", "state": { "branch": null, - "revision": "c615c01e461e8a3495ba4ea75f5d671c76820105", - "version": "1.0.1" + "revision": "909e9bab3ded97350b28a5ab41dd745dd8aa9710", + "version": "1.0.4" } }, { @@ -42,8 +33,8 @@ "repositoryURL": "https://github.com/mxcl/Path.swift.git", "state": { "branch": null, - "revision": "f324b4a562ca421fd2905414a10fb0fc91d58b67", - "version": "0.16.2" + "revision": "dac007e907a4f4c565cfdc55a9ce148a761a11d5", + "version": "0.16.3" } }, { @@ -51,17 +42,17 @@ "repositoryURL": "https://github.com/mxcl/PromiseKit.git", "state": { "branch": null, - "revision": "fe1e9c5b62465227cceb7b0e6e79489ba7b824af", - "version": "6.8.4" + "revision": "1c296a8637838901d2b01e4c46875ee749506133", + "version": "6.8.5" } }, { - "package": "StringScanner", - "repositoryURL": "https://github.com/getGuaka/StringScanner.git", + "package": "swift-argument-parser", + "repositoryURL": "https://github.com/apple/swift-argument-parser", "state": { "branch": null, - "revision": "de1685ad202cb586d626ed52d6de904dd34189f3", - "version": "0.4.1" + "revision": "92646c0cdbaca076c8d3d0207891785b3379cbff", + "version": "0.3.1" } }, { diff --git a/Package.swift b/Package.swift index c4f7bc1..d7b446c 100644 --- a/Package.swift +++ b/Package.swift @@ -12,7 +12,7 @@ let package = Package( .library(name: "AppleAPI", targets: ["AppleAPI"]), ], dependencies: [ - .package(url: "https://github.com/nsomar/Guaka.git", .upToNextMinor(from: "0.4.0")), + .package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "0.3.0")), .package(url: "https://github.com/mxcl/Path.swift.git", .upToNextMinor(from: "0.16.0")), .package(url: "https://github.com/mxcl/Version.git", .upToNextMinor(from: "1.0.3")), .package(url: "https://github.com/mxcl/PromiseKit.git", .upToNextMinor(from: "6.8.3")), @@ -25,7 +25,7 @@ let package = Package( .target( name: "xcodes", dependencies: [ - "Guaka", "XcodesKit" + .product(name: "ArgumentParser", package: "swift-argument-parser"), "XcodesKit" ]), .testTarget( name: "xcodesTests", diff --git a/README.md b/README.md index a602d69..312534d 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,21 @@ Xcode will be installed to /Applications by default, but you can provide the pat - `update`: Update the list of available versions of Xcode - `version`: Print the version number of xcodes itself +### Shell Completion Scripts + +xcodes can generate completion scripts which allow you to press the tab key on your keyboard to autocomplete commands and arguments when typing an xcodes command. The steps to install a completion script depend on the shell that you use. More information about installation instructions for different shells and the underlying implementation is available in the [swift-argument-parser repo](https://github.com/apple/swift-argument-parser/blob/master/Documentation/07%20Completion%20Scripts.md). + +
+Zsh, with oh-my-zsh: + +Run the following commands: + +```sh +mkdir ~/.oh-my-zsh/completions +xcodes --generate-completion-script > ~/.oh-my-zsh/completions/_xcodes +``` +
+ ## Development You'll need Xcode 12 in order to build and run xcodes. diff --git a/Sources/xcodes/ParsableArguments+LegibleError.swift b/Sources/xcodes/ParsableArguments+LegibleError.swift new file mode 100644 index 0000000..e4a85d2 --- /dev/null +++ b/Sources/xcodes/ParsableArguments+LegibleError.swift @@ -0,0 +1,10 @@ +import ArgumentParser +import LegibleError +import XcodesKit + +extension ParsableArguments { + static func exit(withLegibleError error: Error) -> Never { + Current.logging.log(error.legibleLocalizedDescription) + Self.exit(withError: ExitCode.failure) + } +} diff --git a/Sources/xcodes/main.swift b/Sources/xcodes/main.swift index 6d3b48e..bc5fcd0 100644 --- a/Sources/xcodes/main.swift +++ b/Sources/xcodes/main.swift @@ -1,28 +1,15 @@ import Foundation -import Guaka +import ArgumentParser import Version import PromiseKit import XcodesKit import LegibleError import Path -var configuration = Configuration() -try? configuration.load() -let xcodeList = XcodeList() -let installer = XcodeInstaller(configuration: configuration, xcodeList: xcodeList) - -migrateApplicationSupportFiles() - -let globalDirectoryFlag = Flag( - longName: "directory", - type: String.self, - description: "The directory where your Xcodes are installed. Defaults to /Applications.", - inheritable: true -) -func getDirectory(flags: Flags) -> Path { - let directory = flags.getString(name: "directory").flatMap(Path.init) ?? - ProcessInfo.processInfo.environment["XCODES_DIRECTORY"].flatMap(Path.init) ?? - Path.root.join("Applications") +func getDirectory(possibleDirectory: String?, default: Path = Path.root.join("Applications")) -> Path { + let directory = possibleDirectory.flatMap(Path.init) ?? + ProcessInfo.processInfo.environment["XCODES_DIRECTORY"].flatMap(Path.init) ?? + `default` guard directory.isDirectory else { Current.logging.log("Directory argument must be a directory, but was provided \(directory.string).") exit(1) @@ -30,235 +17,329 @@ func getDirectory(flags: Flags) -> Path { return directory } -// This is awkward, but Guaka wants a root command in order to add subcommands, -// but then seems to want it to behave like a normal command even though it'll only ever print the help. -// But it doesn't even print the help without the user providing the --help flag, -// so we need to tell it to do this explicitly -var app: Command! -app = Command(usage: "xcodes", flags: [globalDirectoryFlag]) { _, _ in print(GuakaConfig.helpGenerator.init(command: app).helpMessage) } - -let installed = Command(usage: "installed", - shortMessage: "List the versions of Xcode that are installed") { flags, _ in - let directory = getDirectory(flags: flags) - installer.printInstalledXcodes(directory: directory) - .done { - exit(0) - } - .catch { error in - print(error.legibleLocalizedDescription) - exit(1) - } - - RunLoop.current.run() +struct GlobalDirectoryOption: ParsableArguments { + @Option(help: "The directory where your Xcodes are installed. Defaults to /Applications.", + completion: .directory) + var directory: String? } -app.add(subCommand: installed) - -let printFlag = Flag(shortName: "p", longName: "print-path", value: false, description: "Print the path of the selected Xcode") -let select = Command(usage: "select ", - shortMessage: "Change the selected Xcode", - longMessage: "Change the selected Xcode. Run without any arguments to interactively select from a list, or provide an absolute path.", - flags: [printFlag], - example: """ - xcodes select - xcodes select 11.4.0 - xcodes select /Applications/Xcode-11.4.0.app - xcodes select -p - """) { flags, args in - let directory = getDirectory(flags: flags) - - selectXcode(shouldPrint: flags.getBool(name: "print-path") ?? false, pathOrVersion: args.joined(separator: " "), directory: directory) - .catch { error in - print(error.legibleLocalizedDescription) - exit(1) - } - RunLoop.current.run() -} -app.add(subCommand: select) - -let list = Command(usage: "list", - shortMessage: "List all versions of Xcode that are available to install") { flags, _ in - let directory = getDirectory(flags: flags) +struct Xcodes: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Manage the Xcodes installed on your Mac", + shouldDisplay: true, + subcommands: [Download.self, Install.self, Installed.self, List.self, Select.self, Uninstall.self, Update.self, Version.self] + ) - firstly { () -> Promise in - if xcodeList.shouldUpdate { - return installer.updateAndPrint(directory: directory) - } - else { - return installer.printAvailableXcodes(xcodeList.availableXcodes, installed: Current.files.installedXcodes(directory)) - } + static var xcodesConfiguration = Configuration() + static let xcodeList = XcodeList() + static var installer: XcodeInstaller! + + static func main() { + try? xcodesConfiguration.load() + installer = XcodeInstaller(configuration: xcodesConfiguration, xcodeList: xcodeList) + migrateApplicationSupportFiles() + + self.main(nil) } - .done { - exit(0) + + struct Download: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Download a specific version of Xcode", + discussion: """ + By default, xcodes will use a URLSession to download the specified version. If aria2 (https://aria2.github.io, available in Homebrew) is installed, either somewhere in PATH or at the path specified by the --aria2 flag, then it will be used instead. aria2 will use up to 16 connections to download Xcode 3-5x faster. If you have aria2 installed and would prefer to not use it, you can use the --no-aria2 flag. + + EXAMPLES: + xcodes download 10.2.1 + xcodes download 11 Beta 7 + xcodes download 11.2 GM seed + xcodes download 9.0 --directory ~/Archive + xcodes download --latest-prerelease + """ + ) + + @Argument(help: "The version to download", + completion: .custom { args in xcodeList.availableXcodes.sorted { $0.version < $1.version }.map { $0.version.xcodeDescription } }) + var version: [String] = [] + + @Flag(help: "Update and then download the latest non-prerelease version available.") + var latest: Bool = false + + @Flag(help: "Update and then download the latest prerelease version available, including GM seeds and GMs.") + var latestPrerelease = false + + @Option(help: "The path to an aria2 executable. Searches $PATH by default.", + completion: .file()) + var aria2: String? + + @Flag(help: "Don't use aria2 to download Xcode, even if its available.") + var noAria2: Bool = false + + @Option(help: "The directory to download Xcode to. Defaults to ~/Downloads.", + completion: .directory) + var directory: String? + + func run() { + let versionString = version.joined(separator: " ") + + let installation: XcodeInstaller.InstallationType + // Deliberately not using InstallationType.path here as it doesn't make sense to download an Xcode from a .xip that's already on disk + if latest { + installation = .latest + } else if latestPrerelease { + installation = .latestPrerelease + } else { + installation = .version(versionString) + } + + var downloader = XcodeInstaller.Downloader.urlSession + if let aria2Path = aria2.flatMap(Path.init) ?? Current.shell.findExecutable("aria2c"), + aria2Path.exists, + noAria2 == false { + downloader = .aria2(aria2Path) + } + + let destination = getDirectory(possibleDirectory: directory, default: Path.home.join("Downloads")) + + installer.download(installation, downloader: downloader, destinationDirectory: destination) + .catch { error in + switch error { + case Process.PMKError.execution(let process, let standardOutput, let standardError): + Current.logging.log(""" + Failed executing: `\(process)` (\(process.terminationStatus)) + \([standardOutput, standardError].compactMap { $0 }.joined(separator: "\n")) + """) + default: + Current.logging.log(error.legibleLocalizedDescription) + } + + Install.exit(withError: ExitCode.failure) + } + + RunLoop.current.run() + } } - .catch { error in - print(error.legibleLocalizedDescription) - exit(1) + + struct Install: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Download and install a specific version of Xcode", + discussion: """ + By default, xcodes will use a URLSession to download the specified version. If aria2 (https://aria2.github.io, available in Homebrew) is installed, either somewhere in PATH or at the path specified by the --aria2 flag, then it will be used instead. aria2 will use up to 16 connections to download Xcode 3-5x faster. If you have aria2 installed and would prefer to not use it, you can use the --no-aria2 flag. + + EXAMPLES: + xcodes install 10.2.1 + xcodes install 11 Beta 7 + xcodes install 11.2 GM seed + xcodes install 9.0 --path ~/Archive/Xcode_9.xip + xcodes install --latest-prerelease + xcodes install --latest --directory "/Volumes/Bag Of Holding/" + """ + ) + + @Argument(help: "The version to install", + completion: .custom { args in xcodeList.availableXcodes.sorted { $0.version < $1.version }.map { $0.version.xcodeDescription } }) + var version: [String] = [] + + @Option(name: .customLong("path"), + help: "Local path to Xcode .xip", + completion: .file(extensions: ["xip"])) + var pathString: String? + + @Flag(help: "Update and then install the latest non-prerelease version available.") + var latest: Bool = false + + @Flag(help: "Update and then install the latest prerelease version available, including GM seeds and GMs.") + var latestPrerelease = false + + @Option(help: "The path to an aria2 executable. Searches $PATH by default.", + completion: .file()) + var aria2: String? + + @Flag(help: "Don't use aria2 to download Xcode, even if its available.") + var noAria2: Bool = false + + @Option(help: "The directory to install Xcode into. Defaults to /Applications.", + completion: .directory) + var directory: String? + + func run() { + let versionString = version.joined(separator: " ") + + let installation: XcodeInstaller.InstallationType + if latest { + installation = .latest + } else if latestPrerelease { + installation = .latestPrerelease + } else if let pathString = pathString, let path = Path(pathString) { + installation = .path(versionString, path) + } else { + installation = .version(versionString) + } + + var downloader = XcodeInstaller.Downloader.urlSession + if let aria2Path = aria2.flatMap(Path.init) ?? Current.shell.findExecutable("aria2c"), + aria2Path.exists, + noAria2 == false { + downloader = .aria2(aria2Path) + } + + let destination = getDirectory(possibleDirectory: directory) + + installer.install(installation, downloader: downloader, destination: destination) + .done { Install.exit() } + .catch { error in + switch error { + case Process.PMKError.execution(let process, let standardOutput, let standardError): + Current.logging.log(""" + Failed executing: `\(process)` (\(process.terminationStatus)) + \([standardOutput, standardError].compactMap { $0 }.joined(separator: "\n")) + """) + default: + Current.logging.log(error.legibleLocalizedDescription) + } + + Install.exit(withError: ExitCode.failure) + } + + RunLoop.current.run() + } } - - RunLoop.current.run() -} -app.add(subCommand: list) - -let update = Command(usage: "update", - shortMessage: "Update the list of available versions of Xcode") { flags, _ in - let directory = getDirectory(flags: flags) - - firstly { - installer.updateAndPrint(directory: directory) + + struct Installed: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "List the versions of Xcode that are installed" + ) + + @OptionGroup + var globalDirectory: GlobalDirectoryOption + + func run() { + let directory = getDirectory(possibleDirectory: globalDirectory.directory) + + installer.printInstalledXcodes(directory: directory) + .done { Installed.exit() } + .catch { error in Installed.exit(withLegibleError: error) } + + RunLoop.current.run() + } } - .catch { error in - print(error.legibleLocalizedDescription) - exit(1) + + struct List: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "List all versions of Xcode that are available to install" + ) + + @OptionGroup + var globalDirectory: GlobalDirectoryOption + + func run() { + let directory = getDirectory(possibleDirectory: globalDirectory.directory) + + firstly { () -> Promise in + if xcodeList.shouldUpdate { + return installer.updateAndPrint(directory: directory) + } + else { + return installer.printAvailableXcodes(xcodeList.availableXcodes, installed: Current.files.installedXcodes(directory)) + } + } + .done { List.exit() } + .catch { error in List.exit(withLegibleError: ExitCode.failure) } + + RunLoop.current.run() + } } - - RunLoop.current.run() -} -app.add(subCommand: update) - -let installPathFlag = Flag(longName: "path", type: String.self, description: "Local path to Xcode .xip") -let installLatestFlag = Flag(longName: "latest", value: false, description: "Update and then install the latest non-prerelease version available.") -let installLatestPrereleaseFlag = Flag(longName: "latest-prerelease", value: false, description: "Update and then install the latest prerelease version available, including GM seeds and GMs.") -let directory = Flag(longName: "directory", type: String.self, description: "The directory to install Xcode into. Defaults to /Applications.") -let aria2 = Flag(longName: "aria2", type: String.self, description: "The path to an aria2 executable. Defaults to /usr/local/bin/aria2c.") -let noAria2 = Flag(longName: "no-aria2", value: false, description: "Don't use aria2 to download Xcode, even if its available.") - -let install = Command(usage: "install ", - shortMessage: "Download and install a specific version of Xcode", - longMessage: """ - Download and install a specific version of Xcode - - By default, xcodes will use a URLSession to download the specified version. If aria2 (https://aria2.github.io, available in Homebrew) is installed, either somewhere in PATH or at the path specified by the --aria2 flag, then it will be used instead. aria2 will use up to 16 connections to download Xcode 3-5x faster. If you have aria2 installed and would prefer to not use it, you can use the --no-aria2 flag. - """, - flags: [installPathFlag, installLatestFlag, installLatestPrereleaseFlag, directory, aria2, noAria2], - example: """ - xcodes install 10.2.1 - xcodes install 11 Beta 7 - xcodes install 11.2 GM seed - xcodes install 9.0 --path ~/Archive/Xcode_9.xip - xcodes install --latest-prerelease - xcodes install --latest --directory "/Volumes/Bag Of Holding/" - """) { flags, args in - let versionString = args.joined(separator: " ") - - let installation: XcodeInstaller.InstallationType - if flags.getBool(name: "latest") == true { - installation = .latest - } else if flags.getBool(name: "latest-prerelease") == true { - installation = .latestPrerelease - } else if let pathString = flags.getString(name: "path"), let path = Path(pathString) { - installation = .path(versionString, path) - } else { - installation = .version(versionString) + + struct Select: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Change the selected Xcode", + discussion: """ + Run without any arguments to interactively select from a list, or provide an absolute path. + + EXAMPLES: + xcodes select + xcodes select 11.4.0 + xcodes select /Applications/Xcode-11.4.0.app + xcodes select -p + """ + ) + + @ArgumentParser.Flag(name: [.customShort("p"), .customLong("print-path")], help: "Print the path of the selected Xcode") + var print: Bool = false + + @Argument(help: "Version or path", + completion: .custom { _ in Current.files.installedXcodes(getDirectory(possibleDirectory: nil)).sorted { $0.version < $1.version }.map { $0.version.xcodeDescription } }) + var versionOrPath: [String] = [] + + @OptionGroup + var globalDirectory: GlobalDirectoryOption + + func run() { + let directory = getDirectory(possibleDirectory: globalDirectory.directory) + + selectXcode(shouldPrint: print, pathOrVersion: versionOrPath.joined(separator: " "), directory: directory) + .done { Select.exit() } + .catch { error in Select.exit(withLegibleError: error) } + + RunLoop.current.run() + } } - var downloader = XcodeInstaller.Downloader.urlSession - if let aria2Path = flags.getString(name: "aria2").flatMap(Path.init) ?? Current.shell.findExecutable("aria2c"), - aria2Path.exists, - flags.getBool(name: "no-aria2") != true { - downloader = .aria2(aria2Path) + struct Uninstall: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Uninstall a specific version of Xcode", + discussion: """ + EXAMPLES: + xcodes uninstall 10.2.1 + """ + ) + + @Argument(help: "The version to uninstall", + completion: .custom { _ in Current.files.installedXcodes(getDirectory(possibleDirectory: nil)).sorted { $0.version < $1.version }.map { $0.version.xcodeDescription } }) + var version: [String] = [] + + @OptionGroup + var globalDirectory: GlobalDirectoryOption + + func run() { + let directory = getDirectory(possibleDirectory: globalDirectory.directory) + + installer.uninstallXcode(version.joined(separator: " "), directory: directory) + .done { Uninstall.exit() } + .catch { error in Uninstall.exit(withLegibleError: ExitCode.failure) } + + RunLoop.current.run() + } } - - let destination = getDirectory(flags: flags) - - installer.install(installation, downloader: downloader, destination: destination) - .catch { error in - switch error { - case Process.PMKError.execution(let process, let standardOutput, let standardError): - Current.logging.log(""" - Failed executing: `\(process)` (\(process.terminationStatus)) - \([standardOutput, standardError].compactMap { $0 }.joined(separator: "\n")) - """) - default: - Current.logging.log(error.legibleLocalizedDescription) - } - - exit(1) + + struct Update: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Update the list of available versions of Xcode" + ) + + @OptionGroup + var globalDirectory: GlobalDirectoryOption + + func run() { + let directory = getDirectory(possibleDirectory: globalDirectory.directory) + + installer.updateAndPrint(directory: directory) + .done { Update.exit() } + .catch { error in Update.exit(withLegibleError: error) } + + RunLoop.current.run() } - - RunLoop.current.run() -} -app.add(subCommand: install) - -let downloadDirectoryFlag = Flag(longName: "directory", type: String.self, description: "Directory to download .xip to. Defaults to ~/Downloads.") -let downloadLatestFlag = Flag(longName: "latest", value: false, description: "Update and then download the latest non-prerelease version available.") -let downloadLatestPrereleaseFlag = Flag(longName: "latest-prerelease", value: false, description: "Update and then download the latest prerelease version available, including GM seeds and GMs.") -let download = Command(usage: "download ", - shortMessage: "Download a specific version of Xcode", - longMessage: """ - Download a specific version of Xcode - - By default, xcodes will use a URLSession to download the specified version. If aria2 (https://aria2.github.io, available in Homebrew) is installed, either somewhere in PATH or at the path specified by the --aria2 flag, then it will be used instead. aria2 will use up to 16 connections to download Xcode 3-5x faster. If you have aria2 installed and would prefer to not use it, you can use the --no-aria2 flag. - """, - flags: [downloadDirectoryFlag, downloadLatestFlag, downloadLatestPrereleaseFlag, aria2, noAria2], - example: """ - xcodes download 10.2.1 - xcodes download 11 Beta 7 - xcodes download 11.2 GM seed - xcodes download 9.0 --directory ~/Archive - xcodes download --latest-prerelease - """) { flags, args in - let versionString = args.joined(separator: " ") - - let installation: XcodeInstaller.InstallationType - // Deliberately not using InstallationType.path here as it doesn't make sense to download an Xcode from a .xip that's already on disk - if flags.getBool(name: "latest") == true { - installation = .latest - } else if flags.getBool(name: "latest-prerelease") == true { - installation = .latestPrerelease - } else { - installation = .version(versionString) - } - - var downloader = XcodeInstaller.Downloader.urlSession - if let aria2Path = flags.getString(name: "aria2").flatMap(Path.init) ?? Current.shell.findExecutable("aria2c"), - aria2Path.exists, - flags.getBool(name: "no-aria2") != true { - downloader = .aria2(aria2Path) - } - - let directory = flags.getString(name: "directory").flatMap(Path.init) - installer.download(installation, downloader: downloader, destinationDirectory: directory ?? Path.home.join("Downloads")) - .catch { error in - switch error { - case Process.PMKError.execution(let process, let standardOutput, let standardError): - Current.logging.log(""" - Failed executing: `\(process)` (\(process.terminationStatus)) - \([standardOutput, standardError].compactMap { $0 }.joined(separator: "\n")) - """) - default: - Current.logging.log(error.legibleLocalizedDescription) - } - - exit(1) - } - - RunLoop.current.run() -} -app.add(subCommand: download) - -let uninstall = Command(usage: "uninstall ", - shortMessage: "Uninstall a specific version of Xcode", - example: "xcodes uninstall 10.2.1") { flags, args in - let versionString = args.joined(separator: " ") - - let directory = getDirectory(flags: flags) - - installer.uninstallXcode(versionString, directory: directory) - .catch { error in - print(error.legibleLocalizedDescription) - exit(1) + } + + struct Version: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Print the version number of xcodes itself" + ) + + func run() { + Current.logging.log(XcodesKit.version.description) } - - RunLoop.current.run() -} -app.add(subCommand: uninstall) - -let version = Command(usage: "version", - shortMessage: "Print the version number of xcodes itself") { _, _ in - print(XcodesKit.version) - exit(0) + } } -app.add(subCommand: version) -app.execute() +// @main doesn't work yet because of https://bugs.swift.org/browse/SR-12683 +Xcodes.main()