asher-cli 1.0.2__tar.gz → 1.2.0__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.
- asher_cli-1.2.0/.claude/hooks/block-env.py +14 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/.claude/settings.json +1 -1
- asher_cli-1.2.0/.claude/skills/mcp-bridge/SKILL.md +15 -0
- asher_cli-1.2.0/.github/FUNDING.yml +15 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/.github/workflows/ci.yml +8 -2
- {asher_cli-1.0.2 → asher_cli-1.2.0}/.github/workflows/claude-code-review.yml +1 -1
- {asher_cli-1.0.2 → asher_cli-1.2.0}/.github/workflows/claude.yml +1 -1
- {asher_cli-1.0.2 → asher_cli-1.2.0}/.github/workflows/coverage.yml +6 -1
- {asher_cli-1.0.2 → asher_cli-1.2.0}/.github/workflows/release.yml +16 -13
- {asher_cli-1.0.2 → asher_cli-1.2.0}/CHANGELOG.md +57 -6
- {asher_cli-1.0.2 → asher_cli-1.2.0}/CLAUDE.md +74 -58
- {asher_cli-1.0.2 → asher_cli-1.2.0}/PKG-INFO +142 -28
- {asher_cli-1.0.2 → asher_cli-1.2.0}/README.md +133 -24
- asher_cli-1.2.0/asher/__main__.py +179 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/activity_labels.py +22 -20
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/app.py +10 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/auth.py +13 -11
- asher_cli-1.2.0/asher/autostart.py +370 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/commands/__init__.py +107 -52
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/completion.py +8 -4
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/config.py +10 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/connection/__init__.py +48 -15
- asher_cli-1.2.0/asher/constants.py +24 -0
- asher_cli-1.2.0/asher/daemon.py +429 -0
- asher_cli-1.2.0/asher/desktoptheme.py +192 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/export.py +17 -42
- asher_cli-1.2.0/asher/headless.py +626 -0
- asher_cli-1.2.0/asher/helpers.py +71 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/history_view.py +19 -18
- asher_cli-1.2.0/asher/launcher.py +133 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/monitoring/__init__.py +49 -34
- asher_cli-1.2.0/asher/notifications.py +120 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/slash-commands/__init__.py +2 -0
- asher_cli-1.2.0/asher/theme.py +138 -0
- asher_cli-1.2.0/asher/tray.py +264 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/ui/__init__.py +71 -59
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/ui/style.tcss +26 -26
- asher_cli-1.2.0/asher/updates.py +181 -0
- asher_cli-1.2.0/asher/watcher.py +393 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/cliff.toml +3 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/docs/ROADMAP.md +3 -1
- {asher_cli-1.0.2 → asher_cli-1.2.0}/pyproject.toml +34 -4
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_activity_labels.py +6 -5
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_app_pilot.py +3 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_auth.py +15 -4
- asher_cli-1.2.0/tests/test_autostart.py +341 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_commands_pilot.py +3 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_config.py +4 -4
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_connection.py +77 -0
- asher_cli-1.2.0/tests/test_daemon.py +445 -0
- asher_cli-1.2.0/tests/test_desktoptheme.py +228 -0
- asher_cli-1.2.0/tests/test_headless.py +529 -0
- asher_cli-1.2.0/tests/test_launcher.py +203 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_mcp_command.py +14 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_monitoring.py +7 -0
- asher_cli-1.2.0/tests/test_notifications.py +176 -0
- asher_cli-1.2.0/tests/test_tray.py +305 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_ui.py +8 -4
- asher_cli-1.2.0/tests/test_updates.py +272 -0
- asher_cli-1.2.0/tests/test_watcher.py +379 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/testhelpers.py +26 -5
- {asher_cli-1.0.2 → asher_cli-1.2.0}/uv.lock +1121 -683
- asher_cli-1.0.2/.claude/hooks/block-env.ps1 +0 -9
- asher_cli-1.0.2/.claude/skills/release/SKILL.md +0 -43
- asher_cli-1.0.2/asher/__main__.py +0 -58
- asher_cli-1.0.2/asher/constants.py +0 -22
- asher_cli-1.0.2/asher/helpers.py +0 -45
- asher_cli-1.0.2/asher/notifications.py +0 -48
- asher_cli-1.0.2/tests/test_notifications.py +0 -82
- {asher_cli-1.0.2 → asher_cli-1.2.0}/.claude/skills/pylitterbot-ref/SKILL.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/.claude/skills/textual/SKILL.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/.env.example +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/.githooks/pre-push +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/.github/pull_request_template.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/.github/workflows/bandit.yml +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/.gitignore +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/.vscode/launch.json +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/.vscode/settings.json +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/.vscode/tasks.json +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/CODEOWNERS +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/LICENSE +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/app.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/__init__.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/cats.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/commands/base.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/faults.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/login_flow.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/mcp_bridge.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/mcp_config.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/robot_adapters.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/asher/robot_protocol.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/docs/roadmap-archive/README.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/docs/roadmap-archive/cat-panel-badges.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/docs/roadmap-archive/config-persistence.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/docs/roadmap-archive/desktop-notifications.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/docs/roadmap-archive/fault-monitoring.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/docs/roadmap-archive/headless-export.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/docs/roadmap-archive/history-export.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/docs/roadmap-archive/tab-completion.md +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/renovate.json +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/requirements.txt +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/test.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/__init__.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/conftest.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_auth_pilot.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_cats.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_completion.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_connection_mixin.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_export.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_faults.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_history_view.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_lr5_commands.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_mcp_bridge.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_mcp_config.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_missing_robot_commands.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_new_commands_pilot.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_robot_adapters.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/tests/test_version_command.py +0 -0
- {asher_cli-1.0.2 → asher_cli-1.2.0}/watchrun.py +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
payload = json.load(sys.stdin)
|
|
6
|
+
file_path = payload.get("file_path", "")
|
|
7
|
+
except (json.JSONDecodeError, ValueError):
|
|
8
|
+
sys.exit(0)
|
|
9
|
+
|
|
10
|
+
if file_path.endswith(".env"):
|
|
11
|
+
print("Blocked: .env contains real credentials — edit it manually if needed")
|
|
12
|
+
sys.exit(2)
|
|
13
|
+
|
|
14
|
+
sys.exit(0)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mcp-bridge
|
|
3
|
+
description: How asher-cli's /mcp command and the asher-mcp-launch credential bridge work. Load before changing asher/mcp_config.py, asher/mcp_bridge.py, or the /mcp slash command.
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# MCP bridge (`/mcp`)
|
|
8
|
+
|
|
9
|
+
pylitterbot ships an optional MCP server (`pip install pylitterbot[mcp]`, run via `python -m pylitterbot.mcp`) that lets an MCP client like Claude Desktop monitor/control the robot directly. Its own docs configure it with plaintext credentials in the client's JSON config — asher-cli avoids that:
|
|
10
|
+
|
|
11
|
+
- `/mcp on|off|status` (in `asher/commands/__init__.py`, logic in `asher/mcp_config.py`) adds/removes an entry (named by `mcp_config._SERVER_NAME`) in every `claude_desktop_config.json` this OS's Claude Desktop might read (`mcp_config.config_paths()` — on Windows this includes both the standard installer path and any MSIX/Microsoft Store virtualized path). The entry's `command` is `sys.executable -m asher.mcp_bridge` — never the credentials themselves.
|
|
12
|
+
- `/mcp on` also auto-installs pylitterbot's `mcp` extra via `sys.executable -m pip install "pylitterbot[mcp]==<installed version>"` if the `mcp` package isn't importable yet.
|
|
13
|
+
- `asher/mcp_bridge.py` (console script `asher-mcp-launch`) is what Claude Desktop actually spawns. It reads email/password from the OS keyring at process start, sets them as `LITTER_ROBOT_USERNAME`/`LITTER_ROBOT_PASSWORD` (pylitterbot's expected names — note these differ from asher-cli's own `.env` var `LITTER_ROBOT_USER`) in that process's environment only, then execs `python -m pylitterbot.mcp`. No credentials ever touch the on-disk MCP config.
|
|
14
|
+
- `/mcp on` requires keyring credentials. If none are found but `.env` fallback credentials are set, it copies them into the keyring automatically (since the bridge process can't reliably discover a project-relative `.env` — Claude Desktop controls its working directory, not asher-cli).
|
|
15
|
+
- Requires the `mcp` extra: `uv sync --extra mcp` / `pip install asher-cli[mcp]`. Restart Claude Desktop after toggling for the change to take effect.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# These are supported funding model platforms
|
|
2
|
+
|
|
3
|
+
github: [karanshukla]
|
|
4
|
+
patreon: # Replace with a single Patreon username
|
|
5
|
+
open_collective: # Replace with a single Open Collective username
|
|
6
|
+
ko_fi: # Replace with a single Ko-fi username
|
|
7
|
+
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
8
|
+
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
9
|
+
liberapay: # Replace with a single Liberapay username
|
|
10
|
+
issuehunt: # Replace with a single IssueHunt username
|
|
11
|
+
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
|
12
|
+
polar: # Replace with a single Polar username
|
|
13
|
+
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
|
14
|
+
thanks_dev: # Replace with a single thanks.dev username
|
|
15
|
+
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
@@ -8,13 +8,19 @@ on:
|
|
|
8
8
|
permissions:
|
|
9
9
|
contents: read
|
|
10
10
|
|
|
11
|
+
# `uv run` re-syncs to the default groups on every invocation, which would drag
|
|
12
|
+
# the `tray` group (and so a PyGObject/pycairo source build) back in after the
|
|
13
|
+
# explicit sync below deliberately left it out. Sync once, then never re-sync.
|
|
14
|
+
env:
|
|
15
|
+
UV_NO_SYNC: "1"
|
|
16
|
+
|
|
11
17
|
jobs:
|
|
12
18
|
lint:
|
|
13
19
|
runs-on: ubuntu-latest
|
|
14
20
|
steps:
|
|
15
21
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
16
22
|
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
17
|
-
- run: uv sync --dev
|
|
23
|
+
- run: uv sync --no-default-groups --group dev
|
|
18
24
|
- run: uv run ruff check .
|
|
19
25
|
- run: uv run ruff format --check .
|
|
20
26
|
- run: uv run mypy asher/ --ignore-missing-imports
|
|
@@ -30,5 +36,5 @@ jobs:
|
|
|
30
36
|
steps:
|
|
31
37
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
32
38
|
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
33
|
-
- run: uv sync --dev
|
|
39
|
+
- run: uv sync --no-default-groups --group dev
|
|
34
40
|
- run: uv run pytest tests/ -v --tb=short
|
|
@@ -33,7 +33,7 @@ jobs:
|
|
|
33
33
|
|
|
34
34
|
- name: Run Claude Code Review
|
|
35
35
|
id: claude-review
|
|
36
|
-
uses: anthropics/claude-code-action@
|
|
36
|
+
uses: anthropics/claude-code-action@6b082c41935b4c8a3b8b0ef85ba4ba4d9eeb8975 # v1
|
|
37
37
|
with:
|
|
38
38
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
39
39
|
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
|
|
@@ -32,7 +32,7 @@ jobs:
|
|
|
32
32
|
|
|
33
33
|
- name: Run Claude Code
|
|
34
34
|
id: claude
|
|
35
|
-
uses: anthropics/claude-code-action@
|
|
35
|
+
uses: anthropics/claude-code-action@6b082c41935b4c8a3b8b0ef85ba4ba4d9eeb8975 # v1
|
|
36
36
|
with:
|
|
37
37
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
38
38
|
|
|
@@ -8,13 +8,18 @@ on:
|
|
|
8
8
|
permissions:
|
|
9
9
|
contents: read
|
|
10
10
|
|
|
11
|
+
# See ci.yml: `uv run` would otherwise re-sync the `tray` group back in and try
|
|
12
|
+
# to build PyGObject from source.
|
|
13
|
+
env:
|
|
14
|
+
UV_NO_SYNC: "1"
|
|
15
|
+
|
|
11
16
|
jobs:
|
|
12
17
|
coverage:
|
|
13
18
|
runs-on: ubuntu-latest
|
|
14
19
|
steps:
|
|
15
20
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
16
21
|
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
17
|
-
- run: uv sync --dev
|
|
22
|
+
- run: uv sync --no-default-groups --group dev
|
|
18
23
|
- name: Run tests with coverage
|
|
19
24
|
run: uv run pytest tests/ --cov=asher --cov-report=lcov --cov-report=term-missing
|
|
20
25
|
- name: Upload to Coveralls
|
|
@@ -37,33 +37,36 @@ jobs:
|
|
|
37
37
|
with:
|
|
38
38
|
name: dist
|
|
39
39
|
path: dist/
|
|
40
|
-
- uses: pypa/gh-action-pypi-publish@
|
|
40
|
+
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
|
|
41
41
|
|
|
42
42
|
github-release:
|
|
43
43
|
name: Create GitHub Release
|
|
44
44
|
needs: [build, publish]
|
|
45
45
|
runs-on: ubuntu-latest
|
|
46
46
|
steps:
|
|
47
|
-
# Full history (fetch-depth: 0) so git-cliff can walk commits/tags.
|
|
48
47
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
49
|
-
with:
|
|
50
|
-
fetch-depth: 0
|
|
51
48
|
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
52
49
|
with:
|
|
53
50
|
name: dist
|
|
54
51
|
path: dist/
|
|
55
|
-
-
|
|
56
|
-
- name: Install git-cliff
|
|
57
|
-
run: uv tool install git-cliff
|
|
58
|
-
- name: Generate release notes with git-cliff
|
|
52
|
+
- name: Extract release notes from CHANGELOG.md
|
|
59
53
|
env:
|
|
60
54
|
VERSION: ${{ needs.build.outputs.version }}
|
|
61
55
|
run: |
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
|
|
56
|
+
# CHANGELOG.md is the single source of release prose: it is generated
|
|
57
|
+
# by git-cliff (see `poe changelog-release`) but may be hand-refined
|
|
58
|
+
# before the release is cut, and re-deriving the notes from commits
|
|
59
|
+
# here would silently discard those edits.
|
|
60
|
+
awk -v hdr="## [${VERSION}]" '
|
|
61
|
+
index($0, hdr) == 1 { found = 1; next }
|
|
62
|
+
found && index($0, "## [") == 1 { exit }
|
|
63
|
+
found { print }
|
|
64
|
+
' CHANGELOG.md > RELEASE_NOTES.md
|
|
65
|
+
if [ ! -s RELEASE_NOTES.md ]; then
|
|
66
|
+
echo "::error::CHANGELOG.md has no '## [${VERSION}]' section." \
|
|
67
|
+
"Run 'uv run poe changelog-release ${VERSION}' and commit it before releasing."
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
67
70
|
- name: Create release
|
|
68
71
|
env:
|
|
69
72
|
GH_TOKEN: ${{ github.token }}
|
|
@@ -7,6 +7,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
Generated by [git-cliff](https://git-cliff.org) from conventional commits.
|
|
9
9
|
|
|
10
|
+
## [1.2.0] - 2026-08-16
|
|
11
|
+
|
|
12
|
+
### 🚀 Features
|
|
13
|
+
|
|
14
|
+
- Drop "Clean now" from the tray menu
|
|
15
|
+
- Refuse to start a watcher that has no credentials
|
|
16
|
+
- Open the dashboard from the tray
|
|
17
|
+
- Tone the tray icon to the desktop panel, and stop repeating the drawer
|
|
18
|
+
- [**breaking**] Take credentials only from the keyring outside dev mode
|
|
19
|
+
- Start the watcher at login, and notify (never install) on new releases
|
|
20
|
+
- Background watcher and system-tray icon for notifications
|
|
21
|
+
|
|
22
|
+
### 🐛 Bug Fixes
|
|
23
|
+
|
|
24
|
+
- Keep the tray installed across a plain `uv run`
|
|
25
|
+
- Read the selected completion row against its own background
|
|
26
|
+
- *(deps)* Update dependency pylitterbot to v2025.6.4
|
|
27
|
+
|
|
28
|
+
### 📚 Documentation
|
|
29
|
+
|
|
30
|
+
- Trim the README, move the packaging detail to CLAUDE.md
|
|
31
|
+
- Cover the tray's new behaviour and keyring-only credentials
|
|
32
|
+
|
|
33
|
+
### 🧪 Tests
|
|
34
|
+
|
|
35
|
+
- Stop four tests reading the developer's own machine
|
|
36
|
+
- Cover the notifier fallback on every platform
|
|
37
|
+
- Stop the notification tests toasting the desktop
|
|
38
|
+
- Compare the Windows Run command against its normalised path
|
|
39
|
+
- Pin platform branches in the daemon tests
|
|
40
|
+
|
|
41
|
+
### 👷 CI/CD
|
|
42
|
+
|
|
43
|
+
- Stop `uv run` re-syncing the tray group back in
|
|
44
|
+
|
|
45
|
+
## [1.1.0] - 2026-08-09
|
|
46
|
+
|
|
47
|
+
### 🚀 Features
|
|
48
|
+
|
|
49
|
+
- Headless command surface + Catppuccin Mocha palette
|
|
50
|
+
|
|
51
|
+
### 🐛 Bug Fixes
|
|
52
|
+
|
|
53
|
+
- *(release)* Generate notes against the release tag, not the previous one
|
|
54
|
+
|
|
55
|
+
### 📚 Documentation
|
|
56
|
+
|
|
57
|
+
- Point CLAUDE.md at the release process
|
|
58
|
+
|
|
59
|
+
### 👷 CI/CD
|
|
60
|
+
|
|
61
|
+
- *(release)* Build release notes from CHANGELOG.md
|
|
62
|
+
|
|
63
|
+
### 🔧 Miscellaneous
|
|
64
|
+
|
|
65
|
+
- Trim CLAUDE.md, split MCP bridge docs into a skill, fix env-guard hook
|
|
66
|
+
|
|
10
67
|
## [1.0.2] - 2026-08-05
|
|
11
68
|
|
|
12
69
|
### 🐛 Bug Fixes
|
|
@@ -23,10 +80,6 @@ Generated by [git-cliff](https://git-cliff.org) from conventional commits.
|
|
|
23
80
|
|
|
24
81
|
- *(history)* Copy-to-clipboard + discoverable footer hints
|
|
25
82
|
|
|
26
|
-
### 📚 Documentation
|
|
27
|
-
|
|
28
|
-
- *(changelog)* Cut v1.0.1
|
|
29
|
-
|
|
30
83
|
## [1.0.0] - 2026-08-02
|
|
31
84
|
|
|
32
85
|
### 🚀 Features
|
|
@@ -38,7 +91,6 @@ Generated by [git-cliff](https://git-cliff.org) from conventional commits.
|
|
|
38
91
|
|
|
39
92
|
### 📚 Documentation
|
|
40
93
|
|
|
41
|
-
- *(changelog)* Regenerate for next release, trim duplicate README list
|
|
42
94
|
- *(roadmap)* Archive completed sections to docs/roadmap-archive/
|
|
43
95
|
|
|
44
96
|
### 🔧 Miscellaneous
|
|
@@ -57,7 +109,6 @@ Generated by [git-cliff](https://git-cliff.org) from conventional commits.
|
|
|
57
109
|
|
|
58
110
|
### 📚 Documentation
|
|
59
111
|
|
|
60
|
-
- *(changelog)* Cut v0.2.0
|
|
61
112
|
- *(commands)* Sync stale command lists in CLAUDE.md and slash-commands docstring
|
|
62
113
|
- *(readme)* Sync command tables with code, add changelog
|
|
63
114
|
|
|
@@ -4,21 +4,9 @@ Terminal dashboard for Litter Robot (LR3/LR4/LR5) via the Whisker cloud API.
|
|
|
4
4
|
|
|
5
5
|
## Stack
|
|
6
6
|
|
|
7
|
-
- **
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- **python-dotenv** — credential loading (`.env` fallback)
|
|
11
|
-
- **keyring>=24** — OS credential store (Windows Credential Manager / macOS Keychain / Linux Secret Service)
|
|
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
|
|
7
|
+
- **keyring** resolves to the OS-native credential store: Windows Credential Manager / macOS Keychain / Linux Secret Service.
|
|
8
|
+
|
|
9
|
+
See `pyproject.toml` for the full dependency and dev-tooling list (`[project].dependencies`, `[dependency-groups].dev`, `[tool.poe.tasks]`).
|
|
22
10
|
|
|
23
11
|
## Entry points
|
|
24
12
|
|
|
@@ -37,8 +25,16 @@ asher/
|
|
|
37
25
|
auth.py LoginScreen modal (ModalScreen[tuple[str,str]]) — available, not primary flow
|
|
38
26
|
helpers.py fmt_ago(), drawer_bar(), ts(), robot_model() (pure, testable)
|
|
39
27
|
constants.py STATUS_COLORS, ROBOT_MODELS
|
|
28
|
+
theme.py Catppuccin Mocha palette + semantic roles (BACKGROUND, MUTED, DANGER, …) + CSS_VARIABLES/apply() — the only place a hex literal belongs
|
|
40
29
|
config.py runtime settings persistence — load()/save()/update() over ~/.asher-cli/config.json; holds poll interval, cat-panel visibility/colour, active pet index, notification settings (non-secret UI prefs only; credentials stay in keyring)
|
|
41
|
-
notifications.py desktop toast + audible alert façade
|
|
30
|
+
notifications.py desktop toast + audible alert façade — plyer first, then the platform's own tool (osascript/notify-send); fire/beep are always-safe no-ops on failure/headless
|
|
31
|
+
watcher.py background notification loop with no TUI — pure WatchState (robot snapshots → Alert list) + supervising watch() that reconnects with backoff, plus WatcherRunner (asyncio on a worker thread, for the tray)
|
|
32
|
+
daemon.py detached watcher process control — `start` pre-flights `connection.credentials_available()` (presence only, no network, so starting offline still works) because the watcher claims the pid file *before* authenticating and would otherwise report a pid for a process that stops a moment later — pid file/log in ~/.asher-cli, start/stop/status/run, cross-platform detach + liveness (os.kill would *terminate* on Windows)
|
|
33
|
+
tray.py optional pystray/Pillow system-tray icon over WatcherRunner; every path degrades to a headless watcher. Icon is a panel-toned silhouette + status dot — colour rides the badge, never the whole glyph
|
|
34
|
+
launcher.py open_app() — start the TUI in a new terminal from the tray (a detached tray has none): new console on Windows, Terminal.app via AppleScript on macOS, first installed emulator on Linux (desktop's own preferred)
|
|
35
|
+
desktoptheme.py panel_is_dark() — is the tray/menu-bar background dark? kdeglobals luma / gsettings / AppleInterfaceStyle / the Personalize registry keys, behind a TTL cache; every probe degrades to a fallback, never raises
|
|
36
|
+
autostart.py login items — AutostartBackend ABC + launchd/systemd-user/registry subclasses + backend() factory; all per-user, no admin rights, disable() removes exactly what enable() wrote
|
|
37
|
+
updates.py PyPI release check — read-only over HTTPS, once a day, reports only. Never installs (see its docstring for why that stays manual)
|
|
42
38
|
cats.py CATS dict (ASCII art)
|
|
43
39
|
login_flow.py LoginFlow state machine — inline email/password prompt in command bar
|
|
44
40
|
robot_protocol.py RobotProtocol structural Protocol for pylitterbot robot objects
|
|
@@ -47,9 +43,10 @@ asher/
|
|
|
47
43
|
mcp_bridge.py asher-mcp-launch console script — keyring-backed pylitterbot MCP launcher
|
|
48
44
|
faults.py check_faults(robot) — model-scoped safety/component fault detection (status enum + per-model attr allowlist incl. LR4 USB power fault; hopper never a fault)
|
|
49
45
|
history_view.py HistoryScreen (ModalScreen) + format_history_rows()/format_history_text() — scrollable activity-history pager pushed by the `history` command; `c` copies the full history (plain text) to the clipboard via action_copy_all()
|
|
50
|
-
export.py shared activity-history CSV core +
|
|
46
|
+
export.py shared activity-history CSV core + exit-code contract: build_history_csv(), resolve_dest(), resolve_robot(), parse_days(), EXIT_*, ExportError — no Textual imports; the TUI `export` command and `asher export` both call build_history_csv()
|
|
47
|
+
headless.py headless command surface for `asher <command>` — Session/Result/CommandError, the COMMANDS registry, and run(); plain strings only, no Textual, routes model differences through RobotAdapter
|
|
51
48
|
completion.py pure helpers for command completion: slash popup (slash_matches, enter_completes, render_completion) + inline ghost text (CommandSuggester) — fed by _registry, no Textual imports except the Suggester base class
|
|
52
|
-
__main__.py main() entry point
|
|
49
|
+
__main__.py main() entry point — argparse subcommands (headless) vs no-args (TUI); `--export` kept as a deprecated alias
|
|
53
50
|
commands/
|
|
54
51
|
base.py Command ABC, SlashCommand, CommandRegistry
|
|
55
52
|
__init__.py CommandsMixin — all command classes + registry + dispatch
|
|
@@ -72,11 +69,19 @@ tests/
|
|
|
72
69
|
test_mcp_config.py Claude Desktop config read/write
|
|
73
70
|
test_config.py runtime settings persistence — load/save/update over defaults
|
|
74
71
|
test_notifications.py plyer toast + beep façade — always-safe no-op paths
|
|
72
|
+
test_watcher.py WatchState alert transitions + snapshot/deliver (pure) + watch() against a patched open_session + WatcherRunner threading
|
|
73
|
+
test_daemon.py pid-file bookkeeping, start/stop/status/dispatch with Popen and os.kill mocked
|
|
74
|
+
test_tray.py availability probing, menu/title text, icon rendering (tone inversion + status badge), and the headless fallbacks
|
|
75
|
+
test_launcher.py every platform's open path driven directly (Popen always mocked), terminal preference/fallback order
|
|
76
|
+
test_desktoptheme.py all four panel-tone probes driven directly (so each is covered on every runner) with the readers/registry mocked, plus the TTL cache
|
|
77
|
+
test_autostart.py all three login-item backends driven directly (so each is covered on every runner) with launchctl/systemctl/winreg mocked
|
|
78
|
+
test_updates.py version comparison, the once-a-day cache, HTTPS-only fetching, and a guard that the module spawns no process
|
|
75
79
|
test_mcp_bridge.py mcp_bridge launcher credential/subprocess handling
|
|
76
80
|
test_mcp_command.py /mcp slash command dispatch
|
|
77
81
|
test_faults.py check_faults() — safety statuses, attribute faults, graceful degradation
|
|
78
82
|
test_history_view.py format_history_rows()/format_history_text() + HistoryScreen structure, copy-all + Pilot push/dismiss
|
|
79
|
-
test_export.py build_history_csv/resolve_dest/resolve_robot/parse_days (pure) +
|
|
83
|
+
test_export.py build_history_csv/resolve_dest/resolve_robot/parse_days (pure) + the legacy `--export` flag path (no Pilot, mocks _connect_headless)
|
|
84
|
+
test_headless.py headless registry/rendering (pure) + every command handler against mock robots + run() exit codes + the argparse subcommand surface
|
|
80
85
|
test_completion.py slash_matches/enter_completes/render_completion (pure) + Pilot overlay visibility/navigation/accept
|
|
81
86
|
|
|
82
87
|
.github/workflows/
|
|
@@ -88,45 +93,46 @@ tests/
|
|
|
88
93
|
|
|
89
94
|
Priority order on startup:
|
|
90
95
|
|
|
91
|
-
1. **OS keyring** — set automatically after first interactive login
|
|
92
|
-
2. **`.env` file** —
|
|
96
|
+
1. **OS keyring** — cached OAuth token, then email/password; set automatically after first interactive login
|
|
97
|
+
2. **`.env` file** — **development only**, gated on `ASHER_CLI_DEV_MODE=true`
|
|
93
98
|
3. **Inline login flow** — shown when no credentials found anywhere (email → password prompt in command bar)
|
|
94
99
|
|
|
95
|
-
`.env`
|
|
100
|
+
In a real install the keyring is the **only** source of credentials. `.env` is a
|
|
101
|
+
working-copy convenience: without dev mode a stray `LITTER_ROBOT_USER` in a shell
|
|
102
|
+
profile or a checked-out `.env` would silently outrank the keyring and
|
|
103
|
+
authenticate as the wrong account.
|
|
104
|
+
|
|
105
|
+
`.env` variable names (dev mode only):
|
|
96
106
|
```
|
|
107
|
+
ASHER_CLI_DEV_MODE=true
|
|
97
108
|
LITTER_ROBOT_USER=...
|
|
98
109
|
LITTER_ROBOT_PASSWORD=...
|
|
99
110
|
```
|
|
100
111
|
|
|
112
|
+
Every environment read goes through `_env_credentials()` in
|
|
113
|
+
`asher/connection/__init__.py` — the one place the gate is applied. Don't call
|
|
114
|
+
`os.getenv("LITTER_ROBOT_*")` anywhere else. `helpers.dev_mode()` is the shared
|
|
115
|
+
predicate (it also selects the `dev` version string).
|
|
116
|
+
|
|
101
117
|
Keyring service name: `asher-cli`, keys `email` and `password`.
|
|
102
118
|
Helper functions in `asher/connection/__init__.py`: `_keyring_load()`, `_keyring_save()`, `_keyring_delete()`.
|
|
103
119
|
|
|
104
|
-
## MCP bridge (`/mcp`)
|
|
105
|
-
|
|
106
|
-
pylitterbot ships an optional MCP server (`pip install pylitterbot[mcp]`, run via `python -m pylitterbot.mcp`) that lets an MCP client like Claude Desktop monitor/control the robot directly. Its own docs configure it with plaintext credentials in the client's JSON config — asher-cli avoids that:
|
|
107
|
-
|
|
108
|
-
- `/mcp on|off|status` (in `asher/commands/__init__.py`, logic in `asher/mcp_config.py`) adds/removes an entry (named by `mcp_config._SERVER_NAME`) in every `claude_desktop_config.json` this OS's Claude Desktop might read (`mcp_config.config_paths()` — on Windows this includes both the standard installer path and any MSIX/Microsoft Store virtualized path). The entry's `command` is `sys.executable -m asher.mcp_bridge` — never the credentials themselves.
|
|
109
|
-
- `/mcp on` also auto-installs pylitterbot's `mcp` extra via `sys.executable -m pip install "pylitterbot[mcp]==<installed version>"` if the `mcp` package isn't importable yet.
|
|
110
|
-
- `asher/mcp_bridge.py` (console script `asher-mcp-launch`) is what Claude Desktop actually spawns. It reads email/password from the OS keyring at process start, sets them as `LITTER_ROBOT_USERNAME`/`LITTER_ROBOT_PASSWORD` (pylitterbot's expected names — note these differ from asher-cli's own `.env` var `LITTER_ROBOT_USER`) in that process's environment only, then execs `python -m pylitterbot.mcp`. No credentials ever touch the on-disk MCP config.
|
|
111
|
-
- `/mcp on` requires keyring credentials. If none are found but `.env` fallback credentials are set, it copies them into the keyring automatically (since the bridge process can't reliably discover a project-relative `.env` — Claude Desktop controls its working directory, not asher-cli).
|
|
112
|
-
- Requires the `mcp` extra: `uv sync --extra mcp` / `pip install asher-cli[mcp]`. Restart Claude Desktop after toggling for the change to take effect.
|
|
113
|
-
|
|
114
120
|
## Command convention
|
|
115
121
|
|
|
116
|
-
|
|
117
|
-
`clean`, `status`, `info`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto`, `night-light-brightness <level>`, `panel-brightness <low|medium|high>`, `wait-time <minutes>`, `power on|off`, `rename <name>`, `insight [days|month]`, `sleep-schedule`, `privacy on|off`, `volume <0-100>`, `camera-audio on|off`, `drawer-reset`, `history [count|all]`, `export [days|month]`, `clear`, `help`, `quit`
|
|
122
|
+
Command names, slash-command names, and their args are not listed here — see the `_registry` in `asher/commands/__init__.py`, which is authoritative; `/help` renders it at runtime. `/mcp`'s credential-bridging design is documented in the `mcp-bridge` skill.
|
|
118
123
|
|
|
119
|
-
**
|
|
120
|
-
`/login`, `/logout`, `/robots`, `/robot <index|name>`, `/pets`, `/pet <index|name>`, `/cat on|off|colour <hex>`, `/refresh [seconds|off]`, `/config`, `/notify on|off|sound on|off|test`, `/version`, `/mcp on|off|status`, `/exit`
|
|
124
|
+
**Normal commands** (no prefix) are robot actions only; **slash commands** (`/` prefix) are app management only.
|
|
121
125
|
|
|
122
|
-
`/refresh`, `/cat`, `/pet`, and `/notify` persist their settings to `~/.asher-cli/config.json` (via `asher.config.update()`), so they survive restarts. Credentials and the preferred-robot serial stay in the OS keyring; the config file holds only non-secret UI preferences.
|
|
126
|
+
`/refresh`, `/cat`, `/pet`, and `/notify` persist their settings to `~/.asher-cli/config.json` (via `asher.config.update()`), so they survive restarts. That file is also the only channel to a running watcher, which re-reads it per alert rather than caching at startup. Credentials and the preferred-robot serial stay in the OS keyring; the config file holds only non-secret UI preferences.
|
|
123
127
|
|
|
124
|
-
|
|
128
|
+
Do not add robot-control commands as slash commands, and do not add app-management commands as bare commands.
|
|
125
129
|
|
|
126
130
|
**Special cases** (accepted both with and without `/`):
|
|
127
131
|
`exit`, `quit`, `q` — exit the app
|
|
128
132
|
|
|
129
|
-
|
|
133
|
+
**Headless commands** (`asher <command>`) are a parallel registry in `asher/headless.py`: same robot actions, no Textual, plain-string + JSON output. `asher watch` and `asher update` are deliberately *not* in that registry — `watch` manages a long-lived process rather than doing one thing and exiting, and `update` talks to PyPI rather than a robot, so neither should be forced through `open_session()` and made to authenticate. Both declare their subparser directly in `__main__.py`. Slash commands have no headless equivalent — they configure the TUI, which isn't running. A robot command worth scripting should exist in both registries; the shared logic lives in `RobotAdapter`, not in either command class.
|
|
134
|
+
|
|
135
|
+
> If you add a command, update the tables in `README.md` and the list in `asher/slash-commands/__init__.py`. If it's a robot command, consider adding it to `COMMANDS` in `asher/headless.py` too.
|
|
130
136
|
|
|
131
137
|
## Architecture
|
|
132
138
|
|
|
@@ -163,6 +169,10 @@ LoginScreen (ModalScreen) — available in auth.py but not the primary auth path
|
|
|
163
169
|
| `_refresh_faults(robot)` | run `check_faults()`, render `#fault-banner`, log transitions; sets cat mode to `error` while faults active |
|
|
164
170
|
| `_cycling_chip()` / `_start_cycle_timer()` / `_stop_cycle_timer()` / `_tick_cycle()` | `⟳ Cycling M:SS` chip + lazy 1s elapsed timer |
|
|
165
171
|
| `_poll_status_interval()` | `@work` — poll fallback every 300s (5 min); WebSocket is primary |
|
|
172
|
+
| `watcher.watch()` | supervise a cloud session and notify on every change worth interrupting for — the TUI's `_notify_fault` without a TUI |
|
|
173
|
+
| `daemon.start/stop/status()` | detached watcher process control; `running_pid()` also gates the TUI's own toasts so alerts never double up |
|
|
174
|
+
| `daemon.run_foreground()` | the watcher process itself — claims the pid file (`_pid_file_held`) so a login-started watcher is still visible to `status`/`stop`, and refuses to be a second one |
|
|
175
|
+
| `autostart.enable/disable()` | register/remove the platform login item; `describe()` feeds the `watch status` line |
|
|
166
176
|
| `_tick_cat()` | advances multi-frame cat animation every 0.4s |
|
|
167
177
|
| `_dispatch_command(command, args)` | `@work` — calls `command.run(app, args)` from the registry |
|
|
168
178
|
| `on_input_submitted()` | routes input to login flow or `_dispatch_command` via `CommandRegistry`; Enter on a partial `/cmd` completes it instead of submitting |
|
|
@@ -175,17 +185,15 @@ LoginScreen (ModalScreen) — available in auth.py but not the primary auth path
|
|
|
175
185
|
|
|
176
186
|
## Robot compatibility
|
|
177
187
|
|
|
178
|
-
pylitterbot auto-detects robot type. Commands that differ per model are handled by `RobotAdapter` subclasses in `robot_adapters.py` — `make_adapter(robot)` returns the right one based on `type(robot).__name__`. Status-bar reads use `getattr(..., default)` for graceful degradation on older models.
|
|
188
|
+
pylitterbot auto-detects robot type. Commands that differ per model are handled by `RobotAdapter` subclasses in `robot_adapters.py` — `make_adapter(robot)` returns the right one based on `type(robot).__name__`. Status-bar reads use `getattr(..., default)` for graceful degradation on older models. See the `pylitterbot-ref` skill for the confirmed API surface.
|
|
189
|
+
|
|
190
|
+
## Colour
|
|
179
191
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
- `
|
|
183
|
-
-
|
|
184
|
-
- `
|
|
185
|
-
- `robot.refresh()`, `robot.start_cleaning()`
|
|
186
|
-
- `robot.set_sleep_mode(bool)`, `robot.set_panel_lockout(bool)`
|
|
187
|
-
- `robot.set_night_light_brightness(int)` or `robot.set_night_light_mode(NightLightMode)`
|
|
188
|
-
- `robot.get_activity_history(limit=int)` → list of `Activity` objects with `.timestamp` and `.action` (`LitterBoxStatus` enum)
|
|
192
|
+
Every colour comes from `asher/theme.py` (Catppuccin Mocha). Reference the **semantic roles** (`theme.MUTED`, `theme.DANGER`, …), not the raw swatches (`theme.OVERLAY0`) and never a hex literal — a re-flavour then only repoints the roles.
|
|
193
|
+
|
|
194
|
+
- **Rich styles:** `style=theme.ACCENT`, or `style=f"bold {theme.ACCENT}"`. Prefer building `Text` objects with explicit styles over `Text.from_markup` with inline colours.
|
|
195
|
+
- **`ui/style.tcss`:** use the `$asher-*` variables; `AsherApp.get_css_variables()` supplies them.
|
|
196
|
+
- **Inline `CSS`/`DEFAULT_CSS` on a Screen or Widget:** wrap the block in `theme.apply(...)`, which bakes the `$asher-*` values in at class-definition time. A screen mounted on a host app that isn't `AsherApp` (as the Pilot tests do) would otherwise fail to parse.
|
|
189
197
|
|
|
190
198
|
## Code comments
|
|
191
199
|
|
|
@@ -213,26 +221,34 @@ Commands that need a confirmed cloud state before showing a result (e.g. sleep/w
|
|
|
213
221
|
|
|
214
222
|
**Add a slash command:** create a class inheriting `SlashCommand` (sets `prefix = "/"`), implement `async def run(self, app, args)`, register it, and document in `asher/slash-commands/__init__.py`.
|
|
215
223
|
|
|
224
|
+
**Add a headless command:** write `async def _my_command(session, args) -> Result` in `asher/headless.py` and add a `HeadlessCommand(...)` entry to `COMMANDS`. The argparse subparser is generated from the registry — nothing to add in `__main__.py`. Build the `Result` with `_rows()` (read commands) or `_outcome()` (actions) so text and JSON stay in step, and raise `CommandError` rather than printing.
|
|
225
|
+
|
|
216
226
|
**Change poll interval:** `self.set_interval(300, ...)` in `on_mount`.
|
|
217
227
|
|
|
218
228
|
**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")`.
|
|
219
229
|
|
|
220
230
|
**File naming convention:** no underscores in filenames (except Python-required `__init__.py` and `__main__.py`).
|
|
221
231
|
|
|
232
|
+
## Tray packaging
|
|
233
|
+
|
|
234
|
+
The `tray` extra is what a published install uses (`pip install "asher-cli[tray]"`). It is **mirrored** as a `tray` dependency-group listed in `[tool.uv] default-groups`, because `uv run` re-syncs the environment to the default *groups* on every invocation and prunes everything else — extras included. Without the group, any plain `uv run` silently removed `pystray` and left the watcher drawing an icon no desktop was hosting. Keep the two lists in step when changing either.
|
|
235
|
+
|
|
236
|
+
CI opts out with `uv sync --no-default-groups --group dev` (`ci.yml`, `coverage.yml`): PyGObject builds from source and the runners have no GObject-introspection headers. `[tool.uv] default-extras` is not an option — uv 0.11 doesn't support that key.
|
|
237
|
+
|
|
238
|
+
PyGObject is what selects pystray's `_appindicator` backend. Without it pystray falls back to a legacy XEmbed icon that KDE Plasma and GNOME no longer host: the watcher runs, draws an icon, and nothing shows it, with no error anywhere. Check with `pystray.Icon.__module__`.
|
|
239
|
+
|
|
222
240
|
## Dev workflow
|
|
223
241
|
|
|
224
|
-
|
|
225
|
-
uv sync # install all deps (including dev group)
|
|
226
|
-
uv run poe dev # run with CSS hot reload (textual --dev)
|
|
227
|
-
uv run poe watch # run with Python auto-restart on file change (watchfiles)
|
|
228
|
-
uv run poe test # run test suite
|
|
229
|
-
uv run poe check # ruff + mypy + pytest (same as CI)
|
|
230
|
-
uv run poe fix # auto-fix ruff issues
|
|
231
|
-
uv run poe security # bandit scan (same config the Bandit workflow uses)
|
|
232
|
-
```
|
|
242
|
+
See `[tool.poe.tasks]` in `pyproject.toml` for the full task list (`uv run poe <task>`).
|
|
233
243
|
|
|
234
244
|
Pre-push hook (`.githooks/pre-push`) runs: ruff check → ruff format --check → mypy. Tests are not in the hook — run them manually.
|
|
235
245
|
|
|
246
|
+
## Releasing
|
|
247
|
+
|
|
248
|
+
Follow [README § Releasing](README.md#releasing) exactly, in order: `uv run poe changelog-release X.Y.Z` and commit the result → `uv run bump-my-version bump <part>` (this commits **and tags**) → `git push && git push --tags` → only then cut and push `release/X.Y.Z`.
|
|
249
|
+
|
|
250
|
+
Regenerate with `changelog-release X.Y.Z`, never plain `changelog` — the workflow lifts the `## [X.Y.Z]` section out of the committed `CHANGELOG.md` verbatim for the GitHub Release body, and a file still saying `## [Unreleased]` fails the release job. That extraction is also why hand-refinements to a section survive into the release notes, and why re-running either changelog task afterwards silently discards them: regeneration always re-derives from commits. Refine last.
|
|
251
|
+
|
|
236
252
|
## Testing notes
|
|
237
253
|
|
|
238
254
|
- Pilot-based integration tests use `app.run_test()` with `await pilot.pause()` before querying widgets
|