lib-cli-exit-tools 2.3.1__tar.gz → 2.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/.codacy/cli-config.yaml +0 -0
  2. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/.codacy/codacy.yaml +0 -0
  3. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/.codacy/tools-configs/lizard.yaml +0 -0
  4. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/.codacy/tools-configs/trivy.yaml +0 -0
  5. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/.data_arch_violations.json +0 -0
  6. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/.devcontainer/devcontainer.json +0 -0
  7. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/.devcontainer/settings.json +0 -0
  8. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/.env.example +0 -0
  9. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/.github/actions/extract-metadata/action.yml +16 -0
  10. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/.github/dependabot.yml +0 -0
  11. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/.github/workflows/codeql.yml +1 -1
  12. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/.github/workflows/default_cicd_public.yml +64 -14
  13. lib_cli_exit_tools-2.3.3/.github/workflows/default_release_public.yml +95 -0
  14. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/.gitignore +6 -1
  15. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/.qlty/qlty.toml +0 -0
  16. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/.snyk +0 -0
  17. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/CHANGELOG.md +17 -1
  18. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/CONTRIBUTING.md +0 -0
  19. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/DEVELOPMENT.md +0 -0
  20. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/INSTALL.md +0 -0
  21. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/LICENSE +0 -0
  22. lib_cli_exit_tools-2.3.3/Makefile +453 -0
  23. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/PKG-INFO +18 -17
  24. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/README.md +5 -4
  25. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/codecov.yml +0 -0
  26. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/docs/system-design/module_reference.md +0 -0
  27. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/notebooks/Quickstart.ipynb +5 -4
  28. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/pyproject.toml +69 -15
  29. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/src/lib_cli_exit_tools/__init__.py +0 -0
  30. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/src/lib_cli_exit_tools/__init__conf__.py +4 -2
  31. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/src/lib_cli_exit_tools/__main__.py +0 -0
  32. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/src/lib_cli_exit_tools/adapters/__init__.py +0 -0
  33. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/src/lib_cli_exit_tools/adapters/signals.py +7 -7
  34. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/src/lib_cli_exit_tools/application/__init__.py +0 -0
  35. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/src/lib_cli_exit_tools/application/runner.py +21 -17
  36. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/src/lib_cli_exit_tools/cli/__init__.py +0 -0
  37. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/src/lib_cli_exit_tools/cli/commands.py +1 -2
  38. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/src/lib_cli_exit_tools/cli/group.py +8 -5
  39. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/src/lib_cli_exit_tools/cli/styling.py +4 -1
  40. lib_cli_exit_tools-2.3.3/src/lib_cli_exit_tools/cli/typed_click.py +51 -0
  41. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/src/lib_cli_exit_tools/core/__init__.py +0 -0
  42. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/src/lib_cli_exit_tools/core/configuration.py +8 -6
  43. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/src/lib_cli_exit_tools/core/exit_codes.py +4 -4
  44. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/src/lib_cli_exit_tools/lib_cli_exit_tools.py +12 -12
  45. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/src/lib_cli_exit_tools/py.typed +0 -0
  46. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/tests/conftest.py +3 -1
  47. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/tests/test_adapters_signals.py +0 -1
  48. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/tests/test_application_runner.py +6 -4
  49. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/tests/test_behaviors.py +7 -6
  50. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/tests/test_cli.py +6 -5
  51. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/tests/test_core_configuration.py +7 -7
  52. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/tests/test_core_exit_codes.py +2 -2
  53. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/tests/test_metadata.py +18 -18
  54. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/tests/test_module_entry.py +4 -3
  55. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/tests/test_signals_integration.py +6 -5
  56. lib_cli_exit_tools-2.3.1/.github/workflows/default_release_public.yml +0 -69
  57. lib_cli_exit_tools-2.3.1/Makefile +0 -260
  58. {lib_cli_exit_tools-2.3.1 → lib_cli_exit_tools-2.3.3}/.codacy/cli.sh +0 -0
@@ -29,6 +29,11 @@ outputs:
29
29
  BANDIT_SKIP_FLAG:
30
30
  description: Bandit skip flag string (e.g. "-s B101,B601") or empty
31
31
  value: ${{ steps.meta.outputs.BANDIT_SKIP_FLAG }}
32
+ PYTEST_MARKER_EXPR:
33
+ description: >-
34
+ Marker expression for the CI test run (e.g. "not local_only and not integration"),
35
+ built from tool.ci.pytest-exclude-markers
36
+ value: ${{ steps.meta.outputs.PYTEST_MARKER_EXPR }}
32
37
 
33
38
  runs:
34
39
  using: composite
@@ -69,6 +74,15 @@ runs:
69
74
  bandit_skips = data.get('tool', {}).get('bandit', {}).get('skips', [])
70
75
  bandit_skip_flag = f"-s {','.join(bandit_skips)}" if bandit_skips else ""
71
76
 
77
+ # --- [tool.ci] section ---
78
+ # CI always skips local_only (those tests need a dev box). A project whose
79
+ # suite also carries markers CI cannot satisfy - integration tests needing a
80
+ # container that a separate workflow starts - names them here, rather than
81
+ # every such repo forking this workflow to edit one -m expression.
82
+ ci_config = data.get('tool', {}).get('ci', {})
83
+ exclude_markers = ['local_only', *ci_config.get('pytest-exclude-markers', [])]
84
+ pytest_marker_expr = ' and '.join(f'not {m}' for m in exclude_markers)
85
+
72
86
  # Write to GITHUB_OUTPUT for composite action outputs
73
87
  with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as out:
74
88
  out.write(f"PROJECT_NAME={project}\n")
@@ -79,6 +93,7 @@ runs:
79
93
  out.write(f"COV_REPORT_FILE={cov_report_file}\n")
80
94
  out.write(f"COV_FAIL_UNDER={cov_fail_under}\n")
81
95
  out.write(f"BANDIT_SKIP_FLAG={bandit_skip_flag}\n")
96
+ out.write(f"PYTEST_MARKER_EXPR={pytest_marker_expr}\n")
82
97
 
83
98
  # Also write to GITHUB_ENV so subsequent steps can use ${{ env.VAR }}
84
99
  with open(os.environ['GITHUB_ENV'], 'a', encoding='utf-8') as env:
@@ -90,3 +105,4 @@ runs:
90
105
  env.write(f"COV_REPORT_FILE={cov_report_file}\n")
91
106
  env.write(f"COV_FAIL_UNDER={cov_fail_under}\n")
92
107
  env.write(f"BANDIT_SKIP_FLAG={bandit_skip_flag}\n")
108
+ env.write(f"PYTEST_MARKER_EXPR={pytest_marker_expr}\n")
@@ -23,7 +23,7 @@ jobs:
23
23
 
24
24
  steps:
25
25
  - name: Checkout repository
26
- uses: actions/checkout@v6
26
+ uses: actions/checkout@v7
27
27
 
28
28
  - name: Initialize CodeQL
29
29
  uses: github/codeql-action/init@v4
@@ -18,8 +18,8 @@ jobs:
18
18
  python-latest: ${{ steps.metadata.outputs.python-latest }}
19
19
  os-matrix: ${{ steps.metadata.outputs.os-matrix }}
20
20
  steps:
21
- - uses: actions/checkout@v6
22
- - uses: actions/setup-python@v6
21
+ - uses: actions/checkout@v7
22
+ - uses: actions/setup-python@v7
23
23
  with:
24
24
  python-version: "3.x"
25
25
  - name: Parse pyproject.toml for Python versions and OS matrix
@@ -66,8 +66,8 @@ jobs:
66
66
  UV_LINK_MODE: "copy"
67
67
  UV_BREAK_SYSTEM_PACKAGES: "1"
68
68
  steps:
69
- - uses: actions/checkout@v6
70
- - uses: actions/setup-python@v6
69
+ - uses: actions/checkout@v7
70
+ - uses: actions/setup-python@v7
71
71
  with:
72
72
  python-version: ${{ matrix.python }}
73
73
  - name: Install uv
@@ -169,7 +169,7 @@ jobs:
169
169
  - name: Restore ruff cache
170
170
  if: runner.environment == 'github-hosted'
171
171
  id: ruff-cache
172
- uses: actions/cache/restore@v5
172
+ uses: actions/cache/restore@v6
173
173
  with:
174
174
  path: .ruff_cache
175
175
  key: ruff-${{ runner.os }}-py${{ matrix.python }}-${{ github.sha }}
@@ -201,7 +201,7 @@ jobs:
201
201
  # and stale journal files cause "database is locked" errors.
202
202
  COVERAGE_FILE: "${{ runner.temp }}/.coverage"
203
203
  run: >-
204
- pytest -m "not local_only"
204
+ pytest -m "${{ env.PYTEST_MARKER_EXPR }}"
205
205
  --cov=${{ env.SRC_PATH }}/${{ env.PACKAGE_MODULE }}
