From e4794133630ebfbd7ca85a21aab7099ce8b84e8e Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 24 Nov 2025 02:41:03 -0800 Subject: [PATCH] feat: add mulTmux collaborative terminal tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add mulTmux as an integrated workspace in canvas-website project: - Node.js/TypeScript backend with tmux session management - CLI client with blessed-based terminal UI - WebSocket-based real-time collaboration - Token-based authentication with invite links - Session management (create, join, list) - PM2 deployment scripts for AI server - nginx reverse proxy configuration - Workspace integration with npm scripts Usage: - npm run multmux:build - Build server and CLI - npm run multmux:start - Start production server - multmux create - Create collaborative session - multmux join - Join existing session See MULTMUX_INTEGRATION.md for full documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- MULTMUX_INTEGRATION.md | 232 +++++++++++++++++ multmux/.gitignore | 35 +++ multmux/README.md | 240 ++++++++++++++++++ multmux/infrastructure/deploy.sh | 91 +++++++ multmux/infrastructure/nginx.conf | 53 ++++ multmux/package.json | 19 ++ multmux/packages/cli/package.json | 30 +++ multmux/packages/cli/src/commands/create.ts | 50 ++++ multmux/packages/cli/src/commands/join.ts | 45 ++++ multmux/packages/cli/src/commands/list.ts | 38 +++ .../cli/src/connection/WebSocketClient.ts | 120 +++++++++ multmux/packages/cli/src/index.ts | 34 +++ multmux/packages/cli/src/ui/Terminal.ts | 154 +++++++++++ multmux/packages/cli/tsconfig.json | 8 + multmux/packages/server/package.json | 26 ++ multmux/packages/server/src/api/routes.ts | 96 +++++++ multmux/packages/server/src/index.ts | 55 ++++ .../server/src/managers/SessionManager.ts | 114 +++++++++ .../server/src/managers/TokenManager.ts | 50 ++++ multmux/packages/server/src/types/index.ts | 29 +++ .../server/src/websocket/TerminalHandler.ts | 175 +++++++++++++ multmux/packages/server/tsconfig.json | 8 + multmux/tsconfig.json | 18 ++ package.json | 10 +- 24 files changed, 1729 insertions(+), 1 deletion(-) create mode 100644 MULTMUX_INTEGRATION.md create mode 100644 multmux/.gitignore create mode 100644 multmux/README.md create mode 100755 multmux/infrastructure/deploy.sh create mode 100644 multmux/infrastructure/nginx.conf create mode 100644 multmux/package.json create mode 100644 multmux/packages/cli/package.json create mode 100644 multmux/packages/cli/src/commands/create.ts create mode 100644 multmux/packages/cli/src/commands/join.ts create mode 100644 multmux/packages/cli/src/commands/list.ts create mode 100644 multmux/packages/cli/src/connection/WebSocketClient.ts create mode 100644 multmux/packages/cli/src/index.ts create mode 100644 multmux/packages/cli/src/ui/Terminal.ts create mode 100644 multmux/packages/cli/tsconfig.json create mode 100644 multmux/packages/server/package.json create mode 100644 multmux/packages/server/src/api/routes.ts create mode 100644 multmux/packages/server/src/index.ts create mode 100644 multmux/packages/server/src/managers/SessionManager.ts create mode 100644 multmux/packages/server/src/managers/TokenManager.ts create mode 100644 multmux/packages/server/src/types/index.ts create mode 100644 multmux/packages/server/src/websocket/TerminalHandler.ts create mode 100644 multmux/packages/server/tsconfig.json create mode 100644 multmux/tsconfig.json diff --git a/MULTMUX_INTEGRATION.md b/MULTMUX_INTEGRATION.md new file mode 100644 index 0000000..8611f4c --- /dev/null +++ b/MULTMUX_INTEGRATION.md @@ -0,0 +1,232 @@ +# mulTmux Integration + +mulTmux is now integrated into the canvas-website project as a collaborative terminal tool. This allows multiple developers to work together in the same terminal session. + +## Installation + +From the root of the canvas-website project: + +```bash +# Install all dependencies including mulTmux packages +npm run multmux:install + +# Build mulTmux packages +npm run multmux:build +``` + +## Available Commands + +All commands are run from the **root** of the canvas-website project: + +| Command | Description | +|---------|-------------| +| `npm run multmux:install` | Install mulTmux dependencies | +| `npm run multmux:build` | Build server and CLI packages | +| `npm run multmux:dev:server` | Run server in development mode | +| `npm run multmux:dev:cli` | Run CLI in development mode | +| `npm run multmux:start` | Start the production server | + +## Quick Start + +### 1. Build mulTmux + +```bash +npm run multmux:build +``` + +### 2. Start the Server Locally (for testing) + +```bash +npm run multmux:start +``` + +Server will be available at: +- HTTP API: `http://localhost:3000` +- WebSocket: `ws://localhost:3001` + +### 3. Install CLI Globally + +```bash +cd multmux/packages/cli +npm link +``` + +Now you can use the `multmux` command anywhere! + +### 4. Create a Session + +```bash +# Local testing +multmux create my-session + +# Or specify your AI server (when deployed) +multmux create my-session --server http://your-ai-server:3000 +``` + +### 5. Join from Another Terminal + +```bash +multmux join --server ws://your-ai-server:3001 +``` + +## Deploying to AI Server + +### Option 1: Using the Deploy Script + +```bash +cd multmux +./infrastructure/deploy.sh +``` + +This will: +- Install system dependencies (tmux, Node.js) +- Build the project +- Set up PM2 for process management +- Start the server + +### Option 2: Manual Deployment + +1. **SSH to your AI server** + ```bash + ssh your-ai-server + ``` + +2. **Clone or copy the project** + ```bash + git clone + cd canvas-website + git checkout mulTmux-webtree + ``` + +3. **Install and build** + ```bash + npm install + npm run multmux:build + ``` + +4. **Start with PM2** + ```bash + cd multmux + npm install -g pm2 + pm2 start packages/server/dist/index.js --name multmux-server + pm2 save + pm2 startup + ``` + +## Project Structure + +``` +canvas-website/ +├── multmux/ +│ ├── packages/ +│ │ ├── server/ # Backend (Node.js + tmux) +│ │ └── cli/ # Command-line client +│ ├── infrastructure/ +│ │ ├── deploy.sh # Auto-deployment script +│ │ └── nginx.conf # Reverse proxy config +│ └── README.md # Full documentation +├── package.json # Now includes workspace config +└── MULTMUX_INTEGRATION.md # This file +``` + +## Usage Examples + +### Collaborative Coding Session + +```bash +# Developer 1: Create session in project directory +cd /path/to/project +multmux create coding-session --repo $(pwd) + +# Developer 2: Join and start coding together +multmux join + +# Both can now type in the same terminal! +``` + +### Debugging Together + +```bash +# Create a session for debugging +multmux create debug-auth-issue + +# Share token with teammate +# Both can run commands, check logs, etc. +``` + +### List Active Sessions + +```bash +multmux list +``` + +## Configuration + +### Environment Variables + +You can customize ports by setting environment variables: + +```bash +export PORT=3000 # HTTP API port +export WS_PORT=3001 # WebSocket port +``` + +### Token Expiration + +Default: 60 minutes. To change, edit `/home/jeffe/Github/canvas-website/multmux/packages/server/src/managers/TokenManager.ts:11` + +### Session Cleanup + +Sessions auto-cleanup when all users disconnect. To change this behavior, edit `/home/jeffe/Github/canvas-website/multmux/packages/server/src/managers/SessionManager.ts:64` + +## Troubleshooting + +### "Command not found: multmux" + +Run `npm link` from the CLI package: +```bash +cd multmux/packages/cli +npm link +``` + +### "Connection refused" + +1. Check server is running: + ```bash + pm2 status + ``` + +2. Check ports are available: + ```bash + netstat -tlnp | grep -E '3000|3001' + ``` + +3. Check logs: + ```bash + pm2 logs multmux-server + ``` + +### Token Expired + +Generate a new token: +```bash +curl -X POST http://localhost:3000/api/sessions//tokens \ + -H "Content-Type: application/json" \ + -d '{"expiresInMinutes": 60}' +``` + +## Security Notes + +- Tokens expire after 60 minutes +- Sessions are isolated per tmux instance +- All input is validated on the server +- Use nginx + SSL for production deployments + +## Next Steps + +1. **Test locally first**: Run `npm run multmux:start` and try creating/joining sessions +2. **Deploy to AI server**: Use `./infrastructure/deploy.sh` +3. **Set up nginx**: Copy config from `infrastructure/nginx.conf` for SSL/reverse proxy +4. **Share with team**: Send them tokens to collaborate! + +For full documentation, see `multmux/README.md`. diff --git a/multmux/.gitignore b/multmux/.gitignore new file mode 100644 index 0000000..2e45898 --- /dev/null +++ b/multmux/.gitignore @@ -0,0 +1,35 @@ +# Dependencies +node_modules/ +package-lock.json + +# Build outputs +dist/ +*.tsbuildinfo + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pm2.log + +# Environment variables +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# PM2 +ecosystem.config.js +.pm2/ diff --git a/multmux/README.md b/multmux/README.md new file mode 100644 index 0000000..23857fa --- /dev/null +++ b/multmux/README.md @@ -0,0 +1,240 @@ +# mulTmux + +A collaborative terminal tool that lets multiple users interact with the same tmux session in real-time. + +## Features + +- **Real-time Collaboration**: Multiple users can connect to the same terminal session +- **tmux Backend**: Leverages tmux for robust terminal multiplexing +- **Token-based Auth**: Secure invite links with expiration +- **Presence Indicators**: See who's connected to your session +- **Low Resource Usage**: ~200-300MB RAM for typical usage +- **Easy Deployment**: Works alongside existing services on your server + +## Architecture + +``` +┌─────────────┐ ┌──────────────────┐ +│ Client │ ──── WebSocket ────────> │ Server │ +│ (CLI) │ (token auth) │ │ +└─────────────┘ │ ┌────────────┐ │ + │ │ Node.js │ │ +┌─────────────┐ │ │ Backend │ │ +│ Client 2 │ ──── Invite Link ──────> │ └─────┬──────┘ │ +│ (CLI) │ │ │ │ +└─────────────┘ │ ┌─────▼──────┐ │ + │ │ tmux │ │ + │ │ Sessions │ │ + │ └────────────┘ │ + └──────────────────┘ +``` + +## Installation + +### Server Setup + +1. **Deploy to your AI server:** + ```bash + cd multmux + chmod +x infrastructure/deploy.sh + ./infrastructure/deploy.sh + ``` + + This will: + - Install tmux if needed + - Build the server + - Set up PM2 for process management + - Start the server + +2. **(Optional) Set up nginx reverse proxy:** + ```bash + sudo cp infrastructure/nginx.conf /etc/nginx/sites-available/multmux + sudo ln -s /etc/nginx/sites-available/multmux /etc/nginx/sites-enabled/ + # Edit the file to set your domain + sudo nano /etc/nginx/sites-available/multmux + sudo nginx -t + sudo systemctl reload nginx + ``` + +### CLI Installation + +**On your local machine:** +```bash +cd multmux/packages/cli +npm install +npm run build +npm link # Installs 'multmux' command globally +``` + +## Usage + +### Create a Session + +```bash +multmux create my-project --repo /path/to/repo +``` + +This outputs an invite link like: +``` +multmux join a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 +``` + +### Join a Session + +```bash +multmux join a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 +``` + +### List Active Sessions + +```bash +multmux list +``` + +### Using a Remote Server + +If your server is on a different machine: + +```bash +# Create session +multmux create my-project --server http://your-server:3000 + +# Join session +multmux join --server ws://your-server:3001 +``` + +## CLI Commands + +| Command | Description | +|---------|-------------| +| `multmux create ` | Create a new collaborative session | +| `multmux join ` | Join an existing session | +| `multmux list` | List all active sessions | + +### Options + +**create:** +- `-s, --server ` - Server URL (default: http://localhost:3000) +- `-r, --repo ` - Repository path to cd into + +**join:** +- `-s, --server ` - WebSocket server URL (default: ws://localhost:3001) + +**list:** +- `-s, --server ` - Server URL (default: http://localhost:3000) + +## Server Management + +### PM2 Commands + +```bash +pm2 status # Check server status +pm2 logs multmux-server # View server logs +pm2 restart multmux-server # Restart server +pm2 stop multmux-server # Stop server +``` + +### Resource Usage + +- **Idle**: ~100-150MB RAM +- **Per session**: ~5-10MB RAM +- **Per user**: ~1-2MB RAM +- **Typical usage**: 200-300MB RAM total + +## API Reference + +### HTTP API (default: port 3000) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/sessions` | POST | Create a new session | +| `/api/sessions` | GET | List active sessions | +| `/api/sessions/:id` | GET | Get session info | +| `/api/sessions/:id/tokens` | POST | Generate new invite token | +| `/api/health` | GET | Health check | + +### WebSocket (default: port 3001) + +Connect with: `ws://localhost:3001?token=` + +**Message Types:** +- `output` - Terminal output from server +- `input` - User input to terminal +- `resize` - Terminal resize event +- `presence` - User join/leave notifications +- `joined` - Connection confirmation + +## Security + +- **Token Expiration**: Invite tokens expire after 60 minutes (configurable) +- **Session Isolation**: Each session runs in its own tmux instance +- **Input Validation**: All terminal input is validated +- **No Persistence**: Sessions are destroyed when all users leave + +## Troubleshooting + +### Server won't start + +Check if ports are available: +```bash +netstat -tlnp | grep -E '3000|3001' +``` + +### Can't connect to server + +1. Check server is running: `pm2 status` +2. Check logs: `pm2 logs multmux-server` +3. Verify firewall allows ports 3000 and 3001 + +### Terminal not responding + +1. Check WebSocket connection in browser console +2. Verify token hasn't expired +3. Restart session: `pm2 restart multmux-server` + +## Development + +### Project Structure + +``` +multmux/ +├── packages/ +│ ├── server/ # Backend server +│ │ ├── src/ +│ │ │ ├── managers/ # Session & token management +│ │ │ ├── websocket/ # WebSocket handler +│ │ │ └── api/ # HTTP routes +│ └── cli/ # CLI client +│ ├── src/ +│ │ ├── commands/ # CLI commands +│ │ ├── connection/ # WebSocket client +│ │ └── ui/ # Terminal UI +└── infrastructure/ # Deployment scripts +``` + +### Running in Development + +**Terminal 1 - Server:** +```bash +npm run dev:server +``` + +**Terminal 2 - CLI:** +```bash +cd packages/cli +npm run dev -- create test-session +``` + +### Building + +```bash +npm run build # Builds both packages +``` + +## License + +MIT + +## Contributing + +Contributions welcome! Please open an issue or PR. diff --git a/multmux/infrastructure/deploy.sh b/multmux/infrastructure/deploy.sh new file mode 100755 index 0000000..04f6e75 --- /dev/null +++ b/multmux/infrastructure/deploy.sh @@ -0,0 +1,91 @@ +#!/bin/bash + +# mulTmux Deployment Script for AI Server +# This script sets up mulTmux on your existing droplet + +set -e + +echo "🚀 mulTmux Deployment Script" +echo "============================" +echo "" + +# Check if tmux is installed +if ! command -v tmux &> /dev/null; then + echo "📦 Installing tmux..." + sudo apt-get update + sudo apt-get install -y tmux +else + echo "✅ tmux is already installed" +fi + +# Check if Node.js is installed +if ! command -v node &> /dev/null; then + echo "📦 Installing Node.js..." + curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - + sudo apt-get install -y nodejs +else + echo "✅ Node.js is already installed ($(node --version))" +fi + +# Check if npm is installed +if ! command -v npm &> /dev/null; then + echo "❌ npm is not installed. Please install npm first." + exit 1 +else + echo "✅ npm is already installed ($(npm --version))" +fi + +# Build the server +echo "" +echo "🔨 Building mulTmux..." +cd "$(dirname "$0")/.." +npm install +npm run build + +echo "" +echo "📝 Setting up PM2 for process management..." +if ! command -v pm2 &> /dev/null; then + sudo npm install -g pm2 +fi + +# Create PM2 ecosystem file +cat > ecosystem.config.js << EOF +module.exports = { + apps: [{ + name: 'multmux-server', + script: './packages/server/dist/index.js', + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '500M', + env: { + NODE_ENV: 'production', + PORT: 3000, + WS_PORT: 3001 + } + }] +}; +EOF + +echo "" +echo "🚀 Starting mulTmux server with PM2..." +pm2 start ecosystem.config.js +pm2 save +pm2 startup | tail -n 1 | bash || true + +echo "" +echo "✅ mulTmux deployed successfully!" +echo "" +echo "Server is running on:" +echo " HTTP API: http://localhost:3000" +echo " WebSocket: ws://localhost:3001" +echo "" +echo "Useful PM2 commands:" +echo " pm2 status - Check server status" +echo " pm2 logs multmux-server - View logs" +echo " pm2 restart multmux-server - Restart server" +echo " pm2 stop multmux-server - Stop server" +echo "" +echo "To install the CLI globally:" +echo " cd packages/cli && npm link" +echo "" diff --git a/multmux/infrastructure/nginx.conf b/multmux/infrastructure/nginx.conf new file mode 100644 index 0000000..c4c5281 --- /dev/null +++ b/multmux/infrastructure/nginx.conf @@ -0,0 +1,53 @@ +# nginx configuration for mulTmux +# Place this in /etc/nginx/sites-available/multmux +# Then: sudo ln -s /etc/nginx/sites-available/multmux /etc/nginx/sites-enabled/ + +upstream multmux_api { + server localhost:3000; +} + +upstream multmux_ws { + server localhost:3001; +} + +server { + listen 80; + server_name your-server-domain.com; # Change this to your domain or IP + + # HTTP API + location /api { + proxy_pass http://multmux_api; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # WebSocket + location /ws { + proxy_pass http://multmux_ws; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 86400; + } +} + +# Optional: SSL configuration (if using Let's Encrypt) +# server { +# listen 443 ssl http2; +# server_name your-server-domain.com; +# +# ssl_certificate /etc/letsencrypt/live/your-server-domain.com/fullchain.pem; +# ssl_certificate_key /etc/letsencrypt/live/your-server-domain.com/privkey.pem; +# +# # Same location blocks as above... +# } diff --git a/multmux/package.json b/multmux/package.json new file mode 100644 index 0000000..a90057c --- /dev/null +++ b/multmux/package.json @@ -0,0 +1,19 @@ +{ + "name": "multmux", + "version": "0.1.0", + "private": true, + "description": "Collaborative terminal tool with tmux backend", + "workspaces": [ + "packages/*" + ], + "scripts": { + "build": "npm run build -ws", + "dev:server": "npm run dev -w @multmux/server", + "dev:cli": "npm run dev -w @multmux/cli", + "start:server": "npm run start -w @multmux/server" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0" + } +} diff --git a/multmux/packages/cli/package.json b/multmux/packages/cli/package.json new file mode 100644 index 0000000..fb57749 --- /dev/null +++ b/multmux/packages/cli/package.json @@ -0,0 +1,30 @@ +{ + "name": "@multmux/cli", + "version": "0.1.0", + "description": "mulTmux CLI - collaborative terminal client", + "main": "dist/index.js", + "bin": { + "multmux": "./dist/index.js" + }, + "scripts": { + "build": "tsc", + "dev": "tsx src/index.ts", + "start": "node dist/index.js" + }, + "dependencies": { + "commander": "^11.1.0", + "ws": "^8.16.0", + "blessed": "^0.1.81", + "chalk": "^4.1.2", + "ora": "^5.4.1", + "node-fetch": "^2.7.0" + }, + "devDependencies": { + "@types/ws": "^8.5.10", + "@types/node": "^20.0.0", + "@types/blessed": "^0.1.25", + "@types/node-fetch": "^2.6.9", + "tsx": "^4.7.0", + "typescript": "^5.0.0" + } +} diff --git a/multmux/packages/cli/src/commands/create.ts b/multmux/packages/cli/src/commands/create.ts new file mode 100644 index 0000000..f3fc737 --- /dev/null +++ b/multmux/packages/cli/src/commands/create.ts @@ -0,0 +1,50 @@ +import fetch from 'node-fetch'; +import chalk from 'chalk'; +import ora from 'ora'; + +export async function createSession( + name: string, + options: { server?: string; repo?: string } +): Promise { + const serverUrl = options.server || 'http://localhost:3000'; + const spinner = ora('Creating session...').start(); + + try { + const response = await fetch(`${serverUrl}/api/sessions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name, + repoPath: options.repo, + }), + }); + + if (!response.ok) { + throw new Error(`Failed to create session: ${response.statusText}`); + } + + const data: any = await response.json(); + + spinner.succeed('Session created!'); + + console.log(''); + console.log(chalk.bold('Session Details:')); + console.log(` Name: ${chalk.cyan(data.session.name)}`); + console.log(` ID: ${chalk.gray(data.session.id)}`); + console.log(` Created: ${new Date(data.session.createdAt).toLocaleString()}`); + console.log(''); + console.log(chalk.bold('To join this session:')); + console.log(chalk.green(` ${data.inviteUrl}`)); + console.log(''); + console.log(chalk.bold('Or share this token:')); + console.log(` ${chalk.yellow(data.token)}`); + console.log(''); + console.log(chalk.dim('Token expires in 60 minutes')); + } catch (error) { + spinner.fail('Failed to create session'); + console.error(chalk.red((error as Error).message)); + process.exit(1); + } +} diff --git a/multmux/packages/cli/src/commands/join.ts b/multmux/packages/cli/src/commands/join.ts new file mode 100644 index 0000000..79a4582 --- /dev/null +++ b/multmux/packages/cli/src/commands/join.ts @@ -0,0 +1,45 @@ +import chalk from 'chalk'; +import ora from 'ora'; +import { WebSocketClient } from '../connection/WebSocketClient'; +import { TerminalUI } from '../ui/Terminal'; + +export async function joinSession( + token: string, + options: { server?: string } +): Promise { + const serverUrl = options.server || 'ws://localhost:3001'; + const spinner = ora('Connecting to session...').start(); + + try { + const client = new WebSocketClient(serverUrl, token); + + // Wait for connection + await client.connect(); + spinner.succeed('Connected!'); + + // Wait a moment for the 'joined' event + await new Promise((resolve) => { + client.once('joined', resolve); + setTimeout(resolve, 1000); // Fallback timeout + }); + + console.log(chalk.green('\nJoined session! Press ESC or Ctrl-C to exit.\n')); + + // Create terminal UI + const ui = new TerminalUI(client); + + // Handle errors + client.on('error', (error: Error) => { + console.error(chalk.red('\nConnection error:'), error.message); + }); + + client.on('reconnect-failed', () => { + console.error(chalk.red('\nFailed to reconnect. Exiting...')); + process.exit(1); + }); + } catch (error) { + spinner.fail('Failed to connect'); + console.error(chalk.red((error as Error).message)); + process.exit(1); + } +} diff --git a/multmux/packages/cli/src/commands/list.ts b/multmux/packages/cli/src/commands/list.ts new file mode 100644 index 0000000..8c759c9 --- /dev/null +++ b/multmux/packages/cli/src/commands/list.ts @@ -0,0 +1,38 @@ +import fetch from 'node-fetch'; +import chalk from 'chalk'; +import ora from 'ora'; + +export async function listSessions(options: { server?: string }): Promise { + const serverUrl = options.server || 'http://localhost:3000'; + const spinner = ora('Fetching sessions...').start(); + + try { + const response = await fetch(`${serverUrl}/api/sessions`); + + if (!response.ok) { + throw new Error(`Failed to fetch sessions: ${response.statusText}`); + } + + const data: any = await response.json(); + spinner.stop(); + + if (data.sessions.length === 0) { + console.log(chalk.yellow('No active sessions found.')); + return; + } + + console.log(chalk.bold(`\nActive Sessions (${data.sessions.length}):\n`)); + + data.sessions.forEach((session: any) => { + console.log(chalk.cyan(` ${session.name}`)); + console.log(` ID: ${chalk.gray(session.id)}`); + console.log(` Clients: ${session.activeClients}`); + console.log(` Created: ${new Date(session.createdAt).toLocaleString()}`); + console.log(''); + }); + } catch (error) { + spinner.fail('Failed to fetch sessions'); + console.error(chalk.red((error as Error).message)); + process.exit(1); + } +} diff --git a/multmux/packages/cli/src/connection/WebSocketClient.ts b/multmux/packages/cli/src/connection/WebSocketClient.ts new file mode 100644 index 0000000..cac647c --- /dev/null +++ b/multmux/packages/cli/src/connection/WebSocketClient.ts @@ -0,0 +1,120 @@ +import WebSocket from 'ws'; +import { EventEmitter } from 'events'; + +export interface TerminalMessage { + type: 'output' | 'input' | 'resize' | 'join' | 'leave' | 'presence' | 'joined' | 'error'; + data?: any; + clientId?: string; + timestamp?: number; + sessionId?: string; + sessionName?: string; + message?: string; +} + +export class WebSocketClient extends EventEmitter { + private ws: WebSocket | null = null; + private reconnectAttempts = 0; + private maxReconnectAttempts = 5; + + constructor(private url: string, private token: string) { + super(); + } + + connect(): Promise { + return new Promise((resolve, reject) => { + const wsUrl = `${this.url}?token=${this.token}`; + this.ws = new WebSocket(wsUrl); + + this.ws.on('open', () => { + this.reconnectAttempts = 0; + this.emit('connected'); + resolve(); + }); + + this.ws.on('message', (data) => { + try { + const message: TerminalMessage = JSON.parse(data.toString()); + this.handleMessage(message); + } catch (error) { + console.error('Failed to parse message:', error); + } + }); + + this.ws.on('close', () => { + this.emit('disconnected'); + this.attemptReconnect(); + }); + + this.ws.on('error', (error) => { + this.emit('error', error); + reject(error); + }); + }); + } + + private handleMessage(message: TerminalMessage): void { + switch (message.type) { + case 'output': + this.emit('output', message.data); + break; + case 'joined': + this.emit('joined', { + sessionId: message.sessionId, + sessionName: message.sessionName, + clientId: message.clientId, + }); + break; + case 'presence': + this.emit('presence', message.data); + break; + case 'error': + this.emit('error', new Error(message.message || 'Unknown error')); + break; + } + } + + sendInput(data: string): void { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send( + JSON.stringify({ + type: 'input', + data, + timestamp: Date.now(), + }) + ); + } + } + + resize(cols: number, rows: number): void { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send( + JSON.stringify({ + type: 'resize', + data: { cols, rows }, + timestamp: Date.now(), + }) + ); + } + } + + disconnect(): void { + if (this.ws) { + this.ws.close(); + this.ws = null; + } + } + + private attemptReconnect(): void { + if (this.reconnectAttempts < this.maxReconnectAttempts) { + this.reconnectAttempts++; + setTimeout(() => { + this.emit('reconnecting', this.reconnectAttempts); + this.connect().catch(() => { + // Reconnection failed, will retry + }); + }, 1000 * this.reconnectAttempts); + } else { + this.emit('reconnect-failed'); + } + } +} diff --git a/multmux/packages/cli/src/index.ts b/multmux/packages/cli/src/index.ts new file mode 100644 index 0000000..e1b4938 --- /dev/null +++ b/multmux/packages/cli/src/index.ts @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +import { Command } from 'commander'; +import { createSession } from './commands/create'; +import { joinSession } from './commands/join'; +import { listSessions } from './commands/list'; + +const program = new Command(); + +program + .name('multmux') + .description('Collaborative terminal tool with tmux backend') + .version('0.1.0'); + +program + .command('create ') + .description('Create a new collaborative session') + .option('-s, --server ', 'Server URL', 'http://localhost:3000') + .option('-r, --repo ', 'Repository path to use') + .action(createSession); + +program + .command('join ') + .description('Join an existing session with a token') + .option('-s, --server ', 'WebSocket server URL', 'ws://localhost:3001') + .action(joinSession); + +program + .command('list') + .description('List active sessions') + .option('-s, --server ', 'Server URL', 'http://localhost:3000') + .action(listSessions); + +program.parse(); diff --git a/multmux/packages/cli/src/ui/Terminal.ts b/multmux/packages/cli/src/ui/Terminal.ts new file mode 100644 index 0000000..c9553ac --- /dev/null +++ b/multmux/packages/cli/src/ui/Terminal.ts @@ -0,0 +1,154 @@ +import blessed from 'blessed'; +import { WebSocketClient } from '../connection/WebSocketClient'; + +export class TerminalUI { + private screen: blessed.Widgets.Screen; + private terminal: blessed.Widgets.BoxElement; + private statusBar: blessed.Widgets.BoxElement; + private buffer: string = ''; + + constructor(private client: WebSocketClient) { + // Create screen + this.screen = blessed.screen({ + smartCSR: true, + title: 'mulTmux', + }); + + // Status bar + this.statusBar = blessed.box({ + top: 0, + left: 0, + width: '100%', + height: 1, + style: { + fg: 'white', + bg: 'blue', + }, + content: ' mulTmux - Connecting...', + }); + + // Terminal output + this.terminal = blessed.box({ + top: 1, + left: 0, + width: '100%', + height: '100%-1', + scrollable: true, + alwaysScroll: true, + scrollbar: { + style: { + bg: 'blue', + }, + }, + keys: true, + vi: true, + mouse: true, + content: '', + }); + + this.screen.append(this.statusBar); + this.screen.append(this.terminal); + + // Focus terminal + this.terminal.focus(); + + // Setup event handlers + this.setupEventHandlers(); + + // Render + this.screen.render(); + } + + private setupEventHandlers(): void { + // Handle terminal output from server + this.client.on('output', (data: string) => { + this.buffer += data; + this.terminal.setContent(this.buffer); + this.terminal.setScrollPerc(100); + this.screen.render(); + }); + + // Handle connection events + this.client.on('connected', () => { + this.updateStatus('Connected', 'green'); + }); + + this.client.on('joined', (info: any) => { + this.updateStatus(`Session: ${info.sessionName} (${info.clientId.slice(0, 8)})`, 'green'); + }); + + this.client.on('disconnected', () => { + this.updateStatus('Disconnected', 'red'); + }); + + this.client.on('reconnecting', (attempt: number) => { + this.updateStatus(`Reconnecting (${attempt}/5)...`, 'yellow'); + }); + + this.client.on('presence', (data: any) => { + if (data.action === 'join') { + this.showNotification(`User joined (${data.totalClients} online)`); + } else if (data.action === 'leave') { + this.showNotification(`User left (${data.totalClients} online)`); + } + }); + + // Handle keyboard input + this.screen.on('keypress', (ch: string, key: any) => { + if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { + this.close(); + return; + } + + // Send input to server + if (ch) { + this.client.sendInput(ch); + } else if (key.name) { + // Handle special keys + const specialKeys: { [key: string]: string } = { + enter: '\r', + backspace: '\x7f', + tab: '\t', + up: '\x1b[A', + down: '\x1b[B', + right: '\x1b[C', + left: '\x1b[D', + }; + + if (specialKeys[key.name]) { + this.client.sendInput(specialKeys[key.name]); + } + } + }); + + // Handle resize + this.screen.on('resize', () => { + const { width, height } = this.terminal; + this.client.resize(width as number, (height as number) - 1); + }); + + // Quit on Ctrl-C + this.screen.key(['C-c'], () => { + this.close(); + }); + } + + private updateStatus(text: string, color: string = 'blue'): void { + this.statusBar.style.bg = color; + this.statusBar.setContent(` mulTmux - ${text}`); + this.screen.render(); + } + + private showNotification(text: string): void { + // Append notification to buffer + this.buffer += `\n[mulTmux] ${text}\n`; + this.terminal.setContent(this.buffer); + this.screen.render(); + } + + close(): void { + this.client.disconnect(); + this.screen.destroy(); + process.exit(0); + } +} diff --git a/multmux/packages/cli/tsconfig.json b/multmux/packages/cli/tsconfig.json new file mode 100644 index 0000000..90d76d7 --- /dev/null +++ b/multmux/packages/cli/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/multmux/packages/server/package.json b/multmux/packages/server/package.json new file mode 100644 index 0000000..548e619 --- /dev/null +++ b/multmux/packages/server/package.json @@ -0,0 +1,26 @@ +{ + "name": "@multmux/server", + "version": "0.1.0", + "description": "mulTmux server - collaborative terminal backend", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "dev": "tsx watch src/index.ts", + "start": "node dist/index.js" + }, + "dependencies": { + "express": "^4.18.0", + "ws": "^8.16.0", + "node-pty": "^1.0.0", + "nanoid": "^3.3.7", + "cors": "^2.8.5" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/ws": "^8.5.10", + "@types/node": "^20.0.0", + "@types/cors": "^2.8.17", + "tsx": "^4.7.0", + "typescript": "^5.0.0" + } +} diff --git a/multmux/packages/server/src/api/routes.ts b/multmux/packages/server/src/api/routes.ts new file mode 100644 index 0000000..8148732 --- /dev/null +++ b/multmux/packages/server/src/api/routes.ts @@ -0,0 +1,96 @@ +import { Router } from 'express'; +import { SessionManager } from '../managers/SessionManager'; +import { TokenManager } from '../managers/TokenManager'; + +export function createRouter( + sessionManager: SessionManager, + tokenManager: TokenManager +): Router { + const router = Router(); + + // Create a new session + router.post('/sessions', async (req, res) => { + try { + const { name, repoPath } = req.body; + + if (!name || typeof name !== 'string') { + return res.status(400).json({ error: 'Session name is required' }); + } + + const session = await sessionManager.createSession(name, repoPath); + const token = tokenManager.generateToken(session.id, 60, 'write'); + + res.json({ + session: { + id: session.id, + name: session.name, + createdAt: session.createdAt, + }, + token, + inviteUrl: `multmux join ${token}`, + }); + } catch (error) { + console.error('Failed to create session:', error); + res.status(500).json({ error: 'Failed to create session' }); + } + }); + + // List active sessions + router.get('/sessions', (req, res) => { + const sessions = sessionManager.listSessions(); + res.json({ + sessions: sessions.map((s) => ({ + id: s.id, + name: s.name, + createdAt: s.createdAt, + activeClients: s.clients.size, + })), + }); + }); + + // Get session info + router.get('/sessions/:id', (req, res) => { + const session = sessionManager.getSession(req.params.id); + + if (!session) { + return res.status(404).json({ error: 'Session not found' }); + } + + res.json({ + id: session.id, + name: session.name, + createdAt: session.createdAt, + activeClients: session.clients.size, + }); + }); + + // Generate new invite token for existing session + router.post('/sessions/:id/tokens', (req, res) => { + const session = sessionManager.getSession(req.params.id); + + if (!session) { + return res.status(404).json({ error: 'Session not found' }); + } + + const { expiresInMinutes = 60, permissions = 'write' } = req.body; + const token = tokenManager.generateToken(session.id, expiresInMinutes, permissions); + + res.json({ + token, + inviteUrl: `multmux join ${token}`, + expiresInMinutes, + permissions, + }); + }); + + // Health check + router.get('/health', (req, res) => { + res.json({ + status: 'ok', + activeSessions: sessionManager.listSessions().length, + activeTokens: tokenManager.getActiveTokens(), + }); + }); + + return router; +} diff --git a/multmux/packages/server/src/index.ts b/multmux/packages/server/src/index.ts new file mode 100644 index 0000000..3b2554a --- /dev/null +++ b/multmux/packages/server/src/index.ts @@ -0,0 +1,55 @@ +import express from 'express'; +import { WebSocketServer } from 'ws'; +import cors from 'cors'; +import { SessionManager } from './managers/SessionManager'; +import { TokenManager } from './managers/TokenManager'; +import { TerminalHandler } from './websocket/TerminalHandler'; +import { createRouter } from './api/routes'; + +const PORT = process.env.PORT || 3000; +const WS_PORT = process.env.WS_PORT || 3001; + +async function main() { + // Initialize managers + const sessionManager = new SessionManager(); + const tokenManager = new TokenManager(); + const terminalHandler = new TerminalHandler(sessionManager, tokenManager); + + // HTTP API Server + const app = express(); + app.use(cors()); + app.use(express.json()); + app.use('/api', createRouter(sessionManager, tokenManager)); + + app.listen(PORT, () => { + console.log(`mulTmux HTTP API listening on port ${PORT}`); + }); + + // WebSocket Server + const wss = new WebSocketServer({ port: Number(WS_PORT) }); + + wss.on('connection', (ws, req) => { + // Extract token from query string + const url = new URL(req.url || '', `http://localhost:${WS_PORT}`); + const token = url.searchParams.get('token'); + + if (!token) { + ws.send(JSON.stringify({ type: 'error', message: 'Token required' })); + ws.close(); + return; + } + + terminalHandler.handleConnection(ws, token); + }); + + console.log(`mulTmux WebSocket server listening on port ${WS_PORT}`); + console.log(''); + console.log('mulTmux server is ready!'); + console.log(`API: http://localhost:${PORT}/api`); + console.log(`WebSocket: ws://localhost:${WS_PORT}`); +} + +main().catch((error) => { + console.error('Failed to start server:', error); + process.exit(1); +}); diff --git a/multmux/packages/server/src/managers/SessionManager.ts b/multmux/packages/server/src/managers/SessionManager.ts new file mode 100644 index 0000000..b9e3d46 --- /dev/null +++ b/multmux/packages/server/src/managers/SessionManager.ts @@ -0,0 +1,114 @@ +import { spawn, ChildProcess } from 'child_process'; +import * as pty from 'node-pty'; +import { Session } from '../types'; +import { nanoid } from 'nanoid'; + +export class SessionManager { + private sessions: Map = new Map(); + private terminals: Map = new Map(); + + async createSession(name: string, repoPath?: string): Promise { + const id = nanoid(16); + const tmuxSessionName = `multmux-${id}`; + + const session: Session = { + id, + name, + createdAt: new Date(), + tmuxSessionName, + clients: new Set(), + repoPath, + }; + + this.sessions.set(id, session); + + // Create tmux session + await this.createTmuxSession(tmuxSessionName, repoPath); + + // Attach to tmux session with pty + const terminal = pty.spawn('tmux', ['attach-session', '-t', tmuxSessionName], { + name: 'xterm-256color', + cols: 80, + rows: 24, + cwd: repoPath || process.cwd(), + env: process.env as { [key: string]: string }, + }); + + this.terminals.set(id, terminal); + + return session; + } + + private async createTmuxSession(name: string, cwd?: string): Promise { + return new Promise((resolve, reject) => { + const args = ['new-session', '-d', '-s', name]; + if (cwd) { + args.push('-c', cwd); + } + + const proc = spawn('tmux', args); + + proc.on('exit', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Failed to create tmux session: exit code ${code}`)); + } + }); + }); + } + + getSession(id: string): Session | undefined { + return this.sessions.get(id); + } + + getTerminal(sessionId: string): pty.IPty | undefined { + return this.terminals.get(sessionId); + } + + addClient(sessionId: string, clientId: string): void { + const session = this.sessions.get(sessionId); + if (session) { + session.clients.add(clientId); + } + } + + removeClient(sessionId: string, clientId: string): void { + const session = this.sessions.get(sessionId); + if (session) { + session.clients.delete(clientId); + + // Clean up session if no clients left + if (session.clients.size === 0) { + this.destroySession(sessionId); + } + } + } + + private async destroySession(sessionId: string): Promise { + const session = this.sessions.get(sessionId); + if (!session) return; + + const terminal = this.terminals.get(sessionId); + if (terminal) { + terminal.kill(); + this.terminals.delete(sessionId); + } + + // Kill tmux session + spawn('tmux', ['kill-session', '-t', session.tmuxSessionName]); + + this.sessions.delete(sessionId); + } + + listSessions(): Session[] { + return Array.from(this.sessions.values()); + } + + resizeTerminal(sessionId: string, cols: number, rows: number): void { + const terminal = this.terminals.get(sessionId); + if (terminal) { + terminal.resize(cols, rows); + } + } +} diff --git a/multmux/packages/server/src/managers/TokenManager.ts b/multmux/packages/server/src/managers/TokenManager.ts new file mode 100644 index 0000000..ccc6891 --- /dev/null +++ b/multmux/packages/server/src/managers/TokenManager.ts @@ -0,0 +1,50 @@ +import { nanoid } from 'nanoid'; +import { SessionToken } from '../types'; + +export class TokenManager { + private tokens: Map = new Map(); + + generateToken( + sessionId: string, + expiresInMinutes: number = 60, + permissions: 'read' | 'write' = 'write' + ): string { + const token = nanoid(32); + const expiresAt = new Date(Date.now() + expiresInMinutes * 60 * 1000); + + this.tokens.set(token, { + token, + sessionId, + expiresAt, + permissions, + }); + + // Clean up expired token after expiration + setTimeout(() => this.tokens.delete(token), expiresInMinutes * 60 * 1000); + + return token; + } + + validateToken(token: string): SessionToken | null { + const sessionToken = this.tokens.get(token); + + if (!sessionToken) { + return null; + } + + if (sessionToken.expiresAt < new Date()) { + this.tokens.delete(token); + return null; + } + + return sessionToken; + } + + revokeToken(token: string): void { + this.tokens.delete(token); + } + + getActiveTokens(): number { + return this.tokens.size; + } +} diff --git a/multmux/packages/server/src/types/index.ts b/multmux/packages/server/src/types/index.ts new file mode 100644 index 0000000..029f9da --- /dev/null +++ b/multmux/packages/server/src/types/index.ts @@ -0,0 +1,29 @@ +export interface Session { + id: string; + name: string; + createdAt: Date; + tmuxSessionName: string; + clients: Set; + repoPath?: string; +} + +export interface SessionToken { + token: string; + sessionId: string; + expiresAt: Date; + permissions: 'read' | 'write'; +} + +export interface ClientConnection { + id: string; + sessionId: string; + username?: string; + permissions: 'read' | 'write'; +} + +export interface TerminalMessage { + type: 'output' | 'input' | 'resize' | 'join' | 'leave' | 'presence'; + data: any; + clientId?: string; + timestamp: number; +} diff --git a/multmux/packages/server/src/websocket/TerminalHandler.ts b/multmux/packages/server/src/websocket/TerminalHandler.ts new file mode 100644 index 0000000..30e4e7d --- /dev/null +++ b/multmux/packages/server/src/websocket/TerminalHandler.ts @@ -0,0 +1,175 @@ +import { WebSocket } from 'ws'; +import { nanoid } from 'nanoid'; +import { SessionManager } from '../managers/SessionManager'; +import { TokenManager } from '../managers/TokenManager'; +import { TerminalMessage, ClientConnection } from '../types'; + +export class TerminalHandler { + private clients: Map = new Map(); + + constructor( + private sessionManager: SessionManager, + private tokenManager: TokenManager + ) {} + + handleConnection(ws: WebSocket, token: string): void { + // Validate token + const sessionToken = this.tokenManager.validateToken(token); + if (!sessionToken) { + ws.send(JSON.stringify({ type: 'error', message: 'Invalid or expired token' })); + ws.close(); + return; + } + + // Verify session exists + const session = this.sessionManager.getSession(sessionToken.sessionId); + if (!session) { + ws.send(JSON.stringify({ type: 'error', message: 'Session not found' })); + ws.close(); + return; + } + + const clientId = nanoid(16); + const connection: ClientConnection = { + id: clientId, + sessionId: sessionToken.sessionId, + permissions: sessionToken.permissions, + }; + + this.clients.set(clientId, { ws, connection }); + this.sessionManager.addClient(sessionToken.sessionId, clientId); + + // Attach terminal output to WebSocket + const terminal = this.sessionManager.getTerminal(sessionToken.sessionId); + if (terminal) { + const onData = (data: string) => { + const message: TerminalMessage = { + type: 'output', + data, + timestamp: Date.now(), + }; + ws.send(JSON.stringify(message)); + }; + + terminal.onData(onData); + + // Clean up on disconnect + ws.on('close', () => { + terminal.off('data', onData); + this.handleDisconnect(clientId); + }); + } + + // Send join confirmation + ws.send( + JSON.stringify({ + type: 'joined', + sessionId: session.id, + sessionName: session.name, + clientId, + }) + ); + + // Broadcast presence + this.broadcastToSession(sessionToken.sessionId, { + type: 'presence', + data: { + action: 'join', + clientId, + totalClients: session.clients.size, + }, + timestamp: Date.now(), + }); + + // Handle incoming messages + ws.on('message', (data) => { + this.handleMessage(clientId, data.toString()); + }); + } + + private handleMessage(clientId: string, rawMessage: string): void { + const client = this.clients.get(clientId); + if (!client) return; + + try { + const message: TerminalMessage = JSON.parse(rawMessage); + + switch (message.type) { + case 'input': + this.handleInput(client.connection, message.data); + break; + case 'resize': + this.handleResize(client.connection, message.data); + break; + } + } catch (error) { + console.error('Failed to parse message:', error); + } + } + + private handleInput(connection: ClientConnection, data: string): void { + if (connection.permissions !== 'write') { + return; // Read-only clients can't send input + } + + const terminal = this.sessionManager.getTerminal(connection.sessionId); + if (terminal) { + terminal.write(data); + } + + // Broadcast input to other clients for cursor tracking + this.broadcastToSession( + connection.sessionId, + { + type: 'input', + data, + clientId: connection.id, + timestamp: Date.now(), + }, + connection.id // Exclude sender + ); + } + + private handleResize(connection: ClientConnection, data: { cols: number; rows: number }): void { + this.sessionManager.resizeTerminal(connection.sessionId, data.cols, data.rows); + } + + private handleDisconnect(clientId: string): void { + const client = this.clients.get(clientId); + if (!client) return; + + this.sessionManager.removeClient(client.connection.sessionId, clientId); + this.clients.delete(clientId); + + // Broadcast leave + const session = this.sessionManager.getSession(client.connection.sessionId); + if (session) { + this.broadcastToSession(client.connection.sessionId, { + type: 'presence', + data: { + action: 'leave', + clientId, + totalClients: session.clients.size, + }, + timestamp: Date.now(), + }); + } + } + + private broadcastToSession( + sessionId: string, + message: TerminalMessage, + excludeClientId?: string + ): void { + const session = this.sessionManager.getSession(sessionId); + if (!session) return; + + const messageStr = JSON.stringify(message); + + for (const [clientId, client] of this.clients.entries()) { + if (client.connection.sessionId === sessionId && clientId !== excludeClientId) { + client.ws.send(messageStr); + } + } + } +} diff --git a/multmux/packages/server/tsconfig.json b/multmux/packages/server/tsconfig.json new file mode 100644 index 0000000..90d76d7 --- /dev/null +++ b/multmux/packages/server/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/multmux/tsconfig.json b/multmux/tsconfig.json new file mode 100644 index 0000000..61d0622 --- /dev/null +++ b/multmux/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "exclude": ["node_modules", "dist"] +} diff --git a/package.json b/package.json index 5944c12..2b076c5 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "description": "Jeff Emmett's personal website", "type": "module", + "workspaces": [ + "multmux/packages/*" + ], "scripts": { "dev": "concurrently --kill-others --names client,worker --prefix-colors blue,red \"npm run dev:client\" \"npm run dev:worker:local\"", "dev:client": "vite --host 0.0.0.0 --port 5173", @@ -15,7 +18,12 @@ "deploy:pages": "tsc && vite build", "deploy:worker": "wrangler deploy", "deploy:worker:dev": "wrangler deploy --config wrangler.dev.toml", - "types": "tsc --noEmit" + "types": "tsc --noEmit", + "multmux:install": "npm install --workspaces", + "multmux:build": "npm run build --workspace=@multmux/server --workspace=@multmux/cli", + "multmux:dev:server": "npm run dev --workspace=@multmux/server", + "multmux:dev:cli": "npm run dev --workspace=@multmux/cli", + "multmux:start": "npm run start --workspace=@multmux/server" }, "keywords": [], "author": "Jeff Emmett",