mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
add docs
This commit is contained in:
parent
5d6d630c61
commit
411b79832d
10 changed files with 110 additions and 12 deletions
|
|
@ -4,7 +4,12 @@ import Hummingbird
|
||||||
import HummingbirdCore
|
import HummingbirdCore
|
||||||
import NIOCore
|
import NIOCore
|
||||||
|
|
||||||
/// Middleware that implements HTTP Basic Authentication
|
/// Middleware that implements HTTP Basic Authentication.
|
||||||
|
///
|
||||||
|
/// Provides password-based access control for the VibeTunnel dashboard.
|
||||||
|
/// Validates incoming requests against a configured password using
|
||||||
|
/// standard HTTP Basic Authentication. Exempts health check endpoints
|
||||||
|
/// from authentication requirements.
|
||||||
struct BasicAuthMiddleware<Context: RequestContext>: RouterMiddleware {
|
struct BasicAuthMiddleware<Context: RequestContext>: RouterMiddleware {
|
||||||
let password: String
|
let password: String
|
||||||
let realm: String
|
let realm: String
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,20 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import Logging
|
import Logging
|
||||||
|
|
||||||
/// Generates Asciinema cast v2 format files from terminal session output
|
/// Generates Asciinema cast v2 format files from terminal session output.
|
||||||
|
///
|
||||||
|
/// Creates recordings of terminal sessions in the Asciinema cast format,
|
||||||
|
/// which can be played back using Asciinema players. Handles timing information,
|
||||||
|
/// terminal dimensions, and output/input event recording.
|
||||||
|
///
|
||||||
/// Format specification: https://docs.asciinema.org/manual/asciicast/v2/
|
/// Format specification: https://docs.asciinema.org/manual/asciicast/v2/
|
||||||
struct CastFileGenerator {
|
struct CastFileGenerator {
|
||||||
private let logger = Logger(label: "VibeTunnel.CastFileGenerator")
|
private let logger = Logger(label: "VibeTunnel.CastFileGenerator")
|
||||||
|
|
||||||
|
/// Header structure for Asciinema cast v2 format.
|
||||||
|
///
|
||||||
|
/// Contains metadata about the terminal recording including
|
||||||
|
/// dimensions, timing, and environment information.
|
||||||
struct CastHeader: Codable {
|
struct CastHeader: Codable {
|
||||||
let version: Int = 2
|
let version: Int = 2
|
||||||
let width: Int
|
let width: Int
|
||||||
|
|
@ -30,6 +39,9 @@ struct CastFileGenerator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a single event in the Asciinema recording.
|
||||||
|
///
|
||||||
|
/// Each event captures either terminal output or input at a specific timestamp.
|
||||||
struct CastEvent {
|
struct CastEvent {
|
||||||
let time: TimeInterval
|
let time: TimeInterval
|
||||||
let eventType: String
|
let eventType: String
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@ import Foundation
|
||||||
import os
|
import os
|
||||||
import Security
|
import Security
|
||||||
|
|
||||||
/// Service for managing dashboard password in keychain
|
/// Service for managing dashboard password in keychain.
|
||||||
|
///
|
||||||
|
/// Provides secure storage and retrieval of the dashboard authentication
|
||||||
|
/// password using the macOS Keychain. Handles password generation,
|
||||||
|
/// updates, and deletion with proper error handling and logging.
|
||||||
@MainActor
|
@MainActor
|
||||||
final class DashboardKeychain {
|
final class DashboardKeychain {
|
||||||
static let shared = DashboardKeychain()
|
static let shared = DashboardKeychain()
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,19 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import HTTPTypes
|
import HTTPTypes
|
||||||
|
|
||||||
/// Protocol for HTTP client abstraction to enable testing
|
/// Protocol for HTTP client abstraction to enable testing.
|
||||||
|
///
|
||||||
|
/// Defines the interface for making HTTP requests, allowing for
|
||||||
|
/// easy mocking and testing of network-dependent code.
|
||||||
public protocol HTTPClientProtocol {
|
public protocol HTTPClientProtocol {
|
||||||
func data(for request: HTTPRequest, body: Data?) async throws -> (Data, HTTPResponse)
|
func data(for request: HTTPRequest, body: Data?) async throws -> (Data, HTTPResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Real HTTP client implementation
|
/// Real HTTP client implementation.
|
||||||
|
///
|
||||||
|
/// Concrete implementation of HTTPClientProtocol using URLSession
|
||||||
|
/// for actual network requests. Converts between HTTPTypes and
|
||||||
|
/// Foundation's URLRequest/URLResponse types.
|
||||||
public final class HTTPClient: HTTPClientProtocol {
|
public final class HTTPClient: HTTPClientProtocol {
|
||||||
private let session: URLSession
|
private let session: URLSession
|
||||||
|
|
||||||
|
|
@ -29,6 +36,7 @@ public final class HTTPClient: HTTPClientProtocol {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Errors that can occur during HTTP client operations.
|
||||||
enum HTTPClientError: Error {
|
enum HTTPClientError: Error {
|
||||||
case invalidResponse
|
case invalidResponse
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ class ServerManager {
|
||||||
private(set) var currentServer: ServerProtocol?
|
private(set) var currentServer: ServerProtocol?
|
||||||
private(set) var isRunning = false
|
private(set) var isRunning = false
|
||||||
private(set) var isSwitching = false
|
private(set) var isSwitching = false
|
||||||
|
private(set) var isRestarting = false
|
||||||
private(set) var lastError: Error?
|
private(set) var lastError: Error?
|
||||||
|
|
||||||
private let logger = Logger(subsystem: "com.steipete.VibeTunnel", category: "ServerManager")
|
private let logger = Logger(subsystem: "com.steipete.VibeTunnel", category: "ServerManager")
|
||||||
|
|
@ -193,11 +194,25 @@ class ServerManager {
|
||||||
))
|
))
|
||||||
|
|
||||||
// Update ServerMonitor for compatibility
|
// Update ServerMonitor for compatibility
|
||||||
ServerMonitor.shared.isServerRunning = false
|
// Only set to false if we're not in the middle of a restart
|
||||||
|
if !isRestarting {
|
||||||
|
ServerMonitor.shared.isServerRunning = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restart the current server
|
/// Restart the current server
|
||||||
func restart() async {
|
func restart() async {
|
||||||
|
// Set restarting flag to prevent UI from showing "stopped" state
|
||||||
|
isRestarting = true
|
||||||
|
defer { isRestarting = false }
|
||||||
|
|
||||||
|
// Log that we're restarting
|
||||||
|
logSubject.send(ServerLogEntry(
|
||||||
|
level: .info,
|
||||||
|
message: "Restarting server...",
|
||||||
|
source: serverMode
|
||||||
|
))
|
||||||
|
|
||||||
await stop()
|
await stop()
|
||||||
await start()
|
await start()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,9 @@ public final class ServerMonitor {
|
||||||
|
|
||||||
/// Syncs state with ServerManager
|
/// Syncs state with ServerManager
|
||||||
private func syncWithServerManager() async {
|
private func syncWithServerManager() async {
|
||||||
isServerRunning = ServerManager.shared.isRunning
|
// Consider the server as running if it's actually running OR if it's restarting
|
||||||
|
// This prevents the UI from showing "stopped" during restart
|
||||||
|
isServerRunning = ServerManager.shared.isRunning || ServerManager.shared.isRestarting
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts the server if not already running
|
/// Starts the server if not already running
|
||||||
|
|
@ -68,7 +70,9 @@ public final class ServerMonitor {
|
||||||
|
|
||||||
/// Restarts the server
|
/// Restarts the server
|
||||||
public func restartServer() async throws {
|
public func restartServer() async throws {
|
||||||
|
// During restart, we maintain the running state to prevent UI flicker
|
||||||
await ServerManager.shared.restart()
|
await ServerManager.shared.restart()
|
||||||
|
// Sync after restart completes
|
||||||
await syncWithServerManager()
|
await syncWithServerManager()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,11 @@ import os.log
|
||||||
import Sparkle
|
import Sparkle
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
/// SparkleUpdaterManager with automatic update downloads enabled
|
/// SparkleUpdaterManager with automatic update downloads enabled.
|
||||||
|
///
|
||||||
|
/// Manages application updates using the Sparkle framework. Handles automatic
|
||||||
|
/// update checking, downloading, and installation while respecting user preferences
|
||||||
|
/// and update channels. Integrates with macOS notifications for update announcements.
|
||||||
@available(macOS 10.15, *)
|
@available(macOS 10.15, *)
|
||||||
@MainActor
|
@MainActor
|
||||||
public final class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate {
|
public final class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import os.log
|
import os.log
|
||||||
|
|
||||||
/// Manages interactions with the tty-fwd command-line tool
|
/// Manages interactions with the tty-fwd command-line tool.
|
||||||
|
///
|
||||||
|
/// Provides a high-level interface for executing the bundled tty-fwd
|
||||||
|
/// binary, handling process management, error conditions, and ensuring
|
||||||
|
/// proper executable permissions. Used for terminal multiplexing operations.
|
||||||
@MainActor
|
@MainActor
|
||||||
final class TTYForwardManager {
|
final class TTYForwardManager {
|
||||||
static let shared = TTYForwardManager()
|
static let shared = TTYForwardManager()
|
||||||
|
|
@ -101,7 +105,10 @@ final class TTYForwardManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Errors that can occur when working with the tty-fwd binary
|
/// Errors that can occur when working with the tty-fwd binary.
|
||||||
|
///
|
||||||
|
/// Represents failures specific to tty-fwd execution including
|
||||||
|
/// missing executable, permission issues, and runtime failures.
|
||||||
enum TTYForwardError: LocalizedError {
|
enum TTYForwardError: LocalizedError {
|
||||||
case executableNotFound
|
case executableNotFound
|
||||||
case notExecutable
|
case notExecutable
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,21 @@ import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import Logging
|
import Logging
|
||||||
|
|
||||||
/// Holds pipes for a terminal session
|
/// Holds pipes for a terminal session.
|
||||||
|
///
|
||||||
|
/// Encapsulates the standard I/O pipes used for communicating
|
||||||
|
/// with a terminal process.
|
||||||
private struct SessionPipes {
|
private struct SessionPipes {
|
||||||
let stdin: Pipe
|
let stdin: Pipe
|
||||||
let stdout: Pipe
|
let stdout: Pipe
|
||||||
let stderr: Pipe
|
let stderr: Pipe
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manages terminal sessions and command execution
|
/// Manages terminal sessions and command execution.
|
||||||
|
///
|
||||||
|
/// An actor that handles the lifecycle of terminal sessions, including
|
||||||
|
/// process creation, I/O handling, and command execution. Provides
|
||||||
|
/// thread-safe management of multiple concurrent terminal sessions.
|
||||||
actor TerminalManager {
|
actor TerminalManager {
|
||||||
private var sessions: [UUID: TunnelSession] = [:]
|
private var sessions: [UUID: TunnelSession] = [:]
|
||||||
private var processes: [UUID: Process] = [:]
|
private var processes: [UUID: Process] = [:]
|
||||||
|
|
|
||||||
32
VibeTunnel/Presentation/Views/SharedComponents.swift
Normal file
32
VibeTunnel/Presentation/Views/SharedComponents.swift
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// MARK: - Credit Link Component
|
||||||
|
|
||||||
|
/// Credit link component for individual contributors.
|
||||||
|
///
|
||||||
|
/// This component displays a contributor's handle as a clickable link
|
||||||
|
/// that opens their website when clicked.
|
||||||
|
struct CreditLink: View {
|
||||||
|
let name: String
|
||||||
|
let url: String
|
||||||
|
@State private var isHovering = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: {
|
||||||
|
if let linkURL = URL(string: url) {
|
||||||
|
NSWorkspace.shared.open(linkURL)
|
||||||
|
}
|
||||||
|
}, label: {
|
||||||
|
Text(name)
|
||||||
|
.font(.caption)
|
||||||
|
.underline(isHovering, color: .accentColor)
|
||||||
|
})
|
||||||
|
.buttonStyle(.link)
|
||||||
|
.pointingHandCursor()
|
||||||
|
.onHover { hovering in
|
||||||
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
|
isHovering = hovering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue