-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathuninstall.py
More file actions
executable file
·176 lines (149 loc) · 5.83 KB
/
Copy pathuninstall.py
File metadata and controls
executable file
·176 lines (149 loc) · 5.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#!/usr/bin/env python3
"""Cross-platform uninstaller for Fable mode (Windows / macOS / Linux).
python uninstall.py # Windows
python3 uninstall.py # macOS / Linux
Reverses install.py: removes the files it copied into ~/.claude, strips the
launcher line from your shell rc / PowerShell $PROFILE, and removes the Fable
hook entries from settings.json (writing a fresh backup first).
Conservative on purpose:
- Only removes the skill directories this repo bundles (carrying our marker),
never your other skills.
- Leaves ~/.claude itself and "alwaysThinkingEnabled" untouched (toggle that in
/config if you want it off — it may predate the install).
"""
import os
import sys
import json
import shutil
REPO = os.path.dirname(os.path.abspath(__file__))
HOME = os.path.expanduser("~")
CLAUDE = os.path.join(HOME, ".claude")
IS_WINDOWS = os.name == "nt"
BUNDLED_MARKER = ".fable-mode-bundled"
def rm_file(path):
if os.path.isfile(path):
os.remove(path)
print(" removed " + path)
def rm_skill(path):
"""Remove a skill directory only if install.py marked it as ours. A
same-named directory without the marker is the user's own work."""
if not os.path.isdir(path):
return
if os.path.isfile(os.path.join(path, BUNDLED_MARKER)):
shutil.rmtree(path)
print(" removed " + path)
else:
print(" left {} in place (not installed by fable-mode)".format(path))
def bundled_skill_names():
skills_dir = os.path.join(REPO, "skills")
if not os.path.isdir(skills_dir):
return []
return [n for n in os.listdir(skills_dir)
if os.path.isdir(os.path.join(skills_dir, n))]
def strip_launcher(path, marker):
"""Remove the '# Fable mode' comment + the source/dot line referencing `marker`."""
if not os.path.isfile(path):
return
with open(path, encoding="utf-8") as f:
lines = f.readlines()
out, removed = [], False
for line in lines:
if marker in line:
removed = True
# drop a preceding "# Fable mode" comment and a blank line if present
while out and (out[-1].lstrip().startswith("# Fable mode")
or out[-1].strip() == ""):
out.pop()
continue
out.append(line)
if removed:
with open(path, "w", encoding="utf-8") as f:
f.write("".join(out))
print(" removed launcher from " + path)
def powershell_profile_paths():
"""$PROFILE of every PowerShell present — install.py writes to each."""
import subprocess
paths = []
for exe in ("pwsh", "powershell"):
if shutil.which(exe):
try:
out = subprocess.run([exe, "-NoProfile", "-Command", "$PROFILE"],
capture_output=True, text=True, timeout=20)
p = out.stdout.strip()
if p and p not in paths:
paths.append(p)
except Exception:
pass
if not paths:
paths.append(os.path.join(HOME, "Documents", "PowerShell",
"Microsoft.PowerShell_profile.ps1"))
return paths
def remove_launcher():
if IS_WINDOWS:
for profile in powershell_profile_paths():
strip_launcher(profile, "fable.ps1")
else:
for rc in (".zshrc", ".bashrc"):
strip_launcher(os.path.join(HOME, rc), "fable.zsh")
def clean_settings():
path = os.path.join(CLAUDE, "settings.json")
if not os.path.isfile(path):
return
try:
with open(path) as f:
d = json.load(f)
except Exception:
print(" settings.json unreadable — left untouched")
return
shutil.copy(path, path + ".uninstall.bak")
hooks = d.get("hooks", {})
for event in ("SessionStart", "UserPromptSubmit", "PostToolUse"):
arr = hooks.get(event)
if not isinstance(arr, list):
continue
kept = []
for entry in arr:
cmds = " ".join(h.get("command", "")
for h in entry.get("hooks", []))
if "fable-trigger.py" in cmds or "test-after-edit.py" in cmds:
continue # drop the entry we added
kept.append(entry)
if kept:
hooks[event] = kept
else:
hooks.pop(event, None)
if not hooks:
d.pop("hooks", None)
elif "hooks" in d:
d["hooks"] = hooks
with open(path, "w") as f:
json.dump(d, f, indent=2)
print(" removed Fable hooks from settings.json (backup: settings.json.uninstall.bak)")
def main():
print("-> files in ~/.claude")
rm_file(os.path.join(CLAUDE, "hooks", "fable-trigger.py"))
rm_file(os.path.join(CLAUDE, "hooks", "test-after-edit.py"))
rm_file(os.path.join(CLAUDE, "hooks", "fable-doctor.py"))
rm_file(os.path.join(CLAUDE, "FABLE_PLAYBOOK.md"))
rm_file(os.path.join(CLAUDE, "FABLE_CODE.md"))
rm_file(os.path.join(CLAUDE, "fable-system.md")) # pre-rename installs
rm_file(os.path.join(CLAUDE, "fable-code.md")) # pre-rename installs
rm_file(os.path.join(CLAUDE, "shell", "ultracode.settings.json"))
rm_file(os.path.join(CLAUDE, "agents", "grounding-verifier.md"))
rm_file(os.path.join(CLAUDE, "shell", "fable.ps1"))
rm_file(os.path.join(CLAUDE, "shell", "fable.zsh"))
try:
os.rmdir(os.path.join(CLAUDE, "shell"))
except OSError:
pass # not empty or absent — leave it
for name in bundled_skill_names():
rm_skill(os.path.join(CLAUDE, "skills", name))
print("-> launcher")
remove_launcher()
print("-> settings.json")
clean_settings()
print()
print("Done. Fable mode removed. ~/.claude and 'alwaysThinkingEnabled' were kept.")
print("Reload your shell ('. $PROFILE' or 'source ~/.zshrc') to drop the 'fable' command.")
if __name__ == "__main__":
main()