mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +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
|
||||
# Run any command in the browser
|
||||
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 --title-mode dynamic claude # See real-time Claude status
|
||||
|
||||
# Control terminal titles
|
||||
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 filter vim # Blocks vim from changing title
|
||||
|
||||
# Shell aliases work automatically!
|
||||
vt claude-danger # Your custom aliases are resolved
|
||||
# Use your shell aliases
|
||||
vt gs # Your 'git status' alias works!
|
||||
vt claude-danger # Custom aliases are resolved
|
||||
|
||||
# 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
|
||||
|
|
@ -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 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!
|
||||
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"
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
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`.
|
||||
|
||||
### 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
|
||||
|
||||
- [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 --help
|
||||
|
||||
QUICK VERBOSITY:
|
||||
-q (quiet), -v (verbose), -vv (extra), -vvv (debug)
|
||||
|
||||
DESCRIPTION:
|
||||
This wrapper script allows VibeTunnel to see the output of commands by
|
||||
forwarding TTY data through the vibetunnel utility. When you run commands
|
||||
|
|
@ -160,12 +163,18 @@ EXAMPLES:
|
|||
vt -i # Launch current shell (short form)
|
||||
vt -S ls -la # List files without shell alias resolution
|
||||
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:
|
||||
--shell, -i Launch current shell (equivalent to vt $SHELL)
|
||||
--no-shell-wrap, -S Execute command directly without shell wrapper
|
||||
--title-mode <mode> Terminal title mode (none, filter, static, dynamic)
|
||||
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
|
||||
|
||||
TITLE MODES:
|
||||
|
|
@ -174,6 +183,20 @@ TITLE MODES:
|
|||
static Show working directory and command in title
|
||||
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:
|
||||
This script automatically detects and uses the best available VibeTunnel installation:
|
||||
- Mac app bundle (preferred on macOS)
|
||||
|
|
@ -236,15 +259,15 @@ resolve_command() {
|
|||
case "$shell_name" in
|
||||
zsh)
|
||||
# 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)
|
||||
# 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
|
||||
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
|
||||
}
|
||||
|
|
@ -262,11 +285,27 @@ if [[ "$1" == "title" ]]; then
|
|||
exit 1
|
||||
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)
|
||||
if [[ "$1" == "--shell" || "$1" == "-i" ]]; then
|
||||
shift
|
||||
# Execute current shell through vibetunnel
|
||||
exec "$0" "${SHELL:-/bin/bash}" "$@"
|
||||
exec "$0" ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} "${SHELL:-/bin/bash}" "$@"
|
||||
fi
|
||||
|
||||
# Handle --no-shell-wrap or -S option
|
||||
|
|
@ -287,12 +326,12 @@ fi
|
|||
if [ $# -gt 0 ] && [[ "$1" != -* ]]; then
|
||||
if [[ "$NO_SHELL_WRAP" == "true" ]]; then
|
||||
# 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
|
||||
# Check if the first argument is a real binary
|
||||
if which "$1" >/dev/null 2>&1; then
|
||||
# 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
|
||||
# Not a real binary, try alias resolution
|
||||
resolve_command "$@"
|
||||
|
|
@ -300,5 +339,5 @@ if [ $# -gt 0 ] && [[ "$1" != -* ]]; then
|
|||
fi
|
||||
else
|
||||
# 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
|
||||
|
|
@ -8,13 +8,18 @@ suppressXtermErrors();
|
|||
|
||||
import { startVibeTunnelForward } from './server/fwd.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';
|
||||
|
||||
// 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';
|
||||
initLogger(debugMode);
|
||||
|
||||
initLogger(debugMode, verbosityLevel);
|
||||
const logger = createLogger('cli');
|
||||
|
||||
// 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);
|
||||
});
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,17 @@ import { SessionManager } from './pty/session-manager.js';
|
|||
import { VibeTunnelSocketClient } from './pty/socket-client.js';
|
||||
import { ActivityDetector } from './utils/activity-detector.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 { generateTitleSequence } from './utils/terminal-title.js';
|
||||
import { parseVerbosityFromEnv } from './utils/verbosity-parser.js';
|
||||
import { BUILD_DATE, GIT_COMMIT, VERSION } from './version.js';
|
||||
|
||||
const logger = createLogger('fwd');
|
||||
|
|
@ -33,7 +41,7 @@ function showUsage() {
|
|||
console.log('');
|
||||
console.log('Usage:');
|
||||
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('Options:');
|
||||
|
|
@ -41,6 +49,12 @@ function showUsage() {
|
|||
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(' --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('Title Modes:');
|
||||
console.log(' none - No title management (default)');
|
||||
|
|
@ -48,9 +62,23 @@ function showUsage() {
|
|||
console.log(' static - Show working directory and command');
|
||||
console.log(' dynamic - Show directory, command, and activity (auto-selected for claude)');
|
||||
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(' VIBETUNNEL_TITLE_MODE=<mode> Set default title mode');
|
||||
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('Examples:');
|
||||
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 --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 --verbosity silent npm test');
|
||||
console.log('');
|
||||
console.log('The command will be spawned in the current working directory');
|
||||
console.log('and managed through the VibeTunnel PTY infrastructure.');
|
||||
}
|
||||
|
||||
export async function startVibeTunnelForward(args: string[]) {
|
||||
// Log startup with version (logger already initialized in cli.ts)
|
||||
if (process.env.VIBETUNNEL_DEBUG === '1' || process.env.VIBETUNNEL_DEBUG === 'true') {
|
||||
// Parse verbosity from environment variables
|
||||
let verbosityLevel = parseVerbosityFromEnv();
|
||||
|
||||
// Set debug mode on logger for backward compatibility
|
||||
if (verbosityLevel === VerbosityLevel.DEBUG) {
|
||||
logger.setDebugMode(true);
|
||||
logger.warn('Debug mode enabled');
|
||||
}
|
||||
|
||||
// Parse command line arguments
|
||||
|
|
@ -84,6 +115,7 @@ export async function startVibeTunnelForward(args: string[]) {
|
|||
let sessionId: string | undefined;
|
||||
let titleMode: TitleMode = TitleMode.NONE;
|
||||
let updateTitle: string | undefined;
|
||||
let logFilePath: string | undefined;
|
||||
let remainingArgs = args;
|
||||
|
||||
// Check environment variables for title mode
|
||||
|
|
@ -123,6 +155,20 @@ export async function startVibeTunnelForward(args: string[]) {
|
|||
process.exit(1);
|
||||
}
|
||||
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 {
|
||||
// Not a flag, must be the start of the command
|
||||
break;
|
||||
|
|
@ -135,6 +181,20 @@ export async function startVibeTunnelForward(args: string[]) {
|
|||
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
|
||||
if (updateTitle !== undefined) {
|
||||
if (!sessionId) {
|
||||
|
|
|
|||
|
|
@ -1029,7 +1029,7 @@ export async function createApp(): Promise<AppInstance> {
|
|||
// Start mDNS advertisement if enabled
|
||||
if (config.enableMDNS) {
|
||||
mdnsService.startAdvertising(actualPort).catch((err) => {
|
||||
logger.error('Failed to start mDNS advertisement:', err);
|
||||
logger.warn('Failed to start mDNS advertisement:', err);
|
||||
});
|
||||
} else {
|
||||
logger.debug('mDNS advertisement disabled');
|
||||
|
|
|
|||
|
|
@ -52,11 +52,11 @@ export class MDNSService {
|
|||
});
|
||||
|
||||
this.service.on('error', (...args: unknown[]) => {
|
||||
log.error('mDNS service error:', args[0]);
|
||||
log.warn('mDNS service error:', args[0]);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Failed to start mDNS advertisement:', error);
|
||||
log.warn('Failed to start mDNS advertisement:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -92,7 +92,7 @@ export class MDNSService {
|
|||
this.isAdvertising = false;
|
||||
log.log('Stopped mDNS advertisement');
|
||||
} 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
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
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
|
||||
*/
|
||||
export function initLogger(debug: boolean = false): void {
|
||||
export function initLogger(debug: boolean = false, verbosity?: VerbosityLevel): void {
|
||||
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 (logFileHandle) {
|
||||
return;
|
||||
|
|
@ -39,9 +114,9 @@ export function initLogger(debug: boolean = false): void {
|
|||
if (fs.existsSync(LOG_FILE)) {
|
||||
fs.unlinkSync(LOG_FILE);
|
||||
}
|
||||
} catch (unlinkError) {
|
||||
} catch {
|
||||
// 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
|
||||
|
|
@ -132,16 +207,72 @@ function writeToFile(message: string): void {
|
|||
*/
|
||||
export function setDebugMode(enabled: boolean): void {
|
||||
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)
|
||||
*/
|
||||
export function logFromModule(level: string, module: string, args: unknown[]): void {
|
||||
if (level === 'DEBUG' && !debugMode) return;
|
||||
|
||||
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
|
||||
switch (level) {
|
||||
case 'ERROR':
|
||||
|
|
@ -153,9 +284,6 @@ export function logFromModule(level: string, module: string, args: unknown[]): v
|
|||
default:
|
||||
console.log(consoleMsg);
|
||||
}
|
||||
|
||||
// Log to file
|
||||
writeToFile(fileMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -167,10 +295,22 @@ export function createLogger(moduleName: string) {
|
|||
const prefixedModuleName = moduleName.startsWith('[') ? moduleName : `[SRV] ${moduleName}`;
|
||||
|
||||
return {
|
||||
/**
|
||||
* @deprecated Use info() instead for clarity
|
||||
*/
|
||||
log: (...args: unknown[]) => {
|
||||
const { console: consoleMsg, file: fileMsg } = formatMessage('LOG', prefixedModuleName, args);
|
||||
console.log(consoleMsg);
|
||||
writeToFile(fileMsg);
|
||||
writeToFile(fileMsg); // Always write to file
|
||||
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[]) => {
|
||||
const { console: consoleMsg, file: fileMsg } = formatMessage(
|
||||
|
|
@ -178,8 +318,10 @@ export function createLogger(moduleName: string) {
|
|||
prefixedModuleName,
|
||||
args
|
||||
);
|
||||
console.warn(consoleMsg);
|
||||
writeToFile(fileMsg);
|
||||
writeToFile(fileMsg); // Always write to file
|
||||
if (shouldLog('WARN')) {
|
||||
console.warn(consoleMsg);
|
||||
}
|
||||
},
|
||||
error: (...args: unknown[]) => {
|
||||
const { console: consoleMsg, file: fileMsg } = formatMessage(
|
||||
|
|
@ -187,20 +329,23 @@ export function createLogger(moduleName: string) {
|
|||
prefixedModuleName,
|
||||
args
|
||||
);
|
||||
console.error(consoleMsg);
|
||||
writeToFile(fileMsg);
|
||||
writeToFile(fileMsg); // Always write to file
|
||||
if (shouldLog('ERROR')) {
|
||||
console.error(consoleMsg);
|
||||
}
|
||||
},
|
||||
debug: (...args: unknown[]) => {
|
||||
if (debugMode) {
|
||||
const { console: consoleMsg, file: fileMsg } = formatMessage(
|
||||
'DEBUG',
|
||||
prefixedModuleName,
|
||||
args
|
||||
);
|
||||
const { console: consoleMsg, file: fileMsg } = formatMessage(
|
||||
'DEBUG',
|
||||
prefixedModuleName,
|
||||
args
|
||||
);
|
||||
writeToFile(fileMsg); // Always write to file
|
||||
if (shouldLog('DEBUG')) {
|
||||
console.log(consoleMsg);
|
||||
writeToFile(fileMsg);
|
||||
}
|
||||
},
|
||||
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,
|
||||
NODE_ENV: 'production',
|
||||
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,
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue