Add tty-fwd universal binary build step to release process

- Integrate tty-fwd universal binary build into build.sh
- Automatically build and copy tty-fwd-universal to Resources folder
- Ensure binary is executable and included in app bundle
- Update release process to build universal binary for Intel and Apple Silicon
This commit is contained in:
Peter Steinberger 2025-06-16 03:19:17 +02:00
parent e59fc9d835
commit 7bffb2ae7b
10 changed files with 142 additions and 96 deletions

View file

@ -10,7 +10,7 @@ final class TTYForwardManager {
/// Returns the URL to the bundled tty-fwd executable
var ttyForwardExecutableURL: URL? {
return Bundle.main.url(forResource: "tty-fwd", withExtension: nil, subdirectory: "Resources")
return Bundle.main.url(forResource: "tty-fwd", withExtension: nil)
}
/// Executes the tty-fwd binary with the specified arguments

View file

@ -89,7 +89,7 @@ public final class TunnelServer {
</head>
<body>
<h1>VibeTunnel Server</h1>
<p class="status"> Server is running on port \(portNumber)</p>
<p class="status">Server is running on port \(portNumber)</p>
<p>Available endpoints:</p>
<ul>
<li><a href="/health">/health</a> - Health check</li>

View file

@ -64,12 +64,12 @@ struct AboutView: View {
title: "Report an Issue",
icon: "exclamationmark.bubble"
)
HoverableLink(url: "https://x.com/steipete", title: "Follow @steipete on Twitter", icon: "bird")
HoverableLink(url: "https://x.com/VibeTunnel", title: "Follow @VibeTunnel", icon: "bird")
}
}
private var copyrightSection: some View {
Text("© 2025 Amantus AI • MIT Licensed")
Text("© 2025 VibeTunnel Team • MIT Licensed")
.font(.footnote)
.foregroundStyle(.secondary)
.padding(.bottom, 32)

View file

@ -32,6 +32,7 @@ extension Notification.Name {
struct SettingsView: View {
@State private var selectedTab: SettingsTab = .general
@State private var contentSize: CGSize = .zero
@AppStorage("debugMode") private var debugMode = false
// Define ideal sizes for each tab
private let tabSizes: [SettingsTab: CGSize] = [
@ -55,11 +56,13 @@ struct SettingsView: View {
}
.tag(SettingsTab.advanced)
DebugSettingsView()
.tabItem {
Label(SettingsTab.debug.displayName, systemImage: SettingsTab.debug.icon)
}
.tag(SettingsTab.debug)
if debugMode {
DebugSettingsView()
.tabItem {
Label(SettingsTab.debug.displayName, systemImage: SettingsTab.debug.icon)
}
.tag(SettingsTab.debug)
}
AboutView()
.tabItem {
@ -68,7 +71,7 @@ struct SettingsView: View {
.tag(SettingsTab.about)
}
.frame(width: contentSize.width, height: contentSize.height)
.animatedWindowResizing(size: contentSize)
.animatedWindowContainer(size: contentSize)
.onReceive(NotificationCenter.default.publisher(for: .openSettingsTab)) { notification in
if let tab = notification.object as? SettingsTab {
selectedTab = tab
@ -80,6 +83,12 @@ struct SettingsView: View {
.onAppear {
contentSize = tabSizes[selectedTab] ?? CGSize(width: 500, height: 400)
}
.onChange(of: debugMode) { _, _ in
// If debug mode is disabled and we're on the debug tab, switch to general
if !debugMode && selectedTab == .debug {
selectedTab = .general
}
}
}
}
@ -214,7 +223,6 @@ struct AdvancedSettingsView: View {
.buttonStyle(.bordered)
.disabled(isCheckingForUpdates)
}
.padding(.top, 8)
} header: {
Text("Updates")
.font(.headline)
@ -421,7 +429,7 @@ struct DebugSettingsView: View {
Section {
// API Endpoints with test functionality
VStack(alignment: .leading, spacing: 12) {
ForEach(apiEndpoints, id: \.path) { endpoint in
ForEach(apiEndpoints) { endpoint in
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(endpoint.method)
@ -614,11 +622,20 @@ struct DebugSettingsView: View {
}
// API Endpoint data
struct APIEndpoint {
struct APIEndpoint: Identifiable {
let id: String
let method: String
let path: String
let description: String
let isTestable: Bool
init(method: String, path: String, description: String, isTestable: Bool) {
self.id = "\(method)_\(path)"
self.method = method
self.path = path
self.description = description
self.isTestable = isTestable
}
}
let apiEndpoints = [

View file

@ -0,0 +1,86 @@
import SwiftUI
import AppKit
/// A view that enables animated window resizing for Settings windows
struct AnimatedWindowContainer<Content: View>: NSViewRepresentable {
let content: Content
let targetSize: CGSize
class Coordinator: NSObject {
var lastSize: CGSize = .zero
weak var window: NSWindow?
var isAnimating = false
func animateWindowResize(to newSize: CGSize) {
guard let window = window,
newSize != lastSize,
!isAnimating else { return }
lastSize = newSize
isAnimating = true
// Calculate the new frame maintaining the window's top-left position
var newFrame = window.frame
let heightDifference = newSize.height - newFrame.height
newFrame.size = newSize
newFrame.origin.y -= heightDifference // Keep top edge in place
// Animate the window frame change
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.25
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
window.animator().setFrame(newFrame, display: true)
}, completionHandler: {
self.isAnimating = false
})
}
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeNSView(context: Context) -> NSView {
let view = NSView()
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.clear.cgColor
// Use async to ensure window is available
DispatchQueue.main.async {
if let window = view.window {
context.coordinator.window = window
// Disable automatic window resizing
window.styleMask.remove(.resizable)
// Set initial size
context.coordinator.lastSize = window.frame.size
}
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
// Ensure we have a window
if context.coordinator.window == nil, let window = nsView.window {
context.coordinator.window = window
window.styleMask.remove(.resizable)
context.coordinator.lastSize = window.frame.size
}
// Animate to the new size
context.coordinator.animateWindowResize(to: targetSize)
}
}
/// Extension to make it easy to use with any SwiftUI view
extension View {
func animatedWindowContainer(size: CGSize) -> some View {
background(
AnimatedWindowContainer(
content: Color.clear,
targetSize: size
)
.allowsHitTesting(false)
)
}
}

View file

@ -1,71 +0,0 @@
import AppKit
import SwiftUI
/// A window delegate that handles animated resizing of the settings window
class SettingsWindowDelegate: NSObject, NSWindowDelegate {
static let shared = SettingsWindowDelegate()
private override init() {
super.init()
}
/// Animates the window to a new size
func animateWindowResize(to newSize: CGSize, duration: TimeInterval = 0.3) {
guard let window = NSApp.windows.first(where: { $0.title.contains("Settings") || $0.title.contains("Preferences") }) else {
return
}
// Calculate the new frame maintaining the window's top-left position
var newFrame = window.frame
let heightDifference = newSize.height - newFrame.height
newFrame.size = newSize
newFrame.origin.y -= heightDifference // Keep top edge in place
// Animate the frame change
NSAnimationContext.runAnimationGroup { context in
context.duration = duration
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
window.animator().setFrame(newFrame, display: true)
}
}
func windowWillClose(_ notification: Notification) {
// Clean up if needed
}
}
/// A view modifier that sets up the window delegate for animated resizing
struct AnimatedWindowResizing: ViewModifier {
let size: CGSize
func body(content: Content) -> some View {
content
.onAppear {
setupWindowDelegate()
// Initial resize without animation
SettingsWindowDelegate.shared.animateWindowResize(to: size, duration: 0)
}
.onChange(of: size) { _, newSize in
SettingsWindowDelegate.shared.animateWindowResize(to: newSize)
}
}
private func setupWindowDelegate() {
Task { @MainActor in
// Small delay to ensure window is created
try? await Task.sleep(for: .milliseconds(100))
if let window = NSApp.windows.first(where: { $0.title.contains("Settings") || $0.title.contains("Preferences") }) {
window.delegate = SettingsWindowDelegate.shared
// Disable window resizing by user
window.styleMask.remove(.resizable)
}
}
}
}
extension View {
func animatedWindowResizing(size: CGSize) -> some View {
modifier(AnimatedWindowResizing(size: size))
}
}

View file

@ -3,12 +3,6 @@
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<false/>
</dict>
</plist>

View file

@ -27,7 +27,6 @@
# DEPENDENCIES:
# - Xcode and command line tools
# - xcbeautify (optional, for prettier output)
# - Generated Xcode project (run generate-xcproj.sh first)
#
# EXAMPLES:
# ./scripts/build.sh # Release build
@ -73,6 +72,28 @@ echo "Code signing: $SIGN_APP"
# Clean build directory only if it doesn't exist
mkdir -p "$BUILD_DIR"
# Build tty-fwd universal binary
echo "🔨 Building tty-fwd universal binary..."
if [[ -x "$PROJECT_DIR/tty-fwd/build-universal.sh" ]]; then
cd "$PROJECT_DIR/tty-fwd"
./build-universal.sh
# Verify the binary was built
if [[ -f "$PROJECT_DIR/tty-fwd/target/release/tty-fwd-universal" ]]; then
echo "✓ tty-fwd universal binary built successfully"
# Copy to Resources folder for inclusion in app bundle
cp "$PROJECT_DIR/tty-fwd/target/release/tty-fwd-universal" "$PROJECT_DIR/VibeTunnel/Resources/tty-fwd"
chmod +x "$PROJECT_DIR/VibeTunnel/Resources/tty-fwd"
echo "✓ Copied tty-fwd universal binary to Resources folder"
else
echo "Error: Failed to build tty-fwd universal binary"
exit 1
fi
else
echo "Error: tty-fwd build script not found at $PROJECT_DIR/tty-fwd/build-universal.sh"
exit 1
fi
# Build the app
cd "$PROJECT_DIR"

View file

@ -32,7 +32,7 @@
#
# DEPENDENCIES:
# - preflight-check.sh (validates release readiness)
# - generate-xcproj.sh (Tuist project generation)
# - Xcode workspace and project files
# - build.sh (application building)
# - sign-and-notarize.sh (code signing and notarization)
# - create-dmg.sh (DMG creation)
@ -142,10 +142,9 @@ echo " Build: $BUILD_NUMBER"
echo " Tag: $TAG_NAME"
echo ""
# Step 2: Clean and generate project
echo -e "${BLUE}📋 Step 2/7: Generating Xcode project...${NC}"
# Step 2: Clean build directory
echo -e "${BLUE}📋 Step 2/7: Cleaning build directory...${NC}"
rm -rf "$PROJECT_ROOT/build"
"$SCRIPT_DIR/generate-xcproj.sh"
# Check if Xcode project was modified and commit if needed
if ! git diff --quiet "$PROJECT_ROOT/VibeTunnel.xcodeproj/project.pbxproj"; then