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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and versions are tracked in the repo-root `VERSION` file.
- Keep private runtime files and directories owner-only on POSIX, use inherited
user-profile ACLs on Windows, and make history appends binary-safe across
locking backends.
- Make terminal detection tolerate closed streams and record `COMSPEC` when
Windows has no `SHELL` environment variable.

## [0.2.0] - 2026-08-01

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,8 +499,9 @@ def test_command(tmp_path: Path) -> None:
assert "hello Ada" in result.stdout
```

The helper wraps Click's `CliRunner`, sets `HOME` when requested, and supplies
`cwd` to the invocation for the duration of the test. Calls that use
The helper wraps Click's `CliRunner`, sets `HOME` plus the relevant
`USERPROFILE`, `LOCALAPPDATA`, and `XDG_CACHE_HOME` values when requested, and
supplies `cwd` to the invocation for the duration of the test. Calls that use
`cwd` are serialized and the caller's cwd is restored afterward, but this
remains process-global: do not use it concurrently with code that changes cwd
outside `invoke()` or from threads spawned by the invoked command. A
Expand Down
8 changes: 7 additions & 1 deletion lib/python/base_cli/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def build_finished_record(
"project_root": compact_optional_path(context.project_root),
"manifest": compact_optional_path(context.manifest_path),
"workspace_root": compact_optional_path(context.workspace_root),
"shell": os.environ.get("SHELL"),
"shell": current_shell(),
"scope": context.history_scope,
"parent_run_id": context.history_parent_run_id,
}
Expand Down Expand Up @@ -286,6 +286,12 @@ def normalized_os() -> str:
return system or platform.platform()


def current_shell() -> str | None:
"""Return the active shell identifier across POSIX and Windows."""

return os.environ.get("SHELL") or os.environ.get("COMSPEC")


def redact_history_argv(argv: list[str], sensitive_options: set[str]) -> list[str]:
redacted = redact_argv(argv, sensitive_options)
result: list[str] = []
Expand Down
12 changes: 6 additions & 6 deletions lib/python/base_cli/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ def _handler_formatter(formatter: logging.Formatter | None, *, use_color: bool)


def _use_color(stream: TextIO) -> bool:
return (
os.environ.get("BASE_CLI_COLOR") != "0"
and "NO_COLOR" not in os.environ
and hasattr(stream, "isatty")
and stream.isatty()
)
if os.environ.get("BASE_CLI_COLOR") == "0" or "NO_COLOR" in os.environ:
return False
try:
return bool(stream.isatty())
except (AttributeError, OSError, ValueError):
return False


def secure_log_file_permissions(log_file: Path) -> None:
Expand Down
2 changes: 1 addition & 1 deletion lib/python/base_cli/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def is_terminal(stream: TextIO | None = None) -> bool:
candidate = stream if stream is not None else sys.stdout
try:
return bool(candidate.isatty())
except (AttributeError, OSError):
except (AttributeError, OSError, ValueError):
return False


Expand Down
12 changes: 12 additions & 0 deletions tests/test_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ def locking(self, _fd: int, mode: int, size: int) -> None:


class HistoryAppendTests(unittest.TestCase):
def test_current_shell_falls_back_to_comspec(self) -> None:
with mock.patch.dict("os.environ", {"COMSPEC": r"C:\Windows\System32\cmd.exe"}, clear=True):
self.assertEqual(history.current_shell(), r"C:\Windows\System32\cmd.exe")

def test_current_shell_prefers_shell(self) -> None:
with mock.patch.dict(
"os.environ",
{"SHELL": "/bin/zsh", "COMSPEC": r"C:\Windows\System32\cmd.exe"},
clear=True,
):
self.assertEqual(history.current_shell(), "/bin/zsh")

def test_concurrent_appends_produce_complete_records(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "history.jsonl"
Expand Down
14 changes: 14 additions & 0 deletions tests/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ def test_configure_logger_honors_explicit_color_disable(self) -> None:

self.assertNotIn("\033[", stream.getvalue())

def test_configure_logger_handles_streams_that_reject_isatty(self) -> None:
class ClosedStream(io.StringIO):
def isatty(self) -> bool:
raise ValueError("stream is closed")

stream = ClosedStream()

with mock.patch.dict(os.environ, {}, clear=True):
logger = base_cli.configure_logger("closed-stream", None, debug=False, stream=stream)
logger.info("hello closed stream")

self.assertNotIn("\033[", stream.getvalue())
self.assertIn("hello closed stream", stream.getvalue())

def test_configure_logger_uses_custom_formatter_for_file_handler(self) -> None:
formatter = logging.Formatter("%(levelname)s:%(message)s")
user_stream = io.StringIO()
Expand Down
8 changes: 8 additions & 0 deletions tests/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ def isatty(self) -> bool:
return self.terminal


class _ClosedStream(io.StringIO):
def isatty(self) -> bool:
raise ValueError("stream is closed")


RECORDS = (
{"name": "base", "path": "/work/base"},
{"name": "demo,one", "path": "/work/demo\tone"},
Expand All @@ -26,6 +31,9 @@ def isatty(self) -> bool:


class OutputTest(unittest.TestCase):
def test_closed_stream_is_not_treated_as_terminal(self) -> None:
self.assertEqual(resolve_output_format("text", stream=_ClosedStream()), "tsv")

def test_text_is_pretty_on_terminal(self) -> None:
stream = _Stream(terminal=True)

Expand Down