agent-toggle 0.9.0__tar.gz → 0.11.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.
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/PKG-INFO +31 -10
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/README.md +30 -9
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/__init__.py +1 -1
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/cli.py +8 -2
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/fs.py +4 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/help.py +9 -1
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/locales/zh_tw_cli.py +3 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/shims/claude.md.tmpl +2 -1
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/shims/generic.md.tmpl +1 -1
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/store.py +9 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/update_check.py +4 -2
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/update_prompt.py +47 -8
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle.egg-info/PKG-INFO +31 -10
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_cli.py +10 -4
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_cli_surface.py +1 -1
- agent_toggle-0.11.0/tests/test_update_prompt.py +78 -0
- agent_toggle-0.9.0/tests/test_update_prompt.py +0 -41
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/LICENSE +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/__main__.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/backends/__init__.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/backends/flag_json.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/backends/mcp_json.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/backends/mcp_toml.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/backends/plugin_cli.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/companions.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/config.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/cost.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/doctor.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/harnesses.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/i18n.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/locales/__init__.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/locales/zh_tw_common.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/locales/zh_tw_config.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/locales/zh_tw_picker.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/mechanisms.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/ops.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/output.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/profiles.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/settings.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/spinner.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/toml_check.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/ui/__init__.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/ui/config_menu.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/ui/logo.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/ui/menu.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/ui/model.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/ui/picker.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/ui/theme.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle/undo.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle.egg-info/SOURCES.txt +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle.egg-info/dependency_links.txt +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle.egg-info/entry_points.txt +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle.egg-info/requires.txt +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/agent_toggle.egg-info/top_level.txt +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/pyproject.toml +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/setup.cfg +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_alias.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_all_type.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_backends.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_color.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_companions.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_completions.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_config.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_config_menu.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_conformance.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_containment.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_cost.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_crash.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_doctor.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_encoding.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_fs.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_harnesses.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_help.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_i18n.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_install.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_logo.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_mechanisms.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_menu.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_mods.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_picker.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_platform.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_profiles.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_project.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_safety.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_same_name.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_secret_audit.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_settings.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_store.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_theme.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_undo.py +0 -0
- {agent_toggle-0.9.0 → agent_toggle-0.11.0}/tests/test_update_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-toggle
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Temporarily disable / re-enable AI-agent skills, agents, commands, rules, plugins and MCP servers across harnesses (Claude Code, Codex, Grok, OpenCode, OpenClaw, Copilot, Vibe, Devin, Antigravity)
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/weskao/agent-toggle
|
|
@@ -32,6 +32,8 @@ Dynamic: license-file
|
|
|
32
32
|
|
|
33
33
|
# agent-toggle
|
|
34
34
|
|
|
35
|
+
PyPI: <https://pypi.org/project/agent-toggle/>
|
|
36
|
+
|
|
35
37
|
Temporarily disable and restore AI-agent resources — skills, agents, commands,
|
|
36
38
|
rules, plugins (mods included), MCP servers — across Claude Code, Codex, Grok CLI, OpenCode,
|
|
37
39
|
OpenClaw, Copilot, Vibe, Devin and Antigravity — and show what each one costs at
|
|
@@ -193,6 +195,7 @@ agent-toggle <command> [args] # or: python3 agent_toggle.py <command> [
|
|
|
193
195
|
| `status` | health check: harnesses found, types each supports, parked counts, gitignore, untracked parked items, stale live twins, shared dirs, and state entries whose parked item is gone (a `stale` row with the fix command; read-only, still exit `0`) |
|
|
194
196
|
| `list [type]` | what is currently disabled (`--project <dir>` filters to one project) |
|
|
195
197
|
| `cost [--type T]` | estimated startup tokens per item, biggest first (read-only; `--harness H` filters; `--project <dir>` prices a repo's `.claude/` and `.mcp.json` instead of user scope) |
|
|
198
|
+
| `update` | check PyPI now (ignores the cache and a skipped version) and print the commands that update the CLI, the Claude Code plugin (when installed) and the shims; runs none of them. `/agent-toggle update` in Claude Code does the same; opening the pane toasts a newer release once |
|
|
196
199
|
| `install-shims` | write the skill shim into every installed harness that is on in the settings (an off one is skipped unless named with `--harness`); refuses to overwrite a file it did not write (`--dry-run` shows the plan); reports stale legacy `<dir>-disabled/` lines in a harness-home `.gitignore` (`--gitignore` removes them, `--no-gitignore` skips the check); on a terminal, then asks y/n for each problem `doctor` can fix (see [Fixing](#fixing)) |
|
|
197
200
|
| `disable <type> <name>...` | park one or more items (`--dry-run` shows the plan; `--project <dir>` for a repo's own `.claude/` and `.mcp.json`) |
|
|
198
201
|
| `enable <type> <name>...` | put them back (`--dry-run` shows the plan; `--project <dir>` likewise) |
|
|
@@ -367,7 +370,8 @@ off under `--dry-run`. Esc closes the prompt.
|
|
|
367
370
|
Nothing happens while the picker is open. Changes are staged, the screen is
|
|
368
371
|
torn down, and only then do the real operations run, so their output (which
|
|
369
372
|
companion files moved, which were kept because they are shared) is readable
|
|
370
|
-
instead of fighting curses for the terminal.
|
|
373
|
+
instead of fighting curses for the terminal. Claude Code sessions running the
|
|
374
|
+
[pane mod](#claude-code-pane-mod) pick up the Claude changes within 5 s and reload them.
|
|
371
375
|
|
|
372
376
|
Built on stdlib `curses`, so there is nothing to install on macOS and Linux
|
|
373
377
|
(on Windows, `pip install "agent-toggle[windows]"` pulls `windows-curses`).
|
|
@@ -469,10 +473,15 @@ before the command and is offered after it, on every exit path; it overlaps the
|
|
|
469
473
|
|
|
470
474
|
- **On a terminal** (stdin and stderr are TTYs, no `--json`): a panel on stderr, "agent-toggle X
|
|
471
475
|
is available (you have Y)", with `Update now`, `Skip` and `Skip until next version`, a release
|
|
472
|
-
notes link, and `↑` `↓` `Enter` `q`. `Update now` runs
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
`
|
|
476
|
+
notes link, and `↑` `↓` `Enter` `q`. `Update now` runs, in order and stopping at the first
|
|
477
|
+
failure: `uv tool upgrade agent-toggle` (`pipx upgrade agent-toggle` for a pipx install), then,
|
|
478
|
+
when the Claude Code plugin is installed, `claude plugin marketplace update agent-toggle` and
|
|
479
|
+
`claude plugin update agent-toggle@agent-toggle`, then `agent-toggle install-shims`. A failure
|
|
480
|
+
gives a yellow warning listing the commands to run yourself.
|
|
481
|
+
- **Windows**: a running `agent-toggle.exe` cannot be replaced, so there is no `Update now`; the
|
|
482
|
+
commands are printed instead (as below), to run once agent-toggle has exited.
|
|
483
|
+
- **Off a terminal** (or `--json`): a stderr line, `agent-toggle X is available (you have Y)`,
|
|
484
|
+
then those commands, one per line.
|
|
476
485
|
- It never changes the exit code or stdout, so `--json` output stays one document.
|
|
477
486
|
|
|
478
487
|
It is one `GET https://pypi.org/pypi/agent-toggle/json` on a background thread with a 0.8 s
|
|
@@ -598,7 +607,8 @@ visible rather than silent.
|
|
|
598
607
|
hand-rolled serializer would mangle comments — a text slice is lossless and
|
|
599
608
|
much less code.
|
|
600
609
|
|
|
601
|
-
MCP changes need a **new session** to take effect
|
|
610
|
+
MCP changes need a **new session** to take effect, except in a Claude Code session running the
|
|
611
|
+
agent-toggle mod, which runs `/mcp disable|enable <name>` for you (see the mod section).
|
|
602
612
|
|
|
603
613
|
### Claude MCP scopes
|
|
604
614
|
|
|
@@ -846,6 +856,7 @@ bookkeeping.
|
|
|
846
856
|
| `state.json` | current disabled list (schema v3; atomic write, mode `0600`), saved before each flag write or dir move with the op in flight under an optional `pending` key, and after each MCP / plugin item |
|
|
847
857
|
| `lock` | held by `disable` / `enable` / `enable --all` / `undo` / `profile apply` / `migrate` / `ui` for the whole batch; a second run waits 5 s then exits `3` (stale after 10 min *and* its PID is gone; a live batch refreshes it per item; the error says when its PID is gone, i.e. a killed run left it) |
|
|
848
858
|
| `log.jsonl` | one line per operation (mode `0600`), see below |
|
|
859
|
+
| `reload.json` | the Claude rows the last `ui` Enter changed, rewritten each time; open Claude Code sessions running the mod poll it and reload them |
|
|
849
860
|
| `mcp-backups/` | `<harness>__<server>.json`, or `<sha8>__<harness>__<server>.json` for a project `.mcp.json` (mode `0600` -- may hold auth headers) |
|
|
850
861
|
| `companions/` | parked exclusive helper files |
|
|
851
862
|
| `parked/user/<owner>/<dir>/` | user-scope items, e.g. `parked/user/claude/skills/<name>` (`<owner>` = the harness whose home holds the dir; a dir shared by several harnesses parks once, under its owner); an absolute OpenCode `skills.paths` dir parks under `parked/user/opencode/ext-<sha8>/` (`<sha8>` of its resolved path). Nothing is ever parked inside a harness home |
|
|
@@ -948,7 +959,8 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
|
|
|
948
959
|
- It looks like the `ui` picker: a title with the live-token total, a search line, rows under
|
|
949
960
|
type headings with counts, green `●` live / yellow `○` parked, tokens and a bar per row (a
|
|
950
961
|
parked row shows `(N)`, what restoring it would load), the cursor row in reverse video, and a
|
|
951
|
-
key-chip footer. The list scrolls with the cursor
|
|
962
|
+
key-chip footer. The list scrolls with the cursor; the status line shows its position (`7/23`),
|
|
963
|
+
and the first and last list lines count the rows out of view (`↑ 4`, `↓ 12`).
|
|
952
964
|
- The mouse wheel over the list moves the cursor, as in the terminal picker; no click needed.
|
|
953
965
|
- Click the list once so it takes keys (Esc hands the keyboard back to the prompt). Then `↑` `↓`
|
|
954
966
|
`PgUp` `PgDn` `Home` `End` (also `Ctrl-P` / `Ctrl-N`) move; `Space` or `Enter` toggles the row,
|
|
@@ -958,8 +970,8 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
|
|
|
958
970
|
...), like `disable all <name>`; `Ctrl-U` clears the search. A click moves the cursor.
|
|
959
971
|
- A toggle applies at once (no staging, no Enter to apply); the toast names the target and the state
|
|
960
972
|
with an emoji and a bracketed tag (no colour codes, so nothing garbles; on the Linux console and the
|
|
961
|
-
legacy Windows console, where emoji draw as boxes, the tag alone): `✅ [ENABLED] skill: foo`, `⛔ [DISABLED] mod: bar (
|
|
962
|
-
session)`, `⛔ [DISABLED] all: foo` after `n`, or `❌ [FAILED] disable skill: foo - <error>`.
|
|
973
|
+
legacy Windows console, where emoji draw as boxes, the tag alone): `✅ [ENABLED] skill: foo`, `⛔ [DISABLED] mod: bar (/reload-plugins)`,
|
|
974
|
+
`⛔ [DISABLED] rule: baz (takes effect in a new session)`, `⛔ [DISABLED] all: foo` after `n`, or `❌ [FAILED] disable skill: foo - <error>`.
|
|
963
975
|
The row flips the moment you press (and flips back
|
|
964
976
|
if the CLI fails); the list reloads in the background. While a toggle runs, further keys are ignored.
|
|
965
977
|
- Opening the pane shows the same braille spinner and `Loading resources…` as `agent-toggle ui`
|
|
@@ -970,6 +982,15 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
|
|
|
970
982
|
`undo`, `list`, `status`, `cost` or `doctor` runs the CLI instead of opening the pane and prints
|
|
971
983
|
one line per result row: `/agent-toggle disable skill foo bar` (it adds `--json`, and
|
|
972
984
|
`--harness claude` unless you pass `--harness`). A toggle reloads an open pane.
|
|
985
|
+
- Inside Claude Code the mod then applies the change to the running session by queueing the slash
|
|
986
|
+
command for you, once per kind: skills and commands `/reload-skills`, plugins (mods included)
|
|
987
|
+
`/reload-plugins`, an MCP server `/mcp disable|enable <name>`. The toast names it, e.g.
|
|
988
|
+
`⛔ [DISABLED] skill: foo (/reload-skills)`. Agents and rules have no reload and still say
|
|
989
|
+
`takes effect in a new session`. If Claude refuses a queued command, a toast says so.
|
|
990
|
+
- `agent-toggle ui` in another terminal applies on Enter and writes the Claude rows it changed to
|
|
991
|
+
`~/.agent-toggle/reload.json`. Every open session running the mod checks that file's mtime every
|
|
992
|
+
5 s (one `stat`) and queues the same reloads, toasting `agent-toggle ui: /reload-skills`. A file
|
|
993
|
+
older than the session is ignored.
|
|
973
994
|
- Needs `agent-toggle` on `PATH`.
|
|
974
995
|
- On Windows `agent-toggle` must resolve to a real executable on `PATH` (a pip or uv install
|
|
975
996
|
provides `agent-toggle.exe`).
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# agent-toggle
|
|
2
2
|
|
|
3
|
+
PyPI: <https://pypi.org/project/agent-toggle/>
|
|
4
|
+
|
|
3
5
|
Temporarily disable and restore AI-agent resources — skills, agents, commands,
|
|
4
6
|
rules, plugins (mods included), MCP servers — across Claude Code, Codex, Grok CLI, OpenCode,
|
|
5
7
|
OpenClaw, Copilot, Vibe, Devin and Antigravity — and show what each one costs at
|
|
@@ -161,6 +163,7 @@ agent-toggle <command> [args] # or: python3 agent_toggle.py <command> [
|
|
|
161
163
|
| `status` | health check: harnesses found, types each supports, parked counts, gitignore, untracked parked items, stale live twins, shared dirs, and state entries whose parked item is gone (a `stale` row with the fix command; read-only, still exit `0`) |
|
|
162
164
|
| `list [type]` | what is currently disabled (`--project <dir>` filters to one project) |
|
|
163
165
|
| `cost [--type T]` | estimated startup tokens per item, biggest first (read-only; `--harness H` filters; `--project <dir>` prices a repo's `.claude/` and `.mcp.json` instead of user scope) |
|
|
166
|
+
| `update` | check PyPI now (ignores the cache and a skipped version) and print the commands that update the CLI, the Claude Code plugin (when installed) and the shims; runs none of them. `/agent-toggle update` in Claude Code does the same; opening the pane toasts a newer release once |
|
|
164
167
|
| `install-shims` | write the skill shim into every installed harness that is on in the settings (an off one is skipped unless named with `--harness`); refuses to overwrite a file it did not write (`--dry-run` shows the plan); reports stale legacy `<dir>-disabled/` lines in a harness-home `.gitignore` (`--gitignore` removes them, `--no-gitignore` skips the check); on a terminal, then asks y/n for each problem `doctor` can fix (see [Fixing](#fixing)) |
|
|
165
168
|
| `disable <type> <name>...` | park one or more items (`--dry-run` shows the plan; `--project <dir>` for a repo's own `.claude/` and `.mcp.json`) |
|
|
166
169
|
| `enable <type> <name>...` | put them back (`--dry-run` shows the plan; `--project <dir>` likewise) |
|
|
@@ -335,7 +338,8 @@ off under `--dry-run`. Esc closes the prompt.
|
|
|
335
338
|
Nothing happens while the picker is open. Changes are staged, the screen is
|
|
336
339
|
torn down, and only then do the real operations run, so their output (which
|
|
337
340
|
companion files moved, which were kept because they are shared) is readable
|
|
338
|
-
instead of fighting curses for the terminal.
|
|
341
|
+
instead of fighting curses for the terminal. Claude Code sessions running the
|
|
342
|
+
[pane mod](#claude-code-pane-mod) pick up the Claude changes within 5 s and reload them.
|
|
339
343
|
|
|
340
344
|
Built on stdlib `curses`, so there is nothing to install on macOS and Linux
|
|
341
345
|
(on Windows, `pip install "agent-toggle[windows]"` pulls `windows-curses`).
|
|
@@ -437,10 +441,15 @@ before the command and is offered after it, on every exit path; it overlaps the
|
|
|
437
441
|
|
|
438
442
|
- **On a terminal** (stdin and stderr are TTYs, no `--json`): a panel on stderr, "agent-toggle X
|
|
439
443
|
is available (you have Y)", with `Update now`, `Skip` and `Skip until next version`, a release
|
|
440
|
-
notes link, and `↑` `↓` `Enter` `q`. `Update now` runs
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
`
|
|
444
|
+
notes link, and `↑` `↓` `Enter` `q`. `Update now` runs, in order and stopping at the first
|
|
445
|
+
failure: `uv tool upgrade agent-toggle` (`pipx upgrade agent-toggle` for a pipx install), then,
|
|
446
|
+
when the Claude Code plugin is installed, `claude plugin marketplace update agent-toggle` and
|
|
447
|
+
`claude plugin update agent-toggle@agent-toggle`, then `agent-toggle install-shims`. A failure
|
|
448
|
+
gives a yellow warning listing the commands to run yourself.
|
|
449
|
+
- **Windows**: a running `agent-toggle.exe` cannot be replaced, so there is no `Update now`; the
|
|
450
|
+
commands are printed instead (as below), to run once agent-toggle has exited.
|
|
451
|
+
- **Off a terminal** (or `--json`): a stderr line, `agent-toggle X is available (you have Y)`,
|
|
452
|
+
then those commands, one per line.
|
|
444
453
|
- It never changes the exit code or stdout, so `--json` output stays one document.
|
|
445
454
|
|
|
446
455
|
It is one `GET https://pypi.org/pypi/agent-toggle/json` on a background thread with a 0.8 s
|
|
@@ -566,7 +575,8 @@ visible rather than silent.
|
|
|
566
575
|
hand-rolled serializer would mangle comments — a text slice is lossless and
|
|
567
576
|
much less code.
|
|
568
577
|
|
|
569
|
-
MCP changes need a **new session** to take effect
|
|
578
|
+
MCP changes need a **new session** to take effect, except in a Claude Code session running the
|
|
579
|
+
agent-toggle mod, which runs `/mcp disable|enable <name>` for you (see the mod section).
|
|
570
580
|
|
|
571
581
|
### Claude MCP scopes
|
|
572
582
|
|
|
@@ -814,6 +824,7 @@ bookkeeping.
|
|
|
814
824
|
| `state.json` | current disabled list (schema v3; atomic write, mode `0600`), saved before each flag write or dir move with the op in flight under an optional `pending` key, and after each MCP / plugin item |
|
|
815
825
|
| `lock` | held by `disable` / `enable` / `enable --all` / `undo` / `profile apply` / `migrate` / `ui` for the whole batch; a second run waits 5 s then exits `3` (stale after 10 min *and* its PID is gone; a live batch refreshes it per item; the error says when its PID is gone, i.e. a killed run left it) |
|
|
816
826
|
| `log.jsonl` | one line per operation (mode `0600`), see below |
|
|
827
|
+
| `reload.json` | the Claude rows the last `ui` Enter changed, rewritten each time; open Claude Code sessions running the mod poll it and reload them |
|
|
817
828
|
| `mcp-backups/` | `<harness>__<server>.json`, or `<sha8>__<harness>__<server>.json` for a project `.mcp.json` (mode `0600` -- may hold auth headers) |
|
|
818
829
|
| `companions/` | parked exclusive helper files |
|
|
819
830
|
| `parked/user/<owner>/<dir>/` | user-scope items, e.g. `parked/user/claude/skills/<name>` (`<owner>` = the harness whose home holds the dir; a dir shared by several harnesses parks once, under its owner); an absolute OpenCode `skills.paths` dir parks under `parked/user/opencode/ext-<sha8>/` (`<sha8>` of its resolved path). Nothing is ever parked inside a harness home |
|
|
@@ -916,7 +927,8 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
|
|
|
916
927
|
- It looks like the `ui` picker: a title with the live-token total, a search line, rows under
|
|
917
928
|
type headings with counts, green `●` live / yellow `○` parked, tokens and a bar per row (a
|
|
918
929
|
parked row shows `(N)`, what restoring it would load), the cursor row in reverse video, and a
|
|
919
|
-
key-chip footer. The list scrolls with the cursor
|
|
930
|
+
key-chip footer. The list scrolls with the cursor; the status line shows its position (`7/23`),
|
|
931
|
+
and the first and last list lines count the rows out of view (`↑ 4`, `↓ 12`).
|
|
920
932
|
- The mouse wheel over the list moves the cursor, as in the terminal picker; no click needed.
|
|
921
933
|
- Click the list once so it takes keys (Esc hands the keyboard back to the prompt). Then `↑` `↓`
|
|
922
934
|
`PgUp` `PgDn` `Home` `End` (also `Ctrl-P` / `Ctrl-N`) move; `Space` or `Enter` toggles the row,
|
|
@@ -926,8 +938,8 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
|
|
|
926
938
|
...), like `disable all <name>`; `Ctrl-U` clears the search. A click moves the cursor.
|
|
927
939
|
- A toggle applies at once (no staging, no Enter to apply); the toast names the target and the state
|
|
928
940
|
with an emoji and a bracketed tag (no colour codes, so nothing garbles; on the Linux console and the
|
|
929
|
-
legacy Windows console, where emoji draw as boxes, the tag alone): `✅ [ENABLED] skill: foo`, `⛔ [DISABLED] mod: bar (
|
|
930
|
-
session)`, `⛔ [DISABLED] all: foo` after `n`, or `❌ [FAILED] disable skill: foo - <error>`.
|
|
941
|
+
legacy Windows console, where emoji draw as boxes, the tag alone): `✅ [ENABLED] skill: foo`, `⛔ [DISABLED] mod: bar (/reload-plugins)`,
|
|
942
|
+
`⛔ [DISABLED] rule: baz (takes effect in a new session)`, `⛔ [DISABLED] all: foo` after `n`, or `❌ [FAILED] disable skill: foo - <error>`.
|
|
931
943
|
The row flips the moment you press (and flips back
|
|
932
944
|
if the CLI fails); the list reloads in the background. While a toggle runs, further keys are ignored.
|
|
933
945
|
- Opening the pane shows the same braille spinner and `Loading resources…` as `agent-toggle ui`
|
|
@@ -938,6 +950,15 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
|
|
|
938
950
|
`undo`, `list`, `status`, `cost` or `doctor` runs the CLI instead of opening the pane and prints
|
|
939
951
|
one line per result row: `/agent-toggle disable skill foo bar` (it adds `--json`, and
|
|
940
952
|
`--harness claude` unless you pass `--harness`). A toggle reloads an open pane.
|
|
953
|
+
- Inside Claude Code the mod then applies the change to the running session by queueing the slash
|
|
954
|
+
command for you, once per kind: skills and commands `/reload-skills`, plugins (mods included)
|
|
955
|
+
`/reload-plugins`, an MCP server `/mcp disable|enable <name>`. The toast names it, e.g.
|
|
956
|
+
`⛔ [DISABLED] skill: foo (/reload-skills)`. Agents and rules have no reload and still say
|
|
957
|
+
`takes effect in a new session`. If Claude refuses a queued command, a toast says so.
|
|
958
|
+
- `agent-toggle ui` in another terminal applies on Enter and writes the Claude rows it changed to
|
|
959
|
+
`~/.agent-toggle/reload.json`. Every open session running the mod checks that file's mtime every
|
|
960
|
+
5 s (one `stat`) and queues the same reloads, toasting `agent-toggle ui: /reload-skills`. A file
|
|
961
|
+
older than the session is ignored.
|
|
941
962
|
- Needs `agent-toggle` on `PATH`.
|
|
942
963
|
- On Windows `agent-toggle` must resolve to a real executable on `PATH` (a pip or uv install
|
|
943
964
|
provides `agent-toggle.exe`).
|
|
@@ -325,6 +325,7 @@ def cmd_ui(state: dict, out: Result, dry_run: bool = False, project: str | None
|
|
|
325
325
|
if dry_run:
|
|
326
326
|
out.say("\ndry run -- nothing changed")
|
|
327
327
|
return
|
|
328
|
+
store.signal_reload(out.rows)
|
|
328
329
|
if any(r.type in ("mcp", "plugin") for r in changes):
|
|
329
330
|
out.say("\nMCP/plugin changed -- open a NEW session for it to take effect.")
|
|
330
331
|
|
|
@@ -557,7 +558,7 @@ def _stale_ignore_lines(h, args: argparse.Namespace, out: Result) -> None:
|
|
|
557
558
|
|
|
558
559
|
|
|
559
560
|
COMMANDS = ("ui", "pick", "status", "list", "cost", "migrate", "disable", "enable",
|
|
560
|
-
"install-shims", "profile", "undo", "doctor", "config")
|
|
561
|
+
"install-shims", "profile", "undo", "doctor", "config", "update")
|
|
561
562
|
_GLOBAL_FLAGS = ("--json", "-v", "--verbose")
|
|
562
563
|
|
|
563
564
|
|
|
@@ -692,6 +693,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
692
693
|
pp.add_argument("--project", metavar="dir", help=PROJECT_HELP)
|
|
693
694
|
sub.add_parser("doctor", parents=[common],
|
|
694
695
|
help="check harness layouts and state against disk; offers y/n fixes")
|
|
696
|
+
sub.add_parser("update", parents=[common],
|
|
697
|
+
help="check for a newer release; show the commands that update everything")
|
|
695
698
|
cf = sub.add_parser("config", parents=[common],
|
|
696
699
|
help="Telegram settings for the CI failure alerts (needs the telegram extra)")
|
|
697
700
|
cf.add_argument("action", nargs="?", choices=("test", "sync-ci"),
|
|
@@ -726,7 +729,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
726
729
|
language, color = "en", "auto"
|
|
727
730
|
i18n.set_language(language)
|
|
728
731
|
color = scan_color(argv, color) # --color > env > setting > auto
|
|
729
|
-
|
|
732
|
+
i = _command_index(argv)
|
|
733
|
+
started = None if argv[i:i + 1] == ["update"] else update_prompt.start() # `update` is the check
|
|
730
734
|
try:
|
|
731
735
|
return _main(argv, color)
|
|
732
736
|
except KeyboardInterrupt:
|
|
@@ -794,6 +798,8 @@ def _main(argv: list[str], color: str) -> int:
|
|
|
794
798
|
fixes = doctor.cmd_doctor(args.harness, out)
|
|
795
799
|
if fixes and _interactive(out):
|
|
796
800
|
doctor.offer_fixes(fixes, out)
|
|
801
|
+
elif cmd == "update":
|
|
802
|
+
update_prompt.cmd_update(out)
|
|
797
803
|
elif cmd == "config":
|
|
798
804
|
config.cmd_config(args, out)
|
|
799
805
|
elif cmd == "undo":
|
|
@@ -135,6 +135,14 @@ def commands(default_harness: str = "claude") -> dict[str, Cmd]:
|
|
|
135
135
|
("--dry-run", t("help.opt.config_dry_run",
|
|
136
136
|
"sync-ci: show the plan; set nothing"))],
|
|
137
137
|
examples=["config", "config test", "config sync-ci --dry-run"]),
|
|
138
|
+
"update": Cmd(
|
|
139
|
+
"update",
|
|
140
|
+
t("help.update.summary", "check for a newer release; show how to update"),
|
|
141
|
+
t("help.update.desc", "Ask PyPI now (ignoring the cache and a skipped version) and, "
|
|
142
|
+
"when a newer release exists, print the commands that update "
|
|
143
|
+
"the CLI, the Claude Code plugin (if installed) and the shims. "
|
|
144
|
+
"Runs none of them."),
|
|
145
|
+
examples=["update", "update --json"]),
|
|
138
146
|
"install-shims": Cmd(
|
|
139
147
|
"install-shims [--dry-run] [--gitignore | --no-gitignore]",
|
|
140
148
|
t("help.install_shims.summary", "write the skill shim into every installed harness"),
|
|
@@ -166,7 +174,7 @@ def commands(default_harness: str = "claude") -> dict[str, Cmd]:
|
|
|
166
174
|
def groups() -> list[tuple[str, list[str]]]:
|
|
167
175
|
return [(t("help.group.toggle", "Toggle"), ["ui", "disable", "enable", "undo", "profile"]),
|
|
168
176
|
(t("help.group.inspect", "Inspect"), ["status", "list", "cost", "doctor"]),
|
|
169
|
-
(t("help.group.setup", "Setup"), ["config", "install-shims", "migrate"])]
|
|
177
|
+
(t("help.group.setup", "Setup"), ["config", "install-shims", "update", "migrate"])]
|
|
170
178
|
|
|
171
179
|
|
|
172
180
|
def _table(rows: list[tuple[str, str]], color: bool, role: str = "choice") -> list[str]:
|
|
@@ -27,6 +27,9 @@ CATALOG: dict[str, str] = {
|
|
|
27
27
|
"help.cost.summary": "每個項目的預估啟動 token 數,由大到小",
|
|
28
28
|
"help.cost.desc": "估算每個項目啟動時載入多少 token(字元數/4,±25%),"
|
|
29
29
|
"以及暫存項目已省下多少。",
|
|
30
|
+
"help.update.summary": "檢查新版本;顯示更新方式",
|
|
31
|
+
"help.update.desc": "立即向 PyPI 查詢(忽略快取與已略過的版本);有新版本時,印出更新 CLI、"
|
|
32
|
+
"Claude Code 外掛(若已安裝)與 shim 的指令。不會執行任何指令。",
|
|
30
33
|
"help.doctor.summary": "檢查 harness 目錄結構與狀態",
|
|
31
34
|
"help.doctor.desc": "比對狀態檔與磁碟;有問題時結束碼為 1。在終端機上,可自動修復的問題"
|
|
32
35
|
"(chmod、過期項目、孤立檔案、中斷的操作)會逐一詢問 y/n;否則不做任何變更。",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-toggle
|
|
3
3
|
description: Temporarily disable or restore AI-agent resources — skills, agents, commands, rules, plugins, MCP servers — across Claude Code, Codex, Grok CLI, OpenCode, OpenClaw, Copilot and Vibe. Nothing is deleted; everything is parked and reversible. Use when the user wants to turn something off, trim startup context cost, re-enable something previously disabled, or asks "how do I disable X" / "turn off this MCP" / "add it back". Also when they ask to open, close or toggle the agent-toggle pane ("open agent-toggle" / "close the agent-toggle pane"), and to save, switch, rename or delete a profile ("save my setup as work" / "switch to my work profile").
|
|
4
|
+
argument-hint: "[status | list | cost | disable|enable <type> <name>... | undo | profile | doctor | update | open | close]"
|
|
4
5
|
---
|
|
5
6
|
<!-- agent-toggle shim: written by `agent-toggle install-shims`; re-running it overwrites this file -->
|
|
6
7
|
|
|
@@ -16,7 +17,7 @@ agent-toggle <command> --json
|
|
|
16
17
|
```
|
|
17
18
|
If `agent-toggle` is not on PATH, ask the user to install it (`uv tool install agent-toggle`); do not guess where a checkout lives.
|
|
18
19
|
|
|
19
|
-
Commands: `status` (run first), `doctor` (read-only health check), `list [type]`,
|
|
20
|
+
Commands: `status` (run first), `doctor` (read-only health check), `update` (checks for a newer release and lists the commands that update it; run none yourself, relay them), `list [type]`,
|
|
20
21
|
`cost [--type T]` (startup token estimates, biggest first), `disable <type> <name>...` (type `all`: every type with that name),
|
|
21
22
|
`enable <type> <name>...`, `enable --all`, `undo` (reverse the last batch),
|
|
22
23
|
`profile save|apply|diff|delete <name>`, `profile rename <old> <new>`, `profile list` (see Profiles), `migrate`; add `--harness claude|codex|grok|opencode|openclaw|copilot|vibe|devin|agy`
|
|
@@ -20,7 +20,7 @@ You are running in `__HARNESS__`: pass `--harness __HARNESS__` for its own items
|
|
|
20
20
|
(the default is `claude`). Other harnesses: `--harness claude|codex|grok|opencode|openclaw|copilot|vibe|devin|agy`.
|
|
21
21
|
|
|
22
22
|
Commands: `status` (run first; shows which types this harness supports),
|
|
23
|
-
`doctor` (read-only health check), `list [type]`, `cost [--type T]` (startup
|
|
23
|
+
`doctor` (read-only health check), `update` (checks for a newer release and lists the commands that update it; run none yourself, relay them), `list [type]`, `cost [--type T]` (startup
|
|
24
24
|
token estimates, biggest first), `disable <type> <name>...` (type `all`: every type with that name), `enable <type> <name>...`,
|
|
25
25
|
`enable --all`, `undo` (reverse the last batch), `profile save|apply|diff|delete <name>`,
|
|
26
26
|
`profile rename <old> <new>`, `profile list` (see Profiles); add `--dry-run` to preview. `<type>` = `skill` / `agent` /
|
|
@@ -259,6 +259,15 @@ def log(action: str, type_: str, name: str, result: str, detail: str = "", *,
|
|
|
259
259
|
fh.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
|
260
260
|
|
|
261
261
|
|
|
262
|
+
def signal_reload(rows: list[dict]) -> None:
|
|
263
|
+
"""`ui` runs in a terminal of its own, so it cannot reload a Claude Code session: it leaves the claude rows it
|
|
264
|
+
changed here, and every session running the agent-toggle mod polls the file and reloads them (mod/hooks/register.tsx)."""
|
|
265
|
+
done = [r for r in rows if r["harness"] == "claude" and r["status"] == "ok" and r["action"] in ("disable", "enable")]
|
|
266
|
+
if done:
|
|
267
|
+
fs.private_dir(fs.state_dir())
|
|
268
|
+
fs.atomic_write(fs.reload_file(), json.dumps({"batch": BATCH, "results": done}, ensure_ascii=False))
|
|
269
|
+
|
|
270
|
+
|
|
262
271
|
def migrate(state: dict) -> int:
|
|
263
272
|
"""Import ~/.claude-toggle state so existing rollbacks keep working."""
|
|
264
273
|
legacy_state_dir, backup_dir = fs.legacy_state_dir(), fs.backup_dir()
|
|
@@ -143,8 +143,10 @@ def check(
|
|
|
143
143
|
timeout: float = DEFAULT_TIMEOUT,
|
|
144
144
|
now: float | None = None,
|
|
145
145
|
fetch: Callable[[str, float], str] | None = None,
|
|
146
|
+
ignore_skip: bool = False,
|
|
146
147
|
) -> UpdateAvailable | None:
|
|
147
|
-
"""Return an update when the latest release is newer than *current*, else None.
|
|
148
|
+
"""Return an update when the latest release is newer than *current*, else None.
|
|
149
|
+
*ignore_skip* reports a "skip this version" release too (an explicit ask)."""
|
|
148
150
|
current_parts = _numeric_tuple(current)
|
|
149
151
|
if current_parts is None:
|
|
150
152
|
return None
|
|
@@ -166,7 +168,7 @@ def check(
|
|
|
166
168
|
# offline costs one timeout per TTL, not one per command.
|
|
167
169
|
latest = fetched or latest
|
|
168
170
|
_save_cache(cache_path, stamp, latest)
|
|
169
|
-
if not isinstance(latest, str) or latest == _valid_version(cached.get("skipped")):
|
|
171
|
+
if not isinstance(latest, str) or (not ignore_skip and latest == _valid_version(cached.get("skipped"))):
|
|
170
172
|
return None
|
|
171
173
|
latest_parts = _numeric_tuple(latest)
|
|
172
174
|
if latest_parts is None or latest_parts <= current_parts:
|
|
@@ -18,12 +18,16 @@ from collections.abc import Iterable, Iterator
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
20
|
from . import __version__, fs, settings, update_check
|
|
21
|
+
from .backends import plugin_cli
|
|
21
22
|
from .i18n import t
|
|
22
23
|
from .output import use_color
|
|
23
24
|
from .ui import theme
|
|
24
25
|
|
|
25
26
|
DIST = "agent-toggle"
|
|
26
|
-
|
|
27
|
+
PLUGIN = "agent-toggle@agent-toggle"
|
|
28
|
+
# shortcut: pipx is spotted by "pipx" in the venv path (its default homes); a custom PIPX_HOME gets uv
|
|
29
|
+
UPGRADE = (["pipx", "upgrade", DIST] if "pipx" in Path(sys.prefix).parts
|
|
30
|
+
else ["uv", "tool", "upgrade", DIST])
|
|
27
31
|
RELEASE_NOTES = "https://github.com/weskao/agent-toggle/releases/tag/v%s"
|
|
28
32
|
ANSWERS = (update_check.UPDATE_NOW, update_check.SKIP, update_check.SKIP_VERSION)
|
|
29
33
|
|
|
@@ -36,6 +40,16 @@ def _clean(text: str) -> str:
|
|
|
36
40
|
return _CONTROL.sub("", text)
|
|
37
41
|
|
|
38
42
|
|
|
43
|
+
def upgrade_steps() -> list[list[str]]:
|
|
44
|
+
"""Upgrade the CLI, then the Claude Code plugin when it is installed, then refresh the shims."""
|
|
45
|
+
steps = [UPGRADE]
|
|
46
|
+
# bare "claude": what a person pastes; offer()'s run() resolves it, as claude is often off PATH
|
|
47
|
+
if plugin_cli.claude_bin() and any(p["id"] == PLUGIN for p in plugin_cli.list_plugins(lambda _msg: None)):
|
|
48
|
+
steps += [["claude", "plugin", "marketplace", "update", DIST],
|
|
49
|
+
["claude", "plugin", "update", PLUGIN]]
|
|
50
|
+
return [*steps, [DIST, "install-shims"]]
|
|
51
|
+
|
|
52
|
+
|
|
39
53
|
def cache_path() -> Path:
|
|
40
54
|
return fs.state_dir() / "update-check.json"
|
|
41
55
|
|
|
@@ -64,9 +78,10 @@ def _available(found: update_check.UpdateAvailable) -> str:
|
|
|
64
78
|
|
|
65
79
|
|
|
66
80
|
def hint(found: update_check.UpdateAvailable) -> str:
|
|
67
|
-
"""The `ask` off a terminal (CI, pipes, --json):
|
|
81
|
+
"""The `ask` off a terminal (CI, pipes, --json): the headline, then one command per line."""
|
|
68
82
|
print(_available(found), file=sys.stderr)
|
|
69
|
-
|
|
83
|
+
for step in upgrade_steps():
|
|
84
|
+
print(" " + " ".join(step), file=sys.stderr)
|
|
70
85
|
return update_check.SKIP
|
|
71
86
|
|
|
72
87
|
|
|
@@ -182,20 +197,44 @@ def offer(started: update_check.Started | None, *, json_mode: bool = False,
|
|
|
182
197
|
stderr only and never raises, so neither stdout nor the exit code can change."""
|
|
183
198
|
if started is None:
|
|
184
199
|
return None
|
|
185
|
-
|
|
200
|
+
# Windows cannot replace the agent-toggle.exe that is running: print the commands, run none
|
|
201
|
+
if is_interactive() and not json_mode and not fs.WIN:
|
|
186
202
|
def asker(found):
|
|
187
203
|
return ask(found, color_mode)
|
|
188
204
|
else:
|
|
189
205
|
asker = hint
|
|
190
|
-
def run(cmd, **kw): #
|
|
191
|
-
|
|
192
|
-
|
|
206
|
+
def run(cmd, **kw): # their output must never reach stdout; no step may prompt
|
|
207
|
+
if cmd[0] == "claude":
|
|
208
|
+
cmd = [plugin_cli.claude_bin() or "claude", *cmd[1:]]
|
|
209
|
+
return subprocess.run(cmd, stdout=sys.stderr, stdin=subprocess.DEVNULL, **kw)
|
|
210
|
+
steps: list[list[str]] = []
|
|
211
|
+
def upgrade(_found):
|
|
212
|
+
steps.extend(upgrade_steps())
|
|
213
|
+
return steps
|
|
214
|
+
answer = update_check.offer(started, asker, cache_path=cache_path(), upgrade=upgrade, run=run)
|
|
193
215
|
if answer == update_check.UPGRADE_FAILED:
|
|
194
216
|
try:
|
|
195
217
|
mark = theme.ansi("warning", "⚠" if theme.get_glyphs(sys.stderr).unicode else "!",
|
|
196
218
|
use_color(sys.stderr, color_mode))
|
|
197
219
|
print(mark + " " + t("update.failed", "upgrade did not finish — run it yourself: %s",
|
|
198
|
-
" ".join
|
|
220
|
+
"\n ".join(map(" ".join, steps or [UPGRADE]))), file=sys.stderr)
|
|
199
221
|
except Exception: # noqa: BLE001, S110 - a hint must never fail the command
|
|
200
222
|
pass
|
|
201
223
|
return answer
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def cmd_update(out) -> None:
|
|
227
|
+
"""`update`: check PyPI now (cache and "skipped" ignored), show what would be updated and
|
|
228
|
+
the commands for it. Runs none of them: the picker's Update now does that."""
|
|
229
|
+
found = update_check.check(DIST, __version__, cache_path=cache_path(), ttl_seconds=0,
|
|
230
|
+
timeout=5.0, ignore_skip=True)
|
|
231
|
+
if found is None:
|
|
232
|
+
out.row(None, "cli", DIST, "update", "ok", f"no newer release found (you have {__version__})",
|
|
233
|
+
current=__version__, latest=None, commands=[])
|
|
234
|
+
return
|
|
235
|
+
steps = [" ".join(s) for s in upgrade_steps()]
|
|
236
|
+
out.row(None, "cli", DIST, "update", "ok", f"{found.current} -> {found.latest}", show=False,
|
|
237
|
+
current=found.current, latest=found.latest, commands=steps)
|
|
238
|
+
out.say(_available(found) + ". Run, in order:")
|
|
239
|
+
for step in steps:
|
|
240
|
+
out.say(" " + step)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-toggle
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Temporarily disable / re-enable AI-agent skills, agents, commands, rules, plugins and MCP servers across harnesses (Claude Code, Codex, Grok, OpenCode, OpenClaw, Copilot, Vibe, Devin, Antigravity)
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/weskao/agent-toggle
|
|
@@ -32,6 +32,8 @@ Dynamic: license-file
|
|
|
32
32
|
|
|
33
33
|
# agent-toggle
|
|
34
34
|
|
|
35
|
+
PyPI: <https://pypi.org/project/agent-toggle/>
|
|
36
|
+
|
|
35
37
|
Temporarily disable and restore AI-agent resources — skills, agents, commands,
|
|
36
38
|
rules, plugins (mods included), MCP servers — across Claude Code, Codex, Grok CLI, OpenCode,
|
|
37
39
|
OpenClaw, Copilot, Vibe, Devin and Antigravity — and show what each one costs at
|
|
@@ -193,6 +195,7 @@ agent-toggle <command> [args] # or: python3 agent_toggle.py <command> [
|
|
|
193
195
|
| `status` | health check: harnesses found, types each supports, parked counts, gitignore, untracked parked items, stale live twins, shared dirs, and state entries whose parked item is gone (a `stale` row with the fix command; read-only, still exit `0`) |
|
|
194
196
|
| `list [type]` | what is currently disabled (`--project <dir>` filters to one project) |
|
|
195
197
|
| `cost [--type T]` | estimated startup tokens per item, biggest first (read-only; `--harness H` filters; `--project <dir>` prices a repo's `.claude/` and `.mcp.json` instead of user scope) |
|
|
198
|
+
| `update` | check PyPI now (ignores the cache and a skipped version) and print the commands that update the CLI, the Claude Code plugin (when installed) and the shims; runs none of them. `/agent-toggle update` in Claude Code does the same; opening the pane toasts a newer release once |
|
|
196
199
|
| `install-shims` | write the skill shim into every installed harness that is on in the settings (an off one is skipped unless named with `--harness`); refuses to overwrite a file it did not write (`--dry-run` shows the plan); reports stale legacy `<dir>-disabled/` lines in a harness-home `.gitignore` (`--gitignore` removes them, `--no-gitignore` skips the check); on a terminal, then asks y/n for each problem `doctor` can fix (see [Fixing](#fixing)) |
|
|
197
200
|
| `disable <type> <name>...` | park one or more items (`--dry-run` shows the plan; `--project <dir>` for a repo's own `.claude/` and `.mcp.json`) |
|
|
198
201
|
| `enable <type> <name>...` | put them back (`--dry-run` shows the plan; `--project <dir>` likewise) |
|
|
@@ -367,7 +370,8 @@ off under `--dry-run`. Esc closes the prompt.
|
|
|
367
370
|
Nothing happens while the picker is open. Changes are staged, the screen is
|
|
368
371
|
torn down, and only then do the real operations run, so their output (which
|
|
369
372
|
companion files moved, which were kept because they are shared) is readable
|
|
370
|
-
instead of fighting curses for the terminal.
|
|
373
|
+
instead of fighting curses for the terminal. Claude Code sessions running the
|
|
374
|
+
[pane mod](#claude-code-pane-mod) pick up the Claude changes within 5 s and reload them.
|
|
371
375
|
|
|
372
376
|
Built on stdlib `curses`, so there is nothing to install on macOS and Linux
|
|
373
377
|
(on Windows, `pip install "agent-toggle[windows]"` pulls `windows-curses`).
|
|
@@ -469,10 +473,15 @@ before the command and is offered after it, on every exit path; it overlaps the
|
|
|
469
473
|
|
|
470
474
|
- **On a terminal** (stdin and stderr are TTYs, no `--json`): a panel on stderr, "agent-toggle X
|
|
471
475
|
is available (you have Y)", with `Update now`, `Skip` and `Skip until next version`, a release
|
|
472
|
-
notes link, and `↑` `↓` `Enter` `q`. `Update now` runs
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
`
|
|
476
|
+
notes link, and `↑` `↓` `Enter` `q`. `Update now` runs, in order and stopping at the first
|
|
477
|
+
failure: `uv tool upgrade agent-toggle` (`pipx upgrade agent-toggle` for a pipx install), then,
|
|
478
|
+
when the Claude Code plugin is installed, `claude plugin marketplace update agent-toggle` and
|
|
479
|
+
`claude plugin update agent-toggle@agent-toggle`, then `agent-toggle install-shims`. A failure
|
|
480
|
+
gives a yellow warning listing the commands to run yourself.
|
|
481
|
+
- **Windows**: a running `agent-toggle.exe` cannot be replaced, so there is no `Update now`; the
|
|
482
|
+
commands are printed instead (as below), to run once agent-toggle has exited.
|
|
483
|
+
- **Off a terminal** (or `--json`): a stderr line, `agent-toggle X is available (you have Y)`,
|
|
484
|
+
then those commands, one per line.
|
|
476
485
|
- It never changes the exit code or stdout, so `--json` output stays one document.
|
|
477
486
|
|
|
478
487
|
It is one `GET https://pypi.org/pypi/agent-toggle/json` on a background thread with a 0.8 s
|
|
@@ -598,7 +607,8 @@ visible rather than silent.
|
|
|
598
607
|
hand-rolled serializer would mangle comments — a text slice is lossless and
|
|
599
608
|
much less code.
|
|
600
609
|
|
|
601
|
-
MCP changes need a **new session** to take effect
|
|
610
|
+
MCP changes need a **new session** to take effect, except in a Claude Code session running the
|
|
611
|
+
agent-toggle mod, which runs `/mcp disable|enable <name>` for you (see the mod section).
|
|
602
612
|
|
|
603
613
|
### Claude MCP scopes
|
|
604
614
|
|
|
@@ -846,6 +856,7 @@ bookkeeping.
|
|
|
846
856
|
| `state.json` | current disabled list (schema v3; atomic write, mode `0600`), saved before each flag write or dir move with the op in flight under an optional `pending` key, and after each MCP / plugin item |
|
|
847
857
|
| `lock` | held by `disable` / `enable` / `enable --all` / `undo` / `profile apply` / `migrate` / `ui` for the whole batch; a second run waits 5 s then exits `3` (stale after 10 min *and* its PID is gone; a live batch refreshes it per item; the error says when its PID is gone, i.e. a killed run left it) |
|
|
848
858
|
| `log.jsonl` | one line per operation (mode `0600`), see below |
|
|
859
|
+
| `reload.json` | the Claude rows the last `ui` Enter changed, rewritten each time; open Claude Code sessions running the mod poll it and reload them |
|
|
849
860
|
| `mcp-backups/` | `<harness>__<server>.json`, or `<sha8>__<harness>__<server>.json` for a project `.mcp.json` (mode `0600` -- may hold auth headers) |
|
|
850
861
|
| `companions/` | parked exclusive helper files |
|
|
851
862
|
| `parked/user/<owner>/<dir>/` | user-scope items, e.g. `parked/user/claude/skills/<name>` (`<owner>` = the harness whose home holds the dir; a dir shared by several harnesses parks once, under its owner); an absolute OpenCode `skills.paths` dir parks under `parked/user/opencode/ext-<sha8>/` (`<sha8>` of its resolved path). Nothing is ever parked inside a harness home |
|
|
@@ -948,7 +959,8 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
|
|
|
948
959
|
- It looks like the `ui` picker: a title with the live-token total, a search line, rows under
|
|
949
960
|
type headings with counts, green `●` live / yellow `○` parked, tokens and a bar per row (a
|
|
950
961
|
parked row shows `(N)`, what restoring it would load), the cursor row in reverse video, and a
|
|
951
|
-
key-chip footer. The list scrolls with the cursor
|
|
962
|
+
key-chip footer. The list scrolls with the cursor; the status line shows its position (`7/23`),
|
|
963
|
+
and the first and last list lines count the rows out of view (`↑ 4`, `↓ 12`).
|
|
952
964
|
- The mouse wheel over the list moves the cursor, as in the terminal picker; no click needed.
|
|
953
965
|
- Click the list once so it takes keys (Esc hands the keyboard back to the prompt). Then `↑` `↓`
|
|
954
966
|
`PgUp` `PgDn` `Home` `End` (also `Ctrl-P` / `Ctrl-N`) move; `Space` or `Enter` toggles the row,
|
|
@@ -958,8 +970,8 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
|
|
|
958
970
|
...), like `disable all <name>`; `Ctrl-U` clears the search. A click moves the cursor.
|
|
959
971
|
- A toggle applies at once (no staging, no Enter to apply); the toast names the target and the state
|
|
960
972
|
with an emoji and a bracketed tag (no colour codes, so nothing garbles; on the Linux console and the
|
|
961
|
-
legacy Windows console, where emoji draw as boxes, the tag alone): `✅ [ENABLED] skill: foo`, `⛔ [DISABLED] mod: bar (
|
|
962
|
-
session)`, `⛔ [DISABLED] all: foo` after `n`, or `❌ [FAILED] disable skill: foo - <error>`.
|
|
973
|
+
legacy Windows console, where emoji draw as boxes, the tag alone): `✅ [ENABLED] skill: foo`, `⛔ [DISABLED] mod: bar (/reload-plugins)`,
|
|
974
|
+
`⛔ [DISABLED] rule: baz (takes effect in a new session)`, `⛔ [DISABLED] all: foo` after `n`, or `❌ [FAILED] disable skill: foo - <error>`.
|
|
963
975
|
The row flips the moment you press (and flips back
|
|
964
976
|
if the CLI fails); the list reloads in the background. While a toggle runs, further keys are ignored.
|
|
965
977
|
- Opening the pane shows the same braille spinner and `Loading resources…` as `agent-toggle ui`
|
|
@@ -970,6 +982,15 @@ claude --plugin-dir ./mod # then run /agent-toggle inside Claude
|
|
|
970
982
|
`undo`, `list`, `status`, `cost` or `doctor` runs the CLI instead of opening the pane and prints
|
|
971
983
|
one line per result row: `/agent-toggle disable skill foo bar` (it adds `--json`, and
|
|
972
984
|
`--harness claude` unless you pass `--harness`). A toggle reloads an open pane.
|
|
985
|
+
- Inside Claude Code the mod then applies the change to the running session by queueing the slash
|
|
986
|
+
command for you, once per kind: skills and commands `/reload-skills`, plugins (mods included)
|
|
987
|
+
`/reload-plugins`, an MCP server `/mcp disable|enable <name>`. The toast names it, e.g.
|
|
988
|
+
`⛔ [DISABLED] skill: foo (/reload-skills)`. Agents and rules have no reload and still say
|
|
989
|
+
`takes effect in a new session`. If Claude refuses a queued command, a toast says so.
|
|
990
|
+
- `agent-toggle ui` in another terminal applies on Enter and writes the Claude rows it changed to
|
|
991
|
+
`~/.agent-toggle/reload.json`. Every open session running the mod checks that file's mtime every
|
|
992
|
+
5 s (one `stat`) and queues the same reloads, toasting `agent-toggle ui: /reload-skills`. A file
|
|
993
|
+
older than the session is ignored.
|
|
973
994
|
- Needs `agent-toggle` on `PATH`.
|
|
974
995
|
- On Windows `agent-toggle` must resolve to a real executable on `PATH` (a pip or uv install
|
|
975
996
|
provides `agent-toggle.exe`).
|
|
@@ -78,6 +78,7 @@ class ApplyPlanSeamTest(SandboxCase):
|
|
|
78
78
|
self.assertEqual(plan, [ops.Op("claude", "skill", "disable", "demo-skill")])
|
|
79
79
|
self.assertEqual(spy.call_args.kwargs["batch"], store.BATCH)
|
|
80
80
|
self.assertFalse((self.home / "skills/demo-skill").exists())
|
|
81
|
+
self.assertFalse(fs.reload_file().exists()) # only `ui` signals: the mod reloads its own runs itself
|
|
81
82
|
|
|
82
83
|
def test_cmd_ui_goes_through_apply_plan(self) -> None:
|
|
83
84
|
try:
|
|
@@ -94,6 +95,10 @@ class ApplyPlanSeamTest(SandboxCase):
|
|
|
94
95
|
[ops.Op("claude", "skill", "disable", "demo-skill")])
|
|
95
96
|
self.assertFalse((self.home / "skills/demo-skill").exists())
|
|
96
97
|
self.assertIn("claude:skill:demo-skill", store.load_state()["disabled"])
|
|
98
|
+
# Enter applied it: open Claude Code sessions (the mod polls this file) reload the skill
|
|
99
|
+
sig = json.loads(fs.reload_file().read_text(encoding="utf-8"))
|
|
100
|
+
self.assertEqual([(r["type"], r["name"], r["action"], r["status"]) for r in sig["results"]],
|
|
101
|
+
[("skill", "demo-skill", "disable", "ok")])
|
|
97
102
|
|
|
98
103
|
def test_type_without_a_mechanism_is_an_unsupported_row(self) -> None:
|
|
99
104
|
out = Result()
|
|
@@ -176,7 +181,8 @@ class UpdateCheckTest(CliCase):
|
|
|
176
181
|
def test_hint_on_stderr_off_a_tty(self) -> None:
|
|
177
182
|
rc, out, err = self.run_cli("status")
|
|
178
183
|
self.assertEqual(rc, 0)
|
|
179
|
-
self.assertEqual(err.splitlines()[-
|
|
184
|
+
self.assertEqual(err.splitlines()[-3:], [HINT, " uv tool upgrade agent-toggle",
|
|
185
|
+
" agent-toggle install-shims"])
|
|
180
186
|
self.assertNotIn("99.0.0", out)
|
|
181
187
|
|
|
182
188
|
def test_opt_out_by_env_and_by_setting(self) -> None:
|
|
@@ -228,10 +234,10 @@ class UpdateCheckTest(CliCase):
|
|
|
228
234
|
rc, out, err = self.run_cli("--version") # runs no other subprocess
|
|
229
235
|
self.assertEqual((rc, out.strip()), (0, f"agent-toggle {__version__}"))
|
|
230
236
|
run.assert_called_once_with(["uv", "tool", "upgrade", "agent-toggle"], check=False,
|
|
231
|
-
stdout=mock.ANY)
|
|
237
|
+
stdout=mock.ANY, stdin=subprocess.DEVNULL)
|
|
232
238
|
self.assertIn("did not finish", run.call_args.kwargs["stdout"].getvalue()) # = stderr
|
|
233
|
-
self.assertIn("upgrade did not finish — run it yourself: uv tool upgrade agent-toggle"
|
|
234
|
-
err)
|
|
239
|
+
self.assertIn("upgrade did not finish — run it yourself: uv tool upgrade agent-toggle"
|
|
240
|
+
"\n agent-toggle install-shims", err)
|
|
235
241
|
|
|
236
242
|
def test_tty_skip_version_and_ctrl_c(self) -> None:
|
|
237
243
|
update_prompt.is_interactive.return_value = True
|
|
@@ -250,7 +250,7 @@ class SpellingTest(CliCase):
|
|
|
250
250
|
["profile", "list"], ["profile", "diff", "work"],
|
|
251
251
|
["profile", "apply", "work", "--dry-run"],
|
|
252
252
|
["undo", "--dry-run"], ["enable", "--all", "--dry-run"],
|
|
253
|
-
["doctor"], ["doctor", "--harness", "claude"],
|
|
253
|
+
["doctor"], ["doctor", "--harness", "claude"], ["update"],
|
|
254
254
|
["disable", "skill", "demo-skill", "--project", str(proj), "--dry-run"],
|
|
255
255
|
)
|
|
256
256
|
for argv in cases:
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""The update prompt flushes typeahead around cbreak mode."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import io
|
|
5
|
+
import sys
|
|
6
|
+
import unittest
|
|
7
|
+
from unittest import mock
|
|
8
|
+
|
|
9
|
+
from agent_toggle import update_check, update_prompt
|
|
10
|
+
from agent_toggle.ui import theme
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ReleaseNotesTest(unittest.TestCase):
|
|
14
|
+
def test_a_v_prefixed_version_is_not_doubled_in_the_url(self) -> None:
|
|
15
|
+
for latest in ("10.0", "v10.0"):
|
|
16
|
+
found = update_check.UpdateAvailable(latest=latest, current="1.0")
|
|
17
|
+
text = "\n".join(update_prompt._lines(found, 1, False, theme.ASCII))
|
|
18
|
+
self.assertIn("/releases/tag/v10.0", text, latest)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UpgradeStepsTest(unittest.TestCase):
|
|
22
|
+
def steps(self, claude, plugins):
|
|
23
|
+
with mock.patch.object(update_prompt.plugin_cli, "claude_bin", return_value=claude), \
|
|
24
|
+
mock.patch.object(update_prompt.plugin_cli, "list_plugins",
|
|
25
|
+
return_value=[{"id": i} for i in plugins]):
|
|
26
|
+
return update_prompt.upgrade_steps()
|
|
27
|
+
|
|
28
|
+
def test_the_plugin_is_updated_only_when_it_is_installed(self) -> None:
|
|
29
|
+
cli, shims = update_prompt.UPGRADE, ["agent-toggle", "install-shims"]
|
|
30
|
+
self.assertEqual(self.steps("claude", ["agent-toggle@agent-toggle"]), [
|
|
31
|
+
cli, ["claude", "plugin", "marketplace", "update", "agent-toggle"],
|
|
32
|
+
["claude", "plugin", "update", "agent-toggle@agent-toggle"], shims])
|
|
33
|
+
self.assertEqual(self.steps("claude", ["other@x"]), [cli, shims])
|
|
34
|
+
self.assertEqual(self.steps(None, []), [cli, shims])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CmdUpdateTest(unittest.TestCase):
|
|
38
|
+
def run_update(self, latest):
|
|
39
|
+
out = mock.Mock()
|
|
40
|
+
found = update_check.UpdateAvailable("0.1", latest) if latest else None
|
|
41
|
+
with mock.patch.object(update_check, "check", return_value=found) as check, \
|
|
42
|
+
mock.patch.object(update_prompt, "upgrade_steps", return_value=[["a", "b"], ["c"]]), \
|
|
43
|
+
mock.patch.object(update_prompt, "cache_path", return_value="x"):
|
|
44
|
+
update_prompt.cmd_update(out)
|
|
45
|
+
return out, check
|
|
46
|
+
|
|
47
|
+
def test_a_newer_release_lists_every_command_and_ignores_cache_and_skip(self) -> None:
|
|
48
|
+
out, check = self.run_update("0.2")
|
|
49
|
+
row = out.row.call_args.kwargs
|
|
50
|
+
self.assertEqual((row["latest"], row["commands"]), ("0.2", ["a b", "c"]))
|
|
51
|
+
self.assertEqual((check.call_args.kwargs["ttl_seconds"], check.call_args.kwargs["ignore_skip"]), (0, True))
|
|
52
|
+
|
|
53
|
+
def test_nothing_newer_lists_no_command(self) -> None:
|
|
54
|
+
out, _ = self.run_update(None)
|
|
55
|
+
self.assertEqual(out.row.call_args.kwargs["commands"], [])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class FlushTest(unittest.TestCase):
|
|
59
|
+
def test_input_is_flushed_after_entering_and_after_leaving_cbreak(self) -> None:
|
|
60
|
+
termios = mock.MagicMock()
|
|
61
|
+
calls: list[str] = []
|
|
62
|
+
termios.tcgetattr.return_value = []
|
|
63
|
+
termios.tcflush.side_effect = lambda *_a: calls.append("flush")
|
|
64
|
+
termios.tcsetattr.side_effect = lambda *_a: calls.append("restore")
|
|
65
|
+
tty = mock.MagicMock()
|
|
66
|
+
tty.setcbreak.side_effect = lambda *_a: calls.append("cbreak")
|
|
67
|
+
stdin = mock.MagicMock()
|
|
68
|
+
stdin.fileno.return_value = 0
|
|
69
|
+
found = update_check.UpdateAvailable(current="1.0", latest="2.0")
|
|
70
|
+
with (mock.patch.dict(sys.modules, {"termios": termios, "tty": tty}),
|
|
71
|
+
mock.patch.object(sys, "stdin", stdin), mock.patch.object(sys, "stderr", io.StringIO()),
|
|
72
|
+
mock.patch.object(update_prompt, "_posix_keys", lambda _fd: iter(["quit"]))):
|
|
73
|
+
self.assertEqual(update_prompt.ask(found), update_check.SKIP)
|
|
74
|
+
self.assertEqual(calls, ["cbreak", "flush", "restore", "flush"])
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
unittest.main()
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"""The update prompt flushes typeahead around cbreak mode."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import io
|
|
5
|
-
import sys
|
|
6
|
-
import unittest
|
|
7
|
-
from unittest import mock
|
|
8
|
-
|
|
9
|
-
from agent_toggle import update_check, update_prompt
|
|
10
|
-
from agent_toggle.ui import theme
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class ReleaseNotesTest(unittest.TestCase):
|
|
14
|
-
def test_a_v_prefixed_version_is_not_doubled_in_the_url(self) -> None:
|
|
15
|
-
for latest in ("10.0", "v10.0"):
|
|
16
|
-
found = update_check.UpdateAvailable(latest=latest, current="1.0")
|
|
17
|
-
text = "\n".join(update_prompt._lines(found, 1, False, theme.ASCII))
|
|
18
|
-
self.assertIn("/releases/tag/v10.0", text, latest)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class FlushTest(unittest.TestCase):
|
|
22
|
-
def test_input_is_flushed_after_entering_and_after_leaving_cbreak(self) -> None:
|
|
23
|
-
termios = mock.MagicMock()
|
|
24
|
-
calls: list[str] = []
|
|
25
|
-
termios.tcgetattr.return_value = []
|
|
26
|
-
termios.tcflush.side_effect = lambda *_a: calls.append("flush")
|
|
27
|
-
termios.tcsetattr.side_effect = lambda *_a: calls.append("restore")
|
|
28
|
-
tty = mock.MagicMock()
|
|
29
|
-
tty.setcbreak.side_effect = lambda *_a: calls.append("cbreak")
|
|
30
|
-
stdin = mock.MagicMock()
|
|
31
|
-
stdin.fileno.return_value = 0
|
|
32
|
-
found = update_check.UpdateAvailable(current="1.0", latest="2.0")
|
|
33
|
-
with (mock.patch.dict(sys.modules, {"termios": termios, "tty": tty}),
|
|
34
|
-
mock.patch.object(sys, "stdin", stdin), mock.patch.object(sys, "stderr", io.StringIO()),
|
|
35
|
-
mock.patch.object(update_prompt, "_posix_keys", lambda _fd: iter(["quit"]))):
|
|
36
|
-
self.assertEqual(update_prompt.ask(found), update_check.SKIP)
|
|
37
|
-
self.assertEqual(calls, ["cbreak", "flush", "restore", "flush"])
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if __name__ == "__main__":
|
|
41
|
-
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|