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_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.5;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
|
|
@ -394,7 +394,7 @@
|
|||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.5;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = macosx;
|
||||
|
|
@ -474,7 +474,7 @@
|
|||
CURRENT_PROJECT_VERSION = 100;
|
||||
DEVELOPMENT_TEAM = Y5PE65HELJ;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.5;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.amantus.vibetunnel.Tests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
@ -494,7 +494,7 @@
|
|||
CURRENT_PROJECT_VERSION = 100;
|
||||
DEVELOPMENT_TEAM = Y5PE65HELJ;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.5;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.amantus.vibetunnel.Tests;
|
||||
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")
|
||||
}
|
||||
}
|
||||
.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)
|
||||
|
||||
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.target = self
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,73 @@
|
|||
# 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
|
||||
|
||||
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
|
||||
import Foundation
|
||||
|
|
|
|||
Loading…
Reference in a new issue