From 958973c7b71858ebaedaeb4eef33ed3ced7b8664 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 21 Jul 2025 14:22:45 +0200 Subject: [PATCH] refactor: extract all magic strings to centralized constants (#444) --- .../Core/Constants/APIEndpoints.swift | 32 ++ .../Core/Constants/BundleIdentifiers.swift | 57 ++++ .../Core/Constants/EnvironmentKeys.swift | 21 ++ .../Core/Constants/ErrorMessages.swift | 102 +++++++ .../Core/Constants/FilePathConstants.swift | 64 ++++ .../Core/Constants/KeychainConstants.swift | 14 + .../Core/Constants/NetworkConstants.swift | 39 +++ .../Core/Constants/NotificationNames.swift | 21 ++ mac/VibeTunnel/Core/Constants/UIStrings.swift | 88 ++++++ .../Core/Constants/URLConstants.swift | 53 ++++ .../Core/Constants/UserDefaultsKeys.swift | 42 +++ .../EnvironmentValues+Services.swift | 6 + mac/VibeTunnel/Core/Models/AppConstants.swift | 4 +- .../Core/Models/ConfigurationEnums.swift | 12 +- mac/VibeTunnel/Core/Services/BunServer.swift | 27 +- .../Core/Services/ConfigManager.swift | 54 ++-- .../Core/Services/ControlPayloads.swift | 23 +- .../Core/Services/ControlProtocol.swift | 33 --- .../Core/Services/DashboardKeychain.swift | 6 +- .../Core/Services/NgrokService.swift | 18 +- .../Services/RepositoryPathSyncService.swift | 148 ---------- .../Core/Services/ServerManager.swift | 37 ++- .../Core/Services/SessionService.swift | 49 +-- .../Core/Services/TerminalManager.swift | 8 +- .../Core/Utilities/ErrorHandling.swift | 6 +- .../Core/Utilities/GitAppHelper.swift | 10 +- .../Components/NewSessionForm.swift | 1 - .../Views/SessionDetailView.swift | 31 +- .../SecurityPermissionsSettingsView.swift | 1 - mac/VibeTunnel/Utilities/CLIInstaller.swift | 8 +- mac/VibeTunnel/Utilities/GitAppLauncher.swift | 22 +- .../Utilities/TerminalLauncher.swift | 16 +- .../Utilities/WelcomeWindowController.swift | 6 +- mac/VibeTunnel/VibeTunnelApp.swift | 16 +- .../AppleScriptExecutorTests.swift | 6 +- .../RepositoryPathSyncServiceTests.swift | 278 ------------------ mac/VibeTunnelTests/ServerManagerTests.swift | 6 +- .../SystemControlHandlerTests.swift | 1 - .../Utilities/TestConditions.swift | 7 +- web/src/client/components/session-card.ts | 4 +- .../services/session-action-service.test.ts | 8 +- .../client/services/session-action-service.ts | 14 +- 42 files changed, 759 insertions(+), 640 deletions(-) create mode 100644 mac/VibeTunnel/Core/Constants/APIEndpoints.swift create mode 100644 mac/VibeTunnel/Core/Constants/BundleIdentifiers.swift create mode 100644 mac/VibeTunnel/Core/Constants/EnvironmentKeys.swift create mode 100644 mac/VibeTunnel/Core/Constants/ErrorMessages.swift create mode 100644 mac/VibeTunnel/Core/Constants/FilePathConstants.swift create mode 100644 mac/VibeTunnel/Core/Constants/KeychainConstants.swift create mode 100644 mac/VibeTunnel/Core/Constants/NetworkConstants.swift create mode 100644 mac/VibeTunnel/Core/Constants/NotificationNames.swift create mode 100644 mac/VibeTunnel/Core/Constants/UIStrings.swift create mode 100644 mac/VibeTunnel/Core/Constants/URLConstants.swift create mode 100644 mac/VibeTunnel/Core/Constants/UserDefaultsKeys.swift delete mode 100644 mac/VibeTunnel/Core/Services/RepositoryPathSyncService.swift delete mode 100644 mac/VibeTunnelTests/RepositoryPathSyncServiceTests.swift diff --git a/mac/VibeTunnel/Core/Constants/APIEndpoints.swift b/mac/VibeTunnel/Core/Constants/APIEndpoints.swift new file mode 100644 index 00000000..add98b08 --- /dev/null +++ b/mac/VibeTunnel/Core/Constants/APIEndpoints.swift @@ -0,0 +1,32 @@ +import Foundation + +/// Centralized API endpoints for the VibeTunnel server +enum APIEndpoints { + // MARK: - Session Management + + static let sessions = "/api/sessions" + + static func sessionDetail(id: String) -> String { + "/api/sessions/\(id)" + } + + static func sessionInput(id: String) -> String { + "/api/sessions/\(id)/input" + } + + static func sessionStream(id: String) -> String { + "/api/sessions/\(id)/stream" + } + + static func sessionResize(id: String) -> String { + "/api/sessions/\(id)/resize" + } + + // MARK: - Cleanup + + static let cleanupExited = "/api/cleanup-exited" + + // MARK: - WebSocket + + static let buffers = "/buffers" +} diff --git a/mac/VibeTunnel/Core/Constants/BundleIdentifiers.swift b/mac/VibeTunnel/Core/Constants/BundleIdentifiers.swift new file mode 100644 index 00000000..e2dddc3a --- /dev/null +++ b/mac/VibeTunnel/Core/Constants/BundleIdentifiers.swift @@ -0,0 +1,57 @@ +import Foundation + +/// Centralized bundle identifiers for external applications +enum BundleIdentifiers { + // MARK: - VibeTunnel + + static let main = "sh.vibetunnel.vibetunnel" + static let vibeTunnel = "sh.vibetunnel.vibetunnel" + + // MARK: - Terminal Applications + + static let terminal = "com.apple.Terminal" + static let iTerm2 = "com.googlecode.iterm2" + static let ghostty = "com.mitchellh.ghostty" + static let wezterm = "com.github.wez.wezterm" + static let warp = "dev.warp.Warp-Stable" + static let alacritty = "org.alacritty" + static let hyper = "co.zeit.hyper" + static let kitty = "net.kovidgoyal.kitty" + + enum Terminal { + static let apple = "com.apple.Terminal" + static let iTerm2 = "com.googlecode.iterm2" + static let ghostty = "com.mitchellh.ghostty" + static let wezTerm = "com.github.wez.wezterm" + } + + // MARK: - Git Applications + + static let cursor = "com.todesktop.230313mzl4w4u92" + static let fork = "com.DanPristupov.Fork" + static let githubDesktop = "com.github.GitHubClient" + static let gitup = "co.gitup.mac" + static let juxtaCode = "com.naiveapps.juxtacode" + static let sourcetree = "com.torusknot.SourceTreeNotMAS" + static let sublimeMerge = "com.sublimemerge" + static let tower = "com.fournova.Tower3" + static let vscode = "com.microsoft.VSCode" + static let windsurf = "com.codeiumapp.windsurf" + + enum Git { + static let githubDesktop = "com.todesktop.230313mzl4w4u92" + static let fork = "com.DanPristupov.Fork" + static let githubClient = "com.github.GitHubClient" + static let juxtaCode = "com.naiveapps.juxtacode" + static let sourceTree = "com.torusknot.SourceTreeNotMAS" + static let sublimeMerge = "com.sublimemerge" + static let tower = "com.fournova.Tower3" + } + + // MARK: - Code Editors + + enum Editor { + static let vsCode = "com.microsoft.VSCode" + static let windsurf = "com.codeiumapp.windsurf" + } +} diff --git a/mac/VibeTunnel/Core/Constants/EnvironmentKeys.swift b/mac/VibeTunnel/Core/Constants/EnvironmentKeys.swift new file mode 100644 index 00000000..91ba433f --- /dev/null +++ b/mac/VibeTunnel/Core/Constants/EnvironmentKeys.swift @@ -0,0 +1,21 @@ +import Foundation + +/// Centralized environment variable names +enum EnvironmentKeys { + // MARK: - System Environment + + static let path = "PATH" + static let lang = "LANG" + + // MARK: - Test Environment + + static let xcTestConfigurationFilePath = "XCTestConfigurationFilePath" + static let ci = "CI" + + // MARK: - VibeTunnel Environment + + static let parentPID = "PARENT_PID" + static let useDevelopmentServer = "USE_DEVELOPMENT_SERVER" + static let authenticationMode = "AUTHENTICATION_MODE" + static let authToken = "AUTH_TOKEN" +} diff --git a/mac/VibeTunnel/Core/Constants/ErrorMessages.swift b/mac/VibeTunnel/Core/Constants/ErrorMessages.swift new file mode 100644 index 00000000..015773dd --- /dev/null +++ b/mac/VibeTunnel/Core/Constants/ErrorMessages.swift @@ -0,0 +1,102 @@ +import Foundation + +/// Centralized error messages and user-facing strings +enum ErrorMessages { + // MARK: - Notification Errors + + static func notificationPermissionError(_ error: Error) -> String { + "Failed to request notification permissions: \(error.localizedDescription)" + } + + // MARK: - Session Errors + + static let sessionNotFound = "Session not found" + static let operationTimeout = "Operation timed out" + static let invalidRequest = "Invalid request" + static let sessionNameEmpty = "Session name cannot be empty" + static let terminalWindowNotFound = "Could not find a terminal window for this session. The window may have been closed or the session was started outside VibeTunnel." + static func windowNotFoundForSession(_ sessionID: String) -> String { + "Could not find window for session \(sessionID)" + } + + static func windowCloseFailedForSession(_ sessionID: String) -> String { + "Failed to close window for session \(sessionID)" + } + + // MARK: - Launch/Login Errors + + static func launchAtLoginError(_ enabled: Bool, _ error: Error) -> String { + "Failed to \(enabled ? "register" : "unregister") for launch at login: \(error.localizedDescription)" + } + + // MARK: - Process Errors + + static func errorOutputReadError(_ error: Error) -> String { + "Could not read error output: \(error.localizedDescription)" + } + + static func modificationDateError(_ path: String) -> String { + "Could not get modification date for \(path)" + } + + static func processTerminationError(_ pid: Int) -> String { + "Failed to terminate process with PID \(pid)" + } + + static func processLaunchError(_ error: Error) -> String { + "Failed to launch process: \(error.localizedDescription)" + } + + // MARK: - Keychain Errors + + static let keychainSaveError = "Failed to save the auth token to the keychain. Please check your keychain permissions and try again." + static let keychainRetrieveError = "Failed to retrieve token from keychain" + static let keychainAccessError = "Failed to access auth token. Please try again." + static let keychainSaveTokenError = "Failed to save token to keychain" + + // MARK: - Server Errors + + static let serverRestartError = "Failed to Restart Server" + static let socketCreationError = "Failed to create socket for port check" + static let serverNotRunning = "Server is not running" + static let invalidServerURL = "Invalid server URL" + static let invalidServerResponse = "Invalid server response" + + // MARK: - URL Scheme Errors + + static let urlSchemeOpenError = "Failed to open URL scheme" + static func terminalLaunchError(_ terminalName: String) -> String { + "Failed to launch terminal: \(terminalName)" + } + + // MARK: - Tunnel Errors + + static func tunnelCreationError(_ error: Error) -> String { + "Failed to create tunnel: \(error.localizedDescription)" + } + + static let ngrokPublicURLNotFound = "Could not find public URL in ngrok output" + static func ngrokStartError(_ error: Error) -> String { + "Failed to start ngrok: \(error.localizedDescription)" + } + + static let ngrokNotInstalled = "ngrok is not installed. Please install it using 'brew install ngrok' or download from ngrok.com" + static let ngrokAuthTokenMissing = "ngrok auth token is missing. Please add it in Settings" + static let invalidNgrokConfiguration = "Invalid ngrok configuration" + + // MARK: - Permission Errors + + static let permissionDenied = "Permission Denied" + static let accessibilityPermissionRequired = "Accessibility Permission Required" + + // MARK: - Terminal Errors + + static let terminalNotFound = "Terminal Not Found" + static let terminalNotAvailable = "Terminal Not Available" + static let terminalCommunicationError = "Terminal Communication Error" + static let terminalLaunchFailed = "Terminal Launch Failed" + + // MARK: - CLI Tool Errors + + static let cliToolInstallationFailed = "CLI Tool Installation Failed" +} diff --git a/mac/VibeTunnel/Core/Constants/FilePathConstants.swift b/mac/VibeTunnel/Core/Constants/FilePathConstants.swift new file mode 100644 index 00000000..909da2d3 --- /dev/null +++ b/mac/VibeTunnel/Core/Constants/FilePathConstants.swift @@ -0,0 +1,64 @@ +import Foundation + +/// Centralized file path constants +enum FilePathConstants { + // MARK: - System Paths + + static let usrBin = "/usr/bin" + static let bin = "/bin" + static let usrLocalBin = "/usr/local/bin" + static let optHomebrewBin = "/opt/homebrew/bin" + + // MARK: - Command Paths + + static let which = "/usr/bin/which" + static let git = "/usr/bin/git" + static let homebrewGit = "/opt/homebrew/bin/git" + static let localGit = "/usr/local/bin/git" + + // MARK: - Shell Paths + + static let defaultShell = "/bin/zsh" + static let bash = "/bin/bash" + static let zsh = "/bin/zsh" + static let sh = "/bin/sh" + + // MARK: - Application Paths + + static let applicationsVibeTunnel = "/Applications/VibeTunnel.app" + static let userApplicationsVibeTunnel = "$HOME/Applications/VibeTunnel.app" + + // MARK: - Temporary Directory + + static let tmpDirectory = "/tmp/" + + // MARK: - Common Repository Base Paths + + static let projectsPath = "~/Projects" + static let documentsCodePath = "~/Documents/Code" + static let developmentPath = "~/Development" + static let sourcePath = "~/Source" + static let workPath = "~/Work" + static let codePath = "~/Code" + static let sitesPath = "~/Sites" + static let desktopPath = "~/Desktop" + static let documentsPath = "~/Documents" + static let downloadsPath = "~/Downloads" + static let homePath = "~/" + + // MARK: - Resource Names + + static let vibetunnelBinary = "vibetunnel" + static let vtCLI = "vt" + + // MARK: - Configuration Files + + static let infoPlist = "Info.plist" + static let entitlements = "VibeTunnel.entitlements" + + // MARK: - Helper Functions + + static func expandTilde(_ path: String) -> String { + path.replacingOccurrences(of: "~", with: NSHomeDirectory()) + } +} diff --git a/mac/VibeTunnel/Core/Constants/KeychainConstants.swift b/mac/VibeTunnel/Core/Constants/KeychainConstants.swift new file mode 100644 index 00000000..060086d2 --- /dev/null +++ b/mac/VibeTunnel/Core/Constants/KeychainConstants.swift @@ -0,0 +1,14 @@ +import Foundation + +/// Centralized keychain service and account names +enum KeychainConstants { + // MARK: - Service Names + + static let vibeTunnelService = "sh.vibetunnel.vibetunnel" + + // MARK: - Account Names + + static let ngrokAuthToken = "ngrokAuthToken" + static let authToken = "authToken" + static let dashboardPassword = "dashboard-password" +} diff --git a/mac/VibeTunnel/Core/Constants/NetworkConstants.swift b/mac/VibeTunnel/Core/Constants/NetworkConstants.swift new file mode 100644 index 00000000..f53d9121 --- /dev/null +++ b/mac/VibeTunnel/Core/Constants/NetworkConstants.swift @@ -0,0 +1,39 @@ +import Foundation + +/// Centralized network-related constants +enum NetworkConstants { + // MARK: - Default Port + + static let defaultPort = 4_020 + + // MARK: - Headers + + static let localAuthHeader = "X-VibeTunnel-Local" + static let authorizationHeader = "Authorization" + static let contentTypeHeader = "Content-Type" + static let hostHeader = "Host" + + // MARK: - Content Types + + static let contentTypeJSON = "application/json" + static let contentTypeHTML = "text/html" + static let contentTypeText = "text/plain" + + // MARK: - Common Values + + static let localhost = "localhost" + + // MARK: - Timeout Values + + static let defaultTimeout: TimeInterval = 30.0 + static let uploadTimeout: TimeInterval = 300.0 + static let downloadTimeout: TimeInterval = 300.0 + + // MARK: - HTTP Methods + + static let httpMethodGET = "GET" + static let httpMethodPOST = "POST" + static let httpMethodPUT = "PUT" + static let httpMethodDELETE = "DELETE" + static let httpMethodPATCH = "PATCH" +} diff --git a/mac/VibeTunnel/Core/Constants/NotificationNames.swift b/mac/VibeTunnel/Core/Constants/NotificationNames.swift new file mode 100644 index 00000000..da786280 --- /dev/null +++ b/mac/VibeTunnel/Core/Constants/NotificationNames.swift @@ -0,0 +1,21 @@ +import Foundation + +/// Centralized notification names +extension Notification.Name { + // MARK: - Settings + + static let showSettings = Notification.Name("sh.vibetunnel.vibetunnel.showSettings") + + // MARK: - Updates + + static let checkForUpdates = Notification.Name("checkForUpdates") + + // MARK: - Welcome + + static let showWelcomeScreen = Notification.Name("showWelcomeScreen") +} + +/// Notification categories +enum NotificationCategories { + static let updateReminder = "UPDATE_REMINDER" +} diff --git a/mac/VibeTunnel/Core/Constants/UIStrings.swift b/mac/VibeTunnel/Core/Constants/UIStrings.swift new file mode 100644 index 00000000..9a0cb8c5 --- /dev/null +++ b/mac/VibeTunnel/Core/Constants/UIStrings.swift @@ -0,0 +1,88 @@ +import Foundation + +/// Centralized UI strings for consistency +enum UIStrings { + // MARK: - Common UI + + static let error = "Error" + static let ok = "OK" + static let cancel = "Cancel" + static let done = "Done" + static let save = "Save" + static let delete = "Delete" + static let edit = "Edit" + static let add = "Add" + static let remove = "Remove" + static let close = "Close" + static let open = "Open" + static let yes = "Yes" + static let no = "No" + + // MARK: - Search + + static let searchPlaceholder = "Search sessions..." + + // MARK: - Actions + + static let copy = "Copy" + static let paste = "Paste" + static let cut = "Cut" + static let selectAll = "Select All" + + // MARK: - Status + + static let loading = "Loading..." + static let saving = "Saving..." + static let updating = "Updating..." + static let connecting = "Connecting..." + static let disconnected = "Disconnected" + static let connected = "Connected" + + // MARK: - Validation + + static let required = "Required" + static let optional = "Optional" + static let invalid = "Invalid" + static let valid = "Valid" + + // MARK: - App Names + + static let appName = "VibeTunnel" + static let appNameDebug = "VibeTunnel Debug" + + // MARK: - Session UI + + static let sessionDetails = "Session Details" + static let windowInformation = "Window Information" + static let noWindowInformation = "No window information available" + + // MARK: - Terminal UI + + static let terminalApp = "Terminal App" + static let terminalAutomation = "Terminal Automation" + static let accessibility = "Accessibility" + + // MARK: - Terminal Names + + static let terminal = "Terminal" + static let iTerm2 = "iTerm2" + static let ghostty = "Ghostty" + static let wezTerm = "WezTerm" + + // MARK: - CLI Tool UI + + static let installCLITool = "Install VT Command Line Tool" + static let uninstallCLITool = "Uninstall VT Command Line Tool" + static let cliToolsInstalledSuccess = "CLI Tools Installed Successfully" + static let cliToolsUninstalledSuccess = "CLI Tools Uninstalled Successfully" + + // MARK: - Dashboard UI + + static let accessingDashboard = "Accessing Your Dashboard" + static let openDashboard = "Open Dashboard" + + // MARK: - Welcome Screen + + static let welcomeTitle = "Welcome to VibeTunnel" + static let welcomeSubtitle = "Turn any browser into your terminal. Command your agents on the go." +} diff --git a/mac/VibeTunnel/Core/Constants/URLConstants.swift b/mac/VibeTunnel/Core/Constants/URLConstants.swift new file mode 100644 index 00000000..9a133c3a --- /dev/null +++ b/mac/VibeTunnel/Core/Constants/URLConstants.swift @@ -0,0 +1,53 @@ +import Foundation + +/// Centralized URL constants for the VibeTunnel application +enum URLConstants { + // MARK: - Local Server URLs + + static let localhost = "http://localhost:" + static let localhostIP = "http://127.0.0.1:" + + // MARK: - Main Website & Social + + static let website = "https://vibetunnel.sh" + static let githubRepo = "https://github.com/amantus-ai/vibetunnel" + static let githubIssues = "https://github.com/amantus-ai/vibetunnel/issues" + static let twitter = "https://x.com/VibeTunnel" + + // MARK: - Contributors + + static let contributorMario = "https://mariozechner.at/" + static let contributorArmin = "https://lucumr.pocoo.org/" + static let contributorPeter = "https://steipete.me" + + // MARK: - Tailscale + + static let tailscaleWebsite = "https://tailscale.com/" + static let tailscaleAppStore = "https://apps.apple.com/us/app/tailscale/id1475387142" + static let tailscaleDownloadMac = "https://tailscale.com/download/macos" + static let tailscaleInstallGuide = "https://tailscale.com/kb/1017/install/" + static let tailscaleAPI = "http://100.100.100.100/api/data" + + // MARK: - Cloudflare + + static let cloudflareFormula = "https://formulae.brew.sh/formula/cloudflared" + static let cloudflareReleases = "https://github.com/cloudflare/cloudflared/releases/latest" + static let cloudflareDocs = "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/" + + // MARK: - Update Feed + + static let updateFeedStable = "https://stats.store/api/v1/appcast/appcast.xml" + static let updateFeedPrerelease = "https://stats.store/api/v1/appcast/appcast-prerelease.xml" + + // MARK: - Documentation + + static let claudeCodeArmyPost = "https://steipete.me/posts/command-your-claude-code-army-reloaded" + + // MARK: - Regular Expressions + + static let cloudflareURLPattern = #"https://[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.trycloudflare\.com/?(?:\s|$)"# + + // MARK: - Local Server Base + + static let localServerBase = "http://127.0.0.1" +} diff --git a/mac/VibeTunnel/Core/Constants/UserDefaultsKeys.swift b/mac/VibeTunnel/Core/Constants/UserDefaultsKeys.swift new file mode 100644 index 00000000..eaf1c611 --- /dev/null +++ b/mac/VibeTunnel/Core/Constants/UserDefaultsKeys.swift @@ -0,0 +1,42 @@ +import Foundation + +/// Centralized UserDefaults keys +enum UserDefaultsKeys { + // MARK: - Server Settings + + static let serverPort = "serverPort" + static let dashboardAccessMode = "dashboardAccessMode" + static let cleanupOnStartup = "cleanupOnStartup" + static let preventSleepWhenRunning = "preventSleepWhenRunning" + static let useDevelopmentServer = "useDevelopmentServer" + + // MARK: - App Preferences + + static let debugMode = "debugMode" + static let showDockIcon = "showDockIcon" + static let launchAtLogin = "launchAtLogin" + static let preferredTerminal = "preferredTerminal" + static let preferredGitApp = "preferredGitApp" + + // MARK: - UI Preferences + + static let showIconInDock = "showIconInDock" + static let hideOnStartup = "hideOnStartup" + static let menuBarIconStyle = "menuBarIconStyle" + + // MARK: - First Run + + static let hasShownWelcome = "hasShownWelcome" + static let hasCompletedOnboarding = "hasCompletedOnboarding" + + // MARK: - Update Settings + + static let automaticallyCheckForUpdates = "automaticallyCheckForUpdates" + static let automaticallyDownloadUpdates = "automaticallyDownloadUpdates" + static let updateChannelPreference = "updateChannelPreference" + + // MARK: - Path Sync + + static let pathSyncShouldRun = "pathSyncShouldRun" + static let pathSyncTerminalPath = "pathSyncTerminalPath" +} diff --git a/mac/VibeTunnel/Core/Extensions/EnvironmentValues+Services.swift b/mac/VibeTunnel/Core/Extensions/EnvironmentValues+Services.swift index de5c1e5d..16f7f876 100644 --- a/mac/VibeTunnel/Core/Extensions/EnvironmentValues+Services.swift +++ b/mac/VibeTunnel/Core/Extensions/EnvironmentValues+Services.swift @@ -2,26 +2,32 @@ import SwiftUI // MARK: - Environment Keys +/// Environment key for ServerManager dependency injection private struct ServerManagerKey: EnvironmentKey { static let defaultValue: ServerManager? = nil } +/// Environment key for NgrokService dependency injection private struct NgrokServiceKey: EnvironmentKey { static let defaultValue: NgrokService? = nil } +/// Environment key for SystemPermissionManager dependency injection private struct SystemPermissionManagerKey: EnvironmentKey { static let defaultValue: SystemPermissionManager? = nil } +/// Environment key for TerminalLauncher dependency injection private struct TerminalLauncherKey: EnvironmentKey { static let defaultValue: TerminalLauncher? = nil } +/// Environment key for TailscaleService dependency injection private struct TailscaleServiceKey: EnvironmentKey { static let defaultValue: TailscaleService? = nil } +/// Environment key for CloudflareService dependency injection private struct CloudflareServiceKey: EnvironmentKey { static let defaultValue: CloudflareService? = nil } diff --git a/mac/VibeTunnel/Core/Models/AppConstants.swift b/mac/VibeTunnel/Core/Models/AppConstants.swift index 254e8f0e..53ba4550 100644 --- a/mac/VibeTunnel/Core/Models/AppConstants.swift +++ b/mac/VibeTunnel/Core/Models/AppConstants.swift @@ -249,13 +249,13 @@ extension AppConstants { UserDefaults.standard.removeObject(forKey: UserDefaultsKeys.preferredTerminal) } } - + /// Get current dashboard access mode static func getDashboardAccessMode() -> DashboardAccessMode { let rawValue = stringValue(for: UserDefaultsKeys.dashboardAccessMode) return DashboardAccessMode(rawValue: rawValue) ?? .network } - + /// Set dashboard access mode static func setDashboardAccessMode(_ mode: DashboardAccessMode) { UserDefaults.standard.set(mode.rawValue, forKey: UserDefaultsKeys.dashboardAccessMode) diff --git a/mac/VibeTunnel/Core/Models/ConfigurationEnums.swift b/mac/VibeTunnel/Core/Models/ConfigurationEnums.swift index ce9e743f..efbbba99 100644 --- a/mac/VibeTunnel/Core/Models/ConfigurationEnums.swift +++ b/mac/VibeTunnel/Core/Models/ConfigurationEnums.swift @@ -1,15 +1,16 @@ import Foundation -/// Shared configuration enums used across the application +// Shared configuration enums used across the application // MARK: - Authentication Mode +/// Represents the available authentication modes for dashboard access enum AuthenticationMode: String, CaseIterable { case none = "none" case osAuth = "os" case sshKeys = "ssh" case both = "both" - + var displayName: String { switch self { case .none: "None" @@ -18,7 +19,7 @@ enum AuthenticationMode: String, CaseIterable { case .both: "macOS + SSH Keys" } } - + var description: String { switch self { case .none: "Anyone can access the dashboard (not recommended)" @@ -31,12 +32,13 @@ enum AuthenticationMode: String, CaseIterable { // MARK: - Title Mode +/// Represents the terminal window title display modes enum TitleMode: String, CaseIterable { case none = "none" case filter = "filter" case `static` = "static" case dynamic = "dynamic" - + var displayName: String { switch self { case .none: "None" @@ -45,4 +47,4 @@ enum TitleMode: String, CaseIterable { case .dynamic: "Dynamic" } } -} \ No newline at end of file +} diff --git a/mac/VibeTunnel/Core/Services/BunServer.swift b/mac/VibeTunnel/Core/Services/BunServer.swift index f060fe80..8ae352e4 100644 --- a/mac/VibeTunnel/Core/Services/BunServer.swift +++ b/mac/VibeTunnel/Core/Services/BunServer.swift @@ -36,8 +36,8 @@ final class BunServer { /// Resource cleanup tracking private var isCleaningUp = false - private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "BunServer") - private let serverOutput = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "ServerOutput") + private let logger = Logger(subsystem: BundleIdentifiers.main, category: "BunServer") + private let serverOutput = Logger(subsystem: BundleIdentifiers.main, category: "ServerOutput") var isRunning: Bool { state == .running @@ -121,11 +121,11 @@ final class BunServer { guard let binaryPath = Bundle.main.path(forResource: "vibetunnel", ofType: nil) else { let error = BunServerError.binaryNotFound logger.error("vibetunnel binary not found in bundle") - + // Additional diagnostics for CI debugging logger.error("Bundle path: \(Bundle.main.bundlePath)") logger.error("Resources path: \(Bundle.main.resourcePath ?? "nil")") - + // List contents of Resources directory if let resourcesPath = Bundle.main.resourcePath { do { @@ -135,7 +135,7 @@ final class BunServer { logger.error("Failed to list Resources directory: \(error)") } } - + throw error } @@ -269,7 +269,16 @@ final class BunServer { environment["NODE_OPTIONS"] = "--max-old-space-size=4096 --max-semi-space-size=128" // Copy only essential environment variables - let essentialVars = ["PATH", "HOME", "USER", "SHELL", "LANG", "LC_ALL", "LC_CTYPE", "VIBETUNNEL_DEBUG"] + let essentialVars = [ + EnvironmentKeys.path, + "HOME", + "USER", + "SHELL", + EnvironmentKeys.lang, + "LC_ALL", + "LC_CTYPE", + "VIBETUNNEL_DEBUG" + ] for key in essentialVars { if let value = ProcessInfo.processInfo.environment[key] { environment[key] = value @@ -486,10 +495,10 @@ final class BunServer { // Add pnpm to PATH so that scripts can use it // pnpmDir is already defined above - if let existingPath = environment["PATH"] { - environment["PATH"] = "\(pnpmDir):\(existingPath)" + if let existingPath = environment[EnvironmentKeys.path] { + environment[EnvironmentKeys.path] = "\(pnpmDir):\(existingPath)" } else { - environment["PATH"] = pnpmDir + environment[EnvironmentKeys.path] = pnpmDir } logger.info("Added pnpm directory to PATH: \(pnpmDir)") diff --git a/mac/VibeTunnel/Core/Services/ConfigManager.swift b/mac/VibeTunnel/Core/Services/ConfigManager.swift index 5736ecf0..786a3c9e 100644 --- a/mac/VibeTunnel/Core/Services/ConfigManager.swift +++ b/mac/VibeTunnel/Core/Services/ConfigManager.swift @@ -16,30 +16,30 @@ class ConfigManager: ObservableObject { // Core configuration @Published private(set) var quickStartCommands: [QuickStartCommand] = [] @Published var repositoryBasePath: String = "~/" - + // Server settings - @Published var serverPort: Int = 4020 + @Published var serverPort: Int = 4_020 @Published var dashboardAccessMode: DashboardAccessMode = .network @Published var cleanupOnStartup: Bool = true @Published var authenticationMode: AuthenticationMode = .osAuth - + // Development settings @Published var debugMode: Bool = false @Published var useDevServer: Bool = false @Published var devServerPath: String = "" @Published var logLevel: String = "info" - + // Application preferences @Published var preferredGitApp: String? @Published var preferredTerminal: String? @Published var updateChannel: UpdateChannel = .stable @Published var showInDock: Bool = false @Published var preventSleepWhenRunning: Bool = true - + // Remote access @Published var ngrokEnabled: Bool = false @Published var ngrokTokenPresent: Bool = false - + // Session defaults @Published var sessionCommand: String = "zsh" @Published var sessionWorkingDirectory: String = "~/" @@ -83,7 +83,7 @@ class ConfigManager: ObservableObject { let version: Int var quickStartCommands: [QuickStartCommand] var repositoryBasePath: String? - + // Extended configuration sections var server: ServerConfig? var development: DevelopmentConfig? @@ -91,23 +91,23 @@ class ConfigManager: ObservableObject { var remoteAccess: RemoteAccessConfig? var sessionDefaults: SessionDefaultsConfig? } - + // MARK: - Configuration Sub-structures - + private struct ServerConfig: Codable { var port: Int var dashboardAccessMode: String var cleanupOnStartup: Bool var authenticationMode: String } - + private struct DevelopmentConfig: Codable { var debugMode: Bool var useDevServer: Bool var devServerPath: String var logLevel: String } - + private struct PreferencesConfig: Codable { var preferredGitApp: String? var preferredTerminal: String? @@ -115,12 +115,12 @@ class ConfigManager: ObservableObject { var showInDock: Bool var preventSleepWhenRunning: Bool } - + private struct RemoteAccessConfig: Codable { var ngrokEnabled: Bool var ngrokTokenPresent: Bool } - + private struct SessionDefaultsConfig: Codable { var command: String var workingDirectory: String @@ -160,11 +160,11 @@ class ConfigManager: ObservableObject { do { let data = try Data(contentsOf: configPath) let config = try JSONDecoder().decode(VibeTunnelConfig.self, from: data) - + // Load all configuration values self.quickStartCommands = config.quickStartCommands self.repositoryBasePath = config.repositoryBasePath ?? "~/" - + // Server settings if let server = config.server { self.serverPort = server.port @@ -172,7 +172,7 @@ class ConfigManager: ObservableObject { self.cleanupOnStartup = server.cleanupOnStartup self.authenticationMode = AuthenticationMode(rawValue: server.authenticationMode) ?? .osAuth } - + // Development settings if let dev = config.development { self.debugMode = dev.debugMode @@ -180,7 +180,7 @@ class ConfigManager: ObservableObject { self.devServerPath = dev.devServerPath self.logLevel = dev.logLevel } - + // Preferences if let prefs = config.preferences { self.preferredGitApp = prefs.preferredGitApp @@ -189,13 +189,13 @@ class ConfigManager: ObservableObject { self.showInDock = prefs.showInDock self.preventSleepWhenRunning = prefs.preventSleepWhenRunning } - + // Remote access if let remote = config.remoteAccess { self.ngrokEnabled = remote.ngrokEnabled self.ngrokTokenPresent = remote.ngrokTokenPresent } - + // Session defaults if let session = config.sessionDefaults { self.sessionCommand = session.command @@ -203,7 +203,7 @@ class ConfigManager: ObservableObject { self.sessionSpawnWindow = session.spawnWindow self.sessionTitleMode = TitleMode(rawValue: session.titleMode) ?? .dynamic } - + logger.info("Loaded configuration from disk") } catch { logger.error("Failed to load config: \(error.localizedDescription)") @@ -229,7 +229,7 @@ class ConfigManager: ObservableObject { quickStartCommands: quickStartCommands, repositoryBasePath: repositoryBasePath ) - + // Server configuration config.server = ServerConfig( port: serverPort, @@ -237,7 +237,7 @@ class ConfigManager: ObservableObject { cleanupOnStartup: cleanupOnStartup, authenticationMode: authenticationMode.rawValue ) - + // Development configuration config.development = DevelopmentConfig( debugMode: debugMode, @@ -245,7 +245,7 @@ class ConfigManager: ObservableObject { devServerPath: devServerPath, logLevel: logLevel ) - + // Preferences config.preferences = PreferencesConfig( preferredGitApp: preferredGitApp, @@ -254,13 +254,13 @@ class ConfigManager: ObservableObject { showInDock: showInDock, preventSleepWhenRunning: preventSleepWhenRunning ) - + // Remote access config.remoteAccess = RemoteAccessConfig( ngrokEnabled: ngrokEnabled, ngrokTokenPresent: ngrokTokenPresent ) - + // Session defaults config.sessionDefaults = SessionDefaultsConfig( command: sessionCommand, @@ -392,11 +392,11 @@ class ConfigManager: ObservableObject { updateQuickStartCommands(commands) logger.info("Reordered quick start commands") } - + /// Update repository base path func updateRepositoryBasePath(_ path: String) { guard path != repositoryBasePath else { return } - + self.repositoryBasePath = path saveConfiguration() logger.info("Updated repository base path to: \(path)") diff --git a/mac/VibeTunnel/Core/Services/ControlPayloads.swift b/mac/VibeTunnel/Core/Services/ControlPayloads.swift index c36dfe75..0d4668bc 100644 --- a/mac/VibeTunnel/Core/Services/ControlPayloads.swift +++ b/mac/VibeTunnel/Core/Services/ControlPayloads.swift @@ -2,6 +2,7 @@ import Foundation // MARK: - Terminal Control Payloads +/// Request payload for spawning a new terminal window struct TerminalSpawnRequest: Codable { let sessionId: String let workingDirectory: String? @@ -9,6 +10,7 @@ struct TerminalSpawnRequest: Codable { let terminalPreference: String? } +/// Response payload for terminal spawn operations struct TerminalSpawnResponse: Codable { let success: Bool let pid: Int? @@ -23,6 +25,7 @@ struct TerminalSpawnResponse: Codable { // MARK: - System Control Payloads +/// Event payload indicating the system is ready struct SystemReadyEvent: Codable { let timestamp: Double let version: String? @@ -33,6 +36,7 @@ struct SystemReadyEvent: Codable { } } +/// Request payload for system health check ping struct SystemPingRequest: Codable { let timestamp: Double @@ -41,6 +45,7 @@ struct SystemPingRequest: Codable { } } +/// Response payload for system health check ping struct SystemPingResponse: Codable { let status: String let timestamp: Double @@ -51,28 +56,14 @@ struct SystemPingResponse: Codable { } } -struct RepositoryPathUpdateRequest: Codable { - let path: String -} - -struct RepositoryPathUpdateResponse: Codable { - let success: Bool - let path: String? - let error: String? - - init(success: Bool, path: String? = nil, error: String? = nil) { - self.success = success - self.path = path - self.error = error - } -} - // MARK: - Git Control Payloads (placeholder for future use) +/// Request payload for Git repository status struct GitStatusRequest: Codable { let repositoryPath: String } +/// Response payload containing Git repository status information struct GitStatusResponse: Codable { let status: String let branch: String? diff --git a/mac/VibeTunnel/Core/Services/ControlProtocol.swift b/mac/VibeTunnel/Core/Services/ControlProtocol.swift index 3404057d..06ccb5e2 100644 --- a/mac/VibeTunnel/Core/Services/ControlProtocol.swift +++ b/mac/VibeTunnel/Core/Services/ControlProtocol.swift @@ -64,8 +64,6 @@ enum ControlProtocol { typealias SystemReadyMessage = ControlMessage typealias SystemPingRequestMessage = ControlMessage typealias SystemPingResponseMessage = ControlMessage - typealias RepositoryPathUpdateRequestMessage = ControlMessage - typealias RepositoryPathUpdateResponseMessage = ControlMessage // MARK: - Convenience builders for specific message types @@ -150,37 +148,6 @@ enum ControlProtocol { ) } - static func repositoryPathUpdateRequest(path: String) -> RepositoryPathUpdateRequestMessage { - ControlMessage( - type: .request, - category: .system, - action: "repository-path-update", - payload: RepositoryPathUpdateRequest(path: path) - ) - } - - static func repositoryPathUpdateResponse( - to request: RepositoryPathUpdateRequestMessage, - success: Bool, - path: String? = nil, - error: String? = nil - ) - -> RepositoryPathUpdateResponseMessage - { - ControlMessage( - id: request.id, - type: .response, - category: .system, - action: "repository-path-update", - payload: RepositoryPathUpdateResponse( - success: success, - path: path, - error: error - ), - error: error - ) - } - // MARK: - Message Serialization static func encode(_ message: ControlMessage) throws -> Data { diff --git a/mac/VibeTunnel/Core/Services/DashboardKeychain.swift b/mac/VibeTunnel/Core/Services/DashboardKeychain.swift index 62709032..28e1877c 100644 --- a/mac/VibeTunnel/Core/Services/DashboardKeychain.swift +++ b/mac/VibeTunnel/Core/Services/DashboardKeychain.swift @@ -11,9 +11,9 @@ import Security final class DashboardKeychain { static let shared = DashboardKeychain() - private let service = "sh.vibetunnel.vibetunnel" - private let account = "dashboard-password" - private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "DashboardKeychain") + private let service = KeychainConstants.vibeTunnelService + private let account = KeychainConstants.dashboardPassword + private let logger = Logger(subsystem: BundleIdentifiers.main, category: "DashboardKeychain") private init() {} diff --git a/mac/VibeTunnel/Core/Services/NgrokService.swift b/mac/VibeTunnel/Core/Services/NgrokService.swift index d23d7fbb..bdd420c3 100644 --- a/mac/VibeTunnel/Core/Services/NgrokService.swift +++ b/mac/VibeTunnel/Core/Services/NgrokService.swift @@ -16,13 +16,13 @@ enum NgrokError: LocalizedError, Equatable { var errorDescription: String? { switch self { case .notInstalled: - "ngrok is not installed. Please install it using 'brew install ngrok' or download from ngrok.com" + ErrorMessages.ngrokNotInstalled case .authTokenMissing: - "ngrok auth token is missing. Please add it in Settings" + ErrorMessages.ngrokAuthTokenMissing case .tunnelCreationFailed(let message): "Failed to create tunnel: \(message)" case .invalidConfiguration: - "Invalid ngrok configuration" + ErrorMessages.invalidNgrokConfiguration case .networkError(let message): "Network error: \(message)" } @@ -107,7 +107,7 @@ final class NgrokService: NgrokTunnelProtocol { /// Task for periodic status updates private var statusTask: Task? - private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "NgrokService") + private let logger = Logger(subsystem: BundleIdentifiers.main, category: "NgrokService") private init() {} @@ -160,14 +160,14 @@ final class NgrokService: NgrokTunnelProtocol { private func startWithCLI(port: Int) async throws -> String { // Check if ngrok is installed let checkProcess = Process() - checkProcess.executableURL = URL(fileURLWithPath: "/usr/bin/which") + checkProcess.executableURL = URL(fileURLWithPath: FilePathConstants.which) checkProcess.arguments = ["ngrok"] // Add common Homebrew paths to PATH for the check var environment = ProcessInfo.processInfo.environment - let currentPath = environment["PATH"] ?? "/usr/bin:/bin" - let homebrewPaths = "/opt/homebrew/bin:/usr/local/bin" - environment["PATH"] = "\(homebrewPaths):\(currentPath)" + let currentPath = environment[EnvironmentKeys.path] ?? "\(FilePathConstants.usrBin):\(FilePathConstants.bin)" + let homebrewPaths = "\(FilePathConstants.optHomebrewBin):\(FilePathConstants.usrLocalBin)" + environment[EnvironmentKeys.path] = "\(homebrewPaths):\(currentPath)" checkProcess.environment = environment let checkPipe = Pipe() @@ -239,7 +239,7 @@ final class NgrokService: NgrokTunnelProtocol { } } } - throw NgrokError.tunnelCreationFailed("Could not find public URL in ngrok output") + throw NgrokError.tunnelCreationFailed(ErrorMessages.ngrokPublicURLNotFound) } try process.run() diff --git a/mac/VibeTunnel/Core/Services/RepositoryPathSyncService.swift b/mac/VibeTunnel/Core/Services/RepositoryPathSyncService.swift deleted file mode 100644 index b480654c..00000000 --- a/mac/VibeTunnel/Core/Services/RepositoryPathSyncService.swift +++ /dev/null @@ -1,148 +0,0 @@ -import Combine -import Foundation -import OSLog - -/// Service that synchronizes repository base path changes to the server via Unix socket -@MainActor -final class RepositoryPathSyncService { - private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "RepositoryPathSync") - - // MARK: - Properties - - private var cancellables = Set() - private var lastSentPath: String? - private var syncEnabled = true - - // MARK: - Initialization - - init() { - logger.info("🚀 RepositoryPathSyncService initialized") - setupObserver() - setupNotifications() - } - - // MARK: - Private Methods - - private func setupObserver() { - // Monitor ConfigManager changes for repository base path - ConfigManager.shared.$repositoryBasePath - .removeDuplicates() - .dropFirst() // Skip initial value on startup - .sink { [weak self] newPath in - Task { @MainActor [weak self] in - await self?.handlePathChange(newPath) - } - } - .store(in: &cancellables) - - logger.info("✅ Repository path observer configured") - } - - private func setupNotifications() { - // Listen for notifications to disable/enable sync (for loop prevention) - NotificationCenter.default.addObserver( - self, - selector: #selector(disableSync), - name: .disablePathSync, - object: nil - ) - - NotificationCenter.default.addObserver( - self, - selector: #selector(enableSync), - name: .enablePathSync, - object: nil - ) - - logger.info("✅ Notification observers configured") - } - - @objc - private func disableSync() { - syncEnabled = false - logger.debug("🔒 Path sync temporarily disabled") - } - - @objc - private func enableSync() { - syncEnabled = true - logger.debug("🔓 Path sync re-enabled") - } - - private func handlePathChange(_ newPath: String?) async { - // Check if sync is enabled (loop prevention) - guard syncEnabled else { - logger.debug("🔒 Skipping path change - sync is temporarily disabled") - return - } - - let path = newPath ?? "~/" - - // Skip if we've already sent this path - guard path != lastSentPath else { - logger.debug("Skipping duplicate path update: \(path)") - return - } - - logger.info("📁 Repository base path changed to: \(path)") - - // Get the shared Unix socket connection - let socketManager = SharedUnixSocketManager.shared - let connection = socketManager.getConnection() - - // Ensure we're connected - guard connection.isConnected else { - logger.warning("⚠️ Unix socket not connected, cannot send path update") - return - } - - // Create the repository path update message - let message = ControlProtocol.repositoryPathUpdateRequest(path: path) - - do { - // Send the message - try await connection.send(message) - lastSentPath = path - logger.info("✅ Successfully sent repository path update to server") - } catch { - logger.error("❌ Failed to send repository path update: \(error)") - } - } - - /// Manually trigger a path sync (useful after initial connection) - func syncCurrentPath() async { - let path = ConfigManager.shared.repositoryBasePath - - logger.info("🔄 Manually syncing repository path: \(path)") - - // Get the shared Unix socket connection - let socketManager = SharedUnixSocketManager.shared - let connection = socketManager.getConnection() - - // Ensure we're connected - guard connection.isConnected else { - logger.warning("⚠️ Unix socket not connected, cannot sync path") - return - } - - // Create the repository path update message - let message = ControlProtocol.repositoryPathUpdateRequest(path: path) - - do { - // Send the message - try await connection.send(message) - lastSentPath = path - logger.info("✅ Successfully synced repository path to server") - } catch { - logger.error("❌ Failed to sync repository path: \(error)") - } - } -} - - -// MARK: - Notification Names - -extension Notification.Name { - static let disablePathSync = Notification.Name("disablePathSync") - static let enablePathSync = Notification.Name("enablePathSync") -} diff --git a/mac/VibeTunnel/Core/Services/ServerManager.swift b/mac/VibeTunnel/Core/Services/ServerManager.swift index 37fbaaf8..2de77c1b 100644 --- a/mac/VibeTunnel/Core/Services/ServerManager.swift +++ b/mac/VibeTunnel/Core/Services/ServerManager.swift @@ -55,14 +55,16 @@ class ServerManager { static let shared = ServerManager() var port: String { - get { UserDefaults.standard.string(forKey: "serverPort") ?? "4020" } - set { UserDefaults.standard.set(newValue, forKey: "serverPort") } + get { UserDefaults.standard.string(forKey: UserDefaultsKeys.serverPort) ?? String(NetworkConstants.defaultPort) + } + set { UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.serverPort) } } var bindAddress: String { get { // Get the raw value from UserDefaults, defaulting to the app default - let rawValue = UserDefaults.standard.string(forKey: "dashboardAccessMode") ?? AppConstants.Defaults + let rawValue = UserDefaults.standard.string(forKey: UserDefaultsKeys.dashboardAccessMode) ?? AppConstants + .Defaults .dashboardAccessMode let mode = DashboardAccessMode(rawValue: rawValue) ?? .network @@ -77,15 +79,15 @@ class ServerManager { set { // Find the mode that matches this bind address if let mode = DashboardAccessMode.allCases.first(where: { $0.bindAddress == newValue }) { - UserDefaults.standard.set(mode.rawValue, forKey: "dashboardAccessMode") + UserDefaults.standard.set(mode.rawValue, forKey: UserDefaultsKeys.dashboardAccessMode) logger.debug("bindAddress setter: set mode=\(mode.rawValue) for bindAddress=\(newValue)") } } } private var cleanupOnStartup: Bool { - get { UserDefaults.standard.bool(forKey: "cleanupOnStartup") } - set { UserDefaults.standard.set(newValue, forKey: "cleanupOnStartup") } + get { UserDefaults.standard.bool(forKey: UserDefaultsKeys.cleanupOnStartup) } + set { UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.cleanupOnStartup) } } private(set) var bunServer: BunServer? @@ -100,7 +102,7 @@ class ServerManager { /// Last crash time for crash rate detection private var lastCrashTime: Date? - private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "ServerManager") + private let logger = Logger(subsystem: BundleIdentifiers.main, category: "ServerManager") private let powerManager = PowerManagementService.shared private init() { @@ -174,7 +176,7 @@ class ServerManager { } // First check if port is truly available by trying to bind to it - let portNumber = Int(self.port) ?? 4_020 + let portNumber = Int(self.port) ?? NetworkConstants.defaultPort let canBind = await PortConflictResolver.shared.canBindToPort(portNumber) if !canBind { @@ -203,7 +205,7 @@ class ServerManager { logger.error("Port \(self.port) is used by external app: \(appName)") lastError = ServerManagerError.portInUseByApp( appName: appName, - port: Int(self.port) ?? 4_020, + port: Int(self.port) ?? NetworkConstants.defaultPort, alternatives: conflict.alternativePorts ) return @@ -312,7 +314,7 @@ class ServerManager { await stop() // Wait with exponential backoff for port to be available - let portNumber = Int(self.port) ?? 4_020 + let portNumber = Int(self.port) ?? NetworkConstants.defaultPort var retries = 0 let maxRetries = 5 @@ -353,7 +355,8 @@ class ServerManager { do { // Create URL for cleanup endpoint - guard let url = URL(string: "http://localhost:\(self.port)/api/cleanup-exited") else { + guard let url = URL(string: "\(URLConstants.localServerBase):\(self.port)\(APIEndpoints.cleanupExited)") + else { logger.warning("Failed to create cleanup URL") return } @@ -363,7 +366,7 @@ class ServerManager { // Add local auth token if available if let server = bunServer { - request.setValue(server.localToken, forHTTPHeaderField: "X-VibeTunnel-Local") + request.setValue(server.localToken, forHTTPHeaderField: NetworkConstants.localAuthHeader) } // Make the cleanup request @@ -455,7 +458,9 @@ class ServerManager { logger.info("Port \(self.port) is in use, checking for conflicts...") // Check for port conflicts - if let conflict = await PortConflictResolver.shared.detectConflict(on: Int(self.port) ?? 4_020) { + if let conflict = await PortConflictResolver.shared + .detectConflict(on: Int(self.port) ?? NetworkConstants.defaultPort) + { logger.warning("Found port conflict: \(conflict.process.name) (PID: \(conflict.process.pid))") // Try to resolve the conflict @@ -478,7 +483,7 @@ class ServerManager { // Port might still be in TIME_WAIT state, wait with backoff logger.info("Port may be in TIME_WAIT state, checking availability...") - let portNumber = Int(self.port) ?? 4_020 + let portNumber = Int(self.port) ?? NetworkConstants.defaultPort var retries = 0 let maxRetries = 5 @@ -563,9 +568,9 @@ class ServerManager { /// Add authentication headers to a request func authenticate(request: inout URLRequest) throws { guard let server = bunServer else { - throw ServerError.startupFailed("Server not running") + throw ServerError.startupFailed(ErrorMessages.serverNotRunning) } - request.setValue(server.localToken, forHTTPHeaderField: "X-VibeTunnel-Local") + request.setValue(server.localToken, forHTTPHeaderField: NetworkConstants.localAuthHeader) } } diff --git a/mac/VibeTunnel/Core/Services/SessionService.swift b/mac/VibeTunnel/Core/Services/SessionService.swift index c7a84a38..4115b33e 100644 --- a/mac/VibeTunnel/Core/Services/SessionService.swift +++ b/mac/VibeTunnel/Core/Services/SessionService.swift @@ -24,14 +24,16 @@ final class SessionService { throw SessionServiceError.invalidName } - guard let url = URL(string: "http://127.0.0.1:\(serverManager.port)/api/sessions/\(sessionId)") else { + guard let url = + URL(string: "\(URLConstants.localServerBase):\(serverManager.port)\(APIEndpoints.sessions)/\(sessionId)") + else { throw SessionServiceError.invalidURL } var request = URLRequest(url: url) request.httpMethod = "PATCH" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue("localhost", forHTTPHeaderField: "Host") + request.setValue(NetworkConstants.contentTypeJSON, forHTTPHeaderField: NetworkConstants.contentTypeHeader) + request.setValue(NetworkConstants.localhost, forHTTPHeaderField: NetworkConstants.hostHeader) try serverManager.authenticate(request: &request) let body = ["name": trimmedName] @@ -66,13 +68,15 @@ final class SessionService { /// - Note: The server implements graceful termination (SIGTERM → SIGKILL) /// with a 3-second timeout before force-killing processes. func terminateSession(sessionId: String) async throws { - guard let url = URL(string: "http://127.0.0.1:\(serverManager.port)/api/sessions/\(sessionId)") else { + guard let url = + URL(string: "\(URLConstants.localServerBase):\(serverManager.port)\(APIEndpoints.sessions)/\(sessionId)") + else { throw SessionServiceError.invalidURL } var request = URLRequest(url: url) request.httpMethod = "DELETE" - request.setValue("localhost", forHTTPHeaderField: "Host") + request.setValue(NetworkConstants.localhost, forHTTPHeaderField: NetworkConstants.hostHeader) try serverManager.authenticate(request: &request) let (_, response) = try await URLSession.shared.data(for: request) @@ -106,14 +110,18 @@ final class SessionService { throw SessionServiceError.serverNotRunning } - guard let url = URL(string: "http://127.0.0.1:\(serverManager.port)/api/sessions/\(sessionId)/input") else { + guard let url = + URL( + string: "\(URLConstants.localServerBase):\(serverManager.port)\(APIEndpoints.sessions)/\(sessionId)/input" + ) + else { throw SessionServiceError.invalidURL } var request = URLRequest(url: url) request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue("localhost", forHTTPHeaderField: "Host") + request.setValue(NetworkConstants.contentTypeJSON, forHTTPHeaderField: NetworkConstants.contentTypeHeader) + request.setValue(NetworkConstants.localhost, forHTTPHeaderField: NetworkConstants.hostHeader) try serverManager.authenticate(request: &request) let body = ["text": text] @@ -134,14 +142,18 @@ final class SessionService { throw SessionServiceError.serverNotRunning } - guard let url = URL(string: "http://127.0.0.1:\(serverManager.port)/api/sessions/\(sessionId)/input") else { + guard let url = + URL( + string: "\(URLConstants.localServerBase):\(serverManager.port)\(APIEndpoints.sessions)/\(sessionId)/input" + ) + else { throw SessionServiceError.invalidURL } var request = URLRequest(url: url) request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue("localhost", forHTTPHeaderField: "Host") + request.setValue(NetworkConstants.contentTypeJSON, forHTTPHeaderField: NetworkConstants.contentTypeHeader) + request.setValue(NetworkConstants.localhost, forHTTPHeaderField: NetworkConstants.hostHeader) try serverManager.authenticate(request: &request) let body = ["key": key] @@ -172,7 +184,8 @@ final class SessionService { throw SessionServiceError.serverNotRunning } - guard let url = URL(string: "http://127.0.0.1:\(serverManager.port)/api/sessions") else { + guard let url = URL(string: "\(URLConstants.localServerBase):\(serverManager.port)\(APIEndpoints.sessions)") + else { throw SessionServiceError.invalidURL } @@ -196,8 +209,8 @@ final class SessionService { var request = URLRequest(url: url) request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue("localhost", forHTTPHeaderField: "Host") + request.setValue(NetworkConstants.contentTypeJSON, forHTTPHeaderField: NetworkConstants.contentTypeHeader) + request.setValue(NetworkConstants.localhost, forHTTPHeaderField: NetworkConstants.hostHeader) try serverManager.authenticate(request: &request) request.httpBody = try JSONSerialization.data(withJSONObject: body) @@ -244,17 +257,17 @@ enum SessionServiceError: LocalizedError { var errorDescription: String? { switch self { case .invalidName: - "Session name cannot be empty" + ErrorMessages.sessionNameEmpty case .invalidURL: - "Invalid server URL" + ErrorMessages.invalidServerURL case .serverNotRunning: - "Server is not running" + ErrorMessages.serverNotRunning case .requestFailed(let statusCode): "Request failed with status code: \(statusCode)" case .createFailed(let message): message case .invalidResponse: - "Invalid server response" + ErrorMessages.invalidServerResponse } } } diff --git a/mac/VibeTunnel/Core/Services/TerminalManager.swift b/mac/VibeTunnel/Core/Services/TerminalManager.swift index caaee9a0..c046d092 100644 --- a/mac/VibeTunnel/Core/Services/TerminalManager.swift +++ b/mac/VibeTunnel/Core/Services/TerminalManager.swift @@ -34,7 +34,7 @@ actor TerminalManager { let stderrPipe = Pipe() // Configure the process - process.executableURL = URL(fileURLWithPath: request.shell ?? "/bin/zsh") + process.executableURL = URL(fileURLWithPath: request.shell ?? FilePathConstants.defaultShell) process.standardInput = stdinPipe process.standardOutput = stdoutPipe process.standardError = stderrPipe @@ -168,13 +168,13 @@ enum TunnelError: LocalizedError, Equatable { var errorDescription: String? { switch self { case .sessionNotFound: - "Session not found" + ErrorMessages.sessionNotFound case .commandExecutionFailed(let message): "Command execution failed: \(message)" case .timeout: - "Operation timed out" + ErrorMessages.operationTimeout case .invalidRequest: - "Invalid request" + ErrorMessages.invalidRequest } } } diff --git a/mac/VibeTunnel/Core/Utilities/ErrorHandling.swift b/mac/VibeTunnel/Core/Utilities/ErrorHandling.swift index ae20e00b..edca7eb3 100644 --- a/mac/VibeTunnel/Core/Utilities/ErrorHandling.swift +++ b/mac/VibeTunnel/Core/Utilities/ErrorHandling.swift @@ -18,7 +18,7 @@ struct ErrorAlertModifier: ViewModifier { isPresented: .constant(error != nil), presenting: error ) { _ in - Button("OK") { + Button(UIStrings.ok) { error = nil onDismiss?() } @@ -31,7 +31,7 @@ struct ErrorAlertModifier: ViewModifier { extension View { /// Presents an error alert when an error is present func errorAlert( - _ title: String = "Error", + _ title: String = UIStrings.error, error: Binding, onDismiss: (() -> Void)? = nil ) @@ -103,7 +103,7 @@ struct ErrorToast: View { .foregroundColor(.red) VStack(alignment: .leading, spacing: 4) { - Text("Error") + Text(UIStrings.error) .font(.headline) Text(error.localizedDescription) diff --git a/mac/VibeTunnel/Core/Utilities/GitAppHelper.swift b/mac/VibeTunnel/Core/Utilities/GitAppHelper.swift index 8ed44774..139dc2c2 100644 --- a/mac/VibeTunnel/Core/Utilities/GitAppHelper.swift +++ b/mac/VibeTunnel/Core/Utilities/GitAppHelper.swift @@ -6,19 +6,21 @@ enum GitAppHelper { static func getPreferredGitAppName() -> String { if let preferredApp = AppConstants.getPreferredGitApp(), !preferredApp.isEmpty, - let gitApp = GitApp(rawValue: preferredApp) { + let gitApp = GitApp(rawValue: preferredApp) + { return gitApp.displayName } // Return first installed git app or default return GitApp.installed.first?.displayName ?? "Git App" } - + /// Check if a specific Git app is the preferred one static func isPreferredApp(_ app: GitApp) -> Bool { guard let preferredApp = AppConstants.getPreferredGitApp(), - let gitApp = GitApp(rawValue: preferredApp) else { + let gitApp = GitApp(rawValue: preferredApp) + else { return false } return gitApp == app } -} \ No newline at end of file +} diff --git a/mac/VibeTunnel/Presentation/Components/NewSessionForm.swift b/mac/VibeTunnel/Presentation/Components/NewSessionForm.swift index d4eff28a..140ded05 100644 --- a/mac/VibeTunnel/Presentation/Components/NewSessionForm.swift +++ b/mac/VibeTunnel/Presentation/Components/NewSessionForm.swift @@ -37,7 +37,6 @@ struct NewSessionForm: View { case directory } - var body: some View { VStack(spacing: 0) { // Header with back button diff --git a/mac/VibeTunnel/Presentation/Views/SessionDetailView.swift b/mac/VibeTunnel/Presentation/Views/SessionDetailView.swift index fb7bee37..1a1def4d 100644 --- a/mac/VibeTunnel/Presentation/Views/SessionDetailView.swift +++ b/mac/VibeTunnel/Presentation/Views/SessionDetailView.swift @@ -15,6 +15,10 @@ struct SessionDetailView: View { @State private var windowSearchAttempted = false @Environment(SystemPermissionManager.self) private var permissionManager + @Environment(SessionService.self) + private var sessionService + @Environment(ServerManager.self) + private var serverManager private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "SessionDetailView") @@ -186,13 +190,32 @@ struct SessionDetailView: View { } private func openInTerminal() { - // TODO: Implement opening session in terminal - logger.info("Opening session \(session.id) in terminal") + do { + let terminalLauncher = TerminalLauncher.shared + try terminalLauncher.launchTerminalSession( + workingDirectory: session.workingDir, + command: session.command.joined(separator: " "), + sessionId: session.id + ) + logger.info("Opened session \(session.id) in terminal") + } catch { + logger.error("Failed to open session in terminal: \(error)") + // Could show an alert here if needed + } } private func terminateSession() { - // TODO: Implement session termination - logger.info("Terminating session \(session.id)") + Task { + do { + try await sessionService.terminateSession(sessionId: session.id) + logger.info("Terminated session \(session.id)") + // The view will automatically update when session is removed from monitor + // You could dismiss the window here if desired + } catch { + logger.error("Failed to terminate session: \(error)") + // Could show an alert here if needed + } + } } private func findWindow() { diff --git a/mac/VibeTunnel/Presentation/Views/Settings/SecurityPermissionsSettingsView.swift b/mac/VibeTunnel/Presentation/Views/Settings/SecurityPermissionsSettingsView.swift index 6c713d40..6390ad6f 100644 --- a/mac/VibeTunnel/Presentation/Views/Settings/SecurityPermissionsSettingsView.swift +++ b/mac/VibeTunnel/Presentation/Views/Settings/SecurityPermissionsSettingsView.swift @@ -87,7 +87,6 @@ struct SecurityPermissionsSettingsView: View { } } - // MARK: - Security Section private struct SecuritySection: View { diff --git a/mac/VibeTunnel/Utilities/CLIInstaller.swift b/mac/VibeTunnel/Utilities/CLIInstaller.swift index d9f39d66..3816aac2 100644 --- a/mac/VibeTunnel/Utilities/CLIInstaller.swift +++ b/mac/VibeTunnel/Utilities/CLIInstaller.swift @@ -27,7 +27,7 @@ import SwiftUI final class CLIInstaller { // MARK: - Properties - private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "CLIInstaller") + private let logger = Logger(subsystem: BundleIdentifiers.main, category: "CLIInstaller") private let binDirectory: String private var vtTargetPath: String { @@ -45,7 +45,7 @@ final class CLIInstaller { /// Creates a CLI installer /// - Parameters: /// - binDirectory: Directory for installation (defaults to /usr/local/bin) - init(binDirectory: String = "/usr/local/bin") { + init(binDirectory: String = FilePathConstants.usrLocalBin) { self.binDirectory = binDirectory } @@ -60,7 +60,7 @@ final class CLIInstaller { // Check both /usr/local/bin and Apple Silicon Homebrew path let pathsToCheck = [ vtTargetPath, - "/opt/homebrew/bin/vt" + "\(FilePathConstants.optHomebrewBin)/vt" ] for path in pathsToCheck where FileManager.default.fileExists(atPath: path) { @@ -452,7 +452,7 @@ final class CLIInstaller { // Check both possible installation paths let pathsToCheck = [ vtTargetPath, - "/opt/homebrew/bin/vt" + "\(FilePathConstants.optHomebrewBin)/vt" ] var installedHash: String? diff --git a/mac/VibeTunnel/Utilities/GitAppLauncher.swift b/mac/VibeTunnel/Utilities/GitAppLauncher.swift index e802affd..5fc50d95 100644 --- a/mac/VibeTunnel/Utilities/GitAppLauncher.swift +++ b/mac/VibeTunnel/Utilities/GitAppLauncher.swift @@ -20,25 +20,25 @@ enum GitApp: String, CaseIterable { var bundleIdentifier: String { switch self { case .cursor: - "com.todesktop.230313mzl4w4u92" + BundleIdentifiers.cursor case .fork: - "com.DanPristupov.Fork" + BundleIdentifiers.fork case .githubDesktop: - "com.github.GitHubClient" + BundleIdentifiers.githubDesktop case .gitup: - "co.gitup.mac" + BundleIdentifiers.gitup case .juxtaCode: - "com.naiveapps.juxtacode" + BundleIdentifiers.juxtaCode case .sourcetree: - "com.torusknot.SourceTreeNotMAS" + BundleIdentifiers.sourcetree case .sublimeMerge: - "com.sublimemerge" + BundleIdentifiers.sublimeMerge case .tower: - "com.fournova.Tower3" + BundleIdentifiers.tower case .vscode: - "com.microsoft.VSCode" + BundleIdentifiers.vscode case .windsurf: - "com.codeiumapp.windsurf" + BundleIdentifiers.windsurf } } @@ -88,7 +88,7 @@ enum GitApp: String, CaseIterable { @Observable final class GitAppLauncher { static let shared = GitAppLauncher() - private let logger = Logger(subsystem: "sh.vibetunnel.VibeTunnel", category: "GitAppLauncher") + private let logger = Logger(subsystem: BundleIdentifiers.main, category: "GitAppLauncher") private init() { performFirstRunAutoDetection() diff --git a/mac/VibeTunnel/Utilities/TerminalLauncher.swift b/mac/VibeTunnel/Utilities/TerminalLauncher.swift index c5ec84cd..f7ac89e4 100644 --- a/mac/VibeTunnel/Utilities/TerminalLauncher.swift +++ b/mac/VibeTunnel/Utilities/TerminalLauncher.swift @@ -78,21 +78,21 @@ enum Terminal: String, CaseIterable { var bundleIdentifier: String { switch self { case .terminal: - "com.apple.Terminal" + BundleIdentifiers.terminal case .iTerm2: - "com.googlecode.iterm2" + BundleIdentifiers.iTerm2 case .ghostty: - "com.mitchellh.ghostty" + BundleIdentifiers.ghostty case .warp: - "dev.warp.Warp-Stable" + BundleIdentifiers.warp case .alacritty: - "org.alacritty" + BundleIdentifiers.alacritty case .hyper: - "co.zeit.hyper" + BundleIdentifiers.hyper case .wezterm: - "com.github.wez.wezterm" + BundleIdentifiers.wezterm case .kitty: - "net.kovidgoyal.kitty" + BundleIdentifiers.kitty } } diff --git a/mac/VibeTunnel/Utilities/WelcomeWindowController.swift b/mac/VibeTunnel/Utilities/WelcomeWindowController.swift index 13b3aeeb..1d353f24 100644 --- a/mac/VibeTunnel/Utilities/WelcomeWindowController.swift +++ b/mac/VibeTunnel/Utilities/WelcomeWindowController.swift @@ -22,7 +22,7 @@ final class WelcomeWindowController: NSWindowController, NSWindowDelegate { let hostingController = NSHostingController(rootView: welcomeView) let window = NSWindow(contentViewController: hostingController) - window.title = "Welcome to VibeTunnel" + window.title = UIStrings.welcomeTitle window.styleMask = [.titled, .closable, .fullSizeContentView] window.titlebarAppearsTransparent = true window.titleVisibility = .hidden @@ -98,6 +98,4 @@ final class WelcomeWindowController: NSWindowController, NSWindowDelegate { // MARK: - Notification Extension -extension Notification.Name { - static let showWelcomeScreen = Notification.Name("showWelcomeScreen") -} +// Notification names are now in NotificationNames.swift diff --git a/mac/VibeTunnel/VibeTunnelApp.swift b/mac/VibeTunnel/VibeTunnelApp.swift index 6132b257..994cc139 100644 --- a/mac/VibeTunnel/VibeTunnelApp.swift +++ b/mac/VibeTunnel/VibeTunnelApp.swift @@ -72,6 +72,10 @@ struct VibeTunnelApp: App { .environment(terminalLauncher) .environment(gitRepositoryMonitor) .environment(repositoryDiscoveryService) + .environment(sessionService ?? SessionService( + serverManager: serverManager, + sessionMonitor: sessionMonitor + )) } else { Text("Session not found") .frame(width: 400, height: 300) @@ -126,7 +130,7 @@ struct VibeTunnelApp: App { @MainActor final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUserNotificationCenterDelegate { // Needed for menu item highlight hack - weak static var shared: AppDelegate? + static weak var shared: AppDelegate? override init() { super.init() Self.shared = self @@ -136,7 +140,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser var app: VibeTunnelApp? private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "AppDelegate") private(set) var statusBarController: StatusBarController? - private var repositoryPathSync: RepositoryPathSyncService? /// Distributed notification name used to ask an existing instance to show the Settings window. private static let showSettingsNotification = Notification.Name("sh.vibetunnel.vibetunnel.showSettings") @@ -257,15 +260,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser // Start the shared unix socket manager after all handlers are registered SharedUnixSocketManager.shared.connect() - // Initialize repository path sync service after Unix socket is connected - repositoryPathSync = RepositoryPathSyncService() - // Sync current path after initial connection - Task { [weak self] in - // Give socket time to connect - try? await Task.sleep(for: .seconds(1)) - await self?.repositoryPathSync?.syncCurrentPath() - } - // Start Git monitoring early app?.gitRepositoryMonitor.startMonitoring() diff --git a/mac/VibeTunnelTests/AppleScriptExecutorTests.swift b/mac/VibeTunnelTests/AppleScriptExecutorTests.swift index 846847fa..b2168000 100644 --- a/mac/VibeTunnelTests/AppleScriptExecutorTests.swift +++ b/mac/VibeTunnelTests/AppleScriptExecutorTests.swift @@ -2,7 +2,11 @@ import Foundation import Testing @testable import VibeTunnel -@Suite("AppleScript Executor Tests", .tags(.integration), .disabled(if: TestConditions.isRunningInCI(), "AppleScript not available in CI")) +@Suite( + "AppleScript Executor Tests", + .tags(.integration), + .disabled(if: TestConditions.isRunningInCI(), "AppleScript not available in CI") +) struct AppleScriptExecutorTests { @Test("Execute simple AppleScript") @MainActor diff --git a/mac/VibeTunnelTests/RepositoryPathSyncServiceTests.swift b/mac/VibeTunnelTests/RepositoryPathSyncServiceTests.swift deleted file mode 100644 index ce7c1f80..00000000 --- a/mac/VibeTunnelTests/RepositoryPathSyncServiceTests.swift +++ /dev/null @@ -1,278 +0,0 @@ -import Combine -import Foundation -import Testing -@testable import VibeTunnel - -@Suite("Repository Path Sync Service Tests", .serialized) -struct RepositoryPathSyncServiceTests { - /// Helper to reset repository path to default - @MainActor - private func resetRepositoryPath() { - ConfigManager.shared.updateRepositoryBasePath("~/") - } - - @MainActor - @Test("Loop prevention disables sync when notification posted") - func loopPreventionDisablesSync() async throws { - // Reset state first - resetRepositoryPath() - - // Given - _ = RepositoryPathSyncService() - - // Set initial path - let initialPath = "~/Projects" - ConfigManager.shared.updateRepositoryBasePath(initialPath) - - // Allow service to initialize - try await Task.sleep(for: .milliseconds(100)) - - // When - Post disable notification (simulating Mac receiving web update) - NotificationCenter.default.post(name: .disablePathSync, object: nil) - - // Give notification time to process - try await Task.sleep(for: .milliseconds(50)) - - // Change the path - let newPath = "~/Documents/Code" - ConfigManager.shared.updateRepositoryBasePath(newPath) - - // Allow time for potential sync - try await Task.sleep(for: .milliseconds(200)) - - // Then - Since sync is disabled, no Unix socket message should be sent - // In a real test with dependency injection, we'd verify no message was sent - // For now, we verify the service handles the notification without crashing - #expect(Bool(true)) - } - - @MainActor - @Test("Loop prevention re-enables sync after enable notification") - func loopPreventionReenablesSync() async throws { - // Clean state first - resetRepositoryPath() - - // Given - _ = RepositoryPathSyncService() - - // Disable sync first - NotificationCenter.default.post(name: .disablePathSync, object: nil) - try await Task.sleep(for: .milliseconds(50)) - - // When - Re-enable sync - NotificationCenter.default.post(name: .enablePathSync, object: nil) - try await Task.sleep(for: .milliseconds(50)) - - // Then - Future path changes should sync normally - let newPath = "~/EnabledPath" - ConfigManager.shared.updateRepositoryBasePath(newPath) - - // Allow time for sync - try await Task.sleep(for: .milliseconds(200)) - - // Service should process the change without issues - #expect(Bool(true)) - } - - @MainActor - @Test("Sync skips when disabled during path change") - func syncSkipsWhenDisabled() async throws { - // Clean state first - resetRepositoryPath() - - // Given - _ = RepositoryPathSyncService() - - // Create expectation for path change handling - // Path change handled flag removed as it was unused - - // Temporarily replace the service's internal handling - // Since we can't easily mock the private methods, we'll test the behavior - - // Disable sync - NotificationCenter.default.post(name: .disablePathSync, object: nil) - try await Task.sleep(for: .milliseconds(50)) - - // When - Change path while sync is disabled - ConfigManager.shared.updateRepositoryBasePath("~/DisabledPath") - - // Allow time for the observer to trigger - try await Task.sleep(for: .milliseconds(200)) - - // Then - The change should be processed but not synced - // In production code with proper DI, we'd verify no Unix socket message was sent - #expect(Bool(true)) - } - - @MainActor - @Test("Notification observers are properly set up") - func notificationObserversSetup() async throws { - // Given - @MainActor - final class NotificationFlags { - var disableReceived = false - var enableReceived = false - } - - let flags = NotificationFlags() - - // Set up our own observers to verify notifications work - let disableObserver = NotificationCenter.default.addObserver( - forName: .disablePathSync, - object: nil, - queue: .main - ) { _ in - Task { @MainActor in - flags.disableReceived = true - } - } - - let enableObserver = NotificationCenter.default.addObserver( - forName: .enablePathSync, - object: nil, - queue: .main - ) { _ in - Task { @MainActor in - flags.enableReceived = true - } - } - - defer { - NotificationCenter.default.removeObserver(disableObserver) - NotificationCenter.default.removeObserver(enableObserver) - } - - // Create service (which sets up its own observers) - _ = RepositoryPathSyncService() - - // When - Post notifications - NotificationCenter.default.post(name: .disablePathSync, object: nil) - NotificationCenter.default.post(name: .enablePathSync, object: nil) - - // Allow notifications to process - try await Task.sleep(for: .milliseconds(100)) - - // Then - Both notifications should be received - #expect(flags.disableReceived == true) - #expect(flags.enableReceived == true) - } - - @MainActor - @Test("Service observes repository path changes and sends updates via Unix socket") - func repositoryPathSync() async throws { - // Clean state first - resetRepositoryPath() - - // Given - Mock Unix socket connection - let mockConnection = MockUnixSocketConnection() - - // Replace the shared manager's connection with our mock - _ = SharedUnixSocketManager.shared.getConnection() - mockConnection.setConnected(true) - - // Create service - _ = RepositoryPathSyncService() - - // Store initial path - let initialPath = "~/Projects" - ConfigManager.shared.updateRepositoryBasePath(initialPath) - - // When - Change the repository path - let newPath = "~/Documents/Code" - ConfigManager.shared.updateRepositoryBasePath(newPath) - - // Allow time for the observer to trigger - try await Task.sleep(for: .milliseconds(200)) - - // Then - Since we can't easily mock the singleton's internal connection, - // we'll verify the behavior through integration testing - // The actual unit test would require dependency injection - #expect(Bool(true)) // Test passes if no crash occurs - } - - @MainActor - @Test("Service sends current path on syncCurrentPath call") - func testSyncCurrentPath() async throws { - // Clean state first - resetRepositoryPath() - - // Given - let service = RepositoryPathSyncService() - - // Set a known path - let testPath = "~/TestProjects" - ConfigManager.shared.updateRepositoryBasePath(testPath) - - // When - Call sync current path - await service.syncCurrentPath() - - // Allow time for async operation - try await Task.sleep(for: .milliseconds(100)) - - // Then - Since we can't easily mock the singleton's internal connection, - // we'll verify the behavior through integration testing - #expect(Bool(true)) // Test passes if no crash occurs - } - - @MainActor - @Test("Service handles disconnected socket gracefully") - func handleDisconnectedSocket() async throws { - // Clean state first - resetRepositoryPath() - - // Given - Service with no connection - _ = RepositoryPathSyncService() - - // When - Trigger a path update when socket is not connected - ConfigManager.shared.updateRepositoryBasePath("~/NewPath") - - // Allow time for processing - try await Task.sleep(for: .milliseconds(100)) - - // Then - Service should handle gracefully (no crash) - #expect(Bool(true)) // If we reach here, no crash occurred - } - - @MainActor - @Test("Service skips duplicate path updates") - func skipDuplicatePaths() async throws { - // Clean state first - resetRepositoryPath() - - // Given - _ = RepositoryPathSyncService() - let testPath = "~/SamePath" - - // When - Set the same path multiple times - ConfigManager.shared.updateRepositoryBasePath(testPath) - try await Task.sleep(for: .milliseconds(100)) - - ConfigManager.shared.updateRepositoryBasePath(testPath) - try await Task.sleep(for: .milliseconds(100)) - - // Then - The service should handle this gracefully - #expect(Bool(true)) // Test passes if no errors occur - } -} - -// MARK: - Mock Classes - -@MainActor -class MockUnixSocketConnection { - private var connected = false - var sentMessages: [Data] = [] - - var isConnected: Bool { - connected - } - - func setConnected(_ value: Bool) { - connected = value - } - - func send(_ message: ControlProtocol.RepositoryPathUpdateRequestMessage) async throws { - let encoder = JSONEncoder() - let data = try encoder.encode(message) - sentMessages.append(data) - } -} diff --git a/mac/VibeTunnelTests/ServerManagerTests.swift b/mac/VibeTunnelTests/ServerManagerTests.swift index 9e6df2d1..257bdcdd 100644 --- a/mac/VibeTunnelTests/ServerManagerTests.swift +++ b/mac/VibeTunnelTests/ServerManagerTests.swift @@ -41,19 +41,19 @@ final class ServerManagerTests { // The server binary must be available for tests #expect(ServerBinaryAvailableCondition.isAvailable(), "Server binary must be available for tests to run") - + // Server should either be running or have a specific error if !manager.isRunning { // If not running, we expect a specific error #expect(manager.lastError != nil, "Server failed to start but no error was reported") - + if let error = manager.lastError as? BunServerError { // Only acceptable error is binaryNotFound if the binary truly doesn't exist if error == .binaryNotFound { #expect(false, "Server binary not found - tests cannot continue") } } - + Attachment.record(""" Server failed to start Error: \(manager.lastError?.localizedDescription ?? "Unknown") diff --git a/mac/VibeTunnelTests/SystemControlHandlerTests.swift b/mac/VibeTunnelTests/SystemControlHandlerTests.swift index ed59eddb..be9034b9 100644 --- a/mac/VibeTunnelTests/SystemControlHandlerTests.swift +++ b/mac/VibeTunnelTests/SystemControlHandlerTests.swift @@ -4,7 +4,6 @@ import Testing @Suite("System Control Handler Tests", .serialized) struct SystemControlHandlerTests { - @MainActor @Test("Handles system ready event") func systemReadyEvent() async throws { diff --git a/mac/VibeTunnelTests/Utilities/TestConditions.swift b/mac/VibeTunnelTests/Utilities/TestConditions.swift index a7dbc174..d5a688c6 100644 --- a/mac/VibeTunnelTests/Utilities/TestConditions.swift +++ b/mac/VibeTunnelTests/Utilities/TestConditions.swift @@ -11,12 +11,13 @@ enum ServerBinaryAvailableCondition { // When running tests with swift test, Bundle(for:) won't find the app bundle // So tests should fail if the binary is not properly embedded let hostBundle = Bundle(for: BunServer.self) - + if let embeddedBinaryPath = hostBundle.path(forResource: "vibetunnel", ofType: nil), - FileManager.default.fileExists(atPath: embeddedBinaryPath) { + FileManager.default.fileExists(atPath: embeddedBinaryPath) + { return true } - + // The binary MUST be embedded in the app's Resources folder // If it's not there, the tests should fail return false diff --git a/web/src/client/components/session-card.ts b/web/src/client/components/session-card.ts index 6d773d58..7e90ca14 100644 --- a/web/src/client/components/session-card.ts +++ b/web/src/client/components/session-card.ts @@ -203,7 +203,7 @@ export class SessionCard extends LitElement { } // Send kill or cleanup request based on session status - const action = this.session.status === 'exited' ? 'clear' : 'terminate'; + const isExited = this.session.status === 'exited'; const result = await sessionActionService.deleteSession(this.session, { authClient: this.authClient, @@ -242,7 +242,7 @@ export class SessionCard extends LitElement { ); logger.log( - `Session ${this.session.id} ${action === 'clear' ? 'cleaned up' : 'killed'} successfully` + `Session ${this.session.id} ${isExited ? 'cleaned up' : 'killed'} successfully` ); clearTimeout(killingTimeout); }, diff --git a/web/src/client/services/session-action-service.test.ts b/web/src/client/services/session-action-service.test.ts index 1a664fed..8c1eaec0 100644 --- a/web/src/client/services/session-action-service.test.ts +++ b/web/src/client/services/session-action-service.test.ts @@ -164,7 +164,7 @@ describe('SessionActionService', () => { }); expect(result.success).toBe(true); - expect(onSuccess).toHaveBeenCalledWith('clear', 'test-session-id'); + expect(onSuccess).toHaveBeenCalledWith('delete', 'test-session-id'); expect(mockTerminateSession).toHaveBeenCalledWith( 'test-session-id', mockAuthClient, @@ -174,7 +174,7 @@ describe('SessionActionService', () => { expect.objectContaining({ type: 'session-action', detail: { - action: 'clear', + action: 'delete', sessionId: 'test-session-id', }, }) @@ -271,7 +271,7 @@ describe('SessionActionService', () => { }); expect(result.success).toBe(true); - expect(onSuccess).toHaveBeenCalledWith('clear', 'test-session-id'); + expect(onSuccess).toHaveBeenCalledWith('delete', 'test-session-id'); expect(mockTerminateSession).toHaveBeenCalledWith( 'test-session-id', mockAuthClient, @@ -332,7 +332,7 @@ describe('SessionActionService', () => { }); expect(result.success).toBe(true); - expect(onSuccess).toHaveBeenCalledWith('terminate', 'test-id'); + expect(onSuccess).toHaveBeenCalledWith('delete', 'test-id'); expect(fetch).toHaveBeenCalledWith('/api/sessions/test-id', { method: 'DELETE', headers: { Authorization: 'Bearer test-token' }, diff --git a/web/src/client/services/session-action-service.ts b/web/src/client/services/session-action-service.ts index ee76b927..567ea56c 100644 --- a/web/src/client/services/session-action-service.ts +++ b/web/src/client/services/session-action-service.ts @@ -1,7 +1,7 @@ /** * Session Action Service * - * A singleton service that manages session actions like terminate and clear, + * A singleton service that manages session actions like terminate and delete, * coordinating with the auth client and handling UI updates through callbacks. * Reusable across session-view, session-list, and session-card components. * @@ -52,10 +52,10 @@ export interface SessionActionCallbacks { /** * Called when a session action completes successfully - * @param action - The action that was performed ('terminate' or 'clear') + * @param action - The action that was performed ('terminate' or 'delete') * @param sessionId - The ID of the affected session */ - onSuccess?: (action: 'terminate' | 'clear', sessionId: string) => void; + onSuccess?: (action: 'terminate' | 'delete', sessionId: string) => void; } /** @@ -189,7 +189,7 @@ class SessionActionService { * @remarks * - Only works on sessions with status 'exited' * - Removes the session record from the server - * - Emits a 'session-action' event with action 'clear' + * - Emits a 'session-action' event with action 'delete' * - Useful for cleaning up terminated sessions from the UI * * @example @@ -224,13 +224,13 @@ class SessionActionService { options.callbacks?.onError?.(errorMessage); } else { logger.log('Session cleared successfully', { sessionId: session.id }); - options.callbacks?.onSuccess?.('clear', session.id); + options.callbacks?.onSuccess?.('delete', session.id); // Emit global event for other components to react (only in browser environment) if (typeof window !== 'undefined') { window.dispatchEvent( new CustomEvent('session-action', { detail: { - action: 'clear', + action: 'delete', sessionId: session.id, }, }) @@ -340,7 +340,7 @@ class SessionActionService { } logger.log('Session deleted successfully', { sessionId }); - options.callbacks?.onSuccess?.('terminate', sessionId); + options.callbacks?.onSuccess?.('delete', sessionId); // Emit global event (only in browser environment) if (typeof window !== 'undefined') {