mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +00:00
510 lines
No EOL
14 KiB
Markdown
510 lines
No EOL
14 KiB
Markdown
<!-- Generated: 2025-07-18 11:30:00 UTC -->
|
|
# VibeTunnel Development Guide
|
|
|
|
## Overview
|
|
|
|
VibeTunnel follows modern Swift 6 and TypeScript development practices with a focus on async/await patterns, protocol-oriented design, and reactive UI architectures. The codebase is organized into three main components: macOS app (Swift/SwiftUI), iOS app (Swift/SwiftUI), and web dashboard (TypeScript/Lit).
|
|
|
|
Key architectural principles:
|
|
- **Protocol-oriented design** for flexibility and testability
|
|
- **Async/await** throughout for clean asynchronous code
|
|
- **Observable pattern** for reactive state management
|
|
- **Dependency injection** via environment values in SwiftUI
|
|
|
|
## Code Style
|
|
|
|
### Swift Conventions
|
|
|
|
**Modern Swift 6 patterns** - From `mac/VibeTunnel/Core/Services/ServerManager.swift`:
|
|
```swift
|
|
@MainActor
|
|
@Observable
|
|
class ServerManager {
|
|
@MainActor static let shared = ServerManager()
|
|
|
|
private(set) var serverType: ServerType = .bun
|
|
private(set) var isSwitchingServer = false
|
|
|
|
var port: String {
|
|
get { UserDefaults.standard.string(forKey: "serverPort") ?? "4020" }
|
|
set { UserDefaults.standard.set(newValue, forKey: "serverPort") }
|
|
}
|
|
}
|
|
```
|
|
|
|
**Error handling** - From `mac/VibeTunnel/Core/Protocols/VibeTunnelServer.swift`:
|
|
```swift
|
|
enum ServerError: LocalizedError {
|
|
case binaryNotFound(String)
|
|
case startupFailed(String)
|
|
case portInUse(Int)
|
|
case invalidConfiguration(String)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .binaryNotFound(let binary):
|
|
return "Server binary not found: \(binary)"
|
|
case .startupFailed(let reason):
|
|
return "Server failed to start: \(reason)"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**SwiftUI view patterns** - From `mac/VibeTunnel/Presentation/Views/Settings/GeneralSettingsView.swift`:
|
|
```swift
|
|
struct GeneralSettingsView: View {
|
|
@AppStorage("autostart")
|
|
private var autostart = false
|
|
|
|
@State private var isCheckingForUpdates = false
|
|
|
|
private let startupManager = StartupManager()
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
Section {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Toggle("Launch at Login", isOn: launchAtLoginBinding)
|
|
Text("Automatically start VibeTunnel when you log in.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### TypeScript Conventions
|
|
|
|
**Class-based services** - From `web/src/server/services/buffer-aggregator.ts`:
|
|
```typescript
|
|
interface BufferAggregatorConfig {
|
|
terminalManager: TerminalManager;
|
|
remoteRegistry: RemoteRegistry | null;
|
|
isHQMode: boolean;
|
|
}
|
|
|
|
export class BufferAggregator {
|
|
private config: BufferAggregatorConfig;
|
|
private remoteConnections: Map<string, RemoteWebSocketConnection> = new Map();
|
|
|
|
constructor(config: BufferAggregatorConfig) {
|
|
this.config = config;
|
|
}
|
|
|
|
async handleClientConnection(ws: WebSocket): Promise<void> {
|
|
console.log(chalk.blue('[BufferAggregator] New client connected'));
|
|
// ...
|
|
}
|
|
}
|
|
```
|
|
|
|
**Lit components** - From `web/src/client/components/vibe-terminal-buffer.ts`:
|
|
```typescript
|
|
@customElement('vibe-terminal-buffer')
|
|
export class VibeTerminalBuffer extends LitElement {
|
|
// Disable shadow DOM for Tailwind compatibility
|
|
createRenderRoot() {
|
|
return this as unknown as HTMLElement;
|
|
}
|
|
|
|
@property({ type: String }) sessionId = '';
|
|
@state() private buffer: BufferSnapshot | null = null;
|
|
@state() private error: string | null = null;
|
|
}
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### Service Architecture
|
|
|
|
**Protocol-based services** - Services define protocols for testability:
|
|
```swift
|
|
// mac/VibeTunnel/Core/Protocols/VibeTunnelServer.swift
|
|
@MainActor
|
|
protocol VibeTunnelServer: AnyObject {
|
|
var isRunning: Bool { get }
|
|
var port: String { get set }
|
|
var logStream: AsyncStream<ServerLogEntry> { get }
|
|
|
|
func start() async throws
|
|
func stop() async
|
|
func checkHealth() async -> Bool
|
|
}
|
|
```
|
|
|
|
**Singleton managers** - Core services use thread-safe singletons:
|
|
```swift
|
|
// mac/VibeTunnel/Core/Services/ServerManager.swift:14
|
|
@MainActor static let shared = ServerManager()
|
|
|
|
// ios/VibeTunnel/Services/APIClient.swift:93
|
|
static let shared = APIClient()
|
|
```
|
|
|
|
### Async/Await Patterns
|
|
|
|
**Swift async operations** - From `ios/VibeTunnel/Services/APIClient.swift`:
|
|
```swift
|
|
func getSessions() async throws -> [Session] {
|
|
guard let url = makeURL(path: "/api/sessions") else {
|
|
throw APIError.invalidURL
|
|
}
|
|
|
|
let (data, response) = try await session.data(from: url)
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
throw APIError.invalidResponse
|
|
}
|
|
|
|
if httpResponse.statusCode != 200 {
|
|
throw APIError.serverError(httpResponse.statusCode, nil)
|
|
}
|
|
|
|
return try decoder.decode([Session].self, from: data)
|
|
}
|
|
```
|
|
|
|
**TypeScript async patterns** - From `web/src/server/services/buffer-aggregator.ts`:
|
|
```typescript
|
|
async handleClientMessage(
|
|
clientWs: WebSocket,
|
|
data: { type: string; sessionId?: string }
|
|
): Promise<void> {
|
|
const subscriptions = this.clientSubscriptions.get(clientWs);
|
|
if (!subscriptions) return;
|
|
|
|
if (data.type === 'subscribe' && data.sessionId) {
|
|
// Handle subscription
|
|
}
|
|
}
|
|
```
|
|
|
|
### Error Handling
|
|
|
|
**Swift error enums** - Comprehensive error types with localized descriptions:
|
|
```swift
|
|
// ios/VibeTunnel/Services/APIClient.swift:4-70
|
|
enum APIError: LocalizedError {
|
|
case invalidURL
|
|
case serverError(Int, String?)
|
|
case networkError(Error)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .serverError(let code, let message):
|
|
if let message { return message }
|
|
switch code {
|
|
case 400: return "Bad request"
|
|
case 401: return "Unauthorized"
|
|
default: return "Server error: \(code)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**TypeScript error handling** - Structured error responses:
|
|
```typescript
|
|
// web/src/server/middleware/auth.ts
|
|
try {
|
|
// Operation
|
|
} catch (error) {
|
|
console.error('[Auth] Error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal server error',
|
|
message: error instanceof Error ? error.message : 'Unknown error'
|
|
});
|
|
}
|
|
```
|
|
|
|
### State Management
|
|
|
|
**SwiftUI Observable** - From `mac/VibeTunnel/Core/Services/ServerManager.swift`:
|
|
```swift
|
|
@Observable
|
|
class ServerManager {
|
|
private(set) var isRunning = false
|
|
private(set) var isRestarting = false
|
|
private(set) var lastError: Error?
|
|
}
|
|
```
|
|
|
|
**AppStorage for persistence**:
|
|
```swift
|
|
// mac/VibeTunnel/Presentation/Views/Settings/GeneralSettingsView.swift:5
|
|
@AppStorage("autostart") private var autostart = false
|
|
@AppStorage("updateChannel") private var updateChannelRaw = UpdateChannel.stable.rawValue
|
|
```
|
|
|
|
### UI Patterns
|
|
|
|
**SwiftUI form layouts** - From `mac/VibeTunnel/Presentation/Views/Settings/GeneralSettingsView.swift`:
|
|
```swift
|
|
Form {
|
|
Section {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Toggle("Launch at Login", isOn: launchAtLoginBinding)
|
|
Text("Description")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
} header: {
|
|
Text("Application")
|
|
.font(.headline)
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
```
|
|
|
|
**Lit reactive properties**:
|
|
```typescript
|
|
// web/src/client/components/vibe-terminal-buffer.ts:22-24
|
|
@property({ type: String }) sessionId = '';
|
|
@state() private buffer: BufferSnapshot | null = null;
|
|
@state() private error: string | null = null;
|
|
```
|
|
|
|
## Workflows
|
|
|
|
### Adding a New Service
|
|
|
|
1. **Define the protocol** in `mac/VibeTunnel/Core/Protocols/`:
|
|
```swift
|
|
@MainActor
|
|
protocol MyServiceProtocol {
|
|
func performAction() async throws
|
|
}
|
|
```
|
|
|
|
2. **Implement the service** in `mac/VibeTunnel/Core/Services/`:
|
|
```swift
|
|
@MainActor
|
|
class MyService: MyServiceProtocol {
|
|
static let shared = MyService()
|
|
|
|
func performAction() async throws {
|
|
// Implementation
|
|
}
|
|
}
|
|
```
|
|
|
|
3. **Add to environment** if needed in `mac/VibeTunnel/Core/Extensions/EnvironmentValues+Services.swift`
|
|
|
|
### Creating UI Components
|
|
|
|
**SwiftUI views** follow this pattern:
|
|
```swift
|
|
struct MyView: View {
|
|
@Environment(\.myService) private var service
|
|
@State private var isLoading = false
|
|
|
|
var body: some View {
|
|
// View implementation
|
|
}
|
|
}
|
|
```
|
|
|
|
**Lit components** use decorators:
|
|
```typescript
|
|
@customElement('my-component')
|
|
export class MyComponent extends LitElement {
|
|
@property({ type: String }) value = '';
|
|
|
|
render() {
|
|
return html`<div>${this.value}</div>`;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Testing Patterns
|
|
|
|
**Swift unit tests** - From `mac/VibeTunnelTests/ServerManagerTests.swift`:
|
|
```swift
|
|
@MainActor
|
|
final class ServerManagerTests: XCTestCase {
|
|
override func setUp() async throws {
|
|
await super.setUp()
|
|
// Setup
|
|
}
|
|
|
|
func testServerStart() async throws {
|
|
let manager = ServerManager.shared
|
|
await manager.start()
|
|
XCTAssertTrue(manager.isRunning)
|
|
}
|
|
}
|
|
```
|
|
|
|
**TypeScript tests** use Vitest:
|
|
```typescript
|
|
// web/src/test/setup.ts
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
describe('BufferAggregator', () => {
|
|
it('should handle client connections', async () => {
|
|
// Test implementation
|
|
});
|
|
});
|
|
```
|
|
|
|
## Reference
|
|
|
|
### File Organization
|
|
|
|
**Swift packages**:
|
|
- `mac/VibeTunnel/Core/` - Core business logic, protocols, services
|
|
- `mac/VibeTunnel/Presentation/` - SwiftUI views and view models
|
|
- `mac/VibeTunnel/Utilities/` - Helper classes and extensions
|
|
- `ios/VibeTunnel/Services/` - iOS-specific services
|
|
- `ios/VibeTunnel/Views/` - iOS UI components
|
|
|
|
**TypeScript modules**:
|
|
- `web/src/client/` - Frontend components and utilities
|
|
- `web/src/server/` - Backend services and routes
|
|
- `web/src/server/pty/` - Terminal handling
|
|
- `web/src/test/` - Test files and utilities
|
|
|
|
### Naming Conventions
|
|
|
|
**Swift**:
|
|
- Services: `*Manager`, `*Service` (e.g., `ServerManager`, `APIClient`)
|
|
- Protocols: `*Protocol`, `*able` (e.g., `VibeTunnelServer`, `HTTPClientProtocol`)
|
|
- Views: `*View` (e.g., `GeneralSettingsView`, `TerminalView`)
|
|
- Errors: `*Error` enum (e.g., `ServerError`, `APIError`)
|
|
|
|
**TypeScript**:
|
|
- Services: `*Service`, `*Manager` (e.g., `BufferAggregator`, `TerminalManager`)
|
|
- Components: `vibe-*` custom elements (e.g., `vibe-terminal-buffer`)
|
|
- Types: PascalCase interfaces (e.g., `BufferSnapshot`, `ServerConfig`)
|
|
|
|
### Common Issues
|
|
|
|
**Port conflicts** - Handled in `mac/VibeTunnel/Core/Utilities/PortConflictResolver.swift`
|
|
**Permission management** - See `mac/VibeTunnel/Core/Services/*PermissionManager.swift`
|
|
**WebSocket reconnection** - Implemented in `ios/VibeTunnel/Services/BufferWebSocketClient.swift`
|
|
**Terminal resizing** - Handled in both Swift and TypeScript terminal components
|
|
|
|
### VibeTunnel CLI Wrapper (vt)
|
|
|
|
The `vt` command is a bash wrapper script that allows users to run commands through VibeTunnel's terminal forwarding. It's installed at `/usr/local/bin/vt` when the Mac app is built.
|
|
|
|
**Source location**: `mac/VibeTunnel/vt`
|
|
|
|
**Usage**:
|
|
```bash
|
|
# Run a command through VibeTunnel
|
|
vt ls -la
|
|
|
|
# Run an aliased command (e.g., if 'claude' is an alias)
|
|
vt claude --version
|
|
|
|
# Launch interactive shell
|
|
vt --shell
|
|
vt -i
|
|
|
|
# Run command without shell wrapping (bypass alias resolution)
|
|
vt --no-shell-wrap command
|
|
vt -S command
|
|
```
|
|
|
|
**How it works**:
|
|
1. Locates the VibeTunnel.app bundle (checks standard locations and uses Spotlight if needed)
|
|
2. Finds the `vibetunnel` binary within the app bundle's Resources
|
|
3. Determines if the command is a binary or alias/function
|
|
4. For binaries: executes directly through `vibetunnel fwd`
|
|
5. For aliases/functions: wraps in appropriate shell (`zsh -i -c` or `bash -c`) for proper resolution
|
|
|
|
**Technical Details**:
|
|
- The `--` separator should not be passed to `fwd` as it was being misinterpreted as a command
|
|
- Aliases require interactive shell mode to be resolved properly
|
|
- The script prevents recursive VibeTunnel sessions by checking `VIBETUNNEL_SESSION_ID`
|
|
- The `fwd` binary now properly handles `--` as an argument separator when needed
|
|
|
|
## Web Development
|
|
|
|
### Code Quality Tools
|
|
|
|
VibeTunnel uses several tools to maintain code quality:
|
|
|
|
#### Running All Checks
|
|
|
|
To run all code quality checks (read-only checks run in parallel):
|
|
|
|
```bash
|
|
pnpm run check
|
|
```
|
|
|
|
This runs format checking, linting, and type checking in parallel and reports any issues.
|
|
|
|
#### Individual Tools
|
|
|
|
**Formatting** (Biome):
|
|
```bash
|
|
pnpm run format # Fix formatting issues
|
|
pnpm run format:check # Check formatting without fixing
|
|
```
|
|
|
|
**Linting** (Biome + TypeScript):
|
|
```bash
|
|
pnpm run lint # Check for lint errors
|
|
pnpm run lint:fix # Fix auto-fixable lint errors
|
|
```
|
|
|
|
**Type Checking** (TypeScript):
|
|
```bash
|
|
pnpm run typecheck # Run type checking on all configs
|
|
```
|
|
|
|
#### Auto-fix All Issues
|
|
|
|
To automatically fix all formatting and linting issues:
|
|
|
|
```bash
|
|
pnpm run check:fix
|
|
```
|
|
|
|
This runs format and lint:fix **sequentially** to avoid file conflicts.
|
|
|
|
### Why Sequential Fixes?
|
|
|
|
Running multiple file-modifying tools in parallel can cause race conditions where:
|
|
- Both tools try to write to the same file simultaneously
|
|
- One tool's changes get overwritten by another
|
|
- Git operations fail due to file locks
|
|
|
|
Best practices from the JavaScript community recommend:
|
|
1. **Parallel for checks**: Read-only operations can run simultaneously
|
|
2. **Sequential for fixes**: File modifications should happen one after another
|
|
3. **Biome as unified tool**: Reduces conflicts by combining formatting and linting
|
|
|
|
### Why Multiple Tools?
|
|
|
|
1. **Biome**: Fast, modern formatter and linter for JavaScript/TypeScript
|
|
2. **TypeScript**: Type checking across server, client, and service worker contexts
|
|
3. **Parallel execution**: Saves time by running independent checks simultaneously
|
|
|
|
### Tips for Faster Development
|
|
|
|
1. **Use `pnpm run check` before committing** - Catches all issues at once
|
|
2. **Enable format-on-save in your editor** - Prevents formatting issues
|
|
3. **Run `pnpm run check:fix` to quickly fix issues** - Handles problems sequentially
|
|
|
|
### Continuous Development
|
|
|
|
When developing, you typically want:
|
|
|
|
```bash
|
|
# Terminal 1: Run the dev server
|
|
pnpm run dev
|
|
|
|
# Terminal 2: Run tests in watch mode (when needed)
|
|
pnpm test
|
|
|
|
# Before committing: Run all checks
|
|
pnpm run check
|
|
``` |