Fix cross-platform build issues in web package

Replace shell commands that fail on Windows with Node.js scripts:
- mkdir -p / cp -r → scripts/copy-assets.js
- rm -rf → scripts/clean.js
- Add scripts/ensure-dirs.js for directory creation

This resolves "A subdirectory or file -p already exists" errors
when running npm scripts on Windows with Git Bash.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mario Zechner 2025-06-19 01:43:27 +02:00
parent 83a4bf0f75
commit 6ae43fbde8
5 changed files with 588 additions and 5 deletions

532
API.md Normal file
View file

@ -0,0 +1,532 @@
# VibeTunnel API Analysis
## Summary
This document analyzes the API endpoints implemented across all VibeTunnel servers, what the web client expects, and identifies critical differences, implementation errors, and semantic inconsistencies. The analysis covers:
1. **Node.js/TypeScript Server** (`web/src/server.ts`) - ✅ Complete
2. **Rust API Server** (`tty-fwd/src/api_server.rs`) - ✅ Complete
3. **Go Server** (`linux/pkg/api/server.go`) - ✅ Complete
4. **Swift Server** (`VibeTunnel/Core/Services/TunnelServer.swift`) - ✅ Complete
5. **Web Client** (`web/src/client/`) - Expected API calls and formats
**Note**: Rust HTTP Server (`tty-fwd/src/http_server.rs`) is excluded as it's a utility component for static file serving, not a standalone API server.
## API Endpoint Comparison
| Endpoint | Client Expects | Node.js | Rust API | Go | Swift | Status |
|----------|----------------|---------|----------|----|---------| ------|
| `GET /api/health` | ✅ Used | ✅ | ✅ | ✅ | ✅ | ✅ **Complete** |
| `GET /api/sessions` | ✅ **Critical** | ✅ | ✅ | ✅ | ✅ | ✅ **Complete** |
| `POST /api/sessions` | ✅ **Critical** | ✅ | ✅ | ✅ | ✅ | ✅ **Complete** |
| `DELETE /api/sessions/:id` | ✅ **Critical** | ✅ | ✅ | ✅ | ✅ | ✅ **Complete** |
| `DELETE /api/sessions/:id/cleanup` | ❌ Not used | ✅ | ✅ | ✅ | ✅ | ✅ **Complete** |
| `GET /api/sessions/:id/stream` | ✅ **Critical SSE** | ✅ | ✅ | ✅ | ✅ | ✅ **Complete** |
| `GET /api/sessions/:id/snapshot` | ✅ **Critical** | ✅ | ✅ | ✅ | ✅ | ✅ **Complete** |
| `POST /api/sessions/:id/input` | ✅ **Critical** | ✅ | ✅ | ✅ | ✅ | ✅ **Complete** |
| `POST /api/sessions/:id/resize` | ✅ **Critical** | ✅ | ❌ | ✅ | ❌ | ⚠️ **Missing in Rust API & Swift** |
| `POST /api/cleanup-exited` | ✅ Used | ✅ | ✅ | ✅ | ✅ | ✅ **Complete** |
| `GET /api/fs/browse` | ✅ **Critical** | ✅ | ✅ | ✅ | ✅ | ✅ **Complete** |
| `POST /api/mkdir` | ✅ Used | ✅ | ✅ | ✅ | ✅ | ✅ **Complete** |
| `GET /api/sessions/multistream` | ❌ Not used | ✅ | ✅ | ✅ | ✅ | ✅ **Complete** |
| `GET /api/pty/status` | ❌ Not used | ✅ | ❌ | ❌ | ❌ | **Node.js Only** |
| `GET /api/test-cast` | ❌ Not used | ✅ | ❌ | ❌ | ❌ | **Node.js Only** |
| `POST /api/ngrok/start` | ❌ Not used | ❌ | ❌ | ✅ | ✅ | **Go/Swift Only** |
| `POST /api/ngrok/stop` | ❌ Not used | ❌ | ❌ | ✅ | ✅ | **Go/Swift Only** |
| `GET /api/ngrok/status` | ❌ Not used | ❌ | ❌ | ✅ | ✅ | **Go/Swift Only** |
## Web Client API Requirements
Based on analysis of `web/src/client/`, the client **requires** these endpoints to function:
### Critical Endpoints (App breaks without these):
1. `GET /api/sessions` - Session list (polled every 3s)
2. `POST /api/sessions` - Session creation
3. `DELETE /api/sessions/:id` - Session termination
4. `GET /api/sessions/:id/stream` - **SSE streaming** (real-time terminal output)
5. `GET /api/sessions/:id/snapshot` - Terminal snapshot for initial display
6. `POST /api/sessions/:id/input` - **Keyboard/mouse input** to terminal
7. `POST /api/sessions/:id/resize` - **Terminal resize** (debounced, 250ms)
8. `GET /api/fs/browse` - Directory browsing for session creation
9. `POST /api/cleanup-exited` - Cleanup exited sessions
### Expected Request/Response Formats by Client:
#### Session List Response (GET /api/sessions):
```typescript
Session[] = {
id: string;
command: string;
workingDir: string;
name?: string;
status: 'running' | 'exited';
exitCode?: number;
startedAt: string;
lastModified: string;
pid?: number;
waiting?: boolean; // Node.js only
width?: number; // Go only
height?: number; // Go only
}[]
```
#### Session Creation Request (POST /api/sessions):
```typescript
{
command: string[]; // Required: parsed command array
workingDir: string; // Required: working directory path
name?: string; // Optional: session name
spawn_terminal?: boolean; // Used by Rust API/Swift (always true)
width?: number; // Used by Go (default: 120)
height?: number; // Used by Go (default: 30)
}
```
#### Session Input Request (POST /api/sessions/:id/input):
```typescript
{
text: string; // Input text or special keys: 'enter', 'escape', 'arrow_up', etc.
}
```
#### Terminal Resize Request (POST /api/sessions/:id/resize):
```typescript
{
width: number; // Terminal columns
height: number; // Terminal rows
}
```
## Major Implementation Differences
### 1. **Server Implementation Status**
All API servers are **fully functional and complete**:
**Rust API Server** (`tty-fwd/src/api_server.rs`):
- ✅ **Purpose**: Full terminal session management server
- ✅ **APIs**: Complete implementation of all session endpoints
- ✅ **Features**: Authentication, SSE streaming, file system APIs
- ❌ **Missing**: Terminal resize endpoint only
**Architecture Note**: The Rust HTTP Server (`tty-fwd/src/http_server.rs`) is a utility component for static file serving and HTTP/SSE primitives, not a standalone API server. It's correctly excluded from this analysis.
### 2. **CRITICAL: Missing Terminal Resize API**
**Impact**: ⚠️ **Client expects this endpoint and calls it continuously**
**Affected**: Rust API Server, Swift Server
**Endpoints**: `POST /api/sessions/:id/resize`
**Client Behavior**:
- Calls resize endpoint on window resize events (debounced 250ms)
- Tracks last sent dimensions to avoid redundant requests
- Logs warnings on failure but continues operation
- **Will cause 404 errors** on Rust API and Swift servers
**Working Implementation Analysis**:
```javascript
// Node.js Implementation (✅ Complete)
app.post('/api/sessions/:sessionId/resize', async (req, res) => {
const { width, height } = req.body;
// Validation: 1-1000 range
if (width < 1 || height < 1 || width > 1000 || height > 1000) {
return res.status(400).json({ error: 'Width and height must be between 1 and 1000' });
}
ptyService.resizeSession(sessionId, width, height);
});
```
```go
// Go Implementation (✅ Complete)
func (s *Server) handleResizeSession(w http.ResponseWriter, r *http.Request) {
// Includes validation for positive integers
if req.Width <= 0 || req.Height <= 0 {
http.Error(w, "Width and height must be positive integers", http.StatusBadRequest)
return
}
}
```
**Missing in**:
- Rust API Server: No resize endpoint
- Swift Server: No resize endpoint
### 3. **Session Creation Request Format Inconsistencies**
#### Node.js Format:
```json
{
"command": ["bash", "-l"],
"workingDir": "/path/to/dir",
"name": "session_name"
}
```
#### Rust API Format:
```json
{
"command": ["bash", "-l"],
"workingDir": "/path/to/dir",
"term": "xterm-256color",
"spawn_terminal": true
}
```
#### Go Format:
```json
{
"name": "session_name",
"command": ["bash", "-l"],
"workingDir": "/path/to/dir",
"width": 120,
"height": 30
}
```
#### Swift Format:
```json
{
"command": ["bash", "-l"],
"workingDir": "/path/to/dir",
"term": "xterm-256color",
"spawnTerminal": true
}
```
**Issues**:
1. Inconsistent field naming (`workingDir` vs `working_dir`)
2. Different optional fields across implementations
3. Terminal dimensions only in Go implementation
### 4. **Authentication Implementation Differences**
| Server | Auth Method | Details |
|--------|-------------|---------|
| Node.js | None | No authentication middleware |
| Rust API | Basic Auth | Configurable password, realm="tty-fwd" |
| Go | Basic Auth | Fixed username "admin", realm="VibeTunnel" |
| Swift | Basic Auth | Lazy keychain-based password loading |
**Problems**:
1. Different realm names (`"tty-fwd"` vs `"VibeTunnel"`)
2. Inconsistent username requirements
3. Node.js completely lacks authentication
### 5. **Session Input Handling Inconsistencies**
#### Special Key Mappings Differ:
**Node.js**:
```javascript
const specialKeys = [
'arrow_up', 'arrow_down', 'arrow_left', 'arrow_right',
'escape', 'enter', 'ctrl_enter', 'shift_enter'
];
```
**Go**:
```go
specialKeys := map[string]string{
"arrow_up": "\x1b[A",
"arrow_down": "\x1b[B",
"arrow_right": "\x1b[C",
"arrow_left": "\x1b[D",
"escape": "\x1b",
"enter": "\r", // CR, not LF
"ctrl_enter": "\r", // CR for ctrl+enter
"shift_enter": "\x1b\x0d", // ESC + CR for shift+enter
}
```
**Swift**:
```swift
let specialKeys = [
"arrow_up", "arrow_down", "arrow_left", "arrow_right",
"escape", "enter", "ctrl_enter", "shift_enter"
]
```
**Issues**:
1. Go provides explicit escape sequence mappings
2. Node.js and Swift rely on PTY service for mapping
3. Different enter key handling (`\r` vs `\n`)
### 6. **Session Response Format Inconsistencies**
#### Node.js Session List Response:
```json
{
"id": "session-123",
"command": "bash -l",
"workingDir": "/home/user",
"name": "my-session",
"status": "running",
"exitCode": null,
"startedAt": "2024-01-01T00:00:00Z",
"lastModified": "2024-01-01T00:01:00Z",
"pid": 1234,
"waiting": false
}
```
#### Rust API Session List Response:
```json
{
"id": "session-123",
"command": "bash -l",
"workingDir": "/home/user",
"status": "running",
"exitCode": null,
"startedAt": "2024-01-01T00:00:00Z",
"lastModified": "2024-01-01T00:01:00Z",
"pid": 1234
}
```
**Differences**:
1. Node.js includes `name` and `waiting` fields
2. Rust API missing these fields
3. Field naming inconsistencies across servers
### 7. **File System API Response Format Differences**
#### Node.js FS Browse Response:
```json
{
"absolutePath": "/home/user",
"files": [{
"name": "file.txt",
"created": "2024-01-01T00:00:00Z",
"lastModified": "2024-01-01T00:01:00Z",
"size": 1024,
"isDir": false
}]
}
```
#### Go FS Browse Response:
```json
[{
"name": "file.txt",
"path": "/home/user/file.txt",
"is_dir": false,
"size": 1024,
"mode": "-rw-r--r--",
"mod_time": "2024-01-01T00:01:00Z"
}]
```
**Issues**:
1. Different response structures (object vs array)
2. Different field names (`isDir` vs `is_dir`)
3. Go includes additional fields (`path`, `mode`)
4. Missing `created` field in Go
### 8. **Error Response Format Inconsistencies**
#### Node.js Error Format:
```json
{
"error": "Session not found"
}
```
#### Rust API Error Format:
```json
{
"success": null,
"message": null,
"error": "Session not found",
"sessionId": null
}
```
#### Go Simple Error:
```
"Session not found" (plain text)
```
**Problems**:
1. Inconsistent error response structures
2. Some servers use structured responses, others plain text
3. Different HTTP status codes for same error conditions
## Critical Security Issues
### 1. **Inconsistent Authentication**
- Node.js server has NO authentication
- Different authentication realms across servers
- No standardized credential management
### 2. **Path Traversal Vulnerabilities**
Different path sanitization across servers:
**Node.js** (Proper):
```javascript
function resolvePath(inputPath, fallback) {
if (inputPath.startsWith('~')) {
return path.join(os.homedir(), inputPath.slice(1));
}
return path.resolve(inputPath);
}
```
**Go** (Basic):
```go
// Expand ~ in working directory
if cwd != "" && cwd[0] == '~' {
// Simple tilde expansion
}
```
## Missing Features by Server
### Node.js Missing:
- ngrok tunnel management
- Terminal dimensions in session creation
### Rust HTTP Server Missing:
- **ALL API endpoints** (only static file serving)
### Rust API Server Missing:
- Terminal resize functionality
- ngrok tunnel management
### Go Server Missing:
- None (most complete implementation)
### Swift Server Missing:
- Terminal resize functionality
## Recommendations
### 1. **Immediate Fixes Required**
1. **Standardize Request/Response Formats**:
- Use consistent field naming (camelCase vs snake_case)
- Standardize error response structure
- Align session creation request formats
2. **Implement Missing Critical APIs**:
- Add resize endpoint to Rust API and Swift servers
- Add authentication to Node.js server
- Deprecate or complete Rust HTTP server
3. **Fix Security Issues**:
- Standardize authentication realms
- Implement consistent path sanitization
- Add proper input validation
### 2. **Semantic Alignment**
1. **Session Management**:
- Standardize session ID generation
- Align session status values
- Consistent PID handling
2. **Special Key Handling**:
- Standardize escape sequence mappings
- Consistent enter key behavior
- Align special key names
3. **File System Operations**:
- Standardize directory listing format
- Consistent path resolution
- Align file metadata fields
### 3. **Architecture Improvements**
1. **API Versioning**:
- Implement `/api/v1/` prefix
- Version all endpoint contracts
- Plan backward compatibility
2. **Error Handling**:
- Standardize HTTP status codes
- Consistent error response format
- Proper error categorization
3. **Documentation**:
- OpenAPI/Swagger specifications
- API contract testing
- Cross-server compatibility tests
## Rust Server Architecture Analysis
After deeper analysis, the Rust servers have a clear separation of concerns:
### Rust Session Management (`tty-fwd/src/sessions.rs`)
**Complete session management implementation**:
- `list_sessions()` - ✅ Full session listing with status checking
- `send_key_to_session()` - ✅ Special key input (arrow keys, enter, escape, etc.)
- `send_text_to_session()` - ✅ Text input to sessions
- `send_signal_to_session()` - ✅ Signal sending (SIGTERM, SIGKILL, etc.)
- `cleanup_sessions()` - ✅ Session cleanup with PID validation
- `spawn_command()` - ✅ New session creation
- ✅ Process monitoring and zombie reaping
- ✅ Pipe-based I/O with timeout protection
### Rust Protocol Support (`tty-fwd/src/protocol.rs`)
**Complete streaming and protocol support**:
- ✅ Asciinema format reading/writing
- ✅ SSE streaming with `StreamingIterator`
- ✅ Terminal escape sequence processing
- ✅ Real-time event streaming with file monitoring
- ✅ UTF-8 handling and buffering
### Main Binary (`tty-fwd/src/main.rs`)
**Complete CLI interface**:
- ✅ Session listing: `--list-sessions`
- ✅ Key input: `--send-key <key>`
- ✅ Text input: `--send-text <text>`
- ✅ Process control: `--signal`, `--stop`, `--kill`
- ✅ Cleanup: `--cleanup`
- ✅ **HTTP Server**: `--serve <addr>` (launches API server)
**Key Finding**: `tty-fwd --serve` launches the **API server**, not the HTTP server.
## Corrected Assessment
### Rust Implementation Status: ✅ **COMPLETE AND CORRECT**
**All servers are properly implemented**:
1. **Node.js Server**: ✅ Complete - PTY service wrapper
2. **Rust HTTP Server**: ✅ Complete - Utility HTTP server (not meant for direct client use)
3. **Rust API Server**: ✅ Complete - Full session management server
4. **Go Server**: ✅ Complete - Native session management
5. **Swift Server**: ✅ Complete - Wraps tty-fwd binary
### Remaining Issues (Reduced Severity):
1. **Terminal Resize Missing** (Rust API, Swift) - Client compatibility issue
2. **Request/Response Format Inconsistencies** - Client needs adaptation
3. **Authentication Differences** - Security/compatibility issue
## Updated Recommendations
### 1. **Immediate Priority: Terminal Resize**
Add resize endpoint to Rust API and Swift servers:
```rust
// Rust API Server needs:
POST /api/sessions/{sessionId}/resize
```
### 2. **Response Format Standardization**
Align session list responses across all servers for client compatibility.
### 3. **Authentication Standardization**
Implement consistent Basic Auth across all servers.
## Conclusion
**Previous Assessment Correction**: The Rust servers are **fully functional and complete**. The HTTP server is correctly designed as a utility component, while the API server provides full session management.
**Current Status**: 4 out of 5 servers are **client-compatible**. Only missing terminal resize in Rust API and Swift servers.
**Impact**: Much lower than initially assessed. The main issues are:
1. **Terminal resize functionality** - causes 404s but client continues working
2. **Response format variations** - may cause field mapping issues
3. **Authentication inconsistencies** - different security models
The project has **solid API coverage** across all platforms with minor compatibility issues rather than fundamental implementation gaps.

View file

@ -8,14 +8,15 @@
"watch:server": "tsx watch src/server.ts",
"watch:css": "npx tailwindcss -i ./src/input.css -o ./public/bundle/output.css --watch",
"watch:assets": "chokidar 'src/client/assets/**/*' -c 'npm run bundle:assets'",
"clean": "rm -rf public/* && rm -rf dist/",
"clean": "node scripts/clean.js",
"build": "npm run bundle",
"typecheck": "tsc --noEmit",
"ensure:dirs": "node scripts/ensure-dirs.js",
"bundle": "npm run clean && npm run bundle:assets && npm run bundle:css && npm run bundle:client && npm run bundle:test",
"bundle:assets": "mkdir -p public && cp -r src/client/assets/* public/",
"bundle:css": "mkdir -p public/bundle && npx tailwindcss -i ./src/input.css -o ./public/bundle/output.css --minify",
"bundle:client": "mkdir -p public/bundle && esbuild src/client/app-entry.ts --bundle --outfile=public/bundle/client-bundle.js --format=esm --sourcemap",
"bundle:test": "mkdir -p public/bundle && esbuild src/client/test-terminals-entry.ts --bundle --outfile=public/bundle/terminal.js --format=esm --sourcemap",
"bundle:assets": "node scripts/copy-assets.js",
"bundle:css": "npm run ensure:dirs && npx tailwindcss -i ./src/input.css -o ./public/bundle/output.css --minify",
"bundle:client": "npm run ensure:dirs && esbuild src/client/app-entry.ts --bundle --outfile=public/bundle/client-bundle.js --format=esm --sourcemap",
"bundle:test": "npm run ensure:dirs && esbuild src/client/test-terminals-entry.ts --bundle --outfile=public/bundle/terminal.js --format=esm --sourcemap",
"start": "node dist/server.js",
"lint": "eslint 'src/**/*.{ts,tsx}'",
"lint:fix": "eslint 'src/**/*.{ts,tsx}' --fix",

15
web/scripts/clean.js Normal file
View file

@ -0,0 +1,15 @@
const fs = require('fs');
const path = require('path');
function removeRecursive(dirPath) {
if (fs.existsSync(dirPath)) {
fs.rmSync(dirPath, { recursive: true, force: true });
console.log(`Removed: ${dirPath}`);
}
}
// Clean public and dist directories
removeRecursive('public');
removeRecursive('dist');
console.log('Clean completed successfully');

View file

@ -0,0 +1,20 @@
const fs = require('fs');
const path = require('path');
// Ensure public directory exists
fs.mkdirSync('public', { recursive: true });
// Copy assets
const srcDir = 'src/client/assets';
const destDir = 'public';
if (fs.existsSync(srcDir)) {
fs.readdirSync(srcDir).forEach(file => {
const srcPath = path.join(srcDir, file);
const destPath = path.join(destDir, file);
fs.cpSync(srcPath, destPath, { recursive: true });
});
console.log('Assets copied successfully');
} else {
console.log('No assets directory found, skipping copy');
}

View file

@ -0,0 +1,15 @@
const fs = require('fs');
const path = require('path');
// Ensure all required directories exist
const dirs = [
'public',
'public/bundle'
];
dirs.forEach(dir => {
fs.mkdirSync(dir, { recursive: true });
console.log(`Ensured directory exists: ${dir}`);
});
console.log('All directories created successfully');