206
206
  --cov-report=term-missing
207
207
  --cov-report=xml:${{ env.COV_REPORT_FILE }}
@@ -214,7 +214,7 @@ jobs:
214
214
  && steps.pytest.outcome == 'success'
215
215
  && matrix.os == 'ubuntu-latest'
216
216
  && matrix.python == needs.setup.outputs.python-latest
217
- uses: codecov/codecov-action@v6
217
+ uses: codecov/codecov-action@v7
218
218
  with:
219
219
  files: ${{ env.COV_REPORT_FILE }}
220
220
  fail_ci_if_error: false
@@ -236,12 +236,62 @@ jobs:
236
236
  continue-on-error: true
237
237
  shell: python
238
238
  run: |
239
- import subprocess, sys
239
+ import subprocess, sys, tempfile
240
240
  import rtoml
241
241
  from pathlib import Path
242
+
243
+ # Audit the project's OWN resolved dependency tree, not the ambient
244
+ # environment. "Install dev deps" uses --system, so the interpreter
245
+ # being audited is the runner image's, and that image ships packages
246
+ # this project neither declares nor distributes (setuptools, pip,
247
+ # wheel). Auditing those reported CVEs no consumer of this project is
248
+ # exposed to, and a scan that is permanently yellow is a scan everyone
249
+ # learns to ignore. Resolving pyproject.toml here keeps the result
250
+ # identical on every OS/python and dependent only on what we declare.
242
251
  data = rtoml.load(Path("pyproject.toml"))
252
+
253
+ # Not every project declares dev dependencies, and those that do may use
254
+ # either an extra or a PEP 735 group. Asking uv for one that does not
255
+ # exist is a hard error ("Requested extra not found"), which would leave
256
+ # the audit permanently broken here rather than merely noisy, so select
257
+ # the flag from what this project actually declares.
258
+ selector = []
259
+ if "dev" in data.get("project", {}).get("optional-dependencies", {}):
260
+ selector = ["--extra", "dev"]
261
+ elif "dev" in data.get("dependency-groups", {}):
262
+ selector = ["--group", "dev"]
263
+
264
+ tmp = Path(tempfile.mkdtemp())
265
+ req = tmp / "compiled-requirements.txt"
266
+ subprocess.check_call(
267
+ ["uv", "pip", "compile", *selector, "pyproject.toml", "-o", str(req), "--quiet"]
268
+ )
269
+
270
+ # Drop direct URL references ("pkg @ git+https://...") before auditing.
271
+ # pip-audit cannot audit those (they are not on PyPI) but, given a
272
+ # requirements file, first tries to `git clone` each one to resolve a
273
+ # version - a slow, credential-dependent fetch. No public project here
274
+ # uses one today, yet "allow-direct-references" is enabled, so this
275
+ # guards the day one appears and keeps this step identical to the
276
+ # default_cicd_private template's.
277
+ audit_req = tmp / "audit-requirements.txt"
278
+ kept, dropped = [], []
279
+ for line in req.read_text(encoding="utf-8").splitlines():
280
+ if " @ " in line and not line.lstrip().startswith("#"):
281
+ dropped.append(line.split(" @ ", 1)[0].strip())
282
+ else:
283
+ kept.append(line)
284
+ audit_req.write_text("\n".join(kept) + "\n", encoding="utf-8")
285
+ if dropped:
286
+ # Never drop coverage silently: name what is not being audited here.
287
+ print(f"::notice::pip-audit: not auditing {len(dropped)} direct-URL dependency/ies "
288
+ f"(audited in their own repo): {', '.join(sorted(dropped))}")
289
+
290
+ req = audit_req
243
291
  vulns = data.get("tool", {}).get("pip-audit", {}).get("ignore-vulns", [])
244
- cmd = ["pip-audit", "--skip-editable"]
292
+ # --no-deps: the compiled file is already a complete pinned resolution,
293
+ # so pip-audit must audit it verbatim rather than re-resolving it.
294
+ cmd = ["pip-audit", "--no-deps", "-r", str(req)]
245
295
  for v in vulns:
246
296
  cmd.extend(["--ignore-vuln", v])
247
297
  sys.exit(subprocess.call(cmd))
@@ -293,7 +343,7 @@ jobs:
293
343
 
294
344
  - name: Save ruff cache
295
345
  if: always() && runner.environment == 'github-hosted' && steps.ruff-cache.outputs.cache-hit != 'true'
296
- uses: actions/cache/save@v5
346
+ uses: actions/cache/save@v6
297
347
  with:
