mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +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
|
*.log
|
||||||
*.bak
|
*.bak
|
||||||
*~
|
*~
|
||||||
.*.swp
|
.*.swp
|
||||||
|
|
||||||
|
# Local Development Settings
|
||||||
|
Local.xcconfig
|
||||||
11
README.md
11
README.md
|
|
@ -69,6 +69,17 @@ VibeTunnel/
|
||||||
└── docs/ # Documentation
|
└── 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
|
### Building
|
||||||
|
|
||||||
The project uses standard Xcode build system:
|
The project uses standard Xcode build system:
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,9 @@
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
Info.plist,
|
Info.plist,
|
||||||
|
Local.xcconfig,
|
||||||
|
Local.xcconfig.template,
|
||||||
|
Shared.xcconfig,
|
||||||
version.xcconfig,
|
version.xcconfig,
|
||||||
);
|
);
|
||||||
target = 788687F02DFF4FCB00B22C15 /* VibeTunnel */;
|
target = 788687F02DFF4FCB00B22C15 /* VibeTunnel */;
|
||||||
|
|
@ -430,7 +433,7 @@
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = Y5PE65HELJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
|
@ -444,7 +447,7 @@
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MARKETING_VERSION = 0.1.3;
|
MARKETING_VERSION = 0.1.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.pspdfkit.VibeTunnel;
|
PRODUCT_BUNDLE_IDENTIFIER = sh.vibetunnel.vibetunnel;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|
@ -463,7 +466,7 @@
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = Y5PE65HELJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
|
@ -477,7 +480,7 @@
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MARKETING_VERSION = 0.1.3;
|
MARKETING_VERSION = 0.1.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.pspdfkit.VibeTunnel;
|
PRODUCT_BUNDLE_IDENTIFIER = sh.vibetunnel.vibetunnel;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|
@ -491,11 +494,11 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = 55V3KCX766;
|
DEVELOPMENT_TEAM = "";
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.1;
|
MACOSX_DEPLOYMENT_TARGET = 15.1;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.steipete.VibeTunnelTests;
|
PRODUCT_BUNDLE_IDENTIFIER = sh.vibetunnel.vibetunnelTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|
@ -510,11 +513,11 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = 55V3KCX766;
|
DEVELOPMENT_TEAM = "";
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.1;
|
MACOSX_DEPLOYMENT_TARGET = 15.1;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.steipete.VibeTunnelTests;
|
PRODUCT_BUNDLE_IDENTIFIER = sh.vibetunnel.vibetunnelTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|
@ -528,7 +531,7 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = 55V3KCX766;
|
DEVELOPMENT_TEAM = "";
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.steipete.VibeTunnelUITests;
|
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 Foundation
|
||||||
import Combine
|
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
|
@MainActor
|
||||||
public final class TunnelServerDemo: ObservableObject {
|
public final class TunnelServerDemo: ObservableObject {
|
||||||
@Published public private(set) var isRunning = false
|
@Published public private(set) var isRunning = false
|
||||||
@Published public private(set) var port: Int
|
@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) {
|
public init(port: Int = 8080) {
|
||||||
self.port = port
|
self.port = port
|
||||||
}
|
}
|
||||||
|
|
||||||
public func start() async throws {
|
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 {
|
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
|
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 SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
enum SettingsTab: String, CaseIterable {
|
enum SettingsTab: String, CaseIterable {
|
||||||
case general
|
case general
|
||||||
case advanced
|
case advanced
|
||||||
|
case debug
|
||||||
case about
|
case about
|
||||||
|
|
||||||
var displayName: String {
|
var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .general: "General"
|
case .general: "General"
|
||||||
case .advanced: "Advanced"
|
case .advanced: "Advanced"
|
||||||
|
case .debug: "Debug"
|
||||||
case .about: "About"
|
case .about: "About"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -17,6 +20,7 @@ enum SettingsTab: String, CaseIterable {
|
||||||
switch self {
|
switch self {
|
||||||
case .general: "gear"
|
case .general: "gear"
|
||||||
case .advanced: "gearshape.2"
|
case .advanced: "gearshape.2"
|
||||||
|
case .debug: "hammer"
|
||||||
case .about: "info.circle"
|
case .about: "info.circle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -43,6 +47,12 @@ struct SettingsView: View {
|
||||||
}
|
}
|
||||||
.tag(SettingsTab.advanced)
|
.tag(SettingsTab.advanced)
|
||||||
|
|
||||||
|
DebugSettingsView()
|
||||||
|
.tabItem {
|
||||||
|
Label(SettingsTab.debug.displayName, systemImage: SettingsTab.debug.icon)
|
||||||
|
}
|
||||||
|
.tag(SettingsTab.debug)
|
||||||
|
|
||||||
AboutView()
|
AboutView()
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label(SettingsTab.about.displayName, systemImage: SettingsTab.about.icon)
|
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 {
|
#Preview {
|
||||||
SettingsView()
|
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 {
|
final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
private(set) var sparkleUpdaterManager: SparkleUpdaterManager?
|
private(set) var sparkleUpdaterManager: SparkleUpdaterManager?
|
||||||
private var statusItem: NSStatusItem?
|
private var statusItem: NSStatusItem?
|
||||||
|
private(set) var httpServer: TunnelServerDemo?
|
||||||
|
|
||||||
/// Distributed notification name used to ask an existing instance to show the Settings window.
|
/// Distributed notification name used to ask an existing instance to show the Settings window.
|
||||||
private static let showSettingsNotification = Notification.Name("com.amantus.vibetunnel.showSettings")
|
private static let showSettingsNotification = Notification.Name("com.amantus.vibetunnel.showSettings")
|
||||||
|
|
@ -72,6 +73,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
name: Notification.Name("checkForUpdates"),
|
name: Notification.Name("checkForUpdates"),
|
||||||
object: nil
|
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() {
|
private func handleSingleInstanceCheck() {
|
||||||
|
|
@ -121,6 +139,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationWillTerminate(_ notification: Notification) {
|
func applicationWillTerminate(_ notification: Notification) {
|
||||||
|
// Stop HTTP server
|
||||||
|
Task {
|
||||||
|
try? await httpServer?.stop()
|
||||||
|
}
|
||||||
|
|
||||||
// Remove distributed notification observer
|
// Remove distributed notification observer
|
||||||
let processInfo = ProcessInfo.processInfo
|
let processInfo = ProcessInfo.processInfo
|
||||||
let isRunningInTests = processInfo.environment["XCTestConfigurationFilePath"] != nil
|
let isRunningInTests = processInfo.environment["XCTestConfigurationFilePath"] != nil
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,4 @@ APP_DOMAIN = vibetunnel.sh
|
||||||
GITHUB_URL = https://github.com/amantus-ai/vibetunnel
|
GITHUB_URL = https://github.com/amantus-ai/vibetunnel
|
||||||
|
|
||||||
// Bundle identifier
|
// 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
|
done
|
||||||
fi
|
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
|
# Sign the main executable
|
||||||
log "Signing main executable..."
|
log "Signing main executable..."
|
||||||
codesign --force --options runtime --entitlements "$TMP_ENTITLEMENTS" --sign "$SIGN_IDENTITY" $KEYCHAIN_OPTS "$APP_BUNDLE/Contents/MacOS/VibeTunnel" || true
|
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