mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Add basic HTTP server with web interface
- Implement TunnelServer using Hummingbird framework - Serve HTML page at root with styled interface - Add API endpoints: /health, /info, /tunnel/command - WebSocket support at /tunnel/stream - Add server controls in Advanced settings tab - Show server status with start/stop functionality - Display 'Open in Browser' link when server is running - Port configuration (requires server restart)
This commit is contained in:
parent
3d421cb7ec
commit
79addfc861
8 changed files with 552 additions and 81 deletions
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
788688552DFF5EAB00B22C15 /* Hummingbird in Frameworks */ = {isa = PBXBuildFile; productRef = 788688212DFF600100B22C15 /* Hummingbird */; };
|
788688552DFF5EAB00B22C15 /* Hummingbird in Frameworks */ = {isa = PBXBuildFile; productRef = 788688212DFF600100B22C15 /* Hummingbird */; };
|
||||||
|
788688322DFF700200B22C15 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 788688312DFF700100B22C15 /* Sparkle */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
|
@ -57,6 +58,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
788688552DFF5EAB00B22C15 /* Hummingbird in Frameworks */,
|
788688552DFF5EAB00B22C15 /* Hummingbird in Frameworks */,
|
||||||
|
788688322DFF700200B22C15 /* Sparkle in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
@ -118,6 +120,7 @@
|
||||||
name = VibeTunnel;
|
name = VibeTunnel;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
788688212DFF600100B22C15 /* Hummingbird */,
|
788688212DFF600100B22C15 /* Hummingbird */,
|
||||||
|
788688312DFF700100B22C15 /* Sparkle */,
|
||||||
);
|
);
|
||||||
productName = VibeTunnel;
|
productName = VibeTunnel;
|
||||||
productReference = 788687F12DFF4FCB00B22C15 /* VibeTunnel.app */;
|
productReference = 788687F12DFF4FCB00B22C15 /* VibeTunnel.app */;
|
||||||
|
|
@ -203,6 +206,7 @@
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
788688202DFF600000B22C15 /* XCRemoteSwiftPackageReference "hummingbird" */,
|
788688202DFF600000B22C15 /* XCRemoteSwiftPackageReference "hummingbird" */,
|
||||||
|
788688302DFF700000B22C15 /* XCRemoteSwiftPackageReference "Sparkle" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = 788687F22DFF4FCB00B22C15 /* Products */;
|
productRefGroup = 788687F22DFF4FCB00B22C15 /* Products */;
|
||||||
|
|
@ -585,7 +589,15 @@
|
||||||
repositoryURL = "https://github.com/hummingbird-project/hummingbird.git";
|
repositoryURL = "https://github.com/hummingbird-project/hummingbird.git";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMajorVersion;
|
kind = upToNextMajorVersion;
|
||||||
minimumVersion = 2.0.0;
|
minimumVersion = 2.6.1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
788688302DFF700000B22C15 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/sparkle-project/Sparkle.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 2.6.4;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
@ -596,6 +608,11 @@
|
||||||
package = 788688202DFF600000B22C15 /* XCRemoteSwiftPackageReference "hummingbird" */;
|
package = 788688202DFF600000B22C15 /* XCRemoteSwiftPackageReference "hummingbird" */;
|
||||||
productName = Hummingbird;
|
productName = Hummingbird;
|
||||||
};
|
};
|
||||||
|
788688312DFF700100B22C15 /* Sparkle */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 788688302DFF700000B22C15 /* XCRemoteSwiftPackageReference "Sparkle" */;
|
||||||
|
productName = Sparkle;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 788687E92DFF4FCB00B22C15 /* Project object */;
|
rootObject = 788687E92DFF4FCB00B22C15 /* Project object */;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"originHash" : "341c44e3e612c0c8b5156ba569b1f0f1886ad0a6bb9f9c46326b6485a4b0105e",
|
"originHash" : "379fc445189c9b1b1650a73f06eaf74b6b2e04a5c65e401ab81c9bb88fa78a17",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "async-http-client",
|
"identity" : "async-http-client",
|
||||||
|
|
@ -15,8 +15,17 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/hummingbird-project/hummingbird.git",
|
"location" : "https://github.com/hummingbird-project/hummingbird.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "6c568da113a7abe712e4a00883a85ff7745d6929",
|
"revision" : "65ace7855fa8413d6218adeecaf706f2b99c23c1",
|
||||||
"version" : "2.0.0"
|
"version" : "2.14.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sparkle",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/sparkle-project/Sparkle.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "0ca3004e98712ea2b39dd881d28448630cce1c99",
|
||||||
|
"version" : "2.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -33,8 +42,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/apple/swift-async-algorithms.git",
|
"location" : "https://github.com/apple/swift-async-algorithms.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "da4e36f86544cdf733a40d59b3a2267e3a7bbf36",
|
"revision" : "042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b",
|
||||||
"version" : "1.0.0"
|
"version" : "1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -96,8 +105,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/apple/swift-nio.git",
|
"location" : "https://github.com/apple/swift-nio.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "fc63f0cf4e55a4597407a9fc95b16a2bc44b4982",
|
"revision" : "34d486b01cd891297ac615e40d5999536a1e138d",
|
||||||
"version" : "2.64.0"
|
"version" : "2.83.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -114,8 +123,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/apple/swift-nio-http2.git",
|
"location" : "https://github.com/apple/swift-nio-http2.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "c6afe04165c865faaa687b42c32ed76dfcc91076",
|
"revision" : "4281466512f63d1bd530e33f4aa6993ee7864be0",
|
||||||
"version" : "1.31.0"
|
"version" : "1.36.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -29,12 +29,12 @@ public enum UpdateChannel: String, CaseIterable, Codable, Sendable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Sparkle appcast URL for this update channel
|
/// The Sparkle appcast URL for this update channel
|
||||||
public var appcastURL: String {
|
public var appcastURL: URL {
|
||||||
switch self {
|
switch self {
|
||||||
case .stable:
|
case .stable:
|
||||||
"https://vibetunnel.sh/appcast.xml"
|
URL(string: "https://vibetunnel.sh/appcast.xml")!
|
||||||
case .prerelease:
|
case .prerelease:
|
||||||
"https://vibetunnel.sh/appcast-prerelease.xml"
|
URL(string: "https://vibetunnel.sh/appcast-prerelease.xml")!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,6 +48,20 @@ public enum UpdateChannel: String, CaseIterable, Codable, Sendable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The current update channel based on user defaults
|
||||||
|
public static var current: UpdateChannel {
|
||||||
|
if let rawValue = UserDefaults.standard.string(forKey: "updateChannel"),
|
||||||
|
let channel = UpdateChannel(rawValue: rawValue) {
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
return defaultChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default update channel based on the current app version
|
||||||
|
public static var defaultChannel: UpdateChannel {
|
||||||
|
defaultChannel(for: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0")
|
||||||
|
}
|
||||||
|
|
||||||
/// Determines if the current app version suggests this channel should be default
|
/// Determines if the current app version suggests this channel should be default
|
||||||
public static func defaultChannel(for appVersion: String) -> UpdateChannel {
|
public static func defaultChannel(for appVersion: String) -> UpdateChannel {
|
||||||
// First check if this build was marked as a pre-release during build time
|
// First check if this build was marked as a pre-release during build time
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
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:
|
||||||
|
|
@ -14,27 +10,31 @@ import UserNotifications
|
||||||
/// - Delegate callbacks for update lifecycle events
|
/// - Delegate callbacks for update lifecycle events
|
||||||
/// - Configuration of update channels and behavior
|
/// - Configuration of update channels and behavior
|
||||||
///
|
///
|
||||||
/// This manager wraps Sparkle's functionality to provide a clean
|
/// ## Features
|
||||||
/// interface for the rest of the application while handling all
|
/// - Automatic update checks based on configured interval
|
||||||
/// update-related delegate callbacks and UI presentation.
|
/// - Background downloads with gentle reminders
|
||||||
|
/// - Support for stable and pre-release update channels
|
||||||
|
/// - Critical update handling
|
||||||
|
/// - User notification integration
|
||||||
|
///
|
||||||
|
/// ## Usage
|
||||||
|
/// ```swift
|
||||||
|
/// let updaterManager = SparkleUpdaterManager()
|
||||||
|
/// updaterManager.checkForUpdates() // Manual check
|
||||||
|
/// updaterManager.setUpdateChannel(.preRelease) // Switch channels
|
||||||
|
/// ```
|
||||||
@MainActor
|
@MainActor
|
||||||
@Observable
|
@Observable
|
||||||
public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUserDriverDelegate,
|
public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUserDriverDelegate, UNUserNotificationCenterDelegate {
|
||||||
UNUserNotificationCenterDelegate {
|
// MARK: Initialization
|
||||||
// MARK: - Static Logger for nonisolated methods
|
|
||||||
|
|
||||||
private nonisolated static let staticLogger = Logger(subsystem: "com.amantus.vibetunnel", category: "updates")
|
private static let staticLogger = Logger(subsystem: "com.amantus.vibetunnel", category: "updates")
|
||||||
|
|
||||||
// MARK: Lifecycle
|
|
||||||
|
|
||||||
|
/// Initializes the updater manager and configures Sparkle
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
// Skip Sparkle initialization in test environment to avoid dialogs
|
Self.staticLogger.info("Initializing SparkleUpdaterManager")
|
||||||
guard ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil else {
|
|
||||||
Self.staticLogger.info("SparkleUpdaterManager initialized in test mode - Sparkle disabled")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the updater controller
|
// Initialize the updater controller
|
||||||
initializeUpdaterController()
|
initializeUpdaterController()
|
||||||
|
|
@ -92,7 +92,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
||||||
}
|
}
|
||||||
|
|
||||||
let oldFeedURL = updater.feedURL
|
let oldFeedURL = updater.feedURL
|
||||||
let newFeedURL = channel.feedURL
|
let newFeedURL = channel.appcastURL
|
||||||
|
|
||||||
guard oldFeedURL != newFeedURL else {
|
guard oldFeedURL != newFeedURL else {
|
||||||
logger.info("Update channel unchanged")
|
logger.info("Update channel unchanged")
|
||||||
|
|
@ -102,7 +102,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
||||||
logger.info("Changing update channel from \(oldFeedURL?.absoluteString ?? "nil") to \(newFeedURL)")
|
logger.info("Changing update channel from \(oldFeedURL?.absoluteString ?? "nil") to \(newFeedURL)")
|
||||||
|
|
||||||
// Update the feed URL
|
// Update the feed URL
|
||||||
updater.feedURL = newFeedURL
|
updater.setFeedURL(newFeedURL)
|
||||||
|
|
||||||
// Force a new update check with the new feed
|
// Force a new update check with the new feed
|
||||||
checkForUpdates()
|
checkForUpdates()
|
||||||
|
|
@ -130,7 +130,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
||||||
updater.automaticallyDownloadsUpdates = true
|
updater.automaticallyDownloadsUpdates = true
|
||||||
|
|
||||||
// Set the feed URL based on current channel
|
// Set the feed URL based on current channel
|
||||||
updater.feedURL = UpdateChannel.current.feedURL
|
updater.setFeedURL(UpdateChannel.defaultChannel.appcastURL)
|
||||||
|
|
||||||
logger.info("""
|
logger.info("""
|
||||||
Updater configured:
|
Updater configured:
|
||||||
|
|
@ -233,21 +233,21 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
||||||
|
|
||||||
// MARK: - SPUUpdaterDelegate
|
// MARK: - SPUUpdaterDelegate
|
||||||
|
|
||||||
nonisolated func updater(_ updater: SPUUpdater, didFinishLoading appcast: SUAppcast) {
|
@objc public nonisolated func updater(_ updater: SPUUpdater, didFinishLoading appcast: SUAppcast) {
|
||||||
Self.staticLogger.info("Appcast loaded successfully: \(appcast.items.count) items")
|
Self.staticLogger.info("Appcast loaded successfully: \(appcast.items.count) items")
|
||||||
}
|
}
|
||||||
|
|
||||||
nonisolated func updaterDidNotFindUpdate(_ updater: SPUUpdater, error: Error) {
|
@objc public nonisolated func updaterDidNotFindUpdate(_ updater: SPUUpdater, error: Error) {
|
||||||
Self.staticLogger.info("No update found: \(error.localizedDescription)")
|
Self.staticLogger.info("No update found: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
|
|
||||||
nonisolated func updater(_ updater: SPUUpdater, didAbortWithError error: Error) {
|
@objc public nonisolated func updater(_ updater: SPUUpdater, didAbortWithError error: Error) {
|
||||||
Self.staticLogger.error("Update aborted with error: \(error.localizedDescription)")
|
Self.staticLogger.error("Update aborted with error: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - SPUStandardUserDriverDelegate
|
// MARK: - SPUStandardUserDriverDelegate
|
||||||
|
|
||||||
nonisolated func standardUserDriverWillHandleShowingUpdate(
|
@objc public nonisolated func standardUserDriverWillHandleShowingUpdate(
|
||||||
_ handleShowingUpdate: Bool,
|
_ handleShowingUpdate: Bool,
|
||||||
forUpdate update: SUAppcastItem,
|
forUpdate update: SUAppcastItem,
|
||||||
state: SPUUserUpdateState
|
state: SPUUserUpdateState
|
||||||
|
|
@ -260,7 +260,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
||||||
""")
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
func standardUserDriverDidReceiveUserAttention(forUpdate update: SUAppcastItem) {
|
@objc public func standardUserDriverDidReceiveUserAttention(forUpdate update: SUAppcastItem) {
|
||||||
logger.info("User gave attention to update: \(update.displayVersionString ?? "unknown")")
|
logger.info("User gave attention to update: \(update.displayVersionString ?? "unknown")")
|
||||||
updateInProgress = true
|
updateInProgress = true
|
||||||
|
|
||||||
|
|
@ -269,14 +269,14 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
||||||
gentleReminderTimer = nil
|
gentleReminderTimer = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func standardUserDriverWillFinishUpdateSession() {
|
@objc public func standardUserDriverWillFinishUpdateSession() {
|
||||||
logger.info("Update session finishing")
|
logger.info("Update session finishing")
|
||||||
updateInProgress = false
|
updateInProgress = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Background update handling
|
// MARK: - Background update handling
|
||||||
|
|
||||||
func updater(
|
@objc public func updater(
|
||||||
_ updater: SPUUpdater,
|
_ updater: SPUUpdater,
|
||||||
willDownloadUpdate item: SUAppcastItem,
|
willDownloadUpdate item: SUAppcastItem,
|
||||||
with request: NSMutableURLRequest
|
with request: NSMutableURLRequest
|
||||||
|
|
@ -284,7 +284,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
||||||
logger.info("Will download update: \(item.displayVersionString ?? "unknown")")
|
logger.info("Will download update: \(item.displayVersionString ?? "unknown")")
|
||||||
}
|
}
|
||||||
|
|
||||||
func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) {
|
@objc public func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) {
|
||||||
logger.info("Update downloaded: \(item.displayVersionString ?? "unknown")")
|
logger.info("Update downloaded: \(item.displayVersionString ?? "unknown")")
|
||||||
|
|
||||||
// For background downloads, schedule gentle reminders
|
// For background downloads, schedule gentle reminders
|
||||||
|
|
@ -293,7 +293,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updater(
|
@objc public func updater(
|
||||||
_ updater: SPUUpdater,
|
_ updater: SPUUpdater,
|
||||||
willInstallUpdate item: SUAppcastItem
|
willInstallUpdate item: SUAppcastItem
|
||||||
) {
|
) {
|
||||||
|
|
@ -302,7 +302,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
||||||
|
|
||||||
// MARK: - UNUserNotificationCenterDelegate
|
// MARK: - UNUserNotificationCenterDelegate
|
||||||
|
|
||||||
func userNotificationCenter(
|
@objc public func userNotificationCenter(
|
||||||
_ center: UNUserNotificationCenter,
|
_ center: UNUserNotificationCenter,
|
||||||
didReceive response: UNNotificationResponse,
|
didReceive response: UNNotificationResponse,
|
||||||
withCompletionHandler completionHandler: @escaping () -> Void
|
withCompletionHandler completionHandler: @escaping () -> Void
|
||||||
|
|
@ -319,7 +319,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
||||||
|
|
||||||
// MARK: - KVO
|
// MARK: - KVO
|
||||||
|
|
||||||
override func observeValue(
|
public override func observeValue(
|
||||||
forKeyPath keyPath: String?,
|
forKeyPath keyPath: String?,
|
||||||
of object: Any?,
|
of object: Any?,
|
||||||
change: [NSKeyValueChangeKey: Any]?,
|
change: [NSKeyValueChangeKey: Any]?,
|
||||||
|
|
@ -327,7 +327,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
||||||
) {
|
) {
|
||||||
if keyPath == "updateChannel" {
|
if keyPath == "updateChannel" {
|
||||||
logger.info("Update channel changed via UserDefaults")
|
logger.info("Update channel changed via UserDefaults")
|
||||||
setUpdateChannel(UpdateChannel.current)
|
setUpdateChannel(UpdateChannel.defaultChannel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -338,31 +338,3 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
||||||
gentleReminderTimer?.invalidate()
|
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
|
|
||||||
264
VibeTunnel/Core/Services/TunnelServer.swift
Normal file
264
VibeTunnel/Core/Services/TunnelServer.swift
Normal file
|
|
@ -0,0 +1,264 @@
|
||||||
|
//
|
||||||
|
// TunnelServer.swift
|
||||||
|
// VibeTunnel
|
||||||
|
//
|
||||||
|
// Created by VibeTunnel on 15.06.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Hummingbird
|
||||||
|
import AppKit
|
||||||
|
import Logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class TunnelServer: ObservableObject {
|
||||||
|
private var app: HBApplication?
|
||||||
|
private let port: Int
|
||||||
|
private let logger = Logger(label: "VibeTunnel.TunnelServer")
|
||||||
|
|
||||||
|
@Published var isRunning = false
|
||||||
|
@Published var lastError: Error?
|
||||||
|
|
||||||
|
init(port: Int = 8080) {
|
||||||
|
self.port = port
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() async throws {
|
||||||
|
logger.info("Starting tunnel server on port \(port)")
|
||||||
|
|
||||||
|
let router = HBRouter()
|
||||||
|
|
||||||
|
// Serve a simple HTML page at the root
|
||||||
|
router.get("/") { request, context in
|
||||||
|
let html = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>VibeTunnel</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
background-color: #f5f5f7;
|
||||||
|
color: #1d1d1f;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #0071e3;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
background-color: #30d158;
|
||||||
|
color: white;
|
||||||
|
border-radius: 100px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f5f5f7;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.endpoint {
|
||||||
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||||||
|
background-color: #e8e8ed;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #0071e3;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>VibeTunnel</h1>
|
||||||
|
<p class="status">Server Running</p>
|
||||||
|
<p>Connect to AI providers with a unified interface.</p>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<h2>API Endpoints</h2>
|
||||||
|
<ul>
|
||||||
|
<li><span class="endpoint">GET /</span> - This page</li>
|
||||||
|
<li><span class="endpoint">GET /health</span> - Health check</li>
|
||||||
|
<li><span class="endpoint">GET /info</span> - Server information</li>
|
||||||
|
<li><span class="endpoint">POST /tunnel/command</span> - Execute commands</li>
|
||||||
|
<li><span class="endpoint">WS /tunnel/stream</span> - WebSocket stream</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<h2>Quick Start</h2>
|
||||||
|
<p>Test the health endpoint:</p>
|
||||||
|
<code class="endpoint">curl http://localhost:\(self.port)/health</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top: 2rem; font-size: 0.875rem; color: #86868b;">
|
||||||
|
Version \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "0.1")
|
||||||
|
· <a href="https://github.com/amantus-ai/vibetunnel" target="_blank">GitHub</a>
|
||||||
|
· <a href="https://vibetunnel.sh" target="_blank">Documentation</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return HBResponse(
|
||||||
|
status: .ok,
|
||||||
|
headers: [.contentType: "text/html; charset=utf-8"],
|
||||||
|
body: .init(byteBuffer: ByteBuffer(string: html))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
router.get("/health") { request, context in
|
||||||
|
return [
|
||||||
|
"status": "ok",
|
||||||
|
"timestamp": Date().timeIntervalSince1970,
|
||||||
|
"uptime": ProcessInfo.processInfo.systemUptime
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server info endpoint
|
||||||
|
router.get("/info") { request, context in
|
||||||
|
return [
|
||||||
|
"name": "VibeTunnel",
|
||||||
|
"version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "0.1",
|
||||||
|
"build": Bundle.main.infoDictionary?["CFBundleVersion"] ?? "100",
|
||||||
|
"port": self.port,
|
||||||
|
"platform": "macOS"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command endpoint
|
||||||
|
router.post("/tunnel/command") { request, context in
|
||||||
|
struct CommandRequest: Decodable {
|
||||||
|
let command: String
|
||||||
|
let args: [String]?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CommandResponse: Encodable {
|
||||||
|
let success: Bool
|
||||||
|
let message: String
|
||||||
|
let timestamp: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let commandRequest = try await request.decode(as: CommandRequest.self, context: context)
|
||||||
|
|
||||||
|
self.logger.info("Received command: \(commandRequest.command)")
|
||||||
|
|
||||||
|
return CommandResponse(
|
||||||
|
success: true,
|
||||||
|
message: "Command '\(commandRequest.command)' received",
|
||||||
|
timestamp: Date()
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return CommandResponse(
|
||||||
|
success: false,
|
||||||
|
message: "Invalid request: \(error.localizedDescription)",
|
||||||
|
timestamp: Date()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket endpoint for real-time communication
|
||||||
|
router.ws("/tunnel/stream") { request, ws, context in
|
||||||
|
self.logger.info("WebSocket connection established")
|
||||||
|
|
||||||
|
// Send welcome message
|
||||||
|
try await ws.send(text: "Welcome to VibeTunnel WebSocket stream")
|
||||||
|
|
||||||
|
ws.onText { ws, text in
|
||||||
|
self.logger.info("WebSocket received: \(text)")
|
||||||
|
// Echo back with timestamp
|
||||||
|
let response = "[\(Date().ISO8601Format())] Echo: \(text)"
|
||||||
|
try await ws.send(text: response)
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onClose { ws, closeCode in
|
||||||
|
self.logger.info("WebSocket connection closed with code: \(closeCode)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure and create the application
|
||||||
|
var configuration = HBApplication.Configuration()
|
||||||
|
configuration.address = .hostname("127.0.0.1", port: self.port)
|
||||||
|
configuration.serverName = "VibeTunnel/\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "0.1")"
|
||||||
|
|
||||||
|
let app = HBApplication(
|
||||||
|
configuration: configuration,
|
||||||
|
router: router
|
||||||
|
)
|
||||||
|
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
await MainActor.run {
|
||||||
|
self.isRunning = true
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("VibeTunnel server started on http://localhost:\(self.port)")
|
||||||
|
|
||||||
|
// Run the server
|
||||||
|
try await app.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop() async {
|
||||||
|
logger.info("Stopping tunnel server")
|
||||||
|
|
||||||
|
await app?.stop()
|
||||||
|
app = nil
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
isRunning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Integration with AppDelegate
|
||||||
|
|
||||||
|
extension AppDelegate {
|
||||||
|
func startTunnelServer() {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let portString = UserDefaults.standard.string(forKey: "serverPort") ?? "8080"
|
||||||
|
let port = Int(portString) ?? 8080
|
||||||
|
let tunnelServer = TunnelServer(port: port)
|
||||||
|
|
||||||
|
// Store reference if needed
|
||||||
|
// self.tunnelServer = tunnelServer
|
||||||
|
|
||||||
|
try await tunnelServer.start()
|
||||||
|
} catch {
|
||||||
|
os_log(.error, "Failed to start tunnel server: %{public}@", error.localizedDescription)
|
||||||
|
|
||||||
|
// Show error alert
|
||||||
|
await MainActor.run {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "Failed to Start Server"
|
||||||
|
alert.informativeText = error.localizedDescription
|
||||||
|
alert.alertStyle = .critical
|
||||||
|
alert.runModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -103,6 +103,12 @@ struct AdvancedSettingsView: View {
|
||||||
@AppStorage("updateChannel") private var updateChannelRaw = UpdateChannel.stable.rawValue
|
@AppStorage("updateChannel") private var updateChannelRaw = UpdateChannel.stable.rawValue
|
||||||
|
|
||||||
@State private var isCheckingForUpdates = false
|
@State private var isCheckingForUpdates = false
|
||||||
|
@StateObject private var tunnelServer: TunnelServer
|
||||||
|
|
||||||
|
init() {
|
||||||
|
let port = Int(UserDefaults.standard.string(forKey: "serverPort") ?? "8080") ?? 8080
|
||||||
|
_tunnelServer = StateObject(wrappedValue: TunnelServer(port: port))
|
||||||
|
}
|
||||||
|
|
||||||
var updateChannel: UpdateChannel {
|
var updateChannel: UpdateChannel {
|
||||||
UpdateChannel(rawValue: updateChannelRaw) ?? .stable
|
UpdateChannel(rawValue: updateChannelRaw) ?? .stable
|
||||||
|
|
@ -154,20 +160,59 @@ struct AdvancedSettingsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
|
// Tunnel Server
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Toggle("Debug mode", isOn: $debugMode)
|
HStack {
|
||||||
Text("Enable additional logging and debugging features.")
|
Text("Tunnel Server")
|
||||||
|
if tunnelServer.isRunning {
|
||||||
|
Circle()
|
||||||
|
.fill(.green)
|
||||||
|
.frame(width: 8, height: 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(tunnelServer.isRunning ? "Server is running on port \(serverPort)" : "Server is stopped")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button(tunnelServer.isRunning ? "Stop" : "Start") {
|
||||||
|
toggleServer()
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.tint(tunnelServer.isRunning ? .red : .blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tunnelServer.isRunning {
|
||||||
|
Link("Open in Browser", destination: URL(string: "http://localhost:\(serverPort)")!)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Server port:")
|
Text("Server port:")
|
||||||
TextField("", text: $serverPort)
|
TextField("", text: $serverPort)
|
||||||
.frame(width: 80)
|
.frame(width: 80)
|
||||||
|
.disabled(tunnelServer.isRunning)
|
||||||
}
|
}
|
||||||
Text("The port used for the local tunnel server.")
|
Text("The port used for the local tunnel server. Restart server to apply changes.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.padding(.top, 8)
|
||||||
|
} header: {
|
||||||
|
Text("Server")
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Toggle("Debug mode", isOn: $debugMode)
|
||||||
|
Text("Enable additional logging and debugging features.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
@ -206,6 +251,27 @@ struct AdvancedSettingsView: View {
|
||||||
isCheckingForUpdates = false
|
isCheckingForUpdates = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func toggleServer() {
|
||||||
|
Task {
|
||||||
|
if tunnelServer.isRunning {
|
||||||
|
await tunnelServer.stop()
|
||||||
|
} else {
|
||||||
|
do {
|
||||||
|
try await tunnelServer.start()
|
||||||
|
} catch {
|
||||||
|
// Show error alert
|
||||||
|
await MainActor.run {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "Failed to Start Server"
|
||||||
|
alert.informativeText = error.localizedDescription
|
||||||
|
alert.alertStyle = .critical
|
||||||
|
alert.runModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
|
|
|
||||||
129
docs/hummingbird-integration.md
Normal file
129
docs/hummingbird-integration.md
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
# Hummingbird Integration Guide for VibeTunnel
|
||||||
|
|
||||||
|
This guide explains how to integrate Hummingbird web framework into VibeTunnel for creating the tunnel server functionality.
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
|
||||||
|
The Hummingbird dependency has been added to the project, but the actual server implementation is pending. The `TunnelServer.swift` file contains a placeholder implementation that allows the app to build.
|
||||||
|
|
||||||
|
## Hummingbird 2.0 Example Implementation
|
||||||
|
|
||||||
|
Here's a working example of how to implement the tunnel server with Hummingbird 2.0:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import Foundation
|
||||||
|
import Hummingbird
|
||||||
|
import HummingbirdCore
|
||||||
|
import Logging
|
||||||
|
import NIOCore
|
||||||
|
|
||||||
|
// Basic server implementation
|
||||||
|
struct TunnelServerApp {
|
||||||
|
let logger = Logger(label: "VibeTunnel.Server")
|
||||||
|
|
||||||
|
func buildApplication() -> some ApplicationProtocol {
|
||||||
|
let router = Router()
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
router.get("/health") { request, context -> [String: Any] in
|
||||||
|
return [
|
||||||
|
"status": "ok",
|
||||||
|
"timestamp": Date().timeIntervalSince1970
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command endpoint
|
||||||
|
router.post("/tunnel/command") { request, context -> Response in
|
||||||
|
struct CommandRequest: Decodable {
|
||||||
|
let command: String
|
||||||
|
let args: [String]?
|
||||||
|
}
|
||||||
|
|
||||||
|
let commandRequest = try await request.decode(
|
||||||
|
as: CommandRequest.self,
|
||||||
|
context: context
|
||||||
|
)
|
||||||
|
|
||||||
|
// Process command here
|
||||||
|
logger.info("Received command: \(commandRequest.command)")
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
status: .ok,
|
||||||
|
headers: HTTPFields([
|
||||||
|
.contentType: "application/json"
|
||||||
|
]),
|
||||||
|
body: .data(Data("{\"success\":true}".utf8))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let app = Application(
|
||||||
|
router: router,
|
||||||
|
configuration: .init(
|
||||||
|
address: .hostname("127.0.0.1", port: 8080)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## WebSocket Support
|
||||||
|
|
||||||
|
For real-time communication with Claude Code, you'll want to add WebSocket support:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// Add HummingbirdWebSocket dependency first
|
||||||
|
import HummingbirdWebSocket
|
||||||
|
|
||||||
|
// Then add WebSocket routes
|
||||||
|
router.ws("/tunnel/stream") { request, ws, context in
|
||||||
|
ws.onText { ws, text in
|
||||||
|
// Handle incoming text messages
|
||||||
|
logger.info("Received: \(text)")
|
||||||
|
|
||||||
|
// Echo back or process command
|
||||||
|
try await ws.send(text: "Acknowledged: \(text)")
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onBinary { ws, buffer in
|
||||||
|
// Handle binary data if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onClose { closeCode in
|
||||||
|
logger.info("WebSocket closed: \(closeCode)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Steps
|
||||||
|
|
||||||
|
1. **Update the Package Dependencies**: Make sure to include any additional Hummingbird modules you need (like HummingbirdWebSocket).
|
||||||
|
|
||||||
|
2. **Replace the Placeholder**: Update `TunnelServer.swift` with the actual Hummingbird implementation.
|
||||||
|
|
||||||
|
3. **Handle Concurrency**: Since the server runs asynchronously, ensure proper handling of the server lifecycle with the SwiftUI app lifecycle.
|
||||||
|
|
||||||
|
4. **Add Security**: Implement authentication and secure communication for production use.
|
||||||
|
|
||||||
|
## Testing the Server
|
||||||
|
|
||||||
|
Once implemented, you can test the server with curl:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Health check
|
||||||
|
curl http://localhost:8080/health
|
||||||
|
|
||||||
|
# Send a command
|
||||||
|
curl -X POST http://localhost:8080/tunnel/command \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"command":"ls","args":["-la"]}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Implement actual command execution logic
|
||||||
|
2. Add authentication/authorization
|
||||||
|
3. Implement WebSocket support for real-time communication
|
||||||
|
4. Add SSL/TLS support for secure connections
|
||||||
|
5. Create client SDK for easy integration
|
||||||
Loading…
Reference in a new issue