diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..856974db --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,366 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + activesupport (7.0.8.4) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + algoliasearch (1.27.5) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) + arkana (2.1.1) + dotenv (~> 2.7) + rainbow (~> 3.1.1) + yaml (~> 0.2) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.3.0) + aws-partitions (1.953.0) + aws-sdk-core (3.201.1) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.8) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.88.0) + aws-sdk-core (~> 3, >= 3.201.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.156.0) + aws-sdk-core (~> 3, >= 3.201.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.8.0) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.2.0) + claide (1.1.0) + claide-plugins (0.9.2) + cork + nap + open4 (~> 1.3) + cocoapods (1.15.2) + addressable (~> 2.8) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.15.2) + cocoapods-deintegrate (>= 1.0.3, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.6.0, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored2 (~> 3.1) + escape (~> 0.0.4) + fourflusher (>= 2.3.0, < 3.0) + gh_inspector (~> 1.0) + molinillo (~> 0.8.0) + nap (~> 1.0) + ruby-macho (>= 2.3.0, < 3.0) + xcodeproj (>= 1.23.0, < 2.0) + cocoapods-core (1.15.2) + activesupport (>= 5.0, < 8) + addressable (~> 2.8) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + netrc (~> 0.11) + public_suffix (~> 4.0) + typhoeus (~> 1.0) + cocoapods-deintegrate (1.0.5) + cocoapods-downloader (2.1) + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.1) + cocoapods-trunk (1.6.0) + nap (>= 0.8, < 2.0) + netrc (~> 0.11) + cocoapods-try (1.2.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + concurrent-ruby (1.3.3) + cork (0.3.0) + colored2 (~> 3.1) + danger (9.4.3) + claide (~> 1.0) + claide-plugins (>= 0.9.2) + colored2 (~> 3.1) + cork (~> 0.1) + faraday (>= 0.9.0, < 3.0) + faraday-http-cache (~> 2.0) + git (~> 1.13) + kramdown (~> 2.3) + kramdown-parser-gfm (~> 1.0) + no_proxy_fix + octokit (>= 4.0) + terminal-table (>= 1, < 4) + danger-plugin-api (1.0.0) + danger (> 2.0) + danger-swiftformat (0.9.0) + danger-plugin-api (~> 1.0) + danger-swiftlint (0.36.1) + danger + rake (> 10) + thor (~> 1.0.0) + danger-xcode_summary (1.3.0) + danger-plugin-api (~> 1.0) + xcresult (~> 0.2) + danger-xcov (0.5.0) + danger (>= 2.1) + xcov (>= 1.7.3) + declarative (0.0.20) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + escape (0.0.4) + ethon (0.16.0) + ffi (>= 1.15.0) + excon (0.111.0) + faraday (1.10.3) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-http-cache (2.5.1) + faraday (>= 0.8) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) + faraday (~> 1.0) + fastimage (2.3.1) + fastlane (2.221.1) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored (~> 1.2) + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (>= 0.1.1, < 1.0.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-plugin-firebase_app_distribution (0.9.1) + google-apis-firebaseappdistribution_v1 (~> 0.3.0) + google-apis-firebaseappdistribution_v1alpha (~> 0.2.0) + ffi (1.17.0-arm64-darwin) + fourflusher (2.3.1) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + git (1.19.1) + addressable (~> 2.8) + rchardet (~> 1.8) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-firebaseappdistribution_v1 (0.3.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-firebaseappdistribution_v1alpha (0.2.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.7.0) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.6) + domain_name (~> 0.5) + httpclient (2.8.3) + i18n (1.14.5) + concurrent-ruby (~> 1.0) + jmespath (1.6.2) + json (2.7.2) + jwt (2.8.2) + base64 + kramdown (2.4.0) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + minitest (5.24.1) + molinillo (0.8.0) + multi_json (1.15.0) + multipart-post (2.4.1) + nanaimo (0.3.0) + nap (1.1.0) + naturally (2.2.1) + netrc (0.11.0) + nkf (0.2.0) + no_proxy_fix (0.1.2) + octokit (9.1.0) + faraday (>= 1, < 3) + sawyer (~> 0.9) + open4 (1.3.4) + optparse (0.5.0) + os (1.1.4) + plist (3.7.1) + public_suffix (4.0.7) + rainbow (3.1.1) + rake (13.2.1) + rchardet (1.8.0) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.9) + strscan + rouge (2.0.7) + ruby-macho (2.5.1) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + sawyer (0.9.2) + addressable (>= 2.3.5) + faraday (>= 0.17.3, < 3) + security (0.1.5) + signet (0.19.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + slack-notifier (2.4.0) + strscan (3.1.0) + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + thor (1.0.1) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + typhoeus (1.4.1) + ethon (>= 0.9.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uber (0.1.0) + unicode-display_width (2.5.0) + word_wrap (1.0.0) + xcodeproj (1.24.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + rexml (~> 3.2.4) + xcov (1.8.1) + fastlane (>= 2.141.0, < 3.0.0) + multipart-post + slack-notifier + terminal-table + xcodeproj + xcresult (~> 0.2.0) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + xcresult (0.2.1) + yaml (0.3.0) + +PLATFORMS + arm64-darwin-21 + +DEPENDENCIES + activesupport (~> 7.0.0, >= 7.0.8) + arkana + cocoapods + danger + danger-swiftformat + danger-swiftlint + danger-xcode_summary + danger-xcov + fastlane + fastlane-plugin-firebase_app_distribution + xcov + +BUNDLED WITH + 2.4.22 diff --git a/Scripts/Swift/iOSTemplateMaker/Package.resolved b/Scripts/Swift/iOSTemplateMaker/Package.resolved index d429d392..dce46fe6 100644 --- a/Scripts/Swift/iOSTemplateMaker/Package.resolved +++ b/Scripts/Swift/iOSTemplateMaker/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "ansiterminal", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pakLebah/ANSITerminal.git", + "state" : { + "revision" : "8e24fab68c660eca1244a95019ec69a11400d62b", + "version" : "0.0.3" + } + }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", diff --git a/Scripts/Swift/iOSTemplateMaker/Package.swift b/Scripts/Swift/iOSTemplateMaker/Package.swift index 46e6dd43..49ebc428 100644 --- a/Scripts/Swift/iOSTemplateMaker/Package.swift +++ b/Scripts/Swift/iOSTemplateMaker/Package.swift @@ -16,12 +16,17 @@ let package = Package( url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0" ), + .package( + url: "https://github.com/pakLebah/ANSITerminal.git", + from: "0.0.3" + ) ], targets: [ .executableTarget( name: "iOSTemplateMaker", dependencies: [ - .product(name: "ArgumentParser", package: "swift-argument-parser") + .product(name: "ArgumentParser", package: "swift-argument-parser"), + "ANSITerminal" ] ), ] diff --git a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Extensions/FileManager+Utils.swift b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Extensions/FileManager+Utils.swift index 77b3979a..56f8ae85 100644 --- a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Extensions/FileManager+Utils.swift +++ b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Extensions/FileManager+Utils.swift @@ -2,7 +2,7 @@ import Foundation extension FileManager { - func moveFiles(in directory: String, to destination: String) { + func moveFiles(in directory: String, to destination: String) throws { let currentDirectory = currentDirectoryPath let files = try? contentsOfDirectory( atPath: "\(currentDirectory)/\(directory)" @@ -10,64 +10,44 @@ extension FileManager { if let files = files { for file in files { guard file != ".DS_Store" else { continue } - do { - try moveItem( - atPath: "\(currentDirectory)/\(directory)/\(file)", - toPath:"\(currentDirectory)/\(destination)/\(file)" - ) - } catch { - print("Error \(error)") - } + try moveItem( + atPath: "\(currentDirectory)/\(directory)/\(file)", + toPath:"\(currentDirectory)/\(destination)/\(file)" + ) } } } - func rename(file: String, to destination: String) { + func rename(file: String, to destination: String) throws { let currentDirectory = currentDirectoryPath - do { - try moveItem( - atPath: "\(currentDirectory)/\(file)", - toPath:"\(currentDirectory)/\(destination)" - ) - } catch { - print("Error \(error)") - } + try moveItem( + atPath: "\(currentDirectory)/\(file)", + toPath:"\(currentDirectory)/\(destination)" + ) } - func copy(file: String, to destination: String) { + func copy(file: String, to destination: String) throws { let currentDirectory = currentDirectoryPath - do { - try copyItem( - atPath: "\(currentDirectory)/\(file)", - toPath:"\(currentDirectory)/\(destination)" - ) - } catch { - print("Error \(error)") - } + try copyItem( + atPath: "\(currentDirectory)/\(file)", + toPath:"\(currentDirectory)/\(destination)" + ) } - func createDirectory(path: String) { + func createDirectory(path: String) throws { let currentDirectory = currentDirectoryPath - do { - try createDirectory(atPath: "\(currentDirectory)/\(path)", withIntermediateDirectories: true, attributes: nil) - } catch { - print("Error \(error)") - } + try createDirectory(atPath: "\(currentDirectory)/\(path)", withIntermediateDirectories: true, attributes: nil) } - func createFile(name: String, at directory: String) { + func createFile(name: String, at directory: String) throws { let currentDirectory = currentDirectoryPath - createDirectory(path: directory) + try createDirectory(path: directory) createFile(atPath: "\(currentDirectory)\(directory)\(name)", contents: nil) } - func removeItems(in directory: String) { + func removeItems(in directory: String) throws { let currentDirectory = currentDirectoryPath - do { - try removeItem(atPath: "\(currentDirectory)/\(directory)") - } catch { - print("Error \(error)") - } + try removeItem(atPath: "\(currentDirectory)/\(directory)") } func replaceAllOccurrences(of original: String, to replacing: String) { @@ -77,7 +57,10 @@ extension FileManager { guard let files else { return print("Cannot find any files in current directory") } for file in files { do { - let text = try String(contentsOf: file, encoding: .utf8) + guard let text = try? String(contentsOf: file, encoding: .utf8) else { + continue + } + let modifiedText = text.replacingOccurrences(of: original, with: replacing) try modifiedText.write(to: file, atomically: true, encoding: .utf8) } catch { diff --git a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Extensions/String+Utils.swift b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Extensions/String+Utils.swift index 527a0b9f..865dbf16 100644 --- a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Extensions/String+Utils.swift +++ b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Extensions/String+Utils.swift @@ -9,3 +9,5 @@ extension String { return regex.firstMatch(in: lhs, options: [], range: range) != nil } } + +extension String: Error {} diff --git a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Confirm.swift b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Confirm.swift new file mode 100644 index 00000000..f9e6c31b --- /dev/null +++ b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Confirm.swift @@ -0,0 +1,20 @@ +// +// Confirm.swift +// +// +// Created by MarkG on 17/7/24. +// + +import Foundation + +enum ConfirmResult: String, Titlable, CaseIterable { + + case yes = "Yes" + case no = "No" + + var title: String { rawValue } +} + +func confirm(_ message: String) -> ConfirmResult { + picker(title: message, options: ConfirmResult.allCases) +} diff --git a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Picker.swift b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Picker.swift new file mode 100644 index 00000000..42242ddb --- /dev/null +++ b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Picker.swift @@ -0,0 +1,122 @@ +// +// Picker.swift +// +// +// Created by MarkG on 15/7/24. +// + +import ANSITerminal + +protocol Titlable { + + var title: String { get } +} + +fileprivate struct Option { + + let title: String + let line: Int + + init(title: String, line: Int) { + self.title = title + self.line = line + } +} + +fileprivate class OptionState { + + let options: [Option] + let rangeOfLines: (minimum: Int, maximum: Int) + var activeLine: Int = .zero + + var activeIndex: Int { + options.firstIndex { $0.line == activeLine } ?? .zero + } + + var activeOption: Option? { + options.first { $0.line == activeLine } + } + + init(options: [Option], activeLine: Int, rangeOfLines: (minimum: Int, maximum: Int)) { + self.activeLine = activeLine + self.rangeOfLines = rangeOfLines + self.options = options + } +} + +func picker(title: String, options: [T]) -> T { + cursorOff() + write("◆".foreColor(81).bold) + moveRight() + writeln(title) + + options.forEach { + writeln($0.title) + } + moveUp(options.count) + clearBelow() + let currentLine = readCursorPos().row + let state = OptionState( + options: options.enumerated() + .map { Option(title: $1.title, line: currentLine + $0) }, + activeLine: currentLine, + rangeOfLines: (currentLine, currentLine + options.count - 1) + ) + reRender(state: state) + + let restoreLine = options.count + 1 + while true { + clearBuffer() + + if keyPressed() { + let char = readChar() + if char == NonPrintableChar.enter.char() { + break + } + + let key = readKey() + + switch key.code { + case .up: + if state.activeLine > state.rangeOfLines.minimum { + state.activeLine -= 1 + + moveUp(restoreLine) + reRender(state: state) + } + case .down: + if state.activeLine < state.rangeOfLines.maximum { + state.activeLine += 1 + + moveUp(restoreLine) + reRender(state: state) + } + default: break + } + } + } + + return options[state.activeIndex] +} + +private func reRender(state: OptionState) { + (state.rangeOfLines.minimum...state.rangeOfLines.maximum).forEach { line in + let isActive = line == state.activeLine + + write("│".foreColor(81)) + + moveRight() + let stateIndicator = isActive ? "●".lightGreen : "○".foreColor(250) + write(stateIndicator) + + if let title = state.options.first(where: { + $0.line == line + })?.title { + let title = isActive ? title : title.foreColor(250) + moveRight() + writeln(title) + } + } + + writeln("└".foreColor(81)) +} diff --git a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Terminal.swift b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Terminal.swift new file mode 100644 index 00000000..ba565658 --- /dev/null +++ b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/Helpers/Terminal.swift @@ -0,0 +1,127 @@ +// +// Echo.swift +// +// +// Created by MarkG on 9/7/24. +// + +import Foundation +import ANSITerminal + +enum WriteStyle { + + case section + case success + case error + case warning +} + +func write(_ text: String, style: WriteStyle) { + switch style { + case .section: + writeln("-------------------".green) + writeln(text.green) + writeln("-------------------".green) + case .success: + writeln(text.green) + case .error: + writeln(text.red) + case .warning: + writeln(text.yellow) + } +} + +func writeAt(_ row: Int, _ col: Int, _ text: String) { + moveTo(row, col) + write(text) +} + +func ask( + _ q: String, + note: String? = nil, + defaultValue: String? = nil, + onValidate: (_ input: String) -> String? +) -> String { + write("◆".foreColor(81).bold) + moveRight() + + if defaultValue == nil { + write(q) + moveRight() + writeln("(*)".red) + } else { + writeln(q) + } + + if let note { + writeln(note.gray) + } + + var hasError = false + while(true) { + clearBuffer() + let input = ask("> ") + if let error = onValidate(input) { + hasError = true + write(error, style: .error) + + let count = getWroteLineCount(error) + 1 + moveUp(count) + clearLine() + continue + } + + if hasError { + clearBelow() + } + + if input.isEmpty, + let defaultValue { + return defaultValue + } + + return input + } +} + +func step(title: String, action: () throws -> Void) throws { + writeln("→ \(title.uppercased())".bold) + + do { + try action() + } catch { + let message = error as? String ?? error.localizedDescription + write(message, style: .error) + throw error + } +} + +private func getWroteLineCount(_ text: String) -> Int { + let count = unexpand(text: text).count + let col = readScreenSize().col + return Int(ceil(Double(count) / Double(col))) +} + +private func unexpand(text: String) -> String { + var result = "" + var spaceCount = 0 + for char in text { + if char == " " { + spaceCount += 1 + } else { + if spaceCount > 0 { + let tabs = String(repeating: "\t", count: spaceCount / 8) + let spaces = String(repeating: " ", count: spaceCount % 8) + result += tabs + spaces + spaceCount = 0 + } + result += String(char) + } + } + if spaceCount > 0 { + let tabs = String(repeating: "\t", count: spaceCount / 8) + let spaces = String(repeating: " ", count: spaceCount % 8) + result += tabs + spaces + } + return result +} diff --git a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpCICDService.swift b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpCICDService.swift index 774b9f09..9f212855 100644 --- a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpCICDService.swift +++ b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpCICDService.swift @@ -2,7 +2,7 @@ import Foundation struct SetUpCICDService { - enum CICDService { + enum CICDService: Titlable, CaseIterable { case github, bitrise, codemagic, later @@ -25,9 +25,22 @@ struct SetUpCICDService { return nil } } + + var title: String { + switch self { + case .github: + "Github" + case .bitrise: + "Bitrise" + case .codemagic: + "CodeMagic" + case .later: + "none" + } + } } - enum GithubRunnerType { + enum GithubRunnerType: Titlable, CaseIterable { case macOSLatest, selfHosted, later @@ -48,59 +61,62 @@ struct SetUpCICDService { return nil } } + + var title: String { + switch self { + case .macOSLatest: + "macos" + case .selfHosted: + "self-hosted" + case .later: + "none" + } + } } private let fileManager = FileManager.default - func perform() { - var service: CICDService? = nil - while service == nil { - print("Which CI/CD service do you use (Can be edited later) [(g)ithub/(b)itrise/(c)odemagic/(l)ater]: ") - service = CICDService(readLine().string) - } + func perform() throws { + let service = picker( + title: "Which service do you use?", + options: CICDService.allCases + ) switch service { case .github: - var runnerType: GithubRunnerType? - while runnerType == nil { - print("Which workflow runner do you want to use? [(m)acos-latest/(s)elf-hosted/(l)ater]: ") - runnerType = GithubRunnerType(readLine().string) - } - print("Setting template for Github Actions") - fileManager.removeItems(in: "bitrise.yml") - fileManager.removeItems(in: "codemagic.yaml") - fileManager.removeItems(in: ".github/workflows") - fileManager.createDirectory(path: ".github/workflows") + let runnerType = picker( + title: "Which workflow runner do you want to use?", + options: GithubRunnerType.allCases + ) + + try fileManager.removeItems(in: "bitrise.yml") + try fileManager.removeItems(in: "codemagic.yaml") + try fileManager.removeItems(in: ".github/workflows") + try fileManager.createDirectory(path: ".github/workflows") switch runnerType { case .macOSLatest: - print("Configured to run on the latest macOS.") - fileManager.moveFiles(in: ".github/project_workflows", to: ".github/workflows") - fileManager.removeItems(in: ".github/project_workflows") - fileManager.removeItems(in: ".github/self_hosted_project_workflows") + try fileManager.moveFiles(in: ".github/project_workflows", to: ".github/workflows") + try fileManager.removeItems(in: ".github/project_workflows") + try fileManager.removeItems(in: ".github/self_hosted_project_workflows") case .selfHosted: - print("Configured to run on self-hosted.") - fileManager.moveFiles(in: ".github/self_hosted_project_workflows", to: ".github/workflows") - fileManager.removeItems(in: ".github/project_workflows") - fileManager.removeItems(in: ".github/self_hosted_project_workflows") - case .later, .none: + try fileManager.moveFiles(in: ".github/self_hosted_project_workflows", to: ".github/workflows") + try fileManager.removeItems(in: ".github/project_workflows") + try fileManager.removeItems(in: ".github/self_hosted_project_workflows") + case .later: print("You can manually setup the runner later.") } case .bitrise: - print("Setting template for Bitrise") - fileManager.removeItems(in: "codemagic.yaml") - fileManager.removeItems(in: ".github/workflows") - fileManager.removeItems(in: ".github/project_workflows") - fileManager.removeItems(in: ".github/self_hosted_project_workflows") + try fileManager.removeItems(in: "codemagic.yaml") + try fileManager.removeItems(in: ".github/workflows") + try fileManager.removeItems(in: ".github/project_workflows") + try fileManager.removeItems(in: ".github/self_hosted_project_workflows") case .codemagic: - print("Setting template for CodeMagic") - fileManager.removeItems(in: "bitrise.yml") - fileManager.removeItems(in: ".github/workflows") - fileManager.removeItems(in: ".github/project_workflows") - fileManager.removeItems(in: ".github/self_hosted_project_workflows") - case .later, .none: + try fileManager.removeItems(in: "bitrise.yml") + try fileManager.removeItems(in: ".github/workflows") + try fileManager.removeItems(in: ".github/project_workflows") + try fileManager.removeItems(in: ".github/self_hosted_project_workflows") + case .later: print("You can manually setup the template later.") } - - print("✅ Completed") } } diff --git a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpDeliveryConstants.swift b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpDeliveryConstants.swift index e33e8608..6d1e2c61 100644 --- a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpDeliveryConstants.swift +++ b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpDeliveryConstants.swift @@ -1,21 +1,11 @@ struct SetUpDeliveryConstants { - func perform() { - print("Do you want to set up Constants values? (Can be edited later) [Y/n]: ") - - let arg = readLine() ?? "y" - - switch arg.lowercased() { - case "y", "yes": - do { - let error = try safeShell("open -a Xcode fastlane/Constants/Constant.swift") - guard let error = error, !error.isEmpty else { break } - print("Could not open Xcode. Make sure Xcode is installed and try again.\nRaw error: \(error)") - } catch { - print("Error: \(error)") - } - default: - print("✅ Completed. You can edit this file at 'fastlane/Constants/Constant.swift'.") + func perform() throws { + let result = confirm("Do you want to set up Constants values?") + switch result { + case .yes: + try safeShell("open -a Xcode fastlane/Constants/Constant.swift") + case .no: break } } } diff --git a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpInterface.swift b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpInterface.swift index 9024ea87..5f61dcf5 100644 --- a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpInterface.swift +++ b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpInterface.swift @@ -2,7 +2,7 @@ import Foundation struct SetUpInterface { - enum Interface { + enum Interface: Titlable, CaseIterable { case swiftUI, uiKit @@ -23,27 +23,27 @@ struct SetUpInterface { case .uiKit: return "UIKit" } } + + var title: String { folderName } } private let fileManager = FileManager.default - func perform(_ interface: Interface, _ projectName: String) { + func perform(_ interface: Interface, _ projectName: String) throws { switch interface { case .swiftUI: - print("=> 🦅 Setting up SwiftUI") let swiftUIAppDirectory = "tuist/Interfaces/SwiftUI/Sources/Application" - fileManager.rename( + try fileManager.rename( file: "\(swiftUIAppDirectory)/App.swift", to: "\(swiftUIAppDirectory)/\(projectName)App.swift" ) - case .uiKit: - print("=> 🦉 Setting up UIKit") + case .uiKit: break } let folderName = interface.folderName - fileManager.moveFiles(in: "tuist/Interfaces/\(folderName)/Project", to: "") - fileManager.moveFiles(in: "tuist/Interfaces/\(folderName)/Sources", to: "\(projectName)/Sources") - fileManager.removeItems(in: "tuist/Interfaces") + try fileManager.moveFiles(in: "tuist/Interfaces/\(folderName)/Project", to: "") + try fileManager.moveFiles(in: "tuist/Interfaces/\(folderName)/Sources", to: "\(projectName)/Sources") + try fileManager.removeItems(in: "tuist/Interfaces") } } diff --git a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpiOSProject.swift b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpiOSProject.swift index 412b7a1b..bfdee35d 100644 --- a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpiOSProject.swift +++ b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/SetUpiOSProject.swift @@ -1,10 +1,11 @@ import Foundation +import ANSITerminal class SetUpIOSProject { private let CONSTANT_PROJECT_NAME = "{PROJECT_NAME}" - private let CONSTANT_BUNDLE_PRODUCTION = "{BUNDLE_ID_PRODUCTION}" - private let CONSTANT_BUNDLE_STAGING = "{BUNDLE_ID_STAGING}" + private let CONSTANT_BUNDLE_PRODUCTION = "" + private let CONSTANT_BUNDLE_STAGING = "" private let CONSTANT_MINIMUM_VERSION = "{TARGET_VERSION}" private let fileManager = FileManager.default @@ -31,75 +32,135 @@ class SetUpIOSProject { } func perform() { - readArguments() - print("=> 🐢 Starting init \(projectName) ...") - replaceFileStructure() - createPlaceholderFiles() - SetUpInterface().perform(interface ?? .uiKit, projectName) - try? replaceTextInFiles() - try? runTuist() - try? installDependencies() - try? removeGitkeepFromXcodeProject() - try? removeTemplateFiles() - setUpCICD() - - print("=> 🚀 Done! App is ready to be tested 🙌") - try? openProject() + do { + try step(title: "Fill project information") { + readArguments() + } + + try step(title: "Replace files structure") { + try replaceFileStructure() + } + + try step(title: "Create placeholder files") { + try createPlaceholderFiles() + } + + try step(title: "Setup interface") { + try SetUpInterface().perform(interface ?? .uiKit, projectName) + } + + try step(title: "Replace package and package name within files") { + try replaceTextInFiles() + } + + try step(title: "Run tuist") { + try runTuist() + } + + try step(title: "Install dependencies") { + try installDependencies() + } + + try step(title: "Remove gitkeep files from project") { + try removeGitkeepFromXcodeProject() + } + + try step(title: "Remove template files") { + try removeTemplateFiles() + } + + try step(title: "Setup CI/CD") { + try setUpCICD() + } + + writeln() + write("🚀 Done! App is ready to development 🙌", style: .success) + try? openProject() + } catch {} } private func readArguments() { + var canMoveDown = false + let tryMoveDown: () -> Void = { + if canMoveDown { + writeln() + } + + canMoveDown = true + } + if isCI { minimumVersion = "14.0" } - while bundleIdProduction.isEmpty || !checkPackageName(bundleIdProduction) { - print("BUNDLE ID PRODUCTION (i.e. com.example.project):") - bundleIdProduction = readLine().string - } - while bundleIdStaging.isEmpty || !checkPackageName(bundleIdStaging) { - print("BUNDLE ID STAGING (i.e. com.example.project.staging):") - bundleIdStaging = readLine().string + if bundleIdProduction.isEmpty { + tryMoveDown() + bundleIdProduction = ask( + "Which is the bundle ID for the production environment?", + note: "Ex: com.example.project", + onValidate: validatePackageName + ) } - while projectName.isEmpty { - print("PROJECT NAME (i.e. NewProject):") - projectName = readLine().string + + if bundleIdStaging.isEmpty { + tryMoveDown() + bundleIdProduction = ask( + "Which is the bundle ID for the staging environment?", + note: "Ex: com.example.project.staging", + onValidate: validatePackageName + ) } - while minimumVersion.isEmpty || !checkVersion(minimumVersion) { - print("iOS Minimum Version (i.e. 14.0):") - let version = readLine().string - minimumVersion = !version.isEmpty ? version : "14.0" + + if projectName.isEmpty { + tryMoveDown() + projectName = ask( + "Which is the project name?", + note: "Ex: NewProject", + onValidate: validateProjectName + ) } - while interface == nil { - print("Interface [(S)wiftUI or (U)IKit]:") - interface = SetUpInterface.Interface(readLine().string) + + if minimumVersion.isEmpty { + tryMoveDown() + + let defaultVersion = "14.0" + minimumVersion = ask( + "Which is the iOS minimum version?", + note: "Default: \(defaultVersion)", + defaultValue: defaultVersion, + onValidate: validateVersion + ) } + + moveDown() + interface = picker( + title: "Which is the interface", + options: SetUpInterface.Interface.allCases + ) } - private func replaceFileStructure() { - print("=> 🔎 Replacing files structure...") - fileManager.rename(file: "\(CONSTANT_PROJECT_NAME)Tests", to: "\(projectNameNoSpace)Tests") - fileManager.rename(file: "\(CONSTANT_PROJECT_NAME)KIFUITests", to: "\(projectNameNoSpace)KIFUITests") - fileManager.rename(file: "\(CONSTANT_PROJECT_NAME)", to: "\(projectNameNoSpace)") + private func replaceFileStructure() throws { + try fileManager.rename(file: "\(CONSTANT_PROJECT_NAME)Tests", to: "\(projectNameNoSpace)Tests") + try fileManager.rename(file: "\(CONSTANT_PROJECT_NAME)KIFUITests", to: "\(projectNameNoSpace)KIFUITests") + try fileManager.rename(file: "\(CONSTANT_PROJECT_NAME)", to: "\(projectNameNoSpace)") } - private func createPlaceholderFiles() { + private func createPlaceholderFiles() throws { // Duplicate the env example file to env file - fileManager.copy(file: ".env.example", to: ".env") + try fileManager.copy(file: ".env.example", to: ".env") // Add AutoMockable.generated.swift file - fileManager.createFile(name: "AutoMockable.generated.swift", at: "\(projectNameNoSpace)Tests/Sources/Mocks/Sourcery") + try fileManager.createFile(name: "AutoMockable.generated.swift", at: "\(projectNameNoSpace)Tests/Sources/Mocks/Sourcery") // Add R.generated.swift file. - fileManager.createFile(name: "R.generated.swift", at: "\(projectNameNoSpace)/Sources/Supports/Helpers/Rswift") + try fileManager.createFile(name: "R.generated.swift", at: "\(projectNameNoSpace)/Sources/Supports/Helpers/Rswift") } private func replaceTextInFiles() throws { - print("=> 🔎 Replacing package and package name within files...") fileManager.replaceAllOccurrences(of: CONSTANT_BUNDLE_STAGING, to: bundleIdStaging) fileManager.replaceAllOccurrences(of: CONSTANT_BUNDLE_PRODUCTION, to: bundleIdProduction) fileManager.replaceAllOccurrences(of: CONSTANT_PROJECT_NAME, to: projectNameNoSpace) fileManager.replaceAllOccurrences(of: CONSTANT_MINIMUM_VERSION, to: minimumVersion) - print("✅ Completed") } private func installTuistIfNeeded() throws { @@ -120,74 +181,65 @@ class SetUpIOSProject { private func runTuist() throws { try installTuistIfNeeded() try safeShell("tuist generate --no-open") - print("✅ Completed") } private func installDependencies() throws { - print("Installing gems") try safeShell("bundle install") - - // Install dependencies - print("Run Arkana") try safeShell("bundle exec arkana") - - print("Installing pod dependencies") try safeShell("bundle exec pod install --repo-update") - print("✅ Completed") } private func removeGitkeepFromXcodeProject() throws { - print("Remove gitkeep files from project") let escapedProjectNameNoSpace = projectNameNoSpace.replacingOccurrences(of: ".", with: "\\.") try safeShell("sed -i \"\" \"s/.*\\(gitkeep\\).*,//\" \(escapedProjectNameNoSpace).xcodeproj/project.pbxproj") - print("✅ Complete") } private func removeTemplateFiles() throws { - print("Remove tuist files") - fileManager.removeItems(in: ".tuist-version") - fileManager.removeItems(in: "tuist") - fileManager.removeItems(in: "Project.swift") - fileManager.removeItems(in: "Workspace.swift") - - print("Remove script files and git/index") - fileManager.removeItems(in: ".github/workflows/test_uikit_install_script.yml") - fileManager.removeItems(in: ".github/workflows/test_swiftui_install_script.yml") - fileManager.removeItems(in: ".git/index") + try fileManager.removeItems(in: ".tuist-version") + try fileManager.removeItems(in: "tuist") + try fileManager.removeItems(in: "Project.swift") + try fileManager.removeItems(in: "Workspace.swift") + + try fileManager.removeItems(in: ".github/workflows/test_uikit_install_script.yml") + try fileManager.removeItems(in: ".github/workflows/test_swiftui_install_script.yml") + try fileManager.removeItems(in: ".git/index") try safeShell("git reset") } - private func setUpCICD() { + private func setUpCICD() throws { if !isCI { - SetUpCICDService().perform() - SetUpDeliveryConstants().perform() - fileManager.removeItems(in: "Scripts") + try SetUpCICDService().perform() + try SetUpDeliveryConstants().perform() + try fileManager.removeItems(in: "Scripts") } - print("✅ Completed") } private func openProject() throws { if !isCI { - print("=> 🛠 Opening the project.") try safeShell("open -a Xcode \(projectNameNoSpace).xcworkspace") } } - private func checkPackageName(_ name: String) -> Bool { + private func validatePackageName(_ name: String) -> String? { let packageNameRegex="^[a-z][a-z0-9_]*(\\.[a-z0-9_-]+)+[0-9a-z_-]$" let valid = name ~= packageNameRegex - if !valid { - print("Please pick a valid package name with pattern {com.example.package}") - } - return valid + + return valid ? nil : "Please pick a valid package name with pattern {com.example.package}" + } + + private func validateProjectName(_ name: String) -> String? { + name.isEmpty ? "Please input the project name" : nil } - private func checkVersion(_ version: String) -> Bool { + + private func validateVersion(_ version: String) -> String? { + if version.isEmpty { + return nil + } + let versionRegex="^[0-9_]+(\\.[0-9]+)+$" let valid = version ~= versionRegex - if !valid { - print("Please pick a valid version with pattern {x.y}") - } - return valid + + return valid ? nil : "Please pick a valid version with pattern {x.y}" } } diff --git a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/iOSTemplateMaker.swift b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/iOSTemplateMaker.swift index ea1b341d..7050547e 100644 --- a/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/iOSTemplateMaker.swift +++ b/Scripts/Swift/iOSTemplateMaker/Sources/iOSTemplateMaker/iOSTemplateMaker.swift @@ -6,7 +6,11 @@ struct iOSTemplateMaker: ParsableCommand { static let configuration: CommandConfiguration = CommandConfiguration( abstract: "Set up an iOS Project", - subcommands: [Make.self, MakeTestFirebase.self, MakeTestTestFlight.self], + subcommands: [ + Make.self, + MakeTestFirebase.self, + MakeTestTestFlight.self + ], defaultSubcommand: Make.self ) }