From 390deeefd0da332eb23b2408dad3e4f0ab7e4b1d Mon Sep 17 00:00:00 2001 From: Jon Saad-Falcon Date: Sat, 28 Feb 2026 17:31:49 +0000 Subject: [PATCH] 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 --- src/openjarvis/cli/__init__.py | 11 ++++- src/openjarvis/cli/log_config.py | 70 ++++++++++++++++++++++++++++++++ tests/cli/test_log_config.py | 40 ++++++++++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/openjarvis/cli/log_config.py create mode 100644 tests/cli/test_log_config.py diff --git a/src/openjarvis/cli/__init__.py b/src/openjarvis/cli/__init__.py index bab844c0..936a6239 100644 --- a/src/openjarvis/cli/__init__.py +++ b/src/openjarvis/cli/__init__.py @@ -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") diff --git a/src/openjarvis/cli/log_config.py b/src/openjarvis/cli/log_config.py new file mode 100644 index 00000000..6cf6fa15 --- /dev/null +++ b/src/openjarvis/cli/log_config.py @@ -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 diff --git a/tests/cli/test_log_config.py b/tests/cli/test_log_config.py new file mode 100644 index 00000000..7bacfef0 --- /dev/null +++ b/tests/cli/test_log_config.py @@ -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()