From e4b08b853a27edb94fd146326d0c5d2a74b7d6d4 Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 25 Aug 2026 14:56:15 +0000 Subject: [PATCH 1/2] feat(cli): add --profile flag to aw-cli, remove testing-based log filename filter --profile NAME selects a named instance profile, resolving to the activitywatch- platform dirs introduced in #149. --testing remains as an alias for --profile testing. The qt subcommand now forwards --profile to aw-qt (which gained profile support in aw-qt#128). The logs subcommand relies on AW_PROFILE being set in the environment (done by the group callback) so get_log_dir() already returns the profile-specific directory; no filename-based filtering is needed. find_oldest_log() drops the `testing` parameter accordingly. Step 6 of ActivityWatch/activitywatch#1399. --- aw_cli/__main__.py | 55 ++++++++++++++++++++++++++----------- aw_cli/log.py | 23 ++++++++-------- tests/test_cli_log.py | 64 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 28 deletions(-) create mode 100644 tests/test_cli_log.py diff --git a/aw_cli/__main__.py b/aw_cli/__main__.py index c302be62..bc40e6c1 100644 --- a/aw_cli/__main__.py +++ b/aw_cli/__main__.py @@ -5,6 +5,7 @@ from pathlib import Path from datetime import datetime +import os import subprocess import click @@ -13,23 +14,45 @@ from typing import Optional +def _resolve_profile(profile: Optional[str], testing: bool) -> str: + """Return the active profile name, resolving --testing as an alias for 'testing'.""" + if profile: + return profile + if testing: + return "testing" + return "" + + @click.group() -@click.option("--testing", is_flag=True) -def main(testing: bool = False): - pass +@click.option( + "--profile", + default=None, + help="Named instance profile (e.g. 'testing', 'research').", +) +@click.option("--testing", is_flag=True, help="Alias for --profile testing.") +def main(profile: Optional[str] = None, testing: bool = False): + active = _resolve_profile(profile, testing) + if active: + os.environ["AW_PROFILE"] = active + else: + os.environ.pop("AW_PROFILE", None) @main.command() @click.pass_context def qt(ctx): - return subprocess.call( - ["aw-qt"] + (["--testing"] if ctx.parent.params["testing"] else []) - ) + profile = ctx.parent.params.get("profile") + testing = ctx.parent.params.get("testing") + active = _resolve_profile(profile, testing) + args = ["aw-qt"] + if active: + args += ["--profile", active] + return subprocess.call(args) @main.command() def directories(): - # Print all directories + # Print all directories (respects AW_PROFILE set by the group callback) from aw_core.dirs import get_data_dir, get_config_dir, get_cache_dir, get_log_dir print("Directory paths used") @@ -60,23 +83,23 @@ def logs( ): from aw_core.dirs import get_log_dir - testing = ctx.parent.params["testing"] + # AW_PROFILE was set by the group callback, so get_log_dir returns the profile-specific dir. logdir: Path = Path(get_log_dir(None)) - # find the oldest logfile in each of the subdirectories in the logging directory, and print the last lines in each one. - if module_name: - print_oldest_log(logdir / module_name, testing, since, level) + _print_newest_log(logdir / module_name, since, level) else: for subdir in sorted(logdir.iterdir()): if subdir.is_dir(): - print_oldest_log(subdir, testing, since, level) + _print_newest_log(subdir, since, level) -def print_oldest_log(path, testing, since, level): - path = find_oldest_log(path, testing) - if path: - print_log(path, since, level) +def _print_newest_log( + path: Path, since: Optional[datetime], level: Optional[str] +) -> None: + logfile = find_oldest_log(path) + if logfile: + print_log(logfile, since, level) else: print(f"No logfile found in {path}") diff --git a/aw_cli/log.py b/aw_cli/log.py index e55593ef..ee8deb9f 100644 --- a/aw_cli/log.py +++ b/aw_cli/log.py @@ -39,20 +39,19 @@ def print_log( print(f" (Filtered {lines_printed}/{len(lines)} lines)") -def find_oldest_log(path: Path, testing=False) -> Path: +def find_oldest_log(path: Path) -> Optional[Path]: + """Return the most-recently-modified log file under *path*. + + Since aw-core #149 each profile runs in its own platform directory + (``activitywatch-``), so no filename-based profile filtering is + needed here — every ``.log`` file in *path* belongs to the active profile. + """ if not path.is_dir(): - return + return None - logfiles = [ - f - for f in path.iterdir() - if f.is_file() - and f.name.endswith(".log") - and ("testing" in f.name if testing else "testing" not in f.name) - ] + logfiles = [f for f in path.iterdir() if f.is_file() and f.name.endswith(".log")] if not logfiles: - return + return None logfiles.sort(key=lambda f: f.stat().st_mtime) - logfile = logfiles[-1] - return logfile + return logfiles[-1] diff --git a/tests/test_cli_log.py b/tests/test_cli_log.py new file mode 100644 index 00000000..9ee5b762 --- /dev/null +++ b/tests/test_cli_log.py @@ -0,0 +1,64 @@ +"""Tests for aw_cli.log.find_oldest_log after profile isolation (#149). + +With per-profile appname dirs, every .log file in a module dir belongs to the +active profile — no filename-based filtering is needed. +""" + +import time +from pathlib import Path + +import pytest + +from aw_cli.log import find_oldest_log + +from . import context # noqa: F401 + + +@pytest.fixture() +def log_dir(tmp_path: Path) -> Path: + return tmp_path / "aw-server" + + +def test_returns_none_for_missing_dir(tmp_path: Path) -> None: + assert find_oldest_log(tmp_path / "nonexistent") is None + + +def test_returns_none_for_empty_dir(log_dir: Path) -> None: + log_dir.mkdir() + assert find_oldest_log(log_dir) is None + + +def test_returns_single_log(log_dir: Path) -> None: + log_dir.mkdir() + f = log_dir / "aw-server_2026-01-01.log" + f.write_text("line\n") + assert find_oldest_log(log_dir) == f + + +def test_returns_newest_by_mtime(log_dir: Path) -> None: + log_dir.mkdir() + old = log_dir / "aw-server_old.log" + new = log_dir / "aw-server_new.log" + old.write_text("old\n") + time.sleep(0.01) + new.write_text("new\n") + assert find_oldest_log(log_dir) == new + + +def test_ignores_non_log_files(log_dir: Path) -> None: + log_dir.mkdir() + (log_dir / "notes.txt").write_text("not a log") + assert find_oldest_log(log_dir) is None + + +def test_no_profile_filtering_in_filenames(log_dir: Path) -> None: + """All .log files in the dir belong to the active profile — no name filter.""" + log_dir.mkdir() + default_log = log_dir / "aw-server_2026-01-01.log" + default_log.write_text("line\n") + # A file named with 'testing' in its name is still returned; + # profile selection happens at the directory level, not the filename level. + testing_named = log_dir / "aw-server_testing_2026-01-02.log" + time.sleep(0.01) + testing_named.write_text("testing-named\n") + assert find_oldest_log(log_dir) == testing_named From cbac39f17b181234378996ef7f3638bb0b8de423 Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 25 Aug 2026 15:06:59 +0000 Subject: [PATCH 2/2] fix(cli): preserve inherited AW_PROFILE when no --profile flag given Without this fix, running 'aw-cli ' with AW_PROFILE already set in the environment would silently clear it, making directory/log lookups fall back to the default profile instead of the inherited one. Addresses Greptile's inherited-profile regression note on #151. --- aw_cli/__main__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aw_cli/__main__.py b/aw_cli/__main__.py index bc40e6c1..acea0eae 100644 --- a/aw_cli/__main__.py +++ b/aw_cli/__main__.py @@ -34,8 +34,7 @@ def main(profile: Optional[str] = None, testing: bool = False): active = _resolve_profile(profile, testing) if active: os.environ["AW_PROFILE"] = active - else: - os.environ.pop("AW_PROFILE", None) + # If no explicit flag, preserve any inherited AW_PROFILE from the environment. @main.command()