Clean up old unused files and components

- Remove old app-new entry points and components
- Remove duplicate/experimental files
- Keep only active session-list and file-browser components

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mario Zechner 2025-06-16 03:50:22 +02:00
parent 920b040b61
commit 0bee6f13aa
5 changed files with 0 additions and 901 deletions

View file

@ -1,2 +0,0 @@
// Entry point for the new app
import './app-new.js';

View file

@ -1,144 +0,0 @@
import { LitElement, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
// Import components
import './components/app-header.js';
import './components/session-create-form.js';
import './components/session-list.js';
import type { Session } from './components/session-list.js';
@customElement('vibetunnel-app-new')
export class VibeTunnelAppNew extends LitElement {
// Disable shadow DOM to use Tailwind
createRenderRoot() {
return this;
}
@state() private errorMessage = '';
@state() private sessions: Session[] = [];
@state() private loading = false;
private hotReloadWs: WebSocket | null = null;
connectedCallback() {
super.connectedCallback();
this.setupHotReload();
this.loadSessions();
this.startAutoRefresh();
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.hotReloadWs) {
this.hotReloadWs.close();
}
}
private showError(message: string) {
this.errorMessage = message;
// Clear error after 5 seconds
setTimeout(() => {
this.errorMessage = '';
}, 5000);
}
private clearError() {
this.errorMessage = '';
}
private async loadSessions() {
this.loading = true;
try {
const response = await fetch('/api/sessions');
if (response.ok) {
const sessionsData = await response.json();
this.sessions = sessionsData.map((session: any) => ({
id: session.id,
command: session.command,
workingDir: session.workingDir,
status: session.status,
exitCode: session.exitCode,
startedAt: session.startedAt,
lastModified: session.lastModified,
pid: session.pid
}));
this.clearError();
} else {
this.showError('Failed to load sessions');
}
} catch (error) {
console.error('Error loading sessions:', error);
this.showError('Failed to load sessions');
} finally {
this.loading = false;
}
}
private startAutoRefresh() {
// Refresh sessions every 3 seconds
setInterval(() => {
this.loadSessions();
}, 3000);
}
private handleSessionCreated(e: CustomEvent) {
console.log('Session created:', e.detail);
this.showError('Session created successfully!');
this.loadSessions(); // Refresh the list
}
private handleSessionSelect(e: CustomEvent) {
const session = e.detail as Session;
console.log('Session selected:', session);
this.showError(`Terminal view not implemented yet for session: ${session.id}`);
}
private handleSessionKilled(e: CustomEvent) {
console.log('Session killed:', e.detail);
this.loadSessions(); // Refresh the list
}
private handleRefresh() {
this.loadSessions();
}
private handleError(e: CustomEvent) {
this.showError(e.detail);
}
private setupHotReload(): void {
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}?hotReload=true`;
this.hotReloadWs = new WebSocket(wsUrl);
this.hotReloadWs.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'reload') {
window.location.reload();
}
};
}
}
render() {
return html`
<div class="max-w-4xl mx-auto">
<app-header></app-header>
<session-create-form
@session-created=${this.handleSessionCreated}
@error=${this.handleError}
></session-create-form>
<session-list
.sessions=${this.sessions}
.loading=${this.loading}
@session-select=${this.handleSessionSelect}
@session-killed=${this.handleSessionKilled}
@refresh=${this.handleRefresh}
@error=${this.handleError}
></session-list>
</div>
`;
}
}

View file

@ -1,17 +0,0 @@
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('app-header')
export class AppHeader extends LitElement {
createRenderRoot() {
return this;
}
render() {
return html`
<div class="p-4">
<h1 class="text-vs-user font-mono text-sm m-0">VibeTunnel</h1>
</div>
`;
}
}

View file

@ -1,183 +0,0 @@
import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import './file-browser.js';
export interface SessionCreateData {
command: string[];
workingDir: string;
}
@customElement('session-create-form')
export class SessionCreateForm extends LitElement {
// Disable shadow DOM to use Tailwind
createRenderRoot() {
return this;
}
@property({ type: String }) workingDir = '~/';
@property({ type: String }) command = '';
@property({ type: Boolean }) disabled = false;
@state() private isCreating = false;
@state() private showFileBrowser = false;
private handleWorkingDirChange(e: Event) {
const input = e.target as HTMLInputElement;
this.workingDir = input.value;
this.dispatchEvent(new CustomEvent('working-dir-change', {
detail: this.workingDir
}));
}
private handleCommandChange(e: Event) {
const input = e.target as HTMLInputElement;
this.command = input.value;
}
private handleBrowse() {
this.showFileBrowser = true;
}
private handleDirectorySelected(e: CustomEvent) {
this.workingDir = e.detail;
this.showFileBrowser = false;
}
private handleBrowserCancel() {
this.showFileBrowser = false;
}
private async handleCreate() {
if (!this.workingDir.trim() || !this.command.trim()) {
this.dispatchEvent(new CustomEvent('error', {
detail: 'Please fill in both working directory and command'
}));
return;
}
this.isCreating = true;
const sessionData: SessionCreateData = {
command: this.parseCommand(this.command.trim()),
workingDir: this.workingDir.trim()
};
try {
const response = await fetch('/api/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sessionData)
});
if (response.ok) {
const result = await response.json();
this.command = ''; // Clear command on success
this.dispatchEvent(new CustomEvent('session-created', {
detail: result
}));
} else {
const error = await response.json();
this.dispatchEvent(new CustomEvent('error', {
detail: `Failed to create session: ${error.error}`
}));
}
} catch (error) {
console.error('Error creating session:', error);
this.dispatchEvent(new CustomEvent('error', {
detail: 'Failed to create session'
}));
} finally {
this.isCreating = false;
}
}
private parseCommand(commandStr: string): string[] {
// Simple command parsing - split by spaces but respect quotes
const args: string[] = [];
let current = '';
let inQuotes = false;
let quoteChar = '';
for (let i = 0; i < commandStr.length; i++) {
const char = commandStr[i];
if ((char === '"' || char === "'") && !inQuotes) {
inQuotes = true;
quoteChar = char;
} else if (char === quoteChar && inQuotes) {
inQuotes = false;
quoteChar = '';
} else if (char === ' ' && !inQuotes) {
if (current) {
args.push(current);
current = '';
}
} else {
current += char;
}
}
if (current) {
args.push(current);
}
return args;
}
render() {
return html`
<div class="border border-vs-accent font-mono text-sm p-4 m-4 rounded">
<div class="text-vs-assistant text-sm mb-4">Create New Session</div>
<div class="mb-4">
<div class="text-vs-muted mb-2">Working Directory:</div>
<div class="flex gap-4">
<input
type="text"
class="flex-1 bg-vs-bg text-vs-text border border-vs-border outline-none font-mono px-4 py-2"
.value=${this.workingDir}
@input=${this.handleWorkingDirChange}
placeholder="~/"
?disabled=${this.disabled || this.isCreating}
/>
<button
class="bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-4 py-2 border-none"
@click=${this.handleBrowse}
?disabled=${this.disabled || this.isCreating}
>
browse
</button>
</div>
</div>
<div class="mb-4">
<div class="text-vs-muted mb-2">Command:</div>
<input
type="text"
class="w-full bg-vs-bg text-vs-text border border-vs-border outline-none font-mono px-4 py-2"
.value=${this.command}
@input=${this.handleCommandChange}
@keydown=${(e: KeyboardEvent) => e.key === 'Enter' && this.handleCreate()}
placeholder="zsh"
?disabled=${this.disabled || this.isCreating}
/>
</div>
<button
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-4 py-2 border-none"
@click=${this.handleCreate}
?disabled=${this.disabled || this.isCreating || !this.workingDir.trim() || !this.command.trim()}
>
${this.isCreating ? 'creating...' : 'create'}
</button>
</div>
<file-browser
.visible=${this.showFileBrowser}
.currentPath=${this.workingDir}
@directory-selected=${this.handleDirectorySelected}
@browser-cancel=${this.handleBrowserCancel}
></file-browser>
`;
}
}

View file

@ -1,555 +0,0 @@
import express from 'express';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import path from 'path';
import fs from 'fs';
import os from 'os';
import { spawn } from 'child_process';
const app = express();
const server = createServer(app);
const wss = new WebSocketServer({ server });
const PORT = process.env.PORT || 3000;
// tty-fwd binary path - check multiple possible locations
const possibleTtyFwdPaths = [
path.resolve(__dirname, '..', '..', 'tty-fwd', 'target', 'release', 'tty-fwd'),
path.resolve(__dirname, '..', '..', '..', 'tty-fwd', 'target', 'release', 'tty-fwd'),
'tty-fwd' // System PATH
];
let TTY_FWD_PATH = '';
for (const pathToCheck of possibleTtyFwdPaths) {
if (fs.existsSync(pathToCheck)) {
TTY_FWD_PATH = pathToCheck;
break;
}
}
if (!TTY_FWD_PATH) {
console.error('tty-fwd binary not found. Please ensure it is built and available.');
process.exit(1);
}
const TTY_FWD_CONTROL_DIR = process.env.TTY_FWD_CONTROL_DIR || path.join(os.homedir(), '.vibetunnel');
// Ensure control directory exists
if (!fs.existsSync(TTY_FWD_CONTROL_DIR)) {
fs.mkdirSync(TTY_FWD_CONTROL_DIR, { recursive: true });
console.log(`Created control directory: ${TTY_FWD_CONTROL_DIR}`);
}
console.log(`Using tty-fwd at: ${TTY_FWD_PATH}`);
console.log(`Control directory: ${TTY_FWD_CONTROL_DIR}`);
// Types for tty-fwd responses
interface TtyFwdSession {
cmdline: string[];
cwd: string;
exit_code: number | null;
name: string;
pid: number;
started_at: string;
status: "running" | "exited";
stdin: string;
"stream-out": string;
}
interface TtyFwdListResponse {
[sessionId: string]: TtyFwdSession;
}
// Helper function to execute tty-fwd commands
async function executeTtyFwd(args: string[]): Promise<string> {
return new Promise((resolve, reject) => {
const child = spawn(TTY_FWD_PATH, args);
let output = '';
child.stdout.on('data', (data) => {
output += data.toString();
});
child.on('close', (code) => {
if (code === 0) {
resolve(output);
} else {
reject(new Error(`tty-fwd failed with code ${code}`));
}
});
child.on('error', (error) => {
reject(error);
});
});
}
// Helper function to resolve paths with ~ expansion
function resolvePath(inputPath: string, fallback?: string): string {
if (!inputPath) {
return fallback || process.cwd();
}
if (inputPath.startsWith('~')) {
return path.join(os.homedir(), inputPath.slice(1));
}
return path.resolve(inputPath);
}
// Middleware
app.use(express.json());
app.use(express.static(path.join(__dirname, '..', 'public')));
// Hot reload functionality for development
const hotReloadClients = new Set<any>();
// === SESSION MANAGEMENT ===
// List all sessions
app.get('/api/sessions', async (req, res) => {
try {
const output = await executeTtyFwd(['--control-path', TTY_FWD_CONTROL_DIR, '--list-sessions']);
const sessions: TtyFwdListResponse = JSON.parse(output || '{}');
const sessionData = Object.entries(sessions).map(([sessionId, sessionInfo]) => {
// Get actual last modified time from stream-out file
let lastModified = sessionInfo.started_at;
try {
if (fs.existsSync(sessionInfo["stream-out"])) {
const stats = fs.statSync(sessionInfo["stream-out"]);
lastModified = stats.mtime.toISOString();
}
} catch (e) {
// Use started_at as fallback
}
return {
id: sessionId,
command: sessionInfo.cmdline.join(' '),
workingDir: sessionInfo.cwd,
status: sessionInfo.status,
exitCode: sessionInfo.exit_code,
startedAt: sessionInfo.started_at,
lastModified: lastModified,
pid: sessionInfo.pid
};
});
// Sort by lastModified, most recent first
sessionData.sort((a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime());
res.json(sessionData);
} catch (error) {
console.error('Failed to list sessions:', error);
res.status(500).json({ error: 'Failed to list sessions' });
}
});
// Create new session
app.post('/api/sessions', async (req, res) => {
try {
const { command, workingDir } = req.body;
if (!command || !Array.isArray(command) || command.length === 0) {
return res.status(400).json({ error: 'Command array is required and cannot be empty' });
}
const sessionName = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const cwd = resolvePath(workingDir, process.cwd());
const args = [
'--control-path', TTY_FWD_CONTROL_DIR,
'--session-name', sessionName,
'--'
].concat(command);
console.log(`Creating session: ${TTY_FWD_PATH} ${args.join(' ')}`);
const child = spawn(TTY_FWD_PATH, args, {
cwd: cwd,
detached: false,
stdio: 'pipe'
});
// Log output for debugging
child.stdout.on('data', (data) => {
console.log(`Session ${sessionName} stdout:`, data.toString());
});
child.stderr.on('data', (data) => {
console.log(`Session ${sessionName} stderr:`, data.toString());
});
child.on('close', (code) => {
console.log(`Session ${sessionName} exited with code: ${code}`);
});
// Respond immediately - don't wait for completion
res.json({ sessionId: sessionName });
} catch (error) {
console.error('Error creating session:', error);
res.status(500).json({ error: 'Failed to create session' });
}
});
// Kill session (just kill the process)
app.delete('/api/sessions/:sessionId', async (req, res) => {
const sessionId = req.params.sessionId;
try {
const output = await executeTtyFwd(['--control-path', TTY_FWD_CONTROL_DIR, '--list-sessions']);
const sessions: TtyFwdListResponse = JSON.parse(output || '{}');
const session = sessions[sessionId];
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
if (session.pid) {
try {
process.kill(session.pid, 'SIGTERM');
setTimeout(() => {
try {
process.kill(session.pid, 0); // Check if still alive
process.kill(session.pid, 'SIGKILL'); // Force kill
} catch (e) {
// Process already dead
}
}, 1000);
} catch (error) {
// Process already dead
}
}
res.json({ success: true, message: 'Session killed' });
} catch (error) {
console.error('Error killing session:', error);
res.status(500).json({ error: 'Failed to kill session' });
}
});
// Cleanup session files
app.delete('/api/sessions/:sessionId/cleanup', async (req, res) => {
const sessionId = req.params.sessionId;
try {
await executeTtyFwd([
'--control-path', TTY_FWD_CONTROL_DIR,
'--session', sessionId,
'--cleanup'
]);
res.json({ success: true, message: 'Session cleaned up' });
} catch (error) {
// If tty-fwd cleanup fails, force remove directory
console.log('tty-fwd cleanup failed, force removing directory');
const sessionDir = path.join(TTY_FWD_CONTROL_DIR, sessionId);
try {
if (fs.existsSync(sessionDir)) {
fs.rmSync(sessionDir, { recursive: true, force: true });
}
res.json({ success: true, message: 'Session force cleaned up' });
} catch (fsError) {
console.error('Error force removing session directory:', fsError);
res.status(500).json({ error: 'Failed to cleanup session' });
}
}
});
// === TERMINAL I/O ===
// Server-sent events for terminal output streaming
app.get('/api/sessions/:sessionId/stream', (req, res) => {
const sessionId = req.params.sessionId;
const streamOutPath = path.join(TTY_FWD_CONTROL_DIR, sessionId, 'stream-out');
if (!fs.existsSync(streamOutPath)) {
return res.status(404).json({ error: 'Session not found' });
}
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Cache-Control'
});
const startTime = Date.now() / 1000;
let headerSent = false;
// Send existing content first
// NOTE: Small race condition possible between reading file and starting tail
try {
const content = fs.readFileSync(streamOutPath, 'utf8');
const lines = content.trim().split('\n');
for (const line of lines) {
if (line.trim()) {
try {
const parsed = JSON.parse(line);
if (parsed.version && parsed.width && parsed.height) {
res.write(`data: ${line}\n\n`);
headerSent = true;
} else if (Array.isArray(parsed) && parsed.length >= 3) {
const instantEvent = [0, parsed[1], parsed[2]];
res.write(`data: ${JSON.stringify(instantEvent)}\n\n`);
}
} catch (e) {
// Skip invalid lines
}
}
}
} catch (error) {
console.error('Error reading existing content:', error);
}
// Send default header if none found
if (!headerSent) {
const defaultHeader = {
version: 2,
width: 80,
height: 24,
timestamp: Math.floor(startTime),
env: { TERM: "xterm-256color" }
};
res.write(`data: ${JSON.stringify(defaultHeader)}\n\n`);
}
// Stream new content
const tailProcess = spawn('tail', ['-f', streamOutPath]);
let buffer = '';
tailProcess.stdout.on('data', (chunk) => {
buffer += chunk.toString();
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim()) {
try {
const parsed = JSON.parse(line);
if (parsed.version && parsed.width && parsed.height) {
return; // Skip duplicate headers
}
if (Array.isArray(parsed) && parsed.length >= 3) {
const currentTime = Date.now() / 1000;
const realTimeEvent = [currentTime - startTime, parsed[1], parsed[2]];
res.write(`data: ${JSON.stringify(realTimeEvent)}\n\n`);
}
} catch (e) {
// Handle non-JSON as raw output
const currentTime = Date.now() / 1000;
const castEvent = [currentTime - startTime, "o", line];
res.write(`data: ${JSON.stringify(castEvent)}\n\n`);
}
}
}
});
// Cleanup on disconnect
req.on('close', () => tailProcess.kill('SIGTERM'));
req.on('aborted', () => tailProcess.kill('SIGTERM'));
});
// Get session snapshot (asciinema cast with adjusted timestamps for immediate playback)
app.get('/api/sessions/:sessionId/snapshot', (req, res) => {
const sessionId = req.params.sessionId;
const streamOutPath = path.join(TTY_FWD_CONTROL_DIR, sessionId, 'stream-out');
if (!fs.existsSync(streamOutPath)) {
return res.status(404).json({ error: 'Session not found' });
}
try {
const content = fs.readFileSync(streamOutPath, 'utf8');
const lines = content.trim().split('\n');
let header = null;
const events = [];
let startTime = null;
for (const line of lines) {
if (line.trim()) {
try {
const parsed = JSON.parse(line);
// Header line
if (parsed.version && parsed.width && parsed.height) {
header = parsed;
}
// Event line [timestamp, type, data]
else if (Array.isArray(parsed) && parsed.length >= 3) {
if (startTime === null) {
startTime = parsed[0];
}
// Adjust timestamp to start from 0 and compress time
const adjustedTime = (parsed[0] - startTime) * 0.1; // 10x speed
events.push([adjustedTime, parsed[1], parsed[2]]);
}
} catch (e) {
// Skip invalid lines
}
}
}
// Build the complete asciinema cast
const cast = [];
// Add header if found, otherwise use default
if (header) {
cast.push(JSON.stringify(header));
} else {
cast.push(JSON.stringify({
version: 2,
width: 80,
height: 24,
timestamp: Math.floor(Date.now() / 1000),
env: { TERM: "xterm-256color" }
}));
}
// Add all events
events.forEach(event => {
cast.push(JSON.stringify(event));
});
res.setHeader('Content-Type', 'text/plain');
res.send(cast.join('\n'));
} catch (error) {
console.error('Error reading session snapshot:', error);
res.status(500).json({ error: 'Failed to read session snapshot' });
}
});
// Send input to session
app.post('/api/sessions/:sessionId/input', async (req, res) => {
const sessionId = req.params.sessionId;
const { text } = req.body;
if (text === undefined || text === null) {
return res.status(400).json({ error: 'Text is required' });
}
console.log(`Sending input to session ${sessionId}:`, JSON.stringify(text));
try {
// Check if this is a special key that should use --send-key
const specialKeys = ['arrow_up', 'arrow_down', 'arrow_left', 'arrow_right', 'escape', 'enter'];
const isSpecialKey = specialKeys.includes(text);
if (isSpecialKey) {
await executeTtyFwd([
'--control-path', TTY_FWD_CONTROL_DIR,
'--session', sessionId,
'--send-key', text
]);
console.log(`Successfully sent key: ${text}`);
} else {
await executeTtyFwd([
'--control-path', TTY_FWD_CONTROL_DIR,
'--session', sessionId,
'--send-text', text
]);
console.log(`Successfully sent text: ${text}`);
}
res.json({ success: true });
} catch (error) {
console.error('Error sending input via tty-fwd:', error);
res.status(500).json({ error: 'Failed to send input' });
}
});
// === FILE SYSTEM ===
// Directory listing for file browser
app.get('/api/fs/browse', (req, res) => {
const dirPath = req.query.path as string || '~';
try {
const expandedPath = resolvePath(dirPath, '~');
if (!fs.existsSync(expandedPath)) {
return res.status(404).json({ error: 'Directory not found' });
}
const stats = fs.statSync(expandedPath);
if (!stats.isDirectory()) {
return res.status(400).json({ error: 'Path is not a directory' });
}
const files = fs.readdirSync(expandedPath).map(name => {
const filePath = path.join(expandedPath, name);
const fileStats = fs.statSync(filePath);
return {
name,
created: fileStats.birthtime.toISOString(),
lastModified: fileStats.mtime.toISOString(),
size: fileStats.size,
isDir: fileStats.isDirectory()
};
});
res.json({
absolutePath: expandedPath,
files: files.sort((a, b) => {
// Directories first, then files
if (a.isDir && !b.isDir) return -1;
if (!a.isDir && b.isDir) return 1;
return a.name.localeCompare(b.name);
})
});
} catch (error) {
console.error('Error listing directory:', error);
res.status(500).json({ error: 'Failed to list directory' });
}
});
// === WEBSOCKETS ===
// WebSocket for hot reload
wss.on('connection', (ws, req) => {
const url = new URL(req.url!, `http://${req.headers.host}`);
const isHotReload = url.searchParams.get('hotReload') === 'true';
if (isHotReload) {
hotReloadClients.add(ws);
ws.on('close', () => {
hotReloadClients.delete(ws);
});
return;
}
ws.close(1008, 'Only hot reload connections supported');
});
// Hot reload file watching in development
if (process.env.NODE_ENV !== 'production') {
const chokidar = require('chokidar');
const watcher = chokidar.watch(['public/**/*', 'src/**/*'], {
ignored: /node_modules/,
persistent: true
});
watcher.on('change', (path: string) => {
console.log(`File changed: ${path}`);
hotReloadClients.forEach((ws: any) => {
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify({ type: 'reload' }));
}
});
});
}
server.listen(PORT, () => {
console.log(`VibeTunnel New Server running on http://localhost:${PORT}`);
console.log(`Using tty-fwd: ${TTY_FWD_PATH}`);
});