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
This commit is contained in:
Peter Steinberger 2025-07-08 01:05:15 +01:00
parent d9a30e299d
commit 55a852881c
5 changed files with 154 additions and 25 deletions

View file

@ -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 {

View file

@ -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)

View file

@ -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") {

View file

@ -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
}
}
}
}

View file

@ -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