agent-toggle 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/PKG-INFO +117 -14
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/README.md +114 -13
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/__init__.py +1 -1
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/cli.py +68 -17
- agent_toggle-0.2.0/agent_toggle/config.py +143 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/doctor.py +58 -7
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/fs.py +21 -2
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/harnesses.py +13 -7
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/mechanisms.py +41 -3
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/ops.py +5 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/shims/claude.md.tmpl +26 -4
- agent_toggle-0.2.0/agent_toggle/shims/generic.md.tmpl +68 -0
- agent_toggle-0.2.0/agent_toggle/ui/menu.py +93 -0
- agent_toggle-0.2.0/agent_toggle/ui/model.py +72 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/ui/picker.py +1 -67
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle.egg-info/PKG-INFO +117 -14
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle.egg-info/SOURCES.txt +9 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle.egg-info/requires.txt +3 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/pyproject.toml +1 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_alias.py +3 -2
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_cli.py +10 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_cli_surface.py +24 -0
- agent_toggle-0.2.0/tests/test_completions.py +37 -0
- agent_toggle-0.2.0/tests/test_config.py +141 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_containment.py +2 -3
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_doctor.py +42 -2
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_fs.py +5 -5
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_harnesses.py +11 -4
- agent_toggle-0.2.0/tests/test_install.py +173 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_mechanisms.py +2 -3
- agent_toggle-0.2.0/tests/test_menu.py +86 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_picker.py +26 -26
- agent_toggle-0.2.0/tests/test_platform.py +107 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_project.py +3 -2
- agent_toggle-0.2.0/tests/test_secret_audit.py +253 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_store.py +2 -2
- agent_toggle-0.1.0/tests/test_install.py +0 -98
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/LICENSE +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/__main__.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/backends/__init__.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/backends/flag_json.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/backends/mcp_json.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/backends/mcp_toml.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/backends/plugin_cli.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/companions.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/cost.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/output.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/profiles.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/store.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/ui/__init__.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle/undo.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle.egg-info/dependency_links.txt +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle.egg-info/entry_points.txt +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/agent_toggle.egg-info/top_level.txt +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/setup.cfg +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_backends.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_color.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_companions.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_conformance.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_cost.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_encoding.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_profiles.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_safety.py +0 -0
- {agent_toggle-0.1.0 → agent_toggle-0.2.0}/tests/test_undo.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-toggle
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -26,6 +26,8 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
License-File: LICENSE
|
|
27
27
|
Provides-Extra: windows
|
|
28
28
|
Requires-Dist: windows-curses; sys_platform == "win32" and extra == "windows"
|
|
29
|
+
Provides-Extra: telegram
|
|
30
|
+
Requires-Dist: telegram-kit<0.3,>=0.2.0; extra == "telegram"
|
|
29
31
|
Dynamic: license-file
|
|
30
32
|
|
|
31
33
|
# agent-toggle
|
|
@@ -50,30 +52,31 @@ puts it back where it came from.
|
|
|
50
52
|
|
|
51
53
|
## Install
|
|
52
54
|
|
|
53
|
-
Latest release
|
|
55
|
+
Latest release, from [PyPI](https://pypi.org/project/agent-toggle/):
|
|
54
56
|
|
|
55
57
|
```sh
|
|
56
58
|
uv tool install agent-toggle
|
|
57
59
|
agent-toggle --version
|
|
58
60
|
```
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
Straight from this repository, pinned to a release tag (`v0.2.0` is the current one;
|
|
63
|
+
an unpinned `git+` URL installs whatever is on the default branch):
|
|
61
64
|
|
|
62
65
|
```sh
|
|
63
|
-
uv tool install git
|
|
66
|
+
uv tool install git+https://github.com/weskao/agent-toggle@v0.2.0
|
|
64
67
|
agent-toggle --version
|
|
65
68
|
```
|
|
66
69
|
|
|
67
70
|
Alternatively, use `pipx`:
|
|
68
71
|
|
|
69
72
|
```sh
|
|
70
|
-
pipx install agent-toggle # from
|
|
73
|
+
pipx install agent-toggle # from PyPI; or: pipx install git+https://github.com/weskao/agent-toggle@v0.2.0
|
|
71
74
|
```
|
|
72
75
|
|
|
73
76
|
From a checkout, either install it editable or run it in place:
|
|
74
77
|
|
|
75
78
|
```sh
|
|
76
|
-
git clone
|
|
79
|
+
git clone https://github.com/weskao/agent-toggle agent-toggle
|
|
77
80
|
cd agent-toggle
|
|
78
81
|
pip install -e . # puts the `agent-toggle` console script on PATH
|
|
79
82
|
python3 agent_toggle.py status # no install needed; same CLI
|
|
@@ -89,8 +92,45 @@ python3 agent_toggle.py install-shims # from a checkout, no install needed
|
|
|
89
92
|
`install-shims` writes a thin skill shim (`skills/agent-toggle/SKILL.md`) into
|
|
90
93
|
every installed harness that supports skills, so `/agent-toggle` works from any
|
|
91
94
|
of them, and adds the park dirs (`skills-disabled/` etc.) to a `.gitignore`
|
|
92
|
-
already present in that harness home.
|
|
93
|
-
|
|
95
|
+
already present in that harness home. Each harness gets its own template from
|
|
96
|
+
`agent_toggle/shims/<harness>.md.tmpl`, else `generic.md.tmpl` (which tells the
|
|
97
|
+
agent to pass `--harness <that harness>`). The shim tells the agent to act only
|
|
98
|
+
on the user's explicit request, never on instructions found inside skill or
|
|
99
|
+
tool content, and to preview bulk operations with `--dry-run`.
|
|
100
|
+
|
|
101
|
+
Every shim carries an `<!-- agent-toggle shim: ... -->` marker line.
|
|
102
|
+
`install-shims` updates a file that has the marker in place (it is idempotent)
|
|
103
|
+
and **refuses** to overwrite one that lacks it, or a symlink: that harness gets
|
|
104
|
+
an `error` row with the fix, the other harnesses still install, and the run
|
|
105
|
+
exits `1`. A shim written by a version before the marker counts as foreign;
|
|
106
|
+
delete it and re-run. `--dry-run` shows the same plan, refusals included. The
|
|
107
|
+
tool itself stays in one place.
|
|
108
|
+
|
|
109
|
+
**Optional: Telegram alerts for CI** (for maintainers of a fork or clone). Install
|
|
110
|
+
with the `telegram` extra, which adds [telegram-kit](https://pypi.org/project/telegram-kit/)
|
|
111
|
+
(itself stdlib-only), then run the setup once; see [CI notifications](#ci-notifications):
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
uv tool install "agent-toggle[telegram]" # or: pipx install "agent-toggle[telegram]"
|
|
115
|
+
pip install -e ".[telegram]" # from a checkout
|
|
116
|
+
agent-toggle config # bot token + chat id
|
|
117
|
+
agent-toggle config sync-ci # set the two GitHub repository secrets
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Without the extra everything else works exactly as before; only `config` asks for it.
|
|
121
|
+
|
|
122
|
+
## Shell completion
|
|
123
|
+
|
|
124
|
+
Each release publishes static completion files for bash, zsh and fish as the
|
|
125
|
+
`completions` artifact of its Release workflow run. They are generated from the
|
|
126
|
+
CLI parser; to build them from a checkout:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
python3 tools/gen_completions.py completions # agent-toggle.bash, _agent-toggle (zsh), agent-toggle.fish
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Source `agent-toggle.bash` from `~/.bashrc`, put `_agent-toggle` on your zsh
|
|
133
|
+
`fpath`, or copy `agent-toggle.fish` to `~/.config/fish/completions/`.
|
|
94
134
|
|
|
95
135
|
## Usage
|
|
96
136
|
|
|
@@ -101,20 +141,23 @@ agent-toggle <command> [args] # or: python3 agent_toggle.py <command> [
|
|
|
101
141
|
| command | what it does |
|
|
102
142
|
|---|---|
|
|
103
143
|
| `ui` | interactive picker — cost column, sort, filters; `--dry-run` shows the plan for what you stage and changes nothing |
|
|
104
|
-
| `status` | health check: harnesses found, types each supports, parked counts, gitignore, untracked parked items, stale live twins, shared dirs |
|
|
144
|
+
| `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`) |
|
|
105
145
|
| `list [type]` | what is currently disabled (`--project <dir>` filters to one project) |
|
|
106
146
|
| `cost [--type T]` | estimated startup tokens per item, biggest first (read-only; `--harness H` filters) |
|
|
107
|
-
| `install-shims` | write the skill shim into every installed harness (`--dry-run` shows the plan) |
|
|
147
|
+
| `install-shims` | write the skill shim into every installed harness; refuses to overwrite a file it did not write (`--dry-run` shows the plan) |
|
|
108
148
|
| `disable <type> <name>...` | park one or more items (`--dry-run` shows the plan; `--project <dir>` for a repo's own `.claude/` and `.mcp.json`) |
|
|
109
149
|
| `enable <type> <name>...` | put them back (`--dry-run` shows the plan; `--project <dir>` likewise) |
|
|
110
150
|
| `enable --all` | put back **every** disabled item (`--harness H` narrows it, `--project <dir>` takes only that project's) |
|
|
111
151
|
| `undo` | reverse the last logged batch (`--dry-run` shows the plan) |
|
|
112
152
|
| `profile save\|apply\|diff\|list` | named sets of live items; see [Profiles](#profiles) |
|
|
153
|
+
| `config [test\|sync-ci]` | Telegram settings for the CI failure alerts; see [CI notifications](#ci-notifications) |
|
|
113
154
|
| `doctor` | read-only check of each harness layout and of `state.json` against disk; exit `1` only on an `error` row |
|
|
114
155
|
| `migrate` | import an older `~/.claude-toggle/` state |
|
|
115
156
|
|
|
116
157
|
`<type>` = `skill` / `agent` / `command` / `rule` / `plugin` / `mcp`.
|
|
117
158
|
`rule` is claude-only (`~/.claude/rules/*.md`, parked in `rules-disabled/`).
|
|
159
|
+
Disabling a rule prints a warning (also in `--json` `warnings`, dry run included)
|
|
160
|
+
that rules may carry safety constraints.
|
|
118
161
|
|
|
119
162
|
Flags accepted by every command, before or after the subcommand:
|
|
120
163
|
|
|
@@ -228,6 +271,10 @@ instead of fighting curses for the terminal.
|
|
|
228
271
|
|
|
229
272
|
Built on stdlib `curses`, so there is nothing to install on macOS and Linux
|
|
230
273
|
(on Windows, `pip install "agent-toggle[windows]"` pulls `windows-curses`).
|
|
274
|
+
Without curses, `ui` falls back to a numbered menu with the same staging and the
|
|
275
|
+
same result: type row numbers (`1 3 5-7`) to tick or untick, `/text` to filter,
|
|
276
|
+
`s` / `h` / `t` to sort and cycle the harness and type filters, `a` to apply, `q`
|
|
277
|
+
(or end of input) to cancel.
|
|
231
278
|
|
|
232
279
|
## What each harness supports
|
|
233
280
|
|
|
@@ -243,6 +290,22 @@ Built on stdlib `curses`, so there is nothing to install on macOS and Linux
|
|
|
243
290
|
| devin | `~/.devin` | — | — | — | — | — | — |
|
|
244
291
|
| agy | `~/.antigravity` | — | — | — | — | — | — |
|
|
245
292
|
|
|
293
|
+
Per-harness locations, mechanisms and the harness versions this was checked
|
|
294
|
+
against are in [docs/harnesses.md](docs/harnesses.md).
|
|
295
|
+
|
|
296
|
+
**Homes per OS.** Every `~` above is Python's `Path.home()`: `$HOME` on macOS and
|
|
297
|
+
Linux, `%USERPROFILE%` on Windows (so `~/.claude` is `C:\Users\<you>\.claude`).
|
|
298
|
+
opencode honours an absolute `$XDG_CONFIG_HOME` on every OS and otherwise uses
|
|
299
|
+
`~/.config/opencode`, Windows included; the tool does not look in `%APPDATA%`. This
|
|
300
|
+
tool's own state lives in `~/.agent-toggle/`. The Linux and Windows locations
|
|
301
|
+
follow the harnesses' documentation and have not yet been checked against real
|
|
302
|
+
installs there; if a harness keeps its files elsewhere, `status` shows it as not
|
|
303
|
+
installed.
|
|
304
|
+
|
|
305
|
+
Plugin ids are passed to `claude plugin ...`, which on Windows runs through
|
|
306
|
+
`cmd.exe`. An id with anything outside `A-Z a-z 0-9 . _ @ : / -` (for example `&`,
|
|
307
|
+
`%`, `|` or a space) is refused on every OS before any command runs.
|
|
308
|
+
|
|
246
309
|
Copilot's `config.json` is machine-managed and never edited by this tool; Copilot
|
|
247
310
|
plugins are not yet supported. `claude` plugins are toggled through `claude
|
|
248
311
|
plugin enable/disable`; `openclaw` plugins through a config flag (see [Assumed
|
|
@@ -461,10 +524,16 @@ Only `error` makes the exit code `1`; each problem row names the command that
|
|
|
461
524
|
fixes it. `--harness X` for a harness that is not installed is a `note` (exit
|
|
462
525
|
`0`). Companion files are not checked.
|
|
463
526
|
|
|
527
|
+
A harness item parked with no state entry also carries `orphan`, and its fix
|
|
528
|
+
follows from it: `identical` (the live copy has the same content: delete the
|
|
529
|
+
parked copy), `differs` (a different live copy exists: compare, keep one), or
|
|
530
|
+
`parked-only` (move it back, then `disable` it so the state records it).
|
|
531
|
+
|
|
464
532
|
## Assumed formats
|
|
465
533
|
|
|
466
|
-
Two config shapes
|
|
467
|
-
were
|
|
534
|
+
Two config shapes came from the design survey (`docs/DESIGN.md` §4 / §11). Both
|
|
535
|
+
were since seen on one real install each (openclaw 2026.7.1-2, opencode 2.0.22; see
|
|
536
|
+
`docs/harnesses.md`), but the tool still treats them defensively:
|
|
468
537
|
|
|
469
538
|
- `openclaw.json`: `skills.entries.<name>.enabled` and
|
|
470
539
|
`plugins.entries.<name>.enabled`
|
|
@@ -554,13 +623,47 @@ ruff check .
|
|
|
554
623
|
stdlib `unittest`, no fixtures, no network. Every test runs against a
|
|
555
624
|
throwaway temp `HOME` and a stubbed `claude` CLI.
|
|
556
625
|
|
|
626
|
+
## CI notifications
|
|
627
|
+
|
|
628
|
+
`.github/workflows/ci.yml` can send a Telegram message when the test matrix fails on a
|
|
629
|
+
push (a lint-only failure does not page). Alerts never fire for pull requests or green
|
|
630
|
+
runs. The message names the repository, branch, 7-character commit and a link to the run.
|
|
631
|
+
|
|
632
|
+
Set it up once, with the `telegram` extra installed (see [Install](#install)):
|
|
633
|
+
|
|
634
|
+
```sh
|
|
635
|
+
agent-toggle config # prompts for the bot token (hidden) and the chat id
|
|
636
|
+
agent-toggle config test # send one test message
|
|
637
|
+
agent-toggle config sync-ci # set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID on the repo
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
`config` works like aicp's `--config`: Enter keeps the current value, `-` clears it.
|
|
641
|
+
The bot token is kept only in the OS credential store (macOS Keychain, Linux Secret
|
|
642
|
+
Service, Windows DPAPI) through telegram-kit, never in a file or on a command line, and
|
|
643
|
+
is shown masked. With no credential store it refuses to store the token; set
|
|
644
|
+
`TG_BOT_TOKEN` in the environment instead (`TG_CHAT_ID` likewise for the chat id). The
|
|
645
|
+
chat id is ordinary configuration in `~/.agent-toggle/config.json`: a number, `-100...`
|
|
646
|
+
for a group, or an `@channel`. `config sync-ci` needs the GitHub CLI (`gh`) signed in; it
|
|
647
|
+
passes both values on stdin, and `--repo OWNER/REPO` / `--dry-run` work as elsewhere.
|
|
648
|
+
|
|
649
|
+
To skip the tool, set the secrets by hand (each command prompts for the value):
|
|
650
|
+
|
|
651
|
+
```sh
|
|
652
|
+
gh secret set TELEGRAM_BOT_TOKEN
|
|
653
|
+
gh secret set TELEGRAM_CHAT_ID
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
With either secret unset (a fork, or a clone you have not configured) the notify job
|
|
657
|
+
prints a `::notice::` and exits 0, so it never adds a second red X.
|
|
658
|
+
|
|
557
659
|
## Releasing
|
|
558
660
|
|
|
559
661
|
A release is a git tag `vX.Y.Z` (where `X.Y.Z` must match `agent_toggle.__version__`).
|
|
560
662
|
Pushing the tag triggers `.github/workflows/release.yml`:
|
|
561
663
|
|
|
562
|
-
1. **Build** job checks that the tag matches `__version__
|
|
563
|
-
source distribution
|
|
664
|
+
1. **Build** job checks that the tag matches `__version__`, builds a wheel and
|
|
665
|
+
source distribution, and generates the shell completion files (uploaded as the
|
|
666
|
+
`completions` artifact; the workflow does not create a GitHub Release).
|
|
564
667
|
2. **Publish** job uploads to PyPI using
|
|
565
668
|
[trusted publishing](https://docs.pypi.org/trusted-publishers/),
|
|
566
669
|
with no stored tokens—only the `pypi` environment and OIDC setup.
|
|
@@ -20,30 +20,31 @@ puts it back where it came from.
|
|
|
20
20
|
|
|
21
21
|
## Install
|
|
22
22
|
|
|
23
|
-
Latest release
|
|
23
|
+
Latest release, from [PyPI](https://pypi.org/project/agent-toggle/):
|
|
24
24
|
|
|
25
25
|
```sh
|
|
26
26
|
uv tool install agent-toggle
|
|
27
27
|
agent-toggle --version
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Straight from this repository, pinned to a release tag (`v0.2.0` is the current one;
|
|
31
|
+
an unpinned `git+` URL installs whatever is on the default branch):
|
|
31
32
|
|
|
32
33
|
```sh
|
|
33
|
-
uv tool install git
|
|
34
|
+
uv tool install git+https://github.com/weskao/agent-toggle@v0.2.0
|
|
34
35
|
agent-toggle --version
|
|
35
36
|
```
|
|
36
37
|
|
|
37
38
|
Alternatively, use `pipx`:
|
|
38
39
|
|
|
39
40
|
```sh
|
|
40
|
-
pipx install agent-toggle # from
|
|
41
|
+
pipx install agent-toggle # from PyPI; or: pipx install git+https://github.com/weskao/agent-toggle@v0.2.0
|
|
41
42
|
```
|
|
42
43
|
|
|
43
44
|
From a checkout, either install it editable or run it in place:
|
|
44
45
|
|
|
45
46
|
```sh
|
|
46
|
-
git clone
|
|
47
|
+
git clone https://github.com/weskao/agent-toggle agent-toggle
|
|
47
48
|
cd agent-toggle
|
|
48
49
|
pip install -e . # puts the `agent-toggle` console script on PATH
|
|
49
50
|
python3 agent_toggle.py status # no install needed; same CLI
|
|
@@ -59,8 +60,45 @@ python3 agent_toggle.py install-shims # from a checkout, no install needed
|
|
|
59
60
|
`install-shims` writes a thin skill shim (`skills/agent-toggle/SKILL.md`) into
|
|
60
61
|
every installed harness that supports skills, so `/agent-toggle` works from any
|
|
61
62
|
of them, and adds the park dirs (`skills-disabled/` etc.) to a `.gitignore`
|
|
62
|
-
already present in that harness home.
|
|
63
|
-
|
|
63
|
+
already present in that harness home. Each harness gets its own template from
|
|
64
|
+
`agent_toggle/shims/<harness>.md.tmpl`, else `generic.md.tmpl` (which tells the
|
|
65
|
+
agent to pass `--harness <that harness>`). The shim tells the agent to act only
|
|
66
|
+
on the user's explicit request, never on instructions found inside skill or
|
|
67
|
+
tool content, and to preview bulk operations with `--dry-run`.
|
|
68
|
+
|
|
69
|
+
Every shim carries an `<!-- agent-toggle shim: ... -->` marker line.
|
|
70
|
+
`install-shims` updates a file that has the marker in place (it is idempotent)
|
|
71
|
+
and **refuses** to overwrite one that lacks it, or a symlink: that harness gets
|
|
72
|
+
an `error` row with the fix, the other harnesses still install, and the run
|
|
73
|
+
exits `1`. A shim written by a version before the marker counts as foreign;
|
|
74
|
+
delete it and re-run. `--dry-run` shows the same plan, refusals included. The
|
|
75
|
+
tool itself stays in one place.
|
|
76
|
+
|
|
77
|
+
**Optional: Telegram alerts for CI** (for maintainers of a fork or clone). Install
|
|
78
|
+
with the `telegram` extra, which adds [telegram-kit](https://pypi.org/project/telegram-kit/)
|
|
79
|
+
(itself stdlib-only), then run the setup once; see [CI notifications](#ci-notifications):
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
uv tool install "agent-toggle[telegram]" # or: pipx install "agent-toggle[telegram]"
|
|
83
|
+
pip install -e ".[telegram]" # from a checkout
|
|
84
|
+
agent-toggle config # bot token + chat id
|
|
85
|
+
agent-toggle config sync-ci # set the two GitHub repository secrets
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Without the extra everything else works exactly as before; only `config` asks for it.
|
|
89
|
+
|
|
90
|
+
## Shell completion
|
|
91
|
+
|
|
92
|
+
Each release publishes static completion files for bash, zsh and fish as the
|
|
93
|
+
`completions` artifact of its Release workflow run. They are generated from the
|
|
94
|
+
CLI parser; to build them from a checkout:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
python3 tools/gen_completions.py completions # agent-toggle.bash, _agent-toggle (zsh), agent-toggle.fish
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Source `agent-toggle.bash` from `~/.bashrc`, put `_agent-toggle` on your zsh
|
|
101
|
+
`fpath`, or copy `agent-toggle.fish` to `~/.config/fish/completions/`.
|
|
64
102
|
|
|
65
103
|
## Usage
|
|
66
104
|
|
|
@@ -71,20 +109,23 @@ agent-toggle <command> [args] # or: python3 agent_toggle.py <command> [
|
|
|
71
109
|
| command | what it does |
|
|
72
110
|
|---|---|
|
|
73
111
|
| `ui` | interactive picker — cost column, sort, filters; `--dry-run` shows the plan for what you stage and changes nothing |
|
|
74
|
-
| `status` | health check: harnesses found, types each supports, parked counts, gitignore, untracked parked items, stale live twins, shared dirs |
|
|
112
|
+
| `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`) |
|
|
75
113
|
| `list [type]` | what is currently disabled (`--project <dir>` filters to one project) |
|
|
76
114
|
| `cost [--type T]` | estimated startup tokens per item, biggest first (read-only; `--harness H` filters) |
|
|
77
|
-
| `install-shims` | write the skill shim into every installed harness (`--dry-run` shows the plan) |
|
|
115
|
+
| `install-shims` | write the skill shim into every installed harness; refuses to overwrite a file it did not write (`--dry-run` shows the plan) |
|
|
78
116
|
| `disable <type> <name>...` | park one or more items (`--dry-run` shows the plan; `--project <dir>` for a repo's own `.claude/` and `.mcp.json`) |
|
|
79
117
|
| `enable <type> <name>...` | put them back (`--dry-run` shows the plan; `--project <dir>` likewise) |
|
|
80
118
|
| `enable --all` | put back **every** disabled item (`--harness H` narrows it, `--project <dir>` takes only that project's) |
|
|
81
119
|
| `undo` | reverse the last logged batch (`--dry-run` shows the plan) |
|
|
82
120
|
| `profile save\|apply\|diff\|list` | named sets of live items; see [Profiles](#profiles) |
|
|
121
|
+
| `config [test\|sync-ci]` | Telegram settings for the CI failure alerts; see [CI notifications](#ci-notifications) |
|
|
83
122
|
| `doctor` | read-only check of each harness layout and of `state.json` against disk; exit `1` only on an `error` row |
|
|
84
123
|
| `migrate` | import an older `~/.claude-toggle/` state |
|
|
85
124
|
|
|
86
125
|
`<type>` = `skill` / `agent` / `command` / `rule` / `plugin` / `mcp`.
|
|
87
126
|
`rule` is claude-only (`~/.claude/rules/*.md`, parked in `rules-disabled/`).
|
|
127
|
+
Disabling a rule prints a warning (also in `--json` `warnings`, dry run included)
|
|
128
|
+
that rules may carry safety constraints.
|
|
88
129
|
|
|
89
130
|
Flags accepted by every command, before or after the subcommand:
|
|
90
131
|
|
|
@@ -198,6 +239,10 @@ instead of fighting curses for the terminal.
|
|
|
198
239
|
|
|
199
240
|
Built on stdlib `curses`, so there is nothing to install on macOS and Linux
|
|
200
241
|
(on Windows, `pip install "agent-toggle[windows]"` pulls `windows-curses`).
|
|
242
|
+
Without curses, `ui` falls back to a numbered menu with the same staging and the
|
|
243
|
+
same result: type row numbers (`1 3 5-7`) to tick or untick, `/text` to filter,
|
|
244
|
+
`s` / `h` / `t` to sort and cycle the harness and type filters, `a` to apply, `q`
|
|
245
|
+
(or end of input) to cancel.
|
|
201
246
|
|
|
202
247
|
## What each harness supports
|
|
203
248
|
|
|
@@ -213,6 +258,22 @@ Built on stdlib `curses`, so there is nothing to install on macOS and Linux
|
|
|
213
258
|
| devin | `~/.devin` | — | — | — | — | — | — |
|
|
214
259
|
| agy | `~/.antigravity` | — | — | — | — | — | — |
|
|
215
260
|
|
|
261
|
+
Per-harness locations, mechanisms and the harness versions this was checked
|
|
262
|
+
against are in [docs/harnesses.md](docs/harnesses.md).
|
|
263
|
+
|
|
264
|
+
**Homes per OS.** Every `~` above is Python's `Path.home()`: `$HOME` on macOS and
|
|
265
|
+
Linux, `%USERPROFILE%` on Windows (so `~/.claude` is `C:\Users\<you>\.claude`).
|
|
266
|
+
opencode honours an absolute `$XDG_CONFIG_HOME` on every OS and otherwise uses
|
|
267
|
+
`~/.config/opencode`, Windows included; the tool does not look in `%APPDATA%`. This
|
|
268
|
+
tool's own state lives in `~/.agent-toggle/`. The Linux and Windows locations
|
|
269
|
+
follow the harnesses' documentation and have not yet been checked against real
|
|
270
|
+
installs there; if a harness keeps its files elsewhere, `status` shows it as not
|
|
271
|
+
installed.
|
|
272
|
+
|
|
273
|
+
Plugin ids are passed to `claude plugin ...`, which on Windows runs through
|
|
274
|
+
`cmd.exe`. An id with anything outside `A-Z a-z 0-9 . _ @ : / -` (for example `&`,
|
|
275
|
+
`%`, `|` or a space) is refused on every OS before any command runs.
|
|
276
|
+
|
|
216
277
|
Copilot's `config.json` is machine-managed and never edited by this tool; Copilot
|
|
217
278
|
plugins are not yet supported. `claude` plugins are toggled through `claude
|
|
218
279
|
plugin enable/disable`; `openclaw` plugins through a config flag (see [Assumed
|
|
@@ -431,10 +492,16 @@ Only `error` makes the exit code `1`; each problem row names the command that
|
|
|
431
492
|
fixes it. `--harness X` for a harness that is not installed is a `note` (exit
|
|
432
493
|
`0`). Companion files are not checked.
|
|
433
494
|
|
|
495
|
+
A harness item parked with no state entry also carries `orphan`, and its fix
|
|
496
|
+
follows from it: `identical` (the live copy has the same content: delete the
|
|
497
|
+
parked copy), `differs` (a different live copy exists: compare, keep one), or
|
|
498
|
+
`parked-only` (move it back, then `disable` it so the state records it).
|
|
499
|
+
|
|
434
500
|
## Assumed formats
|
|
435
501
|
|
|
436
|
-
Two config shapes
|
|
437
|
-
were
|
|
502
|
+
Two config shapes came from the design survey (`docs/DESIGN.md` §4 / §11). Both
|
|
503
|
+
were since seen on one real install each (openclaw 2026.7.1-2, opencode 2.0.22; see
|
|
504
|
+
`docs/harnesses.md`), but the tool still treats them defensively:
|
|
438
505
|
|
|
439
506
|
- `openclaw.json`: `skills.entries.<name>.enabled` and
|
|
440
507
|
`plugins.entries.<name>.enabled`
|
|
@@ -524,13 +591,47 @@ ruff check .
|
|
|
524
591
|
stdlib `unittest`, no fixtures, no network. Every test runs against a
|
|
525
592
|
throwaway temp `HOME` and a stubbed `claude` CLI.
|
|
526
593
|
|
|
594
|
+
## CI notifications
|
|
595
|
+
|
|
596
|
+
`.github/workflows/ci.yml` can send a Telegram message when the test matrix fails on a
|
|
597
|
+
push (a lint-only failure does not page). Alerts never fire for pull requests or green
|
|
598
|
+
runs. The message names the repository, branch, 7-character commit and a link to the run.
|
|
599
|
+
|
|
600
|
+
Set it up once, with the `telegram` extra installed (see [Install](#install)):
|
|
601
|
+
|
|
602
|
+
```sh
|
|
603
|
+
agent-toggle config # prompts for the bot token (hidden) and the chat id
|
|
604
|
+
agent-toggle config test # send one test message
|
|
605
|
+
agent-toggle config sync-ci # set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID on the repo
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
`config` works like aicp's `--config`: Enter keeps the current value, `-` clears it.
|
|
609
|
+
The bot token is kept only in the OS credential store (macOS Keychain, Linux Secret
|
|
610
|
+
Service, Windows DPAPI) through telegram-kit, never in a file or on a command line, and
|
|
611
|
+
is shown masked. With no credential store it refuses to store the token; set
|
|
612
|
+
`TG_BOT_TOKEN` in the environment instead (`TG_CHAT_ID` likewise for the chat id). The
|
|
613
|
+
chat id is ordinary configuration in `~/.agent-toggle/config.json`: a number, `-100...`
|
|
614
|
+
for a group, or an `@channel`. `config sync-ci` needs the GitHub CLI (`gh`) signed in; it
|
|
615
|
+
passes both values on stdin, and `--repo OWNER/REPO` / `--dry-run` work as elsewhere.
|
|
616
|
+
|
|
617
|
+
To skip the tool, set the secrets by hand (each command prompts for the value):
|
|
618
|
+
|
|
619
|
+
```sh
|
|
620
|
+
gh secret set TELEGRAM_BOT_TOKEN
|
|
621
|
+
gh secret set TELEGRAM_CHAT_ID
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
With either secret unset (a fork, or a clone you have not configured) the notify job
|
|
625
|
+
prints a `::notice::` and exits 0, so it never adds a second red X.
|
|
626
|
+
|
|
527
627
|
## Releasing
|
|
528
628
|
|
|
529
629
|
A release is a git tag `vX.Y.Z` (where `X.Y.Z` must match `agent_toggle.__version__`).
|
|
530
630
|
Pushing the tag triggers `.github/workflows/release.yml`:
|
|
531
631
|
|
|
532
|
-
1. **Build** job checks that the tag matches `__version__
|
|
533
|
-
source distribution
|
|
632
|
+
1. **Build** job checks that the tag matches `__version__`, builds a wheel and
|
|
633
|
+
source distribution, and generates the shell completion files (uploaded as the
|
|
634
|
+
`completions` artifact; the workflow does not create a GitHub Release).
|
|
534
635
|
2. **Publish** job uploads to PyPI using
|
|
535
636
|
[trusted publishing](https://docs.pypi.org/trusted-publishers/),
|
|
536
637
|
with no stored tokens—only the `pypi` environment and OIDC setup.
|
|
@@ -12,7 +12,7 @@ to the targets -- a user's `git status` must not change because of our
|
|
|
12
12
|
bookkeeping.
|
|
13
13
|
|
|
14
14
|
Usage:
|
|
15
|
-
agent_toggle.py ui [--dry-run] # interactive picker (curses)
|
|
15
|
+
agent_toggle.py ui [--dry-run] # interactive picker (curses, else numbered menu)
|
|
16
16
|
agent_toggle.py cost [--type T] # startup token estimates, biggest first
|
|
17
17
|
agent_toggle.py disable <type> <name>... [--harness H] [--dry-run]
|
|
18
18
|
agent_toggle.py enable <type> <name>... [--harness H] [--dry-run]
|
|
@@ -26,6 +26,7 @@ Usage:
|
|
|
26
26
|
agent_toggle.py migrate # import old ~/.claude-toggle state
|
|
27
27
|
agent_toggle.py profile save|apply|diff|list [name|file] [--out F] [--dry-run]
|
|
28
28
|
agent_toggle.py install-shims [--dry-run] # write the skill shim into each harness
|
|
29
|
+
agent_toggle.py config [test|sync-ci] # Telegram settings for CI failure alerts
|
|
29
30
|
|
|
30
31
|
<type> = skill | agent | command | rule | plugin | mcp
|
|
31
32
|
--json prints exactly one JSON document; exit codes: 0 ok, 1 partial
|
|
@@ -41,7 +42,7 @@ import sys
|
|
|
41
42
|
import traceback
|
|
42
43
|
from pathlib import Path
|
|
43
44
|
|
|
44
|
-
from . import __version__, cost, doctor, fs, ops, profiles, store, undo
|
|
45
|
+
from . import __version__, config, cost, doctor, fs, ops, profiles, store, undo
|
|
45
46
|
from .backends.plugin_cli import claude_bin
|
|
46
47
|
from .fs import gitignored
|
|
47
48
|
from .harnesses import TYPES, harness_of, harnesses, project_view
|
|
@@ -161,6 +162,7 @@ def cmd_status(state: dict, out: Result, only: str | None = None) -> None:
|
|
|
161
162
|
found = list(parked.iterdir())
|
|
162
163
|
else:
|
|
163
164
|
found = [p for p in parked.rglob("*") if p.is_file()]
|
|
165
|
+
found = [p for p in found if not p.name.startswith(".")] # .DS_Store, as doctor
|
|
164
166
|
items += found
|
|
165
167
|
ign = ign and gitignored(
|
|
166
168
|
parked, home if view.owner == hname else table[view.owner].home)
|
|
@@ -188,6 +190,14 @@ def cmd_status(state: dict, out: Result, only: str | None = None) -> None:
|
|
|
188
190
|
for (hname, proj), n in sorted(projects.items(), key=str):
|
|
189
191
|
out.say(f"project {proj} ({n} parked under {fs.parked_dir()})")
|
|
190
192
|
out.row(hname, None, None, "status", "project", "", show=False, project=proj, parked=n)
|
|
193
|
+
# the inverse of `untracked`: a state entry whose parked item is gone (DESIGN s6)
|
|
194
|
+
for k, e in state["disabled"].items():
|
|
195
|
+
if only and only not in (e.get("harness"), *_list_of(e, "shared_with")):
|
|
196
|
+
continue
|
|
197
|
+
if gone := doctor.missing_parked(k, e):
|
|
198
|
+
out.say(f"WARNING {e.get('harness')} {e.get('type')} {e.get('name')}: {gone}", warn=True)
|
|
199
|
+
out.row(e.get("harness"), e.get("type"), e.get("name"), "status", "stale", gone,
|
|
200
|
+
show=False, parked_at=e.get("parked_at"))
|
|
191
201
|
|
|
192
202
|
|
|
193
203
|
def parked_drift(items: list[Path], parked: Path, live: Path,
|
|
@@ -228,8 +238,8 @@ def cmd_cost(state: dict, out: Result, harness: str | None = None,
|
|
|
228
238
|
def cmd_ui(state: dict, out: Result, dry_run: bool = False) -> None:
|
|
229
239
|
try:
|
|
230
240
|
from .ui import picker as ui
|
|
231
|
-
except ImportError
|
|
232
|
-
|
|
241
|
+
except ImportError: # no curses (Windows without windows-curses)
|
|
242
|
+
from .ui import menu as ui
|
|
233
243
|
changes = ui.pick(store.scope_state(state), harnesses(), plugins=not dry_run,
|
|
234
244
|
color=use_color(sys.stdout, out.color))
|
|
235
245
|
if changes is None:
|
|
@@ -289,13 +299,17 @@ def cmd_migrate(out: Result) -> None:
|
|
|
289
299
|
out.row(None, None, None, "migrate", "ok", "; ".join(lines), show=False)
|
|
290
300
|
|
|
291
301
|
|
|
292
|
-
|
|
302
|
+
SHIMS = Path(__file__).with_name("shims")
|
|
303
|
+
# Every template carries this line; install-shims only overwrites a file that has it.
|
|
304
|
+
SHIM_MARKER = ("<!-- agent-toggle shim: written by `agent-toggle install-shims`; "
|
|
305
|
+
"re-running it overwrites this file -->")
|
|
293
306
|
|
|
294
307
|
|
|
295
|
-
def
|
|
296
|
-
"""
|
|
297
|
-
|
|
298
|
-
template =
|
|
308
|
+
def shim_text(harness: str) -> str:
|
|
309
|
+
"""The shim for `harness`: shims/<harness>.md.tmpl, else shims/generic.md.tmpl."""
|
|
310
|
+
tmpl = SHIMS / f"{harness}.md.tmpl"
|
|
311
|
+
template = (tmpl if tmpl.is_file() else SHIMS / "generic.md.tmpl").read_text(encoding="utf-8")
|
|
312
|
+
template = template.replace("__HARNESS__", harness)
|
|
299
313
|
root = Path(__file__).resolve().parent.parent
|
|
300
314
|
if (root / "agent_toggle.py").is_file():
|
|
301
315
|
# Harness dirs often sync across machines: write `~`-relative under $HOME.
|
|
@@ -303,16 +317,38 @@ def cmd_install_shims(args: argparse.Namespace, out: Result) -> None:
|
|
|
303
317
|
shown = "~/" + root.relative_to(fs.home()).as_posix()
|
|
304
318
|
except ValueError:
|
|
305
319
|
shown = str(root)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
320
|
+
return template.replace("__AGENT_TOGGLE_ROOT__", shown)
|
|
321
|
+
# installed package: no checkout to point at
|
|
322
|
+
return "".join(ln for ln in template.splitlines(keepends=True)
|
|
323
|
+
if "__AGENT_TOGGLE_ROOT__" not in ln)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def shim_refusal(dest: Path) -> str | None:
|
|
327
|
+
"""Why `dest` must not be overwritten (it is not a shim we wrote), else None."""
|
|
328
|
+
if dest.is_symlink() or (dest.exists() and not dest.is_file()):
|
|
329
|
+
return "is a symlink or not a regular file"
|
|
330
|
+
if not dest.exists():
|
|
331
|
+
return None
|
|
332
|
+
try:
|
|
333
|
+
text = dest.read_text(encoding="utf-8")
|
|
334
|
+
except (OSError, UnicodeDecodeError):
|
|
335
|
+
text = ""
|
|
336
|
+
# Shims from before the marker existed: our frontmatter name plus our heading.
|
|
337
|
+
legacy = text.startswith("---\nname: agent-toggle\n") and "\n# agent-toggle\n" in text
|
|
338
|
+
ours = SHIM_MARKER in text or legacy
|
|
339
|
+
return None if ours else "exists and lacks the agent-toggle shim marker"
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def cmd_install_shims(args: argparse.Namespace, out: Result) -> None:
|
|
343
|
+
"""Write <home>/skills/agent-toggle/SKILL.md into every installed harness that
|
|
344
|
+
supports skills; keep park dirs out of an existing harness-home .gitignore.
|
|
345
|
+
A file there that is not our shim (no SHIM_MARKER) is refused, never overwritten."""
|
|
310
346
|
table = harnesses()
|
|
311
347
|
park = sorted({f"{sub}-disabled" for h in table.values()
|
|
312
348
|
for subs in h.dirs.values() for sub in subs
|
|
313
349
|
if not Path(sub).is_absolute()}) # skills.paths redirects live elsewhere
|
|
314
350
|
verb = "would install" if args.dry_run else "installed"
|
|
315
|
-
installed = 0
|
|
351
|
+
installed = found = 0
|
|
316
352
|
for hname, h in table.items():
|
|
317
353
|
home = h.home
|
|
318
354
|
if args.harness and hname != args.harness:
|
|
@@ -322,10 +358,17 @@ def cmd_install_shims(args: argparse.Namespace, out: Result) -> None:
|
|
|
322
358
|
"not installed" if not home.is_dir() else "no skill support",
|
|
323
359
|
show=False, home=str(home))
|
|
324
360
|
continue
|
|
361
|
+
found += 1
|
|
325
362
|
dest = home / h.dirs["skill"][0] / "agent-toggle" / "SKILL.md"
|
|
363
|
+
if why := shim_refusal(dest):
|
|
364
|
+
fix = (f"refused: {dest} {why}; fix: move it aside (or delete it if it is an "
|
|
365
|
+
f"older agent-toggle shim), then re-run install-shims")
|
|
366
|
+
out.row(hname, None, None, "install-shims", "error", fix, show=False, home=str(home))
|
|
367
|
+
out.say(f" {fix}", style="red")
|
|
368
|
+
continue
|
|
326
369
|
if not args.dry_run:
|
|
327
370
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
328
|
-
dest.write_text(
|
|
371
|
+
dest.write_text(shim_text(hname), encoding="utf-8")
|
|
329
372
|
out.row(hname, None, None, "install-shims", "planned" if args.dry_run else "ok",
|
|
330
373
|
str(dest), show=False, home=str(home))
|
|
331
374
|
out.say(f" {verb} {dest}")
|
|
@@ -342,13 +385,13 @@ def cmd_install_shims(args: argparse.Namespace, out: Result) -> None:
|
|
|
342
385
|
+ "".join(f"{m}\n" for m in missing))
|
|
343
386
|
if missing:
|
|
344
387
|
out.say(f" gitignore {ignore}: {' '.join(missing)}")
|
|
345
|
-
if not
|
|
388
|
+
if not found:
|
|
346
389
|
die("no harness found", 4)
|
|
347
390
|
out.say(f"{installed} harness(es) {'planned' if args.dry_run else 'installed'}")
|
|
348
391
|
|
|
349
392
|
|
|
350
393
|
COMMANDS = ("ui", "pick", "status", "list", "cost", "migrate", "disable", "enable",
|
|
351
|
-
"install-shims", "profile", "undo", "doctor")
|
|
394
|
+
"install-shims", "profile", "undo", "doctor", "config")
|
|
352
395
|
_GLOBAL_FLAGS = ("--json", "-v", "--verbose")
|
|
353
396
|
|
|
354
397
|
|
|
@@ -427,6 +470,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
427
470
|
pp.add_argument("--project", metavar="dir", help=PROJECT_HELP)
|
|
428
471
|
sub.add_parser("doctor", parents=[common],
|
|
429
472
|
help="read-only check of harness layouts and state against disk")
|
|
473
|
+
cf = sub.add_parser("config", parents=[common],
|
|
474
|
+
help="Telegram settings for the CI failure alerts (needs the telegram extra)")
|
|
475
|
+
cf.add_argument("action", nargs="?", choices=("test", "sync-ci"),
|
|
476
|
+
help="test: send one message | sync-ci: set the GitHub repo secrets")
|
|
477
|
+
cf.add_argument("--repo", metavar="OWNER/REPO", help="sync-ci: repository (default: this one)")
|
|
478
|
+
cf.add_argument("--dry-run", action="store_true", help="sync-ci: show the plan; set nothing")
|
|
430
479
|
up2 = sub.add_parser("undo", parents=[common], help="reverse the last logged batch")
|
|
431
480
|
up2.add_argument("--dry-run", action="store_true", help="show the plan; change nothing")
|
|
432
481
|
for name, verb in (("disable", "park"), ("enable", "restore")):
|
|
@@ -471,6 +520,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
471
520
|
profiles.cmd_profile(args, out)
|
|
472
521
|
elif cmd == "doctor":
|
|
473
522
|
doctor.cmd_doctor(args.harness, out)
|
|
523
|
+
elif cmd == "config":
|
|
524
|
+
config.cmd_config(args, out)
|
|
474
525
|
elif cmd == "undo":
|
|
475
526
|
undo.cmd_undo(args.dry_run, args.harness, out)
|
|
476
527
|
elif cmd == "enable" and args.all:
|