agent-toggle 0.10.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.
Files changed (91) hide show
  1. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/PKG-INFO +11 -5
  2. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/README.md +10 -4
  3. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/__init__.py +1 -1
  4. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/cli.py +7 -2
  5. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/help.py +9 -1
  6. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/locales/zh_tw_cli.py +3 -0
  7. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/shims/claude.md.tmpl +2 -2
  8. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/shims/generic.md.tmpl +1 -1
  9. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/update_check.py +4 -2
  10. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/update_prompt.py +47 -8
  11. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle.egg-info/PKG-INFO +11 -5
  12. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_cli.py +5 -4
  13. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_cli_surface.py +1 -1
  14. agent_toggle-0.11.0/tests/test_update_prompt.py +78 -0
  15. agent_toggle-0.10.0/tests/test_update_prompt.py +0 -41
  16. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/LICENSE +0 -0
  17. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/__main__.py +0 -0
  18. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/backends/__init__.py +0 -0
  19. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/backends/flag_json.py +0 -0
  20. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/backends/mcp_json.py +0 -0
  21. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/backends/mcp_toml.py +0 -0
  22. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/backends/plugin_cli.py +0 -0
  23. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/companions.py +0 -0
  24. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/config.py +0 -0
  25. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/cost.py +0 -0
  26. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/doctor.py +0 -0
  27. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/fs.py +0 -0
  28. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/harnesses.py +0 -0
  29. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/i18n.py +0 -0
  30. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/locales/__init__.py +0 -0
  31. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/locales/zh_tw_common.py +0 -0
  32. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/locales/zh_tw_config.py +0 -0
  33. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/locales/zh_tw_picker.py +0 -0
  34. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/mechanisms.py +0 -0
  35. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/ops.py +0 -0
  36. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/output.py +0 -0
  37. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/profiles.py +0 -0
  38. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/settings.py +0 -0
  39. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/spinner.py +0 -0
  40. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/store.py +0 -0
  41. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/toml_check.py +0 -0
  42. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/ui/__init__.py +0 -0
  43. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/ui/config_menu.py +0 -0
  44. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/ui/logo.py +0 -0
  45. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/ui/menu.py +0 -0
  46. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/ui/model.py +0 -0
  47. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/ui/picker.py +0 -0
  48. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/ui/theme.py +0 -0
  49. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle/undo.py +0 -0
  50. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle.egg-info/SOURCES.txt +0 -0
  51. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle.egg-info/dependency_links.txt +0 -0
  52. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle.egg-info/entry_points.txt +0 -0
  53. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle.egg-info/requires.txt +0 -0
  54. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/agent_toggle.egg-info/top_level.txt +0 -0
  55. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/pyproject.toml +0 -0
  56. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/setup.cfg +0 -0
  57. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_alias.py +0 -0
  58. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_all_type.py +0 -0
  59. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_backends.py +0 -0
  60. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_color.py +0 -0
  61. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_companions.py +0 -0
  62. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_completions.py +0 -0
  63. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_config.py +0 -0
  64. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_config_menu.py +0 -0
  65. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_conformance.py +0 -0
  66. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_containment.py +0 -0
  67. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_cost.py +0 -0
  68. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_crash.py +0 -0
  69. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_doctor.py +0 -0
  70. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_encoding.py +0 -0
  71. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_fs.py +0 -0
  72. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_harnesses.py +0 -0
  73. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_help.py +0 -0
  74. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_i18n.py +0 -0
  75. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_install.py +0 -0
  76. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_logo.py +0 -0
  77. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_mechanisms.py +0 -0
  78. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_menu.py +0 -0
  79. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_mods.py +0 -0
  80. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_picker.py +0 -0
  81. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_platform.py +0 -0
  82. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_profiles.py +0 -0
  83. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_project.py +0 -0
  84. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_safety.py +0 -0
  85. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_same_name.py +0 -0
  86. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_secret_audit.py +0 -0
  87. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_settings.py +0 -0
  88. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_store.py +0 -0
  89. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_theme.py +0 -0
  90. {agent_toggle-0.10.0 → agent_toggle-0.11.0}/tests/test_undo.py +0 -0
  91. {agent_toggle-0.10.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.10.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
@@ -195,6 +195,7 @@ agent-toggle <command> [args] # or: python3 agent_toggle.py <command> [
195
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`) |
196
196
  | `list [type]` | what is currently disabled (`--project <dir>` filters to one project) |
197
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 |
198
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)) |
199
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`) |
200
201
  | `enable <type> <name>...` | put them back (`--dry-run` shows the plan; `--project <dir>` likewise) |
