mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Configure for macOS 14+ and use custom menubar icon
- Update deployment target to macOS 14.0 (from 15.5) - Use custom 'menubar' icon from assets catalog instead of SF Symbol - Set icon as template for proper menu bar appearance - Update settings window frame for better sizing on macOS 14 - Reorganize menubar assets to use imageset format
This commit is contained in:
parent
9c4454b454
commit
01f3666d1f
11 changed files with 518 additions and 10 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
|
|
@ -336,7 +336,7 @@
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.5;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
|
@ -394,7 +394,7 @@
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.5;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
|
|
@ -474,7 +474,7 @@
|
||||||
CURRENT_PROJECT_VERSION = 100;
|
CURRENT_PROJECT_VERSION = 100;
|
||||||
DEVELOPMENT_TEAM = Y5PE65HELJ;
|
DEVELOPMENT_TEAM = Y5PE65HELJ;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.5;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MARKETING_VERSION = 0.1;
|
MARKETING_VERSION = 0.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.amantus.vibetunnel.Tests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.amantus.vibetunnel.Tests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
|
@ -494,7 +494,7 @@
|
||||||
CURRENT_PROJECT_VERSION = 100;
|
CURRENT_PROJECT_VERSION = 100;
|
||||||
DEVELOPMENT_TEAM = Y5PE65HELJ;
|
DEVELOPMENT_TEAM = Y5PE65HELJ;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.5;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MARKETING_VERSION = 0.1;
|
MARKETING_VERSION = 0.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.amantus.vibetunnel.Tests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.amantus.vibetunnel.Tests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
|
|
||||||
Binary file not shown.
26
VibeTunnel/Assets.xcassets/menubar.imageset/Contents.json
vendored
Normal file
26
VibeTunnel/Assets.xcassets/menubar.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "menubar.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "menubar@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "menubar@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "template"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 897 B After Width: | Height: | Size: 897 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
271
VibeTunnel/Core/Services/TunnelClient.swift
Normal file
271
VibeTunnel/Core/Services/TunnelClient.swift
Normal file
|
|
@ -0,0 +1,271 @@
|
||||||
|
//
|
||||||
|
// TunnelClient.swift
|
||||||
|
// VibeTunnel
|
||||||
|
//
|
||||||
|
// Created by VibeTunnel on 15.06.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
/// Client SDK for interacting with the VibeTunnel server
|
||||||
|
public class TunnelClient {
|
||||||
|
private let baseURL: URL
|
||||||
|
private let apiKey: String
|
||||||
|
private var session: URLSession
|
||||||
|
private let decoder = JSONDecoder()
|
||||||
|
private let encoder = JSONEncoder()
|
||||||
|
|
||||||
|
public init(baseURL: URL = URL(string: "http://localhost:8080")!, apiKey: String) {
|
||||||
|
self.baseURL = baseURL
|
||||||
|
self.apiKey = apiKey
|
||||||
|
|
||||||
|
let config = URLSessionConfiguration.default
|
||||||
|
config.httpAdditionalHeaders = ["X-API-Key": apiKey]
|
||||||
|
self.session = URLSession(configuration: config)
|
||||||
|
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
encoder.dateEncodingStrategy = .iso8601
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Health Check
|
||||||
|
|
||||||
|
public func checkHealth() async throws -> Bool {
|
||||||
|
let url = baseURL.appendingPathComponent("health")
|
||||||
|
let (_, response) = try await session.data(from: url)
|
||||||
|
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
|
throw TunnelClientError.invalidResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
return httpResponse.statusCode == 200
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Session Management
|
||||||
|
|
||||||
|
public func createSession(workingDirectory: String? = nil,
|
||||||
|
environment: [String: String]? = nil,
|
||||||
|
shell: String? = nil) async throws -> CreateSessionResponse {
|
||||||
|
let url = baseURL.appendingPathComponent("sessions")
|
||||||
|
let request = CreateSessionRequest(
|
||||||
|
workingDirectory: workingDirectory,
|
||||||
|
environment: environment,
|
||||||
|
shell: shell
|
||||||
|
)
|
||||||
|
|
||||||
|
return try await post(to: url, body: request)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func listSessions() async throws -> [SessionInfo] {
|
||||||
|
let url = baseURL.appendingPathComponent("sessions")
|
||||||
|
let response: ListSessionsResponse = try await get(from: url)
|
||||||
|
return response.sessions
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getSession(id: String) async throws -> SessionInfo {
|
||||||
|
let url = baseURL.appendingPathComponent("sessions/\(id)")
|
||||||
|
return try await get(from: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func closeSession(id: String) async throws {
|
||||||
|
let url = baseURL.appendingPathComponent("sessions/\(id)")
|
||||||
|
try await delete(from: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Command Execution
|
||||||
|
|
||||||
|
public func executeCommand(sessionId: String, command: String, args: [String]? = nil) async throws -> CommandResponse {
|
||||||
|
let url = baseURL.appendingPathComponent("execute")
|
||||||
|
let request = CommandRequest(
|
||||||
|
sessionId: sessionId,
|
||||||
|
command: command,
|
||||||
|
args: args,
|
||||||
|
environment: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
return try await post(to: url, body: request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - WebSocket Connection
|
||||||
|
|
||||||
|
public func connectWebSocket(sessionId: String? = nil) -> TunnelWebSocketClient {
|
||||||
|
var wsURL = baseURL
|
||||||
|
wsURL.scheme = wsURL.scheme == "https" ? "wss" : "ws"
|
||||||
|
wsURL = wsURL.appendingPathComponent("ws/terminal")
|
||||||
|
|
||||||
|
return TunnelWebSocketClient(url: wsURL, apiKey: apiKey, sessionId: sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Helpers
|
||||||
|
|
||||||
|
private func get<T: Decodable>(from url: URL) async throws -> T {
|
||||||
|
let (data, response) = try await session.data(from: url)
|
||||||
|
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
|
throw TunnelClientError.invalidResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
guard httpResponse.statusCode == 200 else {
|
||||||
|
throw TunnelClientError.httpError(statusCode: httpResponse.statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try decoder.decode(T.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func post<T: Encodable, R: Decodable>(to url: URL, body: T) async throws -> R {
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
request.httpBody = try encoder.encode(body)
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
|
throw TunnelClientError.invalidResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
guard (200...299).contains(httpResponse.statusCode) else {
|
||||||
|
throw TunnelClientError.httpError(statusCode: httpResponse.statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try decoder.decode(R.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func delete(from url: URL) async throws {
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "DELETE"
|
||||||
|
|
||||||
|
let (_, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
|
throw TunnelClientError.invalidResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
guard httpResponse.statusCode == 204 else {
|
||||||
|
throw TunnelClientError.httpError(statusCode: httpResponse.statusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WebSocket client for real-time terminal communication
|
||||||
|
public class TunnelWebSocketClient: NSObject {
|
||||||
|
private let url: URL
|
||||||
|
private let apiKey: String
|
||||||
|
private var sessionId: String?
|
||||||
|
private var webSocketTask: URLSessionWebSocketTask?
|
||||||
|
private let messageSubject = PassthroughSubject<WSMessage, Never>()
|
||||||
|
|
||||||
|
public var messages: AnyPublisher<WSMessage, Never> {
|
||||||
|
messageSubject.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
init(url: URL, apiKey: String, sessionId: String? = nil) {
|
||||||
|
self.url = url
|
||||||
|
self.apiKey = apiKey
|
||||||
|
self.sessionId = sessionId
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func connect() {
|
||||||
|
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
|
||||||
|
|
||||||
|
webSocketTask = session.webSocketTask(with: request)
|
||||||
|
webSocketTask?.resume()
|
||||||
|
|
||||||
|
// Send initial connection message if session ID is provided
|
||||||
|
if let sessionId = sessionId {
|
||||||
|
send(WSMessage(type: .connect, sessionId: sessionId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start receiving messages
|
||||||
|
receiveMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func send(_ message: WSMessage) {
|
||||||
|
guard let webSocketTask = webSocketTask else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let data = try JSONEncoder().encode(message)
|
||||||
|
let text = String(data: data, encoding: .utf8) ?? "{}"
|
||||||
|
let message = URLSessionWebSocketTask.Message.string(text)
|
||||||
|
|
||||||
|
webSocketTask.send(message) { error in
|
||||||
|
if let error = error {
|
||||||
|
print("WebSocket send error: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Failed to encode message: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func sendCommand(_ command: String) {
|
||||||
|
guard let sessionId = sessionId else { return }
|
||||||
|
send(WSMessage(type: .command, sessionId: sessionId, data: command))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func disconnect() {
|
||||||
|
webSocketTask?.cancel(with: .goingAway, reason: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func receiveMessage() {
|
||||||
|
webSocketTask?.receive { [weak self] result in
|
||||||
|
switch result {
|
||||||
|
case .success(let message):
|
||||||
|
switch message {
|
||||||
|
case .string(let text):
|
||||||
|
if let data = text.data(using: .utf8),
|
||||||
|
let wsMessage = try? JSONDecoder().decode(WSMessage.self, from: data) {
|
||||||
|
self?.messageSubject.send(wsMessage)
|
||||||
|
}
|
||||||
|
case .data(let data):
|
||||||
|
if let wsMessage = try? JSONDecoder().decode(WSMessage.self, from: data) {
|
||||||
|
self?.messageSubject.send(wsMessage)
|
||||||
|
}
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue receiving messages
|
||||||
|
self?.receiveMessage()
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
print("WebSocket receive error: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - URLSessionWebSocketDelegate
|
||||||
|
|
||||||
|
extension TunnelWebSocketClient: URLSessionWebSocketDelegate {
|
||||||
|
public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
|
||||||
|
print("WebSocket connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
|
||||||
|
print("WebSocket disconnected")
|
||||||
|
messageSubject.send(completion: .finished)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Errors
|
||||||
|
|
||||||
|
public enum TunnelClientError: LocalizedError {
|
||||||
|
case invalidResponse
|
||||||
|
case httpError(statusCode: Int)
|
||||||
|
case decodingError(Error)
|
||||||
|
|
||||||
|
public var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .invalidResponse:
|
||||||
|
return "Invalid response from server"
|
||||||
|
case .httpError(let statusCode):
|
||||||
|
return "HTTP error: \(statusCode)"
|
||||||
|
case .decodingError(let error):
|
||||||
|
return "Decoding error: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
151
VibeTunnel/Core/Services/TunnelServerDemo.swift
Normal file
151
VibeTunnel/Core/Services/TunnelServerDemo.swift
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
//
|
||||||
|
// TunnelServerDemo.swift
|
||||||
|
// VibeTunnel
|
||||||
|
//
|
||||||
|
// Created by VibeTunnel on 15.06.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
/// Demo code showing how to use the VibeTunnel server
|
||||||
|
class TunnelServerDemo {
|
||||||
|
|
||||||
|
static func runDemo() async {
|
||||||
|
// Get the API key (in production, this should be managed securely)
|
||||||
|
let apiKeys = AuthenticationMiddleware.loadStoredAPIKeys()
|
||||||
|
guard let apiKey = apiKeys.first else {
|
||||||
|
print("No API key found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Using API key: \(apiKey)")
|
||||||
|
|
||||||
|
// Create client
|
||||||
|
let client = TunnelClient(apiKey: apiKey)
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Check server health
|
||||||
|
let isHealthy = try await client.checkHealth()
|
||||||
|
print("Server healthy: \(isHealthy)")
|
||||||
|
|
||||||
|
// Create a new session
|
||||||
|
let session = try await client.createSession(
|
||||||
|
workingDirectory: "/tmp",
|
||||||
|
shell: "/bin/zsh"
|
||||||
|
)
|
||||||
|
print("Created session: \(session.sessionId)")
|
||||||
|
|
||||||
|
// Execute a command
|
||||||
|
let response = try await client.executeCommand(
|
||||||
|
sessionId: session.sessionId,
|
||||||
|
command: "echo 'Hello from VibeTunnel!'"
|
||||||
|
)
|
||||||
|
print("Command output: \(response.output ?? "none")")
|
||||||
|
|
||||||
|
// List all sessions
|
||||||
|
let sessions = try await client.listSessions()
|
||||||
|
print("Active sessions: \(sessions.count)")
|
||||||
|
|
||||||
|
// Close the session
|
||||||
|
try await client.closeSession(id: session.sessionId)
|
||||||
|
print("Session closed")
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
print("Demo error: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func runWebSocketDemo() async {
|
||||||
|
let apiKeys = AuthenticationMiddleware.loadStoredAPIKeys()
|
||||||
|
guard let apiKey = apiKeys.first else {
|
||||||
|
print("No API key found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = TunnelClient(apiKey: apiKey)
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Create a session first
|
||||||
|
let session = try await client.createSession()
|
||||||
|
print("Created session for WebSocket: \(session.sessionId)")
|
||||||
|
|
||||||
|
// Connect WebSocket
|
||||||
|
let wsClient = client.connectWebSocket(sessionId: session.sessionId)
|
||||||
|
wsClient.connect()
|
||||||
|
|
||||||
|
// Subscribe to messages
|
||||||
|
let cancellable = wsClient.messages.sink { message in
|
||||||
|
switch message.type {
|
||||||
|
case .output:
|
||||||
|
print("Output: \(message.data ?? "")")
|
||||||
|
case .error:
|
||||||
|
print("Error: \(message.data ?? "")")
|
||||||
|
default:
|
||||||
|
print("Message: \(message.type) - \(message.data ?? "")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send some commands
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
|
||||||
|
wsClient.sendCommand("pwd")
|
||||||
|
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
|
||||||
|
wsClient.sendCommand("ls -la")
|
||||||
|
|
||||||
|
try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
|
||||||
|
|
||||||
|
// Disconnect
|
||||||
|
wsClient.disconnect()
|
||||||
|
cancellable.cancel()
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
print("WebSocket demo error: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - cURL Examples
|
||||||
|
|
||||||
|
/*
|
||||||
|
Here are some example cURL commands to test the server:
|
||||||
|
|
||||||
|
# Set your API key
|
||||||
|
export API_KEY="your-api-key-here"
|
||||||
|
|
||||||
|
# Health check (no auth required)
|
||||||
|
curl http://localhost:8080/health
|
||||||
|
|
||||||
|
# Get server info
|
||||||
|
curl -H "X-API-Key: $API_KEY" http://localhost:8080/info
|
||||||
|
|
||||||
|
# Create a new session
|
||||||
|
curl -X POST http://localhost:8080/sessions \
|
||||||
|
-H "X-API-Key: $API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"workingDirectory": "/tmp",
|
||||||
|
"shell": "/bin/zsh"
|
||||||
|
}'
|
||||||
|
|
||||||
|
# List all sessions
|
||||||
|
curl -H "X-API-Key: $API_KEY" http://localhost:8080/sessions
|
||||||
|
|
||||||
|
# Execute a command
|
||||||
|
curl -X POST http://localhost:8080/execute \
|
||||||
|
-H "X-API-Key: $API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"sessionId": "your-session-id",
|
||||||
|
"command": "ls -la"
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Get session info
|
||||||
|
curl -H "X-API-Key: $API_KEY" http://localhost:8080/sessions/your-session-id
|
||||||
|
|
||||||
|
# Close a session
|
||||||
|
curl -X DELETE -H "X-API-Key: $API_KEY" http://localhost:8080/sessions/your-session-id
|
||||||
|
|
||||||
|
# WebSocket connection (using websocat tool)
|
||||||
|
websocat -H "X-API-Key: $API_KEY" ws://localhost:8080/ws/terminal
|
||||||
|
*/
|
||||||
|
|
@ -20,7 +20,7 @@ struct SettingsView: View {
|
||||||
Label("Advanced", systemImage: "gearshape.2")
|
Label("Advanced", systemImage: "gearshape.2")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: 600, height: 400)
|
.frame(minWidth: 600, idealWidth: 700, minHeight: 400, idealHeight: 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||||
|
|
||||||
if let button = statusItem?.button {
|
if let button = statusItem?.button {
|
||||||
button.image = NSImage(systemSymbolName: "network.badge.shield.half.filled", accessibilityDescription: "VibeTunnel")
|
button.image = NSImage(named: "menubar")
|
||||||
|
button.image?.isTemplate = true
|
||||||
button.action = #selector(statusItemClicked)
|
button.action = #selector(statusItemClicked)
|
||||||
button.target = self
|
button.target = self
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,73 @@
|
||||||
# Hummingbird Integration Guide for VibeTunnel
|
# Hummingbird Integration Guide for VibeTunnel
|
||||||
|
|
||||||
This guide explains how to integrate Hummingbird web framework into VibeTunnel for creating the tunnel server functionality.
|
This guide explains the Hummingbird web framework integration in VibeTunnel for creating the tunnel server functionality.
|
||||||
|
|
||||||
## Current Status
|
## 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.
|
✅ **IMPLEMENTED** - The VibeTunnel server is now fully implemented with:
|
||||||
|
- HTTP REST API endpoints for terminal session management
|
||||||
|
- WebSocket support for real-time terminal communication
|
||||||
|
- Authentication via API keys
|
||||||
|
- Session management with automatic cleanup
|
||||||
|
- Client SDK for easy integration
|
||||||
|
- Comprehensive error handling
|
||||||
|
|
||||||
## Hummingbird 2.0 Example Implementation
|
## Architecture Overview
|
||||||
|
|
||||||
Here's a working example of how to implement the tunnel server with Hummingbird 2.0:
|
The VibeTunnel server is built with the following components:
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
1. **TunnelServer** (`/VibeTunnel/Core/Services/TunnelServer.swift`)
|
||||||
|
- Main server implementation using Hummingbird
|
||||||
|
- Manages HTTP endpoints and WebSocket connections
|
||||||
|
- Handles server lifecycle and configuration
|
||||||
|
|
||||||
|
2. **TerminalManager** (`/VibeTunnel/Core/Services/TerminalManager.swift`)
|
||||||
|
- Actor-based terminal session management
|
||||||
|
- Handles process creation and command execution
|
||||||
|
- Manages pipes for stdin/stdout/stderr communication
|
||||||
|
- Automatic cleanup of inactive sessions
|
||||||
|
|
||||||
|
3. **WebSocketHandler** (`/VibeTunnel/Core/Services/WebSocketHandler.swift`)
|
||||||
|
- Real-time bidirectional communication
|
||||||
|
- JSON-based message protocol
|
||||||
|
- Session-based terminal streaming
|
||||||
|
|
||||||
|
4. **AuthenticationMiddleware** (`/VibeTunnel/Core/Services/AuthenticationMiddleware.swift`)
|
||||||
|
- API key-based authentication
|
||||||
|
- Secure key generation and storage
|
||||||
|
- Protects all endpoints except health check
|
||||||
|
|
||||||
|
5. **TunnelClient** (`/VibeTunnel/Core/Services/TunnelClient.swift`)
|
||||||
|
- Swift SDK for server interaction
|
||||||
|
- Async/await based API
|
||||||
|
- WebSocket client for real-time communication
|
||||||
|
|
||||||
|
### Data Models
|
||||||
|
|
||||||
|
- **TunnelSession** - Represents a terminal session
|
||||||
|
- **CreateSessionRequest/Response** - Session creation
|
||||||
|
- **CommandRequest/Response** - Command execution
|
||||||
|
- **WSMessage** - WebSocket message format
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
- `GET /health` - Health check (no auth required)
|
||||||
|
- `GET /info` - Server information
|
||||||
|
- `GET /sessions` - List all active sessions
|
||||||
|
- `POST /sessions` - Create new terminal session
|
||||||
|
- `GET /sessions/:id` - Get session details
|
||||||
|
- `DELETE /sessions/:id` - Close a session
|
||||||
|
- `POST /execute` - Execute command in session
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
|
||||||
|
- `WS /ws/terminal` - Real-time terminal communication
|
||||||
|
|
||||||
|
## Example Implementation
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue