agent-toggle 0.7.0__tar.gz → 0.7.2__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 (85) hide show
  1. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/PKG-INFO +1 -1
  2. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/__init__.py +1 -1
  3. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/fs.py +15 -1
  4. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/mechanisms.py +25 -0
  5. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle.egg-info/PKG-INFO +1 -1
  6. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_fs.py +17 -0
  7. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_store.py +14 -0
  8. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/LICENSE +0 -0
  9. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/README.md +0 -0
  10. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/__main__.py +0 -0
  11. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/backends/__init__.py +0 -0
  12. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/backends/flag_json.py +0 -0
  13. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/backends/mcp_json.py +0 -0
  14. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/backends/mcp_toml.py +0 -0
  15. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/backends/plugin_cli.py +0 -0
  16. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/cli.py +0 -0
  17. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/companions.py +0 -0
  18. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/config.py +0 -0
  19. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/cost.py +0 -0
  20. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/doctor.py +0 -0
  21. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/harnesses.py +0 -0
  22. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/help.py +0 -0
  23. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/i18n.py +0 -0
  24. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/locales/__init__.py +0 -0
  25. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/locales/zh_tw_cli.py +0 -0
  26. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/locales/zh_tw_common.py +0 -0
  27. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/locales/zh_tw_config.py +0 -0
  28. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/locales/zh_tw_picker.py +0 -0
  29. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/ops.py +0 -0
  30. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/output.py +0 -0
  31. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/profiles.py +0 -0
  32. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/settings.py +0 -0
  33. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/shims/claude.md.tmpl +0 -0
  34. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/shims/generic.md.tmpl +0 -0
  35. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/spinner.py +0 -0
  36. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/store.py +0 -0
  37. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/toml_check.py +0 -0
  38. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/ui/__init__.py +0 -0
  39. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/ui/config_menu.py +0 -0
  40. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/ui/menu.py +0 -0
  41. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/ui/model.py +0 -0
  42. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/ui/picker.py +0 -0
  43. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/ui/theme.py +0 -0
  44. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/undo.py +0 -0
  45. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/update_check.py +0 -0
  46. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle/update_prompt.py +0 -0
  47. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle.egg-info/SOURCES.txt +0 -0
  48. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle.egg-info/dependency_links.txt +0 -0
  49. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle.egg-info/entry_points.txt +0 -0
  50. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle.egg-info/requires.txt +0 -0
  51. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/agent_toggle.egg-info/top_level.txt +0 -0
  52. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/pyproject.toml +0 -0
  53. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/setup.cfg +0 -0
  54. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_alias.py +0 -0
  55. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_backends.py +0 -0
  56. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_cli.py +0 -0
  57. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_cli_surface.py +0 -0
  58. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_color.py +0 -0
  59. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_companions.py +0 -0
  60. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_completions.py +0 -0
  61. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_config.py +0 -0
  62. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_config_menu.py +0 -0
  63. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_conformance.py +0 -0
  64. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_containment.py +0 -0
  65. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_cost.py +0 -0
  66. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_crash.py +0 -0
  67. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_doctor.py +0 -0
  68. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_encoding.py +0 -0
  69. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_harnesses.py +0 -0
  70. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_help.py +0 -0
  71. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_i18n.py +0 -0
  72. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_install.py +0 -0
  73. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_mechanisms.py +0 -0
  74. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_menu.py +0 -0
  75. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_picker.py +0 -0
  76. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_platform.py +0 -0
  77. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_profiles.py +0 -0
  78. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_project.py +0 -0
  79. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_safety.py +0 -0
  80. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_secret_audit.py +0 -0
  81. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_settings.py +0 -0
  82. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_theme.py +0 -0
  83. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_undo.py +0 -0
  84. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/tests/test_update_check.py +0 -0
  85. {agent_toggle-0.7.0 → agent_toggle-0.7.2}/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.0
3
+ Version: 0.7.2
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
@@ -1,3 +1,3 @@
1
1
  """agent-toggle: temporarily disable / re-enable AI-agent resources."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.7.2"
@@ -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(os.readlink(src), tmp)
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-toggle
3
- Version: 0.7.0
3
+ Version: 0.7.2
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
@@ -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
 
@@ -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