@@ -30,6 +30,10 @@ import {
3030 decodePluginMessage ,
3131} from './web-socket-protocol.js' ;
3232import { ActionDispatcher } from './action-dispatcher.js' ;
33+ import {
34+ loadActionSourcesAsync ,
35+ type ActionSource ,
36+ } from '../commands/framework/action-loader.js' ;
3337import {
3438 injectPluginAsync ,
3539 type InjectedPlugin ,
@@ -218,6 +222,8 @@ export class StudioBridgeServer {
218222 private _negotiatedCapabilities : Capability [ ] = [ 'execute' ] ;
219223 private _lastHeartbeatTimestamp : number | undefined ;
220224 private _actionDispatcher = new ActionDispatcher ( ) ;
225+ private _actionsReady = false ;
226+ private _actionSources : ActionSource [ ] | undefined ;
221227
222228 constructor ( options : StudioBridgeServerOptions = { } ) {
223229 this . _options = options ;
@@ -420,6 +426,10 @@ export class StudioBridgeServer {
420426 throw new Error ( 'Cannot execute: no connected client' ) ;
421427 }
422428
429+ // Sync action modules before first execute (state must still be 'ready'
430+ // because performActionAsync checks for it).
431+ await this . _ensureActionsAsync ( ) ;
432+
423433 this . _state = 'executing' ;
424434 this . _onPhase ?.( 'executing' ) ;
425435
@@ -483,6 +493,75 @@ export class StudioBridgeServer {
483493 this . _state = 'stopped' ;
484494 }
485495
496+ // -----------------------------------------------------------------------
497+ // Private: _ensureActionsAsync
498+ // -----------------------------------------------------------------------
499+
500+ /**
501+ * Ensure action modules (like `execute.luau`) are synced to the plugin
502+ * before first use. Uses `syncActions` to check which actions the plugin
503+ * is missing, then registers them via `registerAction`.
504+ *
505+ * Only works with v2 plugins. v1 plugins skip this step (they would need
506+ * actions baked in, which the current plugin architecture does not support).
507+ */
508+ private async _ensureActionsAsync ( ) : Promise < void > {
509+ if ( this . _actionsReady ) return ;
510+
511+ if ( this . _negotiatedProtocolVersion < 2 ) {
512+ this . _actionsReady = true ;
513+ return ;
514+ }
515+
516+ if ( ! this . _actionSources ) {
517+ this . _actionSources = await loadActionSourcesAsync ( ) ;
518+ OutputHelper . verbose (
519+ `[StudioBridge] Loaded ${ this . _actionSources . length } action source(s): ${ this . _actionSources . map ( ( a ) => a . name ) . join ( ', ' ) || '(none)' } ` ,
520+ ) ;
521+ }
522+
523+ if ( this . _actionSources . length === 0 ) {
524+ this . _actionsReady = true ;
525+ return ;
526+ }
527+
528+ const actions : Record < string , string > = { } ;
529+ for ( const action of this . _actionSources ) {
530+ actions [ action . name ] = action . hash ;
531+ }
532+
533+ OutputHelper . verbose ( '[StudioBridge] Syncing actions with plugin' ) ;
534+ const syncResult = await this . performActionAsync < PluginMessage > ( {
535+ type : 'syncActions' ,
536+ payload : { actions } ,
537+ } , 10_000 ) ;
538+
539+ if ( syncResult . type === 'syncActionsResult' ) {
540+ const needed = ( syncResult . payload as Record < string , unknown > ) . needed as string [ ] ;
541+ OutputHelper . verbose (
542+ `[StudioBridge] ${ needed . length } action(s) need registering${ needed . length > 0 ? ': ' + needed . join ( ', ' ) : '' } ` ,
543+ ) ;
544+
545+ for ( const actionName of needed ) {
546+ const action = this . _actionSources . find ( ( a ) => a . name === actionName ) ;
547+ if ( ! action ) continue ;
548+
549+ OutputHelper . verbose ( `[StudioBridge] Registering action: ${ actionName } ` ) ;
550+ await this . performActionAsync < PluginMessage > ( {
551+ type : 'registerAction' ,
552+ payload : {
553+ name : action . name ,
554+ source : action . source ,
555+ hash : action . hash ,
556+ } ,
557+ } , 10_000 ) ;
558+ }
559+ }
560+
561+ this . _actionsReady = true ;
562+ OutputHelper . verbose ( '[StudioBridge] Action sync complete' ) ;
563+ }
564+
486565 // -----------------------------------------------------------------------
487566 // Private: _injectPluginAsync
488567 // -----------------------------------------------------------------------
0 commit comments