@@ -21,9 +21,10 @@ use crate::events::{
2121use crate :: native_template_functions:: { template_function_keyring, template_function_secure} ;
2222use crate :: nodejs:: start_nodejs_plugin_runtime;
2323use crate :: plugin_handle:: PluginHandle ;
24+ use crate :: plugin_meta:: get_plugin_meta;
2425use crate :: server_ws:: PluginRuntimeServerWebsocket ;
2526use log:: { error, info, warn} ;
26- use std:: collections:: HashMap ;
27+ use std:: collections:: { HashMap , HashSet } ;
2728use std:: env;
2829use std:: path:: { Path , PathBuf } ;
2930use std:: sync:: Arc ;
@@ -33,7 +34,7 @@ use tokio::net::TcpListener;
3334use tokio:: sync:: mpsc:: error:: TrySendError ;
3435use tokio:: sync:: { Mutex , mpsc, oneshot} ;
3536use tokio:: time:: { Instant , timeout} ;
36- use yaak_models:: models:: Plugin ;
37+ use yaak_models:: models:: { Plugin , PluginSource } ;
3738use yaak_models:: query_manager:: QueryManager ;
3839use yaak_models:: util:: { UpdateSource , generate_id} ;
3940use yaak_templates:: error:: Error :: RenderError ;
@@ -162,13 +163,14 @@ impl PluginManager {
162163
163164 let bundled_dirs = plugin_manager. list_bundled_plugin_dirs ( ) . await ?;
164165 let db = query_manager. connect ( ) ;
165- for dir in bundled_dirs {
166- if db. get_plugin_by_directory ( & dir) . is_none ( ) {
166+ for dir in & bundled_dirs {
167+ if db. get_plugin_by_directory ( dir) . is_none ( ) {
167168 db. upsert_plugin (
168169 & Plugin {
169- directory : dir,
170+ directory : dir. clone ( ) ,
170171 enabled : true ,
171172 url : None ,
173+ source : PluginSource :: Bundled ,
172174 ..Default :: default ( )
173175 } ,
174176 & UpdateSource :: Background ,
@@ -213,6 +215,57 @@ impl PluginManager {
213215 read_plugins_dir ( & plugins_dir) . await
214216 }
215217
218+ pub async fn resolve_plugins_for_runtime_from_db ( & self , plugins : Vec < Plugin > ) -> Vec < Plugin > {
219+ let bundled_dirs = match self . list_bundled_plugin_dirs ( ) . await {
220+ Ok ( dirs) => dirs,
221+ Err ( err) => {
222+ warn ! ( "Failed to read bundled plugin dirs for resolution: {err:?}" ) ;
223+ Vec :: new ( )
224+ }
225+ } ;
226+ self . resolve_plugins_for_runtime ( plugins, bundled_dirs)
227+ }
228+
229+ /// Resolve the plugin set for the current runtime instance.
230+ ///
231+ /// Rules:
232+ /// - Drop bundled rows that are not present in this instance's bundled directory list.
233+ /// - Deduplicate by plugin metadata name (fallback to directory key when metadata is unreadable).
234+ /// - Prefer sources in this order: filesystem > registry > bundled.
235+ /// - For same-source conflicts, prefer the most recently installed row (`created_at`).
236+ fn resolve_plugins_for_runtime (
237+ & self ,
238+ plugins : Vec < Plugin > ,
239+ bundled_dirs : Vec < String > ,
240+ ) -> Vec < Plugin > {
241+ let bundled_dir_set: HashSet < String > = bundled_dirs. into_iter ( ) . collect ( ) ;
242+ let mut selected: HashMap < String , Plugin > = HashMap :: new ( ) ;
243+
244+ for plugin in plugins {
245+ if matches ! ( plugin. source, PluginSource :: Bundled )
246+ && !bundled_dir_set. contains ( & plugin. directory )
247+ {
248+ continue ;
249+ }
250+
251+ let key = match get_plugin_meta ( Path :: new ( & plugin. directory ) ) {
252+ Ok ( meta) => meta. name ,
253+ Err ( _) => format ! ( "__dir__{}" , plugin. directory) ,
254+ } ;
255+
256+ match selected. get ( & key) {
257+ Some ( existing) if !prefer_plugin ( & plugin, existing) => { }
258+ _ => {
259+ selected. insert ( key, plugin) ;
260+ }
261+ }
262+ }
263+
264+ let mut resolved = selected. into_values ( ) . collect :: < Vec < _ > > ( ) ;
265+ resolved. sort_by ( |a, b| b. created_at . cmp ( & a. created_at ) ) ;
266+ resolved
267+ }
268+
216269 pub async fn uninstall ( & self , plugin_context : & PluginContext , dir : & str ) -> Result < ( ) > {
217270 let plugin = self . get_plugin_by_dir ( dir) . await . ok_or ( PluginNotFoundErr ( dir. to_string ( ) ) ) ?;
218271 self . remove_plugin ( plugin_context, & plugin) . await
@@ -287,7 +340,8 @@ impl PluginManager {
287340 Ok ( ( ) )
288341 }
289342
290- /// Initialize all plugins from the provided list.
343+ /// Initialize all plugins from the provided DB list.
344+ /// Plugin candidates are resolved for this runtime instance before initialization.
291345 /// Returns a list of (plugin_directory, error_message) for any plugins that failed to initialize.
292346 pub async fn initialize_all_plugins (
293347 & self ,
@@ -297,15 +351,18 @@ impl PluginManager {
297351 info ! ( "Initializing all plugins" ) ;
298352 let start = Instant :: now ( ) ;
299353 let mut errors = Vec :: new ( ) ;
354+ let plugins = self . resolve_plugins_for_runtime_from_db ( plugins) . await ;
355+
356+ // Rebuild runtime handles from scratch to avoid stale/duplicate handles.
357+ let existing_handles = { self . plugin_handles . lock ( ) . await . clone ( ) } ;
358+ for plugin_handle in existing_handles {
359+ if let Err ( e) = self . remove_plugin ( plugin_context, & plugin_handle) . await {
360+ error ! ( "Failed to remove plugin {} {e:?}" , plugin_handle. dir) ;
361+ errors. push ( ( plugin_handle. dir . clone ( ) , e. to_string ( ) ) ) ;
362+ }
363+ }
300364
301365 for plugin in plugins {
302- // First remove the plugin if it exists and is enabled
303- if let Some ( plugin_handle) = self . get_plugin_by_dir ( & plugin. directory ) . await {
304- if let Err ( e) = self . remove_plugin ( plugin_context, & plugin_handle) . await {
305- error ! ( "Failed to remove plugin {} {e:?}" , plugin. directory) ;
306- continue ;
307- }
308- }
309366 if let Err ( e) = self . add_plugin ( plugin_context, & plugin) . await {
310367 warn ! ( "Failed to add plugin {} {e:?}" , plugin. directory) ;
311368 errors. push ( ( plugin. directory . clone ( ) , e. to_string ( ) ) ) ;
@@ -1063,6 +1120,24 @@ impl PluginManager {
10631120 }
10641121}
10651122
1123+ fn source_priority ( source : & PluginSource ) -> i32 {
1124+ match source {
1125+ PluginSource :: Filesystem => 3 ,
1126+ PluginSource :: Registry => 2 ,
1127+ PluginSource :: Bundled => 1 ,
1128+ }
1129+ }
1130+
1131+ fn prefer_plugin ( candidate : & Plugin , existing : & Plugin ) -> bool {
1132+ let candidate_priority = source_priority ( & candidate. source ) ;
1133+ let existing_priority = source_priority ( & existing. source ) ;
1134+ if candidate_priority != existing_priority {
1135+ return candidate_priority > existing_priority;
1136+ }
1137+
1138+ candidate. created_at > existing. created_at
1139+ }
1140+
10661141async fn read_plugins_dir ( dir : & PathBuf ) -> Result < Vec < String > > {
10671142 let mut result = read_dir ( dir) . await ?;
10681143 let mut dirs: Vec < String > = vec ! [ ] ;
0 commit comments