├── .gitignore ├── .swift-version ├── Examples └── 1 ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── Sources ├── PackageCatalog.swift ├── PackageGenerator.swift └── main.swift └── Tests └── fixtures └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /.build/ 2 | /Packages/ 3 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 2.2 2 | -------------------------------------------------------------------------------- /Examples/1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env cathode 2 | 3 | import Chores 4 | import Foundation 5 | 6 | extension NSBundle { 7 | func fromAppStore() -> Bool? { 8 | return appStoreReceiptURL?.checkResourceIsReachableAndReturnError(nil) 9 | } 10 | } 11 | 12 | let paths = (>["mdfind", "kMDItemCFBundleIdentifier == 'com.apple.dt.Xcode'"]).stdout.characters 13 | let xcodes = paths.split { $0 == "\n" }.map(String.init) 14 | print(xcodes.filter { !NSBundle(path: $0)!.fromAppStore()! }) 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Boris Bügling 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean build 2 | 3 | build: 4 | swift build 5 | 6 | test: build 7 | ./.build/debug/cathode Examples/1 8 | 9 | clean: 10 | swift build --clean 11 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "cathode", 5 | dependencies: [ 6 | .Package(url: "https://github.com/neonichu/Chores.git", majorVersion: 0), 7 | .Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 6), 8 | .Package(url: "https://github.com/neonichu/Decodable.git", majorVersion: 0), 9 | .Package(url: "https://github.com/neonichu/Version.git", majorVersion: 0, minor: 2), 10 | ] 11 | ) 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cathode 2 | 3 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 4 | 5 | Cathode makes it easy to run Swift scripts, by utilizing [chswift][1] to choose 6 | the right Swift version and the [Swift Package Manager][2] to install missing dependencies. 7 | 8 | If you don't know what Swift scripts even are, check out [Ayaka's talk][3]. 9 | 10 | ## Installation 11 | 12 | ``` 13 | $ brew tap neonichu/formulae 14 | $ brew install cathode 15 | ``` 16 | 17 | ## Usage 18 | 19 | Cathode is supposed to be run via a script's [hash-bang][4] directive: 20 | 21 | ```swift 22 | #!/usr/bin/env cathode 23 | 24 | import Chores 25 | 26 | let result = >["xcodebuild", "-version"] 27 | print(result.stdout) 28 | ``` 29 | 30 | Any frameworks that do not ship with the system will be installed into their own 31 | private directory under `$HOME/.🔋`, named after the script's basename. 32 | 33 | 34 | [1]: https://github.com/neonichu/chswift 35 | [2]: https://github.com/apple/swift-package-manager 36 | [3]: https://speakerdeck.com/ayanonagon/swift-scripting 37 | [4]: http://en.wikipedia.org/wiki/Shebang_(Unix) 38 | -------------------------------------------------------------------------------- /Sources/PackageCatalog.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Decodable 3 | import Version 4 | 5 | let PKG_CATALOGUE_URL = "https://swiftpkgs.ng.bluemix.net/api/packages?items=1000" 6 | 7 | struct Package { 8 | let name: String 9 | let version: Version 10 | let packageId: Int 11 | let gitUrl: NSURL 12 | 13 | func asDependency() -> String { 14 | let minor = version.minor ?? 0 15 | return ".Package(url: \"\(gitUrl)\", majorVersion: \(version.major), minor: \(minor))" 16 | } 17 | } 18 | 19 | extension Package : Decodable { 20 | static func decode(json: AnyObject) throws -> Package { 21 | let gitUrlString: String = try json => "git_clone_url" 22 | let versionString: String? = try? json => "latest_version" 23 | 24 | return Package( 25 | name: try json => "package_name", 26 | version: versionString != nil ? Version(versionString!) : Version(major: 0), 27 | packageId: try json => "package_id", 28 | gitUrl: NSURL(string: gitUrlString)! 29 | ) 30 | } 31 | } 32 | 33 | func fetchPackages(completion: [Package] -> Void) -> NSURLSessionDataTask? { 34 | let sessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration() 35 | let session = NSURLSession(configuration: sessionConfiguration) 36 | guard let url = NSURL(string: PKG_CATALOGUE_URL) else { fatalError("No URL") } 37 | let task = session.dataTaskWithURL(url) { data, response, error in 38 | guard let data = data else { fatalError("No data: \(error)") } 39 | do { 40 | let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) 41 | let packages: [Package] = try json => "data" 42 | completion(packages) 43 | } catch let error { 44 | print(error) 45 | fatalError() 46 | } 47 | } 48 | task.resume() 49 | return task 50 | } 51 | -------------------------------------------------------------------------------- /Sources/PackageGenerator.swift: -------------------------------------------------------------------------------- 1 | import Chores 2 | import Foundation 3 | import PathKit 4 | 5 | extension String { 6 | private func split(char: Character) -> [String] { 7 | return self.characters.split { $0 == char }.map(String.init) 8 | } 9 | 10 | var lines: [String] { 11 | return split("\n") 12 | } 13 | 14 | var words: [String] { 15 | return split(" ") 16 | } 17 | } 18 | 19 | func currentSDK() -> Path { 20 | let result = >["xcrun", "--sdk", "macosx", "--show-sdk-platform-path"] 21 | let platform = Path(result.stdout) 22 | let sdks = try? (platform + "Developer/SDKs").children() 23 | return sdks?.first ?? Path.current 24 | } 25 | 26 | func frameworksPath() -> Path { 27 | let result = >["xcrun", "--show-sdk-path"] 28 | let sdkPath = Path(result.stdout) 29 | return (sdkPath + "../../Library/Frameworks").normalize() 30 | } 31 | 32 | func systemFrameworks() -> [String] { 33 | let systemFrameworksPath = currentSDK() + "System/Library/Frameworks" 34 | var frameworks = (try? systemFrameworksPath.children()) ?? [Path]() 35 | 36 | if let moarFrameworks = try? frameworksPath().children() { 37 | frameworks += moarFrameworks 38 | } 39 | 40 | return frameworks.map { $0.lastComponentWithoutExtension } 41 | } 42 | 43 | func generatePackage(path: Path, _ packages: [Package]) -> String { 44 | var packageMap = [String:Package]() 45 | packages.forEach { 46 | packageMap[$0.name] = $0 47 | } 48 | 49 | guard let fileContents = try? NSString(contentsOfFile: path.description, usedEncoding: nil) else { return "" } 50 | let frameworksToFilter = systemFrameworks() 51 | let frameworks = (fileContents as String).lines.filter { $0.hasPrefix("import") } 52 | .flatMap { $0.words.last } 53 | .filter { !frameworksToFilter.contains($0) } 54 | let dependencies = frameworks.flatMap { packageMap[$0]?.asDependency() } 55 | 56 | return "import PackageDescription\n\n" + 57 | "_ = Package(dependencies: [\n" + dependencies.joinWithSeparator(",\n") + 58 | "\n])" 59 | } 60 | -------------------------------------------------------------------------------- /Sources/main.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Chores 3 | import PathKit 4 | 5 | let args = Process.arguments 6 | 7 | if args.count < 2 { 8 | print("Usage: \(args.first!) SWIFT-FILE") 9 | exit(1) 10 | } 11 | 12 | let filePath = Path(args.last!) 13 | let basename = filePath.lastComponent 14 | 15 | let run = { (packages: [Package]) throws in 16 | let moduleName = "_\(basename)" // FIXME: Create proper C99 identifier 17 | let packageDirectory = Path.home + ".🔋" + moduleName 18 | 19 | let sourcesPath = packageDirectory + "Sources" 20 | try sourcesPath.mkpath() 21 | 22 | let mainSwiftPath = sourcesPath + "main.swift" 23 | _ = try? mainSwiftPath.delete() 24 | try filePath.copy(mainSwiftPath) 25 | 26 | let manifest = generatePackage(filePath, packages) 27 | try manifest.writeToFile((packageDirectory + "Package.swift").description, atomically: true, encoding: NSUTF8StringEncoding) 28 | 29 | var buildCommand = ["swift", "build", "--configuration", "release"] 30 | 31 | var result = >["which", "chswift"] 32 | if result.result == 0 { 33 | // TODO: Make Swift version selectable 34 | buildCommand = ["chswift-exec", "2.2"] + buildCommand 35 | } 36 | 37 | packageDirectory.chdir { 38 | result = >buildCommand 39 | if result.result != 0 { 40 | print(result.stderr) 41 | exit(1) 42 | } 43 | 44 | result = >[".build/release/\(moduleName)"] 45 | print(result.stdout) 46 | exit(result.result) 47 | } 48 | } 49 | 50 | NSApplicationLoad() 51 | 52 | fetchPackages { 53 | do { 54 | try run($0) 55 | exit(0) 56 | } catch let error { 57 | print(error) 58 | exit(1) 59 | } 60 | } 61 | 62 | NSApp.run() 63 | -------------------------------------------------------------------------------- /Tests/fixtures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "package_id": 21520325, 3 | "package_full_name": "kylef/Commander", 4 | "package_name": "Commander", 5 | "description": "Compose beautiful command line interfaces in Swift", 6 | "language": "Swift", 7 | "forks_count": 17, 8 | "stargazers_count": 451, 9 | "api_url": "https://api.github.com/repos/kylef/Commander", 10 | "html_url": "https://github.com/kylef/Commander", 11 | "git_clone_url": "https://github.com/kylef/Commander.git", 12 | "license_file": "Copyright (c) 2015, Kyle Fuller\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n* Neither the name of the {organization} nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.", 13 | "read_me_file": "\"Commander\"\n\n# Commander\n\n[![Build Status](https://img.shields.io/travis/kylef/Commander/master.svg?style=flat)](https://travis-ci.org/kylef/Commander)\n\nCommander is a small Swift framework allowing you to craft beautiful command\nline interfaces in a composable way.\n\n## Usage\n\n##### Simple Hello World\n\n```swift\nimport Commander\n\nlet main = command {\n print(\"Hello World\")\n}\n\nmain.run()\n```\n\n##### Type-safe argument handling\n\nThe closure passed to the command function takes any arguments that\nconform to `ArgumentConvertible`, Commander will automatically convert the\narguments to these types. If they can't be converted the user will receive a\nnice error message informing them that their argument doesn't match the\nexpected type.\n\n`String`, `Int`, `Double`, and `Float` are extended to conform to\n`ArgumentConvertible`, you can easily extend any other class or structure\nso you can use it as an argument to your command.\n\n```swift\ncommand { (hostname:String, port:Int) in\n print(\"Connecting to \\(hostname) on port \\(port)...\")\n}\n```\n\n##### Grouping commands\n\nYou can group a collection of commands together.\n\n```swift\nGroup {\n $0.command(\"login\") { (name:String) in\n print(\"Hello \\(name)\")\n }\n\n $0.command(\"logout\") {\n print(\"Goodbye.\")\n }\n}\n```\n\nUsage:\n\n```shell\n$ auth\nUsage:\n\n $ auth COMMAND\n\nCommands:\n\n + login\n + logout\n\n$ auth login Kyle\nHello Kyle\n$ auth logout\nGoodbye.\n```\n\n#### Describing arguments\n\nYou can describe arguments and options for a command to auto-generate help,\nthis is done by passing in descriptors of these arguments.\n\nFor example, to describe a command which takes two options, `--name` and\n`--count` where the default value for name is `world` and the default value for\ncount is `1`.\n\n```swift\ncommand(\n Option(\"name\", \"world\"),\n Option(\"count\", 1, description: \"The number of times to print.\")\n) { name, count in\n for _ in 0..