asher-cli 0.0.1__tar.gz → 0.0.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 (55) hide show
  1. asher_cli-0.0.3/.env.example +3 -0
  2. {asher_cli-0.0.1 → asher_cli-0.0.3}/.githooks/pre-push +0 -3
  3. {asher_cli-0.0.1 → asher_cli-0.0.3}/.github/workflows/ci.yml +1 -0
  4. asher_cli-0.0.3/.github/workflows/claude-code-review.yml +44 -0
  5. asher_cli-0.0.3/.github/workflows/claude.yml +50 -0
  6. asher_cli-0.0.3/.github/workflows/coverage.yml +24 -0
  7. asher_cli-0.0.3/.github/workflows/dependency-audit.yml +37 -0
  8. asher_cli-0.0.3/.github/workflows/release.yml +60 -0
  9. {asher_cli-0.0.1 → asher_cli-0.0.3}/CLAUDE.md +44 -7
  10. {asher_cli-0.0.1 → asher_cli-0.0.3}/PKG-INFO +110 -15
  11. asher_cli-0.0.3/README.md +208 -0
  12. {asher_cli-0.0.1 → asher_cli-0.0.3}/ROADMAP.md +2 -0
  13. {asher_cli-0.0.1 → asher_cli-0.0.3}/asher/app.py +7 -4
  14. asher_cli-0.0.3/asher/cats.py +93 -0
  15. asher_cli-0.0.3/asher/commands/__init__.py +442 -0
  16. asher_cli-0.0.3/asher/commands/base.py +81 -0
  17. {asher_cli-0.0.1 → asher_cli-0.0.3}/asher/connection/__init__.py +39 -5
  18. asher_cli-0.0.3/asher/login_flow.py +58 -0
  19. asher_cli-0.0.3/asher/slash-commands/__init__.py +19 -0
  20. asher_cli-0.0.3/asher/ui/__init__.py +253 -0
  21. asher_cli-0.0.3/asher/ui/style.tcss +134 -0
  22. {asher_cli-0.0.1 → asher_cli-0.0.3}/pyproject.toml +18 -3
  23. asher_cli-0.0.3/tests/test_app_pilot.py +211 -0
  24. asher_cli-0.0.3/tests/test_auth.py +61 -0
  25. asher_cli-0.0.3/tests/test_auth_pilot.py +95 -0
  26. asher_cli-0.0.3/tests/test_cats.py +64 -0
  27. asher_cli-0.0.3/tests/test_commands_pilot.py +290 -0
  28. asher_cli-0.0.3/tests/test_connection.py +95 -0
  29. asher_cli-0.0.3/tests/test_connection_mixin.py +19 -0
  30. asher_cli-0.0.3/tests/test_monitoring.py +125 -0
  31. asher_cli-0.0.3/tests/test_ui.py +146 -0
  32. {asher_cli-0.0.1 → asher_cli-0.0.3}/tests/testhelpers.py +29 -1
  33. {asher_cli-0.0.1 → asher_cli-0.0.3}/uv.lock +472 -1
  34. asher_cli-0.0.1/.github/workflows/release.yml +0 -36
  35. asher_cli-0.0.1/README.md +0 -113
  36. asher_cli-0.0.1/asher/cats.py +0 -40
  37. asher_cli-0.0.1/asher/commands/__init__.py +0 -350
  38. asher_cli-0.0.1/asher/slash-commands/__init__.py +0 -26
  39. asher_cli-0.0.1/asher/ui/__init__.py +0 -279
  40. {asher_cli-0.0.1 → asher_cli-0.0.3}/.claude/skills/textual/SKILL.md +0 -0
  41. {asher_cli-0.0.1 → asher_cli-0.0.3}/.gitignore +0 -0
  42. {asher_cli-0.0.1 → asher_cli-0.0.3}/.vscode/launch.json +0 -0
  43. {asher_cli-0.0.1 → asher_cli-0.0.3}/.vscode/settings.json +0 -0
  44. {asher_cli-0.0.1 → asher_cli-0.0.3}/.vscode/tasks.json +0 -0
  45. {asher_cli-0.0.1 → asher_cli-0.0.3}/LICENSE +0 -0
  46. {asher_cli-0.0.1 → asher_cli-0.0.3}/app.py +0 -0
  47. {asher_cli-0.0.1 → asher_cli-0.0.3}/asher/__init__.py +0 -0
  48. {asher_cli-0.0.1 → asher_cli-0.0.3}/asher/__main__.py +0 -0
  49. {asher_cli-0.0.1 → asher_cli-0.0.3}/asher/auth.py +0 -0
  50. {asher_cli-0.0.1 → asher_cli-0.0.3}/asher/helpers.py +0 -0
  51. {asher_cli-0.0.1 → asher_cli-0.0.3}/asher/monitoring/__init__.py +0 -0
  52. {asher_cli-0.0.1 → asher_cli-0.0.3}/requirements.txt +0 -0
  53. {asher_cli-0.0.1 → asher_cli-0.0.3}/test.py +0 -0
  54. {asher_cli-0.0.1 → asher_cli-0.0.3}/tests/__init__.py +0 -0
  55. {asher_cli-0.0.1 → asher_cli-0.0.3}/tests/conftest.py +0 -0
