mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
feat: add verbosity control to vt command (#356)
This commit is contained in:
parent
de2f5bcf59
commit
32d92e306a
12 changed files with 967 additions and 52 deletions
150
README.md
150
README.md
|
|
@ -75,20 +75,21 @@ When you run `vt` from the npm package, it:
|
||||||
```bash
|
```bash
|
||||||
# Run any command in the browser
|
# Run any command in the browser
|
||||||
vt pnpm run dev
|
vt pnpm run dev
|
||||||
|
vt npm test
|
||||||
|
vt python script.py
|
||||||
|
|
||||||
# Monitor AI agents (with automatic activity tracking)
|
# Monitor AI agents with automatic activity tracking
|
||||||
vt claude --dangerously-skip-permissions
|
vt claude --dangerously-skip-permissions
|
||||||
|
vt --title-mode dynamic claude # See real-time Claude status
|
||||||
|
|
||||||
# Control terminal titles
|
# Use your shell aliases
|
||||||
vt --title-mode static npm run dev # Shows path and command
|
vt gs # Your 'git status' alias works!
|
||||||
vt --title-mode dynamic python app.py # Shows path, command, and activity
|
vt claude-danger # Custom aliases are resolved
|
||||||
vt --title-mode filter vim # Blocks vim from changing title
|
|
||||||
|
|
||||||
# Shell aliases work automatically!
|
|
||||||
vt claude-danger # Your custom aliases are resolved
|
|
||||||
|
|
||||||
# Open an interactive shell
|
# Open an interactive shell
|
||||||
vt --shell
|
vt --shell # or vt -i
|
||||||
|
|
||||||
|
# For more examples and options, see "The vt Forwarding Command" section below
|
||||||
```
|
```
|
||||||
|
|
||||||
### Git Repository Scanning on First Session
|
### Git Repository Scanning on First Session
|
||||||
|
|
@ -349,6 +350,12 @@ vt --title-mode static npm run dev # Shows path and command
|
||||||
vt --title-mode dynamic python app.py # Shows path, command, and activity
|
vt --title-mode dynamic python app.py # Shows path, command, and activity
|
||||||
vt --title-mode filter vim # Blocks vim from changing title
|
vt --title-mode filter vim # Blocks vim from changing title
|
||||||
|
|
||||||
|
# Control output verbosity
|
||||||
|
vt -q npm test # Quiet mode - no console output
|
||||||
|
vt -v npm run dev # Verbose mode - show more information
|
||||||
|
vt -vv cargo build # Extra verbose - all except debug
|
||||||
|
vt -vvv python app.py # Debug mode - show everything
|
||||||
|
|
||||||
# Shell aliases work automatically!
|
# Shell aliases work automatically!
|
||||||
vt claude-danger # Your custom alias for claude --dangerously-skip-permissions
|
vt claude-danger # Your custom alias for claude --dangerously-skip-permissions
|
||||||
|
|
||||||
|
|
@ -356,6 +363,106 @@ vt claude-danger # Your custom alias for claude --dangerously-skip-permissions
|
||||||
vt title "My Project - Testing"
|
vt title "My Project - Testing"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### The `vt` Forwarding Command
|
||||||
|
|
||||||
|
The `vt` command is VibeTunnel's terminal forwarding wrapper that allows you to run any command while making its output visible in the browser. Under the hood, `vt` is a convenient shortcut for `vibetunnel fwd` - it's a bash script that calls the full command with proper path resolution and additional features like shell alias support. The `vt` wrapper acts as a transparent proxy between your terminal and the command, forwarding all input and output through VibeTunnel's infrastructure.
|
||||||
|
|
||||||
|
#### Command Syntax
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vt [options] <command> [args...]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
**Terminal Title Control:**
|
||||||
|
- `--title-mode <mode>` - Control how terminal titles are managed:
|
||||||
|
- `none` - No title management, apps control their own titles (default)
|
||||||
|
- `filter` - Block all title changes from applications
|
||||||
|
- `static` - Show working directory and command in title
|
||||||
|
- `dynamic` - Show directory, command, and live activity status (auto-enabled for Claude)
|
||||||
|
|
||||||
|
**Verbosity Control:**
|
||||||
|
- `-q, --quiet` - Quiet mode, no console output (logs to file only)
|
||||||
|
- `-v, --verbose` - Verbose mode, show errors, warnings, and info messages
|
||||||
|
- `-vv` - Extra verbose, show all messages except debug
|
||||||
|
- `-vvv` - Debug mode, show all messages including debug
|
||||||
|
|
||||||
|
**Other Options:**
|
||||||
|
- `--shell, -i` - Launch your current shell interactively
|
||||||
|
- `--no-shell-wrap, -S` - Execute command directly without shell interpretation
|
||||||
|
- `--log-file <path>` - Override default log file location (defaults to `~/.vibetunnel/log.txt`)
|
||||||
|
- `--help, -h` - Show help message with all options
|
||||||
|
|
||||||
|
#### Verbosity Levels
|
||||||
|
|
||||||
|
VibeTunnel uses a hierarchical logging system where each level includes all messages from more severe levels:
|
||||||
|
|
||||||
|
| Level | Flag | Environment Variable | Shows |
|
||||||
|
|-------|------|---------------------|-------|
|
||||||
|
| SILENT | `-q` | `VIBETUNNEL_LOG_LEVEL=silent` | No console output (file logging only) |
|
||||||
|
| ERROR | (default) | `VIBETUNNEL_LOG_LEVEL=error` | Errors only |
|
||||||
|
| WARN | - | `VIBETUNNEL_LOG_LEVEL=warn` | Errors and warnings |
|
||||||
|
| INFO | `-v` | `VIBETUNNEL_LOG_LEVEL=info` | Errors, warnings, and informational messages |
|
||||||
|
| VERBOSE | `-vv` | `VIBETUNNEL_LOG_LEVEL=verbose` | All messages except debug |
|
||||||
|
| DEBUG | `-vvv` | `VIBETUNNEL_LOG_LEVEL=debug` | Everything including debug traces |
|
||||||
|
|
||||||
|
**Note:** All logs are always written to `~/.vibetunnel/log.txt` regardless of verbosity settings. The verbosity only controls terminal output.
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic command forwarding
|
||||||
|
vt ls -la # List files with VibeTunnel monitoring
|
||||||
|
vt npm run dev # Run development server
|
||||||
|
vt python script.py # Execute Python script
|
||||||
|
|
||||||
|
# With verbosity control
|
||||||
|
vt -q npm test # Run tests silently
|
||||||
|
vt -v npm install # See detailed installation progress
|
||||||
|
vt -vvv python debug.py # Full debug output
|
||||||
|
vt --log-file debug.log npm run dev # Write logs to custom file
|
||||||
|
|
||||||
|
# Terminal title management
|
||||||
|
vt --title-mode static npm run dev # Fixed title showing command
|
||||||
|
vt --title-mode dynamic claude # Live activity updates
|
||||||
|
vt --title-mode filter vim # Prevent vim from changing title
|
||||||
|
|
||||||
|
# Shell handling
|
||||||
|
vt --shell # Open interactive shell
|
||||||
|
vt -S /usr/bin/python # Run python directly without shell
|
||||||
|
```
|
||||||
|
|
||||||
|
#### How It Works
|
||||||
|
|
||||||
|
1. **Command Resolution**: The `vt` wrapper first checks if your command is an alias, shell function, or binary
|
||||||
|
2. **Session Creation**: It creates a new VibeTunnel session with a unique ID
|
||||||
|
3. **PTY Allocation**: A pseudo-terminal is allocated to preserve terminal features (colors, cursor control, etc.)
|
||||||
|
4. **I/O Forwarding**: All input/output is forwarded between your terminal and the browser in real-time
|
||||||
|
5. **Process Management**: The wrapper monitors the process and handles signals, exit codes, and cleanup
|
||||||
|
|
||||||
|
#### Environment Variables
|
||||||
|
|
||||||
|
- `VIBETUNNEL_LOG_LEVEL` - Set default verbosity level (silent, error, warn, info, verbose, debug)
|
||||||
|
- `VIBETUNNEL_TITLE_MODE` - Set default title mode (none, filter, static, dynamic)
|
||||||
|
- `VIBETUNNEL_DEBUG` - Legacy debug flag, equivalent to `VIBETUNNEL_LOG_LEVEL=debug`
|
||||||
|
- `VIBETUNNEL_CLAUDE_DYNAMIC_TITLE` - Force dynamic title mode for Claude commands
|
||||||
|
|
||||||
|
#### Special Features
|
||||||
|
|
||||||
|
**Automatic Claude Detection**: When running Claude AI, `vt` automatically enables dynamic title mode to show real-time activity status (thinking, writing, idle).
|
||||||
|
|
||||||
|
**Shell Alias Support**: Your shell aliases and functions work transparently through `vt`:
|
||||||
|
```bash
|
||||||
|
alias gs='git status'
|
||||||
|
vt gs # Works as expected
|
||||||
|
```
|
||||||
|
|
||||||
|
**Session Title Updates**: Inside a VibeTunnel session, use `vt title` to update the session name:
|
||||||
|
```bash
|
||||||
|
vt title "Building Production Release"
|
||||||
|
```
|
||||||
|
|
||||||
### Mac App Interoperability
|
### Mac App Interoperability
|
||||||
|
|
||||||
The npm package is designed to work seamlessly alongside the Mac app:
|
The npm package is designed to work seamlessly alongside the Mac app:
|
||||||
|
|
@ -622,6 +729,31 @@ VIBETUNNEL_DEBUG=1 vt your-command
|
||||||
|
|
||||||
Debug logs are written to `~/.vibetunnel/log.txt`.
|
Debug logs are written to `~/.vibetunnel/log.txt`.
|
||||||
|
|
||||||
|
### Verbosity Control
|
||||||
|
|
||||||
|
Control the amount of output from VibeTunnel commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Command-line flags
|
||||||
|
vt -q npm test # Quiet mode - no console output
|
||||||
|
vt npm test # Default - errors only
|
||||||
|
vt -v npm run dev # Verbose - errors, warnings, and info
|
||||||
|
vt -vv cargo build # Extra verbose - all except debug
|
||||||
|
vt -vvv python script.py # Debug mode - everything
|
||||||
|
|
||||||
|
# Environment variable
|
||||||
|
export VIBETUNNEL_LOG_LEVEL=error # Default
|
||||||
|
export VIBETUNNEL_LOG_LEVEL=warn # Show errors and warnings
|
||||||
|
export VIBETUNNEL_LOG_LEVEL=info # Show errors, warnings, and info
|
||||||
|
export VIBETUNNEL_LOG_LEVEL=verbose # All except debug
|
||||||
|
export VIBETUNNEL_LOG_LEVEL=debug # Everything
|
||||||
|
|
||||||
|
# Or use inline
|
||||||
|
VIBETUNNEL_LOG_LEVEL=silent vt npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: All logs are always written to `~/.vibetunnel/log.txt` regardless of verbosity level. The verbosity settings only control what's displayed in the terminal.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [Keyboard Shortcuts](docs/keyboard-shortcuts.md) - Complete keyboard shortcut reference
|
- [Keyboard Shortcuts](docs/keyboard-shortcuts.md) - Complete keyboard shortcut reference
|
||||||
|
|
|
||||||
53
web/bin/vt
53
web/bin/vt
|
|
@ -141,6 +141,9 @@ USAGE:
|
||||||
vt title <new title> # Inside a VibeTunnel session only
|
vt title <new title> # Inside a VibeTunnel session only
|
||||||
vt --help
|
vt --help
|
||||||
|
|
||||||
|
QUICK VERBOSITY:
|
||||||
|
-q (quiet), -v (verbose), -vv (extra), -vvv (debug)
|
||||||
|
|
||||||
DESCRIPTION:
|
DESCRIPTION:
|
||||||
This wrapper script allows VibeTunnel to see the output of commands by
|
This wrapper script allows VibeTunnel to see the output of commands by
|
||||||
forwarding TTY data through the vibetunnel utility. When you run commands
|
forwarding TTY data through the vibetunnel utility. When you run commands
|
||||||
|
|
@ -160,12 +163,18 @@ EXAMPLES:
|
||||||
vt -i # Launch current shell (short form)
|
vt -i # Launch current shell (short form)
|
||||||
vt -S ls -la # List files without shell alias resolution
|
vt -S ls -la # List files without shell alias resolution
|
||||||
vt title "My Project" # Update session title (inside session only)
|
vt title "My Project" # Update session title (inside session only)
|
||||||
|
vt -q npm test # Run with minimal output (errors only)
|
||||||
|
vt -vv npm run dev # Run with verbose output
|
||||||
|
|
||||||
OPTIONS:
|
OPTIONS:
|
||||||
--shell, -i Launch current shell (equivalent to vt $SHELL)
|
--shell, -i Launch current shell (equivalent to vt $SHELL)
|
||||||
--no-shell-wrap, -S Execute command directly without shell wrapper
|
--no-shell-wrap, -S Execute command directly without shell wrapper
|
||||||
--title-mode <mode> Terminal title mode (none, filter, static, dynamic)
|
--title-mode <mode> Terminal title mode (none, filter, static, dynamic)
|
||||||
Default: none (dynamic for claude)
|
Default: none (dynamic for claude)
|
||||||
|
--quiet, -q Quiet mode - only show errors
|
||||||
|
--verbose, -v Verbose mode - show more information
|
||||||
|
-vv Extra verbose - show all except debug
|
||||||
|
-vvv Debug mode - show all messages
|
||||||
--help, -h Show this help message and exit
|
--help, -h Show this help message and exit
|
||||||
|
|
||||||
TITLE MODES:
|
TITLE MODES:
|
||||||
|
|
@ -174,6 +183,20 @@ TITLE MODES:
|
||||||
static Show working directory and command in title
|
static Show working directory and command in title
|
||||||
dynamic Show directory, command, and live activity status (default for web UI)
|
dynamic Show directory, command, and live activity status (default for web UI)
|
||||||
|
|
||||||
|
VERBOSITY:
|
||||||
|
By default, only errors are shown. Use verbosity flags to control output:
|
||||||
|
-q/--quiet Suppress all output except critical errors
|
||||||
|
-v/--verbose Show errors, warnings, and informational messages
|
||||||
|
-vv Show everything except debug messages
|
||||||
|
-vvv Show all messages including debug
|
||||||
|
|
||||||
|
You can also set VIBETUNNEL_LOG_LEVEL environment variable:
|
||||||
|
export VIBETUNNEL_LOG_LEVEL=error # Default
|
||||||
|
export VIBETUNNEL_LOG_LEVEL=warn # Show errors and warnings
|
||||||
|
export VIBETUNNEL_LOG_LEVEL=info # Show errors, warnings, and info
|
||||||
|
export VIBETUNNEL_LOG_LEVEL=verbose # All except debug
|
||||||
|
export VIBETUNNEL_LOG_LEVEL=debug # Everything
|
||||||
|
|
||||||
NOTE:
|
NOTE:
|
||||||
This script automatically detects and uses the best available VibeTunnel installation:
|
This script automatically detects and uses the best available VibeTunnel installation:
|
||||||
- Mac app bundle (preferred on macOS)
|
- Mac app bundle (preferred on macOS)
|
||||||
|
|
@ -236,15 +259,15 @@ resolve_command() {
|
||||||
case "$shell_name" in
|
case "$shell_name" in
|
||||||
zsh)
|
zsh)
|
||||||
# For zsh, we need interactive mode to get aliases
|
# For zsh, we need interactive mode to get aliases
|
||||||
exec "$VIBETUNNEL_BIN" fwd ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$user_shell" -i -c "$(printf '%q ' "$cmd" "$@")"
|
exec "$VIBETUNNEL_BIN" fwd ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$user_shell" -i -c "$(printf '%q ' "$cmd" "$@")"
|
||||||
;;
|
;;
|
||||||
bash)
|
bash)
|
||||||
# For bash, expand aliases in non-interactive mode
|
# For bash, expand aliases in non-interactive mode
|
||||||
exec "$VIBETUNNEL_BIN" fwd ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$user_shell" -c "shopt -s expand_aliases; source ~/.bashrc 2>/dev/null || source ~/.bash_profile 2>/dev/null || true; $(printf '%q ' "$cmd" "$@")"
|
exec "$VIBETUNNEL_BIN" fwd ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$user_shell" -c "shopt -s expand_aliases; source ~/.bashrc 2>/dev/null || source ~/.bash_profile 2>/dev/null || true; $(printf '%q ' "$cmd" "$@")"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
# Generic shell handling
|
# Generic shell handling
|
||||||
exec "$VIBETUNNEL_BIN" fwd ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$user_shell" -c "$(printf '%q ' "$cmd" "$@")"
|
exec "$VIBETUNNEL_BIN" fwd ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$user_shell" -c "$(printf '%q ' "$cmd" "$@")"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
@ -262,11 +285,27 @@ if [[ "$1" == "title" ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Handle verbosity flags
|
||||||
|
VERBOSITY_ARGS=""
|
||||||
|
if [[ "$1" == "--quiet" || "$1" == "-q" ]]; then
|
||||||
|
VERBOSITY_ARGS="--verbosity silent"
|
||||||
|
shift
|
||||||
|
elif [[ "$1" == "--verbose" || "$1" == "-v" ]]; then
|
||||||
|
VERBOSITY_ARGS="--verbosity info"
|
||||||
|
shift
|
||||||
|
elif [[ "$1" == "-vv" ]]; then
|
||||||
|
VERBOSITY_ARGS="--verbosity verbose"
|
||||||
|
shift
|
||||||
|
elif [[ "$1" == "-vvv" ]]; then
|
||||||
|
VERBOSITY_ARGS="--verbosity debug"
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
|
||||||
# Handle --shell or -i option (launch current shell)
|
# Handle --shell or -i option (launch current shell)
|
||||||
if [[ "$1" == "--shell" || "$1" == "-i" ]]; then
|
if [[ "$1" == "--shell" || "$1" == "-i" ]]; then
|
||||||
shift
|
shift
|
||||||
# Execute current shell through vibetunnel
|
# Execute current shell through vibetunnel
|
||||||
exec "$0" "${SHELL:-/bin/bash}" "$@"
|
exec "$0" ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} "${SHELL:-/bin/bash}" "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Handle --no-shell-wrap or -S option
|
# Handle --no-shell-wrap or -S option
|
||||||
|
|
@ -287,12 +326,12 @@ fi
|
||||||
if [ $# -gt 0 ] && [[ "$1" != -* ]]; then
|
if [ $# -gt 0 ] && [[ "$1" != -* ]]; then
|
||||||
if [[ "$NO_SHELL_WRAP" == "true" ]]; then
|
if [[ "$NO_SHELL_WRAP" == "true" ]]; then
|
||||||
# Execute directly without shell wrapper
|
# Execute directly without shell wrapper
|
||||||
exec "$VIBETUNNEL_BIN" fwd ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$@"
|
exec "$VIBETUNNEL_BIN" fwd ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$@"
|
||||||
else
|
else
|
||||||
# Check if the first argument is a real binary
|
# Check if the first argument is a real binary
|
||||||
if which "$1" >/dev/null 2>&1; then
|
if which "$1" >/dev/null 2>&1; then
|
||||||
# It's a real binary, execute directly
|
# It's a real binary, execute directly
|
||||||
exec "$VIBETUNNEL_BIN" fwd ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$@"
|
exec "$VIBETUNNEL_BIN" fwd ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$@"
|
||||||
else
|
else
|
||||||
# Not a real binary, try alias resolution
|
# Not a real binary, try alias resolution
|
||||||
resolve_command "$@"
|
resolve_command "$@"
|
||||||
|
|
@ -300,5 +339,5 @@ if [ $# -gt 0 ] && [[ "$1" != -* ]]; then
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# Run with fwd command (original behavior for options)
|
# Run with fwd command (original behavior for options)
|
||||||
exec "$VIBETUNNEL_BIN" fwd ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$@"
|
exec "$VIBETUNNEL_BIN" fwd ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$@"
|
||||||
fi
|
fi
|
||||||
|
|
@ -8,13 +8,18 @@ suppressXtermErrors();
|
||||||
|
|
||||||
import { startVibeTunnelForward } from './server/fwd.js';
|
import { startVibeTunnelForward } from './server/fwd.js';
|
||||||
import { startVibeTunnelServer } from './server/server.js';
|
import { startVibeTunnelServer } from './server/server.js';
|
||||||
import { closeLogger, createLogger, initLogger } from './server/utils/logger.js';
|
import { closeLogger, createLogger, initLogger, VerbosityLevel } from './server/utils/logger.js';
|
||||||
|
import { parseVerbosityFromEnv } from './server/utils/verbosity-parser.js';
|
||||||
import { VERSION } from './server/version.js';
|
import { VERSION } from './server/version.js';
|
||||||
|
|
||||||
// Initialize logger before anything else
|
// Initialize logger before anything else
|
||||||
// Check VIBETUNNEL_DEBUG environment variable for debug mode
|
// Parse verbosity from environment variables
|
||||||
|
const verbosityLevel = parseVerbosityFromEnv();
|
||||||
|
|
||||||
|
// Check for legacy debug mode (for backward compatibility with initLogger)
|
||||||
const debugMode = process.env.VIBETUNNEL_DEBUG === '1' || process.env.VIBETUNNEL_DEBUG === 'true';
|
const debugMode = process.env.VIBETUNNEL_DEBUG === '1' || process.env.VIBETUNNEL_DEBUG === 'true';
|
||||||
initLogger(debugMode);
|
|
||||||
|
initLogger(debugMode, verbosityLevel);
|
||||||
const logger = createLogger('cli');
|
const logger = createLogger('cli');
|
||||||
|
|
||||||
// Source maps are only included if built with --sourcemap flag
|
// Source maps are only included if built with --sourcemap flag
|
||||||
|
|
@ -61,7 +66,10 @@ if (!module.parent && (require.main === module || require.main === undefined)) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.log('Starting VibeTunnel server...');
|
// Show startup message at INFO level or when debug is enabled
|
||||||
|
if (verbosityLevel !== undefined && verbosityLevel >= VerbosityLevel.INFO) {
|
||||||
|
logger.log('Starting VibeTunnel server...');
|
||||||
|
}
|
||||||
startVibeTunnelServer();
|
startVibeTunnelServer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,17 @@ import { SessionManager } from './pty/session-manager.js';
|
||||||
import { VibeTunnelSocketClient } from './pty/socket-client.js';
|
import { VibeTunnelSocketClient } from './pty/socket-client.js';
|
||||||
import { ActivityDetector } from './utils/activity-detector.js';
|
import { ActivityDetector } from './utils/activity-detector.js';
|
||||||
import { checkAndPatchClaude } from './utils/claude-patcher.js';
|
import { checkAndPatchClaude } from './utils/claude-patcher.js';
|
||||||
import { closeLogger, createLogger } from './utils/logger.js';
|
import {
|
||||||
|
closeLogger,
|
||||||
|
createLogger,
|
||||||
|
parseVerbosityLevel,
|
||||||
|
setLogFilePath,
|
||||||
|
setVerbosityLevel,
|
||||||
|
VerbosityLevel,
|
||||||
|
} from './utils/logger.js';
|
||||||
import { generateSessionName } from './utils/session-naming.js';
|
import { generateSessionName } from './utils/session-naming.js';
|
||||||
import { generateTitleSequence } from './utils/terminal-title.js';
|
import { generateTitleSequence } from './utils/terminal-title.js';
|
||||||
|
import { parseVerbosityFromEnv } from './utils/verbosity-parser.js';
|
||||||
import { BUILD_DATE, GIT_COMMIT, VERSION } from './version.js';
|
import { BUILD_DATE, GIT_COMMIT, VERSION } from './version.js';
|
||||||
|
|
||||||
const logger = createLogger('fwd');
|
const logger = createLogger('fwd');
|
||||||
|
|
@ -33,7 +41,7 @@ function showUsage() {
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('Usage:');
|
console.log('Usage:');
|
||||||
console.log(
|
console.log(
|
||||||
' pnpm exec tsx src/fwd.ts [--session-id <id>] [--title-mode <mode>] <command> [args...]'
|
' pnpm exec tsx src/fwd.ts [--session-id <id>] [--title-mode <mode>] [--verbosity <level>] <command> [args...]'
|
||||||
);
|
);
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('Options:');
|
console.log('Options:');
|
||||||
|
|
@ -41,6 +49,12 @@ function showUsage() {
|
||||||
console.log(' --title-mode <mode> Terminal title mode: none, filter, static, dynamic');
|
console.log(' --title-mode <mode> Terminal title mode: none, filter, static, dynamic');
|
||||||
console.log(' (defaults to none for most commands, dynamic for claude)');
|
console.log(' (defaults to none for most commands, dynamic for claude)');
|
||||||
console.log(' --update-title <title> Update session title and exit (requires --session-id)');
|
console.log(' --update-title <title> Update session title and exit (requires --session-id)');
|
||||||
|
console.log(
|
||||||
|
' --verbosity <level> Set logging verbosity: silent, error, warn, info, verbose, debug'
|
||||||
|
);
|
||||||
|
console.log(' (defaults to error)');
|
||||||
|
console.log(' --log-file <path> Override default log file location');
|
||||||
|
console.log(' (defaults to ~/.vibetunnel/log.txt)');
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('Title Modes:');
|
console.log('Title Modes:');
|
||||||
console.log(' none - No title management (default)');
|
console.log(' none - No title management (default)');
|
||||||
|
|
@ -48,9 +62,23 @@ function showUsage() {
|
||||||
console.log(' static - Show working directory and command');
|
console.log(' static - Show working directory and command');
|
||||||
console.log(' dynamic - Show directory, command, and activity (auto-selected for claude)');
|
console.log(' dynamic - Show directory, command, and activity (auto-selected for claude)');
|
||||||
console.log('');
|
console.log('');
|
||||||
|
console.log('Verbosity Levels:');
|
||||||
|
console.log(` ${chalk.gray('silent')} - No output except critical errors`);
|
||||||
|
console.log(` ${chalk.red('error')} - Only errors ${chalk.gray('(default)')}`);
|
||||||
|
console.log(` ${chalk.yellow('warn')} - Errors and warnings`);
|
||||||
|
console.log(` ${chalk.green('info')} - Errors, warnings, and informational messages`);
|
||||||
|
console.log(` ${chalk.blue('verbose')} - All messages except debug`);
|
||||||
|
console.log(` ${chalk.magenta('debug')} - All messages including debug`);
|
||||||
|
console.log('');
|
||||||
|
console.log(
|
||||||
|
`Quick verbosity: ${chalk.cyan('-q (quiet), -v (verbose), -vv (extra), -vvv (debug)')}`
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
console.log('Environment Variables:');
|
console.log('Environment Variables:');
|
||||||
console.log(' VIBETUNNEL_TITLE_MODE=<mode> Set default title mode');
|
console.log(' VIBETUNNEL_TITLE_MODE=<mode> Set default title mode');
|
||||||
console.log(' VIBETUNNEL_CLAUDE_DYNAMIC_TITLE=1 Force dynamic title for Claude');
|
console.log(' VIBETUNNEL_CLAUDE_DYNAMIC_TITLE=1 Force dynamic title for Claude');
|
||||||
|
console.log(' VIBETUNNEL_LOG_LEVEL=<level> Set default verbosity level');
|
||||||
|
console.log(' VIBETUNNEL_DEBUG=1 Enable debug mode (legacy)');
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('Examples:');
|
console.log('Examples:');
|
||||||
console.log(' pnpm exec tsx src/fwd.ts claude --resume');
|
console.log(' pnpm exec tsx src/fwd.ts claude --resume');
|
||||||
|
|
@ -58,16 +86,19 @@ function showUsage() {
|
||||||
console.log(' pnpm exec tsx src/fwd.ts --title-mode filter vim');
|
console.log(' pnpm exec tsx src/fwd.ts --title-mode filter vim');
|
||||||
console.log(' pnpm exec tsx src/fwd.ts --session-id abc123 claude');
|
console.log(' pnpm exec tsx src/fwd.ts --session-id abc123 claude');
|
||||||
console.log(' pnpm exec tsx src/fwd.ts --update-title "New Title" --session-id abc123');
|
console.log(' pnpm exec tsx src/fwd.ts --update-title "New Title" --session-id abc123');
|
||||||
|
console.log(' pnpm exec tsx src/fwd.ts --verbosity silent npm test');
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('The command will be spawned in the current working directory');
|
console.log('The command will be spawned in the current working directory');
|
||||||
console.log('and managed through the VibeTunnel PTY infrastructure.');
|
console.log('and managed through the VibeTunnel PTY infrastructure.');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startVibeTunnelForward(args: string[]) {
|
export async function startVibeTunnelForward(args: string[]) {
|
||||||
// Log startup with version (logger already initialized in cli.ts)
|
// Parse verbosity from environment variables
|
||||||
if (process.env.VIBETUNNEL_DEBUG === '1' || process.env.VIBETUNNEL_DEBUG === 'true') {
|
let verbosityLevel = parseVerbosityFromEnv();
|
||||||
|
|
||||||
|
// Set debug mode on logger for backward compatibility
|
||||||
|
if (verbosityLevel === VerbosityLevel.DEBUG) {
|
||||||
logger.setDebugMode(true);
|
logger.setDebugMode(true);
|
||||||
logger.warn('Debug mode enabled');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse command line arguments
|
// Parse command line arguments
|
||||||
|
|
@ -84,6 +115,7 @@ export async function startVibeTunnelForward(args: string[]) {
|
||||||
let sessionId: string | undefined;
|
let sessionId: string | undefined;
|
||||||
let titleMode: TitleMode = TitleMode.NONE;
|
let titleMode: TitleMode = TitleMode.NONE;
|
||||||
let updateTitle: string | undefined;
|
let updateTitle: string | undefined;
|
||||||
|
let logFilePath: string | undefined;
|
||||||
let remainingArgs = args;
|
let remainingArgs = args;
|
||||||
|
|
||||||
// Check environment variables for title mode
|
// Check environment variables for title mode
|
||||||
|
|
@ -123,6 +155,20 @@ export async function startVibeTunnelForward(args: string[]) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
remainingArgs = remainingArgs.slice(2);
|
remainingArgs = remainingArgs.slice(2);
|
||||||
|
} else if (remainingArgs[0] === '--verbosity' && remainingArgs.length > 1) {
|
||||||
|
const parsedLevel = parseVerbosityLevel(remainingArgs[1]);
|
||||||
|
if (parsedLevel !== undefined) {
|
||||||
|
verbosityLevel = parsedLevel;
|
||||||
|
} else {
|
||||||
|
logger.error(`Invalid verbosity level: ${remainingArgs[1]}`);
|
||||||
|
logger.error('Valid levels: silent, error, warn, info, verbose, debug');
|
||||||
|
closeLogger();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
remainingArgs = remainingArgs.slice(2);
|
||||||
|
} else if (remainingArgs[0] === '--log-file' && remainingArgs.length > 1) {
|
||||||
|
logFilePath = remainingArgs[1];
|
||||||
|
remainingArgs = remainingArgs.slice(2);
|
||||||
} else {
|
} else {
|
||||||
// Not a flag, must be the start of the command
|
// Not a flag, must be the start of the command
|
||||||
break;
|
break;
|
||||||
|
|
@ -135,6 +181,20 @@ export async function startVibeTunnelForward(args: string[]) {
|
||||||
remainingArgs = remainingArgs.slice(1);
|
remainingArgs = remainingArgs.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply log file path if set
|
||||||
|
if (logFilePath !== undefined) {
|
||||||
|
setLogFilePath(logFilePath);
|
||||||
|
logger.debug(`Log file path set to: ${logFilePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply verbosity level if set
|
||||||
|
if (verbosityLevel !== undefined) {
|
||||||
|
setVerbosityLevel(verbosityLevel);
|
||||||
|
if (verbosityLevel >= VerbosityLevel.INFO) {
|
||||||
|
logger.log(`Verbosity level set to: ${VerbosityLevel[verbosityLevel].toLowerCase()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle special case: --update-title mode
|
// Handle special case: --update-title mode
|
||||||
if (updateTitle !== undefined) {
|
if (updateTitle !== undefined) {
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
|
|
|
||||||
|
|
@ -1029,7 +1029,7 @@ export async function createApp(): Promise<AppInstance> {
|
||||||
// Start mDNS advertisement if enabled
|
// Start mDNS advertisement if enabled
|
||||||
if (config.enableMDNS) {
|
if (config.enableMDNS) {
|
||||||
mdnsService.startAdvertising(actualPort).catch((err) => {
|
mdnsService.startAdvertising(actualPort).catch((err) => {
|
||||||
logger.error('Failed to start mDNS advertisement:', err);
|
logger.warn('Failed to start mDNS advertisement:', err);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.debug('mDNS advertisement disabled');
|
logger.debug('mDNS advertisement disabled');
|
||||||
|
|
|
||||||
|
|
@ -52,11 +52,11 @@ export class MDNSService {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.service.on('error', (...args: unknown[]) => {
|
this.service.on('error', (...args: unknown[]) => {
|
||||||
log.error('mDNS service error:', args[0]);
|
log.warn('mDNS service error:', args[0]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Failed to start mDNS advertisement:', error);
|
log.warn('Failed to start mDNS advertisement:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -92,7 +92,7 @@ export class MDNSService {
|
||||||
this.isAdvertising = false;
|
this.isAdvertising = false;
|
||||||
log.log('Stopped mDNS advertisement');
|
log.log('Stopped mDNS advertisement');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Error stopping mDNS advertisement:', error);
|
log.warn('Error stopping mDNS advertisement:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,78 @@ import * as path from 'path';
|
||||||
|
|
||||||
// Log file path
|
// Log file path
|
||||||
const LOG_DIR = path.join(os.homedir(), '.vibetunnel');
|
const LOG_DIR = path.join(os.homedir(), '.vibetunnel');
|
||||||
const LOG_FILE = path.join(LOG_DIR, 'log.txt');
|
let LOG_FILE = path.join(LOG_DIR, 'log.txt');
|
||||||
|
|
||||||
// Debug mode flag
|
/**
|
||||||
|
* Set custom log file path
|
||||||
|
*/
|
||||||
|
export function setLogFilePath(filePath: string): void {
|
||||||
|
// Close existing file handle if open
|
||||||
|
if (logFileHandle) {
|
||||||
|
logFileHandle.end();
|
||||||
|
logFileHandle = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_FILE = filePath;
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
const dir = path.dirname(LOG_FILE);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-open log file at new location
|
||||||
|
try {
|
||||||
|
logFileHandle = fs.createWriteStream(LOG_FILE, { flags: 'a' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open log file at new location:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verbosity levels
|
||||||
|
export enum VerbosityLevel {
|
||||||
|
SILENT = 0, // No console output (logs to file only)
|
||||||
|
ERROR = 1, // Errors only (default)
|
||||||
|
WARN = 2, // Errors and warnings
|
||||||
|
INFO = 3, // Errors, warnings, and info
|
||||||
|
VERBOSE = 4, // All except debug
|
||||||
|
DEBUG = 5, // Everything
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type-safe mapping of string names to verbosity levels
|
||||||
|
*/
|
||||||
|
export const VERBOSITY_MAP: Record<string, VerbosityLevel> = {
|
||||||
|
silent: VerbosityLevel.SILENT,
|
||||||
|
error: VerbosityLevel.ERROR,
|
||||||
|
warn: VerbosityLevel.WARN,
|
||||||
|
info: VerbosityLevel.INFO,
|
||||||
|
verbose: VerbosityLevel.VERBOSE,
|
||||||
|
debug: VerbosityLevel.DEBUG,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Current verbosity level
|
||||||
|
let verbosityLevel: VerbosityLevel = VerbosityLevel.ERROR;
|
||||||
|
|
||||||
|
// Debug mode flag (kept for backward compatibility)
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: Used for backward compatibility
|
||||||
let debugMode = false;
|
let debugMode = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to check if a string is a valid VerbosityLevel key
|
||||||
|
*/
|
||||||
|
export function isVerbosityLevel(value: string): value is keyof typeof VERBOSITY_MAP {
|
||||||
|
return value.toLowerCase() in VERBOSITY_MAP;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a string to VerbosityLevel, returns undefined if invalid
|
||||||
|
*/
|
||||||
|
export function parseVerbosityLevel(value: string): VerbosityLevel | undefined {
|
||||||
|
const normalized = value.toLowerCase();
|
||||||
|
return VERBOSITY_MAP[normalized];
|
||||||
|
}
|
||||||
|
|
||||||
// File handle for log file
|
// File handle for log file
|
||||||
let logFileHandle: fs.WriteStream | null = null;
|
let logFileHandle: fs.WriteStream | null = null;
|
||||||
|
|
||||||
|
|
@ -20,9 +87,17 @@ const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
|
||||||
/**
|
/**
|
||||||
* Initialize the logger - creates log directory and file
|
* Initialize the logger - creates log directory and file
|
||||||
*/
|
*/
|
||||||
export function initLogger(debug: boolean = false): void {
|
export function initLogger(debug: boolean = false, verbosity?: VerbosityLevel): void {
|
||||||
debugMode = debug;
|
debugMode = debug;
|
||||||
|
|
||||||
|
// Set verbosity level
|
||||||
|
if (verbosity !== undefined) {
|
||||||
|
verbosityLevel = verbosity;
|
||||||
|
} else if (debug) {
|
||||||
|
// If debug mode is enabled, set verbosity to DEBUG
|
||||||
|
verbosityLevel = VerbosityLevel.DEBUG;
|
||||||
|
}
|
||||||
|
|
||||||
// If already initialized, just update debug mode and return
|
// If already initialized, just update debug mode and return
|
||||||
if (logFileHandle) {
|
if (logFileHandle) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -39,9 +114,9 @@ export function initLogger(debug: boolean = false): void {
|
||||||
if (fs.existsSync(LOG_FILE)) {
|
if (fs.existsSync(LOG_FILE)) {
|
||||||
fs.unlinkSync(LOG_FILE);
|
fs.unlinkSync(LOG_FILE);
|
||||||
}
|
}
|
||||||
} catch (unlinkError) {
|
} catch {
|
||||||
// Ignore unlink errors - file might not exist or be locked
|
// Ignore unlink errors - file might not exist or be locked
|
||||||
console.debug('Could not delete old log file:', unlinkError);
|
// Don't log here as logger isn't fully initialized yet
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new log file write stream
|
// Create new log file write stream
|
||||||
|
|
@ -132,16 +207,72 @@ function writeToFile(message: string): void {
|
||||||
*/
|
*/
|
||||||
export function setDebugMode(enabled: boolean): void {
|
export function setDebugMode(enabled: boolean): void {
|
||||||
debugMode = enabled;
|
debugMode = enabled;
|
||||||
|
// If enabling debug mode, also set verbosity to DEBUG
|
||||||
|
if (enabled) {
|
||||||
|
verbosityLevel = VerbosityLevel.DEBUG;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set verbosity level
|
||||||
|
*/
|
||||||
|
export function setVerbosityLevel(level: VerbosityLevel): void {
|
||||||
|
verbosityLevel = level;
|
||||||
|
// Update debug mode flag for backward compatibility
|
||||||
|
debugMode = level >= VerbosityLevel.DEBUG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current verbosity level
|
||||||
|
*/
|
||||||
|
export function getVerbosityLevel(): VerbosityLevel {
|
||||||
|
return verbosityLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if debug logging is enabled
|
||||||
|
*/
|
||||||
|
export function isDebugEnabled(): boolean {
|
||||||
|
return verbosityLevel >= VerbosityLevel.DEBUG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if verbose logging is enabled
|
||||||
|
*/
|
||||||
|
export function isVerbose(): boolean {
|
||||||
|
return verbosityLevel >= VerbosityLevel.VERBOSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a log level should be output based on current verbosity
|
||||||
|
*/
|
||||||
|
function shouldLog(level: string): boolean {
|
||||||
|
switch (level) {
|
||||||
|
case 'ERROR':
|
||||||
|
return verbosityLevel >= VerbosityLevel.ERROR;
|
||||||
|
case 'WARN':
|
||||||
|
return verbosityLevel >= VerbosityLevel.WARN;
|
||||||
|
case 'LOG':
|
||||||
|
return verbosityLevel >= VerbosityLevel.INFO;
|
||||||
|
case 'DEBUG':
|
||||||
|
return verbosityLevel >= VerbosityLevel.DEBUG;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log from a specific module (used by client-side API)
|
* Log from a specific module (used by client-side API)
|
||||||
*/
|
*/
|
||||||
export function logFromModule(level: string, module: string, args: unknown[]): void {
|
export function logFromModule(level: string, module: string, args: unknown[]): void {
|
||||||
if (level === 'DEBUG' && !debugMode) return;
|
|
||||||
|
|
||||||
const { console: consoleMsg, file: fileMsg } = formatMessage(level, module, args);
|
const { console: consoleMsg, file: fileMsg } = formatMessage(level, module, args);
|
||||||
|
|
||||||
|
// Always write to file
|
||||||
|
writeToFile(fileMsg);
|
||||||
|
|
||||||
|
// Check if we should output to console based on verbosity
|
||||||
|
if (!shouldLog(level)) return;
|
||||||
|
|
||||||
// Log to console
|
// Log to console
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case 'ERROR':
|
case 'ERROR':
|
||||||
|
|
@ -153,9 +284,6 @@ export function logFromModule(level: string, module: string, args: unknown[]): v
|
||||||
default:
|
default:
|
||||||
console.log(consoleMsg);
|
console.log(consoleMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log to file
|
|
||||||
writeToFile(fileMsg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -167,10 +295,22 @@ export function createLogger(moduleName: string) {
|
||||||
const prefixedModuleName = moduleName.startsWith('[') ? moduleName : `[SRV] ${moduleName}`;
|
const prefixedModuleName = moduleName.startsWith('[') ? moduleName : `[SRV] ${moduleName}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
/**
|
||||||
|
* @deprecated Use info() instead for clarity
|
||||||
|
*/
|
||||||
log: (...args: unknown[]) => {
|
log: (...args: unknown[]) => {
|
||||||
const { console: consoleMsg, file: fileMsg } = formatMessage('LOG', prefixedModuleName, args);
|
const { console: consoleMsg, file: fileMsg } = formatMessage('LOG', prefixedModuleName, args);
|
||||||
console.log(consoleMsg);
|
writeToFile(fileMsg); // Always write to file
|
||||||
writeToFile(fileMsg);
|
if (shouldLog('LOG')) {
|
||||||
|
console.log(consoleMsg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
info: (...args: unknown[]) => {
|
||||||
|
const { console: consoleMsg, file: fileMsg } = formatMessage('LOG', prefixedModuleName, args);
|
||||||
|
writeToFile(fileMsg); // Always write to file
|
||||||
|
if (shouldLog('LOG')) {
|
||||||
|
console.log(consoleMsg);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
warn: (...args: unknown[]) => {
|
warn: (...args: unknown[]) => {
|
||||||
const { console: consoleMsg, file: fileMsg } = formatMessage(
|
const { console: consoleMsg, file: fileMsg } = formatMessage(
|
||||||
|
|
@ -178,8 +318,10 @@ export function createLogger(moduleName: string) {
|
||||||
prefixedModuleName,
|
prefixedModuleName,
|
||||||
args
|
args
|
||||||
);
|
);
|
||||||
console.warn(consoleMsg);
|
writeToFile(fileMsg); // Always write to file
|
||||||
writeToFile(fileMsg);
|
if (shouldLog('WARN')) {
|
||||||
|
console.warn(consoleMsg);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: (...args: unknown[]) => {
|
error: (...args: unknown[]) => {
|
||||||
const { console: consoleMsg, file: fileMsg } = formatMessage(
|
const { console: consoleMsg, file: fileMsg } = formatMessage(
|
||||||
|
|
@ -187,20 +329,23 @@ export function createLogger(moduleName: string) {
|
||||||
prefixedModuleName,
|
prefixedModuleName,
|
||||||
args
|
args
|
||||||
);
|
);
|
||||||
console.error(consoleMsg);
|
writeToFile(fileMsg); // Always write to file
|
||||||
writeToFile(fileMsg);
|
if (shouldLog('ERROR')) {
|
||||||
|
console.error(consoleMsg);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
debug: (...args: unknown[]) => {
|
debug: (...args: unknown[]) => {
|
||||||
if (debugMode) {
|
const { console: consoleMsg, file: fileMsg } = formatMessage(
|
||||||
const { console: consoleMsg, file: fileMsg } = formatMessage(
|
'DEBUG',
|
||||||
'DEBUG',
|
prefixedModuleName,
|
||||||
prefixedModuleName,
|
args
|
||||||
args
|
);
|
||||||
);
|
writeToFile(fileMsg); // Always write to file
|
||||||
|
if (shouldLog('DEBUG')) {
|
||||||
console.log(consoleMsg);
|
console.log(consoleMsg);
|
||||||
writeToFile(fileMsg);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setDebugMode: (enabled: boolean) => setDebugMode(enabled),
|
setDebugMode: (enabled: boolean) => setDebugMode(enabled),
|
||||||
|
setVerbosity: (level: VerbosityLevel) => setVerbosityLevel(level),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
26
web/src/server/utils/verbosity-parser.ts
Normal file
26
web/src/server/utils/verbosity-parser.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { parseVerbosityLevel, VerbosityLevel } from './logger.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse verbosity level from environment variables
|
||||||
|
* Checks VIBETUNNEL_LOG_LEVEL first, then falls back to VIBETUNNEL_DEBUG for backward compatibility
|
||||||
|
* @returns The parsed verbosity level or undefined if not set
|
||||||
|
*/
|
||||||
|
export function parseVerbosityFromEnv(): VerbosityLevel | undefined {
|
||||||
|
// Check VIBETUNNEL_LOG_LEVEL first
|
||||||
|
if (process.env.VIBETUNNEL_LOG_LEVEL) {
|
||||||
|
const parsed = parseVerbosityLevel(process.env.VIBETUNNEL_LOG_LEVEL);
|
||||||
|
if (parsed !== undefined) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
// Warn about invalid value
|
||||||
|
console.warn(`Invalid VIBETUNNEL_LOG_LEVEL: ${process.env.VIBETUNNEL_LOG_LEVEL}`);
|
||||||
|
console.warn('Valid levels: silent, error, warn, info, verbose, debug');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check legacy VIBETUNNEL_DEBUG for backward compatibility
|
||||||
|
if (process.env.VIBETUNNEL_DEBUG === '1' || process.env.VIBETUNNEL_DEBUG === 'true') {
|
||||||
|
return VerbosityLevel.DEBUG;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
109
web/src/test/unit/cli-verbosity.test.ts
Normal file
109
web/src/test/unit/cli-verbosity.test.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { VerbosityLevel } from '../../server/utils/logger';
|
||||||
|
import { parseVerbosityFromEnv } from '../../server/utils/verbosity-parser';
|
||||||
|
|
||||||
|
describe('CLI Verbosity Environment Variables', () => {
|
||||||
|
const originalEnv = process.env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset modules to ensure fresh imports
|
||||||
|
vi.resetModules();
|
||||||
|
// Clone the original env
|
||||||
|
process.env = { ...originalEnv };
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore original env
|
||||||
|
process.env = originalEnv;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getVerbosityFromEnv(): Promise<VerbosityLevel | undefined> {
|
||||||
|
// Use the shared parser
|
||||||
|
return parseVerbosityFromEnv();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('VIBETUNNEL_LOG_LEVEL', () => {
|
||||||
|
it('should parse silent level', async () => {
|
||||||
|
process.env.VIBETUNNEL_LOG_LEVEL = 'silent';
|
||||||
|
const level = await getVerbosityFromEnv();
|
||||||
|
expect(level).toBe(VerbosityLevel.SILENT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse error level', async () => {
|
||||||
|
process.env.VIBETUNNEL_LOG_LEVEL = 'error';
|
||||||
|
const level = await getVerbosityFromEnv();
|
||||||
|
expect(level).toBe(VerbosityLevel.ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse warn level', async () => {
|
||||||
|
process.env.VIBETUNNEL_LOG_LEVEL = 'warn';
|
||||||
|
const level = await getVerbosityFromEnv();
|
||||||
|
expect(level).toBe(VerbosityLevel.WARN);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse info level', async () => {
|
||||||
|
process.env.VIBETUNNEL_LOG_LEVEL = 'info';
|
||||||
|
const level = await getVerbosityFromEnv();
|
||||||
|
expect(level).toBe(VerbosityLevel.INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse verbose level', async () => {
|
||||||
|
process.env.VIBETUNNEL_LOG_LEVEL = 'verbose';
|
||||||
|
const level = await getVerbosityFromEnv();
|
||||||
|
expect(level).toBe(VerbosityLevel.VERBOSE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse debug level', async () => {
|
||||||
|
process.env.VIBETUNNEL_LOG_LEVEL = 'debug';
|
||||||
|
const level = await getVerbosityFromEnv();
|
||||||
|
expect(level).toBe(VerbosityLevel.DEBUG);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be case-insensitive', async () => {
|
||||||
|
process.env.VIBETUNNEL_LOG_LEVEL = 'DEBUG';
|
||||||
|
const level = await getVerbosityFromEnv();
|
||||||
|
expect(level).toBe(VerbosityLevel.DEBUG);
|
||||||
|
|
||||||
|
process.env.VIBETUNNEL_LOG_LEVEL = 'WaRn';
|
||||||
|
const level2 = await getVerbosityFromEnv();
|
||||||
|
expect(level2).toBe(VerbosityLevel.WARN);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined for invalid values', async () => {
|
||||||
|
process.env.VIBETUNNEL_LOG_LEVEL = 'invalid';
|
||||||
|
const level = await getVerbosityFromEnv();
|
||||||
|
expect(level).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('VIBETUNNEL_DEBUG (backward compatibility)', () => {
|
||||||
|
it('should enable debug mode with value 1', async () => {
|
||||||
|
process.env.VIBETUNNEL_DEBUG = '1';
|
||||||
|
const level = await getVerbosityFromEnv();
|
||||||
|
expect(level).toBe(VerbosityLevel.DEBUG);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable debug mode with value true', async () => {
|
||||||
|
process.env.VIBETUNNEL_DEBUG = 'true';
|
||||||
|
const level = await getVerbosityFromEnv();
|
||||||
|
expect(level).toBe(VerbosityLevel.DEBUG);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not enable debug mode with other values', async () => {
|
||||||
|
process.env.VIBETUNNEL_DEBUG = '0';
|
||||||
|
const level = await getVerbosityFromEnv();
|
||||||
|
expect(level).toBeUndefined();
|
||||||
|
|
||||||
|
process.env.VIBETUNNEL_DEBUG = 'false';
|
||||||
|
const level2 = await getVerbosityFromEnv();
|
||||||
|
expect(level2).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT override valid VIBETUNNEL_LOG_LEVEL when set', async () => {
|
||||||
|
process.env.VIBETUNNEL_LOG_LEVEL = 'warn';
|
||||||
|
process.env.VIBETUNNEL_DEBUG = '1';
|
||||||
|
const level = await getVerbosityFromEnv();
|
||||||
|
expect(level).toBe(VerbosityLevel.WARN); // VIBETUNNEL_LOG_LEVEL takes precedence
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
304
web/src/test/unit/logger-verbosity.test.ts
Normal file
304
web/src/test/unit/logger-verbosity.test.ts
Normal file
|
|
@ -0,0 +1,304 @@
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import {
|
||||||
|
createLogger,
|
||||||
|
getVerbosityLevel,
|
||||||
|
initLogger,
|
||||||
|
isDebugEnabled,
|
||||||
|
isVerbose,
|
||||||
|
isVerbosityLevel,
|
||||||
|
parseVerbosityLevel,
|
||||||
|
setVerbosityLevel,
|
||||||
|
VERBOSITY_MAP,
|
||||||
|
VerbosityLevel,
|
||||||
|
} from '../../server/utils/logger';
|
||||||
|
|
||||||
|
describe('Logger Verbosity Control', () => {
|
||||||
|
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
|
||||||
|
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
|
||||||
|
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset to default verbosity
|
||||||
|
setVerbosityLevel(VerbosityLevel.ERROR);
|
||||||
|
|
||||||
|
// Spy on console methods
|
||||||
|
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Verbosity Level Management', () => {
|
||||||
|
it('should default to ERROR level', () => {
|
||||||
|
expect(getVerbosityLevel()).toBe(VerbosityLevel.ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set and get verbosity level', () => {
|
||||||
|
setVerbosityLevel(VerbosityLevel.INFO);
|
||||||
|
expect(getVerbosityLevel()).toBe(VerbosityLevel.INFO);
|
||||||
|
|
||||||
|
setVerbosityLevel(VerbosityLevel.DEBUG);
|
||||||
|
expect(getVerbosityLevel()).toBe(VerbosityLevel.DEBUG);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize with custom verbosity', () => {
|
||||||
|
initLogger(false, VerbosityLevel.WARN);
|
||||||
|
expect(getVerbosityLevel()).toBe(VerbosityLevel.WARN);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set DEBUG verbosity when debug mode is enabled', () => {
|
||||||
|
initLogger(true);
|
||||||
|
expect(getVerbosityLevel()).toBe(VerbosityLevel.DEBUG);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Console Output Control', () => {
|
||||||
|
const logger = createLogger('test-module');
|
||||||
|
|
||||||
|
it('should only show errors at ERROR level', () => {
|
||||||
|
setVerbosityLevel(VerbosityLevel.ERROR);
|
||||||
|
|
||||||
|
logger.log('info message');
|
||||||
|
logger.warn('warning message');
|
||||||
|
logger.error('error message');
|
||||||
|
logger.debug('debug message');
|
||||||
|
|
||||||
|
expect(consoleLogSpy).not.toHaveBeenCalled();
|
||||||
|
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('error message'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show errors and warnings at WARN level', () => {
|
||||||
|
setVerbosityLevel(VerbosityLevel.WARN);
|
||||||
|
|
||||||
|
logger.log('info message');
|
||||||
|
logger.warn('warning message');
|
||||||
|
logger.error('error message');
|
||||||
|
logger.debug('debug message');
|
||||||
|
|
||||||
|
expect(consoleLogSpy).not.toHaveBeenCalled();
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show info, warnings, and errors at INFO level', () => {
|
||||||
|
setVerbosityLevel(VerbosityLevel.INFO);
|
||||||
|
|
||||||
|
logger.log('info message');
|
||||||
|
logger.warn('warning message');
|
||||||
|
logger.error('error message');
|
||||||
|
logger.debug('debug message');
|
||||||
|
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show all messages at DEBUG level', () => {
|
||||||
|
setVerbosityLevel(VerbosityLevel.DEBUG);
|
||||||
|
|
||||||
|
logger.log('info message');
|
||||||
|
logger.warn('warning message');
|
||||||
|
logger.error('error message');
|
||||||
|
logger.debug('debug message');
|
||||||
|
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledTimes(2); // info + debug
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show nothing at SILENT level except critical errors', () => {
|
||||||
|
setVerbosityLevel(VerbosityLevel.SILENT);
|
||||||
|
|
||||||
|
logger.log('info message');
|
||||||
|
logger.warn('warning message');
|
||||||
|
logger.error('error message');
|
||||||
|
logger.debug('debug message');
|
||||||
|
|
||||||
|
// At SILENT level, even regular errors are suppressed
|
||||||
|
expect(consoleLogSpy).not.toHaveBeenCalled();
|
||||||
|
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||||
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show all except debug at VERBOSE level', () => {
|
||||||
|
setVerbosityLevel(VerbosityLevel.VERBOSE);
|
||||||
|
|
||||||
|
logger.log('info message');
|
||||||
|
logger.warn('warning message');
|
||||||
|
logger.error('error message');
|
||||||
|
logger.debug('debug message');
|
||||||
|
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledTimes(1); // only info
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Backward Compatibility', () => {
|
||||||
|
it('should support setDebugMode for backward compatibility', () => {
|
||||||
|
const logger = createLogger('test-module');
|
||||||
|
|
||||||
|
// Enable debug mode
|
||||||
|
logger.setDebugMode(true);
|
||||||
|
expect(getVerbosityLevel()).toBe(VerbosityLevel.DEBUG);
|
||||||
|
|
||||||
|
// Debug messages should now appear
|
||||||
|
logger.debug('debug message');
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('debug message'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Logger Instance Methods', () => {
|
||||||
|
it('should support per-logger verbosity control', () => {
|
||||||
|
const logger = createLogger('test-module');
|
||||||
|
|
||||||
|
// Set verbosity through logger instance
|
||||||
|
logger.setVerbosity(VerbosityLevel.WARN);
|
||||||
|
expect(getVerbosityLevel()).toBe(VerbosityLevel.WARN);
|
||||||
|
|
||||||
|
logger.log('info message');
|
||||||
|
logger.warn('warning message');
|
||||||
|
|
||||||
|
expect(consoleLogSpy).not.toHaveBeenCalled();
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Logger Methods', () => {
|
||||||
|
const logger = createLogger('test-module');
|
||||||
|
|
||||||
|
it('should have info() method that works the same as log()', () => {
|
||||||
|
setVerbosityLevel(VerbosityLevel.INFO);
|
||||||
|
|
||||||
|
logger.info('info message via info()');
|
||||||
|
logger.log('info message via log()');
|
||||||
|
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('info message via info()')
|
||||||
|
);
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('info message via log()'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('info() method should respect verbosity levels', () => {
|
||||||
|
setVerbosityLevel(VerbosityLevel.ERROR);
|
||||||
|
logger.info('hidden info message');
|
||||||
|
expect(consoleLogSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
setVerbosityLevel(VerbosityLevel.INFO);
|
||||||
|
logger.info('visible info message');
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Type Guards and Parsing', () => {
|
||||||
|
it('should correctly identify valid verbosity level strings', () => {
|
||||||
|
expect(isVerbosityLevel('SILENT')).toBe(true);
|
||||||
|
expect(isVerbosityLevel('silent')).toBe(true);
|
||||||
|
expect(isVerbosityLevel('ERROR')).toBe(true);
|
||||||
|
expect(isVerbosityLevel('error')).toBe(true);
|
||||||
|
expect(isVerbosityLevel('WARN')).toBe(true);
|
||||||
|
expect(isVerbosityLevel('warn')).toBe(true);
|
||||||
|
expect(isVerbosityLevel('INFO')).toBe(true);
|
||||||
|
expect(isVerbosityLevel('info')).toBe(true);
|
||||||
|
expect(isVerbosityLevel('VERBOSE')).toBe(true);
|
||||||
|
expect(isVerbosityLevel('verbose')).toBe(true);
|
||||||
|
expect(isVerbosityLevel('DEBUG')).toBe(true);
|
||||||
|
expect(isVerbosityLevel('debug')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject invalid verbosity level strings', () => {
|
||||||
|
expect(isVerbosityLevel('invalid')).toBe(false);
|
||||||
|
expect(isVerbosityLevel('trace')).toBe(false);
|
||||||
|
expect(isVerbosityLevel('log')).toBe(false);
|
||||||
|
expect(isVerbosityLevel('')).toBe(false);
|
||||||
|
expect(isVerbosityLevel('123')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse valid verbosity levels correctly', () => {
|
||||||
|
expect(parseVerbosityLevel('silent')).toBe(VerbosityLevel.SILENT);
|
||||||
|
expect(parseVerbosityLevel('SILENT')).toBe(VerbosityLevel.SILENT);
|
||||||
|
expect(parseVerbosityLevel('error')).toBe(VerbosityLevel.ERROR);
|
||||||
|
expect(parseVerbosityLevel('ERROR')).toBe(VerbosityLevel.ERROR);
|
||||||
|
expect(parseVerbosityLevel('warn')).toBe(VerbosityLevel.WARN);
|
||||||
|
expect(parseVerbosityLevel('WARN')).toBe(VerbosityLevel.WARN);
|
||||||
|
expect(parseVerbosityLevel('info')).toBe(VerbosityLevel.INFO);
|
||||||
|
expect(parseVerbosityLevel('INFO')).toBe(VerbosityLevel.INFO);
|
||||||
|
expect(parseVerbosityLevel('verbose')).toBe(VerbosityLevel.VERBOSE);
|
||||||
|
expect(parseVerbosityLevel('VERBOSE')).toBe(VerbosityLevel.VERBOSE);
|
||||||
|
expect(parseVerbosityLevel('debug')).toBe(VerbosityLevel.DEBUG);
|
||||||
|
expect(parseVerbosityLevel('DEBUG')).toBe(VerbosityLevel.DEBUG);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined for invalid verbosity levels', () => {
|
||||||
|
expect(parseVerbosityLevel('invalid')).toBeUndefined();
|
||||||
|
expect(parseVerbosityLevel('trace')).toBeUndefined();
|
||||||
|
expect(parseVerbosityLevel('')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct VERBOSITY_MAP structure', () => {
|
||||||
|
expect(VERBOSITY_MAP).toEqual({
|
||||||
|
silent: VerbosityLevel.SILENT,
|
||||||
|
error: VerbosityLevel.ERROR,
|
||||||
|
warn: VerbosityLevel.WARN,
|
||||||
|
info: VerbosityLevel.INFO,
|
||||||
|
verbose: VerbosityLevel.VERBOSE,
|
||||||
|
debug: VerbosityLevel.DEBUG,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse using VERBOSITY_MAP', () => {
|
||||||
|
Object.entries(VERBOSITY_MAP).forEach(([key, value]) => {
|
||||||
|
expect(parseVerbosityLevel(key)).toBe(value);
|
||||||
|
expect(parseVerbosityLevel(key.toUpperCase())).toBe(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Helper Functions', () => {
|
||||||
|
it('isDebugEnabled should return true only for DEBUG level', () => {
|
||||||
|
setVerbosityLevel(VerbosityLevel.SILENT);
|
||||||
|
expect(isDebugEnabled()).toBe(false);
|
||||||
|
|
||||||
|
setVerbosityLevel(VerbosityLevel.ERROR);
|
||||||
|
expect(isDebugEnabled()).toBe(false);
|
||||||
|
|
||||||
|
setVerbosityLevel(VerbosityLevel.WARN);
|
||||||
|
expect(isDebugEnabled()).toBe(false);
|
||||||
|
|
||||||
|
setVerbosityLevel(VerbosityLevel.INFO);
|
||||||
|
expect(isDebugEnabled()).toBe(false);
|
||||||
|
|
||||||
|
setVerbosityLevel(VerbosityLevel.VERBOSE);
|
||||||
|
expect(isDebugEnabled()).toBe(false);
|
||||||
|
|
||||||
|
setVerbosityLevel(VerbosityLevel.DEBUG);
|
||||||
|
expect(isDebugEnabled()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isVerbose should return true for VERBOSE and DEBUG levels', () => {
|
||||||
|
setVerbosityLevel(VerbosityLevel.SILENT);
|
||||||
|
expect(isVerbose()).toBe(false);
|
||||||
|
|
||||||
|
setVerbosityLevel(VerbosityLevel.ERROR);
|
||||||
|
expect(isVerbose()).toBe(false);
|
||||||
|
|
||||||
|
setVerbosityLevel(VerbosityLevel.WARN);
|
||||||
|
expect(isVerbose()).toBe(false);
|
||||||
|
|
||||||
|
setVerbosityLevel(VerbosityLevel.INFO);
|
||||||
|
expect(isVerbose()).toBe(false);
|
||||||
|
|
||||||
|
setVerbosityLevel(VerbosityLevel.VERBOSE);
|
||||||
|
expect(isVerbose()).toBe(true);
|
||||||
|
|
||||||
|
setVerbosityLevel(VerbosityLevel.DEBUG);
|
||||||
|
expect(isVerbose()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
90
web/src/test/unit/verbosity-parser.test.ts
Normal file
90
web/src/test/unit/verbosity-parser.test.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { VerbosityLevel } from '../../server/utils/logger';
|
||||||
|
import { parseVerbosityFromEnv } from '../../server/utils/verbosity-parser';
|
||||||
|
|
||||||
|
describe('Verbosity Parser', () => {
|
||||||
|
const originalEnv = process.env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset environment variables before each test
|
||||||
|
process.env = { ...originalEnv };
|
||||||
|
delete process.env.VIBETUNNEL_LOG_LEVEL;
|
||||||
|
delete process.env.VIBETUNNEL_DEBUG;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = originalEnv;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseVerbosityFromEnv', () => {
|
||||||
|
it('should return undefined when no environment variables are set', () => {
|
||||||
|
expect(parseVerbosityFromEnv()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse VIBETUNNEL_LOG_LEVEL correctly', () => {
|
||||||
|
process.env.VIBETUNNEL_LOG_LEVEL = 'info';
|
||||||
|
expect(parseVerbosityFromEnv()).toBe(VerbosityLevel.INFO);
|
||||||
|
|
||||||
|
process.env.VIBETUNNEL_LOG_LEVEL = 'DEBUG';
|
||||||
|
expect(parseVerbosityFromEnv()).toBe(VerbosityLevel.DEBUG);
|
||||||
|
|
||||||
|
process.env.VIBETUNNEL_LOG_LEVEL = 'silent';
|
||||||
|
expect(parseVerbosityFromEnv()).toBe(VerbosityLevel.SILENT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined for invalid VIBETUNNEL_LOG_LEVEL', () => {
|
||||||
|
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
|
process.env.VIBETUNNEL_LOG_LEVEL = 'invalid';
|
||||||
|
expect(parseVerbosityFromEnv()).toBeUndefined();
|
||||||
|
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledWith('Invalid VIBETUNNEL_LOG_LEVEL: invalid');
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||||
|
'Valid levels: silent, error, warn, info, verbose, debug'
|
||||||
|
);
|
||||||
|
|
||||||
|
consoleWarnSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle VIBETUNNEL_DEBUG=1', () => {
|
||||||
|
process.env.VIBETUNNEL_DEBUG = '1';
|
||||||
|
expect(parseVerbosityFromEnv()).toBe(VerbosityLevel.DEBUG);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle VIBETUNNEL_DEBUG=true', () => {
|
||||||
|
process.env.VIBETUNNEL_DEBUG = 'true';
|
||||||
|
expect(parseVerbosityFromEnv()).toBe(VerbosityLevel.DEBUG);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore VIBETUNNEL_DEBUG when set to other values', () => {
|
||||||
|
process.env.VIBETUNNEL_DEBUG = '0';
|
||||||
|
expect(parseVerbosityFromEnv()).toBeUndefined();
|
||||||
|
|
||||||
|
process.env.VIBETUNNEL_DEBUG = 'false';
|
||||||
|
expect(parseVerbosityFromEnv()).toBeUndefined();
|
||||||
|
|
||||||
|
process.env.VIBETUNNEL_DEBUG = 'yes';
|
||||||
|
expect(parseVerbosityFromEnv()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prioritize VIBETUNNEL_LOG_LEVEL over VIBETUNNEL_DEBUG', () => {
|
||||||
|
process.env.VIBETUNNEL_LOG_LEVEL = 'warn';
|
||||||
|
process.env.VIBETUNNEL_DEBUG = '1';
|
||||||
|
expect(parseVerbosityFromEnv()).toBe(VerbosityLevel.WARN);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return DEBUG when VIBETUNNEL_LOG_LEVEL is invalid but VIBETUNNEL_DEBUG is set', () => {
|
||||||
|
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
|
process.env.VIBETUNNEL_LOG_LEVEL = 'invalid';
|
||||||
|
process.env.VIBETUNNEL_DEBUG = '1';
|
||||||
|
expect(parseVerbosityFromEnv()).toBe(VerbosityLevel.DEBUG);
|
||||||
|
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledWith('Invalid VIBETUNNEL_LOG_LEVEL: invalid');
|
||||||
|
|
||||||
|
consoleWarnSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -163,6 +163,8 @@ export async function startTestServer(config: ServerConfig = {}): Promise<Server
|
||||||
VIBETUNNEL_CONTROL_DIR: controlDir,
|
VIBETUNNEL_CONTROL_DIR: controlDir,
|
||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
FORCE_COLOR: '0',
|
FORCE_COLOR: '0',
|
||||||
|
// Ensure INFO verbosity for tests to see server startup messages
|
||||||
|
VIBETUNNEL_LOG_LEVEL: env.VIBETUNNEL_LOG_LEVEL || env.VIBETUNNEL_DEBUG ? undefined : 'info',
|
||||||
...env,
|
...env,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue