feat(cli): add global verbose/quiet logging flags

New log_config.py with setup_logging() configuring the openjarvis
logger (WARNING default, DEBUG on --verbose, ERROR on --quiet).
RotatingFileHandler (5MB, 3 backups) enabled in verbose mode.
Flags added to root CLI group and forwarded via click context.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jon Saad-Falcon
2026-02-28 17:31:49 +00:00
co-authored by Claude Opus 4.6
parent d1d113c1b1
commit 390deeefd0
3 changed files with 120 additions and 1 deletions
+10 -1
View File
@@ -28,8 +28,17 @@ from openjarvis.cli.workflow_cmd import workflow
@click.group(help="OpenJarvis — modular AI assistant backend")
@click.version_option(version=openjarvis.__version__, prog_name="jarvis")
def cli() -> None:
@click.option("--verbose", is_flag=True, default=False, help="Enable debug logging")
@click.option("--quiet", is_flag=True, default=False, help="Suppress non-error output")
@click.pass_context
def cli(ctx: click.Context, verbose: bool, quiet: bool) -> None:
"""Top-level CLI group."""
from openjarvis.cli.log_config import setup_logging
ctx.ensure_object(dict)
ctx.obj["verbose"] = verbose
ctx.obj["quiet"] = quiet
setup_logging(verbose=verbose, quiet=quiet)
cli.add_command(init, "init")
+70
View File
@@ -0,0 +1,70 @@
"""Global logging configuration for the OpenJarvis CLI."""
from __future__ import annotations
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import Optional, Union
def setup_logging(
verbose: bool = False,
quiet: bool = False,
log_file: Optional[Union[str, Path]] = None,
) -> logging.Logger:
"""Configure the ``openjarvis`` logger.
Parameters
----------
verbose:
Set log level to DEBUG.
quiet:
Set log level to ERROR (overrides verbose if both set).
log_file:
Path for a rotating file handler. When *verbose* is ``True``
and no *log_file* is given, defaults to
``~/.openjarvis/cli.log``.
Returns
-------
The configured ``openjarvis`` logger.
"""
logger = logging.getLogger("openjarvis")
# Clear existing handlers to avoid duplication across calls
logger.handlers.clear()
if quiet:
level = logging.ERROR
elif verbose:
level = logging.DEBUG
else:
level = logging.WARNING
logger.setLevel(level)
# Console handler
console_handler = logging.StreamHandler()
console_handler.setLevel(level)
fmt = logging.Formatter("%(levelname)s %(name)s: %(message)s")
console_handler.setFormatter(fmt)
logger.addHandler(console_handler)
# File handler (verbose or explicit path)
if verbose or log_file is not None:
if log_file is None:
log_dir = Path.home() / ".openjarvis"
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / "cli.log"
file_handler = RotatingFileHandler(
str(log_file), maxBytes=5 * 1024 * 1024, backupCount=3,
)
file_handler.setLevel(logging.DEBUG)
file_fmt = logging.Formatter(
"%(asctime)s %(levelname)s %(name)s: %(message)s"
)
file_handler.setFormatter(file_fmt)
logger.addHandler(file_handler)
return logger
+40
View File
@@ -0,0 +1,40 @@
"""Tests for CLI log configuration."""
from __future__ import annotations
import logging
from openjarvis.cli.log_config import setup_logging
class TestSetupLogging:
def test_default_level_is_warning(self):
logger = setup_logging(verbose=False, quiet=False)
assert logger.level == logging.WARNING
def test_verbose_sets_debug(self):
logger = setup_logging(verbose=True, quiet=False)
assert logger.level == logging.DEBUG
def test_quiet_sets_error(self):
logger = setup_logging(verbose=False, quiet=True)
assert logger.level == logging.ERROR
def test_returns_logger(self):
logger = setup_logging(verbose=False, quiet=False)
assert isinstance(logger, logging.Logger)
assert logger.name == "openjarvis"
def test_log_file_handler_on_verbose(self, tmp_path):
log_file = tmp_path / "cli.log"
logger = setup_logging(verbose=True, quiet=False, log_file=log_file)
# Should have at least one file handler
file_handlers = [
h for h in logger.handlers
if hasattr(h, "baseFilename")
]
assert len(file_handlers) >= 1
# Clean up
for h in logger.handlers[:]:
logger.removeHandler(h)
h.close()