298
348
  path: .ruff_cache
299
349
  key: ruff-${{ runner.os }}-py${{ matrix.python }}-${{ github.sha }}
@@ -302,8 +352,8 @@ jobs:
302
352
  name: pipx/uv verification (ubuntu)
303
353
  runs-on: ubuntu-latest
304
354
  steps:
305
- - uses: actions/checkout@v6
306
- - uses: actions/setup-python@v6
355
+ - uses: actions/checkout@v7
356
+ - uses: actions/setup-python@v7
307
357
  with:
308
358
  python-version: "3.14"
309
359
 
@@ -341,8 +391,8 @@ jobs:
341
391
  name: Execute notebooks (ubuntu, Python 3.14)
342
392
  runs-on: ubuntu-latest
343
393
  steps:
344
- - uses: actions/checkout@v6
345
- - uses: actions/setup-python@v6
394
+ - uses: actions/checkout@v7
395
+ - uses: actions/setup-python@v7
346
396
  with:
347
397
  python-version: "3.14"
348
398
 
@@ -0,0 +1,95 @@
1
+ name: Release
2
+
3
+ # The tag push is the only release event bmk guarantees: it runs
4
+ # `git push <remote> <tag>` unconditionally, whereas `gh release create` sits
5
+ # behind a gh-availability check and runs with check=False, so a missing or
6
+ # failing gh would leave nothing to publish. Listening for `release: published`
7
+ # as well fired a second, redundant run that raced the first to the upload and
8
+ # only ever logged "Skipping ... already exists".
9
+ on:
10
+ push:
11
+ tags:
12
+ - 'v*'
13
+ workflow_dispatch: {}
14
+
15
+ # One publish at a time per tag, so a workflow_dispatch re-run cannot overlap
16
+ # the run the tag push started.
17
+ concurrency:
18
+ group: release-${{ github.ref_name }}
19
+ cancel-in-progress: false
20
+
21
+ jobs:
22
+ build:
23
+ runs-on: ubuntu-latest
24
+ permissions:
25
+ contents: read
26
+ env:
27
+ UV_LINK_MODE: "copy"
28
+ steps:
29
+ - uses: actions/checkout@v7
30
+ - uses: actions/setup-python@v7
31
+ with:
32
+ python-version: '3.14'
33
+
34
+ - name: Install uv
35
+ uses: astral-sh/setup-uv@v7
36
+ with:
37
+ enable-cache: true
38
+ prune-cache: false
39
+
40
+ - name: Build artifacts
41
+ run: uv build
42
+
43
+ - name: Upload artifacts
44
+ uses: actions/upload-artifact@v7
45
+ with:
46
+ name: dist
47
+ path: dist/*
48
+
49
+ - name: Prune uv cache
50
+ continue-on-error: true
51
+ run: uv cache prune --ci
52
+
53
+ publish:
54
+ needs: build
55
+ runs-on: ubuntu-latest
56
+ environment: pypi
57
+ permissions:
58
+ contents: read
59
+ # id-token is required for OIDC / PyPI Trusted Publisher. It is harmless when
60
+ # a PYPI_API_TOKEN secret is configured, in which case the token path runs.
61
+ id-token: write
62
+ steps:
63
+ - name: Select PyPI auth method
64
+ id: auth
65
+ env:
66
+ PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
67
+ run: |
68
+ if [ -n "$PYPI_API_TOKEN" ]; then
69
+ echo "PYPI_API_TOKEN is set; publishing with the API token."
70
+ echo "method=token" >> "$GITHUB_OUTPUT"
71
+ else
72
+ echo "No PYPI_API_TOKEN; publishing via OIDC Trusted Publisher."
73
+ echo "method=oidc" >> "$GITHUB_OUTPUT"
74
+ fi
75
+
76
+ - name: Download artifacts
77
+ uses: actions/download-artifact@v8
78
+ with:
79
+ name: dist
80
+ path: dist/
81
+
82
+ - name: Publish to PyPI (API token)
83
+ if: steps.auth.outputs.method == 'token'
84
+ uses: pypa/gh-action-pypi-publish@release/v1
85
+ with:
86
+ password: ${{ secrets.PYPI_API_TOKEN }}
87
+ attestations: false
88
+ skip-existing: true
89
+
90
+ - name: Publish to PyPI (Trusted Publisher / OIDC)
91
+ if: steps.auth.outputs.method == 'oidc'
92
+ uses: pypa/gh-action-pypi-publish@release/v1
93
+ with:
94
+ attestations: true
95
+ skip-existing: true
@@ -181,4 +181,9 @@ uv.lock
181
181
  /.github_bak/
182
182
  prompts.md
183
183
  CLAUDE.md
184
- .claude/
184
+ .claude/
185
+ # Local, per-developer settings (Claude Code etc.) - never commit
186
+ *.local.json
187
+
188
+ # Python virtual environments (managed by bmk)
189
+ .venv*/
@@ -4,6 +4,22 @@ All notable changes to this project are documented here. The format follows [Kee
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.3.3] 2026-07-24 16:09:16
8
+
9
+ ### Fixed
10
+ - `print_info()` now writes via `sys.stdout.write()` instead of `print()`, and `print_exception_message()`'s `trace_back` / `length_limit` / `stream` parameters are keyword-only, along with several private `application/runner.py` helpers (`_resolve_traceback_choice`, `_render_exception_view`, `_session_config_manager`, `_install_signal_handlers_when_requested`) - closing the ruff PLR0917/FBT001 boolean-positional-argument findings that a newer ruff started reporting. All call sites (in this repo and downstream) already passed these by keyword, so this is not a breaking change.
11
+ - Replaced `typing.Callable`/`Iterable`/`Sequence`/`ContextManager` imports with their `collections.abc`/`contextlib` equivalents (ruff UP035), sorted `__all__` lists (RUF022), quoted `typing.cast()` type arguments (TC006), and moved a handful of type-checking-only imports behind `TYPE_CHECKING` (TC003) across `src` and `tests`.
12
+ - Replaced ambiguous en-dash characters in docstrings with ASCII hyphens (RUF002).
13
+
14
+ ### Changed
15
+ - Added an explicit, curated `[tool.ruff.lint].select` (the fleet-standard rule set) to `pyproject.toml` instead of relying on ruff's default rule set, which had grown to roughly 900 rules on the installed ruff release and was about to make CI red on unrelated, previously-unenforced rule families. Added matching `per-file-ignores` for tests, the CLI layer, `notebooks/Quickstart.ipynb`, and `application/runner.py`'s intentionally-wide dependency-injection seams (`PLR0913`).
16
+ - Documented the three narrow places where suppressing a lint finding is correct rather than fixing it: the `SigIntInterrupt`/`SigTermInterrupt`/`SigBreakInterrupt` exception names stay as-is (`# noqa: N818`) because renaming them would break every downstream `except Sig...Interrupt`; `getattr(signal, "SIGBREAK"/"CTRL_BREAK_EVENT")` and `getattr(exc, "winerror")` stay as `getattr()` (`# noqa: B009`) because typeshed only declares those attributes on Windows/`OSError`, so direct attribute access fails pyright strict on other platforms/types; and the `echo=lambda ...` test double stays a lambda (`# noqa: PLW0108`) because `messages.append` alone cannot accept the `err` keyword the echo protocol requires.
17
+
18
+ ## [2.3.2] 2026-06-14
19
+
20
+ ### Changed
21
+ - Added a `cli/typed_click.py` facade that wraps rich-click's `option` / `version_option` decorators behind explicit, fully-known signatures. This keeps the CLI layer clean under pyright 1.1.410 strict mode (which reports `reportUnknownMemberType` for rich-click's re-exported click decorators) without disabling the rule - the single `# pyright: ignore` is isolated to the facade boundary.
22
+
7
23
  ## [2.3.1] - 2026-04-24
8
24
 
9
25
  ### Fixed
@@ -152,7 +168,7 @@ All notable changes to this project are documented here. The format follows [Kee
152
168
  ### Changed
153
169
  - Hardened `get_system_exit_code` handling for non-integer payloads and switched OS detection to `os.name`.
154
170
  - Updated `_print_output` to decode both `bytes` and `str`, trimming assertions in favour of resilient diagnostics.
155
- - Standardised signal messages (“Aborted (SIGINT).”, etc.) and cached metadata lookups in `__init__conf__`.
171
+ - Standardised signal messages ("Aborted (SIGINT).", etc.) and cached metadata lookups in `__init__conf__`.
156
172
  - Enforced an 85% coverage threshold (in line with `pyproject.toml` and Codecov settings) and removed spurious coverage pragmas to reflect the new test suite.
157
173
  - Repartitioned the library into `core`, `adapters`, and `application` layers with `lib_cli_exit_tools` acting as the facade.
158
174
  - `run_cli` now accepts injectable `exception_handler` and `signal_installer` hooks, and rich-click configuration is applied lazily from `main()`.