mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-01 10:35:56 +00:00
refactor: extract all magic strings to centralized constants (#444)
This commit is contained in:
parent
f8a7cf9537
commit
958973c7b7
42 changed files with 759 additions and 640 deletions
32
mac/VibeTunnel/Core/Constants/APIEndpoints.swift
Normal file
32
mac/VibeTunnel/Core/Constants/APIEndpoints.swift
Normal file
|
|
@ -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"
|
||||
}
|
||||
57
mac/VibeTunnel/Core/Constants/BundleIdentifiers.swift
Normal file
57
mac/VibeTunnel/Core/Constants/BundleIdentifiers.swift
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
21
mac/VibeTunnel/Core/Constants/EnvironmentKeys.swift
Normal file
21
mac/VibeTunnel/Core/Constants/EnvironmentKeys.swift
Normal file
|
|
@ -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"
|
||||
}
|
||||
102
mac/VibeTunnel/Core/Constants/ErrorMessages.swift
Normal file
102
mac/VibeTunnel/Core/Constants/ErrorMessages.swift
Normal file
|
|
@ -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"
|
||||
}
|
||||
64
mac/VibeTunnel/Core/Constants/FilePathConstants.swift
Normal file
64
mac/VibeTunnel/Core/Constants/FilePathConstants.swift
Normal file
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
14
mac/VibeTunnel/Core/Constants/KeychainConstants.swift
Normal file
14
mac/VibeTunnel/Core/Constants/KeychainConstants.swift
Normal file
|
|
@ -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"
|
||||
}
|
||||
39
mac/VibeTunnel/Core/Constants/NetworkConstants.swift
Normal file
39
mac/VibeTunnel/Core/Constants/NetworkConstants.swift
Normal file
|
|
@ -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"
|
||||
}
|
||||
21
mac/VibeTunnel/Core/Constants/NotificationNames.swift
Normal file
21
mac/VibeTunnel/Core/Constants/NotificationNames.swift
Normal file
|
|
@ -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"
|
||||
}
|
||||
88
mac/VibeTunnel/Core/Constants/UIStrings.swift
Normal file
88
mac/VibeTunnel/Core/Constants/UIStrings.swift
Normal file
|
|
@ -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."
|
||||
}
|
||||
53
mac/VibeTunnel/Core/Constants/URLConstants.swift
Normal file
53
mac/VibeTunnel/Core/Constants/URLConstants.swift
Normal file
|
|
@ -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"
|
||||
}
|
||||
42
mac/VibeTunnel/Core/Constants/UserDefaultsKeys.swift
Normal file
42
mac/VibeTunnel/Core/Constants/UserDefaultsKeys.swift
Normal file
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -64,8 +64,6 @@ enum ControlProtocol {
|
|||
typealias SystemReadyMessage = ControlMessage<SystemReadyEvent>
|
||||
typealias SystemPingRequestMessage = ControlMessage<SystemPingRequest>
|
||||
typealias SystemPingResponseMessage = ControlMessage<SystemPingResponse>
|
||||
typealias RepositoryPathUpdateRequestMessage = ControlMessage<RepositoryPathUpdateRequest>
|
||||
typealias RepositoryPathUpdateResponseMessage = ControlMessage<RepositoryPathUpdateResponse>
|
||||
|
||||
// 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<some Codable>) throws -> Data {
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Void, Never>?
|
||||
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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<AnyCancellable>()
|
||||
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")
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Error?>,
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ struct NewSessionForm: View {
|
|||
case directory
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
// Header with back button
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ struct SecurityPermissionsSettingsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Security Section
|
||||
|
||||
private struct SecuritySection: View {
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import Testing
|
|||
|
||||
@Suite("System Control Handler Tests", .serialized)
|
||||
struct SystemControlHandlerTests {
|
||||
|
||||
@MainActor
|
||||
@Test("Handles system ready event")
|
||||
func systemReadyEvent() async throws {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
Loading…
Reference in a new issue