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 */
|
||||
788688552DFF5EAB00B22C15 /* Hummingbird in Frameworks */ = {isa = PBXBuildFile; productRef = 788688212DFF600100B22C15 /* Hummingbird */; };
|
||||
788688322DFF700200B22C15 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 788688312DFF700100B22C15 /* Sparkle */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
|
@ -57,6 +58,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
788688552DFF5EAB00B22C15 /* Hummingbird in Frameworks */,
|
||||
788688322DFF700200B22C15 /* Sparkle in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -118,6 +120,7 @@
|
|||
name = VibeTunnel;
|
||||
packageProductDependencies = (
|
||||
788688212DFF600100B22C15 /* Hummingbird */,
|
||||
788688312DFF700100B22C15 /* Sparkle */,
|
||||
);
|
||||
productName = VibeTunnel;
|
||||
productReference = 788687F12DFF4FCB00B22C15 /* VibeTunnel.app */;
|
||||
|
|
@ -203,6 +206,7 @@
|
|||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
788688202DFF600000B22C15 /* XCRemoteSwiftPackageReference "hummingbird" */,
|
||||
788688302DFF700000B22C15 /* XCRemoteSwiftPackageReference "Sparkle" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 788687F22DFF4FCB00B22C15 /* Products */;
|
||||
|
|
@ -585,7 +589,15 @@
|
|||
repositoryURL = "https://github.com/hummingbird-project/hummingbird.git";
|
||||
requirement = {
|
||||
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 */
|
||||
|
|
@ -596,6 +608,11 @@
|
|||
package = 788688202DFF600000B22C15 /* XCRemoteSwiftPackageReference "hummingbird" */;
|
||||
productName = Hummingbird;
|
||||
};
|
||||
788688312DFF700100B22C15 /* Sparkle */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 788688302DFF700000B22C15 /* XCRemoteSwiftPackageReference "Sparkle" */;
|
||||
productName = Sparkle;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 788687E92DFF4FCB00B22C15 /* Project object */;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"originHash" : "341c44e3e612c0c8b5156ba569b1f0f1886ad0a6bb9f9c46326b6485a4b0105e",
|
||||
"originHash" : "379fc445189c9b1b1650a73f06eaf74b6b2e04a5c65e401ab81c9bb88fa78a17",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "async-http-client",
|
||||
|
|
@ -15,8 +15,17 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/hummingbird-project/hummingbird.git",
|
||||
"state" : {
|
||||
"revision" : "6c568da113a7abe712e4a00883a85ff7745d6929",
|
||||
"version" : "2.0.0"
|
||||
"revision" : "65ace7855fa8413d6218adeecaf706f2b99c23c1",
|
||||
"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",
|
||||
"location" : "https://github.com/apple/swift-async-algorithms.git",
|
||||
"state" : {
|
||||
"revision" : "da4e36f86544cdf733a40d59b3a2267e3a7bbf36",
|
||||
"version" : "1.0.0"
|
||||
"revision" : "042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b",
|
||||
"version" : "1.0.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -96,8 +105,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-nio.git",
|
||||
"state" : {
|
||||
"revision" : "fc63f0cf4e55a4597407a9fc95b16a2bc44b4982",
|
||||
"version" : "2.64.0"
|
||||
"revision" : "34d486b01cd891297ac615e40d5999536a1e138d",
|
||||
"version" : "2.83.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -114,8 +123,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-nio-http2.git",
|
||||
"state" : {
|
||||
"revision" : "c6afe04165c865faaa687b42c32ed76dfcc91076",
|
||||
"version" : "1.31.0"
|
||||
"revision" : "4281466512f63d1bd530e33f4aa6993ee7864be0",
|
||||
"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
|
||||
public var appcastURL: String {
|
||||
public var appcastURL: URL {
|
||||
switch self {
|
||||
case .stable:
|
||||
"https://vibetunnel.sh/appcast.xml"
|
||||
URL(string: "https://vibetunnel.sh/appcast.xml")!
|
||||
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
|
||||
public static func defaultChannel(for appVersion: String) -> UpdateChannel {
|
||||
// First check if this build was marked as a pre-release during build time
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
import os
|
||||
#if canImport(Sparkle)
|
||||
import Sparkle
|
||||
#endif
|
||||
import UserNotifications
|
||||
|
||||
#if canImport(Sparkle)
|
||||
|
||||
/// Manages the Sparkle auto-update framework integration for VibeTunnel.
|
||||
///
|
||||
/// SparkleUpdaterManager provides:
|
||||
|
|
@ -14,27 +10,31 @@ import UserNotifications
|
|||
/// - Delegate callbacks for update lifecycle events
|
||||
/// - Configuration of update channels and behavior
|
||||
///
|
||||
/// This manager wraps Sparkle's functionality to provide a clean
|
||||
/// interface for the rest of the application while handling all
|
||||
/// update-related delegate callbacks and UI presentation.
|
||||
/// ## Features
|
||||
/// - Automatic update checks based on configured interval
|
||||
/// - 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
|
||||
@Observable
|
||||
public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUserDriverDelegate,
|
||||
UNUserNotificationCenterDelegate {
|
||||
// MARK: - Static Logger for nonisolated methods
|
||||
public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUserDriverDelegate, UNUserNotificationCenterDelegate {
|
||||
// MARK: Initialization
|
||||
|
||||
private nonisolated static let staticLogger = Logger(subsystem: "com.amantus.vibetunnel", category: "updates")
|
||||
|
||||
// MARK: Lifecycle
|
||||
private static let staticLogger = Logger(subsystem: "com.amantus.vibetunnel", category: "updates")
|
||||
|
||||
/// Initializes the updater manager and configures Sparkle
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
// Skip Sparkle initialization in test environment to avoid dialogs
|
||||
guard ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil else {
|
||||
Self.staticLogger.info("SparkleUpdaterManager initialized in test mode - Sparkle disabled")
|
||||
return
|
||||
}
|
||||
Self.staticLogger.info("Initializing SparkleUpdaterManager")
|
||||
|
||||
// Initialize the updater controller
|
||||
initializeUpdaterController()
|
||||
|
|
@ -92,7 +92,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
|||
}
|
||||
|
||||
let oldFeedURL = updater.feedURL
|
||||
let newFeedURL = channel.feedURL
|
||||
let newFeedURL = channel.appcastURL
|
||||
|
||||
guard oldFeedURL != newFeedURL else {
|
||||
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)")
|
||||
|
||||
// Update the feed URL
|
||||
updater.feedURL = newFeedURL
|
||||
updater.setFeedURL(newFeedURL)
|
||||
|
||||
// Force a new update check with the new feed
|
||||
checkForUpdates()
|
||||
|
|
@ -130,7 +130,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
|||
updater.automaticallyDownloadsUpdates = true
|
||||
|
||||
// Set the feed URL based on current channel
|
||||
updater.feedURL = UpdateChannel.current.feedURL
|
||||
updater.setFeedURL(UpdateChannel.defaultChannel.appcastURL)
|
||||
|
||||
logger.info("""
|
||||
Updater configured:
|
||||
|
|
@ -233,21 +233,21 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
|||
|
||||
// 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")
|
||||
}
|
||||
|
||||
nonisolated func updaterDidNotFindUpdate(_ updater: SPUUpdater, error: Error) {
|
||||
@objc public nonisolated func updaterDidNotFindUpdate(_ updater: SPUUpdater, error: Error) {
|
||||
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)")
|
||||
}
|
||||
|
||||
// MARK: - SPUStandardUserDriverDelegate
|
||||
|
||||
nonisolated func standardUserDriverWillHandleShowingUpdate(
|
||||
@objc public nonisolated func standardUserDriverWillHandleShowingUpdate(
|
||||
_ handleShowingUpdate: Bool,
|
||||
forUpdate update: SUAppcastItem,
|
||||
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")")
|
||||
updateInProgress = true
|
||||
|
||||
|
|
@ -269,14 +269,14 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
|||
gentleReminderTimer = nil
|
||||
}
|
||||
|
||||
func standardUserDriverWillFinishUpdateSession() {
|
||||
@objc public func standardUserDriverWillFinishUpdateSession() {
|
||||
logger.info("Update session finishing")
|
||||
updateInProgress = false
|
||||
}
|
||||
|
||||
// MARK: - Background update handling
|
||||
|
||||
func updater(
|
||||
@objc public func updater(
|
||||
_ updater: SPUUpdater,
|
||||
willDownloadUpdate item: SUAppcastItem,
|
||||
with request: NSMutableURLRequest
|
||||
|
|
@ -284,7 +284,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
|||
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")")
|
||||
|
||||
// For background downloads, schedule gentle reminders
|
||||
|
|
@ -293,7 +293,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
|||
}
|
||||
}
|
||||
|
||||
func updater(
|
||||
@objc public func updater(
|
||||
_ updater: SPUUpdater,
|
||||
willInstallUpdate item: SUAppcastItem
|
||||
) {
|
||||
|
|
@ -302,7 +302,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
|||
|
||||
// MARK: - UNUserNotificationCenterDelegate
|
||||
|
||||
func userNotificationCenter(
|
||||
@objc public func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
didReceive response: UNNotificationResponse,
|
||||
withCompletionHandler completionHandler: @escaping () -> Void
|
||||
|
|
@ -319,7 +319,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
|||
|
||||
// MARK: - KVO
|
||||
|
||||
override func observeValue(
|
||||
public override func observeValue(
|
||||
forKeyPath keyPath: String?,
|
||||
of object: Any?,
|
||||
change: [NSKeyValueChangeKey: Any]?,
|
||||
|
|
@ -327,7 +327,7 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
|||
) {
|
||||
if keyPath == "updateChannel" {
|
||||
logger.info("Update channel changed via UserDefaults")
|
||||
setUpdateChannel(UpdateChannel.current)
|
||||
setUpdateChannel(UpdateChannel.defaultChannel)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -337,32 +337,4 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
|||
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
|
||||
}
|
||||
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
|
||||
|
||||
@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 {
|
||||
UpdateChannel(rawValue: updateChannelRaw) ?? .stable
|
||||
|
|
@ -154,11 +160,36 @@ struct AdvancedSettingsView: View {
|
|||
}
|
||||
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Toggle("Debug mode", isOn: $debugMode)
|
||||
Text("Enable additional logging and debugging features.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
// Tunnel Server
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
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)
|
||||
.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) {
|
||||
|
|
@ -166,8 +197,22 @@ struct AdvancedSettingsView: View {
|
|||
Text("Server port:")
|
||||
TextField("", text: $serverPort)
|
||||
.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)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
|
@ -206,6 +251,27 @@ struct AdvancedSettingsView: View {
|
|||
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 {
|
||||
|
|
|
|||
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