@@ -472,10 +473,15 @@ before the command and is offered after it, on every exit path; it overlaps the
472
473
 
473
474
  - **On a terminal** (stdin and stderr are TTYs, no `--json`): a panel on stderr, "agent-toggle X
474
475
  is available (you have Y)", with `Update now`, `Skip` and `Skip until next version`, a release
475
- notes link, and `↑` `↓` `Enter` `q`. `Update now` runs `uv tool upgrade agent-toggle`; if that
476
- fails you get a yellow warning with the command to run yourself.
477
- - **Off a terminal**: two stderr lines, `agent-toggle X is available (you have Y)` and
478
- ` uv tool upgrade agent-toggle`.
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.
479
485
  - It never changes the exit code or stdout, so `--json` output stays one document.
480
486
 
481
487
  It is one `GET https://pypi.org/pypi/agent-toggle/json` on a background thread with a 0.8 s
@@ -163,6 +163,7 @@ agent-toggle <command> [args] # or: python3 agent_toggle.py <command> [
163
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`) |
164
164
  | `list [type]` | what is currently disabled (`--project <dir>` filters to one project) |
165
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 |
166
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)) |
167
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`) |
168
169
  | `enable <type> <name>...` | put them back (`--dry-run` shows the plan; `--project <dir>` likewise) |
@@ -440,10 +441,15 @@ before the command and is offered after it, on every exit path; it overlaps the
440
441
 
441
442
  - **On a terminal** (stdin and stderr are TTYs, no `--json`): a panel on stderr, "agent-toggle X
442
443
  is available (you have Y)", with `Update now`, `Skip` and `Skip until next version`, a release
443
- notes link, and `↑` `↓` `Enter` `q`. `Update now` runs `uv tool upgrade agent-toggle`; if that
444
- fails you get a yellow warning with the command to run yourself.
445
- - **Off a terminal**: two stderr lines, `agent-toggle X is available (you have Y)` and
446
- ` uv tool upgrade agent-toggle`.
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.
447
453
  - It never changes the exit code or stdout, so `--json` output stays one document.
448
454
 
449
455
  It is one `GET https://pypi.org/pypi/agent-toggle/json` on a background thread with a 0.8 s
@@ -1,3 +1,3 @@
1
1
  """agent-toggle: temporarily disable / re-enable AI-agent resources."""
2
2
 
3
- __version__ = "0.10.0"
3
+ __version__ = "0.11.0"
@@ -558,7 +558,7 @@ def _stale_ignore_lines(h, args: argparse.Namespace, out: Result) -> None:
558
558
 
559
559
 
560
560
  COMMANDS = ("ui", "pick", "status", "list", "cost", "migrate", "disable", "enable",
561
- "install-shims", "profile", "undo", "doctor", "config")
561
+ "install-shims", "profile", "undo", "doctor", "config", "update")
562
562
  _GLOBAL_FLAGS = ("--json", "-v", "--verbose")
563
563
 
564
564
 
@@ -693,6 +693,8 @@ def build_parser() -> argparse.ArgumentParser:
693
693
  pp.add_argument("--project", metavar="dir", help=PROJECT_HELP)
