asher-cli 0.0.2__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 (53) hide show
  1. asher_cli-0.0.3/.env.example +3 -0
  2. {asher_cli-0.0.2 → asher_cli-0.0.3}/.githooks/pre-push +0 -3
  3. {asher_cli-0.0.2 → 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.2 → asher_cli-0.0.3}/CLAUDE.md +44 -7
  9. {asher_cli-0.0.2 → asher_cli-0.0.3}/PKG-INFO +75 -18
  10. {asher_cli-0.0.2 → asher_cli-0.0.3}/README.md +74 -17
  11. {asher_cli-0.0.2 → asher_cli-0.0.3}/ROADMAP.md +2 -0
  12. {asher_cli-0.0.2 → asher_cli-0.0.3}/asher/app.py +7 -4
  13. asher_cli-0.0.3/asher/cats.py +93 -0
  14. asher_cli-0.0.3/asher/commands/__init__.py +442 -0
  15. asher_cli-0.0.3/asher/commands/base.py +81 -0
  16. {asher_cli-0.0.2 → asher_cli-0.0.3}/asher/connection/__init__.py +39 -5
  17. asher_cli-0.0.3/asher/login_flow.py +58 -0
  18. asher_cli-0.0.3/asher/slash-commands/__init__.py +19 -0
  19. asher_cli-0.0.3/asher/ui/__init__.py +253 -0
  20. asher_cli-0.0.3/asher/ui/style.tcss +134 -0
  21. {asher_cli-0.0.2 → asher_cli-0.0.3}/pyproject.toml +18 -3
  22. asher_cli-0.0.3/tests/test_app_pilot.py +211 -0
  23. asher_cli-0.0.3/tests/test_auth.py +61 -0
  24. asher_cli-0.0.3/tests/test_auth_pilot.py +95 -0
  25. asher_cli-0.0.3/tests/test_cats.py +64 -0
  26. asher_cli-0.0.3/tests/test_commands_pilot.py +290 -0
  27. asher_cli-0.0.3/tests/test_connection.py +95 -0
  28. asher_cli-0.0.3/tests/test_connection_mixin.py +19 -0
  29. asher_cli-0.0.3/tests/test_monitoring.py +125 -0
  30. asher_cli-0.0.3/tests/test_ui.py +146 -0
  31. {asher_cli-0.0.2 → asher_cli-0.0.3}/tests/testhelpers.py +29 -1
  32. {asher_cli-0.0.2 → asher_cli-0.0.3}/uv.lock +472 -1
  33. asher_cli-0.0.2/asher/cats.py +0 -40
  34. asher_cli-0.0.2/asher/commands/__init__.py +0 -350
  35. asher_cli-0.0.2/asher/slash-commands/__init__.py +0 -26
  36. asher_cli-0.0.2/asher/ui/__init__.py +0 -279
  37. {asher_cli-0.0.2 → asher_cli-0.0.3}/.claude/skills/textual/SKILL.md +0 -0
  38. {asher_cli-0.0.2 → asher_cli-0.0.3}/.github/workflows/release.yml +0 -0
  39. {asher_cli-0.0.2 → asher_cli-0.0.3}/.gitignore +0 -0
  40. {asher_cli-0.0.2 → asher_cli-0.0.3}/.vscode/launch.json +0 -0
  41. {asher_cli-0.0.2 → asher_cli-0.0.3}/.vscode/settings.json +0 -0
  42. {asher_cli-0.0.2 → asher_cli-0.0.3}/.vscode/tasks.json +0 -0
  43. {asher_cli-0.0.2 → asher_cli-0.0.3}/LICENSE +0 -0
  44. {asher_cli-0.0.2 → asher_cli-0.0.3}/app.py +0 -0
  45. {asher_cli-0.0.2 → asher_cli-0.0.3}/asher/__init__.py +0 -0
  46. {asher_cli-0.0.2 → asher_cli-0.0.3}/asher/__main__.py +0 -0
  47. {asher_cli-0.0.2 → asher_cli-0.0.3}/asher/auth.py +0 -0
  48. {asher_cli-0.0.2 → asher_cli-0.0.3}/asher/helpers.py +0 -0
  49. {asher_cli-0.0.2 → asher_cli-0.0.3}/asher/monitoring/__init__.py +0 -0
  50. {asher_cli-0.0.2 → asher_cli-0.0.3}/requirements.txt +0 -0
  51. {asher_cli-0.0.2 → asher_cli-0.0.3}/test.py +0 -0
  52. {asher_cli-0.0.2 → asher_cli-0.0.3}/tests/__init__.py +0 -0
  53. {asher_cli-0.0.2 → 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
@@ -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.2
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,6 +68,8 @@ 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
74
  pipx install asher-cli
65
75
  asher
@@ -67,34 +77,27 @@ asher
67
77
 
68
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.
69
79
 
70
- Or with plain pip (ensure Python's `Scripts` folder is on your PATH):
80
+ ### With pip
71
81
 
72
82
  ```bash
73
83
  pip install asher-cli
74
84
  asher
75
85
  ```
76
86
 
77
- Or run from source:
87
+ ### With uv
78
88
 
79
89
  ```bash
80
- git clone https://github.com/karanshukla/asher-cli
81
- cd asher-cli
82
- pip install -e .
90
+ uv tool install asher-cli
83
91
  asher
84
92
  ```
85
93
 
86
- ### Dev setup
94
+ ### Run from source
87
95
 
88
96
  ```bash
89
- uv sync --dev
90
- git config core.hooksPath .githooks # run lint + tests before every push
91
- ```
92
-
93
- ```bash
94
- uv run poe check # ruff + mypy + pytest (same as CI)
95
- uv run poe fix # auto-fix ruff issues
96
- uv run poe test # tests only
97
- 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
98
101
  ```
99
102
 
100
103
  ## Credentials
@@ -179,10 +182,64 @@ python -m pip install pipx
179
182
  python -m pipx install asher-cli
180
183
  ```
181
184
 
182
- ## Testing
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/`:
183
220
 
184
221
  ```bash
185
- 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)
186
243
  ```
187
244
 
188
245
  CI runs on Python 3.10 / 3.11 / 3.12 across Ubuntu, Windows, and macOS on every push.
@@ -1,5 +1,13 @@
1
1
  # Asher CLI
2
2
 
3
+ [![PyPI](https://img.shields.io/badge/PyPI-asher--cli-blue?logo=pypi&logoColor=white)](https://pypi.org/project/asher-cli/)
4
+ ![Version](https://img.shields.io/pypi/v/asher-cli?label=version)
5
+ [![Python](https://img.shields.io/pypi/pyversions/asher-cli)](https://pypi.org/project/asher-cli/)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
7
+ [![CI](https://github.com/karanshukla/asher-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/karanshukla/asher-cli/actions/workflows/ci.yml)
8
+ [![Coverage Status](https://coveralls.io/repos/github/karanshukla/asher-cli/badge.svg?branch=main)](https://coveralls.io/github/karanshukla/asher-cli?branch=main)
9
+ [![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)
10
+
3
11
  A Claude Code-style terminal dashboard for monitoring and controlling Litter Robot via the Whisker cloud API.
4
12
 
5
13
  <img width="808" height="351" alt="image" src="https://github.com/user-attachments/assets/6599966f-837c-419c-8692-bfda3533e730" />
@@ -17,6 +25,8 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
17
25
 
18
26
  ## Install
19
27
 
28
+ ### With pipx (recommended)
29
+
20
30
  ```bash
21
31
  pipx install asher-cli
22
32
  asher
@@ -24,34 +34,27 @@ asher
24
34
 
25
35
  `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.
26
36
 
27
- Or with plain pip (ensure Python's `Scripts` folder is on your PATH):
37
+ ### With pip
28
38
 
29
39
  ```bash
30
40
  pip install asher-cli
31
41
  asher
32
42
  ```
33
43
 
34
- Or run from source:
44
+ ### With uv
35
45
 
36
46
  ```bash
37
- git clone https://github.com/karanshukla/asher-cli
38
- cd asher-cli
39
- pip install -e .
47
+ uv tool install asher-cli
40
48
  asher
41
49
  ```
42
50
 
43
- ### Dev setup
51
+ ### Run from source
44
52
 
45
53
  ```bash
46
- uv sync --dev
47
- git config core.hooksPath .githooks # run lint + tests before every push
48
- ```
49
-
50
- ```bash
51
- uv run poe check # ruff + mypy + pytest (same as CI)
52
- uv run poe fix # auto-fix ruff issues
53
- uv run poe test # tests only
54
- uv run poe types # mypy only
54
+ git clone https://github.com/karanshukla/asher-cli
55
+ cd asher-cli
56
+ uv sync
57
+ uv run asher
55
58
  ```
56
59
 
57
60
  ## Credentials
@@ -136,10 +139,64 @@ python -m pip install pipx
136
139
  python -m pipx install asher-cli
137
140
  ```
138
141
 
139
- ## Testing
142
+ ## Dev Setup
143
+
144
+ ### 1. Clone and install
145
+
146
+ ```bash
147
+ git clone https://github.com/karanshukla/asher-cli
148
+ cd asher-cli
149
+ uv sync # installs all deps including the dev group
150
+ git config core.hooksPath .githooks # lint + type checks run before every push
151
+ ```
152
+
153
+ ### 2. Configure environment (optional)
154
+
155
+ Copy `.env.example` to `.env` and fill in your credentials:
156
+
157
+ ```bash
158
+ cp .env.example .env
159
+ ```
160
+
161
+ ```env
162
+ LITTER_ROBOT_USER=your@email.com
163
+ LITTER_ROBOT_PASSWORD=yourpassword
164
+ ASHER_CLI_DEV_MODE=true # sets version to "dev" instead of the installed package version
165
+ ```
166
+
167
+ ### 3. Run with hot reload
168
+
169
+ **CSS hot reload** — Textual's devtools watch inline `CSS` strings and `.tcss` files and reload them in-place without restarting:
170
+
171
+ ```bash
172
+ uv run poe dev
173
+ # equivalent to: textual run --dev asher/__main__.py
174
+ ```
175
+
176
+ **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/`:
140
177
 
141
178
  ```bash
142
- uv run pytest
179
+ uv run poe watch
180
+ # equivalent to: watchfiles --filter python 'python -m asher' asher
181
+ ```
182
+
183
+ You can combine both — run `poe watch` for Python changes and it will naturally pick up CSS changes too on restart.
184
+
185
+ ### 4. Run tests
186
+
187
+ ```bash
188
+ uv run poe test # run all tests
189
+ uv run pytest tests/ --cov=asher # with coverage report
190
+ ```
191
+
192
+ ### 5. Lint, format, type-check
193
+
194
+ ```bash
195
+ uv run poe fix # auto-fix ruff issues
196
+ uv run poe lint # check only (no fixes)
197
+ uv run poe fmt # check formatting
198
+ uv run poe types # mypy
199
+ uv run poe check # run all of the above + tests (same as CI)
143
200
  ```
144
201
 
145
202
  CI runs on Python 3.10 / 3.11 / 3.12 across Ubuntu, Windows, and macOS on every push.
@@ -1891,5 +1891,7 @@ Ranked by user-visible impact vs. implementation effort:
1891
1891
  22. **Desktop notifications** (§20) — `plyer` toasts + `winsound` bell on fault/cat-detected; `/notify on|off` command
1892
1892
  23. **Dark/light theme toggle** (§11) — CSS variable swap; nice-to-have but not critical
1893
1893
  24. **Startup animation** (§11) — cute but adds friction to quick status checks; could be opt-in
1894
+ 25. **E2E test harness** (§15) — Textual Pilot tests for critical user flows; good for preventing regressions but requires maintenance
1895
+ 26. **Refactor to be more clean code** - example: have a base command class, and have a property to determine whether its a slash command or not, instead of having two separate methods for slash and non-slash commands. This would reduce code duplication and make it easier to add new commands in the future.
1894
1896
 
1895
1897
 
@@ -17,7 +17,7 @@ from .ui import UIMixin
17
17
 
18
18
 
19
19
  class AsherApp(UIMixin, ConnectionMixin, MonitoringMixin, CommandsMixin, App): # type: ignore[type-arg]
20
- CSS = UIMixin.CSS # type: ignore[misc] # must live in AsherApp.__dict__ so Textual gives it full user-CSS priority
20
+ CSS_PATH = "ui/style.tcss"
21
21
  BINDINGS = [
22
22
  Binding("ctrl+c", "quit", "Quit", priority=True),
23
23
  Binding("ctrl+l", "clear_log", "Clear log"),
@@ -31,10 +31,12 @@ class AsherApp(UIMixin, ConnectionMixin, MonitoringMixin, CommandsMixin, App):
31
31
  self._pets: list = []
32
32
  self._cat_mode: str = "idle"
33
33
  self._cat_frame: int = 0
34
+ self._cat_fx_idx: int = 0
34
35
  self._cmd_history: list[str] = []
35
36
  self._hist_idx: int = -1
36
- self._login_state: str = "" # "" | "awaiting_email" | "awaiting_password"
37
- self._login_email: str = ""
37
+ from .login_flow import LoginFlow
38
+
39
+ self._login = LoginFlow()
38
40
  self._last_cat_seen: Any = None
39
41
  self._is_loading: bool = True
40
42
  self._spinner_idx: int = 0
@@ -47,7 +49,7 @@ class AsherApp(UIMixin, ConnectionMixin, MonitoringMixin, CommandsMixin, App):
47
49
  self._show_loading_state()
48
50
  self._connect_worker()
49
51
  self.set_interval(30, self._poll_status_interval)
50
- self.set_interval(0.9, self._tick_cat)
52
+ self.set_interval(0.4, self._tick_cat)
51
53
  # this doesn't work for some reason :(
52
54
  inp = self.query_one("#cmd-input", Input)
53
55
  inp.set_styles(self._INPUT_STYLES)
@@ -62,3 +64,4 @@ class AsherApp(UIMixin, ConnectionMixin, MonitoringMixin, CommandsMixin, App):
62
64
  if self._account:
63
65
  with contextlib.suppress(Exception):
64
66
  await self._account.disconnect()
67
+ print("meow")
@@ -0,0 +1,93 @@
1
+ """ASCII cat art definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def _frame(eyes: str, extra: str = "") -> str:
7
+ """Build a single cat frame with the given eye expression and tail extra."""
8
+ e1, e2 = eyes[0], eyes[2]
9
+ line3 = " \\" + e1 + " " + e2 + "/"
10
+ last = "\\_____/" + extra
11
+ return "\n".join(
12
+ [
13
+ " /\\___/\\",
14
+ " \\/ \\/",
15
+ line3,
16
+ " ==`^ ==",
17
+ " / \\",
18
+ " /| |",
19
+ " || - |",
20
+ " || |",
21
+ " ||| ||_",
22
+ "/\\||_|//",
23
+ last,
24
+ ]
25
+ )
26
+
27
+
28
+ # idle: slow blink cycle (6 frames)
29
+ IDLE = [
30
+ _frame("o o"), # 0 eyes open
31
+ _frame("o o"), # 1
32
+ _frame("- -"), # 2 closing
33
+ _frame("- -"), # 3 closed
34
+ _frame("o o"), # 4 opening
35
+ _frame("o o"), # 5
36
+ ]
37
+
38
+ # happy: eye squint cycle (6 frames)
39
+ HAPPY = [
40
+ _frame("^ ^"), # 0
41
+ _frame("^ ^"), # 1
42
+ _frame("- -"), # 2 squint
43
+ _frame("^ ^"), # 3
44
+ _frame("^ ^"), # 4
45
+ _frame("- -"), # 5 squint
46
+ ]
47
+
48
+ # sleeping: floating zZ (8 frames)
49
+ SLEEPING = [
50
+ _frame("- -", " z"), # 0
51
+ _frame("- -", "zZ"), # 1
52
+ _frame("- -", " Z"), # 2
53
+ _frame("- -", "zZ"), # 3
54
+ _frame("- -", " z"), # 4
55
+ _frame("- -", "zZ"), # 5
56
+ _frame("- -", " Z"), # 6
57
+ _frame("- -", "zZ"), # 7
58
+ ]
59
+
60
+ # cleaning: active cycle (6 frames)
61
+ CLEANING = [
62
+ _frame("@ o"), # 0
63
+ _frame("o @"), # 1
64
+ _frame("* *"), # 2
65
+ _frame("@ o"), # 3
66
+ _frame("o @"), # 4
67
+ _frame("* *"), # 5
68
+ ]
69
+
70
+ # error: alarm flash (4 frames)
71
+ ERROR = [
72
+ _frame("x x"), # 0
73
+ _frame("x x", "!"), # 1 flash
74
+ _frame("x x"), # 2
75
+ _frame("x x", "!"), # 3 flash
76
+ ]
77
+
78
+ # full: agitated eye flash (4 frames)
79
+ FULL = [
80
+ _frame("! !"), # 0
81
+ _frame("x !"), # 1 agitated
82
+ _frame("! !"), # 2
83
+ _frame("! x"), # 3 agitated
84
+ ]
85
+
86
+ CATS: dict[str, list[str]] = {
87
+ "idle": IDLE,
88
+ "happy": HAPPY,
89
+ "sleeping": SLEEPING,
90
+ "cleaning": CLEANING,
91
+ "error": ERROR,
92
+ "full": FULL,
93
+ }