← back to Exo
app
a46f8c3cd134536ce00115eeb278c0bd7fe6e8bf · 2025-08-02 07:14:27 +0500 · Sami Khan
Co-authored-by: Alex Cheema <alexcheema123@gmail.com>
Files touched
A .DS_StoreA .github/workflows/build-macos-app.ymlM .gitignoreA app/exov2/.DS_StoreA app/exov2/exov2.xcodeproj/project.pbxprojA app/exov2/exov2.xcodeproj/project.xcworkspace/contents.xcworkspacedataA app/exov2/exov2.xcodeproj/project.xcworkspace/xcuserdata/samikhan.xcuserdatad/UserInterfaceState.xcuserstateA app/exov2/exov2.xcodeproj/xcshareddata/xcschemes/exov2.xcschemeA app/exov2/exov2.xcodeproj/xcuserdata/samikhan.xcuserdatad/xcschemes/xcschememanagement.plistA app/exov2/exov2/Preview Content/Preview Assets.xcassets/Contents.jsonA app/exov2/exov2/ProcessManager.swiftA app/exov2/exov2/exov2.entitlementsA app/exov2/exov2/exov2App.swiftA app/exov2/exov2Tests/exov2Tests.swiftA app/exov2/exov2UITests/exov2UITests.swiftA app/exov2/exov2UITests/exov2UITestsLaunchTests.swift
Diff
commit a46f8c3cd134536ce00115eeb278c0bd7fe6e8bf
Author: Sami Khan <98742866+samiamjidkhan@users.noreply.github.com>
Date: Sat Aug 2 07:14:27 2025 +0500
app
Co-authored-by: Alex Cheema <alexcheema123@gmail.com>
---
.DS_Store | Bin 0 -> 8196 bytes
.github/workflows/build-macos-app.yml | 198 ++++++++
.gitignore | 1 +
app/exov2/.DS_Store | Bin 0 -> 6148 bytes
app/exov2/exov2.xcodeproj/project.pbxproj | 548 +++++++++++++++++++++
.../project.xcworkspace/contents.xcworkspacedata | 7 +
.../UserInterfaceState.xcuserstate | Bin 0 -> 33984 bytes
.../xcshareddata/xcschemes/exov2.xcscheme | 109 ++++
.../xcschemes/xcschememanagement.plist | 32 ++
.../Preview Assets.xcassets/Contents.json | 6 +
app/exov2/exov2/ProcessManager.swift | 377 ++++++++++++++
app/exov2/exov2/exov2.entitlements | 14 +
app/exov2/exov2/exov2App.swift | 115 +++++
app/exov2/exov2Tests/exov2Tests.swift | 17 +
app/exov2/exov2UITests/exov2UITests.swift | 43 ++
.../exov2UITests/exov2UITestsLaunchTests.swift | 33 ++
16 files changed, 1500 insertions(+)
diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 00000000..1ad06f76
Binary files /dev/null and b/.DS_Store differ
diff --git a/.github/workflows/build-macos-app.yml b/.github/workflows/build-macos-app.yml
new file mode 100644
index 00000000..b9f01998
--- /dev/null
+++ b/.github/workflows/build-macos-app.yml
@@ -0,0 +1,198 @@
+name: Build and Release Exo macOS App
+
+on:
+ push:
+ tags:
+ - 'v*' # Trigger only on version tags
+ branches:
+ - main # Also build on main branch for testing
+ - app-staging # Add app-staging for testing
+ pull_request:
+ branches:
+ - main # Test builds on PRs
+
+jobs:
+ build-exov2-macos:
+ runs-on: macos-15
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup Rust (nightly)
+ uses: actions-rust-lang/setup-rust-toolchain@v1
+ with:
+ toolchain: nightly
+ components: rustfmt, clippy
+ default: true
+
+ - name: Set Rust toolchain override
+ run: |
+ rustup default nightly
+ cd rust && rustup override set nightly
+
+ - name: Install Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: '1.21'
+
+ - name: Install Just
+ run: |
+ brew install just
+
+ - name: Install UV
+ uses: astral-sh/setup-uv@v6
+ with:
+ enable-cache: true
+ cache-dependency-glob: uv.lock
+
+ - name: Setup Python Environment
+ run: |
+ uv python install
+ uv sync --locked --all-extras
+
+ - name: Build Rust Components
+ env:
+ RUSTFLAGS: "-A unused-imports -A dead-code -A unreachable-code"
+ run: |
+ just build-all
+
+ - name: Install Python Bindings
+ run: |
+ uv pip install dist/exo_pyo3_bindings-*.whl
+
+ - name: Verify Python Environment
+ run: |
+ uv run python -c "import exo_pyo3_bindings; print('Python bindings installed successfully')"
+ uv run python -c "import master.main; print('Master module available')"
+ uv run python -c "import worker.main; print('Worker module available')"
+
+ - name: Prepare Code Signing Keychain
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
+ env:
+ MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
+ MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
+ PROVISIONING_PROFILE: ${{ secrets.PROVISIONING_PROFILE }}
+ run: |
+ security create-keychain -p "$MACOS_CERTIFICATE_PASSWORD" exov2.keychain
+ security default-keychain -s exov2.keychain
+ security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" exov2.keychain
+
+ echo "$MACOS_CERTIFICATE" | base64 --decode > /tmp/exov2-certificate.p12
+ security import /tmp/exov2-certificate.p12 -k exov2.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
+ rm /tmp/exov2-certificate.p12
+ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CERTIFICATE_PASSWORD" exov2.keychain
+
+ PROFILES_HOME="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"
+ mkdir -p "$PROFILES_HOME"
+ PROFILE_PATH="$(mktemp "$PROFILES_HOME"/EXOV2_PP.provisionprofile)"
+ echo "$PROVISIONING_PROFILE" | base64 --decode > "$PROFILE_PATH"
+
+ - name: Build Exo Swift App
+ env:
+ MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
+ run: |
+ cd app/exov2
+ sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
+
+ if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == refs/tags/v* ]]; then
+ # Release build with code signing
+ security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" exov2.keychain
+ SIGNING_IDENTITY=$(security find-identity -v -p codesigning | awk -F '"' '{print $2}')
+
+ xcodebuild clean build \
+ -project exov2.xcodeproj \
+ -scheme exov2 \
+ -configuration Release \
+ -derivedDataPath build \
+ CODE_SIGNING_IDENTITY="$SIGNING_IDENTITY" \
+ PROVISIONING_PROFILE_SPECIFIER="Exo Provisioning Profile" \
+ CODE_SIGN_INJECT_BASE_ENTITLEMENTS=YES \
+ OTHER_CODE_SIGN_FLAGS="--timestamp"
+ else
+ # Debug build without code signing for testing
+ xcodebuild clean build \
+ -project exov2.xcodeproj \
+ -scheme exov2 \
+ -configuration Debug \
+ -derivedDataPath build \
+ CODE_SIGN_IDENTITY="" \
+ CODE_SIGNING_REQUIRED=NO
+ fi
+
+ mv build/Build/Products/*/exov2.app ../../
+
+ - name: Sign and Create DMG (Release only)
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
+ env:
+ APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
+ APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
+ APPLE_NOTARIZATION_TEAM: ${{ secrets.APPLE_NOTARIZATION_TEAM }}
+ MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
+ run: |
+ security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" exov2.keychain
+ SIGNING_IDENTITY=$(security find-identity -v -p codesigning | awk -F '"' '{print $2}')
+
+ # Sign the app
+ /usr/bin/codesign --deep --force --timestamp --options runtime \
+ --sign "$SIGNING_IDENTITY" exov2.app
+
+ # Verify the signing
+ codesign -dvv exov2.app
+
+ # Create DMG
+ mkdir -p tmp/dmg-contents
+ cp -r ./exov2.app tmp/dmg-contents/
+ ln -s /Applications tmp/dmg-contents/Applications
+ VERSION=$(git describe --tags --abbrev=0 | sed 's/^v//')
+
+ # Create and sign DMG
+ hdiutil create -volname "Exo" -srcfolder tmp/dmg-contents -ov -format UDZO exov2-${VERSION}.dmg
+ /usr/bin/codesign --deep --force --timestamp --options runtime \
+ --sign "$SIGNING_IDENTITY" exov2-${VERSION}.dmg
+
+ # Setup notarization credentials (optional - comment out if no notarization secrets)
+ if [[ -n "$APPLE_NOTARIZATION_USERNAME" ]]; then
+ xcrun notarytool store-credentials notary_pass \
+ --apple-id "$APPLE_NOTARIZATION_USERNAME" \
+ --password "$APPLE_NOTARIZATION_PASSWORD" \
+ --team-id "$APPLE_NOTARIZATION_TEAM"
+
+ # Submit for notarization
+ xcrun notarytool submit --wait \
+ --team-id "$APPLE_NOTARIZATION_TEAM" \
+ --keychain-profile notary_pass \
+ exov2-${VERSION}.dmg
+
+ # Staple the notarization
+ xcrun stapler staple exov2-${VERSION}.dmg
+ fi
+
+ - name: Create DMG (Debug builds)
+ if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/v')
+ run: |
+ mkdir -p tmp/dmg-contents
+ cp -r ./exov2.app tmp/dmg-contents/
+ ln -s /Applications tmp/dmg-contents/Applications
+ VERSION=$(git rev-parse --short HEAD)
+
+ hdiutil create -volname "Exo Debug" -srcfolder tmp/dmg-contents -ov -format UDZO exov2-debug-${VERSION}.dmg
+
+ - name: Cleanup Keychain
+ if: always() && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
+ run: |
+ security default-keychain -s login.keychain
+ security delete-keychain exov2.keychain
+
+ - name: Upload DMG file
+ uses: actions/upload-artifact@v4
+ with:
+ name: exov2-dmg
+ path: exov2*.dmg
+
+ - name: Upload App Bundle
+ uses: actions/upload-artifact@v4
+ with:
+ name: exov2-app
+ path: exov2.app/
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index b3f86bdf..930ec3e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,4 @@ networking/target/
networking/topology/target/
build/
+*.xcuserstate
\ No newline at end of file
diff --git a/app/exov2/.DS_Store b/app/exov2/.DS_Store
new file mode 100644
index 00000000..31bdd033
Binary files /dev/null and b/app/exov2/.DS_Store differ
diff --git a/app/exov2/exov2.xcodeproj/project.pbxproj b/app/exov2/exov2.xcodeproj/project.pbxproj
new file mode 100644
index 00000000..a4e54fad
--- /dev/null
+++ b/app/exov2/exov2.xcodeproj/project.pbxproj
@@ -0,0 +1,548 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 77;
+ objects = {
+
+/* Begin PBXContainerItemProxy section */
+ E07D64CC2E36127F009BFB4D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = E07D64B22E36127E009BFB4D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = E07D64B92E36127E009BFB4D;
+ remoteInfo = exov2;
+ };
+ E07D64D62E36127F009BFB4D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = E07D64B22E36127E009BFB4D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = E07D64B92E36127E009BFB4D;
+ remoteInfo = exov2;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+ E07D64BA2E36127E009BFB4D /* exov2.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = exov2.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ E07D64CB2E36127F009BFB4D /* exov2Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = exov2Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ E07D64D52E36127F009BFB4D /* exov2UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = exov2UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+ E07D64BC2E36127E009BFB4D /* exov2 */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ path = exov2;
+ sourceTree = "<group>";
+ };
+ E07D64CE2E36127F009BFB4D /* exov2Tests */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ path = exov2Tests;
+ sourceTree = "<group>";
+ };
+ E07D64D82E36127F009BFB4D /* exov2UITests */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ path = exov2UITests;
+ sourceTree = "<group>";
+ };
+/* End PBXFileSystemSynchronizedRootGroup section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ E07D64B72E36127E009BFB4D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ E07D64C82E36127F009BFB4D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ E07D64D22E36127F009BFB4D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ E07D64B12E36127E009BFB4D = {
+ isa = PBXGroup;
+ children = (
+ E07D64BC2E36127E009BFB4D /* exov2 */,
+ E07D64CE2E36127F009BFB4D /* exov2Tests */,
+ E07D64D82E36127F009BFB4D /* exov2UITests */,
+ E07D64BB2E36127E009BFB4D /* Products */,
+ );
+ sourceTree = "<group>";
+ };
+ E07D64BB2E36127E009BFB4D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ E07D64BA2E36127E009BFB4D /* exov2.app */,
+ E07D64CB2E36127F009BFB4D /* exov2Tests.xctest */,
+ E07D64D52E36127F009BFB4D /* exov2UITests.xctest */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ E07D64B92E36127E009BFB4D /* exov2 */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = E07D64DF2E36127F009BFB4D /* Build configuration list for PBXNativeTarget "exov2" */;
+ buildPhases = (
+ E07D64B62E36127E009BFB4D /* Sources */,
+ E07D64B72E36127E009BFB4D /* Frameworks */,
+ E07D64B82E36127E009BFB4D /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ fileSystemSynchronizedGroups = (
+ E07D64BC2E36127E009BFB4D /* exov2 */,
+ );
+ name = exov2;
+ packageProductDependencies = (
+ );
+ productName = exov2;
+ productReference = E07D64BA2E36127E009BFB4D /* exov2.app */;
+ productType = "com.apple.product-type.application";
+ };
+ E07D64CA2E36127F009BFB4D /* exov2Tests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = E07D64E22E36127F009BFB4D /* Build configuration list for PBXNativeTarget "exov2Tests" */;
+ buildPhases = (
+ E07D64C72E36127F009BFB4D /* Sources */,
+ E07D64C82E36127F009BFB4D /* Frameworks */,
+ E07D64C92E36127F009BFB4D /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ E07D64CD2E36127F009BFB4D /* PBXTargetDependency */,
+ );
+ fileSystemSynchronizedGroups = (
+ E07D64CE2E36127F009BFB4D /* exov2Tests */,
+ );
+ name = exov2Tests;
+ packageProductDependencies = (
+ );
+ productName = exov2Tests;
+ productReference = E07D64CB2E36127F009BFB4D /* exov2Tests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ E07D64D42E36127F009BFB4D /* exov2UITests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = E07D64E52E36127F009BFB4D /* Build configuration list for PBXNativeTarget "exov2UITests" */;
+ buildPhases = (
+ E07D64D12E36127F009BFB4D /* Sources */,
+ E07D64D22E36127F009BFB4D /* Frameworks */,
+ E07D64D32E36127F009BFB4D /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ E07D64D72E36127F009BFB4D /* PBXTargetDependency */,
+ );
+ fileSystemSynchronizedGroups = (
+ E07D64D82E36127F009BFB4D /* exov2UITests */,
+ );
+ name = exov2UITests;
+ packageProductDependencies = (
+ );
+ productName = exov2UITests;
+ productReference = E07D64D52E36127F009BFB4D /* exov2UITests.xctest */;
+ productType = "com.apple.product-type.bundle.ui-testing";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ E07D64B22E36127E009BFB4D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1610;
+ LastUpgradeCheck = 1610;
+ TargetAttributes = {
+ E07D64B92E36127E009BFB4D = {
+ CreatedOnToolsVersion = 16.1;
+ };
+ E07D64CA2E36127F009BFB4D = {
+ CreatedOnToolsVersion = 16.1;
+ TestTargetID = E07D64B92E36127E009BFB4D;
+ };
+ E07D64D42E36127F009BFB4D = {
+ CreatedOnToolsVersion = 16.1;
+ TestTargetID = E07D64B92E36127E009BFB4D;
+ };
+ };
+ };
+ buildConfigurationList = E07D64B52E36127E009BFB4D /* Build configuration list for PBXProject "exov2" */;
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = E07D64B12E36127E009BFB4D;
+ minimizedProjectReferenceProxies = 1;
+ preferredProjectObjectVersion = 77;
+ productRefGroup = E07D64BB2E36127E009BFB4D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ E07D64B92E36127E009BFB4D /* exov2 */,
+ E07D64CA2E36127F009BFB4D /* exov2Tests */,
+ E07D64D42E36127F009BFB4D /* exov2UITests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ E07D64B82E36127E009BFB4D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ E07D64C92E36127F009BFB4D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ E07D64D32E36127F009BFB4D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ E07D64B62E36127E009BFB4D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ E07D64C72E36127F009BFB4D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ E07D64D12E36127F009BFB4D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ E07D64CD2E36127F009BFB4D /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = E07D64B92E36127E009BFB4D /* exov2 */;
+ targetProxy = E07D64CC2E36127F009BFB4D /* PBXContainerItemProxy */;
+ };
+ E07D64D72E36127F009BFB4D /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = E07D64B92E36127E009BFB4D /* exov2 */;
+ targetProxy = E07D64D62E36127F009BFB4D /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+ E07D64DD2E36127F009BFB4D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 15.1;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ E07D64DE2E36127F009BFB4D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 15.1;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ };
+ name = Release;
+ };
+ E07D64E02E36127F009BFB4D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = exov2/exov2.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"exov2/Preview Content\"";
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_LSUIElement = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = exolabs.exov2;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ E07D64E12E36127F009BFB4D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = exov2/exov2.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"exov2/Preview Content\"";
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_LSUIElement = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = exolabs.exov2;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+ E07D64E32E36127F009BFB4D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 15.1;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = exolabs.exov2Tests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/exov2.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/exov2";
+ };
+ name = Debug;
+ };
+ E07D64E42E36127F009BFB4D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 15.1;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = exolabs.exov2Tests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/exov2.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/exov2";
+ };
+ name = Release;
+ };
+ E07D64E62E36127F009BFB4D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = exolabs.exov2UITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TEST_TARGET_NAME = exov2;
+ };
+ name = Debug;
+ };
+ E07D64E72E36127F009BFB4D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = exolabs.exov2UITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TEST_TARGET_NAME = exov2;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ E07D64B52E36127E009BFB4D /* Build configuration list for PBXProject "exov2" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ E07D64DD2E36127F009BFB4D /* Debug */,
+ E07D64DE2E36127F009BFB4D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ E07D64DF2E36127F009BFB4D /* Build configuration list for PBXNativeTarget "exov2" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ E07D64E02E36127F009BFB4D /* Debug */,
+ E07D64E12E36127F009BFB4D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ E07D64E22E36127F009BFB4D /* Build configuration list for PBXNativeTarget "exov2Tests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ E07D64E32E36127F009BFB4D /* Debug */,
+ E07D64E42E36127F009BFB4D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ E07D64E52E36127F009BFB4D /* Build configuration list for PBXNativeTarget "exov2UITests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ E07D64E62E36127F009BFB4D /* Debug */,
+ E07D64E72E36127F009BFB4D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = E07D64B22E36127E009BFB4D /* Project object */;
+}
diff --git a/app/exov2/exov2.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/app/exov2/exov2.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..919434a6
--- /dev/null
+++ b/app/exov2/exov2.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "self:">
+ </FileRef>
+</Workspace>
diff --git a/app/exov2/exov2.xcodeproj/project.xcworkspace/xcuserdata/samikhan.xcuserdatad/UserInterfaceState.xcuserstate b/app/exov2/exov2.xcodeproj/project.xcworkspace/xcuserdata/samikhan.xcuserdatad/UserInterfaceState.xcuserstate
new file mode 100644
index 00000000..92fe6a19
Binary files /dev/null and b/app/exov2/exov2.xcodeproj/project.xcworkspace/xcuserdata/samikhan.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/app/exov2/exov2.xcodeproj/xcshareddata/xcschemes/exov2.xcscheme b/app/exov2/exov2.xcodeproj/xcshareddata/xcschemes/exov2.xcscheme
new file mode 100644
index 00000000..b6afb5a7
--- /dev/null
+++ b/app/exov2/exov2.xcodeproj/xcshareddata/xcschemes/exov2.xcscheme
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "1610"
+ version = "1.7">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES"
+ buildArchitectures = "Automatic">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "E07D64B92E36127E009BFB4D"
+ BuildableName = "exov2.app"
+ BlueprintName = "exov2"
+ ReferencedContainer = "container:exov2.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ shouldAutocreateTestPlan = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO"
+ parallelizable = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "E07D64CA2E36127F009BFB4D"
+ BuildableName = "exov2Tests.xctest"
+ BlueprintName = "exov2Tests"
+ ReferencedContainer = "container:exov2.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ <TestableReference
+ skipped = "NO"
+ parallelizable = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "E07D64D42E36127F009BFB4D"
+ BuildableName = "exov2UITests.xctest"
+ BlueprintName = "exov2UITests"
+ ReferencedContainer = "container:exov2.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "E07D64B92E36127E009BFB4D"
+ BuildableName = "exov2.app"
+ BlueprintName = "exov2"
+ ReferencedContainer = "container:exov2.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ <EnvironmentVariables>
+ <EnvironmentVariable
+ key = "EXO_PROJECT_ROOT"
+ value = "/Users/SamiKhan/exo"
+ isEnabled = "YES">
+ </EnvironmentVariable>
+ </EnvironmentVariables>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "E07D64B92E36127E009BFB4D"
+ BuildableName = "exov2.app"
+ BlueprintName = "exov2"
+ ReferencedContainer = "container:exov2.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/app/exov2/exov2.xcodeproj/xcuserdata/samikhan.xcuserdatad/xcschemes/xcschememanagement.plist b/app/exov2/exov2.xcodeproj/xcuserdata/samikhan.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 00000000..f9edf8e6
--- /dev/null
+++ b/app/exov2/exov2.xcodeproj/xcuserdata/samikhan.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>SchemeUserState</key>
+ <dict>
+ <key>exov2.xcscheme_^#shared#^_</key>
+ <dict>
+ <key>orderHint</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <key>SuppressBuildableAutocreation</key>
+ <dict>
+ <key>E07D64B92E36127E009BFB4D</key>
+ <dict>
+ <key>primary</key>
+ <true/>
+ </dict>
+ <key>E07D64CA2E36127F009BFB4D</key>
+ <dict>
+ <key>primary</key>
+ <true/>
+ </dict>
+ <key>E07D64D42E36127F009BFB4D</key>
+ <dict>
+ <key>primary</key>
+ <true/>
+ </dict>
+ </dict>
+</dict>
+</plist>
diff --git a/app/exov2/exov2/Preview Content/Preview Assets.xcassets/Contents.json b/app/exov2/exov2/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 00000000..73c00596
--- /dev/null
+++ b/app/exov2/exov2/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/app/exov2/exov2/ProcessManager.swift b/app/exov2/exov2/ProcessManager.swift
new file mode 100644
index 00000000..81c5275a
--- /dev/null
+++ b/app/exov2/exov2/ProcessManager.swift
@@ -0,0 +1,377 @@
+import Foundation
+import OSLog
+import SwiftUI
+import AppKit
+import ServiceManagement
+
+extension NSApplication {
+ func addTerminationHandler(_ handler: @escaping () -> Void) {
+ NSApp.setActivationPolicy(.accessory)
+ NotificationCenter.default.addObserver(forName: NSApplication.willTerminateNotification,
+ object: nil,
+ queue: .main) { _ in
+ handler()
+ }
+ }
+}
+
+class ProcessManager: ObservableObject {
+ @Published var masterProcess: Process?
+ @Published var workerProcess: Process?
+ @Published var masterStatus: String = "Stopped"
+ @Published var workerStatus: String = "Stopped"
+ @Published var isLoginItemEnabled: Bool = false
+ @Published var isMasterMode: Bool = false // Default to replica mode (false)
+
+ private var masterStdout: Pipe?
+ private var workerStdout: Pipe?
+ private let logger = Logger(subsystem: "exolabs.exov2", category: "ProcessManager")
+
+ // Add file handle properties to track them
+ private var masterFileHandle: FileHandle?
+ private var workerFileHandle: FileHandle?
+
+ private let loginService = SMAppService.mainApp
+
+ // Find uv executable in common installation paths
+ private var uvPath: String? {
+ let commonPaths = [
+ "/usr/local/bin/uv",
+ "/opt/homebrew/bin/uv",
+ "/usr/bin/uv",
+ "/bin/uv",
+ "/Users/\(NSUserName())/.cargo/bin/uv",
+ "/Users/\(NSUserName())/.local/bin/uv"
+ ]
+
+ for path in commonPaths {
+ if FileManager.default.fileExists(atPath: path) {
+ return path
+ }
+ }
+
+ // Try using 'which uv' command as fallback
+ let process = Process()
+ process.executableURL = URL(fileURLWithPath: "/usr/bin/which")
+ process.arguments = ["uv"]
+
+ let pipe = Pipe()
+ process.standardOutput = pipe
+ process.standardError = Pipe()
+
+ do {
+ try process.run()
+ process.waitUntilExit()
+
+ if process.terminationStatus == 0 {
+ let data = pipe.fileHandleForReading.readDataToEndOfFile()
+ if let path = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
+ !path.isEmpty {
+ return path
+ }
+ }
+ } catch {
+ logger.error("Failed to run 'which uv': \(error.localizedDescription)")
+ }
+
+ return nil
+ }
+
+ // Project root path - assuming the app bundle is in the project directory
+ private var projectPath: URL? {
+ // Get the app bundle path and navigate to the project root
+ // This assumes the app is built/run from within the project directory
+ guard let bundlePath = Bundle.main.bundleURL.path as String? else { return nil }
+
+ // Navigate up from the app bundle to find the project root
+ // Look for pyproject.toml to identify the project root
+ var currentPath = URL(fileURLWithPath: bundlePath)
+ while currentPath.pathComponents.count > 1 {
+ let pyprojectPath = currentPath.appendingPathComponent("pyproject.toml")
+ if FileManager.default.fileExists(atPath: pyprojectPath.path) {
+ return currentPath
+ }
+ currentPath = currentPath.deletingLastPathComponent()
+ }
+
+ // Fallback: try to find project in common development locations
+ let homeDir = FileManager.default.homeDirectoryForCurrentUser
+ let commonPaths = [
+ "exo",
+ "Projects/exo",
+ "Documents/exo",
+ "Desktop/exo"
+ ]
+
+ for path in commonPaths {
+ let projectDir = homeDir.appendingPathComponent(path)
+ let pyprojectPath = projectDir.appendingPathComponent("pyproject.toml")
+ if FileManager.default.fileExists(atPath: pyprojectPath.path) {
+ return projectDir
+ }
+ }
+
+ return nil
+ }
+
+ init() {
+ // Add termination handler
+ NSApplication.shared.addTerminationHandler { [weak self] in
+ self?.stopAll()
+ }
+
+ // Check if login item is enabled
+ isLoginItemEnabled = (loginService.status == .enabled)
+
+ // Start processes automatically
+ startMaster()
+ DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
+ self.startWorker()
+ }
+ }
+
+ private func handleProcessOutput(_ pipe: Pipe, processName: String) -> FileHandle {
+ let fileHandle = pipe.fileHandleForReading
+ fileHandle.readabilityHandler = { [weak self] handle in
+ guard let data = try? handle.read(upToCount: 1024),
+ let output = String(data: data, encoding: .utf8) else {
+ return
+ }
+
+ DispatchQueue.main.async {
+ self?.logger.info("\(processName) output: \(output)")
+ print("[\(processName)] \(output)")
+ }
+ }
+ return fileHandle
+ }
+
+ private func cleanupProcess(process: Process?, fileHandle: FileHandle?, pipe: Pipe?) {
+ // Remove readability handler
+ fileHandle?.readabilityHandler = nil
+
+ // Close file handles
+ try? fileHandle?.close()
+ try? pipe?.fileHandleForReading.close()
+ try? pipe?.fileHandleForWriting.close()
+
+ // Terminate process if still running
+ if process?.isRunning == true {
+ process?.terminate()
+ }
+ }
+
+ func startMaster() {
+ guard let projectPath = self.projectPath else {
+ masterStatus = "Error: Project directory not found"
+ logger.error("Could not find project directory with pyproject.toml")
+ return
+ }
+
+ guard let uvPath = self.uvPath else {
+ masterStatus = "Error: uv not found"
+ logger.error("Could not find uv executable in common paths")
+ return
+ }
+
+ // Cleanup any existing process
+ cleanupProcess(process: masterProcess, fileHandle: masterFileHandle, pipe: masterStdout)
+
+ masterProcess = Process()
+ masterStdout = Pipe()
+
+ // Use uv to run the master module
+ masterProcess?.executableURL = URL(fileURLWithPath: uvPath)
+ masterProcess?.arguments = ["run", "python", "-m", "master.main"]
+ masterProcess?.standardOutput = masterStdout
+ masterProcess?.standardError = masterStdout
+
+ // Set up environment
+ var env = ProcessInfo.processInfo.environment
+ env["PYTHONUNBUFFERED"] = "1"
+ env["PYTHONPATH"] = projectPath.path
+
+ // Set replica mode if not in master mode
+ if !self.isMasterMode {
+ env["EXO_RUN_AS_REPLICA"] = "1"
+ }
+
+ masterProcess?.environment = env
+
+ // Set working directory to project root
+ masterProcess?.currentDirectoryURL = projectPath
+
+ // Store the file handle
+ masterFileHandle = handleProcessOutput(masterStdout!, processName: "Master")
+
+ do {
+ logger.info("Starting master process with \(uvPath) run python -m master.main at \(projectPath.path)")
+ try masterProcess?.run()
+ masterStatus = "Running"
+
+ masterProcess?.terminationHandler = { [weak self] process in
+ DispatchQueue.main.async {
+ let status = "Stopped (exit: \(process.terminationStatus))"
+ self?.masterStatus = status
+ self?.logger.error("Master process terminated: \(status)")
+ // Cleanup on termination
+ self?.cleanupProcess(process: self?.masterProcess,
+ fileHandle: self?.masterFileHandle,
+ pipe: self?.masterStdout)
+ }
+ }
+ } catch {
+ masterStatus = "Error: \(error.localizedDescription)"
+ logger.error("Failed to start master: \(error.localizedDescription)")
+ cleanupProcess(process: masterProcess, fileHandle: masterFileHandle, pipe: masterStdout)
+ }
+ }
+
+ func startWorker() {
+ guard let projectPath = self.projectPath else {
+ workerStatus = "Error: Project directory not found"
+ logger.error("Could not find project directory with pyproject.toml")
+ return
+ }
+
+ guard let uvPath = self.uvPath else {
+ workerStatus = "Error: uv not found"
+ logger.error("Could not find uv executable in common paths")
+ return
+ }
+
+ // Cleanup any existing process
+ cleanupProcess(process: workerProcess, fileHandle: workerFileHandle, pipe: workerStdout)
+
+ workerProcess = Process()
+ workerStdout = Pipe()
+
+ // Use uv to run the worker module
+ workerProcess?.executableURL = URL(fileURLWithPath: uvPath)
+ workerProcess?.arguments = ["run", "python", "-m", "worker.main"]
+ workerProcess?.standardOutput = workerStdout
+ workerProcess?.standardError = workerStdout
+
+ // Set up environment
+ var env = ProcessInfo.processInfo.environment
+ env["PYTHONUNBUFFERED"] = "1"
+ env["PYTHONPATH"] = projectPath.path
+ workerProcess?.environment = env
+
+ // Set working directory to project root
+ workerProcess?.currentDirectoryURL = projectPath
+
+ // Store the file handle
+ workerFileHandle = handleProcessOutput(workerStdout!, processName: "Worker")
+
+ do {
+ logger.info("Starting worker process with \(uvPath) run python -m worker.main at \(projectPath.path)")
+ try workerProcess?.run()
+ workerStatus = "Running"
+
+ workerProcess?.terminationHandler = { [weak self] process in
+ DispatchQueue.main.async {
+ let status = "Stopped (exit: \(process.terminationStatus))"
+ self?.workerStatus = status
+ self?.logger.error("Worker process terminated: \(status)")
+ // Cleanup on termination
+ self?.cleanupProcess(process: self?.workerProcess,
+ fileHandle: self?.workerFileHandle,
+ pipe: self?.workerStdout)
+ }
+ }
+ } catch {
+ workerStatus = "Error: \(error.localizedDescription)"
+ logger.error("Failed to start worker: \(error.localizedDescription)")
+ cleanupProcess(process: workerProcess, fileHandle: workerFileHandle, pipe: workerStdout)
+ }
+ }
+
+ func stopAll() {
+ logger.info("Stopping all processes")
+
+ // Clean up master process
+ cleanupProcess(process: masterProcess, fileHandle: masterFileHandle, pipe: masterStdout)
+ masterProcess = nil
+ masterStdout = nil
+ masterFileHandle = nil
+ masterStatus = "Stopped"
+
+ // Clean up worker process
+ cleanupProcess(process: workerProcess, fileHandle: workerFileHandle, pipe: workerStdout)
+ workerProcess = nil
+ workerStdout = nil
+ workerFileHandle = nil
+ workerStatus = "Stopped"
+ }
+
+ func checkBinaries() -> Bool {
+ guard let projectPath = self.projectPath else {
+ logger.error("Could not find project directory")
+ return false
+ }
+
+ guard let uvPath = self.uvPath else {
+ logger.error("Could not find uv executable")
+ return false
+ }
+
+ let fileManager = FileManager.default
+ let pyprojectPath = projectPath.appendingPathComponent("pyproject.toml").path
+ let masterPath = projectPath.appendingPathComponent("master/main.py").path
+ let workerPath = projectPath.appendingPathComponent("worker/main.py").path
+
+ let uvExists = fileManager.fileExists(atPath: uvPath)
+ let pyprojectExists = fileManager.fileExists(atPath: pyprojectPath)
+ let masterExists = fileManager.fileExists(atPath: masterPath)
+ let workerExists = fileManager.fileExists(atPath: workerPath)
+
+ if !uvExists {
+ logger.error("uv not found at \(uvPath)")
+ }
+ if !pyprojectExists {
+ logger.error("pyproject.toml not found at \(pyprojectPath)")
+ }
+ if !masterExists {
+ logger.error("master/main.py not found at \(masterPath)")
+ }
+ if !workerExists {
+ logger.error("worker/main.py not found at \(workerPath)")
+ }
+
+ return uvExists && pyprojectExists && masterExists && workerExists
+ }
+
+ func toggleLoginItem() {
+ do {
+ if isLoginItemEnabled {
+ try loginService.unregister()
+ } else {
+ try loginService.register()
+ }
+ isLoginItemEnabled = (loginService.status == .enabled)
+ } catch {
+ logger.error("Failed to toggle login item: \(error.localizedDescription)")
+ }
+ }
+
+ func toggleMasterMode() {
+ isMasterMode.toggle()
+ logger.info("Toggling master mode to: \(self.isMasterMode ? "Master" : "Replica")")
+
+ // Restart master process with new mode
+ if masterProcess?.isRunning == true {
+ // Clean up current master process
+ cleanupProcess(process: masterProcess, fileHandle: masterFileHandle, pipe: masterStdout)
+ masterProcess = nil
+ masterStdout = nil
+ masterFileHandle = nil
+ masterStatus = "Stopped"
+
+ // Start master with new mode after a brief delay
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+ self.startMaster()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/exov2/exov2/exov2.entitlements b/app/exov2/exov2/exov2.entitlements
new file mode 100644
index 00000000..9b5d06d4
--- /dev/null
+++ b/app/exov2/exov2/exov2.entitlements
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>com.apple.security.app-sandbox</key>
+ <false/>
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
+ <true/>
+ <key>com.apple.security.cs.disable-library-validation</key>
+ <true/>
+ <key>com.apple.security.automation.apple-events</key>
+ <true/>
+</dict>
+</plist>
diff --git a/app/exov2/exov2/exov2App.swift b/app/exov2/exov2/exov2App.swift
new file mode 100644
index 00000000..2a6910b6
--- /dev/null
+++ b/app/exov2/exov2/exov2App.swift
@@ -0,0 +1,115 @@
+//
+// exov2App.swift
+// exov2
+//
+// Created by Sami Khan on 2025-07-27.
+//
+
+import SwiftUI
+import AppKit
+import Foundation
+import OSLog
+import ServiceManagement
+
+@main
+struct exov2App: App {
+ @StateObject private var processManager = ProcessManager()
+
+ private func resizedMenuBarIcon(named: String, size: CGFloat = 18.0) -> NSImage? {
+ guard let original = NSImage(named: named) else {
+ print("Failed to load image named: \(named)")
+ return nil
+ }
+
+ let resized = NSImage(size: NSSize(width: size, height: size), flipped: false) { rect in
+ NSGraphicsContext.current?.imageInterpolation = .high
+ original.draw(in: rect)
+ return true
+ }
+
+ resized.isTemplate = false
+ resized.size = NSSize(width: size, height: size)
+ return resized
+ }
+
+ var body: some Scene {
+ MenuBarExtra {
+ MenuBarView(processManager: processManager)
+ } label: {
+ if let resizedImage = resizedMenuBarIcon(named: "menubar-icon") {
+ Image(nsImage: resizedImage)
+ .opacity(processManager.masterStatus == "Running" ? 1.0 : 0.5)
+ }
+ }
+ .menuBarExtraStyle(.window)
+ }
+}
+
+struct MenuBarView: View {
+ @ObservedObject var processManager: ProcessManager
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ StatusSection(processManager: processManager)
+
+ Divider()
+
+ Toggle("Launch at Login", isOn: Binding(
+ get: { processManager.isLoginItemEnabled },
+ set: { _ in processManager.toggleLoginItem() }
+ ))
+ .padding(.horizontal)
+
+ Toggle("Is Master?", isOn: Binding(
+ get: { processManager.isMasterMode },
+ set: { _ in processManager.toggleMasterMode() }
+ ))
+ .padding(.horizontal)
+
+ Divider()
+
+ Button("Quit") {
+ NSApplication.shared.terminate(nil)
+ }
+ }
+ .padding()
+ .frame(width: 250)
+ .onAppear {
+ if !processManager.checkBinaries() {
+ showEnvironmentError()
+ }
+ }
+ }
+
+ private func showEnvironmentError() {
+ let alert = NSAlert()
+ alert.messageText = "Python Environment Error"
+ alert.informativeText = "Could not find the required Python environment, uv, or project files. Please ensure uv is installed and the project directory is accessible."
+ alert.alertStyle = .critical
+ alert.addButton(withTitle: "OK")
+ alert.runModal()
+ NSApplication.shared.terminate(nil)
+ }
+}
+
+struct StatusSection: View {
+ @ObservedObject var processManager: ProcessManager
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 4) {
+ HStack {
+ Text("Master:")
+ .bold()
+ Text(processManager.masterStatus)
+ .foregroundColor(processManager.masterStatus == "Running" ? .green : .red)
+ }
+
+ HStack {
+ Text("Worker:")
+ .bold()
+ Text(processManager.workerStatus)
+ .foregroundColor(processManager.workerStatus == "Running" ? .green : .red)
+ }
+ }
+ }
+}
diff --git a/app/exov2/exov2Tests/exov2Tests.swift b/app/exov2/exov2Tests/exov2Tests.swift
new file mode 100644
index 00000000..dd137fbd
--- /dev/null
+++ b/app/exov2/exov2Tests/exov2Tests.swift
@@ -0,0 +1,17 @@
+//
+// exov2Tests.swift
+// exov2Tests
+//
+// Created by Sami Khan on 2025-07-27.
+//
+
+import Testing
+@testable import exov2
+
+struct exov2Tests {
+
+ @Test func example() async throws {
+ // Write your test here and use APIs like `#expect(...)` to check expected conditions.
+ }
+
+}
diff --git a/app/exov2/exov2UITests/exov2UITests.swift b/app/exov2/exov2UITests/exov2UITests.swift
new file mode 100644
index 00000000..db1586a9
--- /dev/null
+++ b/app/exov2/exov2UITests/exov2UITests.swift
@@ -0,0 +1,43 @@
+//
+// exov2UITests.swift
+// exov2UITests
+//
+// Created by Sami Khan on 2025-07-27.
+//
+
+import XCTest
+
+final class exov2UITests: XCTestCase {
+
+ 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
+
+ // 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 {
+ // Put teardown code here. This method is called after the invocation of each test method in the class.
+ }
+
+ @MainActor
+ func testExample() throws {
+ // UI tests must launch the application that they test.
+ let app = XCUIApplication()
+ app.launch()
+
+ // Use XCTAssert and related functions to verify your tests produce the correct results.
+ }
+
+ @MainActor
+ func testLaunchPerformance() throws {
+ if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
+ // This measures how long it takes to launch your application.
+ measure(metrics: [XCTApplicationLaunchMetric()]) {
+ XCUIApplication().launch()
+ }
+ }
+ }
+}
diff --git a/app/exov2/exov2UITests/exov2UITestsLaunchTests.swift b/app/exov2/exov2UITests/exov2UITestsLaunchTests.swift
new file mode 100644
index 00000000..928b4443
--- /dev/null
+++ b/app/exov2/exov2UITests/exov2UITestsLaunchTests.swift
@@ -0,0 +1,33 @@
+//
+// exov2UITestsLaunchTests.swift
+// exov2UITests
+//
+// Created by Sami Khan on 2025-07-27.
+//
+
+import XCTest
+
+final class exov2UITestsLaunchTests: XCTestCase {
+
+ override class var runsForEachTargetApplicationUIConfiguration: Bool {
+ true
+ }
+
+ override func setUpWithError() throws {
+ continueAfterFailure = false
+ }
+
+ @MainActor
+ func testLaunch() throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ // Insert steps here to perform after app launch but before taking a screenshot,
+ // such as logging into a test account or navigating somewhere in the app
+
+ let attachment = XCTAttachment(screenshot: app.screenshot())
+ attachment.name = "Launch Screen"
+ attachment.lifetime = .keepAlways
+ add(attachment)
+ }
+}
← 71bafabc Dashboard with instances
·
back to Exo
·
Remove rust 92c9688b →