694
694
  sub.add_parser("doctor", parents=[common],
695
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")
696
698
  cf = sub.add_parser("config", parents=[common],
697
699
  help="Telegram settings for the CI failure alerts (needs the telegram extra)")
698
700
  cf.add_argument("action", nargs="?", choices=("test", "sync-ci"),
@@ -727,7 +729,8 @@ def main(argv: list[str] | None = None) -> int:
727
729
  language, color = "en", "auto"
728
730
  i18n.set_language(language)
729
731
  color = scan_color(argv, color) # --color > env > setting > auto
730
- started = update_prompt.start()
732
+ i = _command_index(argv)
733
+ started = None if argv[i:i + 1] == ["update"] else update_prompt.start() # `update` is the check
731
734
  try:
732
735
  return _main(argv, color)
733
736
  except KeyboardInterrupt:
@@ -795,6 +798,8 @@ def _main(argv: list[str], color: str) -> int:
795
798
  fixes = doctor.cmd_doctor(args.harness, out)
796
799
  if fixes and _interactive(out):
797
800
  doctor.offer_fixes(fixes, out)
801
+ elif cmd == "update":
802
+ update_prompt.cmd_update(out)
798
803
  elif cmd == "config":
799
804
  config.cmd_config(args, out)
800
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,7 +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 | open | close]"
4
+ argument-hint: "[status | list | cost | disable|enable <type> <name>... | undo | profile | doctor | update | open | close]"
5
5
  ---
6
6
  <!-- agent-toggle shim: written by `agent-toggle install-shims`; re-running it overwrites this file -->
7
7
 
@@ -17,7 +17,7 @@ agent-toggle <command> --json
17
17
  ```
18
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.
19
19
 
20
- 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]`,
21
21
  `cost [--type T]` (startup token estimates, biggest first), `disable <type> <name>...` (type `all`: every type with that name),
22
22
  `enable <type> <name>...`, `enable --all`, `undo` (reverse the last batch),
23
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` /
@@ -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
- UPGRADE = ["uv", "tool", "upgrade", DIST]
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): two plain lines, never a block."""
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
- print(" " + " ".join(UPGRADE), file=sys.stderr)
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
- if is_interactive() and not json_mode:
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): # uv's output must never reach stdout
191
- return subprocess.run(cmd, stdout=sys.stderr, **kw)
192
- answer = update_check.offer(started, asker, cache_path=cache_path(), upgrade=UPGRADE, run=run)
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(UPGRADE)), file=sys.stderr)
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.10.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
@@ -195,6 +195,7 @@ agent-toggle <command> [args] # or: python3 agent_toggle.py <command> [
195
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`) |
196
196
  | `list [type]` | what is currently disabled (`--project <dir>` filters to one project) |
197
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 |
198
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)) |
199
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`) |
200
201
  | `enable <type> <name>...` | put them back (`--dry-run` shows the plan; `--project <dir>` likewise) |
@@ -472,10 +473,15 @@ before the command and is offered after it, on every exit path; it overlaps the
472
473
 
473
474
  - **On a terminal** (stdin and stderr are TTYs, no `--json`): a panel on stderr, "agent-toggle X
474
475
  is available (you have Y)", with `Update now`, `Skip` and `Skip until next version`, a release
475
- notes link, and `↑` `↓` `Enter` `q`. `Update now` runs `uv tool upgrade agent-toggle`; if that
476
- fails you get a yellow warning with the command to run yourself.
477
- - **Off a terminal**: two stderr lines, `agent-toggle X is available (you have Y)` and
478
- ` uv tool upgrade agent-toggle`.
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.
479
485
  - It never changes the exit code or stdout, so `--json` output stays one document.
480
486
 
481
487
  It is one `GET https://pypi.org/pypi/agent-toggle/json` on a background thread with a 0.8 s
@@ -181,7 +181,8 @@ class UpdateCheckTest(CliCase):
181
181
  def test_hint_on_stderr_off_a_tty(self) -> None:
182
182
  rc, out, err = self.run_cli("status")
183
183
  self.assertEqual(rc, 0)
184
- self.assertEqual(err.splitlines()[-2:], [HINT, " uv tool upgrade agent-toggle"])
184
+ self.assertEqual(err.splitlines()[-3:], [HINT, " uv tool upgrade agent-toggle",
185
+ " agent-toggle install-shims"])
185
186
  self.assertNotIn("99.0.0", out)
186
187
 
187
188
  def test_opt_out_by_env_and_by_setting(self) -> None:
@@ -233,10 +234,10 @@ class UpdateCheckTest(CliCase):
233
234
  rc, out, err = self.run_cli("--version") # runs no other subprocess
234
235
  self.assertEqual((rc, out.strip()), (0, f"agent-toggle {__version__}"))
235
236
  run.assert_called_once_with(["uv", "tool", "upgrade", "agent-toggle"], check=False,
236
- stdout=mock.ANY)
237
+ stdout=mock.ANY, stdin=subprocess.DEVNULL)
237
238
  self.assertIn("did not finish", run.call_args.kwargs["stdout"].getvalue()) # = stderr
238
- self.assertIn("upgrade did not finish — run it yourself: uv tool upgrade agent-toggle",
239
- err)
239
+ self.assertIn("upgrade did not finish — run it yourself: uv tool upgrade agent-toggle"
240
+ "\n agent-toggle install-shims", err)
240
241
 
241
242
  def test_tty_skip_version_and_ctrl_c(self) -> None:
242
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