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:
Peter Steinberger 2025-06-15 22:23:54 +02:00
parent 3d421cb7ec
commit 79addfc861
8 changed files with 552 additions and 81 deletions

View file

@ -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 */;

View file

@ -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"
}
},
{

View file

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

View file

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

View 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()
}
}
}
}
}

View file

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

View 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