tweek 0.3.1__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.
Files changed (61) hide show
  1. tweek/__init__.py +2 -2
  2. tweek/audit.py +2 -2
  3. tweek/cli.py +78 -6605
  4. tweek/cli_config.py +643 -0
  5. tweek/cli_configure.py +413 -0
  6. tweek/cli_core.py +718 -0
  7. tweek/cli_dry_run.py +390 -0
  8. tweek/cli_helpers.py +316 -0
  9. tweek/cli_install.py +1666 -0
  10. tweek/cli_logs.py +301 -0
  11. tweek/cli_mcp.py +148 -0
  12. tweek/cli_memory.py +343 -0
  13. tweek/cli_plugins.py +748 -0
  14. tweek/cli_protect.py +564 -0
  15. tweek/cli_proxy.py +405 -0
  16. tweek/cli_security.py +236 -0
  17. tweek/cli_skills.py +289 -0
  18. tweek/cli_uninstall.py +551 -0
  19. tweek/cli_vault.py +313 -0
  20. tweek/config/allowed_dirs.yaml +16 -17
  21. tweek/config/families.yaml +4 -1
  22. tweek/config/manager.py +17 -0
  23. tweek/config/patterns.yaml +29 -5
  24. tweek/config/templates/config.yaml.template +212 -0
  25. tweek/config/templates/env.template +45 -0
  26. tweek/config/templates/overrides.yaml.template +121 -0
  27. tweek/config/templates/tweek.yaml.template +20 -0
  28. tweek/config/templates.py +136 -0
  29. tweek/config/tiers.yaml +5 -4
  30. tweek/diagnostics.py +112 -32
  31. tweek/hooks/overrides.py +4 -0
  32. tweek/hooks/post_tool_use.py +46 -1
  33. tweek/hooks/pre_tool_use.py +149 -49
  34. tweek/integrations/openclaw.py +84 -0
  35. tweek/licensing.py +1 -1
  36. tweek/mcp/__init__.py +7 -9
  37. tweek/mcp/clients/chatgpt.py +2 -2
  38. tweek/mcp/clients/claude_desktop.py +2 -2
  39. tweek/mcp/clients/gemini.py +2 -2
  40. tweek/mcp/proxy.py +165 -1
  41. tweek/memory/provenance.py +438 -0
  42. tweek/memory/queries.py +2 -0
  43. tweek/memory/safety.py +23 -4
  44. tweek/memory/schemas.py +1 -0
  45. tweek/memory/store.py +101 -71
  46. tweek/plugins/screening/heuristic_scorer.py +1 -1
  47. tweek/security/integrity.py +77 -0
  48. tweek/security/llm_reviewer.py +162 -68
  49. tweek/security/local_reviewer.py +44 -2
  50. tweek/security/model_registry.py +73 -7
  51. tweek/skill_template/overrides-reference.md +1 -1
  52. tweek/skills/context.py +221 -0
  53. tweek/skills/scanner.py +2 -2
  54. {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/METADATA +8 -7
  55. {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/RECORD +60 -38
  56. tweek/mcp/server.py +0 -320
  57. {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/WHEEL +0 -0
  58. {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/entry_points.txt +0 -0
  59. {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/licenses/LICENSE +0 -0
  60. {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/licenses/NOTICE +0 -0
  61. {tweek-0.3.1.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]")