From 55a852881c14b3a49f562ca44027cb0c8f5173d9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 8 Jul 2025 01:05:15 +0100 Subject: [PATCH] Implement hash-based vt script version detection - Add SHA256 hash comparison for vt script updates - Check vt script version on app startup - Show welcome dialog when vt script is outdated - Add update prompts in both Welcome and Settings views - Ensures users always have the latest vt features (like 'vt title') Fixes #245 --- .../Views/Settings/AdvancedSettingsView.swift | 45 ++++++++----- .../Views/Settings/GeneralSettingsView.swift | 24 +++++++ .../Views/Welcome/VTCommandPageView.swift | 27 ++++++-- mac/VibeTunnel/Utilities/CLIInstaller.swift | 65 +++++++++++++++++++ mac/VibeTunnel/VibeTunnelApp.swift | 18 +++-- 5 files changed, 154 insertions(+), 25 deletions(-) diff --git a/mac/VibeTunnel/Presentation/Views/Settings/AdvancedSettingsView.swift b/mac/VibeTunnel/Presentation/Views/Settings/AdvancedSettingsView.swift index 121d8d1c..8b7416fc 100644 --- a/mac/VibeTunnel/Presentation/Views/Settings/AdvancedSettingsView.swift +++ b/mac/VibeTunnel/Presentation/Views/Settings/AdvancedSettingsView.swift @@ -41,22 +41,37 @@ struct AdvancedSettingsView: View { // Actual content if cliInstaller.isInstalled { HStack(spacing: 8) { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.green) - Text("VT installed") - .foregroundColor(.secondary) + if cliInstaller.isOutdated { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.orange) + Text("VT update available") + .foregroundColor(.secondary) + + Button("Update") { + Task { + await cliInstaller.install() + } + } + .buttonStyle(.bordered) + .disabled(cliInstaller.isInstalling) + } else { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + Text("VT installed") + .foregroundColor(.secondary) - // Show reinstall button in debug mode - if debugMode { - Button(action: { - cliInstaller.installCLITool() - }, label: { - Image(systemName: "arrow.clockwise.circle") - .font(.system(size: 14)) - }) - .buttonStyle(.plain) - .foregroundColor(.accentColor) - .help("Reinstall CLI tool") + // Show reinstall button in debug mode + if debugMode { + Button(action: { + cliInstaller.installCLITool() + }, label: { + Image(systemName: "arrow.clockwise.circle") + .font(.system(size: 14)) + }) + .buttonStyle(.plain) + .foregroundColor(.accentColor) + .help("Reinstall CLI tool") + } } } } else { diff --git a/mac/VibeTunnel/Presentation/Views/Settings/GeneralSettingsView.swift b/mac/VibeTunnel/Presentation/Views/Settings/GeneralSettingsView.swift index e890f582..416f5ad7 100644 --- a/mac/VibeTunnel/Presentation/Views/Settings/GeneralSettingsView.swift +++ b/mac/VibeTunnel/Presentation/Views/Settings/GeneralSettingsView.swift @@ -204,6 +204,14 @@ private struct PermissionsSection: View { .padding(.horizontal, 10) .padding(.vertical, 2) .frame(height: 22) // Match small button height + .contextMenu { + Button("Refresh Status") { + permissionManager.forcePermissionRecheck() + } + Button("Open System Settings...") { + permissionManager.requestPermission(.appleScript) + } + } } else { Button("Grant Permission") { permissionManager.requestPermission(.appleScript) @@ -236,6 +244,14 @@ private struct PermissionsSection: View { .padding(.horizontal, 10) .padding(.vertical, 2) .frame(height: 22) // Match small button height + .contextMenu { + Button("Refresh Status") { + permissionManager.forcePermissionRecheck() + } + Button("Open System Settings...") { + permissionManager.requestPermission(.accessibility) + } + } } else { Button("Grant Permission") { permissionManager.requestPermission(.accessibility) @@ -268,6 +284,14 @@ private struct PermissionsSection: View { .padding(.horizontal, 10) .padding(.vertical, 2) .frame(height: 22) // Match small button height + .contextMenu { + Button("Refresh Status") { + permissionManager.forcePermissionRecheck() + } + Button("Open System Settings...") { + permissionManager.requestPermission(.screenRecording) + } + } } else { Button("Grant Permission") { permissionManager.requestPermission(.screenRecording) diff --git a/mac/VibeTunnel/Presentation/Views/Welcome/VTCommandPageView.swift b/mac/VibeTunnel/Presentation/Views/Welcome/VTCommandPageView.swift index 18b1baac..4ca690d6 100644 --- a/mac/VibeTunnel/Presentation/Views/Welcome/VTCommandPageView.swift +++ b/mac/VibeTunnel/Presentation/Views/Welcome/VTCommandPageView.swift @@ -53,11 +53,28 @@ struct VTCommandPageView: View { // Install VT Binary button VStack(spacing: 12) { if cliInstaller.isInstalled { - HStack { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.green) - Text("CLI tool is installed") - .foregroundColor(.secondary) + if cliInstaller.isOutdated { + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.orange) + Text("CLI tool is outdated") + .foregroundColor(.secondary) + } + + Button("Update VT Command Line Tool") { + Task { + await cliInstaller.install() + } + } + .buttonStyle(.borderedProminent) + .disabled(cliInstaller.isInstalling) + } else { + HStack { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + Text("CLI tool is installed") + .foregroundColor(.secondary) + } } } else { Button("Install VT Command Line Tool") { diff --git a/mac/VibeTunnel/Utilities/CLIInstaller.swift b/mac/VibeTunnel/Utilities/CLIInstaller.swift index b18882a7..19031fba 100644 --- a/mac/VibeTunnel/Utilities/CLIInstaller.swift +++ b/mac/VibeTunnel/Utilities/CLIInstaller.swift @@ -1,4 +1,5 @@ import AppKit +import CryptoKit import Foundation import Observation import os.log @@ -36,6 +37,7 @@ final class CLIInstaller { var isInstalled = false var isInstalling = false var lastError: String? + var isOutdated = false // MARK: - Initialization @@ -79,6 +81,13 @@ final class CLIInstaller { isInstalled = isCorrectlyInstalled logger.info("CLIInstaller: vt script installed: \(self.isInstalled)") + + // If installed, check if it's outdated + if isInstalled { + checkScriptVersion() + } else { + isOutdated = false + } } } @@ -256,4 +265,60 @@ final class CLIInstaller { alert.alertStyle = .critical alert.runModal() } + + // MARK: - Script Version Detection + + /// Calculates SHA256 hash of a file + private func calculateHash(for filePath: String) -> String? { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { + return nil + } + + let hash = SHA256.hash(data: data) + return hash.compactMap { String(format: "%02x", $0) }.joined() + } + + /// Gets the hash of the bundled vt script + private func getBundledScriptHash() -> String? { + guard let scriptPath = Bundle.main.path(forResource: "vt", ofType: nil) else { + logger.error("CLIInstaller: Bundled vt script not found") + return nil + } + + return calculateHash(for: scriptPath) + } + + /// Checks if the installed script is outdated compared to bundled version + func checkScriptVersion() { + Task { @MainActor in + guard let bundledHash = getBundledScriptHash() else { + logger.error("CLIInstaller: Failed to get bundled script hash") + return + } + + // Check both possible installation paths + let pathsToCheck = [ + vtTargetPath, + "/opt/homebrew/bin/vt" + ] + + var installedHash: String? + for path in pathsToCheck where FileManager.default.fileExists(atPath: path) { + if let hash = calculateHash(for: path) { + installedHash = hash + break + } + } + + // Update outdated status + if let installedHash = installedHash { + self.isOutdated = (installedHash != bundledHash) + logger.info("CLIInstaller: Script version check - outdated: \(self.isOutdated)") + logger.debug("CLIInstaller: Bundled hash: \(bundledHash), Installed hash: \(installedHash)") + } else { + // Script not installed + self.isOutdated = false + } + } + } } diff --git a/mac/VibeTunnel/VibeTunnelApp.swift b/mac/VibeTunnel/VibeTunnelApp.swift index 6104343e..59d49a1c 100644 --- a/mac/VibeTunnel/VibeTunnelApp.swift +++ b/mac/VibeTunnel/VibeTunnelApp.swift @@ -194,12 +194,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser // Initialize dock icon visibility through DockIconManager DockIconManager.shared.updateDockVisibility() - // Show welcome screen when version changes + // Check CLI installation status + let cliInstaller = CLIInstaller() + cliInstaller.checkInstallationStatus() + + // Show welcome screen when version changes OR when vt script is outdated let storedWelcomeVersion = UserDefaults.standard.integer(forKey: AppConstants.UserDefaultsKeys.welcomeVersion) - - // Show welcome if version is different from current - if storedWelcomeVersion < AppConstants.currentWelcomeVersion && !isRunningInTests && !isRunningInPreview { - showWelcomeScreen() + + // Small delay to allow CLI check to complete + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + // Show welcome if version is different from current OR if vt script is outdated + if (storedWelcomeVersion < AppConstants.currentWelcomeVersion || cliInstaller.isOutdated) + && !isRunningInTests && !isRunningInPreview { + self?.showWelcomeScreen() + } } // Skip all service initialization during tests