mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-11 12:15:53 +00:00
Refactor tunnel client architecture and improve server communication
This commit is contained in:
parent
8bcc669ccc
commit
8a83d7c2c9
13 changed files with 519 additions and 154 deletions
|
|
@ -31,6 +31,7 @@ let package = Package(
|
|||
sources: [
|
||||
"Core/Models/TunnelSession.swift",
|
||||
"Core/Models/UpdateChannel.swift",
|
||||
"Core/Services/TunnelClient.swift",
|
||||
"Core/Services/TunnelClient2.swift",
|
||||
"Core/Services/TerminalManager.swift",
|
||||
"Core/Services/HTTPClientProtocol.swift"
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 572 B After Width: | Height: | Size: 776 B |
|
|
@ -106,3 +106,118 @@ public struct ListSessionsResponse: Codable {
|
|||
self.sessions = sessions
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Extensions for TunnelClient2
|
||||
|
||||
extension TunnelSession {
|
||||
/// Client information for session creation
|
||||
public struct ClientInfo: Codable, Sendable {
|
||||
public let hostname: String
|
||||
public let username: String
|
||||
public let homeDirectory: String
|
||||
public let operatingSystem: String
|
||||
public let architecture: String
|
||||
|
||||
public init(
|
||||
hostname: String,
|
||||
username: String,
|
||||
homeDirectory: String,
|
||||
operatingSystem: String,
|
||||
architecture: String
|
||||
) {
|
||||
self.hostname = hostname
|
||||
self.username = username
|
||||
self.homeDirectory = homeDirectory
|
||||
self.operatingSystem = operatingSystem
|
||||
self.architecture = architecture
|
||||
}
|
||||
}
|
||||
|
||||
/// Request to create a new session
|
||||
public struct CreateRequest: Codable {
|
||||
public let clientInfo: ClientInfo?
|
||||
|
||||
public init(clientInfo: ClientInfo? = nil) {
|
||||
self.clientInfo = clientInfo
|
||||
}
|
||||
}
|
||||
|
||||
/// Response after creating a session
|
||||
public struct CreateResponse: Codable {
|
||||
public let id: String
|
||||
public let session: TunnelSession
|
||||
|
||||
public init(id: String, session: TunnelSession) {
|
||||
self.id = id
|
||||
self.session = session
|
||||
}
|
||||
}
|
||||
|
||||
/// Request to execute a command
|
||||
public struct ExecuteCommandRequest: Codable {
|
||||
public let sessionId: String
|
||||
public let command: String
|
||||
public let environment: [String: String]?
|
||||
public let workingDirectory: String?
|
||||
|
||||
public init(
|
||||
sessionId: String,
|
||||
command: String,
|
||||
environment: [String: String]? = nil,
|
||||
workingDirectory: String? = nil
|
||||
) {
|
||||
self.sessionId = sessionId
|
||||
self.command = command
|
||||
self.environment = environment
|
||||
self.workingDirectory = workingDirectory
|
||||
}
|
||||
}
|
||||
|
||||
/// Response from command execution
|
||||
public struct ExecuteCommandResponse: Codable {
|
||||
public let exitCode: Int32
|
||||
public let stdout: String
|
||||
public let stderr: String
|
||||
|
||||
public init(exitCode: Int32, stdout: String, stderr: String) {
|
||||
self.exitCode = exitCode
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
}
|
||||
}
|
||||
|
||||
/// Health check response
|
||||
public struct HealthResponse: Codable {
|
||||
public let status: String
|
||||
public let timestamp: Date
|
||||
public let sessions: Int
|
||||
public let version: String
|
||||
|
||||
public init(status: String, timestamp: Date, sessions: Int, version: String) {
|
||||
self.status = status
|
||||
self.timestamp = timestamp
|
||||
self.sessions = sessions
|
||||
self.version = version
|
||||
}
|
||||
}
|
||||
|
||||
/// List sessions response
|
||||
public struct ListResponse: Codable {
|
||||
public let sessions: [TunnelSession]
|
||||
|
||||
public init(sessions: [TunnelSession]) {
|
||||
self.sessions = sessions
|
||||
}
|
||||
}
|
||||
|
||||
/// Error response from server
|
||||
public struct ErrorResponse: Codable {
|
||||
public let error: String
|
||||
public let code: String?
|
||||
|
||||
public init(error: String, code: String? = nil) {
|
||||
self.error = error
|
||||
self.code = code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,8 +37,23 @@ enum HTTPClientError: Error {
|
|||
|
||||
extension URLRequest {
|
||||
init(customHTTPRequest: HTTPRequest) {
|
||||
guard let url = customHTTPRequest.url else {
|
||||
fatalError("HTTPRequest must have a valid URL")
|
||||
// Reconstruct URL from components
|
||||
var urlComponents = URLComponents()
|
||||
urlComponents.scheme = customHTTPRequest.scheme
|
||||
|
||||
if let authority = customHTTPRequest.authority {
|
||||
// Parse host and port from authority
|
||||
let parts = authority.split(separator: ":", maxSplits: 1)
|
||||
urlComponents.host = String(parts[0])
|
||||
if parts.count > 1 {
|
||||
urlComponents.port = Int(String(parts[1]))
|
||||
}
|
||||
}
|
||||
|
||||
urlComponents.path = customHTTPRequest.path ?? "/"
|
||||
|
||||
guard let url = urlComponents.url else {
|
||||
fatalError("HTTPRequest must have valid URL components")
|
||||
}
|
||||
|
||||
self.init(url: url)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import Foundation
|
||||
import os.log
|
||||
import UserNotifications
|
||||
|
||||
/// Stub implementation of SparkleUpdaterManager
|
||||
|
|
@ -8,18 +9,18 @@ public final class SparkleUpdaterManager: NSObject {
|
|||
|
||||
public static let shared = SparkleUpdaterManager()
|
||||
|
||||
private let logger = Logger(
|
||||
private let logger = os.Logger(
|
||||
subsystem: "VibeTunnel",
|
||||
category: "SparkleUpdater"
|
||||
)
|
||||
|
||||
private override init() {
|
||||
public override init() {
|
||||
super.init()
|
||||
logger.info("SparkleUpdaterManager initialized (stub implementation)")
|
||||
}
|
||||
|
||||
public func setUpdateChannel(_ channel: UpdateChannel) {
|
||||
logger.info("Update channel set to: \(channel) (stub)")
|
||||
logger.info("Update channel set to: \(channel.rawValue) (stub)")
|
||||
}
|
||||
|
||||
public func checkForUpdatesInBackground() {
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ public class TunnelClient {
|
|||
}
|
||||
|
||||
/// WebSocket client for real-time terminal communication
|
||||
public class TunnelWebSocketClient: NSObject {
|
||||
public final class TunnelWebSocketClient: NSObject, @unchecked Sendable {
|
||||
private let url: URL
|
||||
private let apiKey: String
|
||||
private var sessionId: String?
|
||||
|
|
|
|||
212
VibeTunnel/Core/Services/TunnelClient2.swift
Normal file
212
VibeTunnel/Core/Services/TunnelClient2.swift
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
// This file is required for testing with dependency injection.
|
||||
// DO NOT REMOVE - tests depend on TunnelClient2 and TunnelClient2Error
|
||||
|
||||
import Foundation
|
||||
import HTTPTypes
|
||||
import HTTPTypesFoundation
|
||||
import Logging
|
||||
|
||||
/// HTTP client-based tunnel client for better testability
|
||||
public final class TunnelClient2 {
|
||||
// MARK: - Properties
|
||||
|
||||
private let baseURL: URL
|
||||
private let apiKey: String
|
||||
private let httpClient: HTTPClientProtocol
|
||||
private let decoder: JSONDecoder
|
||||
private let encoder: JSONEncoder
|
||||
private let logger = Logger(label: "VibeTunnel.TunnelClient2")
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(
|
||||
baseURL: URL,
|
||||
apiKey: String,
|
||||
httpClient: HTTPClientProtocol? = nil
|
||||
) {
|
||||
self.baseURL = baseURL
|
||||
self.apiKey = apiKey
|
||||
self.httpClient = httpClient ?? HTTPClient()
|
||||
|
||||
self.decoder = JSONDecoder()
|
||||
self.decoder.dateDecodingStrategy = .iso8601
|
||||
|
||||
self.encoder = JSONEncoder()
|
||||
self.encoder.dateEncodingStrategy = .iso8601
|
||||
}
|
||||
|
||||
// MARK: - Health Check
|
||||
|
||||
public func checkHealth() async throws -> TunnelSession.HealthResponse {
|
||||
let request = buildRequest(path: "/health", method: .get)
|
||||
let (data, response) = try await httpClient.data(for: request, body: nil)
|
||||
|
||||
guard response.status == .ok else {
|
||||
throw TunnelClient2Error.httpError(statusCode: response.status.code)
|
||||
}
|
||||
|
||||
return try decoder.decode(TunnelSession.HealthResponse.self, from: data)
|
||||
}
|
||||
|
||||
// MARK: - Session Management
|
||||
|
||||
public func createSession(clientInfo: TunnelSession.ClientInfo? = nil) async throws -> TunnelSession.CreateResponse {
|
||||
let requestBody = TunnelSession.CreateRequest(clientInfo: clientInfo)
|
||||
let request = buildRequest(path: "/api/sessions", method: .post)
|
||||
let body = try encoder.encode(requestBody)
|
||||
|
||||
let (data, response) = try await httpClient.data(for: request, body: body)
|
||||
|
||||
guard response.status == .created || response.status == .ok else {
|
||||
if let errorResponse = try? decoder.decode(TunnelSession.ErrorResponse.self, from: data) {
|
||||
throw TunnelClient2Error.serverError(errorResponse.error)
|
||||
}
|
||||
throw TunnelClient2Error.httpError(statusCode: response.status.code)
|
||||
}
|
||||
|
||||
return try decoder.decode(TunnelSession.CreateResponse.self, from: data)
|
||||
}
|
||||
|
||||
public func listSessions() async throws -> [TunnelSession] {
|
||||
let request = buildRequest(path: "/api/sessions", method: .get)
|
||||
let (data, response) = try await httpClient.data(for: request, body: nil)
|
||||
|
||||
guard response.status == .ok else {
|
||||
throw TunnelClient2Error.httpError(statusCode: response.status.code)
|
||||
}
|
||||
|
||||
let listResponse = try decoder.decode(TunnelSession.ListResponse.self, from: data)
|
||||
return listResponse.sessions
|
||||
}
|
||||
|
||||
public func getSession(id: String) async throws -> TunnelSession {
|
||||
let request = buildRequest(path: "/api/sessions/\(id)", method: .get)
|
||||
let (data, response) = try await httpClient.data(for: request, body: nil)
|
||||
|
||||
guard response.status == .ok else {
|
||||
if response.status == .notFound {
|
||||
throw TunnelClient2Error.sessionNotFound
|
||||
}
|
||||
throw TunnelClient2Error.httpError(statusCode: response.status.code)
|
||||
}
|
||||
|
||||
return try decoder.decode(TunnelSession.self, from: data)
|
||||
}
|
||||
|
||||
public func deleteSession(id: String) async throws {
|
||||
let request = buildRequest(path: "/api/sessions/\(id)", method: .delete)
|
||||
let (_, response) = try await httpClient.data(for: request, body: nil)
|
||||
|
||||
guard response.status == .noContent || response.status == .ok else {
|
||||
if response.status == .notFound {
|
||||
throw TunnelClient2Error.sessionNotFound
|
||||
}
|
||||
throw TunnelClient2Error.httpError(statusCode: response.status.code)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Command Execution
|
||||
|
||||
public func executeCommand(
|
||||
sessionId: String,
|
||||
command: String,
|
||||
environment: [String: String]? = nil,
|
||||
workingDirectory: String? = nil
|
||||
) async throws -> TunnelSession.ExecuteCommandResponse {
|
||||
let requestBody = TunnelSession.ExecuteCommandRequest(
|
||||
sessionId: sessionId,
|
||||
command: command,
|
||||
environment: environment,
|
||||
workingDirectory: workingDirectory
|
||||
)
|
||||
|
||||
let request = buildRequest(path: "/api/sessions/\(sessionId)/execute", method: .post)
|
||||
let body = try encoder.encode(requestBody)
|
||||
|
||||
let (data, response) = try await httpClient.data(for: request, body: body)
|
||||
|
||||
guard response.status == .ok else {
|
||||
if response.status == .notFound {
|
||||
throw TunnelClient2Error.sessionNotFound
|
||||
}
|
||||
if let errorResponse = try? decoder.decode(TunnelSession.ErrorResponse.self, from: data) {
|
||||
throw TunnelClient2Error.serverError(errorResponse.error)
|
||||
}
|
||||
throw TunnelClient2Error.httpError(statusCode: response.status.code)
|
||||
}
|
||||
|
||||
return try decoder.decode(TunnelSession.ExecuteCommandResponse.self, from: data)
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
private func buildRequest(path: String, method: HTTPRequest.Method) -> HTTPRequest {
|
||||
let url = baseURL.appendingPathComponent(path)
|
||||
|
||||
// Use URLComponents to get scheme, host, and path
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
|
||||
fatalError("Invalid URL")
|
||||
}
|
||||
|
||||
var request = HTTPRequest(
|
||||
method: method,
|
||||
scheme: components.scheme,
|
||||
authority: components.host.map { host in
|
||||
components.port.map { "\(host):\($0)" } ?? host
|
||||
},
|
||||
path: components.path
|
||||
)
|
||||
|
||||
// Add authentication
|
||||
request.headerFields[.authorization] = "Bearer \(apiKey)"
|
||||
|
||||
// Add content type for POST/PUT requests
|
||||
if method == .post || method == .put {
|
||||
request.headerFields[.contentType] = "application/json"
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
|
||||
public enum TunnelClient2Error: LocalizedError, Equatable {
|
||||
case invalidResponse
|
||||
case httpError(statusCode: Int)
|
||||
case serverError(String)
|
||||
case sessionNotFound
|
||||
case decodingError(String)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidResponse:
|
||||
return "Invalid response from server"
|
||||
case .httpError(let statusCode):
|
||||
return "HTTP error: \(statusCode)"
|
||||
case .serverError(let message):
|
||||
return "Server error: \(message)"
|
||||
case .sessionNotFound:
|
||||
return "Session not found"
|
||||
case .decodingError(let error):
|
||||
return "Decoding error: \(error)"
|
||||
}
|
||||
}
|
||||
|
||||
public static func == (lhs: TunnelClient2Error, rhs: TunnelClient2Error) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.invalidResponse, .invalidResponse):
|
||||
return true
|
||||
case (.httpError(let code1), .httpError(let code2)):
|
||||
return code1 == code2
|
||||
case (.serverError(let msg1), .serverError(let msg2)):
|
||||
return msg1 == msg2
|
||||
case (.sessionNotFound, .sessionNotFound):
|
||||
return true
|
||||
case (.decodingError(let msg1), .decodingError(let msg2)):
|
||||
return msg1 == msg2
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,139 +1,21 @@
|
|||
import Combine
|
||||
import Foundation
|
||||
import Logging
|
||||
import Combine
|
||||
|
||||
/// Demo code showing how to use the VibeTunnel server
|
||||
enum TunnelServerDemo {
|
||||
private static let logger = Logger(label: "VibeTunnel.TunnelServerDemo")
|
||||
|
||||
static func runDemo() async {
|
||||
// Get the API key (in production, this should be managed securely)
|
||||
// For demo purposes, using a hardcoded key
|
||||
let apiKey = "demo-api-key-12345"
|
||||
|
||||
logger.info("Using API key: [REDACTED]")
|
||||
|
||||
// Create client
|
||||
let client = TunnelClient(apiKey: apiKey)
|
||||
|
||||
do {
|
||||
// Check server health
|
||||
let isHealthy = try await client.checkHealth()
|
||||
logger.info("Server healthy: \(isHealthy)")
|
||||
|
||||
// Create a new session
|
||||
let session = try await client.createSession(
|
||||
workingDirectory: "/tmp",
|
||||
shell: "/bin/zsh"
|
||||
)
|
||||
logger.info("Created session: \(session.sessionId)")
|
||||
|
||||
// Execute a command
|
||||
let response = try await client.executeCommand(
|
||||
sessionId: session.sessionId,
|
||||
command: "echo 'Hello from VibeTunnel!'"
|
||||
)
|
||||
logger.info("Command output: \(response.output ?? "none")")
|
||||
|
||||
// List all sessions
|
||||
let sessions = try await client.listSessions()
|
||||
logger.info("Active sessions: \(sessions.count)")
|
||||
|
||||
// Close the session
|
||||
try await client.closeSession(id: session.sessionId)
|
||||
logger.info("Session closed")
|
||||
} catch {
|
||||
logger.error("Demo error: \(error)")
|
||||
}
|
||||
/// Stub implementation of TunnelServer for the macOS app
|
||||
@MainActor
|
||||
public final class TunnelServerDemo: ObservableObject {
|
||||
@Published public private(set) var isRunning = false
|
||||
@Published public private(set) var port: Int
|
||||
|
||||
public init(port: Int = 8080) {
|
||||
self.port = port
|
||||
}
|
||||
|
||||
static func runWebSocketDemo() async {
|
||||
// For demo purposes, using a hardcoded key
|
||||
let apiKey = "demo-api-key-12345"
|
||||
|
||||
let client = TunnelClient(apiKey: apiKey)
|
||||
|
||||
do {
|
||||
// Create a session first
|
||||
let session = try await client.createSession()
|
||||
logger.info("Created session for WebSocket: \(session.sessionId)")
|
||||
|
||||
// Connect WebSocket
|
||||
guard let wsClient = client.connectWebSocket(sessionId: session.sessionId) else {
|
||||
logger.error("Failed to create WebSocket client")
|
||||
return
|
||||
}
|
||||
wsClient.connect()
|
||||
|
||||
// Subscribe to messages
|
||||
let cancellable = wsClient.messages.sink { message in
|
||||
switch message.type {
|
||||
case .output:
|
||||
logger.info("Output: \(message.data ?? "")")
|
||||
case .error:
|
||||
logger.error("Error: \(message.data ?? "")")
|
||||
default:
|
||||
logger.info("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 {
|
||||
logger.error("WebSocket demo error: \(error)")
|
||||
}
|
||||
|
||||
public func start() async throws {
|
||||
isRunning = true
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
public func stop() async throws {
|
||||
isRunning = false
|
||||
}
|
||||
}
|
||||
139
VibeTunnel/Core/Services/TunnelServerExample.swift
Normal file
139
VibeTunnel/Core/Services/TunnelServerExample.swift
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import Combine
|
||||
import Foundation
|
||||
import Logging
|
||||
|
||||
/// Demo code showing how to use the VibeTunnel server
|
||||
enum TunnelServerExample {
|
||||
private static let logger = Logger(label: "VibeTunnel.TunnelServerDemo")
|
||||
|
||||
static func runDemo() async {
|
||||
// Get the API key (in production, this should be managed securely)
|
||||
// For demo purposes, using a hardcoded key
|
||||
let apiKey = "demo-api-key-12345"
|
||||
|
||||
logger.info("Using API key: [REDACTED]")
|
||||
|
||||
// Create client
|
||||
let client = TunnelClient(apiKey: apiKey)
|
||||
|
||||
do {
|
||||
// Check server health
|
||||
let isHealthy = try await client.checkHealth()
|
||||
logger.info("Server healthy: \(isHealthy)")
|
||||
|
||||
// Create a new session
|
||||
let session = try await client.createSession(
|
||||
workingDirectory: "/tmp",
|
||||
shell: "/bin/zsh"
|
||||
)
|
||||
logger.info("Created session: \(session.sessionId)")
|
||||
|
||||
// Execute a command
|
||||
let response = try await client.executeCommand(
|
||||
sessionId: session.sessionId,
|
||||
command: "echo 'Hello from VibeTunnel!'"
|
||||
)
|
||||
logger.info("Command output: \(response.output ?? "none")")
|
||||
|
||||
// List all sessions
|
||||
let sessions = try await client.listSessions()
|
||||
logger.info("Active sessions: \(sessions.count)")
|
||||
|
||||
// Close the session
|
||||
try await client.closeSession(id: session.sessionId)
|
||||
logger.info("Session closed")
|
||||
} catch {
|
||||
logger.error("Demo error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
static func runWebSocketDemo() async {
|
||||
// For demo purposes, using a hardcoded key
|
||||
let apiKey = "demo-api-key-12345"
|
||||
|
||||
let client = TunnelClient(apiKey: apiKey)
|
||||
|
||||
do {
|
||||
// Create a session first
|
||||
let session = try await client.createSession()
|
||||
logger.info("Created session for WebSocket: \(session.sessionId)")
|
||||
|
||||
// Connect WebSocket
|
||||
guard let wsClient = client.connectWebSocket(sessionId: session.sessionId) else {
|
||||
logger.error("Failed to create WebSocket client")
|
||||
return
|
||||
}
|
||||
wsClient.connect()
|
||||
|
||||
// Subscribe to messages
|
||||
let cancellable = wsClient.messages.sink { message in
|
||||
switch message.type {
|
||||
case .output:
|
||||
logger.info("Output: \(message.data ?? "")")
|
||||
case .error:
|
||||
logger.error("Error: \(message.data ?? "")")
|
||||
default:
|
||||
logger.info("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 {
|
||||
logger.error("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
|
||||
|
|
@ -145,11 +145,11 @@ struct AdvancedSettingsView: View {
|
|||
private var updateChannelRaw = UpdateChannel.stable.rawValue
|
||||
|
||||
@State private var isCheckingForUpdates = false
|
||||
@StateObject private var tunnelServer: TunnelServer
|
||||
@StateObject private var tunnelServer: TunnelServerDemo
|
||||
|
||||
init() {
|
||||
let port = Int(UserDefaults.standard.string(forKey: "serverPort") ?? "8080") ?? 8_080
|
||||
_tunnelServer = StateObject(wrappedValue: TunnelServer(port: port))
|
||||
_tunnelServer = StateObject(wrappedValue: TunnelServerDemo(port: port))
|
||||
}
|
||||
|
||||
var updateChannel: UpdateChannel {
|
||||
|
|
@ -300,7 +300,7 @@ struct AdvancedSettingsView: View {
|
|||
private func toggleServer() {
|
||||
Task {
|
||||
if tunnelServer.isRunning {
|
||||
await tunnelServer.stop()
|
||||
try await tunnelServer.stop()
|
||||
} else {
|
||||
do {
|
||||
try await tunnelServer.start()
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
}
|
||||
|
||||
// Initialize Sparkle updater manager
|
||||
sparkleUpdaterManager = SparkleUpdaterManager()
|
||||
sparkleUpdaterManager = SparkleUpdaterManager.shared
|
||||
|
||||
// Configure activation policy based on settings (default to menu bar only)
|
||||
let showInDock = UserDefaults.standard.bool(forKey: "showInDock")
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import Foundation
|
|||
import HTTPTypes
|
||||
@testable import VibeTunnel
|
||||
|
||||
@Suite("TunnelClient Tests")
|
||||
struct TunnelClientTests {
|
||||
@Suite("TunnelClient2 Tests")
|
||||
struct TunnelClient2Tests {
|
||||
let mockClient: MockHTTPClient
|
||||
let tunnelClient: TunnelClient2
|
||||
let testURL = URL(string: "http://localhost:8080")!
|
||||
|
|
@ -52,7 +52,7 @@ struct TunnelClientTests {
|
|||
mockClient.configure(for: "/health", response: .serverError)
|
||||
|
||||
// Act & Assert
|
||||
await #expect(throws: TunnelClientError.httpError(statusCode: 500)) {
|
||||
await #expect(throws: TunnelClient2Error.httpError(statusCode: 500)) {
|
||||
_ = try await tunnelClient.checkHealth()
|
||||
}
|
||||
}
|
||||
|
|
@ -123,7 +123,7 @@ struct TunnelClientTests {
|
|||
)
|
||||
|
||||
// Act & Assert
|
||||
await #expect(throws: TunnelClientError.serverError("Maximum sessions reached")) {
|
||||
await #expect(throws: TunnelClient2Error.serverError("Maximum sessions reached")) {
|
||||
_ = try await tunnelClient.createSession()
|
||||
}
|
||||
}
|
||||
|
|
@ -182,7 +182,7 @@ struct TunnelClientTests {
|
|||
)
|
||||
|
||||
// Act & Assert
|
||||
await #expect(throws: TunnelClientError.sessionNotFound) {
|
||||
await #expect(throws: TunnelClient2Error.sessionNotFound) {
|
||||
_ = try await tunnelClient.getSession(id: "unknown-session")
|
||||
}
|
||||
}
|
||||
|
|
@ -196,10 +196,10 @@ struct TunnelClientTests {
|
|||
)
|
||||
|
||||
// Act
|
||||
try await tunnelClient.deleteSession(id: "session-123")
|
||||
try await tunnelClient.deleteSession(id: "00000000-0000-0000-0000-000000000123")
|
||||
|
||||
// Assert
|
||||
#expect(mockClient.wasRequested(path: "/api/sessions/session-123"))
|
||||
#expect(mockClient.wasRequested(path: "/api/sessions/00000000-0000-0000-0000-000000000123"))
|
||||
let lastRequest = mockClient.lastRequest()!
|
||||
#expect(lastRequest.request.method == .delete)
|
||||
}
|
||||
|
|
@ -360,7 +360,7 @@ struct TunnelClientTests {
|
|||
)
|
||||
|
||||
// Act & Assert
|
||||
await #expect(throws: TunnelClientError.httpError(statusCode: statusCode.code)) {
|
||||
await #expect(throws: TunnelClient2Error.httpError(statusCode: statusCode.code)) {
|
||||
_ = try await tunnelClient.listSessions()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue