tweek 0.3.1__py3-none-any.whl → 0.4.1__py3-none-any.whl
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.
- tweek/__init__.py +2 -2
- tweek/audit.py +2 -2
- tweek/cli.py +78 -6605
- tweek/cli_config.py +643 -0
- tweek/cli_configure.py +413 -0
- tweek/cli_core.py +718 -0
- tweek/cli_dry_run.py +390 -0
- tweek/cli_helpers.py +316 -0
- tweek/cli_install.py +1666 -0
- tweek/cli_logs.py +301 -0
- tweek/cli_mcp.py +148 -0
- tweek/cli_memory.py +343 -0
- tweek/cli_plugins.py +748 -0
- tweek/cli_protect.py +564 -0
- tweek/cli_proxy.py +405 -0
- tweek/cli_security.py +236 -0
- tweek/cli_skills.py +289 -0
- tweek/cli_uninstall.py +551 -0
- tweek/cli_vault.py +313 -0
- tweek/config/allowed_dirs.yaml +16 -17
- tweek/config/families.yaml +4 -1
- tweek/config/manager.py +17 -0
- tweek/config/patterns.yaml +29 -5
- tweek/config/templates/config.yaml.template +212 -0
- tweek/config/templates/env.template +45 -0
- tweek/config/templates/overrides.yaml.template +121 -0
- tweek/config/templates/tweek.yaml.template +20 -0
- tweek/config/templates.py +136 -0
- tweek/config/tiers.yaml +5 -4
- tweek/diagnostics.py +112 -32
- tweek/hooks/overrides.py +4 -0
- tweek/hooks/post_tool_use.py +46 -1
- tweek/hooks/pre_tool_use.py +149 -49
- tweek/integrations/openclaw.py +84 -0
- tweek/licensing.py +1 -1
- tweek/mcp/__init__.py +7 -9
- tweek/mcp/clients/chatgpt.py +2 -2
- tweek/mcp/clients/claude_desktop.py +2 -2
- tweek/mcp/clients/gemini.py +2 -2
- tweek/mcp/proxy.py +165 -1
- tweek/memory/provenance.py +438 -0
- tweek/memory/queries.py +2 -0
- tweek/memory/safety.py +23 -4
- tweek/memory/schemas.py +1 -0
- tweek/memory/store.py +101 -71
- tweek/plugins/screening/heuristic_scorer.py +1 -1
- tweek/security/integrity.py +77 -0
- tweek/security/llm_reviewer.py +170 -74
- tweek/security/local_reviewer.py +44 -2
- tweek/security/model_registry.py +73 -7
- tweek/skill_template/overrides-reference.md +1 -1
- tweek/skills/context.py +221 -0
- tweek/skills/scanner.py +2 -2
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/METADATA +8 -7
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/RECORD +60 -38
- tweek/mcp/server.py +0 -320
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/WHEEL +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/entry_points.txt +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/licenses/NOTICE +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/top_level.txt +0 -0
tweek/cli_uninstall.py
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tweek CLI Uninstall Command
|
|
4
|
+
|
|
5
|
+
Full removal of Tweek from the system:
|
|
6
|
+
tweek uninstall Interactive full removal
|
|
7
|
+
tweek uninstall --all Remove ALL Tweek data system-wide
|
|
8
|
+
tweek uninstall --all --confirm Remove everything without prompts
|
|
9
|
+
|
|
10
|
+
IMPORTANT: Users MUST run ``tweek unprotect --all`` (or ``tweek uninstall``)
|
|
11
|
+
BEFORE removing the pip package with ``pip uninstall tweek``. If the package
|
|
12
|
+
is removed first, the Claude Code hooks in ~/.claude/settings.json become
|
|
13
|
+
orphaned — they still reference the hook scripts on disk but the ``tweek``
|
|
14
|
+
CLI is no longer available to clean them up. Orphaned hooks cause spurious
|
|
15
|
+
security warnings in every Claude Code session.
|
|
16
|
+
|
|
17
|
+
To manually clean up orphaned hooks, remove the ``PreToolUse`` and
|
|
18
|
+
``PostToolUse`` entries that reference ``tweek`` from:
|
|
19
|
+
~/.claude/settings.json (global hooks)
|
|
20
|
+
<project>/.claude/settings.json (project-level hooks)
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import click
|
|
25
|
+
import json
|
|
26
|
+
import shutil
|
|
27
|
+
import subprocess
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
from tweek.cli_helpers import (
|
|
32
|
+
console,
|
|
33
|
+
TWEEK_BANNER,
|
|
34
|
+
_has_tweek_hooks,
|
|
35
|
+
_has_tweek_at,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# =============================================================================
|
|
40
|
+
# UNINSTALL COMMAND
|
|
41
|
+
# =============================================================================
|
|
42
|
+
|
|
43
|
+
@click.command(
|
|
44
|
+
epilog="""\b
|
|
45
|
+
Examples:
|
|
46
|
+
tweek uninstall Interactive full removal
|
|
47
|
+
tweek uninstall --all Remove ALL Tweek data system-wide
|
|
48
|
+
tweek uninstall --all --confirm Remove everything without prompts
|
|
49
|
+
"""
|
|
50
|
+
)
|
|
51
|
+
@click.option("--all", "remove_all", is_flag=True, default=False,
|
|
52
|
+
help="Remove ALL Tweek data: hooks, skills, config, patterns, logs, MCP integrations")
|
|
53
|
+
@click.option("--confirm", is_flag=True, help="Skip confirmation prompts")
|
|
54
|
+
def uninstall(remove_all: bool, confirm: bool):
|
|
55
|
+
"""Fully remove Tweek from your system.
|
|
56
|
+
|
|
57
|
+
Removes all hooks, skills, configuration, data, and optionally
|
|
58
|
+
the Tweek package itself. For removing protection from a single
|
|
59
|
+
tool without uninstalling, use `tweek unprotect` instead.
|
|
60
|
+
|
|
61
|
+
This command can only be run from an interactive terminal.
|
|
62
|
+
AI agents are blocked from running it.
|
|
63
|
+
"""
|
|
64
|
+
# ─────────────────────────────────────────────────────────────
|
|
65
|
+
# HUMAN-ONLY GATE: Block non-interactive execution
|
|
66
|
+
# ─────────────────────────────────────────────────────────────
|
|
67
|
+
if not sys.stdin.isatty():
|
|
68
|
+
console.print("[red]ERROR: tweek uninstall must be run from an interactive terminal.[/red]")
|
|
69
|
+
console.print("[white]This command cannot be run by AI agents or automated scripts.[/white]")
|
|
70
|
+
console.print("[white]Open a terminal and run the command directly.[/white]")
|
|
71
|
+
raise SystemExit(1)
|
|
72
|
+
|
|
73
|
+
console.print(TWEEK_BANNER, style="cyan")
|
|
74
|
+
|
|
75
|
+
tweek_dir = Path("~/.tweek").expanduser()
|
|
76
|
+
global_target = Path("~/.claude").expanduser()
|
|
77
|
+
project_target = Path.cwd() / ".claude"
|
|
78
|
+
|
|
79
|
+
if not remove_all:
|
|
80
|
+
# Interactive: ask what to remove
|
|
81
|
+
console.print("[bold]What would you like to remove?[/bold]")
|
|
82
|
+
console.print()
|
|
83
|
+
console.print(" [bold]1.[/bold] Everything (all hooks, data, config, and package)")
|
|
84
|
+
console.print(" [bold]2.[/bold] Cancel")
|
|
85
|
+
console.print()
|
|
86
|
+
choice = click.prompt("Select", type=click.IntRange(1, 2), default=2)
|
|
87
|
+
if choice == 2:
|
|
88
|
+
console.print("[white]Cancelled[/white]")
|
|
89
|
+
return
|
|
90
|
+
console.print()
|
|
91
|
+
|
|
92
|
+
_uninstall_everything(global_target, project_target, tweek_dir, confirm)
|
|
93
|
+
_show_package_removal_hint()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# =============================================================================
|
|
97
|
+
# UNINSTALL HELPERS
|
|
98
|
+
# =============================================================================
|
|
99
|
+
|
|
100
|
+
def _detect_all_package_managers() -> list:
|
|
101
|
+
"""Detect all places tweek is installed. Returns list of uninstall commands."""
|
|
102
|
+
found = []
|
|
103
|
+
|
|
104
|
+
# Check pipx
|
|
105
|
+
try:
|
|
106
|
+
result = subprocess.run(
|
|
107
|
+
["pipx", "list"], capture_output=True, text=True, timeout=5
|
|
108
|
+
)
|
|
109
|
+
if "tweek" in result.stdout:
|
|
110
|
+
found.append("pipx uninstall tweek")
|
|
111
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
# Check uv
|
|
115
|
+
try:
|
|
116
|
+
result = subprocess.run(
|
|
117
|
+
["uv", "tool", "list"], capture_output=True, text=True, timeout=5
|
|
118
|
+
)
|
|
119
|
+
if "tweek" in result.stdout:
|
|
120
|
+
found.append("uv tool uninstall tweek")
|
|
121
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
# Check pip
|
|
125
|
+
try:
|
|
126
|
+
result = subprocess.run(
|
|
127
|
+
[sys.executable, "-m", "pip", "show", "tweek"],
|
|
128
|
+
capture_output=True, text=True, timeout=5
|
|
129
|
+
)
|
|
130
|
+
if result.returncode == 0:
|
|
131
|
+
found.append("pip uninstall tweek -y")
|
|
132
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
return found
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _show_package_removal_hint():
|
|
139
|
+
"""Offer to remove all tweek CLI package installations for the user."""
|
|
140
|
+
pkg_cmds = _detect_all_package_managers()
|
|
141
|
+
if not pkg_cmds:
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
console.print()
|
|
145
|
+
console.print("[bold yellow]The tweek CLI binary is still installed on your system.[/bold yellow]")
|
|
146
|
+
|
|
147
|
+
if len(pkg_cmds) > 1:
|
|
148
|
+
console.print(f"[white]Found {len(pkg_cmds)} installations:[/white]")
|
|
149
|
+
for cmd in pkg_cmds:
|
|
150
|
+
console.print(f" [white]\u2022 {cmd}[/white]")
|
|
151
|
+
|
|
152
|
+
console.print()
|
|
153
|
+
label = " + ".join(f"[bold]{cmd}[/bold]" for cmd in pkg_cmds)
|
|
154
|
+
console.print(f" [bold]1.[/bold] Remove all now ({label})")
|
|
155
|
+
console.print(f" [bold]2.[/bold] Keep (you can remove later)")
|
|
156
|
+
console.print()
|
|
157
|
+
choice = click.prompt("Select", type=click.IntRange(1, 2), default=2)
|
|
158
|
+
|
|
159
|
+
if choice == 1:
|
|
160
|
+
for pkg_cmd in pkg_cmds:
|
|
161
|
+
console.print()
|
|
162
|
+
console.print(f"[cyan]Running:[/cyan] {pkg_cmd}")
|
|
163
|
+
try:
|
|
164
|
+
result = subprocess.run(
|
|
165
|
+
pkg_cmd.split(), capture_output=True, text=True, timeout=30
|
|
166
|
+
)
|
|
167
|
+
if result.returncode == 0:
|
|
168
|
+
console.print(f"[green]\u2713[/green] Removed ({pkg_cmd})")
|
|
169
|
+
else:
|
|
170
|
+
console.print(f"[red]\u2717[/red] Failed: {result.stderr.strip()}")
|
|
171
|
+
console.print(f" [white]Run manually: {pkg_cmd}[/white]")
|
|
172
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
|
173
|
+
console.print(f"[red]\u2717[/red] Could not run: {e}")
|
|
174
|
+
console.print(f" [white]Run manually: {pkg_cmd}[/white]")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _remove_hooks_from_settings(settings_file: Path) -> list:
|
|
178
|
+
"""Remove Tweek hooks from a settings.json file.
|
|
179
|
+
|
|
180
|
+
Returns list of hook types removed (e.g. ['PreToolUse', 'PostToolUse']).
|
|
181
|
+
"""
|
|
182
|
+
removed = []
|
|
183
|
+
|
|
184
|
+
if not settings_file.exists():
|
|
185
|
+
return removed
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
with open(settings_file) as f:
|
|
189
|
+
settings = json.load(f)
|
|
190
|
+
except (json.JSONDecodeError, IOError):
|
|
191
|
+
return removed
|
|
192
|
+
|
|
193
|
+
if not _has_tweek_hooks(settings):
|
|
194
|
+
return removed
|
|
195
|
+
|
|
196
|
+
for hook_type in ("PreToolUse", "PostToolUse"):
|
|
197
|
+
if "hooks" not in settings or hook_type not in settings["hooks"]:
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
tool_hooks = settings["hooks"][hook_type]
|
|
201
|
+
filtered_hooks = []
|
|
202
|
+
for hook_config in tool_hooks:
|
|
203
|
+
filtered_inner = []
|
|
204
|
+
for hook in hook_config.get("hooks", []):
|
|
205
|
+
if "tweek" not in hook.get("command", "").lower():
|
|
206
|
+
filtered_inner.append(hook)
|
|
207
|
+
if filtered_inner:
|
|
208
|
+
hook_config["hooks"] = filtered_inner
|
|
209
|
+
filtered_hooks.append(hook_config)
|
|
210
|
+
|
|
211
|
+
if filtered_hooks:
|
|
212
|
+
settings["hooks"][hook_type] = filtered_hooks
|
|
213
|
+
else:
|
|
214
|
+
del settings["hooks"][hook_type]
|
|
215
|
+
removed.append(hook_type)
|
|
216
|
+
|
|
217
|
+
# Clean up empty hooks dict
|
|
218
|
+
if "hooks" in settings and not settings["hooks"]:
|
|
219
|
+
del settings["hooks"]
|
|
220
|
+
|
|
221
|
+
with open(settings_file, "w") as f:
|
|
222
|
+
json.dump(settings, f, indent=2)
|
|
223
|
+
|
|
224
|
+
return removed
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _remove_skill_directory(target: Path) -> bool:
|
|
228
|
+
"""Remove the Tweek skill directory from a .claude/ target. Returns True if removed."""
|
|
229
|
+
skill_dir = target / "skills" / "tweek"
|
|
230
|
+
if skill_dir.exists() and skill_dir.is_dir():
|
|
231
|
+
shutil.rmtree(skill_dir)
|
|
232
|
+
return True
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _remove_backup_file(target: Path) -> bool:
|
|
237
|
+
"""Remove the settings.json.tweek-backup file. Returns True if removed."""
|
|
238
|
+
backup = target / "settings.json.tweek-backup"
|
|
239
|
+
if backup.exists():
|
|
240
|
+
backup.unlink()
|
|
241
|
+
return True
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _remove_whitelist_entries(target: Path, tweek_dir: Path) -> int:
|
|
246
|
+
"""Remove whitelist entries for target path from overrides.yaml. Returns count removed."""
|
|
247
|
+
import yaml
|
|
248
|
+
|
|
249
|
+
overrides_path = tweek_dir / "overrides.yaml"
|
|
250
|
+
if not overrides_path.exists():
|
|
251
|
+
return 0
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
with open(overrides_path) as f:
|
|
255
|
+
data = yaml.safe_load(f) or {}
|
|
256
|
+
except (yaml.YAMLError, IOError):
|
|
257
|
+
return 0
|
|
258
|
+
|
|
259
|
+
whitelist = data.get("whitelist", [])
|
|
260
|
+
if not whitelist:
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
target_str = str(target.resolve())
|
|
264
|
+
original_count = len(whitelist)
|
|
265
|
+
|
|
266
|
+
data["whitelist"] = [
|
|
267
|
+
entry for entry in whitelist
|
|
268
|
+
if not (isinstance(entry, dict) and
|
|
269
|
+
str(Path(entry.get("path", "")).resolve()).startswith(target_str))
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
removed_count = original_count - len(data["whitelist"])
|
|
273
|
+
if removed_count > 0:
|
|
274
|
+
with open(overrides_path, "w") as f:
|
|
275
|
+
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
|
276
|
+
|
|
277
|
+
return removed_count
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _remove_tweek_data_dir(tweek_dir: Path) -> list:
|
|
281
|
+
"""Remove ~/.tweek/ directory and all contents. Returns list of items removed."""
|
|
282
|
+
removed = []
|
|
283
|
+
|
|
284
|
+
if not tweek_dir.exists():
|
|
285
|
+
return removed
|
|
286
|
+
|
|
287
|
+
# Remove known items with individual feedback
|
|
288
|
+
items = [
|
|
289
|
+
("config.yaml", "configuration"),
|
|
290
|
+
("overrides.yaml", "security overrides"),
|
|
291
|
+
("security.db", "security log database"),
|
|
292
|
+
]
|
|
293
|
+
for filename, label in items:
|
|
294
|
+
filepath = tweek_dir / filename
|
|
295
|
+
if filepath.exists():
|
|
296
|
+
filepath.unlink()
|
|
297
|
+
removed.append(label)
|
|
298
|
+
|
|
299
|
+
dirs = [
|
|
300
|
+
("patterns", "pattern repository"),
|
|
301
|
+
("chamber", "skill isolation chamber"),
|
|
302
|
+
("jail", "skill jail"),
|
|
303
|
+
("skills", "managed skills"),
|
|
304
|
+
]
|
|
305
|
+
for dirname, label in dirs:
|
|
306
|
+
dirpath = tweek_dir / dirname
|
|
307
|
+
if dirpath.exists() and dirpath.is_dir():
|
|
308
|
+
shutil.rmtree(dirpath)
|
|
309
|
+
removed.append(label)
|
|
310
|
+
|
|
311
|
+
# Remove any remaining files
|
|
312
|
+
remaining = list(tweek_dir.iterdir()) if tweek_dir.exists() else []
|
|
313
|
+
for item in remaining:
|
|
314
|
+
if item.is_dir():
|
|
315
|
+
shutil.rmtree(item)
|
|
316
|
+
else:
|
|
317
|
+
item.unlink()
|
|
318
|
+
|
|
319
|
+
# Remove the directory itself
|
|
320
|
+
if tweek_dir.exists():
|
|
321
|
+
try:
|
|
322
|
+
tweek_dir.rmdir()
|
|
323
|
+
removed.append("data directory (~/.tweek/)")
|
|
324
|
+
except OSError:
|
|
325
|
+
# Not empty for some reason
|
|
326
|
+
shutil.rmtree(tweek_dir, ignore_errors=True)
|
|
327
|
+
removed.append("data directory (~/.tweek/)")
|
|
328
|
+
|
|
329
|
+
return removed
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _remove_mcp_integrations() -> list:
|
|
333
|
+
"""Remove MCP integrations for all known clients. Returns list of clients removed."""
|
|
334
|
+
removed = []
|
|
335
|
+
clients = {
|
|
336
|
+
"claude-desktop": Path("~/Library/Application Support/Claude/claude_desktop_config.json").expanduser(),
|
|
337
|
+
"chatgpt": Path("~/Library/Application Support/com.openai.chat/config.json").expanduser(),
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
for client_name, config_path in clients.items():
|
|
341
|
+
if not config_path.exists():
|
|
342
|
+
continue
|
|
343
|
+
try:
|
|
344
|
+
with open(config_path) as f:
|
|
345
|
+
config = json.load(f)
|
|
346
|
+
mcp_servers = config.get("mcpServers", {})
|
|
347
|
+
tweek_keys = [k for k in mcp_servers if "tweek" in k.lower()]
|
|
348
|
+
if tweek_keys:
|
|
349
|
+
for key in tweek_keys:
|
|
350
|
+
del mcp_servers[key]
|
|
351
|
+
with open(config_path, "w") as f:
|
|
352
|
+
json.dump(config, f, indent=2)
|
|
353
|
+
removed.append(client_name)
|
|
354
|
+
except (json.JSONDecodeError, IOError, KeyError):
|
|
355
|
+
continue
|
|
356
|
+
|
|
357
|
+
return removed
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _uninstall_scope(target: Path, tweek_dir: Path, confirm: bool, scope_label: str):
|
|
361
|
+
"""Uninstall Tweek from a single scope (project or global)."""
|
|
362
|
+
settings_file = target / "settings.json"
|
|
363
|
+
has_hooks = False
|
|
364
|
+
has_skills = (target / "skills" / "tweek").exists()
|
|
365
|
+
has_backup = (target / "settings.json.tweek-backup").exists()
|
|
366
|
+
|
|
367
|
+
if settings_file.exists():
|
|
368
|
+
try:
|
|
369
|
+
with open(settings_file) as f:
|
|
370
|
+
settings = json.load(f)
|
|
371
|
+
has_hooks = _has_tweek_hooks(settings)
|
|
372
|
+
except (json.JSONDecodeError, IOError):
|
|
373
|
+
pass
|
|
374
|
+
|
|
375
|
+
if not has_hooks and not has_skills and not has_backup:
|
|
376
|
+
console.print(f"[yellow]No Tweek installation found at {target}[/yellow]")
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
console.print(f"[bold]Found Tweek installation at:[/bold] {target}")
|
|
380
|
+
console.print()
|
|
381
|
+
console.print("[bold]The following will be removed:[/bold]")
|
|
382
|
+
if has_hooks:
|
|
383
|
+
console.print(" [white]\u2022[/white] PreToolUse and PostToolUse hooks from settings.json")
|
|
384
|
+
if has_skills:
|
|
385
|
+
console.print(" [white]\u2022[/white] Tweek skill directory (skills/tweek/)")
|
|
386
|
+
if has_backup:
|
|
387
|
+
console.print(" [white]\u2022[/white] Backup file (settings.json.tweek-backup)")
|
|
388
|
+
console.print(" [white]\u2022[/white] Project whitelist entries from overrides")
|
|
389
|
+
console.print()
|
|
390
|
+
|
|
391
|
+
if not confirm:
|
|
392
|
+
console.print(f"[yellow]Remove Tweek from this {scope_label}?[/yellow] ", end="")
|
|
393
|
+
if not click.confirm(""):
|
|
394
|
+
console.print("[white]Cancelled[/white]")
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
console.print()
|
|
398
|
+
|
|
399
|
+
# 1. Remove hooks
|
|
400
|
+
removed_hooks = _remove_hooks_from_settings(settings_file)
|
|
401
|
+
for hook_type in removed_hooks:
|
|
402
|
+
console.print(f" [green]\u2713[/green] Removed {hook_type} hook from settings.json")
|
|
403
|
+
if has_hooks and not removed_hooks:
|
|
404
|
+
console.print(f" [red]\u2717[/red] Failed to remove hooks from settings.json")
|
|
405
|
+
|
|
406
|
+
# 2. Remove skill directory
|
|
407
|
+
if _remove_skill_directory(target):
|
|
408
|
+
console.print(f" [green]\u2713[/green] Removed Tweek skill directory (skills/tweek/)")
|
|
409
|
+
else:
|
|
410
|
+
console.print(f" [white]-[/white] Skipped: Tweek skill directory not found")
|
|
411
|
+
|
|
412
|
+
# 3. Remove backup file
|
|
413
|
+
if _remove_backup_file(target):
|
|
414
|
+
console.print(f" [green]\u2713[/green] Removed backup file (settings.json.tweek-backup)")
|
|
415
|
+
else:
|
|
416
|
+
console.print(f" [white]-[/white] Skipped: no backup file found")
|
|
417
|
+
|
|
418
|
+
# 4. Remove whitelist entries
|
|
419
|
+
wl_count = _remove_whitelist_entries(target, tweek_dir)
|
|
420
|
+
if wl_count > 0:
|
|
421
|
+
console.print(f" [green]\u2713[/green] Removed {wl_count} whitelist entry(s) from overrides")
|
|
422
|
+
else:
|
|
423
|
+
console.print(f" [white]-[/white] Skipped: no whitelist entries found for this {scope_label}")
|
|
424
|
+
|
|
425
|
+
console.print()
|
|
426
|
+
console.print(f"[green]Uninstall complete.[/green] Tweek is no longer active for this {scope_label}.")
|
|
427
|
+
if scope_label == "project":
|
|
428
|
+
console.print("[white]Global installation (~/.claude/) was not affected.[/white]")
|
|
429
|
+
else:
|
|
430
|
+
console.print("[white]Project installations were not affected.[/white]")
|
|
431
|
+
|
|
432
|
+
# Offer to remove data directory
|
|
433
|
+
if tweek_dir.exists() and not confirm:
|
|
434
|
+
# Check if the OTHER scope still has tweek installed
|
|
435
|
+
global_target = Path("~/.claude").expanduser()
|
|
436
|
+
project_target = Path.cwd() / ".claude"
|
|
437
|
+
other_target = global_target if scope_label == "project" else project_target
|
|
438
|
+
other_label = "global" if scope_label == "project" else "project"
|
|
439
|
+
other_has_tweek = _has_tweek_at(other_target)
|
|
440
|
+
|
|
441
|
+
console.print()
|
|
442
|
+
console.print("[yellow]Also remove Tweek data directory (~/.tweek/)?[/yellow]")
|
|
443
|
+
console.print("[white]This contains config, patterns, security logs, and overrides.[/white]")
|
|
444
|
+
if other_has_tweek:
|
|
445
|
+
console.print(f"[bold red]Warning:[/bold red] Tweek is still installed at {other_label} scope ({other_target}).")
|
|
446
|
+
console.print(f" Removing ~/.tweek/ will affect that installation (no config, patterns, or logs).")
|
|
447
|
+
console.print()
|
|
448
|
+
console.print(f" [bold]1.[/bold] Keep data (recommended)" if other_has_tweek else f" [bold]1.[/bold] Keep data (can reinstall later without re-downloading patterns)")
|
|
449
|
+
console.print(f" [bold]2.[/bold] Remove data (~/.tweek/)")
|
|
450
|
+
console.print()
|
|
451
|
+
remove_choice = click.prompt("Select", type=click.IntRange(1, 2), default=1)
|
|
452
|
+
if remove_choice == 2:
|
|
453
|
+
console.print()
|
|
454
|
+
data_removed = _remove_tweek_data_dir(tweek_dir)
|
|
455
|
+
for item in data_removed:
|
|
456
|
+
console.print(f" [green]\u2713[/green] Removed {item}")
|
|
457
|
+
if not data_removed:
|
|
458
|
+
console.print(f" [white]-[/white] No data to remove")
|
|
459
|
+
elif tweek_dir.exists():
|
|
460
|
+
console.print("[white]Tweek data directory (~/.tweek/) was preserved.[/white]")
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir: Path, confirm: bool):
|
|
464
|
+
"""Full system removal of all Tweek data."""
|
|
465
|
+
console.print("[bold yellow]FULL REMOVAL[/bold yellow] \u2014 This will remove ALL Tweek data:\n")
|
|
466
|
+
console.print(" [white]\u2022[/white] Hooks from current project (.claude/settings.json)")
|
|
467
|
+
console.print(" [white]\u2022[/white] Hooks from global installation (~/.claude/settings.json)")
|
|
468
|
+
console.print(" [white]\u2022[/white] Tweek skill directories (project + global)")
|
|
469
|
+
console.print(" [white]\u2022[/white] All backup files")
|
|
470
|
+
console.print(" [white]\u2022[/white] Tweek data directory (~/.tweek/)")
|
|
471
|
+
|
|
472
|
+
# Show what exists in ~/.tweek/
|
|
473
|
+
if tweek_dir.exists():
|
|
474
|
+
for item in sorted(tweek_dir.iterdir()):
|
|
475
|
+
if item.is_dir():
|
|
476
|
+
console.print(f" [white]\u251c\u2500\u2500 {item.name}/ [/white]")
|
|
477
|
+
else:
|
|
478
|
+
console.print(f" [white]\u251c\u2500\u2500 {item.name}[/white]")
|
|
479
|
+
|
|
480
|
+
console.print(" [white]\u2022[/white] MCP integrations (Claude Desktop, ChatGPT)")
|
|
481
|
+
console.print()
|
|
482
|
+
|
|
483
|
+
if not confirm:
|
|
484
|
+
console.print("[bold red]Type 'yes' to confirm full removal[/bold red]: ", end="")
|
|
485
|
+
response = input()
|
|
486
|
+
if response.strip().lower() != "yes":
|
|
487
|
+
console.print("[white]Cancelled[/white]")
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
console.print()
|
|
491
|
+
|
|
492
|
+
# ── Project scope ──
|
|
493
|
+
console.print("[bold]Project scope (.claude/):[/bold]")
|
|
494
|
+
removed_hooks = _remove_hooks_from_settings(project_target / "settings.json")
|
|
495
|
+
for hook_type in removed_hooks:
|
|
496
|
+
console.print(f" [green]\u2713[/green] Removed {hook_type} hook from project settings.json")
|
|
497
|
+
if not removed_hooks:
|
|
498
|
+
console.print(f" [white]-[/white] Skipped: no project hooks found")
|
|
499
|
+
|
|
500
|
+
if _remove_skill_directory(project_target):
|
|
501
|
+
console.print(f" [green]\u2713[/green] Removed Tweek skill from project")
|
|
502
|
+
else:
|
|
503
|
+
console.print(f" [white]-[/white] Skipped: no project skill directory")
|
|
504
|
+
|
|
505
|
+
if _remove_backup_file(project_target):
|
|
506
|
+
console.print(f" [green]\u2713[/green] Removed project backup file")
|
|
507
|
+
else:
|
|
508
|
+
console.print(f" [white]-[/white] Skipped: no project backup file")
|
|
509
|
+
|
|
510
|
+
console.print()
|
|
511
|
+
|
|
512
|
+
# ── Global scope ──
|
|
513
|
+
console.print("[bold]Global scope (~/.claude/):[/bold]")
|
|
514
|
+
removed_hooks = _remove_hooks_from_settings(global_target / "settings.json")
|
|
515
|
+
for hook_type in removed_hooks:
|
|
516
|
+
console.print(f" [green]\u2713[/green] Removed {hook_type} hook from global settings.json")
|
|
517
|
+
if not removed_hooks:
|
|
518
|
+
console.print(f" [white]-[/white] Skipped: no global hooks found")
|
|
519
|
+
|
|
520
|
+
if _remove_skill_directory(global_target):
|
|
521
|
+
console.print(f" [green]\u2713[/green] Removed Tweek skill from global installation")
|
|
522
|
+
else:
|
|
523
|
+
console.print(f" [white]-[/white] Skipped: no global skill directory")
|
|
524
|
+
|
|
525
|
+
if _remove_backup_file(global_target):
|
|
526
|
+
console.print(f" [green]\u2713[/green] Removed global backup file")
|
|
527
|
+
else:
|
|
528
|
+
console.print(f" [white]-[/white] Skipped: no global backup file")
|
|
529
|
+
|
|
530
|
+
console.print()
|
|
531
|
+
|
|
532
|
+
# ── Tweek data directory ──
|
|
533
|
+
console.print("[bold]Tweek data (~/.tweek/):[/bold]")
|
|
534
|
+
data_removed = _remove_tweek_data_dir(tweek_dir)
|
|
535
|
+
for item in data_removed:
|
|
536
|
+
console.print(f" [green]\u2713[/green] Removed {item}")
|
|
537
|
+
if not data_removed:
|
|
538
|
+
console.print(f" [white]-[/white] Skipped: no data directory found")
|
|
539
|
+
|
|
540
|
+
console.print()
|
|
541
|
+
|
|
542
|
+
# ── MCP integrations ──
|
|
543
|
+
console.print("[bold]MCP integrations:[/bold]")
|
|
544
|
+
mcp_removed = _remove_mcp_integrations()
|
|
545
|
+
for client in mcp_removed:
|
|
546
|
+
console.print(f" [green]\u2713[/green] Removed {client} MCP integration")
|
|
547
|
+
if not mcp_removed:
|
|
548
|
+
console.print(f" [white]-[/white] Skipped: no MCP integrations found")
|
|
549
|
+
|
|
550
|
+
console.print()
|
|
551
|
+
console.print("[green]All Tweek data has been removed.[/green]")
|