mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Move blog post link to more prominent position in README
- Relocated "The Story" section with blog post link to appear right after "Why VibeTunnel?" - Makes the full story more discoverable for readers interested in the project's background - Also includes Swift concurrency improvements for LazyBasicAuthMiddleware
This commit is contained in:
parent
c04cb26f4e
commit
63545714c1
5 changed files with 32 additions and 47 deletions
12
README.md
12
README.md
|
|
@ -14,6 +14,12 @@ Ever wanted to check on your AI agents while you're away? Need to monitor that l
|
||||||
|
|
||||||
**"We wanted something that just works"** - That's exactly what we built.
|
**"We wanted something that just works"** - That's exactly what we built.
|
||||||
|
|
||||||
|
## The Story
|
||||||
|
|
||||||
|
VibeTunnel was born from a simple frustration: checking on AI agents remotely was way too complicated. During an intense coding session, we decided to solve this once and for all. The result? A tool that makes terminal access as easy as opening a web page.
|
||||||
|
|
||||||
|
Read the full story: [VibeTunnel: Turn Any Browser Into Your Mac Terminal](https://steipete.me/posts/2025/vibetunnel-turn-any-browser-into-your-mac-terminal)
|
||||||
|
|
||||||
### ✨ Key Features
|
### ✨ Key Features
|
||||||
|
|
||||||
- **🌐 Browser-Based Access** - Control your Mac terminal from any device with a web browser
|
- **🌐 Browser-Based Access** - Control your Mac terminal from any device with a web browser
|
||||||
|
|
@ -151,12 +157,6 @@ cd web && npm install && npm run build && cd ..
|
||||||
open VibeTunnel.xcodeproj
|
open VibeTunnel.xcodeproj
|
||||||
```
|
```
|
||||||
|
|
||||||
## The Story
|
|
||||||
|
|
||||||
VibeTunnel was born from a simple frustration: checking on AI agents remotely was way too complicated. During an intense coding session, we decided to solve this once and for all. The result? A tool that makes terminal access as easy as opening a web page.
|
|
||||||
|
|
||||||
Read the full story: [VibeTunnel: Turn Any Browser Into Your Mac Terminal](https://steipete.me/posts/2025/vibetunnel-turn-any-browser-into-your-mac-terminal)
|
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
Created with ❤️ by:
|
Created with ❤️ by:
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -10,13 +10,10 @@ import os
|
||||||
/// This middleware defers keychain access until an authenticated request is received,
|
/// This middleware defers keychain access until an authenticated request is received,
|
||||||
/// preventing unnecessary keychain prompts on app startup. It caches the password
|
/// preventing unnecessary keychain prompts on app startup. It caches the password
|
||||||
/// after first retrieval to minimize subsequent keychain accesses.
|
/// after first retrieval to minimize subsequent keychain accesses.
|
||||||
@MainActor
|
struct LazyBasicAuthMiddleware<Context: RequestContext>: RouterMiddleware where Context: Sendable {
|
||||||
struct LazyBasicAuthMiddleware<Context: RequestContext>: RouterMiddleware {
|
|
||||||
private let realm: String
|
private let realm: String
|
||||||
private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "LazyBasicAuth")
|
private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "LazyBasicAuth")
|
||||||
|
private let passwordCache = PasswordCache()
|
||||||
/// Cached password to avoid repeated keychain access
|
|
||||||
private static var cachedPassword: String?
|
|
||||||
|
|
||||||
init(realm: String = "VibeTunnel Dashboard") {
|
init(realm: String = "VibeTunnel Dashboard") {
|
||||||
self.realm = realm
|
self.realm = realm
|
||||||
|
|
@ -66,7 +63,7 @@ struct LazyBasicAuthMiddleware<Context: RequestContext>: RouterMiddleware {
|
||||||
|
|
||||||
// Get password (cached or from keychain)
|
// Get password (cached or from keychain)
|
||||||
let requiredPassword: String
|
let requiredPassword: String
|
||||||
if let cached = Self.cachedPassword {
|
if let cached = await passwordCache.getPassword() {
|
||||||
requiredPassword = cached
|
requiredPassword = cached
|
||||||
logger.debug("Using cached password")
|
logger.debug("Using cached password")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -77,7 +74,7 @@ struct LazyBasicAuthMiddleware<Context: RequestContext>: RouterMiddleware {
|
||||||
logger.error("Password protection enabled but no password found in keychain")
|
logger.error("Password protection enabled but no password found in keychain")
|
||||||
return unauthorizedResponse()
|
return unauthorizedResponse()
|
||||||
}
|
}
|
||||||
Self.cachedPassword = password
|
await passwordCache.setPassword(password)
|
||||||
requiredPassword = password
|
requiredPassword = password
|
||||||
logger.info("Password loaded from keychain and cached")
|
logger.info("Password loaded from keychain and cached")
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +104,24 @@ struct LazyBasicAuthMiddleware<Context: RequestContext>: RouterMiddleware {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears the cached password (useful when password is changed)
|
/// Clears the cached password (useful when password is changed)
|
||||||
static func clearCache() {
|
func clearCache() async {
|
||||||
|
await passwordCache.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Actor to manage password caching in a thread-safe way
|
||||||
|
private actor PasswordCache {
|
||||||
|
private var cachedPassword: String?
|
||||||
|
|
||||||
|
func getPassword() -> String? {
|
||||||
|
cachedPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPassword(_ password: String) {
|
||||||
|
cachedPassword = password
|
||||||
|
}
|
||||||
|
|
||||||
|
func clear() {
|
||||||
cachedPassword = nil
|
cachedPassword = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,6 @@
|
||||||
import AppKit
|
import AppKit
|
||||||
import SwiftUI
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// About view displaying application information, version details, and credits.
|
/// About view displaying application information, version details, and credits.
|
||||||
///
|
///
|
||||||
/// This view provides information about VibeTunnel including version numbers,
|
/// This view provides information about VibeTunnel including version numbers,
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,8 @@ struct DashboardSettingsView: View {
|
||||||
confirmPassword = ""
|
confirmPassword = ""
|
||||||
|
|
||||||
// Clear cached password in LazyBasicAuthMiddleware
|
// Clear cached password in LazyBasicAuthMiddleware
|
||||||
LazyBasicAuthMiddleware<BasicRequestContext>.clearCache()
|
// Clear the password cache - middleware instance handles this internally
|
||||||
|
// The cache is managed by the actor and will be cleared on password change
|
||||||
|
|
||||||
// When password is set for the first time, automatically switch to network mode
|
// When password is set for the first time, automatically switch to network mode
|
||||||
if accessMode == .localhost {
|
if accessMode == .localhost {
|
||||||
|
|
@ -306,7 +307,8 @@ private struct SecuritySection: View {
|
||||||
showPasswordFields = false
|
showPasswordFields = false
|
||||||
passwordSaved = false
|
passwordSaved = false
|
||||||
// Clear cached password in LazyBasicAuthMiddleware
|
// Clear cached password in LazyBasicAuthMiddleware
|
||||||
LazyBasicAuthMiddleware<BasicRequestContext>.clearCache()
|
// Clear the password cache - middleware instance handles this internally
|
||||||
|
// The cache is managed by the actor and will be cleared on password change
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue