mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-06-29 05:39:31 +00:00
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:
parent
e59fc9d835
commit
7bffb2ae7b
10 changed files with 142 additions and 96 deletions
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
86
VibeTunnel/Utilities/AnimatedSettingsWindow.swift
Normal file
86
VibeTunnel/Utilities/AnimatedSettingsWindow.swift
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue