tweek 0.3.0__py3-none-any.whl → 0.4.0__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 -6559
- 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/__init__.py +8 -0
- tweek/config/allowed_dirs.yaml +16 -17
- tweek/config/families.yaml +4 -1
- tweek/config/manager.py +49 -0
- tweek/config/models.py +307 -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 +162 -68
- 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.0.dist-info → tweek-0.4.0.dist-info}/METADATA +9 -7
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/RECORD +62 -39
- tweek/mcp/server.py +0 -320
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/WHEEL +0 -0
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/entry_points.txt +0 -0
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/licenses/NOTICE +0 -0
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/top_level.txt +0 -0
tweek/cli_plugins.py
ADDED
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tweek CLI Plugins Commands
|
|
3
|
+
|
|
4
|
+
Plugin management commands for Tweek: list, info, set, reset, scan,
|
|
5
|
+
install, update, remove, search, lock, verify, and registry operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from tweek.cli_helpers import console
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.group()
|
|
18
|
+
def plugins():
|
|
19
|
+
"""Manage Tweek plugins (compliance, providers, detectors, screening)."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@plugins.command("list",
|
|
24
|
+
epilog="""\b
|
|
25
|
+
Examples:
|
|
26
|
+
tweek plugins list List all enabled plugins
|
|
27
|
+
tweek plugins list --all Include disabled plugins
|
|
28
|
+
tweek plugins list -c compliance Show only compliance plugins
|
|
29
|
+
tweek plugins list -c screening Show only screening plugins
|
|
30
|
+
"""
|
|
31
|
+
)
|
|
32
|
+
@click.option("--category", "-c", type=click.Choice(["compliance", "providers", "detectors", "screening"]),
|
|
33
|
+
help="Filter by plugin category")
|
|
34
|
+
@click.option("--all", "show_all", is_flag=True, help="Show all plugins including disabled")
|
|
35
|
+
def plugins_list(category: str, show_all: bool):
|
|
36
|
+
"""List installed plugins."""
|
|
37
|
+
try:
|
|
38
|
+
from tweek.plugins import get_registry, init_plugins, PluginCategory, LicenseTier
|
|
39
|
+
from tweek.config.manager import ConfigManager
|
|
40
|
+
|
|
41
|
+
init_plugins()
|
|
42
|
+
registry = get_registry()
|
|
43
|
+
cfg = ConfigManager()
|
|
44
|
+
|
|
45
|
+
category_map = {
|
|
46
|
+
"compliance": PluginCategory.COMPLIANCE,
|
|
47
|
+
"providers": PluginCategory.LLM_PROVIDER,
|
|
48
|
+
"detectors": PluginCategory.TOOL_DETECTOR,
|
|
49
|
+
"screening": PluginCategory.SCREENING,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
categories = [category_map[category]] if category else list(PluginCategory)
|
|
53
|
+
|
|
54
|
+
for cat in categories:
|
|
55
|
+
cat_name = cat.value.split(".")[-1]
|
|
56
|
+
plugins_list = registry.list_plugins(cat)
|
|
57
|
+
|
|
58
|
+
if not plugins_list and not show_all:
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
table = Table(title=f"{cat_name.replace('_', ' ').title()} Plugins")
|
|
62
|
+
table.add_column("Name", style="cyan")
|
|
63
|
+
table.add_column("Version")
|
|
64
|
+
table.add_column("Source")
|
|
65
|
+
table.add_column("Enabled")
|
|
66
|
+
table.add_column("License")
|
|
67
|
+
table.add_column("Description", max_width=40)
|
|
68
|
+
|
|
69
|
+
for info in plugins_list:
|
|
70
|
+
if not show_all and not info.enabled:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
# Get config status
|
|
74
|
+
plugin_cfg = cfg.get_plugin_config(cat_name, info.name)
|
|
75
|
+
|
|
76
|
+
license_tier = info.metadata.requires_license
|
|
77
|
+
license_style = "green" if license_tier == LicenseTier.FREE else "cyan"
|
|
78
|
+
|
|
79
|
+
source_str = info.source.value if hasattr(info, 'source') else "builtin"
|
|
80
|
+
source_style = "blue" if source_str == "git" else "white"
|
|
81
|
+
|
|
82
|
+
table.add_row(
|
|
83
|
+
info.name,
|
|
84
|
+
info.metadata.version,
|
|
85
|
+
f"[{source_style}]{source_str}[/{source_style}]",
|
|
86
|
+
"[green]\u2713[/green]" if info.enabled else "[red]\u2717[/red]",
|
|
87
|
+
f"[{license_style}]{license_tier.value}[/{license_style}]",
|
|
88
|
+
info.metadata.description[:40] + "..." if len(info.metadata.description) > 40 else info.metadata.description,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
console.print(table)
|
|
92
|
+
console.print()
|
|
93
|
+
|
|
94
|
+
# Summary line across all categories
|
|
95
|
+
total_count = 0
|
|
96
|
+
enabled_count = 0
|
|
97
|
+
for cat in list(PluginCategory):
|
|
98
|
+
for info in registry.list_plugins(cat):
|
|
99
|
+
total_count += 1
|
|
100
|
+
if info.enabled:
|
|
101
|
+
enabled_count += 1
|
|
102
|
+
disabled_count = total_count - enabled_count
|
|
103
|
+
console.print(f"Plugins: {total_count} registered, {enabled_count} enabled, {disabled_count} disabled")
|
|
104
|
+
console.print()
|
|
105
|
+
|
|
106
|
+
except ImportError as e:
|
|
107
|
+
console.print(f"[red]Plugin system not available: {e}[/red]")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@plugins.command("info",
|
|
111
|
+
epilog="""\b
|
|
112
|
+
Examples:
|
|
113
|
+
tweek plugins info hipaa Show details for the hipaa plugin
|
|
114
|
+
tweek plugins info pii -c compliance Specify category explicitly
|
|
115
|
+
"""
|
|
116
|
+
)
|
|
117
|
+
@click.argument("plugin_name")
|
|
118
|
+
@click.option("--category", "-c", type=click.Choice(["compliance", "providers", "detectors", "screening"]),
|
|
119
|
+
help="Plugin category (auto-detected if not specified)")
|
|
120
|
+
def plugins_info(plugin_name: str, category: str):
|
|
121
|
+
"""Show detailed information about a plugin."""
|
|
122
|
+
try:
|
|
123
|
+
from tweek.plugins import get_registry, init_plugins, PluginCategory
|
|
124
|
+
from tweek.config.manager import ConfigManager
|
|
125
|
+
|
|
126
|
+
init_plugins()
|
|
127
|
+
registry = get_registry()
|
|
128
|
+
cfg = ConfigManager()
|
|
129
|
+
|
|
130
|
+
category_map = {
|
|
131
|
+
"compliance": PluginCategory.COMPLIANCE,
|
|
132
|
+
"providers": PluginCategory.LLM_PROVIDER,
|
|
133
|
+
"detectors": PluginCategory.TOOL_DETECTOR,
|
|
134
|
+
"screening": PluginCategory.SCREENING,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Find the plugin
|
|
138
|
+
found_info = None
|
|
139
|
+
found_cat = None
|
|
140
|
+
|
|
141
|
+
if category:
|
|
142
|
+
cat_enum = category_map[category]
|
|
143
|
+
found_info = registry.get_info(plugin_name, cat_enum)
|
|
144
|
+
found_cat = category
|
|
145
|
+
else:
|
|
146
|
+
# Search all categories
|
|
147
|
+
for cat_name, cat_enum in category_map.items():
|
|
148
|
+
info = registry.get_info(plugin_name, cat_enum)
|
|
149
|
+
if info:
|
|
150
|
+
found_info = info
|
|
151
|
+
found_cat = cat_name
|
|
152
|
+
break
|
|
153
|
+
|
|
154
|
+
if not found_info:
|
|
155
|
+
console.print(f"[red]Plugin not found: {plugin_name}[/red]")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
# Get config
|
|
159
|
+
plugin_cfg = cfg.get_plugin_config(found_cat, plugin_name)
|
|
160
|
+
|
|
161
|
+
console.print(f"\n[bold]{found_info.name}[/bold] ({found_cat})")
|
|
162
|
+
console.print(f"[white]{found_info.metadata.description}[/white]")
|
|
163
|
+
console.print()
|
|
164
|
+
|
|
165
|
+
table = Table(show_header=False)
|
|
166
|
+
table.add_column("Key", style="cyan")
|
|
167
|
+
table.add_column("Value")
|
|
168
|
+
|
|
169
|
+
table.add_row("Version", found_info.metadata.version)
|
|
170
|
+
table.add_row("Author", found_info.metadata.author or "Unknown")
|
|
171
|
+
table.add_row("License Required", found_info.metadata.requires_license.value.upper())
|
|
172
|
+
table.add_row("Enabled", "Yes" if found_info.enabled else "No")
|
|
173
|
+
table.add_row("Config Source", plugin_cfg.source)
|
|
174
|
+
|
|
175
|
+
if found_info.metadata.tags:
|
|
176
|
+
table.add_row("Tags", ", ".join(found_info.metadata.tags))
|
|
177
|
+
|
|
178
|
+
if plugin_cfg.settings:
|
|
179
|
+
table.add_row("Settings", str(plugin_cfg.settings))
|
|
180
|
+
|
|
181
|
+
if found_info.load_error:
|
|
182
|
+
table.add_row("[red]Load Error[/red]", found_info.load_error)
|
|
183
|
+
|
|
184
|
+
console.print(table)
|
|
185
|
+
|
|
186
|
+
except ImportError as e:
|
|
187
|
+
console.print(f"[red]Plugin system not available: {e}[/red]")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@plugins.command("set",
|
|
191
|
+
epilog="""\b
|
|
192
|
+
Examples:
|
|
193
|
+
tweek plugins set hipaa --enabled -c compliance Enable a plugin
|
|
194
|
+
tweek plugins set hipaa --disabled -c compliance Disable a plugin
|
|
195
|
+
tweek plugins set hipaa threshold 0.8 -c compliance Set a config value
|
|
196
|
+
tweek plugins set hipaa --scope-tools Bash,Edit -c compliance Scope to tools
|
|
197
|
+
tweek plugins set hipaa --scope-clear -c compliance Clear scoping
|
|
198
|
+
"""
|
|
199
|
+
)
|
|
200
|
+
@click.argument("plugin_name")
|
|
201
|
+
@click.argument("key", required=False)
|
|
202
|
+
@click.argument("value", required=False)
|
|
203
|
+
@click.option("--category", "-c", type=click.Choice(["compliance", "providers", "detectors", "screening"]),
|
|
204
|
+
required=True, help="Plugin category")
|
|
205
|
+
@click.option("--scope", type=click.Choice(["user", "project"]), default="user")
|
|
206
|
+
@click.option("--enabled", "set_enabled", is_flag=True, help="Enable the plugin")
|
|
207
|
+
@click.option("--disabled", "set_disabled", is_flag=True, help="Disable the plugin")
|
|
208
|
+
@click.option("--scope-tools", help="Comma-separated tool names for scoping")
|
|
209
|
+
@click.option("--scope-skills", help="Comma-separated skill names for scoping")
|
|
210
|
+
@click.option("--scope-tiers", help="Comma-separated tiers for scoping")
|
|
211
|
+
@click.option("--scope-clear", is_flag=True, help="Clear all scoping")
|
|
212
|
+
def plugins_set(plugin_name: str, key: str, value: str, category: str, scope: str,
|
|
213
|
+
set_enabled: bool, set_disabled: bool, scope_tools: str,
|
|
214
|
+
scope_skills: str, scope_tiers: str, scope_clear: bool):
|
|
215
|
+
"""Set a plugin configuration value, enable/disable, or configure scope."""
|
|
216
|
+
from tweek.config.manager import ConfigManager
|
|
217
|
+
import json
|
|
218
|
+
|
|
219
|
+
cfg = ConfigManager()
|
|
220
|
+
|
|
221
|
+
# Handle enable/disable
|
|
222
|
+
if set_enabled:
|
|
223
|
+
cfg.set_plugin_enabled(category, plugin_name, True, scope=scope)
|
|
224
|
+
console.print(f"[green]\u2713[/green] Enabled plugin '{plugin_name}' ({category}) - {scope} config")
|
|
225
|
+
return
|
|
226
|
+
if set_disabled:
|
|
227
|
+
cfg.set_plugin_enabled(category, plugin_name, False, scope=scope)
|
|
228
|
+
console.print(f"[green]\u2713[/green] Disabled plugin '{plugin_name}' ({category}) - {scope} config")
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
# Handle scope configuration
|
|
232
|
+
if scope_clear:
|
|
233
|
+
cfg.set_plugin_scope(plugin_name, None)
|
|
234
|
+
console.print(f"[green]\u2713[/green] Cleared scope for {plugin_name} (now global)")
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
if any([scope_tools, scope_skills, scope_tiers]):
|
|
238
|
+
scope_config = {}
|
|
239
|
+
if scope_tools:
|
|
240
|
+
scope_config["tools"] = [t.strip() for t in scope_tools.split(",")]
|
|
241
|
+
if scope_skills:
|
|
242
|
+
scope_config["skills"] = [s.strip() for s in scope_skills.split(",")]
|
|
243
|
+
if scope_tiers:
|
|
244
|
+
scope_config["tiers"] = [t.strip() for t in scope_tiers.split(",")]
|
|
245
|
+
cfg.set_plugin_scope(plugin_name, scope_config)
|
|
246
|
+
console.print(f"[green]\u2713[/green] Updated scope for {plugin_name}")
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
# Handle key=value setting
|
|
250
|
+
if not key or not value:
|
|
251
|
+
console.print("[red]Specify key and value, or use --enabled/--disabled/--scope-* flags[/red]")
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
# Try to parse value as JSON (for booleans, numbers, objects)
|
|
255
|
+
try:
|
|
256
|
+
parsed_value = json.loads(value)
|
|
257
|
+
except json.JSONDecodeError:
|
|
258
|
+
parsed_value = value
|
|
259
|
+
|
|
260
|
+
cfg.set_plugin_setting(category, plugin_name, key, parsed_value, scope=scope)
|
|
261
|
+
console.print(f"[green]\u2713[/green] Set {plugin_name}.{key} = {parsed_value} ({scope} config)")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@plugins.command("reset",
|
|
265
|
+
epilog="""\b
|
|
266
|
+
Examples:
|
|
267
|
+
tweek plugins reset hipaa -c compliance Reset hipaa plugin to defaults
|
|
268
|
+
tweek plugins reset pii -c compliance --scope project Reset project-level config
|
|
269
|
+
"""
|
|
270
|
+
)
|
|
271
|
+
@click.argument("plugin_name")
|
|
272
|
+
@click.option("--category", "-c", type=click.Choice(["compliance", "providers", "detectors", "screening"]),
|
|
273
|
+
required=True, help="Plugin category")
|
|
274
|
+
@click.option("--scope", type=click.Choice(["user", "project"]), default="user")
|
|
275
|
+
def plugins_reset(plugin_name: str, category: str, scope: str):
|
|
276
|
+
"""Reset a plugin to default configuration."""
|
|
277
|
+
from tweek.config.manager import ConfigManager
|
|
278
|
+
|
|
279
|
+
cfg = ConfigManager()
|
|
280
|
+
|
|
281
|
+
if cfg.reset_plugin(category, plugin_name, scope=scope):
|
|
282
|
+
console.print(f"[green]\u2713[/green] Reset plugin '{plugin_name}' to defaults ({scope} config)")
|
|
283
|
+
else:
|
|
284
|
+
console.print(f"[yellow]![/yellow] Plugin '{plugin_name}' has no {scope} configuration to reset")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@plugins.command("scan",
|
|
288
|
+
epilog="""\b
|
|
289
|
+
Examples:
|
|
290
|
+
tweek plugins scan "This is TOP SECRET//NOFORN" Scan text for compliance
|
|
291
|
+
tweek plugins scan "Patient MRN: 123456" --plugin hipaa Use specific plugin
|
|
292
|
+
tweek plugins scan @file.txt Scan file contents
|
|
293
|
+
tweek plugins scan "SSN: 123-45-6789" -d input Scan incoming data
|
|
294
|
+
"""
|
|
295
|
+
)
|
|
296
|
+
@click.argument("content")
|
|
297
|
+
@click.option("--direction", "-d", type=click.Choice(["input", "output"]), default="output",
|
|
298
|
+
help="Scan direction (input=incoming data, output=LLM response)")
|
|
299
|
+
@click.option("--plugin", "-p", help="Specific compliance plugin to use (default: all enabled)")
|
|
300
|
+
def plugins_scan(content: str, direction: str, plugin: str):
|
|
301
|
+
"""Run compliance scan on content."""
|
|
302
|
+
try:
|
|
303
|
+
from tweek.plugins import get_registry, init_plugins, PluginCategory
|
|
304
|
+
from tweek.plugins.base import ScanDirection
|
|
305
|
+
|
|
306
|
+
# Handle file input
|
|
307
|
+
if content.startswith("@"):
|
|
308
|
+
file_path = Path(content[1:])
|
|
309
|
+
if file_path.exists():
|
|
310
|
+
content = file_path.read_text()
|
|
311
|
+
else:
|
|
312
|
+
console.print(f"[red]File not found: {file_path}[/red]")
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
init_plugins()
|
|
316
|
+
registry = get_registry()
|
|
317
|
+
direction_enum = ScanDirection(direction)
|
|
318
|
+
|
|
319
|
+
total_findings = []
|
|
320
|
+
|
|
321
|
+
if plugin:
|
|
322
|
+
# Scan with specific plugin
|
|
323
|
+
plugin_instance = registry.get(plugin, PluginCategory.COMPLIANCE)
|
|
324
|
+
if not plugin_instance:
|
|
325
|
+
console.print(f"[red]Plugin not found: {plugin}[/red]")
|
|
326
|
+
return
|
|
327
|
+
plugins_to_use = [plugin_instance]
|
|
328
|
+
else:
|
|
329
|
+
# Use all enabled compliance plugins
|
|
330
|
+
plugins_to_use = registry.get_all(PluginCategory.COMPLIANCE)
|
|
331
|
+
|
|
332
|
+
if not plugins_to_use:
|
|
333
|
+
console.print("[yellow]No compliance plugins enabled.[/yellow]")
|
|
334
|
+
console.print("[white]Enable plugins with: tweek plugins enable <name> -c compliance[/white]")
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
for p in plugins_to_use:
|
|
338
|
+
result = p.scan(content, direction_enum)
|
|
339
|
+
|
|
340
|
+
if result.findings:
|
|
341
|
+
console.print(f"\n[bold]{p.name.upper()}[/bold]: {len(result.findings)} finding(s)")
|
|
342
|
+
|
|
343
|
+
for finding in result.findings:
|
|
344
|
+
severity_styles = {
|
|
345
|
+
"critical": "red bold",
|
|
346
|
+
"high": "red",
|
|
347
|
+
"medium": "yellow",
|
|
348
|
+
"low": "white",
|
|
349
|
+
}
|
|
350
|
+
style = severity_styles.get(finding.severity.value, "white")
|
|
351
|
+
|
|
352
|
+
console.print(f" [{style}]{finding.severity.value.upper()}[/{style}] {finding.pattern_name}")
|
|
353
|
+
console.print(f" [white]Matched: {finding.matched_text[:60]}{'...' if len(finding.matched_text) > 60 else ''}[/white]")
|
|
354
|
+
if finding.description:
|
|
355
|
+
console.print(f" {finding.description}")
|
|
356
|
+
|
|
357
|
+
total_findings.extend(result.findings)
|
|
358
|
+
|
|
359
|
+
if not total_findings:
|
|
360
|
+
console.print("[green]\u2713[/green] No compliance issues found")
|
|
361
|
+
else:
|
|
362
|
+
console.print(f"\n[yellow]Total: {len(total_findings)} finding(s)[/yellow]")
|
|
363
|
+
|
|
364
|
+
except ImportError as e:
|
|
365
|
+
console.print(f"[red]Plugin system not available: {e}[/red]")
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# ============================================================
|
|
369
|
+
# GIT PLUGIN MANAGEMENT COMMANDS
|
|
370
|
+
# ============================================================
|
|
371
|
+
|
|
372
|
+
@plugins.command("install",
|
|
373
|
+
epilog="""\b
|
|
374
|
+
Examples:
|
|
375
|
+
tweek plugins install hipaa-scanner Install a plugin by name
|
|
376
|
+
tweek plugins install hipaa-scanner -v 1.2.0 Install a specific version
|
|
377
|
+
tweek plugins install _ --from-lockfile Install all from lockfile
|
|
378
|
+
tweek plugins install hipaa-scanner --no-verify Skip verification (not recommended)
|
|
379
|
+
"""
|
|
380
|
+
)
|
|
381
|
+
@click.argument("name")
|
|
382
|
+
@click.option("--version", "-v", "version", default=None, help="Specific version to install")
|
|
383
|
+
@click.option("--from-lockfile", is_flag=True, help="Install all plugins from lockfile")
|
|
384
|
+
@click.option("--no-verify", is_flag=True, help="Skip security verification (not recommended)")
|
|
385
|
+
def plugins_install(name: str, version: str, from_lockfile: bool, no_verify: bool):
|
|
386
|
+
"""Install a plugin from the Tweek registry."""
|
|
387
|
+
console.print("[yellow]Note: Plugin registry is experimental and may change.[/yellow]")
|
|
388
|
+
try:
|
|
389
|
+
from tweek.plugins.git_installer import GitPluginInstaller
|
|
390
|
+
from tweek.plugins.git_registry import PluginRegistryClient
|
|
391
|
+
from tweek.plugins.git_lockfile import PluginLockfile
|
|
392
|
+
|
|
393
|
+
if from_lockfile:
|
|
394
|
+
lockfile = PluginLockfile()
|
|
395
|
+
if not lockfile.has_lockfile:
|
|
396
|
+
console.print("[red]No lockfile found. Run 'tweek plugins lock' first.[/red]")
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
locks = lockfile.load()
|
|
400
|
+
registry = PluginRegistryClient()
|
|
401
|
+
installer = GitPluginInstaller(registry_client=registry)
|
|
402
|
+
|
|
403
|
+
for plugin_name, lock in locks.items():
|
|
404
|
+
console.print(f"Installing {plugin_name} v{lock.version}...")
|
|
405
|
+
success, msg = installer.install(
|
|
406
|
+
plugin_name,
|
|
407
|
+
version=lock.version,
|
|
408
|
+
verify=not no_verify,
|
|
409
|
+
)
|
|
410
|
+
if success:
|
|
411
|
+
console.print(f" [green]\u2713[/green] {msg}")
|
|
412
|
+
else:
|
|
413
|
+
console.print(f" [red]\u2717[/red] {msg}")
|
|
414
|
+
return
|
|
415
|
+
|
|
416
|
+
registry = PluginRegistryClient()
|
|
417
|
+
installer = GitPluginInstaller(registry_client=registry)
|
|
418
|
+
|
|
419
|
+
from tweek.cli_helpers import spinner as cli_spinner
|
|
420
|
+
|
|
421
|
+
with cli_spinner(f"Installing {name}"):
|
|
422
|
+
success, msg = installer.install(name, version=version, verify=not no_verify)
|
|
423
|
+
|
|
424
|
+
if success:
|
|
425
|
+
console.print(f"[green]\u2713[/green] {msg}")
|
|
426
|
+
else:
|
|
427
|
+
console.print(f"[red]\u2717[/red] {msg}")
|
|
428
|
+
console.print(f" [white]Hint: Check network connectivity or try: tweek plugins registry --refresh[/white]")
|
|
429
|
+
|
|
430
|
+
except Exception as e:
|
|
431
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
432
|
+
console.print(f" [white]Hint: Check network connectivity and try again[/white]")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@plugins.command("update",
|
|
436
|
+
epilog="""\b
|
|
437
|
+
Examples:
|
|
438
|
+
tweek plugins update hipaa-scanner Update a specific plugin
|
|
439
|
+
tweek plugins update --all Update all installed plugins
|
|
440
|
+
tweek plugins update --check Check for available updates
|
|
441
|
+
tweek plugins update hipaa-scanner -v 2.0.0 Update to specific version
|
|
442
|
+
"""
|
|
443
|
+
)
|
|
444
|
+
@click.argument("name", required=False)
|
|
445
|
+
@click.option("--all", "update_all", is_flag=True, help="Update all installed plugins")
|
|
446
|
+
@click.option("--check", "check_only", is_flag=True, help="Check for updates without installing")
|
|
447
|
+
@click.option("--version", "-v", "version", default=None, help="Specific version to update to")
|
|
448
|
+
@click.option("--no-verify", is_flag=True, help="Skip security verification")
|
|
449
|
+
def plugins_update(name: str, update_all: bool, check_only: bool, version: str, no_verify: bool):
|
|
450
|
+
"""Update installed plugins."""
|
|
451
|
+
console.print("[yellow]Note: Plugin registry is experimental and may change.[/yellow]")
|
|
452
|
+
try:
|
|
453
|
+
from tweek.plugins.git_installer import GitPluginInstaller
|
|
454
|
+
from tweek.plugins.git_registry import PluginRegistryClient
|
|
455
|
+
|
|
456
|
+
registry = PluginRegistryClient()
|
|
457
|
+
installer = GitPluginInstaller(registry_client=registry)
|
|
458
|
+
|
|
459
|
+
if check_only:
|
|
460
|
+
console.print("Checking for updates...")
|
|
461
|
+
updates = installer.check_updates()
|
|
462
|
+
if not updates:
|
|
463
|
+
console.print("[green]All plugins are up to date.[/green]")
|
|
464
|
+
else:
|
|
465
|
+
table = Table(title="Available Updates")
|
|
466
|
+
table.add_column("Plugin", style="cyan")
|
|
467
|
+
table.add_column("Current")
|
|
468
|
+
table.add_column("Latest", style="green")
|
|
469
|
+
for u in updates:
|
|
470
|
+
table.add_row(u["name"], u["current_version"], u["latest_version"])
|
|
471
|
+
console.print(table)
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
if update_all:
|
|
475
|
+
installed = installer.list_installed()
|
|
476
|
+
if not installed:
|
|
477
|
+
console.print("No git plugins installed.")
|
|
478
|
+
return
|
|
479
|
+
for plugin in installed:
|
|
480
|
+
console.print(f"Updating {plugin['name']}...")
|
|
481
|
+
success, msg = installer.update(
|
|
482
|
+
plugin["name"],
|
|
483
|
+
verify=not no_verify,
|
|
484
|
+
)
|
|
485
|
+
if success:
|
|
486
|
+
console.print(f" [green]\u2713[/green] {msg}")
|
|
487
|
+
else:
|
|
488
|
+
console.print(f" [yellow]![/yellow] {msg}")
|
|
489
|
+
return
|
|
490
|
+
|
|
491
|
+
if not name:
|
|
492
|
+
console.print("[red]Specify a plugin name or use --all[/red]")
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
success, msg = installer.update(name, version=version, verify=not no_verify)
|
|
496
|
+
if success:
|
|
497
|
+
console.print(f"[green]\u2713[/green] {msg}")
|
|
498
|
+
else:
|
|
499
|
+
console.print(f"[red]\u2717[/red] {msg}")
|
|
500
|
+
|
|
501
|
+
except Exception as e:
|
|
502
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
@plugins.command("remove",
|
|
506
|
+
epilog="""\b
|
|
507
|
+
Examples:
|
|
508
|
+
tweek plugins remove hipaa-scanner Remove a plugin (with confirmation)
|
|
509
|
+
tweek plugins remove hipaa-scanner -f Remove without confirmation
|
|
510
|
+
"""
|
|
511
|
+
)
|
|
512
|
+
@click.argument("name")
|
|
513
|
+
@click.option("--force", "-f", is_flag=True, help="Skip confirmation")
|
|
514
|
+
def plugins_remove(name: str, force: bool):
|
|
515
|
+
"""Remove an installed git plugin."""
|
|
516
|
+
console.print("[yellow]Note: Plugin registry is experimental and may change.[/yellow]")
|
|
517
|
+
try:
|
|
518
|
+
from tweek.plugins.git_installer import GitPluginInstaller
|
|
519
|
+
from tweek.plugins.git_registry import PluginRegistryClient
|
|
520
|
+
|
|
521
|
+
installer = GitPluginInstaller(registry_client=PluginRegistryClient())
|
|
522
|
+
|
|
523
|
+
if not force:
|
|
524
|
+
if not click.confirm(f"Remove plugin '{name}'?"):
|
|
525
|
+
return
|
|
526
|
+
|
|
527
|
+
success, msg = installer.remove(name)
|
|
528
|
+
if success:
|
|
529
|
+
console.print(f"[green]\u2713[/green] {msg}")
|
|
530
|
+
else:
|
|
531
|
+
console.print(f"[red]\u2717[/red] {msg}")
|
|
532
|
+
|
|
533
|
+
except Exception as e:
|
|
534
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
@plugins.command("search",
|
|
538
|
+
epilog="""\b
|
|
539
|
+
Examples:
|
|
540
|
+
tweek plugins search hipaa Search for plugins by name
|
|
541
|
+
tweek plugins search -c compliance Browse all compliance plugins
|
|
542
|
+
tweek plugins search -t free Show only free-tier plugins
|
|
543
|
+
tweek plugins search pii --include-deprecated Include deprecated results
|
|
544
|
+
"""
|
|
545
|
+
)
|
|
546
|
+
@click.argument("query", required=False)
|
|
547
|
+
@click.option("--category", "-c", type=click.Choice(["compliance", "providers", "detectors", "screening"]),
|
|
548
|
+
help="Filter by category")
|
|
549
|
+
@click.option("--tier", "-t", type=click.Choice(["free", "pro", "enterprise"]),
|
|
550
|
+
help="Filter by license tier")
|
|
551
|
+
@click.option("--include-deprecated", is_flag=True, help="Include deprecated plugins")
|
|
552
|
+
def plugins_search(query: str, category: str, tier: str, include_deprecated: bool):
|
|
553
|
+
"""Search the Tweek plugin registry."""
|
|
554
|
+
console.print("[yellow]Note: Plugin registry is experimental and may change.[/yellow]")
|
|
555
|
+
try:
|
|
556
|
+
from tweek.plugins.git_registry import PluginRegistryClient
|
|
557
|
+
|
|
558
|
+
registry = PluginRegistryClient()
|
|
559
|
+
console.print("Searching registry...")
|
|
560
|
+
results = registry.search(
|
|
561
|
+
query=query,
|
|
562
|
+
category=category,
|
|
563
|
+
tier=tier,
|
|
564
|
+
include_deprecated=include_deprecated,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
if not results:
|
|
568
|
+
console.print("[yellow]No plugins found matching your criteria.[/yellow]")
|
|
569
|
+
return
|
|
570
|
+
|
|
571
|
+
table = Table(title=f"Registry Results ({len(results)} found)")
|
|
572
|
+
table.add_column("Name", style="cyan")
|
|
573
|
+
table.add_column("Version")
|
|
574
|
+
table.add_column("Category")
|
|
575
|
+
table.add_column("Tier")
|
|
576
|
+
table.add_column("Description", max_width=40)
|
|
577
|
+
|
|
578
|
+
for entry in results:
|
|
579
|
+
table.add_row(
|
|
580
|
+
entry.name,
|
|
581
|
+
entry.latest_version,
|
|
582
|
+
entry.category,
|
|
583
|
+
entry.requires_license_tier,
|
|
584
|
+
entry.description[:40] + "..." if len(entry.description) > 40 else entry.description,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
console.print(table)
|
|
588
|
+
|
|
589
|
+
except Exception as e:
|
|
590
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
@plugins.command("lock",
|
|
594
|
+
epilog="""\b
|
|
595
|
+
Examples:
|
|
596
|
+
tweek plugins lock Generate lockfile for all plugins
|
|
597
|
+
tweek plugins lock -p hipaa -v 1.2.0 Lock a specific plugin to a version
|
|
598
|
+
tweek plugins lock --project Create project-level lockfile
|
|
599
|
+
"""
|
|
600
|
+
)
|
|
601
|
+
@click.option("--plugin", "-p", "plugin_name", default=None, help="Lock a specific plugin")
|
|
602
|
+
@click.option("--version", "-v", "version", default=None, help="Lock to specific version")
|
|
603
|
+
@click.option("--project", is_flag=True, help="Create project-level lockfile (.tweek/plugins.lock.json)")
|
|
604
|
+
def plugins_lock(plugin_name: str, version: str, project: bool):
|
|
605
|
+
"""Generate or update a plugin version lockfile."""
|
|
606
|
+
console.print("[yellow]Note: Plugin registry is experimental and may change.[/yellow]")
|
|
607
|
+
try:
|
|
608
|
+
from tweek.plugins.git_lockfile import PluginLockfile
|
|
609
|
+
|
|
610
|
+
lockfile = PluginLockfile()
|
|
611
|
+
target = "project" if project else "user"
|
|
612
|
+
|
|
613
|
+
specific = None
|
|
614
|
+
if plugin_name:
|
|
615
|
+
specific = {plugin_name: version or "latest"}
|
|
616
|
+
|
|
617
|
+
path = lockfile.generate(target=target, specific_plugins=specific)
|
|
618
|
+
console.print(f"[green]\u2713[/green] Lockfile generated: {path}")
|
|
619
|
+
|
|
620
|
+
# Show lock contents
|
|
621
|
+
locks = lockfile.load()
|
|
622
|
+
if locks:
|
|
623
|
+
table = Table(title="Locked Plugins")
|
|
624
|
+
table.add_column("Plugin", style="cyan")
|
|
625
|
+
table.add_column("Version")
|
|
626
|
+
table.add_column("Commit")
|
|
627
|
+
for name, lock in locks.items():
|
|
628
|
+
table.add_row(
|
|
629
|
+
name,
|
|
630
|
+
lock.version,
|
|
631
|
+
lock.commit_sha[:12] if lock.commit_sha else "n/a",
|
|
632
|
+
)
|
|
633
|
+
console.print(table)
|
|
634
|
+
|
|
635
|
+
except Exception as e:
|
|
636
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
@plugins.command("verify",
|
|
640
|
+
epilog="""\b
|
|
641
|
+
Examples:
|
|
642
|
+
tweek plugins verify hipaa-scanner Verify a specific plugin's integrity
|
|
643
|
+
tweek plugins verify --all Verify all installed plugins
|
|
644
|
+
"""
|
|
645
|
+
)
|
|
646
|
+
@click.argument("name", required=False)
|
|
647
|
+
@click.option("--all", "verify_all", is_flag=True, help="Verify all installed plugins")
|
|
648
|
+
def plugins_verify(name: str, verify_all: bool):
|
|
649
|
+
"""Verify integrity of installed git plugins."""
|
|
650
|
+
console.print("[yellow]Note: Plugin registry is experimental and may change.[/yellow]")
|
|
651
|
+
try:
|
|
652
|
+
from tweek.plugins.git_installer import GitPluginInstaller
|
|
653
|
+
from tweek.plugins.git_registry import PluginRegistryClient
|
|
654
|
+
|
|
655
|
+
from tweek.cli_helpers import spinner as cli_spinner
|
|
656
|
+
|
|
657
|
+
installer = GitPluginInstaller(registry_client=PluginRegistryClient())
|
|
658
|
+
|
|
659
|
+
if verify_all:
|
|
660
|
+
with cli_spinner("Verifying plugin integrity"):
|
|
661
|
+
results = installer.verify_all()
|
|
662
|
+
if not results:
|
|
663
|
+
console.print("No git plugins installed.")
|
|
664
|
+
return
|
|
665
|
+
|
|
666
|
+
all_valid = True
|
|
667
|
+
for plugin_name, (valid, issues) in results.items():
|
|
668
|
+
if valid:
|
|
669
|
+
console.print(f" [green]\u2713[/green] {plugin_name}: integrity verified")
|
|
670
|
+
else:
|
|
671
|
+
all_valid = False
|
|
672
|
+
console.print(f" [red]\u2717[/red] {plugin_name}: {len(issues)} issue(s)")
|
|
673
|
+
for issue in issues:
|
|
674
|
+
console.print(f" - {issue}")
|
|
675
|
+
|
|
676
|
+
if all_valid:
|
|
677
|
+
console.print(f"\n[green]All {len(results)} plugin(s) verified.[/green]")
|
|
678
|
+
return
|
|
679
|
+
|
|
680
|
+
if not name:
|
|
681
|
+
console.print("[red]Specify a plugin name or use --all[/red]")
|
|
682
|
+
return
|
|
683
|
+
|
|
684
|
+
valid, issues = installer.verify_plugin(name)
|
|
685
|
+
if valid:
|
|
686
|
+
console.print(f"[green]\u2713[/green] Plugin '{name}' integrity verified")
|
|
687
|
+
else:
|
|
688
|
+
console.print(f"[red]\u2717[/red] Plugin '{name}' failed verification:")
|
|
689
|
+
for issue in issues:
|
|
690
|
+
console.print(f" - {issue}")
|
|
691
|
+
|
|
692
|
+
except Exception as e:
|
|
693
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
@plugins.command("registry",
|
|
697
|
+
epilog="""\b
|
|
698
|
+
Examples:
|
|
699
|
+
tweek plugins registry Show registry summary
|
|
700
|
+
tweek plugins registry --refresh Force refresh the registry cache
|
|
701
|
+
tweek plugins registry --info Show detailed registry metadata
|
|
702
|
+
"""
|
|
703
|
+
)
|
|
704
|
+
@click.option("--refresh", is_flag=True, help="Force refresh the registry cache")
|
|
705
|
+
@click.option("--info", "show_info", is_flag=True, help="Show registry metadata")
|
|
706
|
+
def plugins_registry(refresh: bool, show_info: bool):
|
|
707
|
+
"""Manage the plugin registry cache."""
|
|
708
|
+
console.print("[yellow]Note: Plugin registry is experimental and may change.[/yellow]")
|
|
709
|
+
try:
|
|
710
|
+
from tweek.plugins.git_registry import PluginRegistryClient
|
|
711
|
+
|
|
712
|
+
registry = PluginRegistryClient()
|
|
713
|
+
|
|
714
|
+
if refresh:
|
|
715
|
+
console.print("Refreshing registry...")
|
|
716
|
+
try:
|
|
717
|
+
entries = registry.fetch(force_refresh=True)
|
|
718
|
+
console.print(f"[green]\u2713[/green] Registry refreshed: {len(entries)} plugins available")
|
|
719
|
+
except Exception as e:
|
|
720
|
+
console.print(f"[red]\u2717[/red] Failed to refresh: {e}")
|
|
721
|
+
return
|
|
722
|
+
|
|
723
|
+
if show_info:
|
|
724
|
+
info = registry.get_registry_info()
|
|
725
|
+
panel_content = "\n".join([
|
|
726
|
+
f"URL: {info.get('url', 'unknown')}",
|
|
727
|
+
f"Cache: {info.get('cache_path', 'unknown')}",
|
|
728
|
+
f"Cache TTL: {info.get('cache_ttl_seconds', 0)}s",
|
|
729
|
+
f"Cache valid: {info.get('cache_valid', False)}",
|
|
730
|
+
f"Schema version: {info.get('schema_version', 'unknown')}",
|
|
731
|
+
f"Last updated: {info.get('updated_at', 'unknown')}",
|
|
732
|
+
f"Total plugins: {info.get('total_plugins', 'unknown')}",
|
|
733
|
+
f"Cache fetched: {info.get('cache_fetched_at', 'never')}",
|
|
734
|
+
])
|
|
735
|
+
console.print(Panel(panel_content, title="Registry Info"))
|
|
736
|
+
return
|
|
737
|
+
|
|
738
|
+
# Default: show summary
|
|
739
|
+
try:
|
|
740
|
+
entries = registry.fetch()
|
|
741
|
+
verified = [e for e in entries.values() if e.verified and not e.deprecated]
|
|
742
|
+
console.print(f"Registry: {len(verified)} verified plugins available")
|
|
743
|
+
console.print("Use 'tweek plugins search' to browse or 'tweek plugins registry --refresh' to update cache")
|
|
744
|
+
except Exception as e:
|
|
745
|
+
console.print(f"[yellow]Registry unavailable: {e}[/yellow]")
|
|
746
|
+
|
|
747
|
+
except Exception as e:
|
|
748
|
+
console.print(f"[red]Error: {e}[/red]")
|