@@ -0,0 +1,3 @@
1
+ LITTER_ROBOT_USER=email
2
+ LITTER_ROBOT_PASSWORD=password
3
+ ASHER_CLI_DEV_MODE=true
@@ -10,7 +10,4 @@ uv run ruff format --check .
10
10
  echo "► mypy"
11
11
  uv run mypy asher/ --ignore-missing-imports
12
12
 
13
- echo "► pytest"
14
- uv run pytest tests/ -v --tb=short
15
-
16
13
  echo "✓ all checks passed"
@@ -19,6 +19,7 @@ jobs:
19
19
  - run: uv run ruff format --check .
20
20
  - run: uv run mypy asher/ --ignore-missing-imports
21
21
 
22
+
22
23
  test:
23
24
  needs: lint
24
25
  strategy:
@@ -0,0 +1,44 @@
1
+ name: Claude Code Review
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, ready_for_review, reopened]
6
+ # Optional: Only run on specific file changes
7
+ # paths:
8
+ # - "src/**/*.ts"
9
+ # - "src/**/*.tsx"
10
+ # - "src/**/*.js"
11
+ # - "src/**/*.jsx"
12
+
13
+ jobs:
14
+ claude-review:
15
+ # Optional: Filter by PR author
16
+ if: |
17
+ github.event.pull_request.user.login == 'external-contributor' ||
18
+ github.event.pull_request.user.login == 'new-developer' ||
19
+ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
20
+
21
+ runs-on: ubuntu-latest
22
+ permissions:
23
+ contents: read
24
+ pull-requests: read
25
+ issues: read
26
+ id-token: write
27
+
28
+ steps:
29
+ - name: Checkout repository
30
+ uses: actions/checkout@v4
31
+ with:
32
+ fetch-depth: 1
33
+
34
+ - name: Run Claude Code Review
35
+ id: claude-review
36
+ uses: anthropics/claude-code-action@v1
37
+ with:
38
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
39
+ plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
40
+ plugins: 'code-review@claude-code-plugins'
41
+ prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
42
+ # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
43
+ # or https://code.claude.com/docs/en/cli-reference for available options
44
+
@@ -0,0 +1,50 @@
1
+ name: Claude Code
2
+
3
+ on:
4
+ issue_comment:
5
+ types: [created]
6
+ pull_request_review_comment:
7
+ types: [created]
8
+ issues:
9
+ types: [opened, assigned]
10
+ pull_request_review:
11
+ types: [submitted]
12
+
13
+ jobs:
14
+ claude:
15
+ if: |
16
+ (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
17
+ (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
18
+ (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19
+ (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
20
+ runs-on: ubuntu-latest
21
+ permissions:
22
+ contents: read
23
+ pull-requests: read
24
+ issues: read
25
+ id-token: write
26
+ actions: read # Required for Claude to read CI results on PRs
27
+ steps:
28
+ - name: Checkout repository
29
+ uses: actions/checkout@v4
30
+ with:
31
+ fetch-depth: 1
32
+
33
+ - name: Run Claude Code
34
+ id: claude
35
+ uses: anthropics/claude-code-action@v1
36
+ with:
37
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38
+
39
+ # This is an optional setting that allows Claude to read CI results on PRs
40
+ additional_permissions: |
41
+ actions: read
42
+
43
+ # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
44
+ # prompt: 'Update the pull request description to include a summary of changes.'
45
+
46
+ # Optional: Add claude_args to customize behavior and configuration
47
+ # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
48
+ # or https://code.claude.com/docs/en/cli-reference for available options
49
+ # claude_args: '--allowed-tools Bash(gh pr *)'
50
+
@@ -0,0 +1,24 @@
1
+ name: Coverage
2
+
3
+ on:
4
+ schedule:
5
+ - cron: "0 6 * * *" # daily at 06:00 UTC
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ coverage:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v3
17
+ - run: uv sync --dev
18
+ - name: Run tests with coverage
19
+ run: uv run pytest tests/ --cov=asher --cov-report=lcov --cov-report=term-missing
20
+ - name: Upload to Coveralls
21
+ uses: coverallsapp/github-action@v2
22
+ with:
23
+ github-token: ${{ secrets.GITHUB_TOKEN }}
24
+ path-to-lcov: coverage.lcov
@@ -0,0 +1,37 @@
1
+ name: Dependency Audit
2
+
3
+ on:
4
+ schedule:
5
+ - cron: "0 0 * * 1" # every Monday at midnight UTC
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: write
10
+ pull-requests: write
11
+
12
+ jobs:
13
+ dependency-audit:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v3
18
+ - run: uv export --format requirements-txt > /tmp/requirements.txt
19
+ - name: Audit Python dependencies
20
+ uses: pypa/gh-action-pip-audit@v1.0.8
21
+ with:
22
+ inputs: /tmp/requirements.txt
23
+ - name: Upgrade lock file
24
+ run: uv lock --upgrade
25
+ - name: Open upgrade PR
26
+ uses: peter-evans/create-pull-request@v7
27
+ with:
28
+ commit-message: "chore: weekly dependency upgrades"
29
+ title: "chore: weekly dependency upgrades"
30
+ body: |
31
+ Automated weekly dependency upgrade via `uv lock --upgrade`.
32
+
33
+ Upgrades packages to their latest versions within the constraints
34
+ defined in `pyproject.toml`. Review the lock file diff before merging.
35
+ branch: chore/weekly-dep-upgrades
36
+ delete-branch: true
37
+ add-paths: uv.lock
@@ -0,0 +1,60 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - "release/*"
7
+
8
+ permissions:
9
+ contents: write # needed to create GitHub Releases
10
+ id-token: write # needed for PyPI OIDC trusted publishing
11
+
12
+ jobs:
13
+ build:
14
+ name: Build distribution
15
+ runs-on: ubuntu-latest
16
+ outputs:
17
+ version: ${{ steps.version.outputs.version }}
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - name: Extract version from branch name
21
+ id: version
22
+ run: echo "version=${GITHUB_REF_NAME#release/}" >> "$GITHUB_OUTPUT"
23
+ - uses: astral-sh/setup-uv@v3
24
+ - run: uv build
25
+ - uses: actions/upload-artifact@v4
26
+ with:
27
+ name: dist
28
+ path: dist/
29
+
30
+ publish:
31
+ name: Publish to PyPI
32
+ needs: build
33
+ runs-on: ubuntu-latest
34
+ environment: pypi
35
+ steps:
36
+ - uses: actions/download-artifact@v4
37
+ with:
38
+ name: dist
39
+ path: dist/
40
+ - uses: pypa/gh-action-pypi-publish@release/v1
41
+
42
+ github-release:
43
+ name: Create GitHub Release
44
+ needs: [build, publish]
45
+ runs-on: ubuntu-latest
46
+ steps:
47
+ - uses: actions/checkout@v4
48
+ - uses: actions/download-artifact@v4
49
+ with:
50
+ name: dist
51
+ path: dist/
52
+ - name: Create release
53
+ env:
54
+ GH_TOKEN: ${{ github.token }}
55
+ VERSION: ${{ needs.build.outputs.version }}
56
+ run: |
57
+ gh release create "v${VERSION}" dist/* \
58
+ --title "v${VERSION}" \
59
+ --generate-notes \
60
+ --verify-tag=false
@@ -10,12 +10,22 @@ Terminal dashboard for Litter Robot (LR3/LR4/LR5) via the Whisker cloud API.
10
10
  - **python-dotenv** — credential loading (`.env` fallback)
11
11
  - **keyring>=24** — OS credential store (Windows Credential Manager / macOS Keychain / Linux Secret Service)
12
12
 
13
+ ## Tooling
14
+
15
+ - **uv** — dependency management and task runner (`uv sync`, `uv run`)
16
+ - **poethepoet** — task aliases via `uv run poe <task>`
17
+ - **ruff** — linter and formatter
18
+ - **mypy** — static type checking
19
+ - **pytest + pytest-asyncio + pytest-cov** — tests
20
+ - **textual-dev** — CSS hot reload devtools
21
+ - **watchfiles** — Python auto-restart on file change
22
+
13
23
  ## Entry points
14
24
 
15
25
  ```
16
26
  python app.py # compatibility shim (calls asher/__main__.py)
17
27
  python -m asher # run as module
18
- asher # after: pip install -e .
28
+ asher # after: uv sync && uv run asher OR pip install -e .
19
29
  ```
20
30
 
21
31
  ## Package structure
@@ -35,8 +45,16 @@ asher/
35
45
  slash-commands/ Convention doc + future slash-command registry
36
46
 
37
47
  tests/
38
- conftest.py shared fixtures (mock_robot, mock_account)
39
- testhelpers.py unit tests for helpers.py
48
+ testhelpers.py unit tests for helpers.py
49
+ test_cats.py CATS dict structure
50
+ test_auth.py LoginScreen CSS / structure
51
+ test_auth_pilot.py Textual Pilot integration tests for LoginScreen
52
+ test_app_pilot.py Textual Pilot integration tests for AsherApp
53
+ test_commands_pilot.py Textual Pilot integration tests for command dispatch
54
+ test_connection.py keyring helper functions
55
+ test_connection_mixin.py ConnectionMixin structure
56
+ test_monitoring.py MonitoringMixin async methods
57
+ test_ui.py UIMixin constants, CSS, helper existence
40
58
 
41
59
  .github/workflows/
42
60
  ci.yml ruff + mypy + pytest on every push/PR
@@ -52,8 +70,8 @@ Priority order on startup:
52
70
 
53
71
  `.env` variable names (for fallback):
54
72
  ```
55
- LITTER_ROBOT_USER=... # or LR4_EMAIL
56
- LITTER_ROBOT_PASSWORD=... # or LR4_PASSWORD
73
+ LITTER_ROBOT_USER=...
74
+ LITTER_ROBOT_PASSWORD=...
57
75
  ```
58
76
 
59
77
  Keyring service name: `asher-cli`, keys `email` and `password`.
@@ -132,6 +150,25 @@ pylitterbot auto-detects robot type. Any attribute/method missing on a given mod
132
150
 
133
151
  **Add a new cat state:** add entry to `CATS` dict in `asher/cats.py` (str for static, list[str] for animated), then call `_set_cat("name", "label")`.
134
152
 
135
- **Run tests:** `pytest tests/` (or `uv run pytest` if using uv).
136
-
137
153
  **File naming convention:** no underscores in filenames (except Python-required `__init__.py` and `__main__.py`).
154
+
155
+ ## Dev workflow
156
+
157
+ ```bash
158
+ uv sync # install all deps (including dev group)
159
+ uv run poe dev # run with CSS hot reload (textual --dev)
160
+ uv run poe watch # run with Python auto-restart on file change (watchfiles)
161
+ uv run poe test # run test suite
162
+ uv run poe check # ruff + mypy + pytest (same as CI)
163
+ uv run poe fix # auto-fix ruff issues
164
+ ```
165
+
166
+ Pre-push hook (`.githooks/pre-push`) runs: ruff check → ruff format --check → mypy. Tests are not in the hook — run them manually.
167
+
168
+ ## Testing notes
169
+
170
+ - Pilot-based integration tests use `app.run_test()` with `await pilot.pause()` before querying widgets
171
+ - Helper app wrappers for screens must **not** start with `Test` (pytest will try to collect them); use e.g. `LoginTestApp`
172
+ - Mock external deps with `unittest.mock.AsyncMock` for async robot/account methods
173
+ - `from pylitterbot import Account` is a local import inside `_connect_worker` — patch it at `pylitterbot.Account`, not `asher.connection.Account`
174
+ - Coverage: ~76% overall; main gaps are async exception paths and `_connect_worker` auth flow
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asher-cli
3
- Version: 0.0.1
3
+ Version: 0.0.3
4
4
  Summary: Terminal dashboard for Litter Robot (LR3/LR4/LR5) via the Whisker cloud API
5
5
  License: MIT License
6
6
 
@@ -43,6 +43,14 @@ Description-Content-Type: text/markdown
43
43
 
44
44
  # Asher CLI
45
45
 
46
+ [![PyPI](https://img.shields.io/badge/PyPI-asher--cli-blue?logo=pypi&logoColor=white)](https://pypi.org/project/asher-cli/)
47
+ ![Version](https://img.shields.io/pypi/v/asher-cli?label=version)
48
+ [![Python](https://img.shields.io/pypi/pyversions/asher-cli)](https://pypi.org/project/asher-cli/)
49
+ [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
50
+ [![CI](https://github.com/karanshukla/asher-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/karanshukla/asher-cli/actions/workflows/ci.yml)
51
+ [![Coverage Status](https://coveralls.io/repos/github/karanshukla/asher-cli/badge.svg?branch=main)](https://coveralls.io/github/karanshukla/asher-cli?branch=main)
52
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
53
+
46
54
  A Claude Code-style terminal dashboard for monitoring and controlling Litter Robot via the Whisker cloud API.
47
55
 
48
56
  <img width="808" height="351" alt="image" src="https://github.com/user-attachments/assets/6599966f-837c-419c-8692-bfda3533e730" />
@@ -60,32 +68,36 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
60
68
 
61
69
  ## Install
62
70
 
71
+ ### With pipx (recommended)
72
+
63
73
  ```bash
64
- pip install asher-cli
74
+ pipx install asher-cli
65
75
  asher
66
76
  ```
67
77
 
68
- Or run from source:
78
+ `pipx` installs the CLI into an isolated environment and puts `asher` on your PATH automatically. Install `pipx` with `pip install pipx` if you don't have it.
79
+
80
+ ### With pip
69
81
 
70
82
  ```bash
71
- git clone https://github.com/karanshukla/asher-cli
72
- cd asher-cli
73
- pip install -e .
83
+ pip install asher-cli
74
84
  asher
75
85
  ```
76
86
 
77
- ### Dev setup
87
+ ### With uv
78
88
 
79
89
  ```bash
80
- uv sync --dev
81
- git config core.hooksPath .githooks # run lint + tests before every push
90
+ uv tool install asher-cli
91
+ asher
82
92
  ```
83
93
 
94
+ ### Run from source
95
+
84
96
  ```bash
85
- uv run poe check # ruff + mypy + pytest (same as CI)
86
- uv run poe fix # auto-fix ruff issues
87
- uv run poe test # tests only
88
- uv run poe types # mypy only
97
+ git clone https://github.com/karanshukla/asher-cli
98
+ cd asher-cli
99
+ uv sync
100
+ uv run asher
89
101
  ```
90
102
 
91
103
  ## Credentials
@@ -141,10 +153,93 @@ git checkout -b release/0.0.2
141
153
  git push origin release/0.0.2
142
154
  ```
143
155
 
144
- ## Testing
156
+ ## Troubleshooting
157
+
158
+ **`asher` not found after `pip install asher-cli`**
159
+
160
+ Python's `Scripts` folder isn't on your PATH. Use `pipx` instead — it handles this automatically:
161
+
162
+ ```bash
163
+ pip install pipx
164
+ pipx install asher-cli
165
+ asher
166
+ ```
167
+
168
+ If you're in a virtualenv, deactivate it first:
169
+
170
+ ```bash
171
+ deactivate
172
+ pip install pipx
173
+ pipx install asher-cli
174
+ ```
175
+
176
+ **`pipx: command not found`**
177
+
178
+ Run it as a module instead:
179
+
180
+ ```bash
181
+ python -m pip install pipx
182
+ python -m pipx install asher-cli
183
+ ```
184
+
185
+ ## Dev Setup
186
+
187
+ ### 1. Clone and install
188
+
189
+ ```bash
190
+ git clone https://github.com/karanshukla/asher-cli
191
+ cd asher-cli
192
+ uv sync # installs all deps including the dev group
193
+ git config core.hooksPath .githooks # lint + type checks run before every push
194
+ ```
195
+
196
+ ### 2. Configure environment (optional)
197
+
198
+ Copy `.env.example` to `.env` and fill in your credentials:
199
+
200
+ ```bash
201
+ cp .env.example .env
202
+ ```
203
+
204
+ ```env
205
+ LITTER_ROBOT_USER=your@email.com
206
+ LITTER_ROBOT_PASSWORD=yourpassword
207
+ ASHER_CLI_DEV_MODE=true # sets version to "dev" instead of the installed package version
208
+ ```
209
+
210
+ ### 3. Run with hot reload
211
+
212
+ **CSS hot reload** — Textual's devtools watch inline `CSS` strings and `.tcss` files and reload them in-place without restarting:
213
+
214
+ ```bash
215
+ uv run poe dev
216
+ # equivalent to: textual run --dev asher/__main__.py
217
+ ```
218
+
219
+ **Python auto-restart** — true in-process reload isn't possible with Textual's event loop, but `watchfiles` will kill and relaunch the app whenever a `.py` file changes in `asher/`:
145
220
 
146
221
  ```bash
147
- uv run pytest
222
+ uv run poe watch
223
+ # equivalent to: watchfiles --filter python 'python -m asher' asher
224
+ ```
225
+
226
+ You can combine both — run `poe watch` for Python changes and it will naturally pick up CSS changes too on restart.
227
+
228
+ ### 4. Run tests
229
+
230
+ ```bash
231
+ uv run poe test # run all tests
232
+ uv run pytest tests/ --cov=asher # with coverage report
233
+ ```
234
+
235
+ ### 5. Lint, format, type-check
236
+
237
+ ```bash
238
+ uv run poe fix # auto-fix ruff issues
239
+ uv run poe lint # check only (no fixes)
240
+ uv run poe fmt # check formatting
241
+ uv run poe types # mypy
242
+ uv run poe check # run all of the above + tests (same as CI)
148
243
  ```
149
244
 
150
245
  CI runs on Python 3.10 / 3.11 / 3.12 across Ubuntu, Windows, and macOS on every push.