mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Fix vt test for conditional installation (#393)
This commit is contained in:
parent
5bdc7f7b1b
commit
87454cf4b2
11 changed files with 550 additions and 28 deletions
|
|
@ -66,9 +66,13 @@ final class CLIInstaller {
|
||||||
// Check if it contains the correct app path reference
|
// Check if it contains the correct app path reference
|
||||||
if let content = try? String(contentsOfFile: path, encoding: .utf8) {
|
if let content = try? String(contentsOfFile: path, encoding: .utf8) {
|
||||||
// Verify it's our wrapper script with all expected components
|
// Verify it's our wrapper script with all expected components
|
||||||
|
// Check for the exec command with flexible quoting and optional arguments
|
||||||
|
// Allow for optional variables or arguments between $VIBETUNNEL_BIN and fwd
|
||||||
|
let hasValidExecCommand = content.range(of: #"exec\s+["']?\$VIBETUNNEL_BIN["']?\s+fwd"#, options: .regularExpression) != nil
|
||||||
|
|
||||||
if content.contains("VibeTunnel CLI wrapper") &&
|
if content.contains("VibeTunnel CLI wrapper") &&
|
||||||
content.contains("$TRY_PATH/Contents/Resources/vibetunnel") &&
|
content.contains("$TRY_PATH/Contents/Resources/vibetunnel") &&
|
||||||
content.contains("exec \"$VIBETUNNEL_BIN\" fwd")
|
hasValidExecCommand
|
||||||
{
|
{
|
||||||
isCorrectlyInstalled = true
|
isCorrectlyInstalled = true
|
||||||
logger.info("CLIInstaller: Found valid vt script at \(path)")
|
logger.info("CLIInstaller: Found valid vt script at \(path)")
|
||||||
|
|
|
||||||
|
|
@ -80,4 +80,11 @@ Do NOT use three separate commands (add, commit, push) as this is slow.
|
||||||
- Do NOT modify `package.json` or `pnpm-lock.yaml` unless explicitly requested
|
- Do NOT modify `package.json` or `pnpm-lock.yaml` unless explicitly requested
|
||||||
- Always ask for permission before suggesting new dependencies
|
- Always ask for permission before suggesting new dependencies
|
||||||
- Understand and work with the existing codebase architecture first
|
- Understand and work with the existing codebase architecture first
|
||||||
- This project has custom implementations - don't assume we need standard packages
|
- This project has custom implementations - don't assume we need standard packages
|
||||||
|
|
||||||
|
## CRITICAL: vt Command in package.json
|
||||||
|
**IMPORTANT: DO NOT add "vt": "./bin/vt" to the bin section of package.json or package.npm.json!**
|
||||||
|
- The vt command must NOT be registered as a global binary in package.json
|
||||||
|
- This is because it conflicts with other tools that use 'vt' (there are many)
|
||||||
|
- Instead, vt is conditionally installed via postinstall script only if available
|
||||||
|
- The postinstall script checks if vt already exists before creating a symlink
|
||||||
|
|
@ -28,7 +28,8 @@ pnpm run build
|
||||||
**npm package**:
|
**npm package**:
|
||||||
- Pre-built binaries for common platforms (macOS x64/arm64, Linux x64/arm64)
|
- Pre-built binaries for common platforms (macOS x64/arm64, Linux x64/arm64)
|
||||||
- Automatic fallback to source compilation if pre-built binaries unavailable
|
- Automatic fallback to source compilation if pre-built binaries unavailable
|
||||||
- Global installation makes `vibetunnel` and `vt` commands available system-wide
|
- Global installation makes `vibetunnel` command available system-wide
|
||||||
|
- Conditional `vt` command installation (see [VT Installation Guide](docs/VT_INSTALLATION.md))
|
||||||
- Includes production dependencies only
|
- Includes production dependencies only
|
||||||
|
|
||||||
**Source installation**:
|
**Source installation**:
|
||||||
|
|
|
||||||
113
web/docs/VT_INSTALLATION.md
Normal file
113
web/docs/VT_INSTALLATION.md
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
# VT Command Installation Guide
|
||||||
|
|
||||||
|
The `vt` command is VibeTunnel's convenient wrapper that allows you to run any command with terminal sharing enabled. This guide explains how the installation works and how to manage it.
|
||||||
|
|
||||||
|
## Installation Behavior
|
||||||
|
|
||||||
|
When you install VibeTunnel via npm, the `vt` command installation follows these rules:
|
||||||
|
|
||||||
|
### Global Installation (`npm install -g vibetunnel`)
|
||||||
|
- **Checks for existing `vt` command** to avoid conflicts with other tools
|
||||||
|
- If no `vt` command exists, creates it globally
|
||||||
|
- If `vt` already exists, skips installation and shows a warning
|
||||||
|
- You can still use `npx vt` or `vibetunnel fwd` as alternatives
|
||||||
|
|
||||||
|
### Local Installation (`npm install vibetunnel`)
|
||||||
|
- Configures `vt` for local use only
|
||||||
|
- Access via `npx vt` within your project
|
||||||
|
|
||||||
|
## Platform Support
|
||||||
|
|
||||||
|
### macOS and Linux
|
||||||
|
- Creates a symlink to the `vt` script
|
||||||
|
- Falls back to copying if symlink creation fails
|
||||||
|
- Script is made executable automatically
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
- Creates a `.cmd` wrapper for proper command execution
|
||||||
|
- Copies the actual script alongside the wrapper
|
||||||
|
- Works with Command Prompt, PowerShell, and Git Bash
|
||||||
|
|
||||||
|
## Common Scenarios
|
||||||
|
|
||||||
|
### Existing VT Command
|
||||||
|
If you already have a `vt` command from another tool:
|
||||||
|
```bash
|
||||||
|
# You'll see this warning during installation:
|
||||||
|
⚠️ A "vt" command already exists in your system
|
||||||
|
VibeTunnel's vt wrapper was not installed to avoid conflicts
|
||||||
|
You can still use "npx vt" or the full path to run VibeTunnel's vt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternatives:**
|
||||||
|
- Use `npx vt` (works globally if installed with -g)
|
||||||
|
- Use `vibetunnel fwd` directly
|
||||||
|
- Manually install to a different name (see below)
|
||||||
|
|
||||||
|
### Manual Installation
|
||||||
|
If automatic installation fails or you want to customize:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find where npm installs global packages
|
||||||
|
npm config get prefix
|
||||||
|
|
||||||
|
# On macOS/Linux, create symlink manually
|
||||||
|
ln -s $(npm root -g)/vibetunnel/bin/vt /usr/local/bin/vt
|
||||||
|
|
||||||
|
# Or copy and rename to avoid conflicts
|
||||||
|
cp $(npm root -g)/vibetunnel/bin/vt /usr/local/bin/vibetunnel-vt
|
||||||
|
chmod +x /usr/local/bin/vibetunnel-vt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Force Reinstallation
|
||||||
|
To force VibeTunnel to overwrite an existing `vt` command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remove existing vt first
|
||||||
|
rm -f $(which vt)
|
||||||
|
|
||||||
|
# Then reinstall VibeTunnel
|
||||||
|
npm install -g vibetunnel
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Permission Denied
|
||||||
|
If you get permission errors during global installation:
|
||||||
|
```bash
|
||||||
|
# Option 1: Use a Node version manager (recommended)
|
||||||
|
# With nvm: https://github.com/nvm-sh/nvm
|
||||||
|
# With fnm: https://github.com/Schniz/fnm
|
||||||
|
|
||||||
|
# Option 2: Change npm's default directory
|
||||||
|
# See: https://docs.npmjs.com/resolving-eacces-permissions-errors
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Not Found
|
||||||
|
If `vt` is installed but not found:
|
||||||
|
```bash
|
||||||
|
# Check if npm bin directory is in PATH
|
||||||
|
echo $PATH
|
||||||
|
npm config get prefix
|
||||||
|
|
||||||
|
# Add to your shell profile (.bashrc, .zshrc, etc.)
|
||||||
|
export PATH="$(npm config get prefix)/bin:$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows Specific Issues
|
||||||
|
- Ensure Node.js is in your system PATH
|
||||||
|
- Restart your terminal after installation
|
||||||
|
- Try using `vt.cmd` explicitly if `vt` doesn't work
|
||||||
|
|
||||||
|
## Uninstallation
|
||||||
|
|
||||||
|
The `vt` command is removed automatically when you uninstall VibeTunnel:
|
||||||
|
```bash
|
||||||
|
npm uninstall -g vibetunnel
|
||||||
|
```
|
||||||
|
|
||||||
|
If it persists, remove manually:
|
||||||
|
```bash
|
||||||
|
rm -f $(which vt)
|
||||||
|
# On Windows: del "%APPDATA%\npm\vt.cmd"
|
||||||
|
```
|
||||||
|
|
@ -4,8 +4,7 @@
|
||||||
"description": "Terminal sharing server with web interface - supports macOS, Linux, and headless environments",
|
"description": "Terminal sharing server with web interface - supports macOS, Linux, and headless environments",
|
||||||
"main": "lib/cli.js",
|
"main": "lib/cli.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"vibetunnel": "./bin/vibetunnel",
|
"vibetunnel": "./bin/vibetunnel"
|
||||||
"vt": "./bin/vt"
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"lib/",
|
"lib/",
|
||||||
|
|
|
||||||
|
|
@ -600,7 +600,8 @@ async function main() {
|
||||||
|
|
||||||
// Scripts
|
// Scripts
|
||||||
{ src: 'scripts/postinstall-npm.js', dest: 'scripts/postinstall.js' },
|
{ src: 'scripts/postinstall-npm.js', dest: 'scripts/postinstall.js' },
|
||||||
{ src: 'scripts/node-pty-plugin.js', dest: 'scripts/node-pty-plugin.js' }
|
{ src: 'scripts/node-pty-plugin.js', dest: 'scripts/node-pty-plugin.js' },
|
||||||
|
{ src: 'scripts/install-vt-command.js', dest: 'scripts/install-vt-command.js' }
|
||||||
];
|
];
|
||||||
|
|
||||||
function copyRecursive(src, dest) {
|
function copyRecursive(src, dest) {
|
||||||
|
|
|
||||||
121
web/scripts/install-vt-command.js
Normal file
121
web/scripts/install-vt-command.js
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
// Helper function to detect global installation
|
||||||
|
const detectGlobalInstall = () => {
|
||||||
|
if (process.env.npm_config_global === 'true') return true;
|
||||||
|
if (process.env.npm_config_global === 'false') return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const globalPrefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim();
|
||||||
|
const globalModules = path.join(globalPrefix, process.platform === 'win32' ? 'node_modules' : 'lib/node_modules');
|
||||||
|
const packagePath = path.resolve(__dirname, '..');
|
||||||
|
return packagePath.startsWith(globalModules);
|
||||||
|
} catch {
|
||||||
|
return false; // Default to local install
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to get npm global bin directory
|
||||||
|
const getNpmBinDir = () => {
|
||||||
|
try {
|
||||||
|
// Try npm config first (more reliable)
|
||||||
|
const npmPrefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim();
|
||||||
|
return path.join(npmPrefix, 'bin');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('⚠️ Could not determine npm global bin directory');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to install vt globally
|
||||||
|
const installGlobalVt = (vtSource, npmBinDir) => {
|
||||||
|
const vtTarget = path.join(npmBinDir, 'vt');
|
||||||
|
const isWindows = process.platform === 'win32';
|
||||||
|
|
||||||
|
// Check if vt already exists
|
||||||
|
if (fs.existsSync(vtTarget) || (isWindows && fs.existsSync(vtTarget + '.cmd'))) {
|
||||||
|
console.log('⚠️ A "vt" command already exists in your system');
|
||||||
|
console.log(' VibeTunnel\'s vt wrapper was not installed to avoid conflicts');
|
||||||
|
console.log(' You can still use "npx vt" or the full path to run VibeTunnel\'s vt');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isWindows) {
|
||||||
|
// On Windows, create a .cmd wrapper
|
||||||
|
const cmdContent = `@echo off\r\nnode "%~dp0\\vt" %*\r\n`;
|
||||||
|
fs.writeFileSync(vtTarget + '.cmd', cmdContent);
|
||||||
|
// Also copy the actual script
|
||||||
|
fs.copyFileSync(vtSource, vtTarget);
|
||||||
|
console.log('✓ vt command installed globally (Windows)');
|
||||||
|
} else {
|
||||||
|
// On Unix-like systems, create symlink
|
||||||
|
fs.symlinkSync(vtSource, vtTarget);
|
||||||
|
console.log('✓ vt command installed globally');
|
||||||
|
}
|
||||||
|
console.log(' You can now use "vt" to wrap commands with VibeTunnel');
|
||||||
|
return true;
|
||||||
|
} catch (symlinkError) {
|
||||||
|
// If symlink fails on Unix, try copying the file
|
||||||
|
if (!isWindows) {
|
||||||
|
try {
|
||||||
|
fs.copyFileSync(vtSource, vtTarget);
|
||||||
|
fs.chmodSync(vtTarget, '755');
|
||||||
|
console.log('✓ vt command installed globally (copied)');
|
||||||
|
console.log(' You can now use "vt" to wrap commands with VibeTunnel');
|
||||||
|
return true;
|
||||||
|
} catch (copyError) {
|
||||||
|
console.warn('⚠️ Could not install vt command globally:', copyError.message);
|
||||||
|
console.log(' Use "npx vt" or "vibetunnel fwd" instead');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ Could not install vt command on Windows:', symlinkError.message);
|
||||||
|
console.log(' Use "npx vt" or "vibetunnel fwd" instead');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Install vt command handler
|
||||||
|
const installVtCommand = (vtSource, isGlobalInstall) => {
|
||||||
|
if (!fs.existsSync(vtSource)) {
|
||||||
|
console.warn('⚠️ vt command script not found in package');
|
||||||
|
console.log(' Use "vibetunnel" command instead');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Make vt script executable (Unix-like systems only)
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
fs.chmodSync(vtSource, '755');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isGlobalInstall) {
|
||||||
|
console.log('✓ vt command configured for local use');
|
||||||
|
console.log(' Use "npx vt" to run the vt wrapper');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const npmBinDir = getNpmBinDir();
|
||||||
|
if (!npmBinDir) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return installGlobalVt(vtSource, npmBinDir);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ Could not configure vt command:', error.message);
|
||||||
|
console.log(' Use "vibetunnel" command instead');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
detectGlobalInstall,
|
||||||
|
getNpmBinDir,
|
||||||
|
installGlobalVt,
|
||||||
|
installVtCommand
|
||||||
|
};
|
||||||
|
|
@ -232,27 +232,19 @@ for (const module of modules) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Import vt installation functions
|
||||||
|
const { detectGlobalInstall, installVtCommand } = require('./install-vt-command');
|
||||||
|
|
||||||
// Install vt symlink/wrapper
|
// Install vt symlink/wrapper
|
||||||
if (!hasErrors && !isDevelopment) {
|
if (!hasErrors && !isDevelopment) {
|
||||||
console.log('\nSetting up vt command...');
|
console.log('\nSetting up vt command...');
|
||||||
|
|
||||||
const vtSource = path.join(__dirname, '..', 'bin', 'vt');
|
const vtSource = path.join(__dirname, '..', 'bin', 'vt');
|
||||||
|
|
||||||
// Check if vt script exists
|
// Use the improved global install detection
|
||||||
if (!fs.existsSync(vtSource)) {
|
const isGlobalInstall = detectGlobalInstall();
|
||||||
console.warn('⚠️ vt command script not found in package');
|
console.log(` Detected ${isGlobalInstall ? 'global' : 'local'} installation`);
|
||||||
console.log(' Use "vibetunnel" command instead');
|
installVtCommand(vtSource, isGlobalInstall);
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
// Make vt script executable
|
|
||||||
fs.chmodSync(vtSource, '755');
|
|
||||||
console.log('✓ vt command configured');
|
|
||||||
console.log(' Note: The vt command is available through npm/npx');
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('⚠️ Could not configure vt command:', error.message);
|
|
||||||
console.log(' Use "vibetunnel" command instead');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasErrors) {
|
if (hasErrors) {
|
||||||
|
|
|
||||||
94
web/scripts/test-vt-install.js
Executable file
94
web/scripts/test-vt-install.js
Executable file
|
|
@ -0,0 +1,94 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Test script for vt installation functionality
|
||||||
|
* This tests the install-vt-command module without relying on command substitution
|
||||||
|
*/
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const os = require('os');
|
||||||
|
let { execSync } = require('child_process');
|
||||||
|
|
||||||
|
// Import the module we're testing
|
||||||
|
const { detectGlobalInstall, installVtCommand } = require('./install-vt-command');
|
||||||
|
|
||||||
|
// Create a test directory
|
||||||
|
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vt-install-test-'));
|
||||||
|
const testBinDir = path.join(testDir, 'bin');
|
||||||
|
const vtPath = path.join(testBinDir, 'vt');
|
||||||
|
|
||||||
|
// Create test vt script
|
||||||
|
fs.mkdirSync(testBinDir, { recursive: true });
|
||||||
|
fs.writeFileSync(vtPath, '#!/bin/bash\necho "test vt script"', { mode: 0o755 });
|
||||||
|
|
||||||
|
console.log('Running vt installation tests...\n');
|
||||||
|
|
||||||
|
// Test 1: Local installation
|
||||||
|
console.log('Test 1: Local installation');
|
||||||
|
process.env.npm_config_global = 'false';
|
||||||
|
const localResult = installVtCommand(vtPath, false);
|
||||||
|
console.log(` Result: ${localResult ? 'PASS' : 'FAIL'}`);
|
||||||
|
console.log(` Expected: vt configured for local use\n`);
|
||||||
|
|
||||||
|
// Test 2: Global installation detection
|
||||||
|
console.log('Test 2: Global installation detection');
|
||||||
|
process.env.npm_config_global = 'true';
|
||||||
|
const isGlobal = detectGlobalInstall();
|
||||||
|
console.log(` Detected as global: ${isGlobal}`);
|
||||||
|
console.log(` Result: ${isGlobal === true ? 'PASS' : 'FAIL'}\n`);
|
||||||
|
|
||||||
|
// Test 3: Global installation with existing vt
|
||||||
|
console.log('Test 3: Global installation with existing vt');
|
||||||
|
// Mock the existence check
|
||||||
|
const originalExistsSync = fs.existsSync;
|
||||||
|
const originalSymlinkSync = fs.symlinkSync;
|
||||||
|
let existsCheckCalled = false;
|
||||||
|
let symlinkCalled = false;
|
||||||
|
|
||||||
|
fs.existsSync = (path) => {
|
||||||
|
if (path.endsWith('/vt')) {
|
||||||
|
existsCheckCalled = true;
|
||||||
|
return true; // Simulate existing vt
|
||||||
|
}
|
||||||
|
return originalExistsSync(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.symlinkSync = (target, path) => {
|
||||||
|
symlinkCalled = true;
|
||||||
|
return originalSymlinkSync(target, path);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a mock npm bin directory
|
||||||
|
const mockNpmBinDir = path.join(testDir, 'npm-bin');
|
||||||
|
fs.mkdirSync(mockNpmBinDir, { recursive: true });
|
||||||
|
|
||||||
|
// Mock execSync to return our test directory
|
||||||
|
const originalExecSync = require('child_process').execSync;
|
||||||
|
require('child_process').execSync = (cmd, opts) => {
|
||||||
|
if (cmd.includes('npm config get prefix')) {
|
||||||
|
return testDir;
|
||||||
|
}
|
||||||
|
return originalExecSync(cmd, opts);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.env.npm_config_global = 'true';
|
||||||
|
const globalResult = installVtCommand(vtPath, true);
|
||||||
|
console.log(` Existing vt check called: ${existsCheckCalled}`);
|
||||||
|
console.log(` Symlink attempted: ${symlinkCalled}`);
|
||||||
|
console.log(` Result: ${globalResult && existsCheckCalled && !symlinkCalled ? 'PASS' : 'FAIL'}`);
|
||||||
|
console.log(` Expected: Should detect existing vt and skip installation\n`);
|
||||||
|
|
||||||
|
// Restore original functions
|
||||||
|
fs.existsSync = originalExistsSync;
|
||||||
|
fs.symlinkSync = originalSymlinkSync;
|
||||||
|
require('child_process').execSync = originalExecSync;
|
||||||
|
|
||||||
|
// Test 4: Missing vt script
|
||||||
|
console.log('Test 4: Missing vt script');
|
||||||
|
const missingResult = installVtCommand(path.join(testDir, 'nonexistent'), false);
|
||||||
|
console.log(` Result: ${!missingResult ? 'PASS' : 'FAIL'}`);
|
||||||
|
console.log(` Expected: Should return false for missing script\n`);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
fs.rmSync(testDir, { recursive: true, force: true });
|
||||||
|
|
||||||
|
console.log('All tests completed.');
|
||||||
|
|
@ -53,20 +53,22 @@ if awk '/if.*then/{start=NR; in_if=1; has_content=0} in_if && !/^[[:space:]]*#/
|
||||||
fi
|
fi
|
||||||
echo "✅ vt script has no empty if statements"
|
echo "✅ vt script has no empty if statements"
|
||||||
|
|
||||||
# Test 6: Check if package.json includes vt in bin section
|
# Test 6: Check that package.json does NOT include vt in bin section
|
||||||
|
# (vt is installed conditionally via postinstall script to avoid conflicts)
|
||||||
PACKAGE_JSON="$PROJECT_ROOT/package.json"
|
PACKAGE_JSON="$PROJECT_ROOT/package.json"
|
||||||
if [ -f "$PACKAGE_JSON" ]; then
|
if [ -f "$PACKAGE_JSON" ]; then
|
||||||
if ! grep -q '"vt".*:.*"./bin/vt"' "$PACKAGE_JSON"; then
|
if grep -q '"vt".*:.*"./bin/vt"' "$PACKAGE_JSON"; then
|
||||||
echo "❌ ERROR: package.json missing vt in bin section"
|
echo "❌ ERROR: package.json should NOT include vt in bin section"
|
||||||
|
echo " vt must be installed conditionally via postinstall to avoid conflicts"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "✅ package.json includes vt in bin section"
|
echo "✅ package.json correctly omits vt from bin section (installed conditionally)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 7: Basic functionality test (help command)
|
# Test 7: Basic functionality test (help command)
|
||||||
# Skip this test if we're already inside a VibeTunnel session
|
# Skip if already inside a VibeTunnel session (recursive sessions not supported)
|
||||||
if [ -n "$VIBETUNNEL_SESSION_ID" ]; then
|
if [ -n "$VIBETUNNEL_SESSION_ID" ]; then
|
||||||
echo "✅ vt script detected we're inside a VibeTunnel session (expected behavior)"
|
echo "⚠️ Skipping vt --help test (already inside VibeTunnel session)"
|
||||||
else
|
else
|
||||||
# Use gtimeout if available, otherwise skip timeout
|
# Use gtimeout if available, otherwise skip timeout
|
||||||
if command -v gtimeout >/dev/null 2>&1; then
|
if command -v gtimeout >/dev/null 2>&1; then
|
||||||
|
|
@ -81,7 +83,7 @@ else
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
echo "✅ vt --help command works"
|
||||||
fi
|
fi
|
||||||
echo "✅ vt --help command works"
|
|
||||||
|
|
||||||
echo "🎉 All vt command tests passed!"
|
echo "🎉 All vt command tests passed!"
|
||||||
188
web/src/test/unit/postinstall-vt.test.ts
Normal file
188
web/src/test/unit/postinstall-vt.test.ts
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
describe('postinstall vt installation', () => {
|
||||||
|
let testDir: string;
|
||||||
|
let originalEnv: NodeJS.ProcessEnv;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vt-test-'));
|
||||||
|
originalEnv = { ...process.env };
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up
|
||||||
|
process.env = originalEnv;
|
||||||
|
fs.rmSync(testDir, { recursive: true, force: true });
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('installVtCommand - local installation', () => {
|
||||||
|
it('should configure vt for local use when not global install', () => {
|
||||||
|
const vtSource = path.join(testDir, 'vt');
|
||||||
|
fs.writeFileSync(vtSource, '#!/bin/bash\necho "test vt"');
|
||||||
|
|
||||||
|
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
|
||||||
|
const { installVtCommand } = require('../../../scripts/install-vt-command');
|
||||||
|
const result = installVtCommand(vtSource, false);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith('✓ vt command configured for local use');
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith(' Use "npx vt" to run the vt wrapper');
|
||||||
|
|
||||||
|
// Check file is executable on Unix
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
const stats = fs.statSync(vtSource);
|
||||||
|
expect(stats.mode & 0o111).toBeTruthy(); // Check execute bit
|
||||||
|
}
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing vt script gracefully', () => {
|
||||||
|
const vtSource = path.join(testDir, 'nonexistent');
|
||||||
|
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
|
||||||
|
const { installVtCommand } = require('../../../scripts/install-vt-command');
|
||||||
|
const result = installVtCommand(vtSource, false);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledWith('⚠️ vt command script not found in package');
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(' Use "vibetunnel" command instead');
|
||||||
|
|
||||||
|
consoleWarnSpy.mockRestore();
|
||||||
|
consoleLogSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('detectGlobalInstall - environment variables', () => {
|
||||||
|
it('should detect global install when npm_config_global is true', () => {
|
||||||
|
process.env.npm_config_global = 'true';
|
||||||
|
|
||||||
|
const { detectGlobalInstall } = require('../../../scripts/install-vt-command');
|
||||||
|
const result = detectGlobalInstall();
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect local install when npm_config_global is false', () => {
|
||||||
|
process.env.npm_config_global = 'false';
|
||||||
|
|
||||||
|
const { detectGlobalInstall } = require('../../../scripts/install-vt-command');
|
||||||
|
const result = detectGlobalInstall();
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Installation helpers', () => {
|
||||||
|
it('should check for existing vt command', () => {
|
||||||
|
const mockBinDir = path.join(testDir, 'bin');
|
||||||
|
fs.mkdirSync(mockBinDir);
|
||||||
|
|
||||||
|
// Test when vt doesn't exist
|
||||||
|
const vtPath = path.join(mockBinDir, 'vt');
|
||||||
|
expect(fs.existsSync(vtPath)).toBe(false);
|
||||||
|
|
||||||
|
// Create vt and test it exists
|
||||||
|
fs.writeFileSync(vtPath, '#!/bin/bash\necho "test"');
|
||||||
|
expect(fs.existsSync(vtPath)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Windows .cmd files', () => {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
// Skip on non-Windows
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockBinDir = path.join(testDir, 'bin');
|
||||||
|
fs.mkdirSync(mockBinDir);
|
||||||
|
|
||||||
|
const cmdPath = path.join(mockBinDir, 'vt.cmd');
|
||||||
|
const cmdContent = '@echo off\r\nnode "%~dp0\\vt" %*\r\n';
|
||||||
|
|
||||||
|
fs.writeFileSync(cmdPath, cmdContent);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(cmdPath, 'utf8');
|
||||||
|
expect(content).toContain('@echo off');
|
||||||
|
expect(content).toContain('node "%~dp0\\vt" %*');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle symlink creation', () => {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
// Skip on Windows
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceFile = path.join(testDir, 'source');
|
||||||
|
const targetLink = path.join(testDir, 'target');
|
||||||
|
|
||||||
|
fs.writeFileSync(sourceFile, '#!/bin/bash\necho "test"');
|
||||||
|
|
||||||
|
// Create symlink
|
||||||
|
fs.symlinkSync(sourceFile, targetLink);
|
||||||
|
|
||||||
|
// Verify symlink exists and points to correct file
|
||||||
|
expect(fs.existsSync(targetLink)).toBe(true);
|
||||||
|
expect(fs.lstatSync(targetLink).isSymbolicLink()).toBe(true);
|
||||||
|
expect(fs.readlinkSync(targetLink)).toBe(sourceFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle file copying as fallback', () => {
|
||||||
|
const sourceFile = path.join(testDir, 'source');
|
||||||
|
const targetFile = path.join(testDir, 'target');
|
||||||
|
|
||||||
|
const content = '#!/bin/bash\necho "test"';
|
||||||
|
fs.writeFileSync(sourceFile, content);
|
||||||
|
|
||||||
|
// Copy file
|
||||||
|
fs.copyFileSync(sourceFile, targetFile);
|
||||||
|
|
||||||
|
// Make executable on Unix
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
fs.chmodSync(targetFile, '755');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify copy
|
||||||
|
expect(fs.existsSync(targetFile)).toBe(true);
|
||||||
|
expect(fs.readFileSync(targetFile, 'utf8')).toBe(content);
|
||||||
|
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
const stats = fs.statSync(targetFile);
|
||||||
|
expect(stats.mode & 0o111).toBeTruthy(); // Check execute bit
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge cases', () => {
|
||||||
|
it('should handle permission errors gracefully', () => {
|
||||||
|
const vtSource = path.join(testDir, 'vt');
|
||||||
|
fs.writeFileSync(vtSource, '#!/bin/bash\necho "test vt"');
|
||||||
|
|
||||||
|
// Create a read-only directory to trigger permission error
|
||||||
|
const readOnlyDir = path.join(testDir, 'readonly');
|
||||||
|
fs.mkdirSync(readOnlyDir);
|
||||||
|
|
||||||
|
// This would normally fail with permission denied if we made it truly read-only
|
||||||
|
// but that's hard to test cross-platform, so we just verify the setup
|
||||||
|
expect(fs.existsSync(readOnlyDir)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle path with spaces', () => {
|
||||||
|
const dirWithSpaces = path.join(testDir, 'dir with spaces');
|
||||||
|
fs.mkdirSync(dirWithSpaces);
|
||||||
|
|
||||||
|
const vtSource = path.join(dirWithSpaces, 'vt');
|
||||||
|
fs.writeFileSync(vtSource, '#!/bin/bash\necho "test vt"');
|
||||||
|
|
||||||
|
expect(fs.existsSync(vtSource)).toBe(true);
|
||||||
|
expect(fs.readFileSync(vtSource, 'utf8')).toContain('test vt');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue