mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-31 10:25:57 +00:00
Add systemd service support for Linux (#426)
Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: hewigovens <360470+hewigovens@users.noreply.github.com>
This commit is contained in:
parent
337ef43b00
commit
12e6c6d61c
5 changed files with 921 additions and 25 deletions
|
|
@ -147,7 +147,8 @@
|
|||
"web/docs/performance",
|
||||
"web/docs/playwright-testing",
|
||||
"web/src/test/playwright/SEQUENTIAL_OPTIMIZATIONS",
|
||||
"web/docs/npm"
|
||||
"web/docs/npm",
|
||||
"web/docs/systemd"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -165,6 +165,8 @@ vibetunnel fwd --session-id abc123 npm test
|
|||
vibetunnel fwd --session-id abc123 python script.py
|
||||
```
|
||||
|
||||
Linux users can install VibeTunnel as a systemd service with `vibetunnel systemd` for automatic startup and process management - see [detailed systemd documentation](docs/systemd.md).
|
||||
|
||||
### Environment Variables
|
||||
|
||||
VibeTunnel respects the following environment variables:
|
||||
|
|
@ -197,6 +199,7 @@ This npm package includes:
|
|||
- Native PTY support for terminal emulation
|
||||
- Web interface with xterm.js
|
||||
- Session management and forwarding
|
||||
- Built-in systemd service management for Linux
|
||||
|
||||
## Platform Support
|
||||
|
||||
|
|
|
|||
383
web/docs/systemd.md
Normal file
383
web/docs/systemd.md
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
# VibeTunnel Systemd Service Guide
|
||||
|
||||
This guide covers installing and managing VibeTunnel as a systemd service on Linux systems.
|
||||
|
||||
## Overview
|
||||
|
||||
VibeTunnel includes built-in systemd integration that allows you to run it as a persistent service on Linux. The service runs as a **user-level systemd service** under your account (not system-wide), providing automatic startup, restart on failure, and proper resource management.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Install the systemd service (run as regular user, NOT root)
|
||||
vibetunnel systemd
|
||||
|
||||
# Start the service
|
||||
systemctl --user start vibetunnel
|
||||
|
||||
# Enable auto-start on boot
|
||||
systemctl --user enable vibetunnel
|
||||
|
||||
# Check status
|
||||
systemctl --user status vibetunnel
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Linux system with systemd (most modern distributions)
|
||||
- VibeTunnel installed globally via npm (`npm install -g vibetunnel`)
|
||||
- Regular user account (do not run as root)
|
||||
|
||||
### Install Command
|
||||
|
||||
```bash
|
||||
vibetunnel systemd
|
||||
```
|
||||
|
||||
This command will:
|
||||
1. Verify VibeTunnel is installed and accessible
|
||||
2. Create a wrapper script at `~/.local/bin/vibetunnel-systemd`
|
||||
3. Install the service file at `~/.config/systemd/user/vibetunnel.service`
|
||||
4. Enable the service for automatic startup
|
||||
5. Configure user lingering for boot startup
|
||||
|
||||
## Service Management
|
||||
|
||||
### Basic Commands
|
||||
|
||||
```bash
|
||||
# Start the service
|
||||
systemctl --user start vibetunnel
|
||||
|
||||
# Stop the service
|
||||
systemctl --user stop vibetunnel
|
||||
|
||||
# Restart the service
|
||||
systemctl --user restart vibetunnel
|
||||
|
||||
# Check service status
|
||||
systemctl --user status vibetunnel
|
||||
|
||||
# Enable auto-start
|
||||
systemctl --user enable vibetunnel
|
||||
|
||||
# Disable auto-start
|
||||
systemctl --user disable vibetunnel
|
||||
|
||||
# Check VibeTunnel's systemd status
|
||||
vibetunnel systemd status
|
||||
```
|
||||
|
||||
### Viewing Logs
|
||||
|
||||
```bash
|
||||
# Follow logs in real-time
|
||||
journalctl --user -u vibetunnel -f
|
||||
|
||||
# View all logs
|
||||
journalctl --user -u vibetunnel
|
||||
|
||||
# View logs from the last hour
|
||||
journalctl --user -u vibetunnel --since "1 hour ago"
|
||||
|
||||
# View only error messages
|
||||
journalctl --user -u vibetunnel -p err
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Default Settings
|
||||
|
||||
The service runs with these defaults:
|
||||
- **Port**: 4020
|
||||
- **Bind Address**: 0.0.0.0 (all interfaces)
|
||||
- **Working Directory**: Your home directory
|
||||
- **Restart Policy**: Always restart on failure
|
||||
- **Restart Delay**: 10 seconds
|
||||
- **Memory Limit**: 512MB soft, 1GB hard
|
||||
- **File Descriptor Limit**: 65536
|
||||
- **Environment**: `NODE_ENV=production`, `VIBETUNNEL_LOG_LEVEL=info`
|
||||
|
||||
### Service File Location
|
||||
|
||||
The service configuration is stored at:
|
||||
```
|
||||
~/.config/systemd/user/vibetunnel.service
|
||||
```
|
||||
|
||||
### Customizing the Service
|
||||
|
||||
To modify service settings:
|
||||
|
||||
1. Edit the service file:
|
||||
```bash
|
||||
nano ~/.config/systemd/user/vibetunnel.service
|
||||
```
|
||||
|
||||
2. Common customizations:
|
||||
```ini
|
||||
# Change port
|
||||
ExecStart=/home/user/.local/bin/vibetunnel-systemd --port 8080 --bind 0.0.0.0
|
||||
|
||||
# Add authentication
|
||||
ExecStart=/home/user/.local/bin/vibetunnel-systemd --port 4020 --bind 0.0.0.0 --auth system
|
||||
|
||||
# Change log level
|
||||
Environment=VIBETUNNEL_LOG_LEVEL=debug
|
||||
|
||||
# Adjust memory limits
|
||||
MemoryHigh=1G
|
||||
MemoryMax=2G
|
||||
|
||||
# Add custom environment variables
|
||||
Environment=MY_CUSTOM_VAR=value
|
||||
```
|
||||
|
||||
3. Reload and restart:
|
||||
```bash
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user restart vibetunnel
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Why User-Level Service?
|
||||
|
||||
VibeTunnel uses user-level systemd services for several reasons:
|
||||
|
||||
1. **Security**: Runs with user privileges, not root
|
||||
2. **Node.js Compatibility**: Works with user-installed Node.js version managers (nvm, fnm)
|
||||
3. **User Data Access**: Natural access to your projects and Git repositories
|
||||
4. **Simplicity**: No sudo required for management
|
||||
5. **Isolation**: Each user can run their own instance
|
||||
|
||||
### The Wrapper Script
|
||||
|
||||
The installer creates a wrapper script at `~/.local/bin/vibetunnel-systemd` that:
|
||||
- Searches for VibeTunnel in multiple locations
|
||||
- Handles nvm and fnm installations
|
||||
- Falls back to system-wide Node.js if needed
|
||||
- Provides detailed logging for troubleshooting
|
||||
|
||||
### User Lingering
|
||||
|
||||
The installer enables "user lingering" which allows your user services to run even when you're not logged in:
|
||||
|
||||
```bash
|
||||
# This is done automatically during installation
|
||||
loginctl enable-linger $USER
|
||||
|
||||
# To check lingering status
|
||||
loginctl show-user $USER | grep Linger
|
||||
|
||||
# To disable lingering (if desired)
|
||||
loginctl disable-linger $USER
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Service Won't Start
|
||||
|
||||
1. Check if VibeTunnel is installed:
|
||||
```bash
|
||||
which vibetunnel
|
||||
```
|
||||
|
||||
2. Check service logs:
|
||||
```bash
|
||||
journalctl --user -u vibetunnel -n 50
|
||||
```
|
||||
|
||||
3. Verify the wrapper script exists:
|
||||
```bash
|
||||
ls -la ~/.local/bin/vibetunnel-systemd
|
||||
```
|
||||
|
||||
4. Test the wrapper script directly:
|
||||
```bash
|
||||
~/.local/bin/vibetunnel-systemd --version
|
||||
```
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
If port 4020 is already in use:
|
||||
|
||||
1. Find what's using the port:
|
||||
```bash
|
||||
lsof -i :4020
|
||||
```
|
||||
|
||||
2. Either stop the conflicting service or change VibeTunnel's port in the service file
|
||||
|
||||
### Node.js Version Manager Issues
|
||||
|
||||
If using nvm or fnm, ensure they're properly initialized:
|
||||
|
||||
1. Check your shell configuration:
|
||||
```bash
|
||||
# For nvm
|
||||
echo $NVM_DIR
|
||||
|
||||
# For fnm
|
||||
echo $FNM_DIR
|
||||
```
|
||||
|
||||
2. The wrapper script searches these locations:
|
||||
- nvm: `~/.nvm`
|
||||
- fnm: `~/.local/share/fnm`
|
||||
- Global npm: `/usr/local/bin/npm`, `/usr/bin/npm`
|
||||
|
||||
### Permission Denied
|
||||
|
||||
If you get permission errors:
|
||||
|
||||
1. Ensure you're NOT running as root
|
||||
2. Check file permissions:
|
||||
```bash
|
||||
ls -la ~/.config/systemd/user/
|
||||
ls -la ~/.local/bin/vibetunnel-systemd
|
||||
```
|
||||
|
||||
3. Fix permissions if needed:
|
||||
```bash
|
||||
chmod 755 ~/.local/bin/vibetunnel-systemd
|
||||
chmod 644 ~/.config/systemd/user/vibetunnel.service
|
||||
```
|
||||
|
||||
## Uninstallation
|
||||
|
||||
To completely remove the systemd service:
|
||||
|
||||
```bash
|
||||
# Stop and disable the service
|
||||
systemctl --user stop vibetunnel
|
||||
systemctl --user disable vibetunnel
|
||||
|
||||
# Remove service files
|
||||
vibetunnel systemd uninstall
|
||||
|
||||
# Optional: Disable user lingering
|
||||
loginctl disable-linger $USER
|
||||
```
|
||||
|
||||
This will:
|
||||
- Stop the running service
|
||||
- Disable automatic startup
|
||||
- Remove the service file
|
||||
- Remove the wrapper script
|
||||
- Reload systemd configuration
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Multiple Instances
|
||||
|
||||
To run multiple VibeTunnel instances:
|
||||
|
||||
1. Copy the service file with a new name:
|
||||
```bash
|
||||
cp ~/.config/systemd/user/vibetunnel.service ~/.config/systemd/user/vibetunnel-dev.service
|
||||
```
|
||||
|
||||
2. Edit the new service file to use a different port:
|
||||
```ini
|
||||
ExecStart=/home/user/.local/bin/vibetunnel-systemd --port 4021 --bind 0.0.0.0
|
||||
```
|
||||
|
||||
3. Manage the new instance:
|
||||
```bash
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user start vibetunnel-dev
|
||||
```
|
||||
|
||||
### Environment-Specific Configuration
|
||||
|
||||
Create environment-specific service overrides:
|
||||
|
||||
```bash
|
||||
# Create override directory
|
||||
mkdir -p ~/.config/systemd/user/vibetunnel.service.d/
|
||||
|
||||
# Create override file
|
||||
cat > ~/.config/systemd/user/vibetunnel.service.d/override.conf << EOF
|
||||
[Service]
|
||||
Environment=NODE_ENV=development
|
||||
Environment=VIBETUNNEL_LOG_LEVEL=debug
|
||||
ExecStart=
|
||||
ExecStart=/home/user/.local/bin/vibetunnel-systemd --port 4020 --bind 127.0.0.1
|
||||
EOF
|
||||
|
||||
# Reload and restart
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user restart vibetunnel
|
||||
```
|
||||
|
||||
### Integration with Other Services
|
||||
|
||||
To make VibeTunnel depend on other services:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
After=network-online.target postgresql.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
# ... rest of configuration
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Firewall Configuration
|
||||
|
||||
If binding to 0.0.0.0, ensure your firewall is properly configured:
|
||||
|
||||
```bash
|
||||
# UFW example
|
||||
sudo ufw allow 4020/tcp
|
||||
|
||||
# firewalld example
|
||||
sudo firewall-cmd --add-port=4020/tcp --permanent
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
### Restricting Access
|
||||
|
||||
To limit access to localhost only, modify the service:
|
||||
|
||||
```ini
|
||||
ExecStart=/home/user/.local/bin/vibetunnel-systemd --port 4020 --bind 127.0.0.1
|
||||
```
|
||||
|
||||
### Resource Limits
|
||||
|
||||
The service includes resource limits for stability:
|
||||
- Memory: 512MB soft limit, 1GB hard limit
|
||||
- File descriptors: 65536
|
||||
- Automatic restart with 10-second delay
|
||||
|
||||
Adjust these based on your needs and system resources.
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Why doesn't the service run as root?**
|
||||
A: VibeTunnel doesn't require root privileges and running as a regular user is more secure. It also ensures compatibility with user-installed Node.js version managers.
|
||||
|
||||
**Q: Can I run this on a server without a GUI?**
|
||||
A: Yes, the systemd service works perfectly on headless servers. User lingering ensures it starts at boot.
|
||||
|
||||
**Q: How do I run VibeTunnel on a different port?**
|
||||
A: Edit the service file and change the `--port` parameter in the `ExecStart` line, then reload and restart.
|
||||
|
||||
**Q: What if I use a custom Node.js installation?**
|
||||
A: The wrapper script searches common locations. If your installation isn't found, you can modify the wrapper script at `~/.local/bin/vibetunnel-systemd`.
|
||||
|
||||
**Q: Can multiple users run VibeTunnel on the same system?**
|
||||
A: Yes, each user can install their own service. Just ensure they use different ports.
|
||||
|
||||
## Support
|
||||
|
||||
For issues specific to the systemd service:
|
||||
1. Check the logs with `journalctl --user -u vibetunnel`
|
||||
2. Verify the installation with `vibetunnel systemd status`
|
||||
3. Report issues at https://github.com/amantus-ai/vibetunnel/issues
|
||||
152
web/src/cli.ts
152
web/src/cli.ts
|
|
@ -54,30 +54,134 @@ process.on('unhandledRejection', (reason, promise) => {
|
|||
process.exit(1);
|
||||
});
|
||||
|
||||
// Only execute if this is the main module (or in SEA/bundled context where require.main is undefined)
|
||||
// In bundled builds, both module.parent and require.main are undefined
|
||||
// In npm package context, check if we're the actual CLI entry point
|
||||
const isMainModule =
|
||||
!module.parent &&
|
||||
(require.main === module ||
|
||||
require.main === undefined ||
|
||||
(require.main?.filename?.endsWith('/vibetunnel-cli') ?? false));
|
||||
/**
|
||||
* Print help message with version and usage information
|
||||
*/
|
||||
function printHelp(): void {
|
||||
console.log(`VibeTunnel Server v${VERSION}`);
|
||||
console.log('');
|
||||
console.log('Usage:');
|
||||
console.log(' vibetunnel [options] Start VibeTunnel server');
|
||||
console.log(' vibetunnel fwd <session-id> <command> Forward command to session');
|
||||
console.log(' vibetunnel systemd [action] Manage systemd service (Linux)');
|
||||
console.log(' vibetunnel version Show version');
|
||||
console.log(' vibetunnel help Show this help');
|
||||
console.log('');
|
||||
console.log('Systemd Service Actions:');
|
||||
console.log(' install - Install VibeTunnel as systemd service (default)');
|
||||
console.log(' uninstall - Remove VibeTunnel systemd service');
|
||||
console.log(' status - Check systemd service status');
|
||||
console.log('');
|
||||
console.log('Examples:');
|
||||
console.log(' vibetunnel --port 8080 --no-auth');
|
||||
console.log(' vibetunnel fwd abc123 "ls -la"');
|
||||
console.log(' vibetunnel systemd');
|
||||
console.log(' vibetunnel systemd uninstall');
|
||||
console.log('');
|
||||
console.log('For more options, run: vibetunnel --help');
|
||||
}
|
||||
|
||||
if (isMainModule) {
|
||||
if (process.argv[2] === 'version') {
|
||||
console.log(`VibeTunnel Server v${VERSION}`);
|
||||
process.exit(0);
|
||||
} else if (process.argv[2] === 'fwd') {
|
||||
startVibeTunnelForward(process.argv.slice(3)).catch((error) => {
|
||||
logger.error('Fatal error:', error);
|
||||
closeLogger();
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
// Show startup message at INFO level or when debug is enabled
|
||||
if (verbosityLevel !== undefined && verbosityLevel >= VerbosityLevel.INFO) {
|
||||
logger.log('Starting VibeTunnel server...');
|
||||
}
|
||||
startVibeTunnelServer();
|
||||
/**
|
||||
* Print version information
|
||||
*/
|
||||
function printVersion(): void {
|
||||
console.log(`VibeTunnel Server v${VERSION}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command forwarding to a session
|
||||
*/
|
||||
async function handleForwardCommand(): Promise<void> {
|
||||
try {
|
||||
await startVibeTunnelForward(process.argv.slice(3));
|
||||
} catch (error) {
|
||||
logger.error('Fatal error:', error);
|
||||
closeLogger();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle systemd service installation and management
|
||||
*/
|
||||
async function handleSystemdService(): Promise<void> {
|
||||
try {
|
||||
// Import systemd installer dynamically to avoid loading it on every startup
|
||||
const { installSystemdService } = await import('./server/services/systemd-installer.js');
|
||||
const action = process.argv[3] || 'install';
|
||||
installSystemdService(action);
|
||||
} catch (error) {
|
||||
logger.error('Failed to load systemd installer:', error);
|
||||
closeLogger();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the VibeTunnel server with optional startup logging
|
||||
*/
|
||||
function handleStartServer(): void {
|
||||
// Show startup message at INFO level or when debug is enabled
|
||||
if (verbosityLevel !== undefined && verbosityLevel >= VerbosityLevel.INFO) {
|
||||
logger.log('Starting VibeTunnel server...');
|
||||
}
|
||||
startVibeTunnelServer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command line arguments and execute appropriate action
|
||||
*/
|
||||
async function parseCommandAndExecute(): Promise<void> {
|
||||
const command = process.argv[2];
|
||||
|
||||
switch (command) {
|
||||
case 'version':
|
||||
printVersion();
|
||||
process.exit(0);
|
||||
break;
|
||||
|
||||
case 'help':
|
||||
case '--help':
|
||||
case '-h':
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
break;
|
||||
|
||||
case 'fwd':
|
||||
await handleForwardCommand();
|
||||
break;
|
||||
|
||||
case 'systemd':
|
||||
await handleSystemdService();
|
||||
break;
|
||||
|
||||
default:
|
||||
// No command provided - start the server
|
||||
handleStartServer();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this module is being run directly (not imported)
|
||||
*/
|
||||
function isMainModule(): boolean {
|
||||
return (
|
||||
!module.parent &&
|
||||
(require.main === module ||
|
||||
require.main === undefined ||
|
||||
(require.main?.filename?.endsWith('/vibetunnel-cli') ?? false))
|
||||
);
|
||||
}
|
||||
|
||||
// Main execution
|
||||
if (isMainModule()) {
|
||||
parseCommandAndExecute().catch((error) => {
|
||||
logger.error('Unhandled error in main execution:', error);
|
||||
if (error instanceof Error) {
|
||||
logger.error('Stack trace:', error.stack);
|
||||
}
|
||||
closeLogger();
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
405
web/src/server/services/systemd-installer.ts
Normal file
405
web/src/server/services/systemd-installer.ts
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { chmodSync, existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
// Colors for output
|
||||
const RED = '\x1b[0;31m';
|
||||
const GREEN = '\x1b[0;32m';
|
||||
const BLUE = '\x1b[0;34m';
|
||||
const NC = '\x1b[0m'; // No Color
|
||||
|
||||
// Configuration
|
||||
const SERVICE_NAME = 'vibetunnel';
|
||||
const SERVICE_FILE = 'vibetunnel.service';
|
||||
|
||||
// Get the current user (regular user only, no sudo/root)
|
||||
function getCurrentUser(): { username: string; home: string } {
|
||||
const username = process.env.USER || 'unknown';
|
||||
const home = process.env.HOME || `/home/${username}`;
|
||||
|
||||
return { username, home };
|
||||
}
|
||||
|
||||
// Print colored output
|
||||
function printInfo(message: string): void {
|
||||
console.log(`${BLUE}[INFO]${NC} ${message}`);
|
||||
}
|
||||
|
||||
function printSuccess(message: string): void {
|
||||
console.log(`${GREEN}[SUCCESS]${NC} ${message}`);
|
||||
}
|
||||
|
||||
function printError(message: string): void {
|
||||
console.log(`${RED}[ERROR]${NC} ${message}`);
|
||||
}
|
||||
|
||||
// Create a stable wrapper script that can find vibetunnel regardless of node version manager
|
||||
function createVibetunnelWrapper(): string {
|
||||
const { username, home } = getCurrentUser();
|
||||
const wrapperPath = `${home}/.local/bin/vibetunnel-systemd`;
|
||||
const wrapperContent = `#!/bin/bash
|
||||
# VibeTunnel Systemd Wrapper Script
|
||||
# This script finds and executes vibetunnel for user: ${username}
|
||||
|
||||
# Function to log messages
|
||||
log_info() {
|
||||
echo "[INFO] $1" >&2
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo "[ERROR] $1" >&2
|
||||
}
|
||||
|
||||
# Set up environment for user ${username}
|
||||
export HOME="${home}"
|
||||
export USER="${username}"
|
||||
|
||||
# Try to find vibetunnel in various ways
|
||||
find_vibetunnel() {
|
||||
# Method 1: Check if vibetunnel is in PATH
|
||||
if command -v vibetunnel >/dev/null 2>&1; then
|
||||
log_info "Found vibetunnel in PATH"
|
||||
vibetunnel "$@"
|
||||
return $?
|
||||
fi
|
||||
|
||||
# Method 2: Check for nvm installations
|
||||
if [ -d "${home}/.nvm" ]; then
|
||||
log_info "Checking nvm installation for user ${username}"
|
||||
export NVM_DIR="${home}/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||
if command -v vibetunnel >/dev/null 2>&1; then
|
||||
log_info "Found vibetunnel via nvm"
|
||||
vibetunnel "$@"
|
||||
return $?
|
||||
fi
|
||||
fi
|
||||
|
||||
# Method 3: Check for fnm installations
|
||||
if [ -d "${home}/.local/share/fnm" ] && [ -x "${home}/.local/share/fnm/fnm" ]; then
|
||||
log_info "Checking fnm installation for user ${username}"
|
||||
export FNM_DIR="${home}/.local/share/fnm"
|
||||
export PATH="${home}/.local/share/fnm:$PATH"
|
||||
export SHELL="/bin/bash" # Force shell for fnm
|
||||
# Initialize fnm with explicit shell and use the default node version
|
||||
eval "$("${home}/.local/share/fnm/fnm" env --shell bash)" 2>/dev/null || true
|
||||
# Try to use the default node version or current version
|
||||
"${home}/.local/share/fnm/fnm" use default >/dev/null 2>&1 || "${home}/.local/share/fnm/fnm" use current >/dev/null 2>&1 || true
|
||||
if command -v vibetunnel >/dev/null 2>&1; then
|
||||
log_info "Found vibetunnel via fnm"
|
||||
vibetunnel "$@"
|
||||
return $?
|
||||
fi
|
||||
fi
|
||||
|
||||
# Method 4: Check common global npm locations
|
||||
for npm_bin in "/usr/local/bin/npm" "/usr/bin/npm" "/opt/homebrew/bin/npm"; do
|
||||
if [ -x "$npm_bin" ]; then
|
||||
log_info "Trying npm global with $npm_bin"
|
||||
NPM_PREFIX=$("$npm_bin" config get prefix 2>/dev/null)
|
||||
if [ -n "$NPM_PREFIX" ] && [ -x "$NPM_PREFIX/bin/vibetunnel" ]; then
|
||||
log_info "Found vibetunnel via npm global: $NPM_PREFIX/bin/vibetunnel"
|
||||
"$NPM_PREFIX/bin/vibetunnel" "$@"
|
||||
return $?
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Method 5: Try to run with node directly using global npm package
|
||||
for node_bin in "/usr/local/bin/node" "/usr/bin/node" "/opt/homebrew/bin/node"; do
|
||||
if [ -x "$node_bin" ]; then
|
||||
for script_path in "/usr/local/lib/node_modules/vibetunnel/dist/cli.js" "/usr/lib/node_modules/vibetunnel/dist/cli.js"; do
|
||||
if [ -f "$script_path" ]; then
|
||||
log_info "Running vibetunnel via node: $node_bin $script_path"
|
||||
"$node_bin" "$script_path" "$@"
|
||||
return $?
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
log_error "Could not find vibetunnel installation for user ${username}"
|
||||
log_error "Please ensure vibetunnel is installed globally: npm install -g vibetunnel"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Execute the function with all arguments
|
||||
find_vibetunnel "$@"
|
||||
`;
|
||||
|
||||
try {
|
||||
// Ensure ~/.local/bin directory exists
|
||||
const localBinDir = `${home}/.local/bin`;
|
||||
if (!existsSync(localBinDir)) {
|
||||
mkdirSync(localBinDir, { recursive: true });
|
||||
printInfo(`Created directory: ${localBinDir}`);
|
||||
}
|
||||
|
||||
// Create the wrapper script
|
||||
writeFileSync(wrapperPath, wrapperContent);
|
||||
chmodSync(wrapperPath, 0o755);
|
||||
|
||||
printSuccess(`Created wrapper script at ${wrapperPath}`);
|
||||
return wrapperPath;
|
||||
} catch (error) {
|
||||
printError(`Failed to create wrapper script: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that vibetunnel is accessible and return wrapper path
|
||||
function checkVibetunnelAndCreateWrapper(): string {
|
||||
// First, verify that vibetunnel is actually installed somewhere
|
||||
try {
|
||||
const vibetunnelPath = execSync('which vibetunnel', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
||||
printInfo(`Found VibeTunnel at: ${vibetunnelPath}`);
|
||||
} catch (_error) {
|
||||
printError('VibeTunnel is not installed or not accessible. Please install it first:');
|
||||
console.log(' npm install -g vibetunnel');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create and return the wrapper script path
|
||||
return createVibetunnelWrapper();
|
||||
}
|
||||
|
||||
// Remove wrapper script during uninstall
|
||||
function removeVibetunnelWrapper(): void {
|
||||
const { home } = getCurrentUser();
|
||||
const wrapperPath = `${home}/.local/bin/vibetunnel-systemd`;
|
||||
try {
|
||||
if (existsSync(wrapperPath)) {
|
||||
unlinkSync(wrapperPath);
|
||||
printInfo('Removed wrapper script');
|
||||
}
|
||||
} catch (_error) {
|
||||
// Ignore errors when removing wrapper
|
||||
}
|
||||
}
|
||||
|
||||
// No need to create users or directories - using current user
|
||||
|
||||
// Get the systemd service template
|
||||
function getServiceTemplate(vibetunnelPath: string): string {
|
||||
const { home } = getCurrentUser();
|
||||
|
||||
return `[Unit]
|
||||
Description=VibeTunnel - Terminal sharing server with web interface
|
||||
Documentation=https://github.com/amantus-ai/vibetunnel
|
||||
After=network.target
|
||||
Wants=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=${home}
|
||||
ExecStart=${vibetunnelPath} --port 4020 --bind 0.0.0.0
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=${SERVICE_NAME}
|
||||
|
||||
# Environment - preserve user environment for node version managers
|
||||
Environment=NODE_ENV=production
|
||||
Environment=VIBETUNNEL_LOG_LEVEL=info
|
||||
Environment=HOME=%h
|
||||
Environment=USER=%i
|
||||
|
||||
# Resource limits
|
||||
LimitNOFILE=65536
|
||||
MemoryHigh=512M
|
||||
MemoryMax=1G
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target`;
|
||||
}
|
||||
|
||||
// Install systemd service
|
||||
function installService(vibetunnelPath: string): void {
|
||||
printInfo('Installing user systemd service...');
|
||||
|
||||
const { home } = getCurrentUser();
|
||||
const systemdDir = `${home}/.config/systemd/user`;
|
||||
const serviceContent = getServiceTemplate(vibetunnelPath);
|
||||
const servicePath = join(systemdDir, SERVICE_FILE);
|
||||
|
||||
try {
|
||||
// Create user systemd directory if it doesn't exist
|
||||
mkdirSync(systemdDir, { recursive: true });
|
||||
|
||||
writeFileSync(servicePath, serviceContent);
|
||||
chmodSync(servicePath, 0o644);
|
||||
|
||||
// Reload user systemd
|
||||
execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
|
||||
printSuccess('User systemd service installed');
|
||||
} catch (error) {
|
||||
printError(`Failed to install service: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Configure service
|
||||
function configureService(): void {
|
||||
printInfo('Configuring service...');
|
||||
|
||||
try {
|
||||
// Enable the user service
|
||||
execSync(`systemctl --user enable ${SERVICE_NAME}`, { stdio: 'pipe' });
|
||||
printSuccess('User service enabled for automatic startup');
|
||||
|
||||
// Enable lingering so service starts on boot even when user not logged in
|
||||
try {
|
||||
const { username } = getCurrentUser();
|
||||
execSync(`loginctl enable-linger ${username}`, { stdio: 'pipe' });
|
||||
printSuccess('User lingering enabled - service will start on boot');
|
||||
} catch (error) {
|
||||
printError(`Failed to enable lingering: ${error}`);
|
||||
printError('Service will only start when user logs in');
|
||||
}
|
||||
} catch (error) {
|
||||
printError(`Failed to configure service: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Display usage instructions
|
||||
function showUsage(): void {
|
||||
const { username, home } = getCurrentUser();
|
||||
|
||||
printSuccess('VibeTunnel systemd service installation completed!');
|
||||
console.log('');
|
||||
console.log('Usage:');
|
||||
console.log(` systemctl --user start ${SERVICE_NAME} # Start the service`);
|
||||
console.log(` systemctl --user stop ${SERVICE_NAME} # Stop the service`);
|
||||
console.log(` systemctl --user restart ${SERVICE_NAME} # Restart the service`);
|
||||
console.log(` systemctl --user status ${SERVICE_NAME} # Check service status`);
|
||||
console.log(` systemctl --user enable ${SERVICE_NAME} # Enable auto-start (already done)`);
|
||||
console.log(` systemctl --user disable ${SERVICE_NAME} # Disable auto-start`);
|
||||
console.log('');
|
||||
console.log('Logs:');
|
||||
console.log(` journalctl --user -u ${SERVICE_NAME} -f # Follow logs in real-time`);
|
||||
console.log(` journalctl --user -u ${SERVICE_NAME} # View all logs`);
|
||||
console.log('');
|
||||
console.log('Configuration:');
|
||||
console.log(' Service runs on port 4020 by default');
|
||||
console.log(' Web interface: http://localhost:4020');
|
||||
console.log(` Service runs as user: ${username}`);
|
||||
console.log(` Working directory: ${home}`);
|
||||
console.log(` Wrapper script: ${home}/.local/bin/vibetunnel-systemd`);
|
||||
console.log('');
|
||||
console.log(`To customize the service, edit: ${home}/.config/systemd/user/${SERVICE_FILE}`);
|
||||
console.log(
|
||||
`Then run: systemctl --user daemon-reload && systemctl --user restart ${SERVICE_NAME}`
|
||||
);
|
||||
}
|
||||
|
||||
// Uninstall function
|
||||
function uninstallService(): void {
|
||||
printInfo('Uninstalling VibeTunnel user systemd service...');
|
||||
|
||||
try {
|
||||
// Stop and disable user service
|
||||
try {
|
||||
execSync(`systemctl --user is-active ${SERVICE_NAME}`, { stdio: 'pipe' });
|
||||
execSync(`systemctl --user stop ${SERVICE_NAME}`, { stdio: 'pipe' });
|
||||
printInfo('User service stopped');
|
||||
} catch (_error) {
|
||||
// Service not running
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(`systemctl --user is-enabled ${SERVICE_NAME}`, { stdio: 'pipe' });
|
||||
execSync(`systemctl --user disable ${SERVICE_NAME}`, { stdio: 'pipe' });
|
||||
printInfo('User service disabled');
|
||||
} catch (_error) {
|
||||
// Service not enabled
|
||||
}
|
||||
|
||||
// Remove service file
|
||||
const { home } = getCurrentUser();
|
||||
const systemdDir = `${home}/.config/systemd/user`;
|
||||
const servicePath = join(systemdDir, SERVICE_FILE);
|
||||
if (existsSync(servicePath)) {
|
||||
unlinkSync(servicePath);
|
||||
printInfo('Service file removed');
|
||||
}
|
||||
|
||||
// Reload user systemd
|
||||
execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
|
||||
|
||||
// Remove wrapper script
|
||||
removeVibetunnelWrapper();
|
||||
|
||||
// Optionally disable lingering (ask user)
|
||||
const { username } = getCurrentUser();
|
||||
printInfo('Note: User lingering is still enabled. To disable:');
|
||||
console.log(` loginctl disable-linger ${username}`);
|
||||
|
||||
printSuccess('VibeTunnel user systemd service uninstalled');
|
||||
} catch (error) {
|
||||
printError(`Failed to uninstall service: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Check service status
|
||||
function checkServiceStatus(): void {
|
||||
try {
|
||||
const status = execSync(`systemctl --user status ${SERVICE_NAME}`, { encoding: 'utf8' });
|
||||
console.log(status);
|
||||
} catch (error) {
|
||||
// systemctl status returns non-zero for inactive services, which is normal
|
||||
if (error instanceof Error && 'stdout' in error) {
|
||||
console.log(error.stdout);
|
||||
} else {
|
||||
printError(`Failed to get service status: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if running as root and prevent execution
|
||||
function checkNotRoot(): void {
|
||||
if (process.getuid && process.getuid() === 0) {
|
||||
printError('This installer must NOT be run as root!');
|
||||
printError('VibeTunnel systemd service should run as a regular user for security.');
|
||||
printError('Please run this command as a regular user (without sudo).');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Main installation function
|
||||
export function installSystemdService(action: string = 'install'): void {
|
||||
// Prevent running as root for security
|
||||
checkNotRoot();
|
||||
|
||||
switch (action) {
|
||||
case 'install': {
|
||||
printInfo('Installing VibeTunnel user systemd service...');
|
||||
|
||||
const wrapperPath = checkVibetunnelAndCreateWrapper();
|
||||
installService(wrapperPath);
|
||||
configureService();
|
||||
showUsage();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'uninstall': {
|
||||
uninstallService();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'status':
|
||||
checkServiceStatus();
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Usage: vibetunnel systemd [install|uninstall|status]');
|
||||
console.log(' install - Install VibeTunnel user systemd service (default)');
|
||||
console.log(' uninstall - Remove VibeTunnel user systemd service');
|
||||
console.log(' status - Check service status');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue