From b9c6226b0080750f2d61646cdd30d4b11a9d4de1 Mon Sep 17 00:00:00 2001 From: William Brawner Date: Fri, 28 Jan 2022 19:13:11 -0700 Subject: [PATCH] WIP: implement CLI --- Twigs.xcodeproj/project.pbxproj | 132 ++++++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 9 ++ .../xcshareddata/xcschemes/twigs-cli.xcscheme | 92 ++++++++++++ twigs-cli/TwigsCli.swift | 124 ++++++++++++++++ 4 files changed, 357 insertions(+) create mode 100644 Twigs.xcodeproj/xcshareddata/xcschemes/twigs-cli.xcscheme create mode 100644 twigs-cli/TwigsCli.swift diff --git a/Twigs.xcodeproj/project.pbxproj b/Twigs.xcodeproj/project.pbxproj index 6a75007..c875268 100644 --- a/Twigs.xcodeproj/project.pbxproj +++ b/Twigs.xcodeproj/project.pbxproj @@ -54,6 +54,10 @@ 80820145275FFD380040996E /* SidebarBudgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80820144275FFD380040996E /* SidebarBudgetView.swift */; }; 8094A9C327567CAC006C6C62 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 8094A9C227567CAC006C6C62 /* Collections */; }; 809B942327221EC800B1DAE2 /* CategoryFormSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 809B942227221EC800B1DAE2 /* CategoryFormSheet.swift */; }; + 80A419ED2787C0A00090C515 /* TwigsCli.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A419EC2787C0A00090C515 /* TwigsCli.swift */; }; + 80A419F32787C13E0090C515 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 80A419F22787C13E0090C515 /* ArgumentParser */; }; + 80A419F52787C1520090C515 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 80A419F42787C1520090C515 /* ArgumentParser */; }; + 80A419F72788A3880090C515 /* TwigsCore in Frameworks */ = {isa = PBXBuildFile; productRef = 80A419F62788A3880090C515 /* TwigsCore */; }; 80D1FC14277C1EF9007F17FB /* InlineLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D1FC13277C1EF9007F17FB /* InlineLoadingView.swift */; }; /* End PBXBuildFile section */ @@ -74,6 +78,18 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 80A419E82787C0A00090C515 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 2821265F23555FD300072D52 /* TransactionFormSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionFormSheet.swift; sourceTree = ""; }; 282126A0235929B800072D52 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; @@ -129,6 +145,8 @@ 808582CD277E5E9E00006859 /* TwigsCore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TwigsCore; path = ../TwigsCore; sourceTree = ""; }; 809B942227221EC800B1DAE2 /* CategoryFormSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryFormSheet.swift; sourceTree = ""; }; 809B94242722597800B1DAE2 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + 80A419EA2787C0A00090C515 /* twigs-cli */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "twigs-cli"; sourceTree = BUILT_PRODUCTS_DIR; }; + 80A419EC2787C0A00090C515 /* TwigsCli.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwigsCli.swift; sourceTree = ""; }; 80D1FC13277C1EF9007F17FB /* InlineLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineLoadingView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -137,6 +155,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 80A419F32787C13E0090C515 /* ArgumentParser in Frameworks */, 8005FD5B277E623900E48B23 /* TwigsCore in Frameworks */, 8094A9C327567CAC006C6C62 /* Collections in Frameworks */, ); @@ -156,6 +175,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 80A419E72787C0A00090C515 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 80A419F72788A3880090C515 /* TwigsCore in Frameworks */, + 80A419F52787C1520090C515 /* ArgumentParser in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -207,6 +235,7 @@ isa = PBXGroup; children = ( 80DBED432774AE4F00CB0A88 /* Packages */, + 80A419EB2787C0A00090C515 /* twigs-cli */, 28AC94EB233C373900BFB70A /* Products */, 28AC94EC233C373900BFB70A /* Twigs */, 28AC9503233C373A00BFB70A /* TwigsTests */, @@ -221,6 +250,7 @@ 28AC94EA233C373900BFB70A /* Twigs.app */, 28AC9500233C373A00BFB70A /* TwigsTests.xctest */, 28AC950B233C373A00BFB70A /* TwigsUITests.xctest */, + 80A419EA2787C0A00090C515 /* twigs-cli */, ); name = Products; sourceTree = ""; @@ -320,6 +350,14 @@ name = Frameworks; sourceTree = ""; }; + 80A419EB2787C0A00090C515 /* twigs-cli */ = { + isa = PBXGroup; + children = ( + 80A419EC2787C0A00090C515 /* TwigsCli.swift */, + ); + path = "twigs-cli"; + sourceTree = ""; + }; 80D6B1EF275B11C10075D0EC /* Recurring Transactions */ = { isa = PBXGroup; children = ( @@ -358,6 +396,7 @@ packageProductDependencies = ( 8094A9C227567CAC006C6C62 /* Collections */, 8005FD5A277E623900E48B23 /* TwigsCore */, + 80A419F22787C13E0090C515 /* ArgumentParser */, ); productName = Budget; productReference = 28AC94EA233C373900BFB70A /* Twigs.app */; @@ -399,6 +438,27 @@ productReference = 28AC950B233C373A00BFB70A /* TwigsUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + 80A419E92787C0A00090C515 /* twigs-cli */ = { + isa = PBXNativeTarget; + buildConfigurationList = 80A419F02787C0A00090C515 /* Build configuration list for PBXNativeTarget "twigs-cli" */; + buildPhases = ( + 80A419E62787C0A00090C515 /* Sources */, + 80A419E72787C0A00090C515 /* Frameworks */, + 80A419E82787C0A00090C515 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "twigs-cli"; + packageProductDependencies = ( + 80A419F42787C1520090C515 /* ArgumentParser */, + 80A419F62788A3880090C515 /* TwigsCore */, + ); + productName = "twigs-cli"; + productReference = 80A419EA2787C0A00090C515 /* twigs-cli */; + productType = "com.apple.product-type.tool"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -420,6 +480,9 @@ CreatedOnToolsVersion = 11.0; TestTargetID = 28AC94E9233C373900BFB70A; }; + 80A419E92787C0A00090C515 = { + CreatedOnToolsVersion = 13.2.1; + }; }; }; buildConfigurationList = 28AC94E5233C373900BFB70A /* Build configuration list for PBXProject "Twigs" */; @@ -434,6 +497,7 @@ mainGroup = 28AC94E1233C373900BFB70A; packageReferences = ( 8094A9C127567CAC006C6C62 /* XCRemoteSwiftPackageReference "swift-collections" */, + 80A419F12787C13E0090C515 /* XCRemoteSwiftPackageReference "swift-argument-parser" */, ); productRefGroup = 28AC94EB233C373900BFB70A /* Products */; projectDirPath = ""; @@ -442,6 +506,7 @@ 28AC94E9233C373900BFB70A /* Twigs */, 28AC94FF233C373A00BFB70A /* TwigsTests */, 28AC950A233C373A00BFB70A /* TwigsUITests */, + 80A419E92787C0A00090C515 /* twigs-cli */, ); }; /* End PBXProject section */ @@ -538,6 +603,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 80A419E62787C0A00090C515 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 80A419ED2787C0A00090C515 /* TwigsCli.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -834,6 +907,34 @@ }; name = Release; }; + 80A419EE2787C0A00090C515 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = VJ33S6H7W7; + ENABLE_HARDENED_RUNTIME = YES; + MACOSX_DEPLOYMENT_TARGET = 12.1; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 80A419EF2787C0A00090C515 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = VJ33S6H7W7; + ENABLE_HARDENED_RUNTIME = YES; + MACOSX_DEPLOYMENT_TARGET = 12.1; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -873,6 +974,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 80A419F02787C0A00090C515 /* Build configuration list for PBXNativeTarget "twigs-cli" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 80A419EE2787C0A00090C515 /* Debug */, + 80A419EF2787C0A00090C515 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -884,6 +994,14 @@ minimumVersion = 1.0.0; }; }; + 80A419F12787C13E0090C515 /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-argument-parser.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -896,6 +1014,20 @@ package = 8094A9C127567CAC006C6C62 /* XCRemoteSwiftPackageReference "swift-collections" */; productName = Collections; }; + 80A419F22787C13E0090C515 /* ArgumentParser */ = { + isa = XCSwiftPackageProductDependency; + package = 80A419F12787C13E0090C515 /* XCRemoteSwiftPackageReference "swift-argument-parser" */; + productName = ArgumentParser; + }; + 80A419F42787C1520090C515 /* ArgumentParser */ = { + isa = XCSwiftPackageProductDependency; + package = 80A419F12787C13E0090C515 /* XCRemoteSwiftPackageReference "swift-argument-parser" */; + productName = ArgumentParser; + }; + 80A419F62788A3880090C515 /* TwigsCore */ = { + isa = XCSwiftPackageProductDependency; + productName = TwigsCore; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 28AC94E2233C373900BFB70A /* Project object */; diff --git a/Twigs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Twigs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e56059c..fa4219d 100644 --- a/Twigs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Twigs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "swift-argument-parser", + "repositoryURL": "https://github.com/apple/swift-argument-parser.git", + "state": { + "branch": null, + "revision": "e1465042f195f374b94f915ba8ca49de24300a0d", + "version": "1.0.2" + } + }, { "package": "swift-collections", "repositoryURL": "https://github.com/apple/swift-collections.git", diff --git a/Twigs.xcodeproj/xcshareddata/xcschemes/twigs-cli.xcscheme b/Twigs.xcodeproj/xcshareddata/xcschemes/twigs-cli.xcscheme new file mode 100644 index 0000000..29ca381 --- /dev/null +++ b/Twigs.xcodeproj/xcshareddata/xcschemes/twigs-cli.xcscheme @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/twigs-cli/TwigsCli.swift b/twigs-cli/TwigsCli.swift new file mode 100644 index 0000000..1b12a4a --- /dev/null +++ b/twigs-cli/TwigsCli.swift @@ -0,0 +1,124 @@ +// +// main.swift +// twigs-cli +// +// Created by William Brawner on 1/6/22. +// Copyright © 2022 William Brawner. All rights reserved. +// + +import Foundation +import ArgumentParser +import TwigsCore + +@main +enum TwigsCli { + static func main() async throws { + await Twigs.main() + } +} + +protocol AsyncParsableCommand: ParsableCommand { + mutating func runAsync() async throws +} + +extension ParsableCommand { + static func main() async { + do { + var command = try parseAsRoot(nil) + if var asyncCommand = command as? AsyncParsableCommand { + try await asyncCommand.runAsync() + } else { + try command.run() + } + } catch { + exit(withError: error) + } + } +} + +struct Twigs: ParsableCommand { + static var configuration = CommandConfiguration( + commandName: "twigs-cli", + abstract: "A CLI for Twigs, a personal finance application focused on individual and family budgeting", + version: "1.0.0", + subcommands: [Twigs.Auth.self] + ) +} + +extension Twigs { + struct Auth: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "commands for authentication with a Twigs server", + subcommands: [Twigs.Auth.Login.self] + ) + } +} + +extension Twigs.Auth { + struct Login: AsyncParsableCommand { + static var configuration = CommandConfiguration( + abstract: "login with an existing account" + ) + + @Option(name: [.short, .long], help: "") + var token: String = "" + + @Option(name: [.short, .long], parsing: SingleValueParsingStrategy.next, help: "the URL to the twigs server") + var url: String = "" + + mutating func runAsync() async throws { + let config = Config(url: url, token: token) + // TODO: Check if token was provided, if not check config file + if let token = await config.token, !token.isEmpty { + print("using token for login") + // TODO: Save token to disk + return + } + print("Username:", terminator: " ") + if let input = readLine(), !input.isEmpty { + await config.setUsername(input) + } else { + throw TwigsErrors.input("ERROR: Username cannot be empty") + } + guard let passChars = getpass("Password: ") else { + throw TwigsErrors.input("ERROR: Unable to read password") + } + let password = String(cString: passChars) + if password.isEmpty { + throw TwigsErrors.input("ERROR: Password cannot be empty") + } + await config.setPassword(password) + if let username = await config.username, let password = await config.password { + print("Logging in as \(username) with password \(password)") + let apiService = TwigsApiService() + apiService.baseUrl = await config.url + try await apiService.login(username: username, password: password) + // TODO: Persist url and token in config file + } + } + } +} + +actor Config { + var url: String? = nil + var username: String? = nil + var password: String? = nil + var token: String? = nil + + init(url: String? = nil, token: String? = nil) { + self.url = url + self.token = token + } + + func setUsername(_ username: String) { + self.username = username + } + + func setPassword(_ password: String) { + self.password = password + } +} + +enum TwigsErrors: Error { + case input(String) +}