mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-24 14:47:39 +00:00
feat: implement working HTTP server with debug panel
- Replace stub TunnelServerDemo with full Hummingbird HTTP server - Add comprehensive debug settings view with server info and controls - Implement API endpoint listing with interactive testing - Add TTYForwardManager for tty-fwd integration - Auto-start HTTP server on app launch on port 8080 - Show server status, port, and base URL in debug panel - Add test buttons for GET endpoints with live response preview - Include debug mode toggle and log level selector - Add quick access to Console.app and Application Support - Update bundle identifier to sh.vibetunnel.vibetunnel - Add code signing configuration templates
This commit is contained in:
parent
8d80935bdb
commit
99b77c9b53
16 changed files with 785 additions and 13 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -77,4 +77,7 @@ DerivedData/
|
|||
*.log
|
||||
*.bak
|
||||
*~
|
||||
.*.swp
|
||||
.*.swp
|
||||
|
||||
# Local Development Settings
|
||||
Local.xcconfig
|
||||
11
README.md
11
README.md
|
|
@ -69,6 +69,17 @@ VibeTunnel/
|
|||
└── docs/ # Documentation
|
||||
```
|
||||
|
||||
### Code Signing Setup
|
||||
|
||||
This project uses xcconfig files to manage developer-specific settings, preventing code signing conflicts when multiple developers work on the project.
|
||||
|
||||
**For new developers:**
|
||||
1. Copy the template: `cp VibeTunnel/Local.xcconfig.template VibeTunnel/Local.xcconfig`
|
||||
2. Edit `VibeTunnel/Local.xcconfig` and add your development team ID
|
||||
3. Open the project in Xcode - it will use your settings automatically
|
||||
|
||||
See [docs/CODE_SIGNING_SETUP.md](docs/CODE_SIGNING_SETUP.md) for detailed instructions.
|
||||
|
||||
### Building
|
||||
|
||||
The project uses standard Xcode build system:
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@
|
|||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
Local.xcconfig,
|
||||
Local.xcconfig.template,
|
||||
Shared.xcconfig,
|
||||
version.xcconfig,
|
||||
);
|
||||
target = 788687F02DFF4FCB00B22C15 /* VibeTunnel */;
|
||||
|
|
@ -430,7 +433,7 @@
|
|||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = Y5PE65HELJ;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
|
|
@ -444,7 +447,7 @@
|
|||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 0.1.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.pspdfkit.VibeTunnel;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = sh.vibetunnel.vibetunnel;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -463,7 +466,7 @@
|
|||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = Y5PE65HELJ;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
|
|
@ -477,7 +480,7 @@
|
|||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 0.1.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.pspdfkit.VibeTunnel;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = sh.vibetunnel.vibetunnel;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -491,11 +494,11 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 55V3KCX766;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.1;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.steipete.VibeTunnelTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = sh.vibetunnel.vibetunnelTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -510,11 +513,11 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 55V3KCX766;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.1;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.steipete.VibeTunnelTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = sh.vibetunnel.vibetunnelTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -528,7 +531,7 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 55V3KCX766;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.steipete.VibeTunnelUITests;
|
||||
|
|
|
|||
Binary file not shown.
93
VibeTunnel/Core/Services/TTYForwardManager.swift
Normal file
93
VibeTunnel/Core/Services/TTYForwardManager.swift
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import Foundation
|
||||
import os.log
|
||||
|
||||
final class TTYForwardManager {
|
||||
static let shared = TTYForwardManager()
|
||||
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "VibeTunnel", category: "TTYForwardManager")
|
||||
|
||||
private init() {}
|
||||
|
||||
/// Returns the URL to the bundled tty-fwd executable
|
||||
var ttyForwardExecutableURL: URL? {
|
||||
return Bundle.main.url(forResource: "tty-fwd", withExtension: nil, subdirectory: "Resources")
|
||||
}
|
||||
|
||||
/// Executes the tty-fwd binary with the specified arguments
|
||||
/// - Parameters:
|
||||
/// - arguments: Command line arguments to pass to tty-fwd
|
||||
/// - completion: Completion handler with the process result
|
||||
func executeTTYForward(with arguments: [String], completion: @escaping (Result<Process, Error>) -> Void) {
|
||||
guard let executableURL = ttyForwardExecutableURL else {
|
||||
completion(.failure(TTYForwardError.executableNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the executable exists and is executable
|
||||
let fileManager = FileManager.default
|
||||
var isDirectory: ObjCBool = false
|
||||
guard fileManager.fileExists(atPath: executableURL.path, isDirectory: &isDirectory),
|
||||
!isDirectory.boolValue else {
|
||||
completion(.failure(TTYForwardError.executableNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if executable permission is set
|
||||
guard fileManager.isExecutableFile(atPath: executableURL.path) else {
|
||||
logger.error("tty-fwd binary is not executable at path: \(executableURL.path)")
|
||||
completion(.failure(TTYForwardError.notExecutable))
|
||||
return
|
||||
}
|
||||
|
||||
let process = Process()
|
||||
process.executableURL = executableURL
|
||||
process.arguments = arguments
|
||||
|
||||
// Set up pipes for stdout and stderr
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
process.standardOutput = outputPipe
|
||||
process.standardError = errorPipe
|
||||
|
||||
// Log the command being executed
|
||||
logger.info("Executing tty-fwd with arguments: \(arguments.joined(separator: " "))")
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
completion(.success(process))
|
||||
} catch {
|
||||
logger.error("Failed to execute tty-fwd: \(error.localizedDescription)")
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new tty-fwd process configured but not yet started
|
||||
/// - Parameter arguments: Command line arguments to pass to tty-fwd
|
||||
/// - Returns: A configured Process instance or nil if the executable is not found
|
||||
func createTTYForwardProcess(with arguments: [String]) -> Process? {
|
||||
guard let executableURL = ttyForwardExecutableURL else {
|
||||
logger.error("tty-fwd executable not found in bundle")
|
||||
return nil
|
||||
}
|
||||
|
||||
let process = Process()
|
||||
process.executableURL = executableURL
|
||||
process.arguments = arguments
|
||||
|
||||
return process
|
||||
}
|
||||
}
|
||||
|
||||
enum TTYForwardError: LocalizedError {
|
||||
case executableNotFound
|
||||
case notExecutable
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .executableNotFound:
|
||||
return "tty-fwd executable not found in application bundle"
|
||||
case .notExecutable:
|
||||
return "tty-fwd binary does not have executable permissions"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,214 @@
|
|||
import Foundation
|
||||
import Combine
|
||||
import HTTPTypes
|
||||
import Hummingbird
|
||||
import HummingbirdCore
|
||||
import Logging
|
||||
import NIOCore
|
||||
import NIOPosix
|
||||
import os
|
||||
|
||||
/// Stub implementation of TunnelServer for the macOS app
|
||||
/// HTTP server implementation for the macOS app
|
||||
@MainActor
|
||||
public final class TunnelServerDemo: ObservableObject {
|
||||
@Published public private(set) var isRunning = false
|
||||
@Published public private(set) var port: Int
|
||||
@Published public var lastError: Error?
|
||||
|
||||
private var app: Application<Router<BasicRequestContext>.Responder>?
|
||||
private let logger = Logger(label: "VibeTunnel.TunnelServer")
|
||||
private let terminalManager = TerminalManager()
|
||||
private var serverTask: Task<Void, Error>?
|
||||
|
||||
public init(port: Int = 8080) {
|
||||
self.port = port
|
||||
}
|
||||
|
||||
public func start() async throws {
|
||||
isRunning = true
|
||||
guard !isRunning else { return }
|
||||
|
||||
do {
|
||||
let router = Router(context: BasicRequestContext.self)
|
||||
|
||||
// Add middleware
|
||||
router.add(middleware: LogRequestsMiddleware(.info))
|
||||
|
||||
// Health check endpoint
|
||||
router.get("/health") { _, _ -> HTTPResponse.Status in
|
||||
.ok
|
||||
}
|
||||
|
||||
// Info endpoint
|
||||
router.get("/info") { _, _ -> Response in
|
||||
let info = [
|
||||
"name": "VibeTunnel",
|
||||
"version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0",
|
||||
"uptime": ProcessInfo.processInfo.systemUptime
|
||||
]
|
||||
|
||||
let jsonData = try! JSONSerialization.data(withJSONObject: info)
|
||||
var buffer = ByteBuffer()
|
||||
buffer.writeBytes(jsonData)
|
||||
|
||||
return Response(
|
||||
status: .ok,
|
||||
headers: [.contentType: "application/json"],
|
||||
body: ResponseBody(byteBuffer: buffer)
|
||||
)
|
||||
}
|
||||
|
||||
// Simple test endpoint
|
||||
let portNumber = self.port // Capture port value before closure
|
||||
router.get("/") { _, _ -> Response in
|
||||
let html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>VibeTunnel Server</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; margin: 40px; }
|
||||
h1 { color: #333; }
|
||||
.status { color: green; font-weight: bold; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>VibeTunnel Server</h1>
|
||||
<p class="status">✓ Server is running on port \(portNumber)</p>
|
||||
<p>Available endpoints:</p>
|
||||
<ul>
|
||||
<li><a href="/health">/health</a> - Health check</li>
|
||||
<li><a href="/info">/info</a> - Server information</li>
|
||||
<li><a href="/sessions">/sessions</a> - List tty-fwd sessions</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
var buffer = ByteBuffer()
|
||||
buffer.writeString(html)
|
||||
|
||||
return Response(
|
||||
status: .ok,
|
||||
headers: [.contentType: "text/html"],
|
||||
body: ResponseBody(byteBuffer: buffer)
|
||||
)
|
||||
}
|
||||
|
||||
// Sessions endpoint - calls tty-fwd --list-sessions
|
||||
router.get("/sessions") { _, _ -> Response in
|
||||
let ttyManager = TTYForwardManager.shared
|
||||
guard let process = ttyManager.createTTYForwardProcess(with: ["--list-sessions"]) else {
|
||||
self.logger.error("Failed to create tty-fwd process")
|
||||
let errorJson = "{\"error\": \"tty-fwd binary not found\"}"
|
||||
var buffer = ByteBuffer()
|
||||
buffer.writeString(errorJson)
|
||||
return Response(
|
||||
status: .internalServerError,
|
||||
headers: [.contentType: "application/json"],
|
||||
body: ResponseBody(byteBuffer: buffer)
|
||||
)
|
||||
}
|
||||
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
process.standardOutput = outputPipe
|
||||
process.standardError = errorPipe
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
if process.terminationStatus == 0 {
|
||||
// Read the JSON output
|
||||
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
var buffer = ByteBuffer()
|
||||
buffer.writeBytes(outputData)
|
||||
|
||||
return Response(
|
||||
status: .ok,
|
||||
headers: [.contentType: "application/json"],
|
||||
body: ResponseBody(byteBuffer: buffer)
|
||||
)
|
||||
} else {
|
||||
// Read error output
|
||||
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let errorString = String(data: errorData, encoding: .utf8) ?? "Unknown error"
|
||||
self.logger.error("tty-fwd failed with status \(process.terminationStatus): \(errorString)")
|
||||
|
||||
let errorJson = "{\"error\": \"Failed to list sessions: \(errorString.replacingOccurrences(of: "\"", with: "\\\""))\"}"
|
||||
var buffer = ByteBuffer()
|
||||
buffer.writeString(errorJson)
|
||||
return Response(
|
||||
status: .internalServerError,
|
||||
headers: [.contentType: "application/json"],
|
||||
body: ResponseBody(byteBuffer: buffer)
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
self.logger.error("Failed to run tty-fwd: \(error)")
|
||||
let errorJson = "{\"error\": \"Failed to execute tty-fwd: \(error.localizedDescription.replacingOccurrences(of: "\"", with: "\\\""))\"}"
|
||||
var buffer = ByteBuffer()
|
||||
buffer.writeString(errorJson)
|
||||
return Response(
|
||||
status: .internalServerError,
|
||||
headers: [.contentType: "application/json"],
|
||||
body: ResponseBody(byteBuffer: buffer)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Create application configuration
|
||||
let configuration = ApplicationConfiguration(
|
||||
address: .hostname("127.0.0.1", port: port),
|
||||
serverName: "VibeTunnel"
|
||||
)
|
||||
|
||||
// Create the application
|
||||
let app = Application(
|
||||
responder: router.buildResponder(),
|
||||
configuration: configuration,
|
||||
logger: logger
|
||||
)
|
||||
|
||||
self.app = app
|
||||
|
||||
// Run the server in a separate task
|
||||
serverTask = Task { @Sendable [weak self] in
|
||||
do {
|
||||
try await app.run()
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self?.lastError = error
|
||||
self?.isRunning = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Give the server a moment to start
|
||||
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
|
||||
|
||||
isRunning = true
|
||||
logger.info("Server started on port \(port)")
|
||||
|
||||
} catch {
|
||||
lastError = error
|
||||
isRunning = false
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public func stop() async throws {
|
||||
guard isRunning else { return }
|
||||
|
||||
logger.info("Stopping server...")
|
||||
|
||||
// Cancel the server task - this will stop the application
|
||||
serverTask?.cancel()
|
||||
serverTask = nil
|
||||
|
||||
// Clear the application reference
|
||||
self.app = nil
|
||||
|
||||
isRunning = false
|
||||
}
|
||||
}
|
||||
13
VibeTunnel/Local.xcconfig.template
Normal file
13
VibeTunnel/Local.xcconfig.template
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Local Development Configuration Template
|
||||
// Copy this file as Local.xcconfig and set your personal development team
|
||||
// DO NOT commit Local.xcconfig to version control
|
||||
|
||||
// Set your personal development team ID here
|
||||
// You can find your team ID in Xcode: Preferences > Accounts > Your Account > Team ID
|
||||
DEVELOPMENT_TEAM = YOUR_TEAM_ID_HERE
|
||||
|
||||
// Code signing style (Automatic or Manual)
|
||||
CODE_SIGN_STYLE = Automatic
|
||||
|
||||
// Optional: Override bundle identifier for local development
|
||||
// PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.VibeTunnel
|
||||
BIN
VibeTunnel/Resources/tty-fwd
Executable file
BIN
VibeTunnel/Resources/tty-fwd
Executable file
Binary file not shown.
|
|
@ -1,14 +1,17 @@
|
|||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
enum SettingsTab: String, CaseIterable {
|
||||
case general
|
||||
case advanced
|
||||
case debug
|
||||
case about
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .general: "General"
|
||||
case .advanced: "Advanced"
|
||||
case .debug: "Debug"
|
||||
case .about: "About"
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +20,7 @@ enum SettingsTab: String, CaseIterable {
|
|||
switch self {
|
||||
case .general: "gear"
|
||||
case .advanced: "gearshape.2"
|
||||
case .debug: "hammer"
|
||||
case .about: "info.circle"
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +47,12 @@ struct SettingsView: View {
|
|||
}
|
||||
.tag(SettingsTab.advanced)
|
||||
|
||||
DebugSettingsView()
|
||||
.tabItem {
|
||||
Label(SettingsTab.debug.displayName, systemImage: SettingsTab.debug.icon)
|
||||
}
|
||||
.tag(SettingsTab.debug)
|
||||
|
||||
AboutView()
|
||||
.tabItem {
|
||||
Label(SettingsTab.about.displayName, systemImage: SettingsTab.about.icon)
|
||||
|
|
@ -319,6 +329,338 @@ struct AdvancedSettingsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
// Helper class to observe server state
|
||||
@MainActor
|
||||
class ServerObserver: ObservableObject {
|
||||
@Published var httpServer: TunnelServerDemo?
|
||||
@Published var isServerRunning = false
|
||||
@Published var serverPort = 8080
|
||||
|
||||
private var cancellable: AnyCancellable?
|
||||
|
||||
init() {
|
||||
setupServerConnection()
|
||||
}
|
||||
|
||||
func setupServerConnection() {
|
||||
if let appDelegate = NSApp.delegate as? AppDelegate {
|
||||
httpServer = appDelegate.httpServer
|
||||
isServerRunning = appDelegate.httpServer?.isRunning ?? false
|
||||
serverPort = appDelegate.httpServer?.port ?? 8080
|
||||
|
||||
// Observe server state changes
|
||||
cancellable = httpServer?.objectWillChange.sink { [weak self] _ in
|
||||
DispatchQueue.main.async {
|
||||
self?.isServerRunning = self?.httpServer?.isRunning ?? false
|
||||
self?.serverPort = self?.httpServer?.port ?? 8080
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DebugSettingsView: View {
|
||||
@StateObject private var serverObserver = ServerObserver()
|
||||
@State private var lastError: String?
|
||||
@State private var testResult: String?
|
||||
@State private var isTesting = false
|
||||
@AppStorage("debugMode") private var debugMode = false
|
||||
@AppStorage("logLevel") private var logLevel = "info"
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section {
|
||||
// HTTP Server Control
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text("HTTP Server")
|
||||
if serverObserver.isServerRunning {
|
||||
Circle()
|
||||
.fill(.green)
|
||||
.frame(width: 8, height: 8)
|
||||
}
|
||||
}
|
||||
Text(serverObserver.isServerRunning ? "Server is running on port \(serverObserver.serverPort)" : "Server is stopped")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Toggle("", isOn: Binding(
|
||||
get: { serverObserver.isServerRunning },
|
||||
set: { newValue in
|
||||
Task {
|
||||
await toggleServer(newValue)
|
||||
}
|
||||
}
|
||||
))
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
|
||||
if serverObserver.isServerRunning, let serverURL = URL(string: "http://localhost:\(serverObserver.serverPort)") {
|
||||
Link("Open in Browser", destination: serverURL)
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
if let lastError = lastError {
|
||||
Text(lastError)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
} header: {
|
||||
Text("HTTP Server")
|
||||
.font(.headline)
|
||||
} footer: {
|
||||
Text("The HTTP server provides REST API endpoints for terminal session management.")
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
Section {
|
||||
// Server Information
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
LabeledContent("Status") {
|
||||
HStack {
|
||||
Image(systemName: serverObserver.isServerRunning ? "checkmark.circle.fill" : "xmark.circle.fill")
|
||||
.foregroundStyle(serverObserver.isServerRunning ? .green : .secondary)
|
||||
Text(serverObserver.isServerRunning ? "Running" : "Stopped")
|
||||
}
|
||||
}
|
||||
|
||||
LabeledContent("Port") {
|
||||
Text("\(serverObserver.serverPort)")
|
||||
}
|
||||
|
||||
LabeledContent("Base URL") {
|
||||
Text("http://localhost:\(serverObserver.serverPort)")
|
||||
.font(.system(.body, design: .monospaced))
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Server Information")
|
||||
.font(.headline)
|
||||
}
|
||||
|
||||
Section {
|
||||
// API Endpoints with test functionality
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
ForEach(apiEndpoints, id: \.path) { endpoint in
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(endpoint.method)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.foregroundStyle(.blue)
|
||||
.frame(width: 45, alignment: .leading)
|
||||
|
||||
Text(endpoint.path)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
|
||||
Spacer()
|
||||
|
||||
if serverObserver.isServerRunning && endpoint.isTestable {
|
||||
Button("Test") {
|
||||
testEndpoint(endpoint)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.font(.caption)
|
||||
.disabled(isTesting)
|
||||
}
|
||||
}
|
||||
|
||||
Text(endpoint.description)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
|
||||
if let testResult = testResult {
|
||||
Text(testResult)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.green)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("API Endpoints")
|
||||
.font(.headline)
|
||||
} footer: {
|
||||
Text("Click 'Test' to send a request to the endpoint and see the response.")
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
// Debug Options
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Toggle("Debug mode", isOn: $debugMode)
|
||||
Text("Enable additional logging and debugging features.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text("Log Level")
|
||||
Spacer()
|
||||
Picker("", selection: $logLevel) {
|
||||
Text("Error").tag("error")
|
||||
Text("Warning").tag("warning")
|
||||
Text("Info").tag("info")
|
||||
Text("Debug").tag("debug")
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.labelsHidden()
|
||||
}
|
||||
Text("Set the verbosity of application logs.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} header: {
|
||||
Text("Debug Options")
|
||||
.font(.headline)
|
||||
}
|
||||
|
||||
// Developer Tools
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text("Server Logs")
|
||||
Spacer()
|
||||
Button("Open Console") {
|
||||
openConsole()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
Text("View server logs in Console.app")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text("Application Support")
|
||||
Spacer()
|
||||
Button("Show in Finder") {
|
||||
showApplicationSupport()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
Text("Open the application support directory")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} header: {
|
||||
Text("Developer Tools")
|
||||
.font(.headline)
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.scrollContentBackground(.hidden)
|
||||
.navigationTitle("Debug Settings")
|
||||
.onAppear {
|
||||
serverObserver.setupServerConnection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleServer(_ shouldStart: Bool) async {
|
||||
lastError = nil
|
||||
|
||||
if shouldStart {
|
||||
// Create a new server if needed
|
||||
if serverObserver.httpServer == nil {
|
||||
let newServer = TunnelServerDemo(port: serverObserver.serverPort)
|
||||
serverObserver.httpServer = newServer
|
||||
// Store reference in AppDelegate
|
||||
if let appDelegate = NSApp.delegate as? AppDelegate {
|
||||
appDelegate.setHTTPServer(newServer)
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try await serverObserver.httpServer?.start()
|
||||
serverObserver.isServerRunning = true
|
||||
} catch {
|
||||
lastError = error.localizedDescription
|
||||
serverObserver.isServerRunning = false
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
try await serverObserver.httpServer?.stop()
|
||||
serverObserver.isServerRunning = false
|
||||
} catch {
|
||||
lastError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func testEndpoint(_ endpoint: APIEndpoint) {
|
||||
isTesting = true
|
||||
testResult = nil
|
||||
|
||||
Task {
|
||||
do {
|
||||
let url = URL(string: "http://localhost:\(serverObserver.serverPort)\(endpoint.path)")!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = endpoint.method
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
let statusEmoji = httpResponse.statusCode == 200 ? "✅" : "❌"
|
||||
let preview = String(data: data, encoding: .utf8)?.prefix(100) ?? ""
|
||||
testResult = "\(statusEmoji) \(httpResponse.statusCode) - \(preview)..."
|
||||
}
|
||||
} catch {
|
||||
testResult = "❌ Error: \(error.localizedDescription)"
|
||||
}
|
||||
|
||||
isTesting = false
|
||||
|
||||
// Clear result after 5 seconds
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 5_000_000_000)
|
||||
testResult = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func openConsole() {
|
||||
NSWorkspace.shared.open(URL(fileURLWithPath: "/System/Applications/Utilities/Console.app"))
|
||||
}
|
||||
|
||||
private func showApplicationSupport() {
|
||||
if let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first {
|
||||
let appDirectory = appSupport.appendingPathComponent("VibeTunnel")
|
||||
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: appDirectory.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// API Endpoint data
|
||||
struct APIEndpoint {
|
||||
let method: String
|
||||
let path: String
|
||||
let description: String
|
||||
let isTestable: Bool
|
||||
}
|
||||
|
||||
let apiEndpoints = [
|
||||
APIEndpoint(method: "GET", path: "/", description: "Web interface - displays server status", isTestable: true),
|
||||
APIEndpoint(method: "GET", path: "/health", description: "Health check - returns OK if server is running", isTestable: true),
|
||||
APIEndpoint(method: "GET", path: "/info", description: "Server information - returns version and uptime", isTestable: true),
|
||||
APIEndpoint(method: "GET", path: "/sessions", description: "List tty-fwd sessions", isTestable: true),
|
||||
APIEndpoint(method: "POST", path: "/sessions", description: "Create new terminal session", isTestable: false),
|
||||
APIEndpoint(method: "GET", path: "/sessions/:id", description: "Get specific session information", isTestable: false),
|
||||
APIEndpoint(method: "DELETE", path: "/sessions/:id", description: "Close a terminal session", isTestable: false),
|
||||
APIEndpoint(method: "POST", path: "/execute", description: "Execute command in a session", isTestable: false)
|
||||
]
|
||||
|
||||
#Preview {
|
||||
SettingsView()
|
||||
}
|
||||
|
|
|
|||
14
VibeTunnel/Shared.xcconfig
Normal file
14
VibeTunnel/Shared.xcconfig
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Shared Configuration
|
||||
// This file contains settings shared across all configurations
|
||||
|
||||
// Include version configuration
|
||||
#include "version.xcconfig"
|
||||
|
||||
// Include local development settings (if exists)
|
||||
// This file is ignored by git and contains personal development team settings
|
||||
#include? "Local.xcconfig"
|
||||
|
||||
// Default values (can be overridden in Local.xcconfig)
|
||||
// These will be used if Local.xcconfig doesn't exist or doesn't define them
|
||||
DEVELOPMENT_TEAM = $(inherited)
|
||||
CODE_SIGN_STYLE = $(inherited)
|
||||
|
|
@ -28,6 +28,7 @@ struct VibeTunnelApp: App {
|
|||
final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
private(set) var sparkleUpdaterManager: SparkleUpdaterManager?
|
||||
private var statusItem: NSStatusItem?
|
||||
private(set) var httpServer: TunnelServerDemo?
|
||||
|
||||
/// Distributed notification name used to ask an existing instance to show the Settings window.
|
||||
private static let showSettingsNotification = Notification.Name("com.amantus.vibetunnel.showSettings")
|
||||
|
|
@ -72,6 +73,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
name: Notification.Name("checkForUpdates"),
|
||||
object: nil
|
||||
)
|
||||
|
||||
// Initialize and start HTTP server
|
||||
let serverPort = UserDefaults.standard.integer(forKey: "httpServerPort")
|
||||
httpServer = TunnelServerDemo(port: serverPort > 0 ? serverPort : 8080)
|
||||
|
||||
Task {
|
||||
do {
|
||||
try await httpServer?.start()
|
||||
print("HTTP server started automatically on port \(httpServer?.port ?? 8080)")
|
||||
} catch {
|
||||
print("Failed to start HTTP server: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setHTTPServer(_ server: TunnelServerDemo?) {
|
||||
httpServer = server
|
||||
}
|
||||
|
||||
private func handleSingleInstanceCheck() {
|
||||
|
|
@ -121,6 +139,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
}
|
||||
|
||||
func applicationWillTerminate(_ notification: Notification) {
|
||||
// Stop HTTP server
|
||||
Task {
|
||||
try? await httpServer?.stop()
|
||||
}
|
||||
|
||||
// Remove distributed notification observer
|
||||
let processInfo = ProcessInfo.processInfo
|
||||
let isRunningInTests = processInfo.environment["XCTestConfigurationFilePath"] != nil
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@ APP_DOMAIN = vibetunnel.sh
|
|||
GITHUB_URL = https://github.com/amantus-ai/vibetunnel
|
||||
|
||||
// Bundle identifier
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.amantus.vibetunnel
|
||||
PRODUCT_BUNDLE_IDENTIFIER = sh.vibetunnel.vibetunnel
|
||||
37
docs/CODE_SIGNING_SETUP.md
Normal file
37
docs/CODE_SIGNING_SETUP.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Code Signing Setup for VibeTunnel
|
||||
|
||||
This project uses xcconfig files to manage developer team settings, allowing multiple developers to work on the project without constantly changing the code signing configuration in git.
|
||||
|
||||
## Initial Setup
|
||||
|
||||
1. Copy the template file to create your local configuration:
|
||||
```bash
|
||||
cp VibeTunnel/Local.xcconfig.template VibeTunnel/Local.xcconfig
|
||||
```
|
||||
|
||||
2. Edit `VibeTunnel/Local.xcconfig` and add your personal development team ID:
|
||||
```
|
||||
DEVELOPMENT_TEAM = YOUR_TEAM_ID_HERE
|
||||
```
|
||||
|
||||
You can find your team ID in Xcode:
|
||||
- Open Xcode → Preferences (or Settings on newer versions)
|
||||
- Go to Accounts tab
|
||||
- Select your Apple ID
|
||||
- Look for your Team ID in the team details
|
||||
|
||||
3. Open the project in Xcode. It should now use your personal development team automatically.
|
||||
|
||||
## How It Works
|
||||
|
||||
- `Shared.xcconfig` - Contains shared configuration and includes the local settings
|
||||
- `Local.xcconfig` - Your personal settings (ignored by git)
|
||||
- `Local.xcconfig.template` - Template for new developers
|
||||
|
||||
The project is configured to use these xcconfig files for code signing settings, so each developer can have their own `Local.xcconfig` without affecting others.
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Never commit `Local.xcconfig` to git (it's already in .gitignore)
|
||||
- If you need to override other settings locally, you can add them to your `Local.xcconfig`
|
||||
- The xcconfig files are automatically loaded by Xcode when you open the project
|
||||
|
|
@ -93,6 +93,12 @@ if [ -d "$APP_BUNDLE/Contents/Frameworks" ]; then
|
|||
done
|
||||
fi
|
||||
|
||||
# Sign embedded binaries (like tty-fwd)
|
||||
if [ -f "$APP_BUNDLE/Contents/Resources/tty-fwd" ]; then
|
||||
log "Signing tty-fwd binary..."
|
||||
codesign --force --options runtime --sign "$SIGN_IDENTITY" $KEYCHAIN_OPTS "$APP_BUNDLE/Contents/Resources/tty-fwd" || log "Warning: Failed to sign tty-fwd"
|
||||
fi
|
||||
|
||||
# Sign the main executable
|
||||
log "Signing main executable..."
|
||||
codesign --force --options runtime --entitlements "$TMP_ENTITLEMENTS" --sign "$SIGN_IDENTITY" $KEYCHAIN_OPTS "$APP_BUNDLE/Contents/MacOS/VibeTunnel" || true
|
||||
|
|
|
|||
19
test-server.sh
Executable file
19
test-server.sh
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "Testing VibeTunnel HTTP Server..."
|
||||
echo
|
||||
|
||||
# Test root endpoint
|
||||
echo "Testing root endpoint (/):"
|
||||
curl -s http://localhost:8080/ | head -20
|
||||
echo
|
||||
|
||||
# Test health endpoint
|
||||
echo "Testing health endpoint (/health):"
|
||||
curl -s -w "\nHTTP Status: %{http_code}\n" http://localhost:8080/health
|
||||
echo
|
||||
|
||||
# Test info endpoint
|
||||
echo "Testing info endpoint (/info):"
|
||||
curl -s http://localhost:8080/info | jq .
|
||||
echo
|
||||
15
test-sessions-endpoint.sh
Executable file
15
test-sessions-endpoint.sh
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "Testing /sessions endpoint..."
|
||||
echo ""
|
||||
|
||||
# Test if the server is running and call the sessions endpoint
|
||||
response=$(curl -s -w "\nHTTP_STATUS:%{http_code}" http://localhost:8080/sessions)
|
||||
|
||||
# Extract the body and status
|
||||
body=$(echo "$response" | sed -n '1,/^HTTP_STATUS:/p' | sed '$d')
|
||||
status=$(echo "$response" | grep "HTTP_STATUS:" | cut -d: -f2)
|
||||
|
||||
echo "HTTP Status: $status"
|
||||
echo "Response Body:"
|
||||
echo "$body" | jq . 2>/dev/null || echo "$body"
|
||||
Loading…
Reference in a new issue