plugin_catalog_server.py
1 from flask import Flask, jsonify, request, send_from_directory 2 import os 3 import shutil 4 import logging 5 from pathlib import Path 6 7 from .plugin_catalog.scraper import PluginScraper 8 from .plugin_catalog.registry_manager import RegistryManager 9 from .plugin_catalog.constants import PLUGIN_CATALOG_TEMP_DIR 10 11 logger = logging.getLogger(__name__) 12 if not logger.hasHandlers(): 13 logging.basicConfig( 14 level=logging.INFO, 15 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 16 ) 17 18 registry_manager = RegistryManager() 19 plugin_scraper = PluginScraper() 20 21 22 def create_plugin_catalog_app(shared_config=None): 23 current_dir = Path(__file__).parent 24 static_folder_path = ( 25 Path(__file__).resolve().parent.parent / "frontend" / "static" / "client" 26 ) 27 app = Flask(__name__, static_folder=str(static_folder_path), static_url_path="") 28 app.config["SHARED_CONFIG"] = shared_config 29 30 logger.info("Performing initial plugin scrape on startup...") 31 try: 32 initial_registries = registry_manager.get_all_registries() 33 plugin_scraper.get_all_plugins(initial_registries, force_refresh=True) 34 logger.info( 35 f"Initial scrape complete. Found {len(plugin_scraper.plugin_cache)} plugins." 36 ) 37 except Exception as e: 38 logger.error(f"Error during initial plugin scrape: {e}", exc_info=True) 39 40 @app.route("/") 41 def serve_index(): 42 return send_from_directory(app.static_folder, "index.html") 43 44 @app.route("/assets/<path:filename>") 45 def serve_assets(filename): 46 assets_dir = Path(app.static_folder) / "assets" 47 return send_from_directory(str(assets_dir), filename) 48 49 @app.route("/api/plugin_catalog/plugins", methods=["GET"]) 50 def get_plugins_api(): 51 search_query = request.args.get("search", "").lower() 52 53 if not plugin_scraper.is_cache_populated: 54 logger.info( 55 "Plugin cache not populated, attempting to refresh for /api/plugin_catalog/plugins request." 56 ) 57 all_regs = registry_manager.get_all_registries() 58 plugin_scraper.get_all_plugins(all_regs, force_refresh=True) 59 60 plugins_to_filter = plugin_scraper.plugin_cache 61 62 if search_query: 63 plugins_to_filter = [ 64 p 65 for p in plugins_to_filter 66 if search_query in p.pyproject.name.lower() 67 or ( 68 p.pyproject.description 69 and search_query in p.pyproject.description.lower() 70 ) 71 ] 72 return jsonify([p.model_dump(exclude_none=True) for p in plugins_to_filter]) 73 74 @app.route("/api/plugin_catalog/plugins/<plugin_id>/details", methods=["GET"]) 75 def get_plugin_details_api(plugin_id: str): 76 details = plugin_scraper.get_plugin_details(plugin_id) 77 if details: 78 return jsonify(details.model_dump(exclude_none=True)) 79 return jsonify({"error": "Plugin not found", "status": "failure"}), 404 80 81 @app.route("/api/plugin_catalog/plugins/install", methods=["POST"]) 82 def install_plugin_api(): 83 data = request.json 84 if not data: 85 return ( 86 jsonify({"error": "Request body must be JSON", "status": "failure"}), 87 400, 88 ) 89 90 plugin_id = data.get("pluginId") 91 component_name = data.get("componentName") 92 93 if not plugin_id or not component_name: 94 return ( 95 jsonify( 96 { 97 "error": "pluginId and componentName are required", 98 "status": "failure", 99 } 100 ), 101 400, 102 ) 103 104 plugin_info = plugin_scraper.get_plugin_details(plugin_id) 105 if not plugin_info: 106 return ( 107 jsonify({"error": "Plugin not found to install", "status": "failure"}), 108 404, 109 ) 110 111 success, message = plugin_scraper.install_plugin_cli( 112 plugin_info, component_name 113 ) 114 if success: 115 return jsonify({"message": message, "status": "success"}) 116 else: 117 return jsonify({"error": message, "status": "failure"}), 500 118 119 @app.route("/api/plugin_catalog/registries", methods=["GET"]) 120 def get_registries_api(): 121 registries = registry_manager.get_all_registries() 122 return jsonify([r.model_dump(exclude_none=True) for r in registries]) 123 124 @app.route("/api/plugin_catalog/registries", methods=["POST"]) 125 def add_registry_api(): 126 data = request.json 127 if not data: 128 return ( 129 jsonify({"error": "Request body must be JSON", "status": "failure"}), 130 400, 131 ) 132 133 path_or_url = data.get("path_or_url") 134 name = data.get("name") 135 136 if not path_or_url: 137 return ( 138 jsonify({"error": "path_or_url is required", "status": "failure"}), 139 400, 140 ) 141 142 success, is_update = registry_manager.add_registry(path_or_url, name=name) 143 144 if success: 145 action = "updated" if is_update else "added" 146 logger.info( 147 f"Registry '{path_or_url}' (Name: {name if name else 'N/A'}) {action} by user." 148 ) 149 150 # For re-added registries, clear the git cache to force fresh clone 151 if is_update: 152 registry_id = registry_manager._generate_registry_id(path_or_url) 153 plugin_scraper.clear_git_cache(registry_id) 154 logger.info(f"Cleared git cache for re-added registry: {registry_id}") 155 156 # Always force refresh plugins, and use fresh clone for re-added registries 157 all_regs = registry_manager.get_all_registries() 158 plugin_scraper.get_all_plugins(all_regs, force_refresh=True, force_fresh_clone=is_update) 159 160 message = f"Registry {action} and plugins refreshed." 161 return jsonify({ 162 "message": message, 163 "status": "success", 164 "is_update": is_update 165 }) 166 else: 167 return ( 168 jsonify({ 169 "error": "Failed to add registry (invalid path/URL or file system error)", 170 "status": "failure" 171 }), 172 400, 173 ) 174 175 @app.route("/api/plugin_catalog/registries/refresh", methods=["POST"]) 176 def refresh_registries_api(): 177 logger.info("Refreshing all plugin registries via API call...") 178 all_regs = registry_manager.get_all_registries() 179 plugin_scraper.get_all_plugins(all_regs, force_refresh=True) 180 logger.info( 181 f"Refresh complete. Found {len(plugin_scraper.plugin_cache)} plugins." 182 ) 183 return jsonify( 184 { 185 "message": f"Registries refreshed. Found {len(plugin_scraper.plugin_cache)} plugins.", 186 "status": "success", 187 } 188 ) 189 190 @app.route("/api/shutdown", methods=["POST"]) 191 def shutdown_api(): 192 shared_config = app.config.get("SHARED_CONFIG") 193 if shared_config is not None: 194 shared_config["status"] = "shutdown_requested" 195 196 temp_dir_path = Path(os.path.expanduser(PLUGIN_CATALOG_TEMP_DIR)) 197 if temp_dir_path.exists(): 198 logger.info(f"Cleaning up temporary directory: {temp_dir_path}") 199 try: 200 shutil.rmtree(temp_dir_path) 201 except Exception as e: 202 logger.error( 203 f"Error cleaning up temporary directory {temp_dir_path}: {e}" 204 ) 205 206 func = request.environ.get("werkzeug.server.shutdown") 207 if func is None: 208 logger.warning( 209 "Werkzeug server shutdown function not found. Attempting os._exit(0)." 210 ) 211 os._exit(0) 212 try: 213 func() 214 logger.info("Server shutdown initiated.") 215 except Exception as e: 216 logger.error(f"Error during server shutdown: {e}. Forcing exit.") 217 os._exit(1) 218 return jsonify({"status": "success", "message": "Server shutting down"}) 219 220 return app 221 222 223 if __name__ == "__main__": 224 app = create_plugin_catalog_app() 225 print( 226 "Starting Plugin Catalog Flask app directly for testing on http://127.0.0.1:5003" 227 ) 228 app.run(host="127.0.0.1", port=5003, debug=True)