agent-toggle 0.7.0__tar.gz → 0.7.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/PKG-INFO +5 -1
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/README.md +4 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/__init__.py +1 -1
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/backends/plugin_cli.py +2 -1
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/fs.py +15 -1
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/mechanisms.py +25 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/ui/picker.py +21 -12
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle.egg-info/PKG-INFO +5 -1
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_fs.py +17 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_picker.py +13 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_platform.py +12 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_store.py +14 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/LICENSE +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/__main__.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/backends/__init__.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/backends/flag_json.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/backends/mcp_json.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/backends/mcp_toml.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/cli.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/companions.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/config.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/cost.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/doctor.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/harnesses.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/help.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/i18n.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/locales/__init__.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/locales/zh_tw_cli.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/locales/zh_tw_common.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/locales/zh_tw_config.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/locales/zh_tw_picker.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/ops.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/output.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/profiles.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/settings.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/shims/claude.md.tmpl +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/shims/generic.md.tmpl +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/spinner.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/store.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/toml_check.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/ui/__init__.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/ui/config_menu.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/ui/menu.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/ui/model.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/ui/theme.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/undo.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/update_check.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle/update_prompt.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle.egg-info/SOURCES.txt +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle.egg-info/dependency_links.txt +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle.egg-info/entry_points.txt +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle.egg-info/requires.txt +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/agent_toggle.egg-info/top_level.txt +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/pyproject.toml +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/setup.cfg +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_alias.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_backends.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_cli.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_cli_surface.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_color.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_companions.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_completions.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_config.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_config_menu.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_conformance.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_containment.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_cost.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_crash.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_doctor.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_encoding.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_harnesses.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_help.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_i18n.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_install.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_mechanisms.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_menu.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_profiles.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_project.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_safety.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_secret_audit.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_settings.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_theme.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_undo.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_update_check.py +0 -0
- {agent_toggle-0.7.0 → agent_toggle-0.7.3}/tests/test_update_prompt.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-toggle
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.3
|
|
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
|
|
@@ -855,6 +855,10 @@ ruff check .
|
|
|
855
855
|
stdlib `unittest`, no fixtures, no network. Every test runs against a
|
|
856
856
|
throwaway temp `HOME` and a stubbed `claude` CLI.
|
|
857
857
|
|
|
858
|
+
`python3 tools/wheel_smoke.py` builds the wheel, installs it without extras into a
|
|
859
|
+
fresh venv (what a PyPI user gets) and runs the CLI against a temp `HOME`; CI runs
|
|
860
|
+
it on macOS, Linux and Windows.
|
|
861
|
+
|
|
858
862
|
## CI notifications
|
|
859
863
|
|
|
860
864
|
`.github/workflows/ci.yml` can send a Telegram message when the test matrix fails on a
|
|
@@ -823,6 +823,10 @@ ruff check .
|
|
|
823
823
|
stdlib `unittest`, no fixtures, no network. Every test runs against a
|
|
824
824
|
throwaway temp `HOME` and a stubbed `claude` CLI.
|
|
825
825
|
|
|
826
|
+
`python3 tools/wheel_smoke.py` builds the wheel, installs it without extras into a
|
|
827
|
+
fresh venv (what a PyPI user gets) and runs the CLI against a temp `HOME`; CI runs
|
|
828
|
+
it on macOS, Linux and Windows.
|
|
829
|
+
|
|
826
830
|
## CI notifications
|
|
827
831
|
|
|
828
832
|
`.github/workflows/ci.yml` can send a Telegram message when the test matrix fails on a
|
|
@@ -28,7 +28,8 @@ def claude_bin() -> str | None:
|
|
|
28
28
|
if found:
|
|
29
29
|
return found
|
|
30
30
|
home = fs.home()
|
|
31
|
-
for cand in (home / ".local/bin/claude", home / ".
|
|
31
|
+
for cand in (home / ".local/bin/claude", home / ".local/bin/claude.exe", # .exe: Windows
|
|
32
|
+
home / ".claude/local/claude"):
|
|
32
33
|
if cand.is_file() and os.access(cand, os.X_OK):
|
|
33
34
|
return str(cand)
|
|
34
35
|
return None
|
|
@@ -416,6 +416,9 @@ def safe_move(src: Path, dest_dir: Path) -> Path:
|
|
|
416
416
|
if target.exists() or target.is_symlink():
|
|
417
417
|
raise FileExistsError(f"{target} already exists -- refusing to overwrite")
|
|
418
418
|
|
|
419
|
+
if src.is_symlink() and link_text(src, dest_dir) != os.readlink(src):
|
|
420
|
+
_copy_move(src, target) # a rename keeps the relative text, now dangling
|
|
421
|
+
return target
|
|
419
422
|
try:
|
|
420
423
|
os.rename(src, target) # same filesystem: one atomic step
|
|
421
424
|
except OSError as e:
|
|
@@ -425,6 +428,17 @@ def safe_move(src: Path, dest_dir: Path) -> Path:
|
|
|
425
428
|
return target
|
|
426
429
|
|
|
427
430
|
|
|
431
|
+
def link_text(src: Path, dest_dir: Path) -> str:
|
|
432
|
+
"""The symlink `src`'s text as seen from `dest_dir`: a relative link keeps its target,
|
|
433
|
+
not its text, since parks sit at another depth (skills/ vs parked/user/<h>/skills/)."""
|
|
434
|
+
link = os.readlink(src)
|
|
435
|
+
if os.path.isabs(link):
|
|
436
|
+
return link
|
|
437
|
+
# the kernel resolves a relative link from the link's physical dir
|
|
438
|
+
target = os.path.normpath(os.path.join(os.path.realpath(src.parent), link))
|
|
439
|
+
return os.path.relpath(target, os.path.realpath(dest_dir))
|
|
440
|
+
|
|
441
|
+
|
|
428
442
|
def move_leftovers(src: Path, target: Path) -> tuple[Path, Path]:
|
|
429
443
|
"""(half copy, trash) a killed cross-fs move can leave beside `target` / `src`."""
|
|
430
444
|
return (target.with_name(f".{target.name}.agent-toggle-tmp"),
|
|
@@ -477,7 +491,7 @@ def _copy_move(src: Path, target: Path) -> None:
|
|
|
477
491
|
remove_leftover(tmp) # a killed earlier attempt's half copy
|
|
478
492
|
try:
|
|
479
493
|
if src.is_symlink():
|
|
480
|
-
os.symlink(
|
|
494
|
+
os.symlink(link_text(src, target.parent), tmp)
|
|
481
495
|
elif src.is_dir():
|
|
482
496
|
shutil.copytree(src, tmp, symlinks=True)
|
|
483
497
|
else:
|
|
@@ -360,6 +360,31 @@ def migrate_parks(state: dict, table: dict | None = None) -> list[str]:
|
|
|
360
360
|
lines.append(f"moved {len(moved)} item(s) {old} -> {v.parked}"
|
|
361
361
|
+ (f", repointed {repointed} state entry(ies)" if repointed else "")
|
|
362
362
|
+ ("" if _exists(old) else f", removed {old}"))
|
|
363
|
+
return lines + repair_parked_links(table)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def repair_parked_links(table: dict) -> list[str]:
|
|
367
|
+
"""0.7.0's migrate moved relative symlinks with their text unchanged, so they dangle
|
|
368
|
+
from the deeper park dir; re-aim each one whose text still resolves from its live dir."""
|
|
369
|
+
lines, seen = [], set()
|
|
370
|
+
for hn, h in table.items():
|
|
371
|
+
for type_, subs in h.dirs.items():
|
|
372
|
+
for sub in subs:
|
|
373
|
+
v = dir_view(table, hn, type_, h.home, sub)
|
|
374
|
+
if v.parked in seen or not v.parked.is_dir() or v.parked.is_symlink():
|
|
375
|
+
continue
|
|
376
|
+
seen.add(v.parked)
|
|
377
|
+
for p in sorted(v.parked.iterdir()):
|
|
378
|
+
if not p.is_symlink() or p.exists() or os.path.isabs(link := os.readlink(p)):
|
|
379
|
+
continue
|
|
380
|
+
target = os.path.normpath(os.path.join(os.path.realpath(v.live), link))
|
|
381
|
+
if not os.path.exists(target):
|
|
382
|
+
continue # dangles from its live dir too: not ours
|
|
383
|
+
tmp = fs.move_leftovers(p, p)[0]
|
|
384
|
+
fs.remove_leftover(tmp)
|
|
385
|
+
os.symlink(os.path.relpath(target, os.path.realpath(v.parked)), tmp)
|
|
386
|
+
fs.replace(tmp, p) # atomic: the link is never missing
|
|
387
|
+
lines.append(f"re-aimed dangling link {p} -> {target}")
|
|
363
388
|
return lines
|
|
364
389
|
|
|
365
390
|
|
|
@@ -201,22 +201,18 @@ def heading(type_: str, count: int, width: int, g: theme.Glyphs) -> list[theme.S
|
|
|
201
201
|
return [(label, "title"), (str(count), "muted"), (" " + g.h * max(0, rest), "muted")]
|
|
202
202
|
|
|
203
203
|
|
|
204
|
-
def detail_lines(r: Row, width: int, g: theme.Glyphs
|
|
205
|
-
|
|
204
|
+
def detail_lines(r: Row, width: int, g: theme.Glyphs,
|
|
205
|
+
height: int | None = None) -> list[list[theme.Segment]]:
|
|
206
|
+
"""The detail pane for the cursor row: one segment list per screen line. With `height`,
|
|
207
|
+
the description is cut (with an ellipsis) so the fields below it still fit."""
|
|
206
208
|
lab = 12 # "description" is 11 cells; leave a gap
|
|
207
209
|
val = max(1, width - lab)
|
|
208
210
|
out = [[(theme.truncate(r.name, width, g.ellipsis), "title")], []]
|
|
211
|
+
rest: list[list[theme.Segment]] = []
|
|
209
212
|
|
|
210
213
|
def field(label: str, text: str, role: str = "text") -> None:
|
|
211
214
|
for n in range(0, max(1, len(text)), val): # a long path wraps
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if r.description: # frontmatter prose, not a path
|
|
215
|
-
chunks = wrap_cells(r.description, val)
|
|
216
|
-
label = t("picker.d.description", "description")
|
|
217
|
-
for n, chunk in enumerate(chunks):
|
|
218
|
-
out.append([(pad(label if n == 0 else "", lab), "muted"), (chunk, "text")])
|
|
219
|
-
out.append([])
|
|
215
|
+
rest.append([(pad(label if n == 0 else "", lab), "muted"), (text[n:n + val], role)])
|
|
220
216
|
|
|
221
217
|
state = t("picker.state.live", "live") if r.enabled else t("picker.state.parked", "parked")
|
|
222
218
|
field(t("picker.d.harness", "harness"), r.harness, theme.harness_role(r.harness))
|
|
@@ -242,7 +238,20 @@ def detail_lines(r: Row, width: int, g: theme.Glyphs) -> list[list[theme.Segment
|
|
|
242
238
|
field(t("picker.d.since", "since"), r.since[:19].replace("T", " "))
|
|
243
239
|
if r.mechanism:
|
|
244
240
|
field(t("picker.d.mechanism", "mechanism"), r.mechanism)
|
|
245
|
-
|
|
241
|
+
|
|
242
|
+
if r.description: # frontmatter prose, not a path
|
|
243
|
+
chunks = wrap_cells(r.description, val)
|
|
244
|
+
# the "staged" line comes and goes with Space; reserving it keeps the text from jumping
|
|
245
|
+
spare = len(rest) + (not r.changed)
|
|
246
|
+
room = len(chunks) if height is None else max(1, height - len(out) - spare - 1)
|
|
247
|
+
if len(chunks) > room:
|
|
248
|
+
chunks = chunks[:room - 1] + [theme.truncate(" ".join(chunks[room - 1:]), val,
|
|
249
|
+
g.ellipsis)]
|
|
250
|
+
label = t("picker.d.description", "description")
|
|
251
|
+
for n, chunk in enumerate(chunks):
|
|
252
|
+
out.append([(pad(label if n == 0 else "", lab), "muted"), (chunk, "text")])
|
|
253
|
+
out.append([])
|
|
254
|
+
return out + rest
|
|
246
255
|
|
|
247
256
|
|
|
248
257
|
def chips(v: View, g: theme.Glyphs) -> list[tuple[str, str]]:
|
|
@@ -322,7 +331,7 @@ def draw(win, v: View, shown: list[Row], pal: theme.Palette, g: theme.Glyphs,
|
|
|
322
331
|
else:
|
|
323
332
|
put(y, row_segments(line[1], lw, v, g, line[1] is cursor_row, any_shared))
|
|
324
333
|
if dw:
|
|
325
|
-
pane = detail_lines(cursor_row, dw - 2, g) if cursor_row else []
|
|
334
|
+
pane = detail_lines(cursor_row, dw - 2, g, body) if cursor_row else []
|
|
326
335
|
for i in range(body):
|
|
327
336
|
put(BODY_TOP + i, [(g.v, "muted"), (" ", "text"), *(pane[i] if i < len(pane) else [])],
|
|
328
337
|
lw)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-toggle
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.3
|
|
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
|
|
@@ -855,6 +855,10 @@ ruff check .
|
|
|
855
855
|
stdlib `unittest`, no fixtures, no network. Every test runs against a
|
|
856
856
|
throwaway temp `HOME` and a stubbed `claude` CLI.
|
|
857
857
|
|
|
858
|
+
`python3 tools/wheel_smoke.py` builds the wheel, installs it without extras into a
|
|
859
|
+
fresh venv (what a PyPI user gets) and runs the CLI against a temp `HOME`; CI runs
|
|
860
|
+
it on macOS, Linux and Windows.
|
|
861
|
+
|
|
858
862
|
## CI notifications
|
|
859
863
|
|
|
860
864
|
`.github/workflows/ci.yml` can send a Telegram message when the test matrix fails on a
|
|
@@ -38,6 +38,23 @@ class SafeMoveTest(SandboxCase):
|
|
|
38
38
|
# existing parked copy untouched
|
|
39
39
|
self.assertEqual((self.home / "skills-disabled/a/SKILL.md").read_text(encoding="utf-8"), "old")
|
|
40
40
|
|
|
41
|
+
@unittest.skipUnless(CAN_SYMLINK, "cannot create symlinks here")
|
|
42
|
+
def test_relative_link_keeps_its_target_across_depths(self) -> None:
|
|
43
|
+
real = self.write(".repos/r/demo/SKILL.md", "hi").parent
|
|
44
|
+
link = self.home / "skills" / "demo"
|
|
45
|
+
link.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
link.symlink_to("../.repos/r/demo")
|
|
47
|
+
parked = fs.safe_move(link, fs.user_park("claude", "skills"))
|
|
48
|
+
self.assertEqual(parked.resolve(), real.resolve()) # not dangling while parked
|
|
49
|
+
back = fs.safe_move(parked, self.home / "skills")
|
|
50
|
+
# the round trip is exact (Windows re-aims with native separators)
|
|
51
|
+
self.assertEqual(os.path.normpath(os.readlink(back)), os.path.normpath("../.repos/r/demo"))
|
|
52
|
+
absolute = self.home / "skills" / "abs"
|
|
53
|
+
absolute.symlink_to(real)
|
|
54
|
+
text = os.readlink(absolute) # Windows may add \\?\
|
|
55
|
+
self.assertEqual(os.readlink(fs.safe_move(absolute, fs.user_park("claude", "skills"))),
|
|
56
|
+
text) # absolute: untouched
|
|
57
|
+
|
|
41
58
|
|
|
42
59
|
POSIX = os.name != "nt"
|
|
43
60
|
|
|
@@ -465,6 +465,19 @@ class LayoutTest(ScreenCase):
|
|
|
465
465
|
self.assertIn("alpha beta", body)
|
|
466
466
|
self.assertIn("gamma delta", body)
|
|
467
467
|
|
|
468
|
+
def test_long_description_is_cut_so_the_fields_still_show(self) -> None:
|
|
469
|
+
r = model.Row("claude", "skill", "solo", True, description="word " * 200,
|
|
470
|
+
mechanism="move")
|
|
471
|
+
lines = picker.detail_lines(r, 30, theme.ASCII, height=12)
|
|
472
|
+
self.assertLessEqual(len(lines), 12)
|
|
473
|
+
flat = ["".join(s for s, _ in line) for line in lines]
|
|
474
|
+
self.assertTrue(any(line.rstrip().endswith("...") for line in flat))
|
|
475
|
+
self.assertTrue(any(line.startswith("mechanism") for line in flat))
|
|
476
|
+
r.staged = False # toggling adds the "staged" line, not a cut
|
|
477
|
+
toggled = picker.detail_lines(r, 30, theme.ASCII, height=12)
|
|
478
|
+
self.assertLessEqual(len(toggled), 12)
|
|
479
|
+
self.assertEqual(lines[:8], toggled[:8])
|
|
480
|
+
|
|
468
481
|
def test_detail_pane_description_is_translated(self) -> None:
|
|
469
482
|
self.addCleanup(i18n.set_language, i18n.LANGUAGE)
|
|
470
483
|
i18n.set_language("zh-TW")
|
|
@@ -11,6 +11,7 @@ from base import SandboxCase
|
|
|
11
11
|
|
|
12
12
|
from agent_toggle import fs
|
|
13
13
|
from agent_toggle import mechanisms as mech
|
|
14
|
+
from agent_toggle.backends import plugin_cli
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class PluginIdTest(SandboxCase):
|
|
@@ -103,5 +104,16 @@ class CaseRulesTest(SandboxCase):
|
|
|
103
104
|
self.assertEqual(mech.live_names(self.home / "commands", "command"), ["group:sub:tool"])
|
|
104
105
|
|
|
105
106
|
|
|
107
|
+
class ClaudeBinTest(SandboxCase):
|
|
108
|
+
def test_off_path_windows_install_is_found_by_its_exe_name(self) -> None:
|
|
109
|
+
"""The native Windows installer writes ~/.local/bin/claude.exe; PATH may lack it."""
|
|
110
|
+
plugin_cli.which = lambda name: None
|
|
111
|
+
exe = self.tmp / ".local" / "bin" / "claude.exe"
|
|
112
|
+
exe.parent.mkdir(parents=True)
|
|
113
|
+
exe.touch()
|
|
114
|
+
exe.chmod(0o755)
|
|
115
|
+
self.assertEqual(plugin_cli.claude_bin(), str(exe))
|
|
116
|
+
|
|
117
|
+
|
|
106
118
|
if __name__ == "__main__":
|
|
107
119
|
unittest.main()
|
|
@@ -152,6 +152,20 @@ class MigrateParksTest(CliCase):
|
|
|
152
152
|
self.assertEqual(store.load_state()["disabled"]["claude:skill:demo-skill"]["parked_at"],
|
|
153
153
|
str(self.user_parked("skills", "demo-skill")))
|
|
154
154
|
|
|
155
|
+
@unittest.skipUnless(CAN_SYMLINK, "cannot create symlinks here")
|
|
156
|
+
def test_a_link_left_dangling_by_0_7_0_is_re_aimed(self) -> None:
|
|
157
|
+
real = self.write(".repos/r/linked/SKILL.md", "hi").parent
|
|
158
|
+
link = self.user_parked("skills", "linked")
|
|
159
|
+
link.parent.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
link.symlink_to("../.repos/r/linked") # 0.7.0 kept the text
|
|
161
|
+
gone = self.user_parked("skills", "gone")
|
|
162
|
+
gone.symlink_to("../.repos/nowhere") # dangles from skills/ too
|
|
163
|
+
self.run_json("migrate")
|
|
164
|
+
self.assertEqual(link.resolve(), real.resolve())
|
|
165
|
+
self.assertEqual(os.readlink(gone), "../.repos/nowhere") # left alone
|
|
166
|
+
rc, env = self.run_json("migrate")
|
|
167
|
+
self.assertNotIn("re-aimed", env["results"][0]["detail"]) # idempotent
|
|
168
|
+
|
|
155
169
|
|
|
156
170
|
class LogSchemaTest(SandboxCase):
|
|
157
171
|
FIELDS = {"ts", "harness", "type", "name", "action", "result", "batch",
|
|
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
|