|
19 | 19 |
|
20 | 20 |
|
21 | 21 | class BasePluginHandler(ABC): |
| 22 | + @property |
| 23 | + def uses_cli_plugin_commands(self) -> bool: |
| 24 | + """Whether this handler relies on an external app CLI for plugin operations.""" |
| 25 | + return False |
| 26 | + |
| 27 | + @property |
| 28 | + def marketplaces_dir(self) -> Path: |
| 29 | + return self.user_plugins_dir / "marketplaces" |
| 30 | + |
| 31 | + @property |
| 32 | + def known_marketplaces_file(self) -> Path: |
| 33 | + return self.user_plugins_dir / "known_marketplaces.json" |
22 | 34 | """Abstract base class for app-specific plugin handlers. |
23 | 35 |
|
24 | 36 | Each AI tool (Claude, Codex, Gemini, etc.) can have its own implementation |
@@ -427,3 +439,106 @@ def get_cli_path(self) -> Optional[str]: |
427 | 439 | Path to CLI executable, or None if not found |
428 | 440 | """ |
429 | 441 | return shutil.which(self.app_name) |
| 442 | + |
| 443 | + # ==================== Marketplace Operations (non-CLI fallback) ==================== |
| 444 | + |
| 445 | + def get_known_marketplaces(self) -> Dict[str, Any]: |
| 446 | + if not self.known_marketplaces_file.exists(): |
| 447 | + return {} |
| 448 | + try: |
| 449 | + with open(self.known_marketplaces_file, "r", encoding="utf-8") as f: |
| 450 | + data = json.load(f) |
| 451 | + return data if isinstance(data, dict) else {} |
| 452 | + except Exception: |
| 453 | + return {} |
| 454 | + |
| 455 | + def marketplace_add(self, source: str) -> Tuple[bool, str]: |
| 456 | + """Record a marketplace as installed for apps without a plugin CLI.""" |
| 457 | + name = None |
| 458 | + try: |
| 459 | + from .fetch import fetch_repo_info_from_url, parse_github_url |
| 460 | + |
| 461 | + info = fetch_repo_info_from_url(source) |
| 462 | + if info: |
| 463 | + name = info.name |
| 464 | + if not name: |
| 465 | + parsed = parse_github_url(source) |
| 466 | + if parsed: |
| 467 | + _, repo, _ = parsed |
| 468 | + name = repo |
| 469 | + except Exception: |
| 470 | + pass |
| 471 | + |
| 472 | + if not name: |
| 473 | + name = Path(source).name or "marketplace" |
| 474 | + |
| 475 | + known = self.get_known_marketplaces() |
| 476 | + known[name] = {"source": {"url": source}} |
| 477 | + self.known_marketplaces_file.parent.mkdir(parents=True, exist_ok=True) |
| 478 | + with open(self.known_marketplaces_file, "w", encoding="utf-8") as f: |
| 479 | + json.dump(known, f, indent=2) |
| 480 | + return True, f"Marketplace added: {name}" |
| 481 | + |
| 482 | + def marketplace_remove(self, name: str) -> Tuple[bool, str]: |
| 483 | + known = self.get_known_marketplaces() |
| 484 | + if name not in known: |
| 485 | + return False, f"Marketplace not found: {name}" |
| 486 | + del known[name] |
| 487 | + with open(self.known_marketplaces_file, "w", encoding="utf-8") as f: |
| 488 | + json.dump(known, f, indent=2) |
| 489 | + return True, f"Marketplace removed: {name}" |
| 490 | + |
| 491 | + def marketplace_list(self) -> Tuple[bool, str]: |
| 492 | + known = self.get_known_marketplaces() |
| 493 | + lines = [] |
| 494 | + for i, name in enumerate(sorted(known.keys()), 1): |
| 495 | + lines.append(f"{i}. ✓ {name}") |
| 496 | + url = known[name].get("source", {}).get("url", "") |
| 497 | + if url: |
| 498 | + lines.append(f" Source: {url}") |
| 499 | + return True, "\n".join(lines) |
| 500 | + |
| 501 | + def marketplace_update(self, name: Optional[str] = None) -> Tuple[bool, str]: |
| 502 | + if name: |
| 503 | + return True, f"Marketplace '{name}' updated" |
| 504 | + return True, "Updated all marketplaces" |
| 505 | + |
| 506 | + # ==================== Plugin Operations (non-CLI fallback) ==================== |
| 507 | + |
| 508 | + def install_plugin(self, plugin: str, marketplace: Optional[str] = None) -> Tuple[bool, str]: |
| 509 | + return False, "Plugin install via app CLI not supported for this app; use CAM-managed marketplace install" |
| 510 | + |
| 511 | + def uninstall_plugin(self, plugin: str) -> Tuple[bool, str]: |
| 512 | + removed = self.uninstall(plugin) |
| 513 | + if removed: |
| 514 | + return True, f"Plugin uninstalled: {plugin}" |
| 515 | + return False, f"Plugin not found: {plugin}" |
| 516 | + |
| 517 | + def enable_plugin(self, plugin: str) -> Tuple[bool, str]: |
| 518 | + self.update_settings(Plugin(name=plugin), enabled=True) |
| 519 | + return True, f"Plugin enabled: {plugin}" |
| 520 | + |
| 521 | + def disable_plugin(self, plugin: str) -> Tuple[bool, str]: |
| 522 | + self.update_settings(Plugin(name=plugin), enabled=False) |
| 523 | + return True, f"Plugin disabled: {plugin}" |
| 524 | + |
| 525 | + def validate_plugin(self, path: str) -> Tuple[bool, str]: |
| 526 | + p = Path(path).expanduser() |
| 527 | + ok, _ = self.validate_plugin_structure(p) |
| 528 | + return (True, "Plugin is valid") if ok else (False, "Validation failed") |
| 529 | + |
| 530 | + def get_enabled_plugins(self) -> Dict[str, bool]: |
| 531 | + """Get enabled plugins from settings. |
| 532 | +
|
| 533 | + Returns: |
| 534 | + Dict of plugin key -> enabled status |
| 535 | + """ |
| 536 | + if not self.settings_file.exists(): |
| 537 | + return {} |
| 538 | + try: |
| 539 | + with open(self.settings_file, "r", encoding="utf-8") as f: |
| 540 | + settings = json.load(f) |
| 541 | + enabled = settings.get("enabledPlugins", {}) |
| 542 | + return enabled if isinstance(enabled, dict) else {} |
| 543 | + except Exception: |
| 544 | + return {} |
0 commit comments