mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +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/performance",
|
||||||
"web/docs/playwright-testing",
|
"web/docs/playwright-testing",
|
||||||
"web/src/test/playwright/SEQUENTIAL_OPTIMIZATIONS",
|
"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
|
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
|
### Environment Variables
|
||||||
|
|
||||||
VibeTunnel respects the following environment variables:
|
VibeTunnel respects the following environment variables:
|
||||||
|
|
@ -197,6 +199,7 @@ This npm package includes:
|
||||||
- Native PTY support for terminal emulation
|
- Native PTY support for terminal emulation
|
||||||
- Web interface with xterm.js
|
- Web interface with xterm.js
|
||||||
- Session management and forwarding
|
- Session management and forwarding
|
||||||
|
- Built-in systemd service management for Linux
|
||||||
|
|
||||||
## Platform Support
|
## 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
|
||||||
136
web/src/cli.ts
136
web/src/cli.ts
|
|
@ -54,30 +54,134 @@ process.on('unhandledRejection', (reason, promise) => {
|
||||||
process.exit(1);
|
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
|
* Print help message with version and usage information
|
||||||
// In npm package context, check if we're the actual CLI entry point
|
*/
|
||||||
const isMainModule =
|
function printHelp(): void {
|
||||||
!module.parent &&
|
|
||||||
(require.main === module ||
|
|
||||||
require.main === undefined ||
|
|
||||||
(require.main?.filename?.endsWith('/vibetunnel-cli') ?? false));
|
|
||||||
|
|
||||||
if (isMainModule) {
|
|
||||||
if (process.argv[2] === 'version') {
|
|
||||||
console.log(`VibeTunnel Server v${VERSION}`);
|
console.log(`VibeTunnel Server v${VERSION}`);
|
||||||
process.exit(0);
|
console.log('');
|
||||||
} else if (process.argv[2] === 'fwd') {
|
console.log('Usage:');
|
||||||
startVibeTunnelForward(process.argv.slice(3)).catch((error) => {
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
logger.error('Fatal error:', error);
|
||||||
closeLogger();
|
closeLogger();
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
// Show startup message at INFO level or when debug is enabled
|
||||||
if (verbosityLevel !== undefined && verbosityLevel >= VerbosityLevel.INFO) {
|
if (verbosityLevel !== undefined && verbosityLevel >= VerbosityLevel.INFO) {
|
||||||
logger.log('Starting VibeTunnel server...');
|
logger.log('Starting VibeTunnel server...');
|
||||||
}
|
}
|
||||||
startVibeTunnelServer();
|
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