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:
Peter Steinberger 2025-06-16 02:00:56 +02:00
parent 8d80935bdb
commit 99b77c9b53
16 changed files with 785 additions and 13 deletions

5
.gitignore vendored
View file

@ -77,4 +77,7 @@ DerivedData/
*.log
*.bak
*~
.*.swp
.*.swp
# Local Development Settings
Local.xcconfig

View file

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

View file

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

View 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"
}
}
}

View file

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

View 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

Binary file not shown.

View file

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

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

View file

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

View file

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

View 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

View file

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