Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 38 additions & 16 deletions aw_cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from pathlib import Path
from datetime import datetime
import os
import subprocess

import click
Expand All @@ -13,23 +14,44 @@
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
# If no explicit flag, preserve any inherited AW_PROFILE from the environment.


@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")
Expand Down Expand Up @@ -60,23 +82,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}")

Expand Down
23 changes: 11 additions & 12 deletions aw_cli/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -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-<profile>``), 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]
64 changes: 64 additions & 0 deletions tests/test_cli_log.py
Original file line number Diff line number Diff line change
@@ -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
Loading