diff --git a/apps/macos/.gitignore b/apps/macos/.gitignore new file mode 100644 index 00000000..fda4de3a --- /dev/null +++ b/apps/macos/.gitignore @@ -0,0 +1,62 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output \ No newline at end of file diff --git a/apps/macos/CodeVetter.xcodeproj/project.pbxproj b/apps/macos/CodeVetter.xcodeproj/project.pbxproj new file mode 100644 index 00000000..547aef90 --- /dev/null +++ b/apps/macos/CodeVetter.xcodeproj/project.pbxproj @@ -0,0 +1,355 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 8B90D1482DEDD44A00FBD189 /* CodeVetterFeature in Frameworks */ = {isa = PBXBuildFile; productRef = 8B90D1472DEDD44A00FBD189 /* CodeVetterFeature */; }; + 8C6A50172DF0000100A11CE5 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 8C6A50162DF0000100A11CE5 /* Sparkle */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 8BC265752DEDD3F500F279A4 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8BC265542DEDD3F400F279A4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8BC2655B2DEDD3F400F279A4; + remoteInfo = CodeVetter; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 8BC2655C2DEDD3F400F279A4 /* CodeVetter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CodeVetter.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8BC265742DEDD3F500F279A4 /* CodeVetter.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CodeVetter.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 8BD71C1E2DEE424400CEDD92 /* Exceptions for "Config" folder in "CodeVetter" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Debug.xcconfig, + Release.xcconfig, + Shared.xcconfig, + Tests.xcconfig, + ); + target = 8BC2655B2DEDD3F400F279A4 /* CodeVetter */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 8B90D1BC2DEE373300FBD189 /* Config */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 8BD71C1E2DEE424400CEDD92 /* Exceptions for "Config" folder in "CodeVetter" target */, + ); + path = Config; + sourceTree = ""; + }; + 8BC2655E2DEDD3F400F279A4 /* CodeVetter */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = CodeVetter; + sourceTree = ""; + }; + 8BC265772DEDD3F500F279A4 /* CodeVetterUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = CodeVetterUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8BC265592DEDD3F400F279A4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8B90D1482DEDD44A00FBD189 /* CodeVetterFeature in Frameworks */, + 8C6A50172DF0000100A11CE5 /* Sparkle in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8BC265712DEDD3F500F279A4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 8B90D1462DEDD44A00FBD189 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 8BC265532DEDD3F400F279A4 = { + isa = PBXGroup; + children = ( + 8B90D1BC2DEE373300FBD189 /* Config */, + 8BC2655E2DEDD3F400F279A4 /* CodeVetter */, + 8BC265772DEDD3F500F279A4 /* CodeVetterUITests */, + 8B90D1462DEDD44A00FBD189 /* Frameworks */, + 8BC2655D2DEDD3F400F279A4 /* Products */, + ); + sourceTree = ""; + }; + 8BC2655D2DEDD3F400F279A4 /* Products */ = { + isa = PBXGroup; + children = ( + 8BC2655C2DEDD3F400F279A4 /* CodeVetter.app */, + 8BC265742DEDD3F500F279A4 /* CodeVetter.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8BC2655B2DEDD3F400F279A4 /* CodeVetter */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8BC2657E2DEDD3F500F279A4 /* Build configuration list for PBXNativeTarget "CodeVetter" */; + buildPhases = ( + 8BC265582DEDD3F400F279A4 /* Sources */, + 8BC265592DEDD3F400F279A4 /* Frameworks */, + 8BC2655A2DEDD3F400F279A4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 8B90D1BC2DEE373300FBD189 /* Config */, + 8BC2655E2DEDD3F400F279A4 /* CodeVetter */, + ); + name = CodeVetter; + packageProductDependencies = ( + 8B90D1472DEDD44A00FBD189 /* CodeVetterFeature */, + 8C6A50162DF0000100A11CE5 /* Sparkle */, + ); + productName = CodeVetter; + productReference = 8BC2655C2DEDD3F400F279A4 /* CodeVetter.app */; + productType = "com.apple.product-type.application"; + }; + 8BC265732DEDD3F500F279A4 /* CodeVetterUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8BC265842DEDD3F500F279A4 /* Build configuration list for PBXNativeTarget "CodeVetterUITests" */; + buildPhases = ( + 8BC265702DEDD3F500F279A4 /* Sources */, + 8BC265712DEDD3F500F279A4 /* Frameworks */, + 8BC265722DEDD3F500F279A4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 8BC265762DEDD3F500F279A4 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 8BC265772DEDD3F500F279A4 /* CodeVetterUITests */, + ); + name = CodeVetterUITests; + packageProductDependencies = ( + ); + productName = CodeVetterUITests; + productReference = 8BC265742DEDD3F500F279A4 /* CodeVetter.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8BC265542DEDD3F400F279A4 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1630; + LastUpgradeCheck = 1630; + ORGANIZATIONNAME = "$(ORGANIZATION_NAME)"; + TargetAttributes = { + 8BC2655B2DEDD3F400F279A4 = { + CreatedOnToolsVersion = 16.3; + }; + 8BC265732DEDD3F500F279A4 = { + CreatedOnToolsVersion = 16.3; + TestTargetID = 8BC2655B2DEDD3F400F279A4; + }; + }; + }; + buildConfigurationList = 8BC265572DEDD3F400F279A4 /* Build configuration list for PBXProject "CodeVetter" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8BC265532DEDD3F400F279A4; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 8BC2655D2DEDD3F400F279A4 /* Products */; + packageReferences = ( + 8C6A50152DF0000100A11CE5 /* XCRemoteSwiftPackageReference "Sparkle" */, + ); + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8BC2655B2DEDD3F400F279A4 /* CodeVetter */, + 8BC265732DEDD3F500F279A4 /* CodeVetterUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8BC2655A2DEDD3F400F279A4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8BC265722DEDD3F500F279A4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8BC265582DEDD3F400F279A4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8BC265702DEDD3F500F279A4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 8BC265762DEDD3F500F279A4 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8BC2655B2DEDD3F400F279A4 /* CodeVetter */; + targetProxy = 8BC265752DEDD3F500F279A4 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 8BC2657C2DEDD3F500F279A4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 8BC2657D2DEDD3F500F279A4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 8BC2657F2DEDD3F500F279A4 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 8B90D1BC2DEE373300FBD189 /* Config */; + baseConfigurationReferenceRelativePath = Debug.xcconfig; + buildSettings = { + INFOPLIST_KEY_CFBundleDisplayName = "$(PRODUCT_DISPLAY_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 8BC265802DEDD3F500F279A4 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 8B90D1BC2DEE373300FBD189 /* Config */; + baseConfigurationReferenceRelativePath = Release.xcconfig; + buildSettings = { + INFOPLIST_KEY_CFBundleDisplayName = "$(PRODUCT_DISPLAY_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 8BC265852DEDD3F500F279A4 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 8B90D1BC2DEE373300FBD189 /* Config */; + baseConfigurationReferenceRelativePath = Tests.xcconfig; + buildSettings = { + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 8BC265862DEDD3F500F279A4 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 8B90D1BC2DEE373300FBD189 /* Config */; + baseConfigurationReferenceRelativePath = Tests.xcconfig; + buildSettings = { + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8BC265572DEDD3F400F279A4 /* Build configuration list for PBXProject "CodeVetter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8BC2657C2DEDD3F500F279A4 /* Debug */, + 8BC2657D2DEDD3F500F279A4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8BC2657E2DEDD3F500F279A4 /* Build configuration list for PBXNativeTarget "CodeVetter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8BC2657F2DEDD3F500F279A4 /* Debug */, + 8BC265802DEDD3F500F279A4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8BC265842DEDD3F500F279A4 /* Build configuration list for PBXNativeTarget "CodeVetterUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8BC265852DEDD3F500F279A4 /* Debug */, + 8BC265862DEDD3F500F279A4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 8B90D1472DEDD44A00FBD189 /* CodeVetterFeature */ = { + isa = XCSwiftPackageProductDependency; + productName = CodeVetterFeature; + }; + 8C6A50162DF0000100A11CE5 /* Sparkle */ = { + isa = XCSwiftPackageProductDependency; + package = 8C6A50152DF0000100A11CE5 /* XCRemoteSwiftPackageReference "Sparkle" */; + productName = Sparkle; + }; +/* End XCSwiftPackageProductDependency section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 8C6A50152DF0000100A11CE5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sparkle-project/Sparkle"; + requirement = { + kind = exactVersion; + version = 2.9.6; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + }; + rootObject = 8BC265542DEDD3F400F279A4 /* Project object */; +} diff --git a/apps/macos/CodeVetter.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/apps/macos/CodeVetter.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/apps/macos/CodeVetter.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/apps/macos/CodeVetter.xcodeproj/xcshareddata/xcschemes/CodeVetter.xcscheme b/apps/macos/CodeVetter.xcodeproj/xcshareddata/xcschemes/CodeVetter.xcscheme new file mode 100644 index 00000000..dde4b3be --- /dev/null +++ b/apps/macos/CodeVetter.xcodeproj/xcshareddata/xcschemes/CodeVetter.xcscheme @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/macos/CodeVetter.xcworkspace/contents.xcworkspacedata b/apps/macos/CodeVetter.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..75deb286 --- /dev/null +++ b/apps/macos/CodeVetter.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/apps/macos/CodeVetter.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apps/macos/CodeVetter.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..4a2eee62 --- /dev/null +++ b/apps/macos/CodeVetter.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "5d35d4ce666815b670b48f8180db914be45eeef88ba6a4c90bd194292a8de3ed", + "pins" : [ + { + "identity" : "sparkle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sparkle-project/Sparkle", + "state" : { + "revision" : "ac2def288cbff5cfc7df3ffef6abdf45b72bcb0a", + "version" : "2.9.6" + } + } + ], + "version" : 3 +} diff --git a/apps/macos/CodeVetter/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/macos/CodeVetter/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..afd6910e --- /dev/null +++ b/apps/macos/CodeVetter/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.239", + "green" : "0.678", + "red" : "0.953" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png b/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png new file mode 100644 index 00000000..570a89fe Binary files /dev/null and b/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png differ diff --git a/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png b/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png new file mode 100644 index 00000000..addd3bea Binary files /dev/null and b/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png differ diff --git a/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png b/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png new file mode 100644 index 00000000..4e5348b8 Binary files /dev/null and b/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png differ diff --git a/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png b/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png new file mode 100644 index 00000000..c21ebcde Binary files /dev/null and b/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png differ diff --git a/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png b/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png new file mode 100644 index 00000000..a0a54863 Binary files /dev/null and b/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png differ diff --git a/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png b/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png new file mode 100644 index 00000000..71721bb0 Binary files /dev/null and b/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png differ diff --git a/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-64.png b/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-64.png new file mode 100644 index 00000000..bdf643bf Binary files /dev/null and b/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/AppIcon-64.png differ diff --git a/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..f17bd734 --- /dev/null +++ b/apps/macos/CodeVetter/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "AppIcon-16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "AppIcon-32.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "AppIcon-32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "AppIcon-64.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "AppIcon-128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "AppIcon-256.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "AppIcon-256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "AppIcon-512.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "AppIcon-512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "AppIcon-1024.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/macos/CodeVetter/Assets.xcassets/Contents.json b/apps/macos/CodeVetter/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/apps/macos/CodeVetter/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/macos/CodeVetter/CodeVetter.xctestplan b/apps/macos/CodeVetter/CodeVetter.xctestplan new file mode 100644 index 00000000..f0cc8868 --- /dev/null +++ b/apps/macos/CodeVetter/CodeVetter.xctestplan @@ -0,0 +1,35 @@ +{ + "configurations" : [ + { + "id" : "CA3AAE1D-B5A1-4A59-8FA5-F07765DC5EB6", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "targetForVariableExpansion" : { + "containerPath" : "container:CodeVetter.xcodeproj", + "identifier" : "8BC2655B2DEDD3F400F279A4", + "name" : "CodeVetter" + } + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:CodeVetter.xcodeproj", + "identifier" : "8BC265732DEDD3F500F279A4", + "name" : "CodeVetterUITests" + } + }, + { + "target" : { + "containerPath" : "container:CodeVetterPackage", + "identifier" : "CodeVetterFeatureTests", + "name" : "CodeVetterFeatureTests" + } + } + ], + "version" : 1 +} diff --git a/apps/macos/CodeVetter/CodeVetterApp.swift b/apps/macos/CodeVetter/CodeVetterApp.swift new file mode 100644 index 00000000..0942ec45 --- /dev/null +++ b/apps/macos/CodeVetter/CodeVetterApp.swift @@ -0,0 +1,211 @@ +import AppKit +import CodeVetterFeature +import SwiftUI + +@main +@MainActor +final class CodeVetterAppDelegate: NSObject, NSApplicationDelegate { + private static let retainedDelegate = CodeVetterAppDelegate() + private let model = WorkbenchModel(repositoryAccessStore: RepositoryAccessStore.standard) + private let updater = NativeUpdaterController() + private var windowController: NSWindowController? + + static func main() { + let application = NSApplication.shared + application.setActivationPolicy(.regular) + application.appearance = requestedAppearance + application.delegate = retainedDelegate + _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) + } + + private static var requestedAppearance: NSAppearance? { + guard + let flagIndex = CommandLine.arguments.firstIndex(of: "--appearance"), + CommandLine.arguments.indices.contains(flagIndex + 1) + else { return nil } + + return switch CommandLine.arguments[flagIndex + 1].lowercased() { + case "light": NSAppearance(named: .aqua) + case "dark": NSAppearance(named: .darkAqua) + default: nil + } + } + + func applicationDidFinishLaunching(_ notification: Notification) { + applyRequestedInitialState() + showMainWindowIfNeeded() + } + + private func applyRequestedInitialState() { + if let repository = launchValue(after: "--ui-test-repository") { + model.selectRepository( + URL(fileURLWithPath: repository, isDirectory: true), + persist: false + ) + } + if let sectionName = launchValue(after: "--ui-test-section"), + let section = WorkbenchSection.allCases.first(where: { $0.rawValue == sectionName }) + { + model.section = section + } + } + + private func launchValue(after flag: String) -> String? { + guard let index = CommandLine.arguments.firstIndex(of: flag), + CommandLine.arguments.indices.contains(index + 1) + else { return nil } + let value = CommandLine.arguments[index + 1] + return value.isEmpty ? nil : value + } + + private func showMainWindowIfNeeded() { + guard windowController == nil else { return } + installMainMenu() + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 1_280, height: 800), + styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], + backing: .buffered, + defer: false + ) + window.title = "CodeVetter" + window.titleVisibility = .hidden + window.titlebarAppearsTransparent = true + window.toolbarStyle = .unified + window.minSize = NSSize(width: 980, height: 640) + window.center() + window.contentViewController = NSHostingController( + rootView: PremiumWorkbenchRootView(model: model)) + window.isReleasedWhenClosed = false + window.tabbingMode = .preferred + + let controller = NSWindowController(window: window) + windowController = controller + controller.showWindow(nil) + DispatchQueue.main.async { [weak window] in + window?.makeKeyAndOrderFront(nil) + NSApp.activate() + } + } + + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + true + } + + func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool + { + guard !flag, let window = windowController?.window else { return true } + window.makeKeyAndOrderFront(nil) + sender.activate() + return true + } + + private func installMainMenu() { + let mainMenu = NSMenu() + + let appItem = NSMenuItem() + let appMenu = NSMenu(title: "CodeVetter") + appMenu.addItem( + withTitle: "About CodeVetter", + action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), + keyEquivalent: "" + ) + let updates = appMenu.addItem( + withTitle: "Check for Updates…", + action: #selector(NativeUpdaterController.checkForUpdates(_:)), + keyEquivalent: "" + ) + updates.target = updater + updates.toolTip = updater.configuration.status + appMenu.addItem(.separator()) + appMenu.addItem(makeMenuItem("Settings…", action: #selector(showSettings), key: ",")) + appMenu.addItem(.separator()) + appMenu.addItem( + withTitle: "Hide CodeVetter", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h") + let hideOthers = appMenu.addItem( + withTitle: "Hide Others", action: #selector(NSApplication.hideOtherApplications(_:)), + keyEquivalent: "h") + hideOthers.keyEquivalentModifierMask = [.command, .option] + appMenu.addItem( + withTitle: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), + keyEquivalent: "") + appMenu.addItem(.separator()) + appMenu.addItem( + withTitle: "Quit CodeVetter", action: #selector(NSApplication.terminate(_:)), + keyEquivalent: "q") + appItem.submenu = appMenu + mainMenu.addItem(appItem) + + let fileItem = NSMenuItem() + let fileMenu = NSMenu(title: "File") + fileMenu.addItem( + makeMenuItem("Open Repository…", action: #selector(openRepository), key: "o")) + fileMenu.addItem(.separator()) + fileMenu.addItem( + withTitle: "Close", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w") + fileItem.submenu = fileMenu + mainMenu.addItem(fileItem) + + let editItem = NSMenuItem() + let editMenu = NSMenu(title: "Edit") + editMenu.addItem(withTitle: "Undo", action: Selector(("undo:")), keyEquivalent: "z") + let redo = editMenu.addItem(withTitle: "Redo", action: Selector(("redo:")), keyEquivalent: "Z") + redo.keyEquivalentModifierMask = [.command, .shift] + editMenu.addItem(.separator()) + editMenu.addItem(withTitle: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x") + editMenu.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c") + editMenu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v") + editMenu.addItem( + withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a") + editItem.submenu = editMenu + mainMenu.addItem(editItem) + + let viewItem = NSMenuItem() + let viewMenu = NSMenu(title: "View") + viewMenu.addItem( + makeMenuItem("Command Palette…", action: #selector(showCommandPalette), key: "k")) + viewMenu.addItem(.separator()) + for (index, section) in WorkbenchSection.allCases.enumerated() { + let item = makeMenuItem( + section.rawValue, action: #selector(showSection(_:)), key: String(index + 1)) + item.tag = index + viewMenu.addItem(item) + } + viewItem.submenu = viewMenu + mainMenu.addItem(viewItem) + + let windowItem = NSMenuItem() + let windowMenu = NSMenu(title: "Window") + windowMenu.addItem( + withTitle: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m") + windowMenu.addItem( + withTitle: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: "") + windowItem.submenu = windowMenu + mainMenu.addItem(windowItem) + NSApp.windowsMenu = windowMenu + NSApp.mainMenu = mainMenu + } + + private func makeMenuItem(_ title: String, action: Selector, key: String) -> NSMenuItem { + let item = NSMenuItem(title: title, action: action, keyEquivalent: key) + item.target = self + return item + } + + @objc private func openRepository() { + model.section = .review + model.choosingRepository = true + } + + @objc private func showSettings() { + model.section = .settings + } + + @objc private func showCommandPalette() { + model.commandPalettePresented = true + } + + @objc private func showSection(_ sender: NSMenuItem) { + guard WorkbenchSection.allCases.indices.contains(sender.tag) else { return } + model.section = WorkbenchSection.allCases[sender.tag] + } +} diff --git a/apps/macos/CodeVetter/NativeUpdaterController.swift b/apps/macos/CodeVetter/NativeUpdaterController.swift new file mode 100644 index 00000000..2be52b3d --- /dev/null +++ b/apps/macos/CodeVetter/NativeUpdaterController.swift @@ -0,0 +1,37 @@ +import AppKit +import CodeVetterFeature +import Sparkle + +@MainActor +final class NativeUpdaterController: NSObject, NSMenuItemValidation { + static let frameworkVersion = "2.9.6" + + let configuration: NativeUpdaterConfiguration + private let controller: SPUStandardUpdaterController + + override init() { + configuration = NativeUpdaterConfiguration() + controller = SPUStandardUpdaterController( + startingUpdater: false, + updaterDelegate: nil, + userDriverDelegate: nil + ) + super.init() + if configuration.ready { + controller.startUpdater() + } + } + + var canCheckForUpdates: Bool { + configuration.ready && controller.updater.canCheckForUpdates + } + + @objc func checkForUpdates(_ sender: Any?) { + guard configuration.ready else { return } + controller.checkForUpdates(sender) + } + + func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { + menuItem.action == #selector(checkForUpdates(_:)) ? canCheckForUpdates : true + } +} diff --git a/apps/macos/CodeVetterPackage/.gitignore b/apps/macos/CodeVetterPackage/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/apps/macos/CodeVetterPackage/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/apps/macos/CodeVetterPackage/Package.swift b/apps/macos/CodeVetterPackage/Package.swift new file mode 100644 index 00000000..c50e13d1 --- /dev/null +++ b/apps/macos/CodeVetterPackage/Package.swift @@ -0,0 +1,35 @@ +// swift-tools-version: 6.1 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "CodeVetterFeature", + platforms: [.macOS(.v14)], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "CodeVetterFeature", + targets: ["CodeVetterFeature"] + ), + .executable( + name: "NativeBridgeBenchmark", + targets: ["NativeBridgeBenchmark"] + ), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "CodeVetterFeature", + resources: [.process("Resources")] + ), + .testTarget( + name: "CodeVetterFeatureTests", + dependencies: [ + "CodeVetterFeature" + ] + ), + .executableTarget(name: "NativeBridgeBenchmark"), + ] +) diff --git a/apps/macos/CodeVetterPackage/Sources/CodeVetterFeature/UnpackModels.swift b/apps/macos/CodeVetterPackage/Sources/CodeVetterFeature/UnpackModels.swift new file mode 100644 index 00000000..0ce932a4 --- /dev/null +++ b/apps/macos/CodeVetterPackage/Sources/CodeVetterFeature/UnpackModels.swift @@ -0,0 +1,854 @@ +import Foundation + +public struct UnpackSnapshotSummary: Codable, Identifiable, Sendable { + public let id: String + public let repoPath: String + public let repoName: String + public let commitSHA: String? + public let status: String + public let errorMessage: String? + public let agentUsed: String? + public let modelUsed: String? + public let filesScanned: Int + public let filesSkipped: Int + public let runtimeMS: Int? + public let costUSD: Double? + public let startedAt: String? + public let completedAt: String? + public let createdAt: String + public let analysisReady: Bool + + enum CodingKeys: String, CodingKey { + case id, status + case repoPath = "repo_path" + case repoName = "repo_name" + case commitSHA = "commit_sha" + case errorMessage = "error_message" + case agentUsed = "agent_used" + case modelUsed = "model_used" + case filesScanned = "files_scanned" + case filesSkipped = "files_skipped" + case runtimeMS = "runtime_ms" + case costUSD = "cost_usd" + case startedAt = "started_at" + case completedAt = "completed_at" + case createdAt = "created_at" + case analysisReady = "analysis_ready" + } +} + +public struct UnpackHistoryReceipt: Codable, Sendable { + public let schemaVersion: String + public let generatedAt: String + public let databaseAvailable: Bool + public let repoPath: String? + public let limit: Int + public let returned: Int + public let reports: [UnpackSnapshotSummary] + + enum CodingKeys: String, CodingKey { + case limit, returned, reports + case schemaVersion = "schema_version" + case generatedAt = "generated_at" + case databaseAvailable = "database_available" + case repoPath = "repo_path" + } +} + +public struct UnpackSnapshotRecord: Codable, Identifiable, Sendable { + public let id: String + public let repoPath: String + public let repoName: String + public let commitSHA: String? + public let status: String + public let errorMessage: String? + public let agentUsed: String? + public let modelUsed: String? + public let filesScanned: Int + public let filesSkipped: Int + public let runtimeMS: Int? + public let costUSD: Double? + public let startedAt: String? + public let completedAt: String? + public let createdAt: String + public let analysisReady: Bool + public let inventoryJSON: String? + public let reportJSON: String? + public let bytesScanned: Int + + enum CodingKeys: String, CodingKey { + case id, status + case repoPath = "repo_path" + case repoName = "repo_name" + case commitSHA = "commit_sha" + case errorMessage = "error_message" + case agentUsed = "agent_used" + case modelUsed = "model_used" + case filesScanned = "files_scanned" + case filesSkipped = "files_skipped" + case runtimeMS = "runtime_ms" + case costUSD = "cost_usd" + case startedAt = "started_at" + case completedAt = "completed_at" + case createdAt = "created_at" + case analysisReady = "analysis_ready" + case inventoryJSON = "inventory_json" + case reportJSON = "report_json" + case bytesScanned = "bytes_scanned" + } + + public func decodeInventory() throws -> UnpackInventory? { + guard let inventoryJSON else { return nil } + return try JSONDecoder().decode(UnpackInventory.self, from: Data(inventoryJSON.utf8)) + } + + public func decodeReport() throws -> UnpackReport? { + guard let reportJSON else { return nil } + return try JSONDecoder().decode(UnpackReport.self, from: Data(reportJSON.utf8)) + } +} + +public struct UnpackScanProfileStep: Codable, Identifiable, Sendable { + public let id: String + public let label: String + public let ms: UInt64 + public let pct: Double +} + +public struct UnpackScanProfile: Codable, Identifiable, Sendable { + public let stage: String + public let totalMS: UInt64 + public let peakRSSBytes: UInt64? + public let steps: [UnpackScanProfileStep] + public var id: String { stage } + + enum CodingKeys: String, CodingKey { + case stage, steps + case totalMS = "total_ms" + case peakRSSBytes = "peak_rss_bytes" + } +} + +public struct UnpackScanReceipt: Codable, Sendable { + public let schemaVersion: String + public let reportID: String + public let status: String + public let createdAt: String + public let inventory: UnpackInventory + public let profiles: [UnpackScanProfile] + + enum CodingKeys: String, CodingKey { + case status, inventory, profiles + case schemaVersion = "schema_version" + case reportID = "report_id" + case createdAt = "created_at" + } +} + +public enum RepositoryQueryDomain: String, Codable, CaseIterable, Identifiable, Sendable { + case graph + case history + + public var id: String { rawValue } + + public var label: String { + switch self { + case .graph: "Structure" + case .history: "History" + } + } +} + +public enum RepositoryQueryMode: String, Codable, Sendable { + case search + case explain + case impact + case path + case trace +} + +public enum RepositoryGraphDirection: String, Codable, CaseIterable, Identifiable, Sendable { + case incoming + case outgoing + case both + + public var id: String { rawValue } +} + +public enum RepositoryHistorySelectorKind: String, Codable, Sendable { + case event + case entity + case revision + case release + case episode +} + +public struct RepositoryQueryReceipt: Codable, Sendable { + public let schemaVersion: String + public let authority: String + public let repoPath: String + public let query: String + public let domain: RepositoryQueryDomain + public let mode: RepositoryQueryMode + public let target: String? + public let direction: RepositoryGraphDirection? + public let depth: Int? + public let historySelector: RepositoryHistorySelectorKind? + public let limit: Int + public let status: String + public let issue: String? + public let graphStatus: RepositoryGraphStatus + public let historyStatus: RepositoryHistoryStatus + public let graphResult: RepositoryGraphSearchResult? + public let graphExplanation: RepositoryGraphExplanation? + public let graphImpact: RepositoryGraphImpactResult? + public let graphPath: RepositoryGraphPathResult? + public let historyResult: RepositoryHistorySearchResult? + public let historyTrace: RepositoryHistoryTrace? + + public var resultCount: Int { + graphResult?.hits.count + ?? graphImpact?.affected.count + ?? graphPath?.nodes.count + ?? historyResult?.items.count + ?? historyTrace?.episodes.count + ?? (graphExplanation == nil ? 0 : 1) + } + + enum CodingKeys: String, CodingKey { + case authority, query, domain, mode, target, direction, depth, limit, status, issue + case schemaVersion = "schema_version" + case repoPath = "repo_path" + case historySelector = "history_selector" + case graphStatus = "graph_status" + case historyStatus = "history_status" + case graphResult = "graph_result" + case graphExplanation = "graph_explanation" + case graphImpact = "graph_impact" + case graphPath = "graph_path" + case historyResult = "history_result" + case historyTrace = "history_trace" + } +} + +public struct RepositoryGraphStatus: Codable, Sendable { + public let indexed: Bool + public let stale: Bool + public let currentHead: String? + public let indexedHead: String? + public let snapshotID: String? + public let engineID: String? + public let engineVersion: String? + public let indexedFiles: Int + public let nodeCount: Int + public let edgeCount: Int + public let truncated: Bool + + enum CodingKeys: String, CodingKey { + case indexed, stale, truncated + case currentHead = "current_head" + case indexedHead = "indexed_head" + case snapshotID = "snapshot_id" + case engineID = "engine_id" + case engineVersion = "engine_version" + case indexedFiles = "indexed_files" + case nodeCount = "node_count" + case edgeCount = "edge_count" + } +} + +public struct RepositoryHistoryStatus: Codable, Sendable { + public let indexed: Bool + public let stale: Bool + public let currentHead: String + public let indexedHead: String? + public let checkpointCount: Int + public let eventCount: Int + public let updatedAt: String? + + enum CodingKeys: String, CodingKey { + case indexed, stale + case currentHead = "current_head" + case indexedHead = "indexed_head" + case checkpointCount = "checkpoint_count" + case eventCount = "event_count" + case updatedAt = "updated_at" + } +} + +public struct RepositorySourceAnchor: Codable, Identifiable, Sendable { + public let path: String + public let startLine: UInt? + public let endLine: UInt? + public var id: String { "\(path)\u{0}\(startLine ?? 0)\u{0}\(endLine ?? 0)" } + + enum CodingKeys: String, CodingKey { + case path + case startLine = "start_line" + case endLine = "end_line" + } +} + +public struct RepositoryGraphQueryNode: Codable, Identifiable, Sendable { + public let id: String + public let kind: String + public let label: String + public let qualifiedName: String? + public let path: String? + public let detail: String? + public let language: String? + public let communityID: String? + public let trust: String + public let sources: [RepositorySourceAnchor] + + enum CodingKeys: String, CodingKey { + case id, kind, label, path, detail, language, trust, sources + case qualifiedName = "qualified_name" + case communityID = "community_id" + } +} + +public struct RepositoryGraphSearchHit: Codable, Identifiable, Sendable { + public let node: RepositoryGraphQueryNode + public let score: UInt + public let matchedBy: String + public var id: String { node.id } + + enum CodingKeys: String, CodingKey { + case node, score + case matchedBy = "matched_by" + } +} + +public struct RepositoryGraphSearchResult: Codable, Sendable { + public let hits: [RepositoryGraphSearchHit] + public let truncated: Bool + public let nextCursor: String? + + enum CodingKeys: String, CodingKey { + case hits, truncated + case nextCursor = "next_cursor" + } +} + +public struct RepositoryGraphQueryEdge: Codable, Identifiable, Sendable { + public let id: String + public let from: String + public let to: String + public let kind: String + public let evidence: String + public let trust: String + public let sources: [RepositorySourceAnchor] +} + +public struct RepositoryGraphExplanation: Codable, Sendable { + public let node: RepositoryGraphQueryNode + public let incomingCount: Int + public let outgoingCount: Int + public let incomingKinds: [String] + public let outgoingKinds: [String] + public let truncated: Bool + + enum CodingKeys: String, CodingKey { + case node, truncated + case incomingCount = "incoming_count" + case outgoingCount = "outgoing_count" + case incomingKinds = "incoming_kinds" + case outgoingKinds = "outgoing_kinds" + } +} + +public struct RepositoryGraphImpactResult: Codable, Sendable { + public let root: RepositoryGraphQueryNode + public let affected: [RepositoryGraphQueryNode] + public let edges: [RepositoryGraphQueryEdge] + public let depthReached: Int + public let truncated: Bool + + enum CodingKeys: String, CodingKey { + case root, affected, edges, truncated + case depthReached = "depth_reached" + } +} + +public struct RepositoryGraphPathResult: Codable, Sendable { + public let nodes: [RepositoryGraphQueryNode] + public let edges: [RepositoryGraphQueryEdge] + public let totalCost: Double + public let visited: Int + public let truncated: Bool + + enum CodingKeys: String, CodingKey { + case nodes, edges, visited, truncated + case totalCost = "total_cost" + } +} + +public struct RepositoryHistorySearchItem: Codable, Identifiable, Sendable { + public let kind: String + public let id: String + public let label: String + public let summary: String + public let revision: String? + public let recordedAt: String? + public let trust: String + public let sourceIDs: [String] + + enum CodingKeys: String, CodingKey { + case kind, id, label, summary, revision, trust + case recordedAt = "recorded_at" + case sourceIDs = "source_ids" + } +} + +public struct RepositoryHistorySearchResult: Codable, Sendable { + public let schemaVersion: Int + public let items: [RepositoryHistorySearchItem] + public let truncated: Bool + public let nextOffset: Int? + + enum CodingKeys: String, CodingKey { + case items, truncated + case schemaVersion = "schema_version" + case nextOffset = "next_offset" + } +} + +public struct RepositoryHistoryTrace: Codable, Sendable { + public let schemaVersion: Int + public let repoPath: String + public let episodes: [RepositoryHistoryEpisode] + public let indexedHead: String + public let stale: Bool + public let gaps: [String] + public let scannedEvents: Int + public let totalEvents: Int + public let truncated: Bool + + enum CodingKeys: String, CodingKey { + case episodes, stale, gaps, truncated + case schemaVersion = "schema_version" + case repoPath = "repo_path" + case indexedHead = "indexed_head" + case scannedEvents = "scanned_events" + case totalEvents = "total_events" + } +} + +public struct RepositoryHistoryEpisode: Codable, Identifiable, Sendable { + public let id: String + public let events: [RepositoryHistoryCausalEvent] + public let links: [RepositoryHistoryCausalLink] + public let qualifiedLeads: [RepositoryHistoryCausalLink] + public let stagesPresent: [String] + public let gaps: [String] + public let contradictions: [String] + public let startedAt: String + public let endedAt: String + public let truncated: Bool + + enum CodingKeys: String, CodingKey { + case id, events, links, gaps, contradictions, truncated + case qualifiedLeads = "qualified_leads" + case stagesPresent = "stages_present" + case startedAt = "started_at" + case endedAt = "ended_at" + } +} + +public struct RepositoryHistoryCausalEvent: Codable, Identifiable, Sendable { + public let id: String + public let revisionSHA: String? + public let eventKind: String + public let stage: String + public let summary: String + public let trust: String + public let origin: String + public let sourceID: String + public let recordedAt: String + public let sources: [RepositorySourceAnchor] + + enum CodingKeys: String, CodingKey { + case id, stage, summary, trust, origin, sources + case revisionSHA = "revision_sha" + case eventKind = "event_kind" + case sourceID = "source_id" + case recordedAt = "recorded_at" + } +} + +public struct RepositoryHistoryCausalLink: Codable, Identifiable, Sendable { + public let id: String + public let fromEventID: String + public let toEventID: String + public let relation: String + public let status: String + public let trust: String + public let evidence: String + public let sources: [RepositorySourceAnchor] + + enum CodingKeys: String, CodingKey { + case id, relation, status, trust, evidence, sources + case fromEventID = "from_event_id" + case toEventID = "to_event_id" + } +} + +public struct UnpackSnapshotChangedFile: Codable, Identifiable, Sendable { + public let path: String + public let additions: UInt64 + public let deletions: UInt64 + public var id: String { path } +} + +public struct UnpackSnapshotCommit: Codable, Identifiable, Sendable { + public let sha: String + public let date: String + public let author: String + public let subject: String + public let additions: UInt64 + public let deletions: UInt64 + public let files: [UnpackSnapshotChangedFile] + public var id: String { sha } +} + +public struct UnpackSnapshotComparison: Codable, Sendable { + public let baseCommit: String + public let headCommit: String + public let commitCount: UInt64 + public let commits: [UnpackSnapshotCommit] + public let truncated: Bool + + enum CodingKeys: String, CodingKey { + case commits, truncated + case baseCommit = "base_commit" + case headCommit = "head_commit" + case commitCount = "commit_count" + } +} + +public enum UnpackExportFormat: String, CaseIterable, Identifiable, Sendable { + case markdown + case html + case repositoryGraphJSON = "repo_graph_json" + case agentContextMarkdown = "agent_context_markdown" + case repositoryMemoryMarkdown = "repo_memory_markdown" + + public var id: String { rawValue } + + public var label: String { + switch self { + case .markdown: "Full Markdown" + case .html: "Offline HTML" + case .repositoryGraphJSON: "Repository graph JSON" + case .agentContextMarkdown: "Agent context Markdown" + case .repositoryMemoryMarkdown: "Repository memory Markdown" + } + } + + public var pathExtension: String { + switch self { + case .html: "html" + case .repositoryGraphJSON: "json" + default: "md" + } + } +} + +public struct UnpackExportReceipt: Codable, Sendable { + public let schemaVersion: String + public let reportID: String + public let format: String + public let content: String + + enum CodingKeys: String, CodingKey { + case format, content + case schemaVersion = "schema_version" + case reportID = "report_id" + } +} + +public struct UnpackReportClaim: Codable, Identifiable, Sendable { + public let claim: String + public let sources: [String] + public let kind: String? + public var id: String { "\(claim)\u{0}\(sources.first ?? "")" } +} + +public struct UnpackReportSection: Codable, Sendable { + public let title: String + public let summary: String + public let claims: [UnpackReportClaim] +} + +public struct UnpackReport: Codable, Sendable { + public let systemMap: UnpackReportSection? + public let featureCatalog: UnpackReportSection? + public let dataFlow: UnpackReportSection? + public let behaviorTraces: UnpackReportSection? + public let testingSignals: UnpackReportSection? + public let riskMap: UnpackReportSection? + public let extensionPoints: UnpackReportSection? + public let agentHandoff: UnpackReportSection? + public let agentPrompt: String? + public let overview: String? + + enum CodingKeys: String, CodingKey { + case overview + case systemMap = "system_map" + case featureCatalog = "feature_catalog" + case dataFlow = "data_flow" + case behaviorTraces = "behavior_traces" + case testingSignals = "testing_signals" + case riskMap = "risk_map" + case extensionPoints = "extension_points" + case agentHandoff = "agent_handoff" + case agentPrompt = "agent_prompt" + } +} + +public struct UnpackLanguage: Codable, Identifiable, Sendable { + public let language: String + public let files: Int + public let bytes: UInt64 + public var id: String { language } +} + +public struct UnpackManifest: Codable, Identifiable, Sendable { + public let path: String + public let kind: String + public let name: String? + public let version: String? + public let dependencies: [String] + public let scripts: [String] + public var id: String { path } +} + +public struct UnpackEntrypoint: Codable, Identifiable, Sendable { + public let path: String + public let kind: String + public let reason: String + public var id: String { path } +} + +public struct UnpackDirectorySummary: Codable, Identifiable, Sendable { + public let path: String + public let fileCount: Int + public let bytes: UInt64 + public var id: String { path } + + enum CodingKeys: String, CodingKey { + case path, bytes + case fileCount = "file_count" + } +} + +public struct UnpackDocument: Codable, Identifiable, Sendable { + public let path: String + public let bytes: UInt64 + public let preview: String + public var id: String { path } +} + +public struct UnpackWorkspaceUnit: Codable, Identifiable, Sendable { + public let path: String + public let name: String + public let kind: String + public let manifestPath: String? + public let buildSystem: String? + public let fileCount: Int + public let languages: [UnpackLanguage] + public let scripts: [String] + public let entrypoints: [String] + public let testFiles: [String] + public let tags: [String] + public var id: String { path } + + enum CodingKeys: String, CodingKey { + case path, name, kind, languages, scripts, entrypoints, tags + case manifestPath = "manifest_path" + case buildSystem = "build_system" + case fileCount = "file_count" + case testFiles = "test_files" + } +} + +public struct UnpackDirectoryNode: Codable, Identifiable, Sendable { + public let name: String + public let path: String + public let isDirectory: Bool + public let fileCount: Int + public let children: [UnpackDirectoryNode] + public var id: String { path.isEmpty ? "root" : path } + + enum CodingKeys: String, CodingKey { + case name, path, children + case isDirectory = "is_dir" + case fileCount = "file_count" + } +} + +public struct UnpackGraph: Codable, Sendable { + public let schemaVersion: Int + public let nodes: [UnpackGraphNode] + public let edges: [UnpackGraphEdge] + public let truncated: Bool + + enum CodingKeys: String, CodingKey { + case nodes, edges, truncated + case schemaVersion = "schema_version" + } +} + +public struct UnpackGraphNode: Codable, Identifiable, Sendable { + public let id: String + public let kind: String + public let label: String + public let path: String? + public let detail: String? + public let sources: [String] +} + +public struct UnpackGraphEdge: Codable, Identifiable, Sendable { + public let from: String + public let to: String + public let kind: String + public let evidence: String + public let sources: [String] + public let trust: String + public var id: String { "\(from)\u{0}\(kind)\u{0}\(to)" } +} + +public struct UnpackHistoryBrief: Codable, Sendable { + public let summary: String + public let recentCommits: [UnpackHistoryCommit] + public let decisions: [UnpackHistoryDecision] + public let testHints: [UnpackTestHint] + public let sources: [String] + public let truncated: Bool + + enum CodingKeys: String, CodingKey { + case summary, decisions, sources, truncated + case recentCommits = "recent_commits" + case testHints = "test_hints" + } +} + +public struct UnpackHistoryCommit: Codable, Identifiable, Sendable { + public let sha: String + public let date: String? + public let subject: String + public let files: [String] + public var id: String { sha } +} + +public struct UnpackHistoryDecision: Codable, Identifiable, Sendable { + public let marker: String + public let text: String + public let source: String + public var id: String { "\(marker)\u{0}\(source)" } +} + +public struct UnpackTestHint: Codable, Identifiable, Sendable { + public let path: String + public let reason: String + public var id: String { path } +} + +public struct UnpackHealth: Codable, Sendable { + public let summary: String + public let averageScore: Double + public let hotspotCount: Int + public let filesAnalyzed: Int + public let filesWithTestSignal: Int + public let topFiles: [UnpackHealthFile] + public let truncated: Bool + + enum CodingKeys: String, CodingKey { + case summary, truncated + case averageScore = "average_score" + case hotspotCount = "hotspot_count" + case filesAnalyzed = "files_analyzed" + case filesWithTestSignal = "files_with_test_signal" + case topFiles = "top_files" + } +} + +public struct UnpackHealthFile: Codable, Identifiable, Sendable { + public let path: String + public let score: Double + public let bucket: String + public let lines: Int + public let bytes: UInt64 + public let churn: Int + public let hasTestSignal: Bool + public let refactoringTargets: [String] + public var id: String { path } + + enum CodingKeys: String, CodingKey { + case path, score, bucket, lines, bytes, churn + case hasTestSignal = "has_test_signal" + case refactoringTargets = "refactoring_targets" + } +} + +public struct UnpackCoverage: Codable, Sendable { + public let strategy: String + public let sampledFiles: Int + public let totalFiles: Int? + public let samplePercent: Double? + public let notes: [String] + + enum CodingKeys: String, CodingKey { + case strategy, notes + case sampledFiles = "sampled_files" + case totalFiles = "total_files" + case samplePercent = "sample_percent" + } +} + +public struct UnpackInventory: Codable, Sendable { + public let repoPath: String + public let repoName: String + public let commitSHA: String? + public let branch: String? + public let filesScanned: Int + public let filesSkipped: Int + public let bytesScanned: UInt64 + public let maxFilesHit: Bool + public let languages: [UnpackLanguage] + public let manifests: [UnpackManifest] + public let entrypoints: [UnpackEntrypoint] + public let topLevelDirectories: [UnpackDirectorySummary] + public let documents: [UnpackDocument] + public let configFiles: [String] + public let stackTags: [String] + public let workspaceUnits: [UnpackWorkspaceUnit] + public let graph: UnpackGraph + public let history: UnpackHistoryBrief + public let health: UnpackHealth + public let coverage: UnpackCoverage + public let allFilesCapped: Bool + public let directoryTree: UnpackDirectoryNode + + enum CodingKeys: String, CodingKey { + case branch, languages, manifests, entrypoints, coverage + case repoPath = "repo_path" + case repoName = "repo_name" + case commitSHA = "commit_sha" + case filesScanned = "files_scanned" + case filesSkipped = "files_skipped" + case bytesScanned = "bytes_scanned" + case maxFilesHit = "max_files_hit" + case topLevelDirectories = "top_level_dirs" + case documents = "docs" + case configFiles = "config_files" + case stackTags = "stack_tags" + case workspaceUnits = "workspace_units" + case graph = "repo_graph" + case history = "history_brief" + case health = "repo_health" + case allFilesCapped = "all_files_capped" + case directoryTree = "dir_tree_preview" + } +} diff --git a/apps/macos/CodeVetterUITests/CodeVetterUITests.swift b/apps/macos/CodeVetterUITests/CodeVetterUITests.swift new file mode 100644 index 00000000..a8d10ca1 --- /dev/null +++ b/apps/macos/CodeVetterUITests/CodeVetterUITests.swift @@ -0,0 +1,303 @@ +import XCTest + +final class CodeVetterUITests: XCTestCase { + private var testingRepository: URL! + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + testingRepository = FileManager.default.temporaryDirectory + .appending(path: "codevetter-ui-repository-\(UUID().uuidString)") + try FileManager.default.createDirectory( + at: testingRepository.appending(path: ".git"), + withIntermediateDirectories: true + ) + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + if let testingRepository { + try? FileManager.default.removeItem(at: testingRepository) + } + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testPrimaryWorkbenchIsVisible() throws { + let app = XCUIApplication() + app.launch() + XCTAssertTrue(app.staticTexts["Can this change ship?"].waitForExistence(timeout: 5)) + XCTAssertTrue(app.buttons["Choose repository"].exists) + XCTAssertTrue(app.staticTexts["PROOF SEQUENCE"].exists) + for destination in [ + "Usage", "Repo Unpack", "Review", "Testing", "Performance", "Runs", "Capabilities", + "Settings", + ] { + XCTAssertTrue(app.buttons[destination].exists, "Missing retained surface: \(destination)") + } + + app.menuBars.menuBarItems["View"].click() + XCTAssertTrue(app.menuItems["Usage"].exists) + app.typeKey(.escape, modifierFlags: []) + app.typeKey("1", modifierFlags: .command) + XCTAssertTrue(app.staticTexts["LOCAL COMPUTE LEDGER"].waitForExistence(timeout: 5)) + + app.buttons["Runs"].click() + assertSelected(app.buttons["Runs"]) + XCTAssertTrue(app.staticTexts["EVIDENCE LEDGER"].waitForExistence(timeout: 5)) + } + + @MainActor + func testCommandPaletteSearchesAndOpensAWorkspaceFromTheKeyboard() throws { + let app = XCUIApplication() + app.launch() + XCTAssertTrue(app.staticTexts["Can this change ship?"].waitForExistence(timeout: 5)) + app.activate() + + let palette = app.descendants(matching: .any)["command-palette"] + openCommandPaletteWithKeyboard(app, palette: palette) + let search = app.textFields["command-palette-search"] + XCTAssertTrue(search.waitForExistence(timeout: 2)) + search.typeText("Performance") + search.typeKey(.return, modifierFlags: []) + + XCTAssertTrue(palette.waitForNonExistence(timeout: 3)) + assertSelected(app.buttons["workbench-section-performance"]) + XCTAssertTrue(app.staticTexts["Measure what changed."].waitForExistence(timeout: 2)) + + openCommandPaletteWithKeyboard(app, palette: palette) + let reopenedSearch = app.textFields["command-palette-search"] + XCTAssertTrue(reopenedSearch.waitForExistence(timeout: 2)) + reopenedSearch.typeKey(.downArrow, modifierFlags: []) + reopenedSearch.typeKey(.downArrow, modifierFlags: []) + reopenedSearch.typeKey(.return, modifierFlags: []) + XCTAssertTrue(palette.waitForNonExistence(timeout: 3)) + assertSelected(app.buttons["workbench-section-review"]) + + openCommandPaletteWithKeyboard(app, palette: palette) + app.typeKey(.escape, modifierFlags: []) + XCTAssertTrue(palette.waitForNonExistence(timeout: 3)) + assertSelected(app.buttons["workbench-section-review"]) + } + + @MainActor + func testTestingWorkspaceExposesTheDirectPreviewContract() throws { + let app = XCUIApplication() + app.launch() + + app.buttons["Testing"].click() + + assertSelected(app.buttons["Testing"]) + XCTAssertTrue( + app.staticTexts["Test the change where users touch it."].waitForExistence(timeout: 2)) + XCTAssertTrue(app.buttons["Choose testing repository"].exists) + XCTAssertTrue(app.radioButtons["Git range"].exists) + XCTAssertTrue(app.radioButtons["GitHub pull request"].exists) + XCTAssertTrue( + app.descendants(matching: .any)["testing-scope-planner"].waitForExistence(timeout: 2)) + XCTAssertTrue(app.buttons["testing-scope-planner-resolve"].exists) + XCTAssertTrue(app.checkBoxes["Allow this bounded preview verification"].exists) + XCTAssertTrue(app.buttons["Run preview proof"].exists) + XCTAssertFalse(app.buttons["Run preview proof"].isEnabled) + XCTAssertTrue(app.staticTexts["RUST EXECUTION CONTRACT"].exists) + XCTAssertTrue(app.staticTexts["codevetter trex"].exists) + } + + @MainActor + func testTestingWorkspaceExposesEveryMigratedForegroundWorkbench() throws { + let app = XCUIApplication() + app.launchArguments = [ + "--ui-test-repository", testingRepository.path, + "--ui-test-section", "Testing", + ] + app.launch() + + assertSelected(app.buttons["Testing"]) + for workspace in [ + ("Warm changed proof", "warm-verification-workspace"), + ("Differential", "differential-verification-workspace"), + ("Scenarios", "scenario-compiler-workspace"), + ("PR watcher", "trex-watcher-workspace"), + ] { + let trigger = app.buttons[workspace.0] + XCTAssertTrue(trigger.waitForExistence(timeout: 3), "Missing \(workspace.0) trigger") + XCTAssertTrue(trigger.isEnabled, "\(workspace.0) should be reachable with a repository") + trigger.click() + let surface = app.descendants(matching: .any)[workspace.1] + XCTAssertTrue(surface.waitForExistence(timeout: 5), "Missing \(workspace.1)") + XCTAssertTrue(app.buttons["Done"].waitForExistence(timeout: 2)) + app.buttons["Done"].click() + XCTAssertTrue(surface.waitForNonExistence(timeout: 3), "\(workspace.0) did not dismiss") + } + } + + @MainActor + func testUsageWorkspacePreservesLocalAndLiveProviderBoundaries() throws { + let app = XCUIApplication() + app.launch() + XCTAssertTrue(app.staticTexts["Can this change ship?"].waitForExistence(timeout: 5)) + + let usageSection = app.buttons["workbench-section-usage"] + XCTAssertTrue(usageSection.waitForExistence(timeout: 2)) + usageSection.click() + + assertSelected(app.buttons["workbench-section-usage"]) + XCTAssertTrue(app.staticTexts["LOCAL COMPUTE LEDGER"].waitForExistence(timeout: 2)) + XCTAssertTrue(app.buttons["Usage refresh"].exists) + XCTAssertTrue(app.staticTexts["Claude · Codex · Grok"].waitForExistence(timeout: 2)) + XCTAssertTrue(app.staticTexts["Devin activity"].exists) + XCTAssertTrue(app.staticTexts["Provider quotas"].exists) + } + + @MainActor + func testRepoUnpackWorkspacePreservesRustScanAndInspectionAuthority() throws { + let app = XCUIApplication() + app.launch() + + app.buttons["Repo Unpack"].click() + + assertSelected(app.buttons["Repo Unpack"]) + XCTAssertTrue(app.staticTexts["REPOSITORY MEMORY"].waitForExistence(timeout: 2)) + XCTAssertTrue(app.staticTexts["SNAPSHOT LEDGER"].exists) + XCTAssertTrue( + app.descendants(matching: .any)["repo-unpack-read-only-status"].waitForExistence(timeout: 2) + ) + XCTAssertTrue(app.buttons["Refresh Repo Unpack snapshots"].exists) + XCTAssertTrue(app.staticTexts["Rust-owned SQLite history"].exists) + XCTAssertTrue( + app.descendants(matching: .any)["repo-unpack-choose-repository"].exists + ) + let scan = app.descendants(matching: .any)["repo-unpack-scan"] + XCTAssertTrue(scan.exists) + XCTAssertFalse(scan.isEnabled, "A scan requires an explicitly selected repository") + XCTAssertTrue(app.descendants(matching: .any)["repo-unpack-export"].exists) + } + + @MainActor + func testReviewWorkspaceExposesIndependentClaudeAndCodexStrategy() throws { + let app = XCUIApplication() + app.launchArguments = [ + "--ui-test-repository", testingRepository.path, + "--ui-test-section", "Review", + ] + app.launch() + + assertSelected(app.buttons["Review"]) + let strategy = app.descendants(matching: .any)["review-strategy"] + XCTAssertTrue(strategy.waitForExistence(timeout: 3)) + XCTAssertTrue(app.radioButtons["Claude"].exists) + XCTAssertTrue(app.radioButtons["Codex"].exists) + let cross = app.radioButtons["Claude + Codex"] + XCTAssertTrue(cross.exists) + cross.click() + XCTAssertTrue( + app.staticTexts[ + "Runs independent Claude then Codex passes against the same immutable change. Agreement is coverage, not proof." + ].waitForExistence(timeout: 4), + "The selected strategy did not update the review contract" + ) + } + + @MainActor + func testSettingsWorkspaceExcludesSecretsAndPreservesEverySection() throws { + let app = XCUIApplication() + app.launch() + + app.buttons["Settings"].click() + + assertSelected(app.buttons["Settings"]) + XCTAssertTrue(app.staticTexts["LOCAL CONTROL PLANE"].waitForExistence(timeout: 2)) + XCTAssertTrue(app.staticTexts["SETTINGS SECTIONS"].exists) + XCTAssertTrue(app.buttons["Refresh native settings"].exists) + XCTAssertTrue(app.staticTexts["Rust owns persistence"].exists) + for section in [ + "General", "Appearance", "Integrations", "Agents", "Agent MCP", "Notifications", "Usage", + "Rubrics", "Ops", "Memories", "About", + ] { + XCTAssertTrue(app.buttons[section].exists, "Missing settings section: \(section)") + } + + let mcpSection = app.buttons["Agent MCP"] + mcpSection.click() + assertSelected(mcpSection) + + let usageSection = app.buttons["settings-section-usage"] + usageSection.click() + assertSelected(usageSection) + XCTAssertTrue(app.buttons["preview-session-retention"].waitForExistence(timeout: 2)) + XCTAssertTrue(app.buttons["checkpoint-session-archive"].exists) + XCTAssertTrue(app.buttons["vacuum-session-archive"].exists) + + let rubricsSection = app.buttons["settings-section-rubrics"] + rubricsSection.click() + XCTAssertTrue( + app.descendants(matching: .any)["rubric-settings-workspace"].waitForExistence(timeout: 5), + "Rubrics workspace did not open" + ) + } + + @MainActor + func testPerformanceWorkspaceExposesTheRustAdmissionContract() throws { + let app = XCUIApplication() + app.launch() + + app.buttons["Performance"].click() + + assertSelected(app.buttons["Performance"]) + XCTAssertTrue(app.staticTexts["Measure what changed."].waitForExistence(timeout: 2)) + XCTAssertTrue(app.buttons["Choose performance repository"].exists) + XCTAssertTrue(app.popUpButtons["Performance adapter"].exists) + XCTAssertTrue( + app.descendants(matching: .any)["performance-scope-planner"].waitForExistence(timeout: 2)) + XCTAssertTrue(app.buttons["performance-scope-planner-resolve"].exists) + XCTAssertTrue(app.buttons["Plan"].exists) + XCTAssertFalse(app.buttons["Plan"].isEnabled) + XCTAssertTrue(app.buttons["Capture evidence"].exists) + XCTAssertFalse(app.buttons["Capture evidence"].isEnabled) + XCTAssertTrue(app.staticTexts["No performance claim without admission."].exists) + XCTAssertTrue(app.staticTexts["Authority: codevetter performance"].exists) + } + + @MainActor + func testLaunchPerformance() { + measure(metrics: [XCTApplicationLaunchMetric(waitUntilResponsive: true)]) { + XCUIApplication().launch() + } + } + + @MainActor + private func assertSelected( + _ element: XCUIElement, + timeout: TimeInterval = 3, + file: StaticString = #filePath, + line: UInt = #line + ) { + XCTAssertTrue( + waitUntilSelected(element, timeout: timeout), + "Selection did not settle", + file: file, + line: line + ) + } + + @MainActor + private func waitUntilSelected(_ element: XCUIElement, timeout: TimeInterval) -> Bool { + let expectation = XCTNSPredicateExpectation( + predicate: NSPredicate { object, _ in + (object as? XCUIElement)?.isSelected == true + }, + object: element + ) + return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed + } + + @MainActor + private func openCommandPaletteWithKeyboard(_ app: XCUIApplication, palette: XCUIElement) { + app.menuBars.menuBarItems["View"].click() + XCTAssertTrue(app.menuItems["Command Palette…"].waitForExistence(timeout: 2)) + app.typeKey("k", modifierFlags: .command) + XCTAssertTrue(palette.waitForExistence(timeout: 3)) + } +} diff --git a/apps/macos/Config/CodeVetter.Debug.entitlements b/apps/macos/Config/CodeVetter.Debug.entitlements new file mode 100644 index 00000000..8cc185af --- /dev/null +++ b/apps/macos/Config/CodeVetter.Debug.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.cs.disable-library-validation + + + diff --git a/apps/macos/Config/CodeVetter.Release.entitlements b/apps/macos/Config/CodeVetter.Release.entitlements new file mode 100644 index 00000000..0c67376e --- /dev/null +++ b/apps/macos/Config/CodeVetter.Release.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/macos/Config/CodeVetter.entitlements b/apps/macos/Config/CodeVetter.entitlements new file mode 100644 index 00000000..18aff0ce --- /dev/null +++ b/apps/macos/Config/CodeVetter.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/apps/macos/Config/Debug.xcconfig b/apps/macos/Config/Debug.xcconfig new file mode 100644 index 00000000..52940005 --- /dev/null +++ b/apps/macos/Config/Debug.xcconfig @@ -0,0 +1,11 @@ +// Debug.xcconfig +// Debug configuration for macOS projects - minimal overrides only +// Generated by XcodeBuildMCP + +#include "Shared.xcconfig" + +// Development must exercise the real supervised Rust/Node/browser process +// boundary. Ad-hoc development code has no shared Team ID with Sparkle, so the +// Debug-only entitlement disables Library Validation. Release remains empty +// and production must sign every nested component with one Developer ID team. +CODE_SIGN_ENTITLEMENTS = Config/CodeVetter.Debug.entitlements diff --git a/apps/macos/Config/Release.xcconfig b/apps/macos/Config/Release.xcconfig new file mode 100644 index 00000000..725fb525 --- /dev/null +++ b/apps/macos/Config/Release.xcconfig @@ -0,0 +1,24 @@ +// Release.xcconfig +// Release configuration for macOS projects - minimal overrides only +// Generated by XcodeBuildMCP + +#include "Shared.xcconfig" + +// CodeVetter executes user-selected repository tools and supervised helpers. +// Outside the Mac App Store, Developer ID distribution requires Hardened +// Runtime but does not require App Sandbox. A sandbox would silently remove +// the product's core execution authority. +CODE_SIGN_ENTITLEMENTS = Config/CodeVetter.Release.entitlements +CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO + +// Release packages must not carry test coverage instrumentation. Preserve +// symbols in the adjacent dSYM while stripping the shipped executable and +// dead code during the Release build. +ENABLE_CODE_COVERAGE = NO +CLANG_ENABLE_CODE_COVERAGE = NO +CLANG_COVERAGE_MAPPING = NO +DEAD_CODE_STRIPPING = YES +DEBUG_INFORMATION_FORMAT = dwarf-with-dsym +DEPLOYMENT_POSTPROCESSING = YES +STRIP_INSTALLED_PRODUCT = YES +STRIP_SWIFT_SYMBOLS = YES diff --git a/apps/macos/Config/Shared.xcconfig b/apps/macos/Config/Shared.xcconfig new file mode 100644 index 00000000..d05d0401 --- /dev/null +++ b/apps/macos/Config/Shared.xcconfig @@ -0,0 +1,40 @@ +// Shared.xcconfig +// Minimal shared configuration for scaffold tool customization +// All other settings use Xcode project defaults +// Generated by XcodeBuildMCP + +// ========================================== +// Project Identity +// ========================================== +PRODUCT_NAME = CodeVetter +PRODUCT_DISPLAY_NAME = CodeVetter +// Keep the visible application name while leaving the canonical lowercase +// `codevetter` CLI available as a distinct auxiliary executable on the default +// case-insensitive macOS filesystem. +EXECUTABLE_NAME = CodeVetterNative +// Keep the migration candidate isolated from the shipped Tauri application. +// The production identifier transfers only after the retirement gate passes. +PRODUCT_BUNDLE_IDENTIFIER = com.codevetter.desktop.native-preview +MARKETING_VERSION = 1.11.0 +CURRENT_PROJECT_VERSION = 11100 + +// ========================================== +// Platform Configuration +// ========================================== +MACOSX_DEPLOYMENT_TARGET = 14.0 +ALWAYS_SEARCH_USER_PATHS = NO +ENABLE_HARDENED_RUNTIME = YES +LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks + +// ========================================== +// Info PLIST +// ========================================== +GENERATE_INFOPLIST_FILE = YES +INFOPLIST_KEY_LSApplicationCategoryType = public.app-category.developer-tools +INFOPLIST_KEY_NSHumanReadableCopyright = Copyright 2026 CodeVetter + +// ========================================== +// Entitlements +// ========================================== +// AI agents can modify Config/CodeVetter.entitlements to add capabilities +CODE_SIGN_ENTITLEMENTS = Config/CodeVetter.entitlements diff --git a/apps/macos/Config/Tests.xcconfig b/apps/macos/Config/Tests.xcconfig new file mode 100644 index 00000000..3b2728a1 --- /dev/null +++ b/apps/macos/Config/Tests.xcconfig @@ -0,0 +1,16 @@ +// Tests.xcconfig +// Test configuration for macOS projects - minimal overrides only +// Generated by XcodeBuildMCP + +#include "Shared.xcconfig" + +// ========================================== +// Test Target Settings (Customizable by scaffold tool) +// ========================================== +PRODUCT_BUNDLE_IDENTIFIER = com.codevetter.desktop.native-preview.uitests +TEST_TARGET_NAME = CodeVetter +EXECUTABLE_NAME = CodeVetterUITests +ENABLE_HARDENED_RUNTIME = NO + +// Fix duplicate module name issue +PRODUCT_MODULE_NAME = $(PRODUCT_NAME)UITests diff --git a/apps/macos/README.md b/apps/macos/README.md new file mode 100644 index 00000000..95a61b93 --- /dev/null +++ b/apps/macos/README.md @@ -0,0 +1,129 @@ +# CodeVetter - macOS App + +A modern macOS application using a **workspace + SPM package** architecture for clean separation between app shell and feature code. + +## Project Architecture + +``` +CodeVetter/ +├── CodeVetter.xcworkspace/ # Open this file in Xcode +├── CodeVetter.xcodeproj/ # App shell project +├── CodeVetter/ # App target (minimal) +│ ├── Assets.xcassets/ # App-level assets (icons, colors) +│ ├── CodeVetterApp.swift # App entry point +│ ├── CodeVetter.entitlements # App sandbox settings +│ └── CodeVetter.xctestplan # Test configuration +├── CodeVetterPackage/ # 🚀 Primary development area +│ ├── Package.swift # Package configuration +│ ├── Sources/CodeVetterFeature/ # Your feature code +│ └── Tests/CodeVetterFeatureTests/ # Unit tests +└── CodeVetterUITests/ # UI automation tests +``` + +## Key Architecture Points + +### Workspace + SPM Structure +- **App Shell**: `CodeVetter/` contains minimal app lifecycle code +- **Feature Code**: `CodeVetterPackage/Sources/CodeVetterFeature/` is where most development happens +- **Separation**: Business logic lives in the SPM package, app target just imports and displays it + +### Buildable Folders (Xcode 16) +- Files added to the filesystem automatically appear in Xcode +- No need to manually add files to project targets +- Reduces project file conflicts in teams + +### App Sandbox +The app is sandboxed by default with basic file access permissions. Modify `CodeVetter.entitlements` to add capabilities as needed. + +## Development Notes + +### Code Organization +Most development happens in `CodeVetterPackage/Sources/CodeVetterFeature/` - organize your code as you prefer. + +### Public API Requirements +Types exposed to the app target need `public` access: +```swift +public struct SettingsView: View { + public init() {} + + public var body: some View { + // Your view code + } +} +``` + +### Adding Dependencies +Edit `CodeVetterPackage/Package.swift` to add SPM dependencies: +```swift +dependencies: [ + .package(url: "https://github.com/example/SomePackage", from: "1.0.0") +], +targets: [ + .target( + name: "CodeVetterFeature", + dependencies: ["SomePackage"] + ), +] +``` + +### Test Structure +- **Unit Tests**: `CodeVetterPackage/Tests/CodeVetterFeatureTests/` (Swift Testing framework) +- **UI Tests**: `CodeVetterUITests/` (XCUITest framework) +- **Test Plan**: `CodeVetter.xctestplan` coordinates all tests + +## Configuration + +### XCConfig Build Settings +Build settings are managed through **XCConfig files** in `Config/`: +- `Config/Shared.xcconfig` - Common settings (bundle ID, versions, deployment target) +- `Config/Debug.xcconfig` - Debug-specific settings +- `Config/Release.xcconfig` - Release-specific settings +- `Config/Tests.xcconfig` - Test-specific settings + +### App Sandbox & Entitlements +The app is sandboxed by default with basic file access. Edit `CodeVetter/CodeVetter.entitlements` to add capabilities: +```xml +com.apple.security.files.user-selected.read-write + +com.apple.security.network.client + + +``` + +## macOS-Specific Features + +### Window Management +Add multiple windows and settings panels: +```swift +@main +struct CodeVetterApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + + Settings { + SettingsView() + } + } +} +``` + +### Asset Management +- **App-Level Assets**: `CodeVetter/Assets.xcassets/` (app icon with multiple sizes, accent color) +- **Feature Assets**: Add `Resources/` folder to SPM package if needed + +### SPM Package Resources +To include assets in your feature package: +```swift +.target( + name: "CodeVetterFeature", + dependencies: [], + resources: [.process("Resources")] +) +``` + +## Notes + +### Generated with XcodeBuildMCP +This project was scaffolded using [XcodeBuildMCP](https://github.com/cameroncooke/XcodeBuildMCP), which provides tools for AI-assisted macOS development workflows. \ No newline at end of file